allowing logs to be read

This commit is contained in:
2026-03-20 23:53:21 +00:00
parent dc908ef251
commit 158a9e4bae

View File

@@ -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 @@
<span>All Systems Running Normally</span>
</div>
<!-- OVERVIEW STATS (4 cols — added uptime) -->
<!-- OVERVIEW STATS -->
<div class="overview fade-up" style="animation-delay:0.15s">
<div class="stat-card">
<span id="serverDot" class="dot green"></span>
@@ -381,15 +407,15 @@
<div class="card-title">🚀 &nbsp;<span>Quick</span>&nbsp;Links</div>
</div>
<div class="quick-links">
<a href="https://notes.zainezq.com" target="_blank" class="link-card"><span class="icon">📚</span>Notes</a>
<a href="https://zainezq.com" target="_blank" class="link-card"><span class="icon">🌍</span>Website</a>
<a href="https://notes.zainezq.com" target="_blank" class="link-card"><span class="icon">📚</span>Notes</a>
<a href="https://zainezq.com" target="_blank" class="link-card"><span class="icon">🌍</span>Website</a>
<a href="https://filebrowser.zainezq.com" target="_blank" class="link-card"><span class="icon">📂</span>Files</a>
<a href="https://calibre.zainezq.com" target="_blank" class="link-card"><span class="icon">📖</span>Library</a>
<a href="https://miniflux.zainezq.com" target="_blank" class="link-card"><span class="icon">📰</span>News</a>
<a href="https://guac.zainezq.com" target="_blank" class="link-card"><span class="icon">🖥️</span>RDP</a>
<a href="https://portainer.zainezq.com" target="_blank" class="link-card"><span class="icon">🐳</span>Portainer</a>
<a href="https://nextcloud.zainezq.com" target="_blank" class="link-card"><span class="icon">☁️</span>Nextcloud</a>
<a href="https://pgadmin.zainezq.com" target="_blank" class="link-card"><span class="icon">🗄️</span>Databases</a>
<a href="https://calibre.zainezq.com" target="_blank" class="link-card"><span class="icon">📖</span>Library</a>
<a href="https://miniflux.zainezq.com" target="_blank" class="link-card"><span class="icon">📰</span>News</a>
<a href="https://guac.zainezq.com" target="_blank" class="link-card"><span class="icon">🖥️</span>RDP</a>
<a href="https://portainer.zainezq.com" target="_blank" class="link-card"><span class="icon">🐳</span>Portainer</a>
<a href="https://nextcloud.zainezq.com" target="_blank" class="link-card"><span class="icon">☁️</span>Nextcloud</a>
<a href="https://pgadmin.zainezq.com" target="_blank" class="link-card"><span class="icon">🗄️</span>Databases</a>
</div>
</div>
@@ -401,15 +427,12 @@
<div class="card-title">📡 &nbsp;<span>Server</span>&nbsp;Status</div>
<span id="healthBadge" class="badge neutral">Checking</span>
</div>
<!-- Uptime breakdown -->
<div class="uptime-grid" id="uptimeGrid">
<div class="uptime-item"><div class="uptime-item-label">Days up</div><div class="uptime-item-value" id="uptimeDays"></div></div>
<div class="uptime-item"><div class="uptime-item-label">Hours</div><div class="uptime-item-value" id="uptimeHours"></div></div>
<div class="uptime-item"><div class="uptime-item-label">Minutes</div><div class="uptime-item-value" id="uptimeMins"></div></div>
<div class="uptime-item"><div class="uptime-item-label">Started at</div><div class="uptime-item-value" id="uptimeStart" style="font-size:0.72rem"></div></div>
</div>
<div class="uptime-bar-wrap">
<div class="uptime-label"><span>Ping History (30 checks)</span><span id="uptimePct"></span></div>
<div class="uptime-bar"><div class="uptime-fill" id="uptimeFill"></div></div>
@@ -432,7 +455,8 @@
<p id="buildResult" class="status-text">💤 Waiting for action</p>
<div class="last-run-meta" id="webLastRunMeta"></div>
<button class="btn btn-ghost toggleLogs" data-target="logOutput">Show logs</button>
<pre id="logOutput" class="log-box hidden"></pre>
<!-- div instead of pre — innerHTML is set directly, pre would double-escape -->
<div id="logOutput" class="log-box hidden"></div>
</div>
<!-- KNOWLEDGE BASE -->
@@ -446,18 +470,15 @@
<button class="btn btn-combined" id="combinedBtn">⚡▶ Emacs + Notes</button>
<button class="btn btn-danger kill-btn" id="killRoamBtn">✕ Cancel</button>
</div>
<!-- Pipeline progress (shown during combined run) -->
<div class="pipeline-steps hidden" id="pipelineSteps">
<div class="pipeline-step" id="pipeStep1">⚡ Emacs</div>
<div class="pipeline-arrow"></div>
<div class="pipeline-step" id="pipeStep2">▶ Notes</div>
</div>
<p id="orgRoamResult" class="status-text">💤 Waiting for action</p>
<div class="last-run-meta" id="roamLastRunMeta"></div>
<button class="btn btn-ghost toggleLogs" data-target="orgRoamLogs">Show logs</button>
<pre id="orgRoamLogs" class="log-box hidden"></pre>
<div id="orgRoamLogs" class="log-box hidden"></div>
</div>
</div>
@@ -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 <span> elements
"done" build finished; close the EventSource
Each line is appended as a block-level <span> 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();
</script>
</body>
</html>
</html>