From 825cd4bb746c7ec1e8a5862189f81bc36f2138ef Mon Sep 17 00:00:00 2001 From: Fredrik Johansson Date: Mon, 20 Jul 2026 21:39:23 +0200 Subject: [PATCH] Show operational events in private UI --- src/receipts.mjs | 3 +++ src/server.mjs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/receipts.mjs b/src/receipts.mjs index ab7e2a0..262281b 100644 --- a/src/receipts.mjs +++ b/src/receipts.mjs @@ -14,3 +14,6 @@ export function createReceiptStore(db,config=''){ if(path==='/api/v1/public/projects'&&req.method==='GET'){const rows=db.prepare(`SELECT e.* FROM deployment_events e JOIN (SELECT project,MAX(occurred_at) occurred_at FROM deployment_events WHERE event_type='deployment' GROUP BY project) l ON l.project=e.project AND l.occurred_at=e.occurred_at ORDER BY e.project`).all();return send(res,200,{projects:rows.map(publicEvent)})}return false; }return{handle,publicEvent}; } + +const escapeHtml=value=>String(value??'').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"'); +export function renderEventsPage(db){const rows=db.prepare('SELECT * FROM deployment_events ORDER BY occurred_at DESC LIMIT 100').all();const counts=db.prepare('SELECT event_type,result,count(*) count FROM deployment_events GROUP BY event_type,result ORDER BY event_type,result').all();return`operational events · trace
trace

Operational events

Builds say what CI produced. Deployments say what a host actually ran.

${counts.map(x=>`${escapeHtml(x.event_type)} · ${escapeHtml(x.result)}: ${x.count}`).join('')}
${rows.map(row=>{const payload=JSON.parse(row.payload),artifacts=payload.artifacts||[];return`
${escapeHtml(row.project)} · ${escapeHtml(row.event_type)}

${escapeHtml(row.source_commit.slice(0,12))} ${escapeHtml(row.source_branch)} ${row.environment?`· ${escapeHtml(row.environment)}`:''}

${escapeHtml(row.result)}

${escapeHtml(row.occurred_at)} · received ${escapeHtml(row.received_at)}

${artifacts.map(x=>`

${escapeHtml(x.component)} ${escapeHtml(x.digest||'digest unavailable')}

`).join('')}
raw private receipt
${escapeHtml(JSON.stringify(payload,null,2))}
`}).join('')||'

No operational events received yet.

'}
`} diff --git a/src/server.mjs b/src/server.mjs index 436a6c5..c76c533 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -5,7 +5,7 @@ import { promisify } from 'node:util'; import { mkdirSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import crypto from 'node:crypto'; -import { createReceiptStore } from './receipts.mjs'; +import { createReceiptStore, renderEventsPage } from './receipts.mjs'; const exec = promisify(execFile), port = Number(process.env.PORT || 3082), password = process.env.TRACE_PASSWORD || ''; mkdirSync('data', { recursive: true }); mkdirSync('data/exports', { recursive: true }); @@ -36,4 +36,4 @@ if(u.pathname==='/candidates'&&req.method==='POST'){await scan();redirect(res,'/ const link=u.pathname.match(/^\/candidates\/(\d+)\/link$/);if(link&&req.method==='POST'){const f=await form(req);db.prepare("UPDATE candidates SET incident_id=?,state='linked' WHERE id=?").run(f.incident_id,link[1]);redirect(res,'/candidates');return;} if(u.pathname==='/api/receipts'&&req.method==='POST'){let raw='';for await(const c of req)raw+=c;const x=JSON.parse(raw);db.prepare('INSERT OR IGNORE INTO receipts(project,commit_sha,deployed_at,environment,health,payload) VALUES(?,?,?,?,?,?)').run(x.project,x.commit,x.deployedAt,x.environment||'',x.health||'',raw);res.writeHead(201,{'content-type':'application/json'}).end('{"ok":true}');return;} res.writeHead(404).end(layout('not found','

Not found

'));} -http.createServer(async(req,res)=>{try{const path=new URL(req.url,'http://x').pathname;const handled=await receiptStore.handle(req,res,path);if(handled===false)await handler(req,res);}catch(e){console.error(e);if(!res.headersSent)res.writeHead(500);res.end('Internal error');}}).listen(port,()=>console.log(`trace listening on ${port}`)); +http.createServer(async(req,res)=>{try{const path=new URL(req.url,'http://x').pathname;if(path==='/events'){if(auth(req,res))res.end(renderEventsPage(db));return;}const handled=await receiptStore.handle(req,res,path);if(handled===false)await handler(req,res);}catch(e){console.error(e);if(!res.headersSent)res.writeHead(500);res.end('Internal error');}}).listen(port,()=>console.log(`trace listening on ${port}`));