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

260 lines
12 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 "title"}}Настройки - PriceForge{{end}}
{{define "content"}}
<div class="space-y-6">
<div class="bg-white rounded-lg shadow p-6">
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Настройки</h1>
<p class="text-sm text-gray-500 mt-1">Scheduler и подключение к MariaDB.</p>
</div>
<div id="status" class="hidden md:max-w-md p-3 rounded-md text-sm"></div>
</div>
<div id="status-mobile" class="hidden mt-4 p-3 rounded-md text-sm md:hidden"></div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="text-xl font-semibold text-gray-900">Scheduler</h2>
<p class="text-sm text-gray-500 mt-1">Встроенные периодические задачи с координацией через DB lock.</p>
</div>
<button type="button" onclick="loadSchedulerRuns()" class="px-3 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition">Обновить</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Job</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Started</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Finished</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Next run</th>
<th class="px-4 py-2 text-center text-xs font-medium text-gray-500 uppercase">Status</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Error</th>
</tr>
</thead>
<tbody id="scheduler-runs-list" class="bg-white divide-y divide-gray-200">
<tr><td colspan="6" class="px-4 py-3 text-sm text-gray-500">Загрузка...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="mb-4">
<h2 class="text-xl font-semibold text-gray-900">Подключение к БД</h2>
<p class="text-sm text-gray-500 mt-1">Компактная форма настройки MariaDB. Изменения сохраняются в конфиг и требуют restart.</p>
</div>
<form id="setup-form" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-5 gap-3">
<div class="md:col-span-1">
<label class="block text-xs font-medium text-gray-500 uppercase mb-1">Host</label>
<input type="text" name="host" id="host"
value="{{.Settings.Host}}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500"
placeholder="localhost">
</div>
<div class="md:col-span-1">
<label class="block text-xs font-medium text-gray-500 uppercase mb-1">Port</label>
<input type="number" name="port" id="port"
value="{{.Settings.Port}}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500"
placeholder="3306">
</div>
<div class="md:col-span-1">
<label class="block text-xs font-medium text-gray-500 uppercase mb-1">Database</label>
<input type="text" name="database" id="database"
value="{{.Settings.Database}}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500"
placeholder="RFQ_LOG">
</div>
<div class="md:col-span-1">
<label class="block text-xs font-medium text-gray-500 uppercase mb-1">User</label>
<input type="text" name="user" id="user"
value="{{.Settings.User}}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500"
placeholder="username">
</div>
<div class="md:col-span-1">
<label class="block text-xs font-medium text-gray-500 uppercase mb-1">Password</label>
<input type="password" name="password" id="password"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500"
placeholder="Оставьте пустым, чтобы не менять">
</div>
</div>
<div class="flex items-center justify-end gap-3 pt-1">
<button type="button" onclick="testConnection()" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition">
Проверить
</button>
<button type="submit" class="px-4 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 transition">
Сохранить
</button>
</div>
</form>
</div>
</div>
<script>
function showStatus(message, type) {
const desktop = document.getElementById('status');
const mobile = document.getElementById('status-mobile');
[desktop, mobile].forEach(status => {
if (!status) return;
status.classList.remove('hidden', 'bg-green-100', 'text-green-800', 'bg-red-100', 'text-red-800', 'bg-orange-100', 'text-orange-800', 'bg-yellow-100', 'text-yellow-800');
if (type === 'success') {
status.classList.add('bg-green-100', 'text-green-800');
} else if (type === 'error') {
status.classList.add('bg-red-100', 'text-red-800');
} else if (type === 'warning') {
status.classList.add('bg-yellow-100', 'text-yellow-800');
} else {
status.classList.add('bg-orange-100', 'text-orange-800');
}
status.textContent = message;
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text || '';
return div.innerHTML;
}
function formatSchedulerDate(value) {
if (!value) return '—';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '—';
return date.toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' });
}
function renderSchedulerStatus(status) {
const value = String(status || 'idle').trim().toLowerCase();
if (value === 'success') return '<span class="px-2 py-0.5 bg-green-100 text-green-800 rounded text-xs font-medium">success</span>';
if (value === 'failed') return '<span class="px-2 py-0.5 bg-red-100 text-red-700 rounded text-xs font-medium">failed</span>';
if (value === 'running') return '<span class="px-2 py-0.5 bg-orange-100 text-orange-800 rounded text-xs font-medium">running</span>';
return '<span class="px-2 py-0.5 bg-gray-100 text-gray-600 rounded text-xs">idle</span>';
}
async function loadSchedulerRuns() {
const tbody = document.getElementById('scheduler-runs-list');
if (!tbody) return;
try {
const resp = await fetch('/api/admin/pricing/scheduler-runs');
const data = await resp.json();
if (!resp.ok) throw new Error(data.error || 'Ошибка загрузки');
const runs = Array.isArray(data.runs) ? data.runs : [];
if (runs.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="px-4 py-3 text-sm text-gray-500">Scheduler state is empty</td></tr>';
return;
}
tbody.innerHTML = runs.map(run => {
const lastError = String(run.last_error || '').trim();
return `<tr class="hover:bg-gray-50">
<td class="px-4 py-2 font-mono text-xs">${String(run.job_name || '')}</td>
<td class="px-4 py-2 text-sm text-gray-600">${formatSchedulerDate(run.last_started_at)}</td>
<td class="px-4 py-2 text-sm text-gray-600">${formatSchedulerDate(run.last_finished_at)}</td>
<td class="px-4 py-2 text-sm text-gray-600">${formatSchedulerDate(run.next_run_at)}</td>
<td class="px-4 py-2 text-center">${renderSchedulerStatus(run.last_status)}</td>
<td class="px-4 py-2 text-sm ${lastError ? 'text-red-700' : 'text-gray-400'}">${escapeHtml(lastError || '—')}</td>
</tr>`;
}).join('');
} catch (e) {
tbody.innerHTML = `<tr><td colspan="6" class="px-4 py-3 text-sm text-red-600">Ошибка загрузки: ${escapeHtml(e.message)}</td></tr>`;
}
}
async function testConnection() {
showStatus('Проверка подключения...', 'info');
const formData = new FormData(document.getElementById('setup-form'));
try {
const resp = await fetch('/setup/test', {
method: 'POST',
body: formData
});
const data = await resp.json();
if (data.success) {
let msg = data.message;
if (data.can_write) {
msg += ' Права на запись: есть.';
} else {
msg += ' Права на запись: только чтение.';
}
showStatus(msg, 'success');
} else {
showStatus(data.error, 'error');
}
} catch (e) {
showStatus('Ошибка сети: ' + e.message, 'error');
}
}
async function checkServerReady() {
let attempts = 0;
const maxAttempts = 30;
const checkInterval = setInterval(async () => {
attempts++;
try {
const resp = await fetch('/health', { method: 'GET' });
const data = await resp.json();
if (data.status === 'ok') {
clearInterval(checkInterval);
showStatus('Приложение запущено. Перенаправление...', 'success');
setTimeout(() => {
window.location.href = '/setup';
}, 800);
}
} catch (e) {
if (attempts >= maxAttempts) {
clearInterval(checkInterval);
showStatus('Сервер не отвечает. Обновите страницу вручную.', 'error');
}
}
}, 1000);
}
async function requestRestartAndWait() {
showStatus('Перезапуск приложения...', 'info');
try {
await fetch('/api/restart', { method: 'POST' });
} catch (e) {
}
checkServerReady();
}
document.getElementById('setup-form').addEventListener('submit', async function(e) {
e.preventDefault();
showStatus('Сохранение настроек...', 'info');
const formData = new FormData(this);
try {
const resp = await fetch('/setup', {
method: 'POST',
body: formData
});
const data = await resp.json();
if (data.success) {
showStatus(data.message || 'Настройки сохранены.', 'success');
if (data.restart_required) {
setTimeout(() => {
requestRestartAndWait();
}, 1200);
}
} else {
showStatus(data.error, 'error');
}
} catch (e) {
showStatus('Ошибка сети: ' + e.message, 'error');
}
});
loadSchedulerRuns();
</script>
{{end}}
{{template "base" .}}