Files
PriceForge/web/templates/base.html
Mikhail Chusavitin c939ce18ad Merge origin/main
2026-02-18 10:12:07 +03:00

305 lines
16 KiB
HTML
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.
{{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">&times;</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}}