Files
amneziavpnphp/templates/settings/scenario_form.twig
T
2026-01-23 17:55:40 +03:00

251 lines
12 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "layout.twig" %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-10 offset-md-1">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
{% if scenario %}
{{ 'Редактирование сценария:' | trans }} {{ scenario.name }}
{% else %}
{{ 'Новый сценарий' | trans }}
{% endif %}
</h5>
</div>
<div class="card-body">
<form id="scenarioForm">
<input type="hidden" name="id" value="{{ scenario.id | default('') }}">
<div class="mb-3">
<label for="slug" class="form-label">{{ 'Уникальный идентификатор' | trans }} *</label>
<input type="text" class="form-control" id="slug" name="slug"
value="{{ scenario.slug | default('') }}" required
pattern="^[a-z0-9\-]+$" title="{{ 'Только строчные буквы, цифры и дефисы' | trans }}">
<small class="form-text text-muted">{{ 'например: xray-vless, openvpn-tls' | trans }}</small>
</div>
<div class="mb-3">
<label for="name" class="form-label">{{ 'Название протокола' | trans }} *</label>
<input type="text" class="form-control" id="name" name="name"
value="{{ scenario.name | default('') }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">{{ 'Описание' | trans }}</label>
<textarea class="form-control" id="description" name="description" rows="2">{{ scenario.description | default('') }}</textarea>
</div>
<div class="mb-3">
<label for="definition" class="form-label">{{ 'Определение сценария (JSON)' | trans }} *</label>
<textarea class="form-control font-monospace" id="definition" name="definition"
rows="20" required>{{ templateDefinition }}</textarea>
<small class="form-text text-muted d-block mt-2">
<strong>{{ 'Структура JSON:' | trans }}</strong><br>
<code>{ "engine": "shell|builtin_awg", "metadata": {...}, "scripts": { "detect": "...", "install": "...", "restore": "..." } }</code>
</small>
<small class="form-text text-muted d-block mt-2">
<strong>{{ 'Доступные переменные в скриптах:' | trans }}</strong><br>
<code>{{ "{{server.host}}, {{server.username}}, {{server.container_name}}, {{metadata.*}}" | trans }}</code>
</small>
<div id="jsonError" class="alert alert-danger mt-2" style="display: none;"></div>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="is_active" name="is_active" value="1"
{% if scenario.is_active ?? true %}checked{% endif %}>
<label class="form-check-label" for="is_active">
{{ 'Активный сценарий' | trans }}
</label>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-success">
<i class="fas fa-save"></i> {{ 'Сохранить' | trans }}
</button>
<a href="/admin/scenarios" class="btn btn-secondary">
{{ 'Отмена' | trans }}
</a>
{% if scenario %}
<button type="button" class="btn btn-info ms-auto" id="testBtn">
<i class="fas fa-flask"></i> {{ 'Тест на сервере' | trans }}
</button>
{% endif %}
</div>
</form>
</div>
</div>
<!-- JSON Validation Helper -->
<div class="card mt-3">
<div class="card-header bg-light">
<h6 class="mb-0">{{ 'Справка по формату' | trans }}</h6>
</div>
<div class="card-body">
<h6>{{ 'Поля сценария:' | trans }}</h6>
<ul>
<li><strong>engine:</strong> Тип движка ("shell" или "builtin_awg")</li>
<li><strong>metadata:</strong> Объект с параметрами протокола (container_name, config_path и т.д.)</li>
<li><strong>scripts:</strong> Объект со скриптами (detect, install, restore)</li>
</ul>
<h6 class="mt-3">{{ 'Поля скриптов:' | trans }}</h6>
<ul>
<li><strong>detect:</strong> Bash скрипт для определения установленной конфигурации. Должен вывести JSON с полями "status" (absent/partial/existing) и "details"</li>
<li><strong>install:</strong> Bash скрипт для установки протокола. Должен вывести JSON с "success": true/false</li>
<li><strong>restore:</strong> Bash скрипт для восстановления конфигурации из detection результата</li>
</ul>
<h6 class="mt-3">{{ 'Переменные окружения в скриптах:' | trans }}</h6>
<ul>
<li><code>SERVER_HOST</code> - IP/домен сервера</li>
<li><code>SERVER_USER</code> - SSH пользователь</li>
<li><code>SERVER_CONTAINER</code> - имя контейнера</li>
<li><code>PROTOCOL_*</code> - все поля из metadata (например, PROTOCOL_CONTAINER_NAME, PROTOCOL_CONFIG_PATH)</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Test Modal -->
<div class="modal fade" id="testModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ 'Тест сценария' | trans }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="testServer" class="form-label">{{ 'Выбрать сервер' | trans }}</label>
<select class="form-control" id="testServer">
<option value="">{{ 'Загружаю...' | trans }}</option>
</select>
</div>
<div id="testResult" class="mt-3" style="display: none;">
<strong>{{ 'Результат:' | trans }}</strong>
<pre id="testResultContent" class="bg-light p-3 rounded" style="max-height: 300px; overflow-y: auto;"></pre>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'Закрыть' | trans }}</button>
<button type="button" class="btn btn-primary" id="runTestBtn">{{ 'Запустить тест' | trans }}</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('scenarioForm');
const definitionTextarea = document.getElementById('definition');
const jsonError = document.getElementById('jsonError');
const testBtn = document.getElementById('testBtn');
// Validate JSON on change
definitionTextarea.addEventListener('change', validateJson);
definitionTextarea.addEventListener('blur', validateJson);
function validateJson() {
jsonError.style.display = 'none';
try {
JSON.parse(definitionTextarea.value);
} catch (e) {
jsonError.textContent = `{{ "Ошибка JSON:" | trans }} ${e.message}`;
jsonError.style.display = 'block';
}
}
// Form submission
form.addEventListener('submit', async function(e) {
e.preventDefault();
if (!validateJson()) return;
const formData = new FormData(this);
try {
const response = await fetch('/admin/scenario', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
alert('{{ "Сценарий успешно сохранен" | trans }}');
window.location.href = data.redirect;
} else {
alert(`{{ "Ошибка:" | trans }} ${data.message}`);
}
} catch (err) {
alert(`{{ "Ошибка отправки:" | trans }} ${err}`);
}
});
// Test button
if (testBtn) {
testBtn.addEventListener('click', function() {
loadServers();
new bootstrap.Modal(document.getElementById('testModal')).show();
});
}
// Load available servers
async function loadServers() {
try {
const response = await fetch('/api/servers?limit=50');
const data = await response.json();
const select = document.getElementById('testServer');
select.innerHTML = '';
if (data.servers && data.servers.length > 0) {
data.servers.forEach(server => {
const option = document.createElement('option');
option.value = server.id;
option.textContent = `${server.name} (${server.host})`;
select.appendChild(option);
});
} else {
select.innerHTML = '<option value="">{{ "Сервера не найдены" | trans }}</option>';
}
} catch (err) {
console.error('Error loading servers:', err);
}
}
// Run test
document.getElementById('runTestBtn').addEventListener('click', async function() {
const serverId = document.getElementById('testServer').value;
if (!serverId) {
alert('{{ "Выберите сервер" | trans }}');
return;
}
const scenarioId = document.querySelector('input[name="id"]').value;
try {
const response = await fetch(`/admin/scenario/${scenarioId}/test`, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `server_id=${serverId}`
});
const data = await response.json();
const resultDiv = document.getElementById('testResult');
const resultContent = document.getElementById('testResultContent');
resultContent.textContent = JSON.stringify(data.result, null, 2);
resultDiv.style.display = 'block';
} catch (err) {
alert(`{{ "Ошибка теста:" | trans }} ${err}`);
}
});
});
</script>
{% endblock %}