feat: add dead-man's switch overlay and console warning
- Poll /health every 5s; show full-screen overlay after 2 consecutive failures telling the user the console was closed - Auto-hide overlay when backend comes back online - Added to base.html (all main pages) and setup.html (first-run/settings) - setup.html: suppress false-positive overlay during intentional restart via awaitingRestart flag - setup.html: add amber warning banner that the console must stay open - .gitignore: block *_import.sql and *_export.csv to prevent future accidental commits of real supplier/pricing data Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +1,10 @@
|
|||||||
# QuoteForge
|
# QuoteForge
|
||||||
config.yaml
|
config.yaml
|
||||||
|
|
||||||
|
# Data exports and imports with real supplier/pricing data
|
||||||
|
*_import.sql
|
||||||
|
*_export.csv
|
||||||
|
test_export.csv
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
*.pem
|
*.pem
|
||||||
|
|||||||
@@ -50,6 +50,25 @@
|
|||||||
|
|
||||||
<div id="toast" class="fixed bottom-4 right-4 z-50"></div>
|
<div id="toast" class="fixed bottom-4 right-4 z-50"></div>
|
||||||
|
|
||||||
|
<!-- Dead-man's switch overlay: shown when backend process stops responding -->
|
||||||
|
<div id="backend-offline-overlay" class="hidden fixed inset-0 z-[9999] bg-black/80 flex items-center justify-center p-6">
|
||||||
|
<div class="bg-white rounded-xl shadow-2xl max-w-md w-full p-8 text-center">
|
||||||
|
<div class="text-red-500 mb-4">
|
||||||
|
<svg class="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-bold text-gray-900 mb-3">Приложение остановлено</h2>
|
||||||
|
<p class="text-gray-600 mb-4">Консольное окно QuoteForge было закрыто — без него программа не работает.</p>
|
||||||
|
<div class="bg-amber-50 border border-amber-200 rounded-lg p-3 mb-6 text-sm text-amber-800">
|
||||||
|
Запустите программу заново и нажмите «Обновить страницу».
|
||||||
|
</div>
|
||||||
|
<button onclick="window.location.reload()" class="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">
|
||||||
|
Обновить страницу
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Sync Info Modal -->
|
<!-- Sync Info Modal -->
|
||||||
<div id="sync-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden p-4">
|
<div id="sync-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden p-4">
|
||||||
<div class="bg-white rounded-lg shadow-xl max-w-lg w-full max-h-[90vh] flex flex-col">
|
<div class="bg-white rounded-lg shadow-xl max-w-lg w-full max-h-[90vh] flex flex-col">
|
||||||
@@ -493,6 +512,35 @@
|
|||||||
loadDBUser();
|
loadDBUser();
|
||||||
checkWritePermission();
|
checkWritePermission();
|
||||||
|
|
||||||
|
// Dead-man's switch: detect if the backend process has stopped
|
||||||
|
(function() {
|
||||||
|
const POLL_MS = 5000;
|
||||||
|
const FAIL_THRESHOLD = 2;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
async function checkBackend() {
|
||||||
|
try {
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
const timer = setTimeout(() => ctrl.abort(), 3000);
|
||||||
|
const resp = await fetch('/health', { signal: ctrl.signal });
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (resp.ok) {
|
||||||
|
failCount = 0;
|
||||||
|
document.getElementById('backend-offline-overlay').classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
if (failCount >= FAIL_THRESHOLD) {
|
||||||
|
document.getElementById('backend-offline-overlay').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(checkBackend, POLL_MS);
|
||||||
|
})();
|
||||||
|
|
||||||
// Load last sync time - removed since dropdown is gone
|
// Load last sync time - removed since dropdown is gone
|
||||||
// document.addEventListener('DOMContentLoaded', loadLastSyncTime);
|
// document.addEventListener('DOMContentLoaded', loadLastSyncTime);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,6 +16,13 @@
|
|||||||
<p class="text-gray-600 mt-2">Настройка подключения к базе данных</p>
|
<p class="text-gray-600 mt-2">Настройка подключения к базе данных</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-amber-50 border border-amber-300 rounded-md p-3 mb-4 flex items-start gap-2">
|
||||||
|
<svg class="w-5 h-5 text-amber-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/>
|
||||||
|
</svg>
|
||||||
|
<p class="text-sm text-amber-800"><span class="font-semibold">Важно:</span> не закрывайте консольное окно приложения — без него программа не работает.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form id="setup-form" class="space-y-4">
|
<form id="setup-form" class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Хост сервера</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Хост сервера</label>
|
||||||
@@ -85,7 +92,28 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Dead-man's switch overlay -->
|
||||||
|
<div id="backend-offline-overlay" class="hidden fixed inset-0 z-[9999] bg-black/80 flex items-center justify-center p-6">
|
||||||
|
<div class="bg-white rounded-xl shadow-2xl max-w-md w-full p-8 text-center">
|
||||||
|
<div class="text-red-500 mb-4">
|
||||||
|
<svg class="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-bold text-gray-900 mb-3">Приложение остановлено</h2>
|
||||||
|
<p class="text-gray-600 mb-4">Консольное окно QuoteForge было закрыто — без него программа не работает.</p>
|
||||||
|
<div class="bg-amber-50 border border-amber-200 rounded-lg p-3 mb-6 text-sm text-amber-800">
|
||||||
|
Запустите программу заново и нажмите «Обновить страницу».
|
||||||
|
</div>
|
||||||
|
<button onclick="window.location.reload()" class="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">
|
||||||
|
Обновить страницу
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
let awaitingRestart = false;
|
||||||
|
|
||||||
function showStatus(message, type) {
|
function showStatus(message, type) {
|
||||||
const status = document.getElementById('status');
|
const status = document.getElementById('status');
|
||||||
status.classList.remove('hidden', 'bg-green-100', 'text-green-800', 'bg-red-100', 'text-red-800', 'bg-blue-100', 'text-blue-800', 'bg-yellow-100', 'text-yellow-800');
|
status.classList.remove('hidden', 'bg-green-100', 'text-green-800', 'bg-red-100', 'text-red-800', 'bg-blue-100', 'text-blue-800', 'bg-yellow-100', 'text-yellow-800');
|
||||||
@@ -160,6 +188,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function requestRestartAndWait() {
|
async function requestRestartAndWait() {
|
||||||
|
awaitingRestart = true;
|
||||||
showStatus('Перезапуск приложения...', 'info');
|
showStatus('Перезапуск приложения...', 'info');
|
||||||
try {
|
try {
|
||||||
await fetch('/api/restart', { method: 'POST' });
|
await fetch('/api/restart', { method: 'POST' });
|
||||||
@@ -205,6 +234,35 @@
|
|||||||
showStatus('Ошибка сети: ' + e.message, 'error');
|
showStatus('Ошибка сети: ' + e.message, 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Dead-man's switch: detect if the backend process has stopped
|
||||||
|
(function() {
|
||||||
|
const POLL_MS = 5000;
|
||||||
|
const FAIL_THRESHOLD = 2;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
async function checkBackend() {
|
||||||
|
if (awaitingRestart) return;
|
||||||
|
try {
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
const timer = setTimeout(() => ctrl.abort(), 3000);
|
||||||
|
const resp = await fetch('/health', { signal: ctrl.signal });
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (resp.ok) {
|
||||||
|
failCount = 0;
|
||||||
|
document.getElementById('backend-offline-overlay').classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
if (failCount >= FAIL_THRESHOLD) {
|
||||||
|
document.getElementById('backend-offline-overlay').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(checkBackend, POLL_MS);
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user