Added countdown timer to loading page — progress bar with sweep ETA
This commit is contained in:
@@ -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
18
scripts/clean.mjs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user