305 lines
16 KiB
HTML
305 lines
16 KiB
HTML
{{define "base"}}
|
||
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>{{template "title" .}}</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||
<style>
|
||
.htmx-request { opacity: 0.5; }
|
||
.line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||
</style>
|
||
</head>
|
||
<body class="bg-gray-100 min-h-screen">
|
||
<nav class="bg-white shadow-sm sticky top-0 z-40">
|
||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||
<div class="flex justify-between h-14">
|
||
<div class="flex items-center space-x-8">
|
||
<a href="/" class="text-xl font-bold text-orange-600">PriceForge</a>
|
||
<div class="hidden md:flex space-x-4">
|
||
<a href="/lot" class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm">LOT</a>
|
||
<a href="/admin/pricing" class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm">Администратор цен</a>
|
||
<button type="button" onclick="openConnectionSettingsModal()" class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm">Настройки</button>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center space-x-4">
|
||
<div id="task-indicator" class="hidden items-center gap-1 text-sm"></div>
|
||
<span id="db-connection-status" class="inline-flex items-center gap-1 text-xs text-gray-600">
|
||
<span class="inline-block w-2 h-2 rounded-full bg-gray-400"></span>
|
||
DB: checking
|
||
</span>
|
||
<span id="db-user" class="text-sm text-gray-600"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||
{{template "content" .}}
|
||
</main>
|
||
|
||
<div id="connection-settings-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50 px-4">
|
||
<div class="bg-white rounded-lg shadow-lg w-full max-w-md">
|
||
<div class="flex items-center justify-between px-5 py-4 border-b">
|
||
<h2 class="text-lg font-semibold text-gray-900">Параметры подключения</h2>
|
||
<button type="button" onclick="closeConnectionSettingsModal()" class="text-gray-400 hover:text-gray-700 text-xl leading-none">×</button>
|
||
</div>
|
||
<form id="connection-settings-form" class="p-5 space-y-3">
|
||
<div>
|
||
<label class="block text-sm text-gray-700 mb-1">Хост</label>
|
||
<input id="connection-host" name="host" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-700 mb-1">Порт</label>
|
||
<input id="connection-port" name="port" type="number" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-700 mb-1">База данных</label>
|
||
<input id="connection-database" name="database" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-700 mb-1">Пользователь</label>
|
||
<input id="connection-user" name="user" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm text-gray-700 mb-1">Пароль</label>
|
||
<input id="connection-password" name="password" type="password" autocomplete="new-password" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500">
|
||
<p class="text-xs text-gray-500 mt-1">Оставьте пустым, чтобы сохранить текущий пароль.</p>
|
||
</div>
|
||
<div id="connection-settings-status" class="hidden text-sm rounded-md px-3 py-2"></div>
|
||
<div class="flex items-center justify-end gap-2 pt-2">
|
||
<button type="button" onclick="testConnectionSettings()" class="px-3 py-2 border border-gray-300 rounded-md text-sm hover:bg-gray-50">Проверить</button>
|
||
<button type="submit" class="px-3 py-2 bg-orange-600 text-white rounded-md text-sm hover:bg-orange-700">Сохранить</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="toast" class="fixed bottom-4 right-4 z-50"></div>
|
||
|
||
<script>
|
||
function showToast(msg, type) {
|
||
const colors = { success: 'bg-green-500', error: 'bg-red-500', info: 'bg-orange-500' };
|
||
const el = document.getElementById('toast');
|
||
el.replaceChildren();
|
||
const toast = document.createElement('div');
|
||
toast.className = (colors[type] || colors.info) + ' text-white px-4 py-2 rounded shadow';
|
||
toast.textContent = String(msg || '');
|
||
el.appendChild(toast);
|
||
setTimeout(() => el.replaceChildren(), 3000);
|
||
}
|
||
|
||
function renderDBStatus(connected, errorText) {
|
||
const el = document.getElementById('db-connection-status');
|
||
if (!el) return;
|
||
if (connected) {
|
||
el.innerHTML = '<span class="inline-block w-2 h-2 rounded-full bg-green-500"></span>DB: online';
|
||
el.title = '';
|
||
return;
|
||
}
|
||
el.innerHTML = '<span class="inline-block w-2 h-2 rounded-full bg-red-500"></span>DB: offline';
|
||
el.title = errorText || 'Database not connected';
|
||
}
|
||
|
||
const knownTasks = new Map(); // taskID -> {status, notified}
|
||
|
||
async function refreshSystemStatus() {
|
||
try {
|
||
// Poll DB status
|
||
const dbResp = await fetch('/api/db-status');
|
||
const dbData = await dbResp.json();
|
||
renderDBStatus(dbData.connected === true, dbData.error || '');
|
||
|
||
const userEl = document.getElementById('db-user');
|
||
if (dbData.connected && dbData.db_user) {
|
||
userEl.textContent = '@' + String(dbData.db_user);
|
||
} else if (userEl) {
|
||
userEl.textContent = '';
|
||
}
|
||
|
||
// Poll tasks
|
||
const tasksResp = await fetch('/api/tasks');
|
||
const tasksData = await tasksResp.json();
|
||
updateTaskIndicator(tasksData.tasks || []);
|
||
} catch (e) {
|
||
renderDBStatus(false, 'Status request failed');
|
||
}
|
||
}
|
||
|
||
function updateTaskIndicator(tasks) {
|
||
const indicator = document.getElementById('task-indicator');
|
||
if (!indicator) return;
|
||
|
||
const runningTasks = tasks.filter(t => t.status === 'running');
|
||
|
||
if (runningTasks.length > 0) {
|
||
indicator.classList.remove('hidden');
|
||
indicator.classList.add('flex');
|
||
indicator.innerHTML = `
|
||
<svg class="animate-spin h-4 w-4 text-orange-600" fill="none" viewBox="0 0 24 24">
|
||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||
</svg>
|
||
<span class="text-xs">${runningTasks.length}</span>
|
||
`;
|
||
indicator.title = runningTasks.map(t => t.message).join(', ');
|
||
} else {
|
||
indicator.classList.add('hidden');
|
||
indicator.classList.remove('flex');
|
||
indicator.innerHTML = '';
|
||
}
|
||
|
||
// Check for newly completed/errored tasks
|
||
tasks.forEach(task => {
|
||
const known = knownTasks.get(task.id);
|
||
|
||
if (!known) {
|
||
// New task - check if it's recently completed (within last 10 seconds)
|
||
const isRecentlyCompleted = task.status !== 'running' && task.done_at;
|
||
if (isRecentlyCompleted) {
|
||
const doneAt = new Date(task.done_at);
|
||
const now = new Date();
|
||
const ageSeconds = (now - doneAt) / 1000;
|
||
|
||
// Show notification for tasks completed within the last 10 seconds
|
||
if (ageSeconds < 10) {
|
||
if (task.status === 'completed') {
|
||
showToast(task.message || 'Задача завершена', 'success');
|
||
} else if (task.status === 'error') {
|
||
showToast(task.error || 'Ошибка выполнения задачи', 'error');
|
||
}
|
||
knownTasks.set(task.id, {status: task.status, notified: true});
|
||
} else {
|
||
knownTasks.set(task.id, {status: task.status, notified: false});
|
||
}
|
||
} else {
|
||
knownTasks.set(task.id, {status: task.status, notified: false});
|
||
}
|
||
} else if (known.status === 'running' && task.status !== 'running' && !known.notified) {
|
||
// Task transitioned from running to completed/error
|
||
if (task.status === 'completed') {
|
||
showToast(task.message || 'Задача завершена', 'success');
|
||
} else if (task.status === 'error') {
|
||
showToast(task.error || 'Ошибка выполнения задачи', 'error');
|
||
}
|
||
knownTasks.set(task.id, {status: task.status, notified: true});
|
||
}
|
||
});
|
||
|
||
// Cleanup old tasks from knownTasks
|
||
for (const [taskID, info] of knownTasks.entries()) {
|
||
if (!tasks.find(t => t.id === taskID)) {
|
||
knownTasks.delete(taskID);
|
||
}
|
||
}
|
||
}
|
||
|
||
function closeConnectionSettingsModal() {
|
||
const modal = document.getElementById('connection-settings-modal');
|
||
if (!modal) return;
|
||
modal.classList.add('hidden');
|
||
modal.classList.remove('flex');
|
||
}
|
||
|
||
function showConnectionSettingsStatus(message, type) {
|
||
const el = document.getElementById('connection-settings-status');
|
||
if (!el) return;
|
||
el.classList.remove('hidden', 'bg-green-100', 'text-green-800', 'bg-red-100', 'text-red-800', 'bg-orange-100', 'text-orange-800');
|
||
if (type === 'success') {
|
||
el.classList.add('bg-green-100', 'text-green-800');
|
||
} else if (type === 'error') {
|
||
el.classList.add('bg-red-100', 'text-red-800');
|
||
} else {
|
||
el.classList.add('bg-orange-100', 'text-orange-800');
|
||
}
|
||
el.textContent = message;
|
||
}
|
||
|
||
async function openConnectionSettingsModal() {
|
||
const modal = document.getElementById('connection-settings-modal');
|
||
if (!modal) return;
|
||
modal.classList.remove('hidden');
|
||
modal.classList.add('flex');
|
||
showConnectionSettingsStatus('Загрузка текущих параметров...', 'info');
|
||
|
||
try {
|
||
const resp = await fetch('/api/connection-settings');
|
||
const data = await resp.json();
|
||
document.getElementById('connection-host').value = data.host || '';
|
||
document.getElementById('connection-port').value = data.port || 3306;
|
||
document.getElementById('connection-database').value = data.database || '';
|
||
document.getElementById('connection-user').value = data.user || '';
|
||
document.getElementById('connection-password').value = '';
|
||
document.getElementById('connection-settings-status').classList.add('hidden');
|
||
} catch (e) {
|
||
showConnectionSettingsStatus('Не удалось загрузить параметры: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
async function testConnectionSettings() {
|
||
const form = document.getElementById('connection-settings-form');
|
||
if (!form) return;
|
||
showConnectionSettingsStatus('Проверка подключения...', 'info');
|
||
try {
|
||
const resp = await fetch('/api/connection-settings/test', {
|
||
method: 'POST',
|
||
body: new FormData(form),
|
||
});
|
||
const data = await resp.json();
|
||
if (data.success) {
|
||
showConnectionSettingsStatus('Подключение успешно.', 'success');
|
||
} else {
|
||
showConnectionSettingsStatus(data.error || 'Ошибка проверки подключения.', 'error');
|
||
}
|
||
} catch (e) {
|
||
showConnectionSettingsStatus('Ошибка сети: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
const form = document.getElementById('connection-settings-form');
|
||
if (form) {
|
||
form.addEventListener('submit', async function (e) {
|
||
e.preventDefault();
|
||
showConnectionSettingsStatus('Сохранение параметров...', 'info');
|
||
try {
|
||
const resp = await fetch('/api/connection-settings', {
|
||
method: 'POST',
|
||
body: new FormData(form),
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok || !data.success) {
|
||
showConnectionSettingsStatus(data.error || 'Ошибка сохранения параметров.', 'error');
|
||
return;
|
||
}
|
||
|
||
showConnectionSettingsStatus('Параметры сохранены. Выполняется перезапуск...', 'success');
|
||
setTimeout(async function () {
|
||
try {
|
||
await fetch('/api/restart', { method: 'POST' });
|
||
} catch (_) {
|
||
}
|
||
}, 300);
|
||
} catch (e) {
|
||
showConnectionSettingsStatus('Ошибка сети: ' + e.message, 'error');
|
||
}
|
||
});
|
||
}
|
||
|
||
const modal = document.getElementById('connection-settings-modal');
|
||
if (modal) {
|
||
modal.addEventListener('click', function (e) {
|
||
if (e.target === modal) closeConnectionSettingsModal();
|
||
});
|
||
}
|
||
|
||
refreshSystemStatus();
|
||
setInterval(refreshSystemStatus, 5000); // 5 second polling
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
{{end}}
|