"""Standalone report renderer. All interactive data stays inside the HTML file.""" import html import json CSS = ''' :root{color-scheme:dark;--bg:#111815;--panel:#1b261f;--text:#e2e8df;--muted:#afbbb0;--line:#405244;--green:#abc99d;--clay:#e3a184;--blue:#9ec2d0;--gold:#d5ba78}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:15px/1.6 system-ui,-apple-system,sans-serif}main,header,footer{width:min(1280px,calc(100% - 48px));margin:auto}header{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding:26px 0;gap:20px}.eyebrow,.tiny{font:11px/1.6 ui-monospace,monospace;color:var(--green);letter-spacing:.07em}h1{font-size:34px;letter-spacing:-.04em;margin:5px 0}h2{font-size:20px;margin:0 0 16px}p{margin:10px 0;color:var(--muted)}a{color:var(--green);text-underline-offset:4px}button,input,select{font:inherit;color:inherit}button,select{cursor:pointer}button{background:#293b2c;border:1px solid #6c8564;border-radius:4px;padding:9px 14px;font-size:12px;min-height:42px}input,select{background:#121c15;border:1px solid #5a7259;border-radius:4px;padding:9px 12px;min-height:42px;max-width:100%}a:focus-visible,button:focus-visible,select:focus-visible,input:focus-visible,summary:focus-visible{outline:2px solid var(--clay);outline-offset:3px}label{font-size:12px;color:var(--muted)}header label{display:grid;gap:6px;min-width:150px}.intro{max-width:850px;padding:22px 0}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:8px 0 28px}.stat{padding:18px 20px;border:1px solid var(--line);background:var(--panel);border-radius:6px}.stat b{display:block;font:30px ui-monospace,monospace;font-weight:400}.stat span{font-size:12px;color:var(--muted)}.stat:nth-child(2) b{color:var(--clay)}.stat:nth-child(3) b{color:var(--gold)}.toolbar{display:flex;flex-wrap:wrap;align-items:end;gap:14px;margin-bottom:20px}.toolbar label{display:grid;gap:6px}.toolbar .search{flex:1;min-width:200px}.toolbar input{width:100%}.count{font:12px ui-monospace,monospace;color:var(--muted);padding:10px 0}.table-wrap{overflow:auto;border:1px solid var(--line);border-radius:6px}table{border-collapse:collapse;width:100%;font-size:12px;min-width:720px}th,td{text-align:left;padding:14px 16px;border-bottom:1px solid var(--line);vertical-align:top}th{font:10px ui-monospace,monospace;color:var(--muted);background:#202d23}td:first-child{min-width:240px;max-width:380px}.test-name{font-weight:600;overflow-wrap:anywhere}.test-class{font:10px/1.6 ui-monospace,monospace;color:var(--muted);display:block;overflow-wrap:anywhere}.badge{font-size:10px;white-space:nowrap;display:inline-block;padding:3px 8px;border:1px solid currentColor;border-radius:3px}.pass,.recovered{color:var(--green)}.failure,.error,.new-failure,.repeated-failure,.failure-without-baseline{color:var(--clay)}.retry-pass{color:var(--gold)}.skipped{color:var(--blue)}.not-reported{color:var(--muted)}.history{display:flex;gap:4px}.history span{display:grid;place-items:center;width:29px;min-height:29px;border:1px solid currentColor;border-radius:3px;font:11px ui-monospace,monospace}.history-head{font:10px ui-monospace,monospace;color:var(--muted);margin-top:8px;max-width:220px}.detail td{padding:0 16px 14px}.details-content{padding:15px;border:1px solid var(--line);background:#18231c;border-radius:4px}details summary{cursor:pointer;font-size:12px;min-height:30px;color:var(--blue)}pre{white-space:pre-wrap;overflow-wrap:anywhere;margin:10px 0;font:11px/1.7 ui-monospace,monospace;color:var(--text)}.attempt{border-top:1px solid var(--line);padding-top:10px;margin-top:10px}.attempt h3{font-size:12px;margin:0 0 7px}.attempt button{font-size:10px;min-height:32px;padding:5px 10px}.empty{padding:30px;text-align:center;color:var(--muted)}.method{display:grid;grid-template-columns:1fr 1fr;gap:30px;margin-top:36px}.method p,.method li{font-size:12px;color:var(--muted)}.method ul{padding-left:18px}footer{margin-top:35px;padding:20px 0 35px;border-top:1px solid var(--line);font-size:11px;color:var(--muted)}.chart{width:100%;height:auto;max-height:280px}.chart text{font:12px ui-monospace,monospace;fill:var(--muted)}.live{color:var(--green)}[hidden]{display:none!important}.js-only{display:none}.js .js-only{display:block}.js .toolbar.js-only{display:flex}.js .no-js{display:none}@media(max-width:700px){main,header,footer{width:calc(100% - 32px)}header{align-items:start;flex-direction:column}header label{width:100%}h1{font-size:29px}.stats{grid-template-columns:1fr 1fr;gap:10px}.stat{padding:14px}.stat b{font-size:25px}.method{grid-template-columns:1fr;gap:18px}.toolbar label{width:100%}.toolbar .search{min-width:0}.toolbar button{flex:1}.intro{padding:15px 0}.intro p{font-size:13px}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto}}@media print{body{background:white;color:black}.toolbar,header select,button{display:none}table{min-width:0}.table-wrap{overflow:visible}.badge,th,td,p{color:black!important}.method{display:block}} ''' JS = r''' document.documentElement.classList.add('js'); const data=JSON.parse(document.querySelector('#data').textContent); const build=document.querySelector('#build'),search=document.querySelector('#search'),category=document.querySelector('#category'),table=document.querySelector('#rows'); const statuses=['new failure','repeated failure','failure without baseline','retry pass','recovered','pass','skipped','not reported']; const friendly={pass:'P',failure:'F',error:'E',skipped:'S','not reported':'-'}; function element(tag,text,cls){const el=document.createElement(tag);if(text!==undefined)el.textContent=text;if(cls)el.className=cls;return el;} function slug(text){return text.replaceAll(' ','-');} function download(content,name,type){const blob=new Blob([content],{type}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);} function artifactLink(row,attempt){const item=data.artifacts.find(a=>a.build===build.value&&a.job===row.job&&a.attempt===attempt.attempt&&a.name===attempt.artifact);if(!item)return element('span','Artifact unavailable');const button=element('button','Download '+item.name);button.type='button';button.addEventListener('click',()=>{const raw=atob(item.content),bytes=Uint8Array.from(raw,c=>c.charCodeAt(0));download(bytes,item.name,'application/xml');});return button;} function filtered(){return data.selections[build.value].rows.filter(row=>(category.value==='all'||row.category===category.value)&&[row.job,row.name,row.classname,row.file,row.message].join(' ').toLowerCase().includes(search.value.trim().toLowerCase()));} function show(){const selected=data.selections[build.value],rows=filtered();table.replaceChildren();document.querySelector('#count').textContent=rows.length+' of '+selected.rows.length+' test identities';document.querySelector('#tests').textContent=selected.rows.length;document.querySelector('#failures').textContent=selected.failed;document.querySelector('#retry').textContent=selected.counts['retry pass'];document.querySelector('#unreported').textContent=selected.counts['not reported'];document.querySelector('#runtime').textContent=selected.execution_seconds.toFixed(3)+' s across '+selected.observations+' observations in '+selected.attempts+' job attempts. This is summed test time, not pipeline wall time.'; for(const row of rows){const tr=element('tr'),test=element('td');test.append(element('span',row.name,'test-name'),element('span',row.job+' / '+row.classname,'test-class'),element('span',row.file||'File not provided','test-class'));const state=element('td');state.append(element('span',row.category,'badge '+slug(row.category)));const history=element('td'),matrix=element('div',undefined,'history');for(const h of row.history){const label=h.retry_pass?'R':friendly[h.status],cell=element('span',label,h.retry_pass?'retry-pass':slug(h.status));cell.title=h.build+': '+(h.retry_pass?'retry pass':h.status);cell.setAttribute('aria-label',cell.title);matrix.append(cell);}history.append(matrix,element('div',row.history.map(h=>h.build).join(' / '),'history-head'));const duration=element('td',row.duration===null?'Not recorded':row.duration.toFixed(3)+' s');tr.append(test,state,history,duration);table.append(tr); const detail=element('tr',undefined,'detail'),td=element('td'),details=element('details'),summary=element('summary','Attempts and evidence');td.colSpan=4;details.append(summary);const content=element('div',undefined,'details-content');content.append(element('p','Final: '+row.status+(row.attempt?' in attempt '+row.attempt:'. No result was supplied for this identity.')));if(row.attempts.length){for(const a of row.attempts){const block=element('div',undefined,'attempt');block.append(element('h3','Attempt '+a.attempt+' / '+a.status),element('pre',a.message||'No failure or skip message.'),artifactLink(row,a));content.append(block);}}else content.append(element('p',row.message));details.append(content);td.append(details);detail.append(td);table.append(detail);} document.querySelector('#empty').hidden=rows.length!==0; } for(const b of data.builds){const option=element('option',b.name);option.value=b.name;build.append(option);}build.value=data.builds.at(-1).name; for(const value of statuses){const option=element('option',value);option.value=value;category.append(option);} build.addEventListener('change',show);category.addEventListener('change',show);search.addEventListener('input',show); document.querySelector('#reset').addEventListener('click',()=>{search.value='';category.value='all';show();}); document.querySelector('#csv').addEventListener('click',()=>{const safe=v=>{let s=v===null?'':String(v);if(/^[\s]*[=+\-@]|^[\t\r\n]/.test(s))s="'"+s;return '"'+s.replaceAll('"','""')+'"';};const fields=['job','file','classname','name','category','status','duration','attempt'];download([fields.join(','),...filtered().map(row=>fields.map(key=>safe(row[key])).join(','))].join('\r\n'),'triage-filtered.csv','text/csv');}); show(); ''' def render(data): latest=data['builds'][-1]['name'];selection=data['selections'][latest] encoded=json.dumps(data,separators=(',',':'),ensure_ascii=True).replace('<','\\u003c').replace('>','\\u003e').replace('&','\\u0026') fallback=''.join(''+html.escape(row['job']+' / '+row['name'])+''+html.escape(row['category'])+''+html.escape(' / '.join(h['status'] for h in row['history']))+''+('Not recorded' if row['duration'] is None else f'{row["duration"]:.3f} s')+'' for row in selection['rows']) return f'''CI failure triage
LOCAL TEST ARTIFACT REVIEW

CI failure triage

Compare final results, inspect retries, and download the original XML. Build order comes from the sequence supplied at ingestion.

Latest build: {html.escape(latest)}

{len(selection['rows'])}Known test identities
{selection['failed']}Final failures / errors
{selection['counts']['retry pass']}Passed after retry
{selection['counts']['not reported']}Not reported

History: P pass / F failure / E error / S skipped / R passed on retry / - not reported

{fallback}
Test identityClassificationBuild historyFinal observed duration

How results are classified

  • A passing final result after a failed attempt is marked retry pass. It is a reason to investigate, not proof of a flaky implementation.
  • A new failure follows a pass in the immediately preceding build. A repeated failure follows a failure or error.
  • Missing reports and skipped tests are kept separate from passing results.
  • A full attempt replaces the job's test membership. A partial retry changes only the supplied test identities.

Evidence and scope

This report contains the ingested XML bytes, their SHA-256 digests, and the normalized results. It works without a server or network connection. Source filenames and messages are rendered as text.

Use one database for one repository and comparison stream. Branch comparison, provider authentication and automatic CI artifact retrieval are outside this file-based tool. A renamed test is a different identity.

Individual export files are replaced atomically. A whole export directory is not an atomic transaction; the HTML is self-contained.

'''