From 158a9e4baeb6a637c50b8a7263f7a3cfa6ee401a Mon Sep 17 00:00:00 2001 From: Zaine Arch Date: Fri, 20 Mar 2026 23:53:21 +0000 Subject: [PATCH] allowing logs to be read --- index.html | 146 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 96 insertions(+), 50 deletions(-) diff --git a/index.html b/index.html index e61c8a6..850a8fe 100644 --- a/index.html +++ b/index.html @@ -198,7 +198,6 @@ .btn-row { display: flex; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 0.8rem; } - /* kill button — shown only when running */ .kill-btn { display: none; } .kill-btn.visible { display: inline-flex; } @@ -214,13 +213,40 @@ /* ── LOG BOX ── */ .log-box { - background: #030810; border: 1px solid var(--border); color: #4ade80; - padding: 1rem; height: 200px; overflow-y: auto; border-radius: 6px; - font-size: 0.73rem; line-height: 1.6; font-family: 'Space Mono', monospace; margin-top: 0.5rem; + background: #030810; + border: 1px solid var(--border); + padding: 1rem; + height: 240px; + overflow-y: auto; + border-radius: 6px; + font-size: 0.73rem; + line-height: 1.6; + font-family: 'Space Mono', monospace; + margin-top: 0.5rem; + /* white-space keeps indentation; word-break prevents horizontal scroll */ + white-space: pre-wrap; + word-break: break-all; + /* default text colour — ANSI spans override this */ + color: #8ba0b8; } .log-box::-webkit-scrollbar { width: 4px; } .log-box::-webkit-scrollbar-thumb { background: var(--border-hi); border-radius: 2px; } + /* Each line is a block-level span so they stack vertically */ + .log-line { display: block; } + + /* ── ANSI colour classes (set by BuildController.ansiToHtml) ── */ + .ansi-bold { font-weight: 700; } + .ansi-dim { opacity: 0.55; } + .ansi-red { color: #f14c4c; } + .ansi-green { color: #4ec994; } + .ansi-yellow { color: #e5c07b; } + .ansi-blue { color: #61afef; } + .ansi-magenta { color: #c678dd; } + .ansi-cyan { color: #56b6c2; } + .ansi-white { color: #d4d4d4; } + .ansi-reset { color: inherit; font-weight: inherit; opacity: inherit; } + .hidden { display: none !important; } /* ── UPTIME DISPLAY ── */ @@ -237,7 +263,7 @@ .ping-history { display: flex; gap: 3px; margin-top: 0.8rem; align-items: flex-end; height: 32px; } .ping-bar { width: 6px; background: var(--accent3); border-radius: 1px; opacity: 0.7; transition: height 0.4s ease, background 0.3s; flex-shrink: 0; } - /* ── COMBINED PROGRESS STEPS ── */ + /* ── COMBINED PIPELINE STEPS ── */ .pipeline-steps { display: flex; align-items: center; gap: 0; margin: 1rem 0 0.5rem; } .pipeline-step { display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: var(--muted); padding: 0.4rem 0.8rem; border: 1px solid var(--border); border-radius: 4px; transition: all 0.3s; } .pipeline-step.active { color: var(--warn); border-color: rgba(245,158,11,0.4); background: rgba(245,158,11,0.07); } @@ -318,7 +344,7 @@ All Systems Running Normally - +
@@ -381,15 +407,15 @@
🚀  Quick Links
@@ -401,15 +427,12 @@
📡  Server Status
Checking - -
Days up
Hours
Minutes
Started at
-
Ping History (30 checks)
@@ -432,7 +455,8 @@

💤 Waiting for action

- + +
@@ -446,18 +470,15 @@ - - -

💤 Waiting for action

- + @@ -534,7 +555,6 @@ function showConfirm(title, body){ /* ── PING HISTORY ── */ const PING_MAX = 30; let pingResults = []; - function addPingResult(ok){ pingResults.push(ok); if(pingResults.length > PING_MAX) pingResults.shift(); @@ -560,7 +580,7 @@ async function fetchUptime(){ document.getElementById('uptimeMins').textContent = m; document.getElementById('uptimeStart').textContent = new Date(data.serverStart).toLocaleTimeString('en-GB',{hour:'2-digit',minute:'2-digit'}); document.getElementById('uptimeStat').textContent = `${d}d ${h}h ${m}m`; - } catch { /* server offline, health check handles UI */ } + } catch { /* server offline */ } } /* ── LAST RUN META ── */ @@ -572,7 +592,6 @@ async function fetchLastRuns(){ renderLastRun('roamLastRunMeta', data.roam); } catch {} } - function renderLastRun(elId, info){ const el = document.getElementById(elId); if(!el) return; @@ -621,9 +640,53 @@ document.querySelectorAll('.toggleLogs').forEach(btn=>{ }); }); -/* ── STREAM HELPERS ── */ +/* ── LOG STREAMING ───────────────────────────────────────────────────────── + The server sends two named SSE event types: + "log" – one line already converted from ANSI → HTML elements + "done" – build finished; close the EventSource + + Each line is appended as a block-level inside a div.log-box so + that innerHTML renders the colour spans correctly. The old approach used + pre.textContent += line which (a) stripped the spans and (b) couldn't + show colours, and (c) used the default onmessage which only fires for + unnamed events — it would never have received our named "log" events. + ────────────────────────────────────────────────────────────────────────── */ let emacsES=null, webES=null, roamES=null, combinedES=null; -function startLogStream(url,el,setter){ const s=new EventSource(url); s.onmessage=e=>{el.textContent+=e.data+'\n';el.scrollTop=el.scrollHeight;}; s.onerror=()=>s.close(); setter(s); } + +function startLogStream(url, container, setter) { + // Clear previous content + container.innerHTML = ''; + + const es = new EventSource(url); + + // Named "log" event — server sends one line of ANSI→HTML converted output + es.addEventListener('log', e => { + const line = document.createElement('span'); + line.className = 'log-line'; + line.innerHTML = e.data; // safe: server HTML-escaped raw text first + container.appendChild(line); + container.scrollTop = container.scrollHeight; + }); + + // Named "done" event — stream has ended, close cleanly + es.addEventListener('done', () => { + es.close(); + setter(null); + }); + + // Network error / server closed without "done" + es.onerror = () => { + es.close(); + setter(null); + const line = document.createElement('span'); + line.className = 'log-line ansi-dim'; + line.textContent = '— stream closed —'; + container.appendChild(line); + }; + + setter(es); +} + function closeStream(s){ if(s) s.close(); } /* ── KILL HELPERS ── */ @@ -631,7 +694,7 @@ async function killEndpoint(url, label){ try { const res = await fetch(url, {method:'DELETE'}); if(res.ok) showToast(`${label} cancelled`, 'warn'); - else showToast(`Nothing to cancel`, 'info'); + else showToast('Nothing to cancel', 'info'); } catch { showToast('Kill request failed','error'); } } @@ -645,14 +708,13 @@ async function runWebBuild(){ res.textContent='🔄 Updating website...'; res.className='status-text running'; document.getElementById('webDot').className='dot yellow'; document.getElementById('webStatus').textContent='Deploying'; - document.getElementById('logOutput').textContent=''; showToast('Website build started…','info'); try { const r = await fetch('/api/build-web',{method:'POST'}); if(!r.ok) throw new Error(); closeStream(webES); startLogStream('/api/build-web/logs', document.getElementById('logOutput'), s=>webES=s); - pollStatus('/api/build-web/status', 'buildResult', 'buildBtn', 'killWebBtn', 'webDot', 'webStatus', ()=>{closeStream(webES); fetchLastRuns();}, 'Website', null); + pollStatus('/api/build-web/status','buildResult','buildBtn','killWebBtn','webDot','webStatus',()=>{closeStream(webES);fetchLastRuns();},'Website',null); } catch { res.textContent='❌ Could not start update'; res.className='status-text failed'; document.getElementById('buildBtn').disabled=false; @@ -672,14 +734,13 @@ async function runOrgRoamBuild(){ res.textContent='🔄 Refreshing notes...'; res.className='status-text running'; document.getElementById('roamDot').className='dot yellow'; document.getElementById('roamStatus').textContent='Syncing'; - document.getElementById('orgRoamLogs').textContent=''; showToast('Knowledge base refresh started…','info'); try { const r = await fetch('/api/build-roam',{method:'POST'}); if(!r.ok) throw new Error(); closeStream(roamES); startLogStream('/api/build-roam/logs', document.getElementById('orgRoamLogs'), s=>roamES=s); - pollStatus('/api/build-roam/status','orgRoamResult','orgRoamBtn','killRoamBtn','roamDot','roamStatus',()=>{closeStream(roamES); fetchLastRuns();},'Notes',null); + pollStatus('/api/build-roam/status','orgRoamResult','orgRoamBtn','killRoamBtn','roamDot','roamStatus',()=>{closeStream(roamES);fetchLastRuns();},'Notes',null); } catch { res.textContent='❌ Could not start refresh'; res.className='status-text failed'; document.getElementById('orgRoamBtn').disabled=false; @@ -699,7 +760,6 @@ async function rerunEmacs(){ res.textContent='🔄 Running Emacs...'; res.className='status-text running'; document.getElementById('roamDot').className='dot yellow'; document.getElementById('roamStatus').textContent='Running'; - document.getElementById('orgRoamLogs').textContent=''; showToast('Emacs starting…','info'); try { const r = await fetch('/api/rerun-emacs',{method:'POST'}); @@ -720,25 +780,17 @@ async function rerunEmacs(){ async function runCombined(){ const ok = await showConfirm('Emacs + Notes Refresh','This will run Emacs then rebuild your Org Roam notes sequentially. Continue?'); if(!ok) return; - - // Disable all kb buttons, show kill ['orgRoamBtn','orgRoamBtn2','combinedBtn'].forEach(id=>document.getElementById(id).disabled=true); document.getElementById('killRoamBtn').classList.add('visible'); - const res = document.getElementById('orgRoamResult'); res.textContent='⚡▶ Running Emacs → Notes...'; res.className='status-text running'; document.getElementById('roamDot').className='dot yellow'; document.getElementById('roamStatus').textContent='Pipeline'; - document.getElementById('orgRoamLogs').textContent=''; - - // Show pipeline steps const steps = document.getElementById('pipelineSteps'); steps.classList.remove('hidden'); document.getElementById('pipeStep1').className='pipeline-step active'; document.getElementById('pipeStep2').className='pipeline-step'; - showToast('Emacs + Notes pipeline started…','warn'); - try { const r = await fetch('/api/run-combined',{method:'POST'}); if(!r.ok) throw new Error(); @@ -756,25 +808,20 @@ async function runCombined(){ } function pollCombinedStatus(){ - // We watch the log to detect which step we're on const logEl = document.getElementById('orgRoamLogs'); const interval = setInterval(async()=>{ const r = await fetch('/api/run-combined/status'); const s = await r.json(); - - // Update step indicators from log text - const log = logEl.textContent; - if(log.includes('[2/2]')){ + // Detect step transition from rendered log text + if(logEl.textContent.includes('[2/2]')){ document.getElementById('pipeStep1').className='pipeline-step done'; document.getElementById('pipeStep2').className='pipeline-step active'; } - if(!s.running){ clearInterval(interval); closeStream(combinedES); ['orgRoamBtn','orgRoamBtn2','combinedBtn'].forEach(id=>document.getElementById(id).disabled=false); document.getElementById('killRoamBtn').classList.remove('visible'); - const res = document.getElementById('orgRoamResult'); if(s.lastExitCode===0){ res.textContent='✅ Emacs + Notes completed'; res.className='status-text success'; @@ -851,7 +898,6 @@ document.getElementById('killWebBtn').addEventListener('click', async()=>{ const ok = await showConfirm('Cancel Build','Force-kill the running website build?'); if(ok) killEndpoint('/api/build-web','Web build'); }); - document.getElementById('killRoamBtn').addEventListener('click', async()=>{ const ok = await showConfirm('Cancel Process','Force-kill the running process?'); if(ok) killEndpoint('/api/run-combined','Process'); @@ -867,4 +913,4 @@ document.getElementById('combinedBtn').addEventListener('click', runCombined); fetchHealth(); - + \ No newline at end of file