Files
PriceForge/web/templates/base.html
2026-03-07 21:10:20 +03:00

171 lines
8.4 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-2">
<a href="/lot" class="{{if eq .ActivePage `lot`}}bg-orange-50 text-orange-700{{else}}text-gray-600 hover:text-gray-900 hover:bg-gray-50{{end}} px-3 py-2 text-sm rounded-md transition">LOT</a>
<a href="/vendor-mappings" class="{{if eq .ActivePage `vendor_mappings`}}bg-orange-50 text-orange-700{{else}}text-gray-600 hover:text-gray-900 hover:bg-gray-50{{end}} px-3 py-2 text-sm rounded-md transition">Vendor mappings</a>
<a href="/admin/pricing" class="{{if eq .ActivePage `admin`}}bg-orange-50 text-orange-700{{else}}text-gray-600 hover:text-gray-900 hover:bg-gray-50{{end}} px-3 py-2 text-sm rounded-md transition">Администратор цен</a>
<a href="/setup" class="{{if eq .ActivePage `setup`}}bg-orange-50 text-orange-700{{else}}text-gray-600 hover:text-gray-900 hover:bg-gray-50{{end}} px-3 py-2 text-sm rounded-md transition">Настройки</a>
</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="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);
}
}
}
document.addEventListener('DOMContentLoaded', function () {
refreshSystemStatus();
setInterval(refreshSystemStatus, 5000); // 5 second polling
});
</script>
</body>
</html>
{{end}}