Serve published failure details as JSON
All checks were successful
Docker / test-build-push (push) Successful in 36s
All checks were successful
Docker / test-build-push (push) Successful in 36s
This commit is contained in:
@@ -7,7 +7,7 @@ cp .env.example .env
|
|||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
Node 24+, no runtime dependencies. Uses built-in `node:sqlite`. Set `TRACE_PASSWORD`. Incidents remain private until a human explicitly publishes them. Published projections are available through `GET /api/v1/public/failures`, `GET /api/v1/public/failures/:slug`, and `GET /api/v1/public/failures/:slug.md` for runtime and 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const publicSeverity={annoyance:'paper-cut',degraded:'minor',unavailable:'signif
|
|||||||
const publicStatus={investigating:'open',mitigated:'worked-around',resolved:'resolved',abandoned:'abandoned'};
|
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 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 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 publicDetail(x){return {...publicIncident(x),sections:[['Symptom',x.symptom],['What was actually happening',x.root_cause],['How we proved it',x.verification],['The fix',x.fix],['What changed afterward',x.prevention],['Remaining risk',x.remaining_risk],['Lesson',x.lesson]].filter(([,body])=>body)};}
|
||||||
function incidentForm(v={}){return `<form method=post class=stack><label>Title<input name=title required value="${esc(v.title)}"></label><div class=cols><label>Project<input name=project value="${esc(v.project)}"></label><label>Detected<input type=datetime-local name=detected_at required value="${esc((v.detected_at||new Date().toISOString()).slice(0,16))}"></label><label>Status<select name=status>${['investigating','mitigated','resolved','abandoned'].map(x=>`<option ${v.status===x?'selected':''}>${x}</option>`)}</select></label><label>Severity<select name=severity>${['annoyance','degraded','unavailable','data-risk','security-risk'].map(x=>`<option ${v.severity===x?'selected':''}>${x}</option>`)}</select></label></div><label>Symptom<textarea name=symptom required>${esc(v.symptom)}</textarea></label>${fields.map(x=>`<label>${x.replaceAll('_',' ')}<textarea name=${x}>${esc(v[x])}</textarea></label>`).join('')}<label>Confidence<select name=confidence>${['unknown','low','medium','high'].map(x=>`<option ${v.confidence===x?'selected':''}>${x}</option>`)}</select></label><button>save</button></form>`;}
|
function incidentForm(v={}){return `<form method=post class=stack><label>Title<input name=title required value="${esc(v.title)}"></label><div class=cols><label>Project<input name=project value="${esc(v.project)}"></label><label>Detected<input type=datetime-local name=detected_at required value="${esc((v.detected_at||new Date().toISOString()).slice(0,16))}"></label><label>Status<select name=status>${['investigating','mitigated','resolved','abandoned'].map(x=>`<option ${v.status===x?'selected':''}>${x}</option>`)}</select></label><label>Severity<select name=severity>${['annoyance','degraded','unavailable','data-risk','security-risk'].map(x=>`<option ${v.severity===x?'selected':''}>${x}</option>`)}</select></label></div><label>Symptom<textarea name=symptom required>${esc(v.symptom)}</textarea></label>${fields.map(x=>`<label>${x.replaceAll('_',' ')}<textarea name=${x}>${esc(v[x])}</textarea></label>`).join('')}<label>Confidence<select name=confidence>${['unknown','low','medium','high'].map(x=>`<option ${v.confidence===x?'selected':''}>${x}</option>`)}</select></label><button>save</button></form>`;}
|
||||||
function redirect(res,to){res.writeHead(303,{Location:to}).end();}
|
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}`)}
|
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}`)}
|
||||||
@@ -52,6 +53,8 @@ async function publicContent(req,res,path){
|
|||||||
}
|
}
|
||||||
const md=path.match(/^\/api\/v1\/public\/failures\/([^/]+)\.md$/);
|
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;}
|
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;}
|
||||||
|
const detail=path.match(/^\/api\/v1\/public\/failures\/([^/]+)$/);
|
||||||
|
if(req.method==='GET'&&detail){const x=db.prepare("SELECT * FROM incidents WHERE public_slug=? AND published_at<>''").get(decodeURIComponent(detail[1]));if(!x){res.writeHead(404,{'content-type':'application/json; charset=utf-8','access-control-allow-origin':'*'}).end('{"error":"not_found"}');return true;}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,failure:publicDetail(x)}));return true;}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user