Added countdown timer to loading page — progress bar with sweep ETA

This commit is contained in:
Virgílio Santos
2026-03-14 15:53:55 -03:00
parent 1933e063ae
commit 4c601ed142
3 changed files with 81 additions and 9 deletions

View File

@@ -22,15 +22,19 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--m
@keyframes fadein{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}} @keyframes fadein{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.logo-text{font-size:18px;font-weight:700;letter-spacing:0.2em;color:var(--accent)} .logo-text{font-size:18px;font-weight:700;letter-spacing:0.2em;color:var(--accent)}
.container{display:flex;flex-direction:column;align-items:center;gap:32px} .container{display:flex;flex-direction:column;align-items:center;gap:32px}
#bootLines{font-size:12px;color:var(--dim);text-align:left;line-height:2;min-width:340px} #bootLines{font-size:12px;color:var(--dim);text-align:left;line-height:2;min-width:340px}
#bootLines .line{opacity:0;animation:fadein 0.3s ease forwards} #bootLines .line{opacity:0;animation:fadein 0.3s ease forwards}
#bootLines .ok{color:var(--accent)} #bootLines .ok{color:var(--accent)}
#status{font-size:12px;color:var(--accent);letter-spacing:0.15em;margin-top:4px;min-height:20px} #status{font-size:12px;color:var(--accent);letter-spacing:0.15em;margin-top:4px;min-height:20px;display:flex;align-items:center;gap:8px}
#status .blink{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);box-shadow:0 0 8px var(--accent);animation:pulse-blink 1.5s ease-in-out infinite;vertical-align:middle;margin-right:8px} #status .blink{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);box-shadow:0 0 8px var(--accent);animation:pulse-blink 1.5s ease-in-out infinite;flex-shrink:0}
#countdown{margin-top:8px;font-size:11px;color:var(--dim);letter-spacing:0.1em;text-align:center;min-height:16px}
#countdown .eta{color:var(--text)}
#countdown .bar-wrap{width:340px;height:2px;background:rgba(100,240,200,0.08);margin-top:8px;overflow:hidden}
#countdown .bar-fill{height:100%;background:var(--accent);width:0%;transition:width 1s linear;box-shadow:0 0 6px var(--accent)}
</style> </style>
</head> </head>
<body> <body>
@@ -41,10 +45,20 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--m
<div id="bootLines"></div> <div id="bootLines"></div>
<div id="status"><span class="blink"></span><span id="statusText">COLLECTING DATA...</span></div> <div id="status">
<span class="blink"></span>
<span id="statusText">COLLECTING DATA...</span>
</div>
<div id="countdown">
<span id="etaText"></span>
<div class="bar-wrap"><div class="bar-fill" id="barFill"></div></div>
</div>
</div> </div>
<script> <script>
const SWEEP_ESTIMATE_S = 50; // estimated sweep duration in seconds
const lines = [ const lines = [
'CRUCIX INTELLIGENCE ENGINE v2.0.0', 'CRUCIX INTELLIGENCE ENGINE v2.0.0',
'INITIATING FIRST SWEEP...', 'INITIATING FIRST SWEEP...',
@@ -56,13 +70,11 @@ const lines = [
]; ];
const container = document.getElementById('bootLines'); const container = document.getElementById('bootLines');
lines.forEach((text, i) => { lines.forEach((text, i) => {
setTimeout(() => { setTimeout(() => {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'line'; div.className = 'line';
div.innerHTML = text; div.innerHTML = text;
div.style.animationDelay = '0ms';
container.appendChild(div); container.appendChild(div);
}, i * 220); }, i * 220);
}); });
@@ -81,20 +93,59 @@ setInterval(() => {
statusText.textContent = statusMessages[statusIdx]; statusText.textContent = statusMessages[statusIdx];
}, 4000); }, 4000);
// SSE — wait for sweep to complete, then redirect // === Countdown ===
const etaText = document.getElementById('etaText');
const barFill = document.getElementById('barFill');
let countdownInterval = null;
function startCountdown(elapsedSeconds) {
let remaining = Math.max(0, SWEEP_ESTIMATE_S - elapsedSeconds);
function tick() {
if (remaining <= 0) {
etaText.textContent = 'FINALIZING...';
barFill.style.width = '99%';
return;
}
const pct = Math.min(99, ((SWEEP_ESTIMATE_S - remaining) / SWEEP_ESTIMATE_S) * 100);
barFill.style.width = pct + '%';
etaText.innerHTML = `EST. READY IN <span class="eta">~${remaining}s</span>`;
remaining--;
}
tick();
countdownInterval = setInterval(tick, 1000);
}
// Fetch health to get elapsed time since sweep started
fetch('/api/health')
.then(r => r.json())
.then(h => {
let elapsed = 0;
if (h.sweepStartedAt) {
elapsed = Math.floor((Date.now() - new Date(h.sweepStartedAt).getTime()) / 1000);
}
startCountdown(elapsed);
})
.catch(() => startCountdown(0));
// === SSE — wait for sweep to complete, then redirect ===
const es = new EventSource('/events'); const es = new EventSource('/events');
es.onmessage = (e) => { es.onmessage = (e) => {
try { try {
const msg = JSON.parse(e.data); const msg = JSON.parse(e.data);
if (msg.type === 'update') { if (msg.type === 'update') {
es.close(); es.close();
clearInterval(countdownInterval);
barFill.style.transition = 'width 0.4s ease';
barFill.style.width = '100%';
etaText.textContent = '';
statusText.textContent = 'TERMINAL READY — LOADING DASHBOARD'; statusText.textContent = 'TERMINAL READY — LOADING DASHBOARD';
setTimeout(() => location.replace('/'), 800); setTimeout(() => location.replace('/'), 800);
} }
} catch {} } catch {}
}; };
es.onerror = () => { es.onerror = () => {
// Server went away — reload and retry
es.close(); es.close();
setTimeout(() => location.reload(), 3000); setTimeout(() => location.reload(), 3000);
}; };

18
scripts/clean.mjs Normal file
View File

@@ -0,0 +1,18 @@
import { rm, access } from 'fs/promises';
import { join } from 'path';
const targets = [
'runs/latest.json',
'runs/memory',
];
for (const target of targets) {
const full = join(process.cwd(), target);
try {
await access(full);
await rm(full, { recursive: true });
console.log(`removed: ${target}`);
} catch {
// not found — skip silently
}
}

View File

@@ -29,6 +29,7 @@ for (const dir of [RUNS_DIR, MEMORY_DIR, join(MEMORY_DIR, 'cold')]) {
// === State === // === State ===
let currentData = null; // Current synthesized dashboard data let currentData = null; // Current synthesized dashboard data
let lastSweepTime = null; // Timestamp of last sweep let lastSweepTime = null; // Timestamp of last sweep
let sweepStartedAt = null; // Timestamp when current/last sweep started
let sweepInProgress = false; let sweepInProgress = false;
const startTime = Date.now(); const startTime = Date.now();
const sseClients = new Set(); const sseClients = new Set();
@@ -255,6 +256,7 @@ app.get('/api/health', (req, res) => {
? new Date(new Date(lastSweepTime).getTime() + config.refreshIntervalMinutes * 60000).toISOString() ? new Date(new Date(lastSweepTime).getTime() + config.refreshIntervalMinutes * 60000).toISOString()
: null, : null,
sweepInProgress, sweepInProgress,
sweepStartedAt,
sourcesOk: currentData?.meta?.sourcesOk || 0, sourcesOk: currentData?.meta?.sourcesOk || 0,
sourcesFailed: currentData?.meta?.sourcesFailed || 0, sourcesFailed: currentData?.meta?.sourcesFailed || 0,
llmEnabled: !!config.llm.provider, llmEnabled: !!config.llm.provider,
@@ -292,7 +294,8 @@ async function runSweepCycle() {
} }
sweepInProgress = true; sweepInProgress = true;
broadcast({ type: 'sweep_start', timestamp: new Date().toISOString() }); sweepStartedAt = new Date().toISOString();
broadcast({ type: 'sweep_start', timestamp: sweepStartedAt });
console.log(`\n${'='.repeat(60)}`); console.log(`\n${'='.repeat(60)}`);
console.log(`[Crucix] Starting sweep at ${new Date().toLocaleTimeString()}`); console.log(`[Crucix] Starting sweep at ${new Date().toLocaleTimeString()}`);
console.log(`${'='.repeat(60)}`); console.log(`${'='.repeat(60)}`);