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
+156
View File
@@ -0,0 +1,156 @@
{% 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">
<h1>{{ 'Сценарии установки протоколов' | trans }}</h1>
<div class="btn-group">
<a href="/admin/scenario/create" class="btn btn-primary">
<i class="fas fa-plus"></i> {{ 'Новый сценарий' | trans }}
</a>
<button class="btn btn-success" id="btnImport">
<i class="fas fa-upload"></i> {{ 'Импорт' | trans }}
</button>
</div>
</div>
{% if scenarios | length > 0 %}
<div class="table-responsive">
<table class="table table-hover table-sm">
<thead class="table-light">
<tr>
<th>{{ 'Протокол' | trans }}</th>
<th>{{ 'Описание' | trans }}</th>
<th>{{ 'Движок' | trans }}</th>
<th>{{ 'Статус' | trans }}</th>
<th>{{ 'Действия' | trans }}</th>
</tr>
</thead>
<tbody>
{% for scenario in scenarios %}
<tr>
<td>
<strong>{{ scenario.name }}</strong>
<br>
<small class="text-muted">{{ scenario.slug }}</small>
</td>
<td>{{ scenario.description | default('-') }}</td>
<td>
<span class="badge bg-info">
{{ scenario.definition.engine | default('unknown') }}
</span>
</td>
<td>
{% if scenario.is_active %}
<span class="badge bg-success">{{ 'Активный' | trans }}</span>
{% else %}
<span class="badge bg-secondary">{{ 'Отключен' | trans }}</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="/admin/scenario/{{ scenario.id }}" class="btn btn-info" title="{{ 'Просмотр' | trans }}">
<i class="fas fa-eye"></i>
</a>
<a href="/admin/scenario/{{ scenario.id }}/edit" class="btn btn-warning" title="{{ 'Редактировать' | trans }}">
<i class="fas fa-edit"></i>
</a>
<a href="/admin/scenario/{{ scenario.id }}/export" class="btn btn-secondary" title="{{ 'Экспорт' | trans }}">
<i class="fas fa-download"></i>
</a>
{% if scenario.slug != 'amnezia-wg' %}
<button class="btn btn-danger delete-scenario" data-id="{{ scenario.id }}" data-name="{{ scenario.name }}" title="{{ 'Удалить' | trans }}">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info" role="alert">
{{ 'Нет доступных сценариев' | trans }}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Import Modal -->
<div class="modal fade" id="importModal" 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>
<form id="importForm" enctype="multipart/form-data">
<div class="modal-body">
<div class="mb-3">
<label for="importFile" class="form-label">{{ 'JSON файл сценария' | trans }}</label>
<input type="file" class="form-control" id="importFile" name="file" accept=".json" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'Отмена' | trans }}</button>
<button type="submit" class="btn btn-primary">{{ 'Импортировать' | trans }}</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Import button handler
document.getElementById('btnImport').addEventListener('click', function() {
new bootstrap.Modal(document.getElementById('importModal')).show();
});
// Import form handler
document.getElementById('importForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('/admin/scenario/import', {
method: 'POST',
body: formData
})
.then(r => r.json())
.then(data => {
if (data.success) {
window.location.href = data.redirect;
} else {
alert('{{ "Ошибка импорта:" | trans }} ' + data.message);
}
})
.catch(err => alert('{{ "Ошибка:" | trans }} ' + err));
});
// Delete buttons
document.querySelectorAll('.delete-scenario').forEach(btn => {
btn.addEventListener('click', function() {
const id = this.dataset.id;
const name = this.dataset.name;
if (confirm(`{{ "Удалить сценарий" | trans }}: ${name}?`)) {
fetch(`/admin/scenario/${id}/delete`, {method: 'POST'})
.then(r => r.json())
.then(data => {
if (data.success) {
window.location.href = data.redirect;
} else {
alert('{{ "Ошибка удаления:" | trans }} ' + data.message);
}
});
}
});
});
});
</script>
{% endblock %}