feat: ssh auth, protocol management, and cleanup

This commit is contained in:
infosave2007
2026-01-23 17:55:40 +03:00
parent 60fc55fd47
commit bbab877eac
70 changed files with 16225 additions and 986 deletions
+246
View File
@@ -0,0 +1,246 @@
{% extends "layout.twig" %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1>{{ scenario.name }}</h1>
<small class="text-muted">{{ scenario.slug }}</small>
</div>
<div class="btn-group">
<a href="/admin/scenario/{{ scenario.id }}/edit" class="btn btn-primary">
<i class="fas fa-edit"></i> {{ 'Редактировать' | trans }}
</a>
<a href="/admin/scenario/{{ scenario.id }}/export" class="btn btn-secondary">
<i class="fas fa-download"></i> {{ 'Экспорт' | trans }}
</a>
<a href="/admin/scenarios" class="btn btn-light">
<i class="fas fa-arrow-left"></i> {{ 'Назад' | trans }}
</a>
</div>
</div>
<!-- Scenario Info -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">{{ 'Информация о сценарии' | trans }}</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<dl>
<dt>{{ 'Статус' | trans }}</dt>
<dd>
{% if scenario.is_active %}
<span class="badge bg-success">{{ 'Активный' | trans }}</span>
{% else %}
<span class="badge bg-secondary">{{ 'Отключен' | trans }}</span>
{% endif %}
</dd>
<dt>{{ 'Движок' | trans }}</dt>
<dd><code>{{ definition.engine | default('unknown') }}</code></dd>
<dt>{{ 'Описание' | trans }}</dt>
<dd>{{ scenario.description | default('—') }}</dd>
</dl>
</div>
<div class="col-md-6">
<dl>
<dt>{{ 'Контейнер' | trans }}</dt>
<dd><code>{{ definition.metadata.container_name | default('—') }}</code></dd>
<dt>{{ 'Путь конфигурации' | trans }}</dt>
<dd><code>{{ definition.metadata.config_path | default('—') }}</code></dd>
<dt>{{ 'Порт по умолчанию' | trans }}</dt>
<dd>{{ definition.metadata.default_port | default('—') }}</dd>
</dl>
</div>
</div>
</div>
</div>
<!-- Scripts Info -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">{{ 'Скрипты' | trans }}</h5>
</div>
<div class="card-body">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="detect-tab" data-bs-toggle="tab" data-bs-target="#detect-content" type="button" role="tab">
{{ 'Обнаружение (detect)' | trans }}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="install-tab" data-bs-toggle="tab" data-bs-target="#install-content" type="button" role="tab">
{{ 'Установка (install)' | trans }}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="restore-tab" data-bs-toggle="tab" data-bs-target="#restore-content" type="button" role="tab">
{{ 'Восстановление (restore)' | trans }}
</button>
</li>
</ul>
<div class="tab-content mt-3">
<div class="tab-pane fade show active" id="detect-content" role="tabpanel">
<pre class="bg-light p-3 rounded"><code>{{ definition.scripts.detect | default('—') }}</code></pre>
</div>
<div class="tab-pane fade" id="install-content" role="tabpanel">
<pre class="bg-light p-3 rounded"><code>{{ definition.scripts.install | default('—') }}</code></pre>
</div>
<div class="tab-pane fade" id="restore-content" role="tabpanel">
<pre class="bg-light p-3 rounded"><code>{{ definition.scripts.restore | default('—') }}</code></pre>
</div>
</div>
</div>
</div>
<!-- Metadata -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">{{ 'Метаданные' | trans }}</h5>
</div>
<div class="card-body">
<pre class="bg-light p-3 rounded"><code>{{ definition.metadata | json_encode(constant('JSON_PRETTY_PRINT') | constant('JSON_UNESCAPED_SLASHES')) }}</code></pre>
</div>
</div>
<!-- Actions -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">{{ 'Действия' | trans }}</h5>
</div>
<div class="card-body">
<button class="btn btn-info" id="testScenarioBtn">
<i class="fas fa-flask"></i> {{ 'Тест на сервере' | trans }}
</button>
{% if scenario.slug != 'amnezia-wg' %}
<button class="btn btn-danger" id="deleteScenarioBtn">
<i class="fas fa-trash"></i> {{ 'Удалить сценарий' | trans }}
</button>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Test Modal -->
<div class="modal fade" id="testModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ 'Тест сценария' | trans }} - {{ scenario.name }}</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: 400px; overflow-y: auto;"></pre>
</div>
<div id="testError" class="alert alert-danger mt-3" style="display: none;"></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 scenarioId = {{ scenario.id }};
// Test button
document.getElementById('testScenarioBtn').addEventListener('click', function() {
loadServers();
new bootstrap.Modal(document.getElementById('testModal')).show();
});
// Delete button
const deleteBtn = document.getElementById('deleteScenarioBtn');
if (deleteBtn) {
deleteBtn.addEventListener('click', function() {
if (confirm('{{ "Вы уверены? Это действие нельзя отменить." | trans }}')) {
fetch(`/admin/scenario/${scenarioId}/delete`, {method: 'POST'})
.then(r => r.json())
.then(data => {
if (data.success) {
window.location.href = data.redirect;
} else {
alert(`{{ "Ошибка:" | trans }} ${data.message}`);
}
});
}
});
}
// Load 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);
});
}
} 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;
}
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');
const errorDiv = document.getElementById('testError');
errorDiv.style.display = 'none';
if (data.success) {
resultContent.textContent = JSON.stringify(data.result, null, 2);
resultDiv.style.display = 'block';
} else {
errorDiv.textContent = data.message;
errorDiv.style.display = 'block';
}
} catch (err) {
const errorDiv = document.getElementById('testError');
errorDiv.textContent = `{{ "Ошибка:" | trans }} ${err}`;
errorDiv.style.display = 'block';
}
});
});
</script>
{% endblock %}