From 944813c0c4b3d990f4cb5051196914ced115a28f Mon Sep 17 00:00:00 2001 From: Fredrik Johansson Date: Tue, 21 Jul 2026 02:32:12 +0200 Subject: [PATCH] Publish reviewed failure content feed --- README.md | 2 +- src/server.mjs | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2d623f2..4f46060 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ cp .env.example .env npm start ``` -Node 24+, no runtime dependencies. Uses built-in `node:sqlite`. Set `TRACE_PASSWORD`. Nothing publishes automatically: export creates a draft under `data/exports/` for manual review. +Node 24+, no runtime dependencies. Uses built-in `node:sqlite`. Set `TRACE_PASSWORD`. Incidents remain private until a human explicitly publishes them. Published, sanitized Markdown is available through `GET /api/v1/public/failures` and `GET /api/v1/public/failures/:slug.md` for build-time consumers. Git candidate scanning is optional and disabled by default. If an installation deliberately mounts read-only repository mirrors, list their container paths in `TRACE_REPOS`; production receipt ingestion does not require repository access. diff --git a/src/server.mjs b/src/server.mjs index f1b35a6..50e7f9c 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -17,14 +17,20 @@ CREATE TABLE IF NOT EXISTS timeline(id INTEGER PRIMARY KEY AUTOINCREMENT,inciden CREATE TABLE IF NOT EXISTS hypotheses(id INTEGER PRIMARY KEY AUTOINCREMENT,incident_id TEXT NOT NULL REFERENCES incidents(id) ON DELETE CASCADE,statement TEXT NOT NULL,status TEXT NOT NULL DEFAULT 'untested',evidence TEXT DEFAULT ''); CREATE TABLE IF NOT EXISTS candidates(id INTEGER PRIMARY KEY AUTOINCREMENT,repo TEXT NOT NULL,sha TEXT NOT NULL,committed_at TEXT NOT NULL,subject TEXT NOT NULL,paths TEXT DEFAULT '',reason TEXT NOT NULL,state TEXT NOT NULL DEFAULT 'new',incident_id TEXT,UNIQUE(repo,sha)); CREATE TABLE IF NOT EXISTS receipts(id INTEGER PRIMARY KEY AUTOINCREMENT,project TEXT NOT NULL,commit_sha TEXT NOT NULL,deployed_at TEXT NOT NULL,environment TEXT DEFAULT '',health TEXT DEFAULT '',payload TEXT NOT NULL,UNIQUE(project,commit_sha,deployed_at));`); +for(const sql of ["ALTER TABLE incidents ADD COLUMN published_at TEXT DEFAULT ''","ALTER TABLE incidents ADD COLUMN public_slug TEXT DEFAULT ''"]){try{db.exec(sql)}catch(error){if(!String(error.message).includes('duplicate column'))throw error;}} const receiptStore = createReceiptStore(db, process.env.TRACE_RECEIPT_TOKENS || ''); const repos = new Map((process.env.TRACE_REPOS || '').split(',').filter(Boolean).map(x => { const i=x.indexOf(':'); return [x.slice(0,i),x.slice(i+1)]; })); const giteaRepos=(process.env.TRACE_GITEA_REPOS||'').split(',').map(x=>x.trim()).filter(Boolean),giteaUrl=process.env.TRACE_GITEA_URL||'https://repo.explewd.com',giteaToken=process.env.TRACE_GITEA_TOKEN||''; const esc=s=>String(s??'').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"'); const form=async req=>{let b='';for await(const c of req)b+=c;return Object.fromEntries(new URLSearchParams(b));}; function auth(req,res){if(!password)return true;const expected='Basic '+Buffer.from('trace:'+password).toString('base64');if(req.headers.authorization===expected)return true;res.writeHead(401,{'WWW-Authenticate':'Basic realm="trace"'}).end('Authentication required');return false;} -function layout(title,body){return `${esc(title)} · trace
trace
${body}
`;} +function layout(title,body){return `${esc(title)} · trace
trace
${body}
`;} const fields=['impact','root_cause','fix','verification','prevention','remaining_risk','lesson','public_summary']; +const slugify=s=>String(s||'failure').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'')||'failure'; +const publicSeverity={annoyance:'paper-cut',degraded:'minor',unavailable:'significant','data-risk':'significant','security-risk':'significant'}; +const publicStatus={investigating:'open',mitigated:'worked-around',resolved:'resolved',abandoned:'abandoned'}; +function publicMarkdown(x){return `---\ntitle: ${JSON.stringify(x.title)}\nsummary: ${JSON.stringify(x.public_summary||x.symptom)}\ndate: ${x.detected_at.slice(0,10)}\nprojectSlug: ${JSON.stringify(x.project||'')}\nkind: bug\nseverity: ${publicSeverity[x.severity]||'minor'}\nstatus: ${publicStatus[x.status]||'open'}\ntags: []\n---\n\n## Symptom\n\n${x.symptom}\n\n## What was actually happening\n\n${x.root_cause}\n\n## How we proved it\n\n${x.verification}\n\n## The fix\n\n${x.fix}\n\n## What changed afterward\n\n${x.prevention}\n\n## Remaining risk\n\n${x.remaining_risk}\n\n## Lesson\n\n${x.lesson}\n`;} +function publicIncident(x){return {id:x.id,slug:x.public_slug,title:x.title,summary:x.public_summary||x.symptom,date:x.detected_at.slice(0,10),projectSlug:x.project||'',kind:'bug',severity:publicSeverity[x.severity]||'minor',status:publicStatus[x.status]||'open',publishedAt:x.published_at,updatedAt:x.updated_at,markdownUrl:`/api/v1/public/failures/${encodeURIComponent(x.public_slug)}.md`};} function incidentForm(v={}){return `
${fields.map(x=>``).join('')}
`;} function redirect(res,to){res.writeHead(303,{Location:to}).end();} async function scan(){for(const [id,path] of repos){let stdout;try{stdout=(await exec('git',['-C',path,'log','--all','--no-merges','-n','300','--pretty=format:%H%x1f%ad%x1f%s','--date=iso-strict'],{maxBuffer:2e6})).stdout;}catch(error){console.warn(`Skipping unavailable repository ${id} at ${path}: ${error.message}`);continue;}for(const line of stdout.split('\n')){const [sha,date,subject]=line.split('\x1f');if(!/fix|bug|broken|race|crash|regression|rollback|restore|duplicate|auth|fail/i.test(subject||''))continue;let paths='';try{paths=(await exec('git',['-C',path,'show','--name-only','--pretty=format:',sha],{maxBuffer:2e5})).stdout.trim();}catch{}db.prepare('INSERT OR IGNORE INTO candidates(repo,sha,committed_at,subject,paths,reason) VALUES(?,?,?,?,?,?)').run(id,sha,date,subject,paths,'local git keyword');}}const remote=await scanGiteaCandidates({baseUrl:giteaUrl,repositories:giteaRepos,token:giteaToken});for(const candidate of remote.candidates)db.prepare('INSERT OR IGNORE INTO candidates(repo,sha,committed_at,subject,paths,reason) VALUES(?,?,?,?,?,?)').run(candidate.repo,candidate.sha,candidate.committedAt,candidate.subject,candidate.paths,candidate.reason);for(const error of remote.errors)console.warn(`Skipping Gitea repository ${error.repo}: ${error.error}`)} @@ -38,4 +44,23 @@ 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;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}`)); + +async function publicContent(req,res,path){ + if(req.method==='GET'&&path==='/api/v1/public/failures'){ + const rows=db.prepare("SELECT * FROM incidents WHERE published_at<>'' ORDER BY detected_at DESC").all(); + res.writeHead(200,{'content-type':'application/json; charset=utf-8','access-control-allow-origin':'*','cache-control':'public, max-age=60'}).end(JSON.stringify({schemaVersion:1,generatedAt:new Date().toISOString(),failures:rows.map(publicIncident)}));return true; + } + const md=path.match(/^\/api\/v1\/public\/failures\/([^/]+)\.md$/); + if(req.method==='GET'&&md){const x=db.prepare("SELECT * FROM incidents WHERE public_slug=? AND published_at<>''").get(decodeURIComponent(md[1]));if(!x){res.writeHead(404).end('Not found');return true;}res.writeHead(200,{'content-type':'text/markdown; charset=utf-8','access-control-allow-origin':'*','cache-control':'public, max-age=60'}).end(publicMarkdown(x));return true;} + return false; +} + +async function publicationAction(req,res,path){ + const match=path.match(/^\/incidents\/([^/]+)\/(publish|unpublish)$/);if(!match||req.method!=='POST')return false;if(!auth(req,res))return true; + const x=db.prepare('SELECT * FROM incidents WHERE id=?').get(match[1]);if(!x){res.writeHead(404).end();return true;} + if(match[2]==='unpublish')db.prepare("UPDATE incidents SET published_at='',updated_at=? WHERE id=?").run(new Date().toISOString(),x.id); + else {let slug=x.public_slug||slugify(x.title);const collision=db.prepare('SELECT id FROM incidents WHERE public_slug=? AND id<>?').get(slug,x.id);if(collision)slug=`${slug}-${x.id.slice(0,8)}`;const now=new Date().toISOString();db.prepare('UPDATE incidents SET public_slug=?,published_at=?,updated_at=? WHERE id=?').run(slug,now,now,x.id);} + redirect(res,`/incidents/${x.id}`);return true; +} + +http.createServer(async(req,res)=>{try{const path=new URL(req.url,'http://x').pathname;if(await publicContent(req,res,path))return;if(await publicationAction(req,res,path))return;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}`));