feat: ssh auth, protocol management, and cleanup

This commit is contained in:
infosave2007
2026-01-23 17:55:40 +03:00
parent 4995147bad
commit ea82b78a7d
70 changed files with 16225 additions and 986 deletions
+343
View File
@@ -0,0 +1,343 @@
{% extends "layout.twig" %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="mb-8 flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900">{{ 'Логи приложения' | trans }}</h1>
<p class="mt-2 text-sm text-gray-600">{{ 'Просмотр, поиск и управление файлами логов' | trans }}</p>
</div>
{% if log_files | length > 0 %}
<div class="flex space-x-2">
<button class="px-4 py-2 bg-yellow-500 text-white rounded-md hover:bg-yellow-600" id="btnClearAll" title="{{ 'Удалить все логи' | trans }}">
<i class="fas fa-trash mr-2"></i>{{ 'Очистить все' | trans }}
</button>
<a href="/admin/logs/download?file={{ selected_file }}"
class="px-4 py-2 rounded-md border {{ selected_file ? 'bg-white text-gray-700 hover:bg-gray-50' : 'bg-gray-100 text-gray-400 cursor-not-allowed' }}">
<i class="fas fa-download mr-2"></i>{{ 'Скачать' | trans }}
</a>
</div>
{% endif %}
</div>
{% if user and user.role == 'admin' %}
<div class="mb-6 border-b border-gray-200">
<nav class="-mb-px flex space-x-8">
<a href="/settings" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-user mr-2"></i>{{ t('settings.profile') }}
</a>
<a href="/settings" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-key mr-2"></i>{{ t('settings.api_keys') }}
</a>
<a href="/settings" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-language mr-2"></i>{{ t('settings.translations') }}
</a>
<a href="/settings" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-users mr-2"></i>{{ t('settings.users') }}
</a>
<a href="/settings/ldap" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-network-wired mr-2"></i>LDAP
</a>
<a href="/settings/protocols" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-cubes mr-2"></i>Protocols
</a>
<a href="/admin/logs" class="tab-link border-purple-500 text-purple-600 py-4 px-1 border-b-2 font-medium text-sm">
<i class="fas fa-file-alt mr-2"></i>{{ 'Логи' | trans }}
</a>
</nav>
</div>
{% endif %}
<div class="grid grid-cols-12 gap-6">
<!-- Sidebar with file list -->
<div class="col-span-12 md:col-span-3">
<div class="bg-white shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">{{ 'Файлы логов' | trans }}</h2>
</div>
<div class="max-h-[600px] overflow-y-auto">
{% if log_files | length > 0 %}
{% for file in log_files %}
<a href="/admin/logs?file={{ file.path }}" class="block px-6 py-4 border-b border-gray-100 hover:bg-gray-50 {{ selected_file == file.path ? 'bg-purple-50' : '' }}">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="text-sm font-medium" style="word-break: break-all;">{{ file.name }}</div>
<div class="text-xs text-gray-500">{{ file.size_formatted }}</div>
</div>
<button type="button" class="text-red-600 hover:text-red-800 text-sm delete-log" data-file="{{ file.path }}" title="{{ 'Удалить' | trans }}">
<i class="fas fa-trash-alt"></i>
</button>
</div>
<div class="text-xs text-gray-500 mt-2">{{ file.modified_formatted }}</div>
</a>
{% endfor %}
{% else %}
<div class="px-6 py-10 text-center text-gray-500">
{{ 'Логи не найдены' | trans }}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Main content area -->
<div class="col-span-12 md:col-span-9">
{% if selected_file %}
<!-- File info -->
<div class="bg-white shadow rounded-lg mb-4">
<div class="px-6 py-4 border-b border-gray-200">
<div class="md:flex md:items-center md:justify-between">
<div class="mb-2 md:mb-0">
<h3 class="text-sm text-gray-700">{{ 'Файл:' | trans }} <code class="text-gray-900">{{ selected_file }}</code></h3>
</div>
<div class="text-xs text-gray-500">
<strong>{{ 'Размер:' | trans }}</strong> {{ file_size | default(0) | bytes_format }}
<span class="mx-2">•</span>
<strong>{{ 'Строк:' | trans }}</strong> {{ line_count | number_format(0, '.', ' ') }}
</div>
</div>
</div>
</div>
<!-- Search form -->
<div class="bg-white shadow rounded-lg mb-4">
<div class="px-6 py-4">
<form id="searchForm" class="md:flex md:items-center md:space-x-3">
<input type="hidden" name="file" value="{{ selected_file }}">
<div class="flex-1 mb-3 md:mb-0">
<input type="text" id="searchQuery" name="query" required
placeholder="{{ 'Поиск в логе...' | trans }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
</div>
<label class="inline-flex items-center mb-3 md:mb-0">
<input type="checkbox" id="caseSensitive" name="case_sensitive" class="rounded border-gray-300 text-purple-600 focus:ring-purple-500">
<span class="ml-2 text-sm text-gray-600">{{ 'Учитывать регистр' | trans }}</span>
</label>
<button type="submit" class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
<i class="fas fa-search mr-2"></i>{{ 'Найти' | trans }}
</button>
<button type="button" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200" id="statsBtn">
<i class="fas fa-chart-bar mr-2"></i>{{ 'Статистика' | trans }}
</button>
</form>
</div>
</div>
<!-- Search results -->
<div id="searchResults" class="hidden bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded mb-4">
<strong>{{ 'Результаты поиска:' | trans }}</strong>
<div id="resultsContent" class="mt-2 text-sm"></div>
</div>
<!-- Statistics -->
<div id="statsPanel" class="hidden bg-white shadow rounded-lg mb-4">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-sm font-medium text-gray-900">{{ 'Статистика логов' | trans }}</h3>
</div>
<div class="px-6 py-4">
<div class="grid grid-cols-4 gap-4">
<div class="text-center p-3 border-r">
<div class="text-2xl text-purple-600" id="totalLines">0</div>
<div class="text-xs text-gray-500">{{ 'Всего строк' | trans }}</div>
</div>
<div class="text-center p-3 border-r">
<div class="text-2xl text-red-600" id="errorCount">0</div>
<div class="text-xs text-gray-500">{{ 'Ошибок' | trans }}</div>
</div>
<div class="text-center p-3 border-r">
<div class="text-2xl text-yellow-600" id="warningCount">0</div>
<div class="text-xs text-gray-500">{{ 'Предупреждений' | trans }}</div>
</div>
<div class="text-center p-3">
<div class="text-2xl text-green-600" id="successCount">0</div>
<div class="text-xs text-gray-500">{{ 'Успехов' | trans }}</div>
</div>
</div>
<div class="text-xs text-gray-500 mt-2" id="lastModified"></div>
</div>
</div>
<!-- Log content -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-3 border-b border-gray-200 flex items-center justify-between">
<h3 class="text-sm font-medium text-gray-900">{{ 'Содержание логов' | trans }}</h3>
<button type="button" class="px-2 py-1 text-sm border rounded hover:bg-gray-50" id="toggleLineNumbers">
<i class="fas fa-list-ol"></i>
</button>
</div>
<div class="px-6 py-4">
<pre class="mb-0 max-h-[600px] overflow-auto"><code id="logContent" class="show-line-numbers">{{ log_content }}</code></pre>
</div>
</div>
{% else %}
<div class="bg-blue-50 border border-blue-200 text-blue-800 px-6 py-10 rounded text-center">
<i class="fas fa-info-circle text-2xl mb-3"></i>
<div class="text-lg">{{ 'Выберите файл логов для просмотра' | trans }}</div>
</div>
{% endif %}
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const selectedFile = '{{ selected_file }}';
// Delete log file
document.querySelectorAll('.delete-log').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const file = this.dataset.file;
if (confirm('{{ "Удалить этот файл логов?" | trans }}')) {
fetch('/admin/logs/delete', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'file=' + encodeURIComponent(file)
})
.then(r => r.json())
.then(data => {
if (data.success) {
window.location.href = data.redirect;
} else {
alert('{{ "Ошибка:" | trans }} ' + data.message);
}
});
}
});
});
// Clear all logs
const clearAllBtn = document.getElementById('btnClearAll');
if (clearAllBtn) {
clearAllBtn.addEventListener('click', function() {
if (confirm('{{ "Удалить ВСЕ файлы логов? Это действие необратимо." | trans }}')) {
fetch('/admin/logs/clear-all', {method: 'POST'})
.then(r => r.json())
.then(data => {
alert(data.message);
window.location.href = data.redirect;
});
}
});
}
// Search logs
document.getElementById('searchForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('/admin/logs/search', {
method: 'POST',
body: new URLSearchParams(formData)
})
.then(r => r.json())
.then(data => {
if (data.success) {
showSearchResults(data);
} else {
alert('{{ "Ошибка:" | trans }} ' + data.message);
}
});
});
// Statistics
document.getElementById('statsBtn').addEventListener('click', function() {
if (!selectedFile) return;
fetch('/admin/logs/stats', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'file=' + encodeURIComponent(selectedFile)
})
.then(r => r.json())
.then(data => {
if (data.success) {
showStatistics(data);
}
});
});
// Toggle line numbers
document.getElementById('toggleLineNumbers').addEventListener('click', function() {
const content = document.getElementById('logContent');
content.classList.toggle('show-line-numbers');
});
function showSearchResults(data) {
const resultsDiv = document.getElementById('searchResults');
const resultsContent = document.getElementById('resultsContent');
if (data.results_count === 0) {
resultsContent.innerHTML = '<p class="mb-0">{{ "Результатов не найдено" | trans }}</p>';
} else {
let html = '<p class="mb-2">{{ "Найдено совпадений:" | trans }} <strong>' + data.results_count + '</strong></p>';
html += '<div class="results-list" style="max-height: 300px; overflow-y: auto;">';
data.results.forEach((result, idx) => {
html += '<div class="border-bottom pb-2 mb-2">';
html += '<small class="text-muted">{{ "Строка" | trans }} ' + result.line + ':</small><br>';
html += '<code>' + escapeHtml(result.content.substring(0, 200)) + (result.content.length > 200 ? '...' : '') + '</code>';
html += '</div>';
});
html += '</div>';
resultsContent.innerHTML = html;
}
resultsDiv.style.display = 'block';
}
function showStatistics(data) {
document.getElementById('totalLines').textContent = data.total_lines;
document.getElementById('errorCount').textContent = data.errors;
document.getElementById('warningCount').textContent = data.warnings;
document.getElementById('successCount').textContent = data.success;
document.getElementById('lastModified').textContent = '{{ "Последнее обновление:" | trans }} ' + data.last_modified;
document.getElementById('statsPanel').style.display = 'block';
}
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
});
</script>
<style>
#logContent {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.4;
}
#logContent.show-line-numbers {
counter-reset: line;
}
#logContent.show-line-numbers::before {
content: '';
}
.results-list {
border: 1px solid #ddd;
border-radius: 3px;
padding: 10px;
background: #fafafa;
}
.list-group-item.active {
background-color: #007bff;
border-color: #007bff;
}
</style>
{% endblock %}