Board: Decomp view (progress tiers, function inspector, next up per band, deferred) and RE tools in Health; Verified tier from the emulator

This commit is contained in:
KeelrLeek committed 2026-10-05 16:08:51 -05:00
1 parent a9efc2e5c5
commit 87631bec52
5 files changed
+331 -6

No files matched your search

+98 -2
View File
@@ -55,6 +55,20 @@ body { overflow: hidden; }
.content { flex: 1; min-height: 0; overflow-y: auto; padding: 24px 32px 56px; }
.view { display: none; max-width: 1380px; margin: 0 auto; gap: 20px; }
.view.on { display: grid; }
.tierbar { display: flex; height: 22px; border-radius: 7px; overflow: hidden; background: var(--surface-3); gap: 2px; }
.tierbar span { display: block; height: 100%; min-width: 3px; }
.tier-exact { background: var(--green); } .tier-verified { background: var(--blue); } .tier-readable { background: var(--violet); }
.tier-deferred { background: var(--amber); } .tier-raw { background: var(--line-2); }
.tier-library, .tier-engine { background: repeating-linear-gradient(45deg, var(--surface-3) 0 4px, var(--line-2) 4px 8px); }
.tier-key { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 8px; vertical-align: -1px; }
td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.fn-head { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center; margin-top: 16px; }
.fn-head h3 { margin: 0; font-size: 16px; }
.fn-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 7px 16px; font-size: 13px; margin: 14px 0 0; }
.fn-facts dt { color: var(--muted); } .fn-facts dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.disasm { white-space: pre; font: 11.5px/1.5 var(--mono); max-height: 440px; overflow: auto; padding: 10px 12px; border-radius: 9px; background: var(--surface-3); margin: 8px 0 0; }
.band { padding: 12px 18px; border-top: 1px solid var(--line); } .band:first-child { border-top: 0; }
.band .cell-who { gap: 8px; } .band ul { margin: 8px 0 0; }
.cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 460px), 1fr)); gap: 20px; align-items: start; }
.sub-h { font-size: 11px; letter-spacing: .8px; text-transform: uppercase; color: var(--faint); font-weight: 700; margin: 20px 0 8px; }
.flush th:first-child, .flush td:first-child { padding-left: 18px; }
@@ -431,6 +445,7 @@ button.chip:hover { color: var(--accent-ink); background: var(--accent-soft); }
<button class="nav-btn" data-go="launch" title="Launch (2)"><svg class="i"><use href="static/icons.svg#rocket"/></svg><span>Launch</span><span class="count-badge" id="navLaunch" hidden></span></button>
<button class="nav-btn" data-go="tasks" title="Tasks (3)"><svg class="i"><use href="static/icons.svg#tasks"/></svg><span>Tasks</span><span class="count-badge" id="navTasks" hidden></span></button>
<button class="nav-btn" data-go="review" title="Review (4)"><svg class="i"><use href="static/icons.svg#review"/></svg><span>Review</span><span class="count-badge" id="navReview" hidden></span></button>
<button class="nav-btn" data-go="decomp" title="Decomp (8)"><svg class="i"><use href="static/icons.svg#terminal"/></svg><span>Decomp</span></button>
<button class="nav-btn" data-go="activity" title="Activity (5)"><svg class="i"><use href="static/icons.svg#activity"/></svg><span>Activity</span><span class="count-badge" id="navActivity" hidden></span></button>
<button class="nav-btn" data-go="knowledge" title="Knowledge (6)"><svg class="i"><use href="static/icons.svg#book"/></svg><span>Knowledge</span><span class="count-badge" id="navKnowledge" hidden></span></button>
<button class="nav-btn" data-go="health" title="Health (7)"><svg class="i"><use href="static/icons.svg#heart"/></svg><span>Health</span></button>
@@ -622,12 +637,44 @@ button.chip:hover { color: var(--accent-ink); background: var(--accent-soft); }
</div>
</section>
<!-- decomp -->
<section class="view" id="view-decomp" aria-label="Decomp">
<section class="card" aria-labelledby="h-progress">
<header><h2 id="h-progress"><svg class="i"><use href="static/icons.svg#trophy"/></svg>Progress by function</h2><span class="count" id="progressSum"></span><span class="grow"></span>
<button class="btn sm" id="refreshDecomp"><svg class="i sm"><use href="static/icons.svg#refresh"/></svg>Refresh</button></header>
<div class="body" id="progress"></div>
</section>
<section class="card" aria-labelledby="h-inspect">
<header><h2 id="h-inspect"><svg class="i"><use href="static/icons.svg#search"/></svg>Inspect a function</h2><span class="note">original name, source file, convention, match and near-duplicates</span></header>
<div class="body">
<form class="searchbar" id="fnForm"><div class="field"><svg class="i"><use href="static/icons.svg#search"/></svg>
<input type="search" id="fnTarget" placeholder="An address (0x4ADC70) or a name (CanCarry)" aria-label="Function"></div>
<button class="btn primary">Inspect</button></form>
<div id="fnInfo"></div>
</div>
</section>
<div class="cols">
<section class="card" aria-labelledby="h-next">
<header><h2 id="h-next"><svg class="i"><use href="static/icons.svg#list"/></svg>Next up in each band</h2><span class="note">what next_functions hands out, easiest first</span></header>
<div class="body flush scroll" id="nextUp"></div>
</section>
<section class="card" aria-labelledby="h-deferred">
<header><h2 id="h-deferred"><svg class="i"><use href="static/icons.svg#clock"/></svg>Deferred</h2><span class="count" id="deferCount"></span></header>
<div class="body flush scroll" id="deferred"></div>
</section>
</div>
</section>
<!-- health -->
<section class="view" id="view-health" aria-label="Health">
<section class="card" aria-labelledby="h-notify">
<header><h2 id="h-notify"><svg class="i"><use href="static/icons.svg#bell"/></svg>Notifications</h2></header>
<div class="body"><label class="switch" title="Windows notifications for questions, urgent messages and messages to you - also with this page closed"><input type="checkbox" id="toasts"> Windows notifications for questions, urgent messages and messages to you, also while this page is closed</label></div>
</section>
<section class="card" aria-labelledby="h-retools">
<header><h2 id="h-retools"><svg class="i"><use href="static/icons.svg#terminal"/></svg>Reverse-engineering tools</h2><span class="count" id="reSum"></span></header>
<div class="body flush scroll" id="retools"></div>
</section>
<section class="card" aria-labelledby="h-health">
<header><h2 id="h-health"><svg class="i"><use href="static/icons.svg#heart"/></svg>Health and connections</h2><span class="count" id="healthSum"></span><span class="grow"></span>
<button class="btn sm primary" id="runDoctor"><svg class="i sm"><use href="static/icons.svg#refresh"/></svg>Run checks</button></header>
@@ -709,7 +756,8 @@ async function get(path){const r=await fetch(path,{cache:'no-store'});const j=aw
const VIEWS={overview:['Overview','Who is working, what needs you, and what happened','home'],launch:['Launch','Bring agents online one careful step at a time','rocket'],
tasks:['Tasks','The shared queue that idle agents take work from','tasks'],review:['Review','Submitted work, acknowledgements and claims','review'],
activity:['Activity','Live tool calls, the team pulse and the scoreboard','activity'],knowledge:['Knowledge','Search every message, pinned decisions and threads','book'],
health:['Health','Harbor in every agent app, the database, backups and the server','heart'],chat:['Chat','The team’s conversation','chat']};
health:['Health','Harbor in every agent app, the database, backups and the server','heart'],
decomp:['Decomp','Progress by function, the function inspector and what each agent takes next','terminal'],chat:['Chat','The team’s conversation','chat']};
function show(view){if(!VIEWS[view])view='overview';current=view;
document.querySelectorAll('.view').forEach(v=>v.classList.toggle('on',v.id==='view-'+view));
document.querySelectorAll('.nav-btn[data-go]').forEach(b=>{if(b.dataset.go===view)b.setAttribute('aria-current','page');else b.removeAttribute('aria-current')});
@@ -722,6 +770,8 @@ function show(view){if(!VIEWS[view])view='overview';current=view;
if(data&&view==='overview')renderDigest();
if(data&&view==='activity'){renderPulse();renderFeed()}
if(data&&view==='launch')renderLaunch();
if(data&&view==='decomp')renderDecomp();
if(view==='health')renderTools();
if(view==='health'&&!doctorRan)runDoctor()}
function paintTitle(){document.title=(unseen&&document.hidden?'('+unseen+') ':'')+(data?data.project.title+' · ':'')+(current?VIEWS[current][0]:'Harbor')}
@@ -962,6 +1012,52 @@ el('searchForm').onsubmit=async e=>{e.preventDefault();const q=el('q').value.tri
el('menuBtn').onclick=()=>el('shell').classList.add('open');
el('scrim').onclick=()=>el('shell').classList.remove('open');
window.addEventListener('hashchange',()=>{const v=location.hash.slice(1);if(v&&v!==current)show(v)});
const TIERS=[['exact','Exact match','byte-identical with the game’s own compiler'],['verified','Verified','behaves the same as the game in the emulator'],
['readable','Readable','readable C that passed review'],['deferred','Deferred','set aside with a reason after failed attempts'],['raw','Raw','not decompiled yet'],
['library','Library','C runtime, D3DX9 and other linked libraries: never decompiled'],['engine','Engine','Gamebryo engine code: names only']];
const fmt=n=>Number(n||0).toLocaleString('en-US');
function renderDecomp(){renderProgress();renderNextUp();renderDeferred()}
async function renderProgress(){let p;try{p=await get('api/decomp/progress')}catch(e){put('progress','<div class="empty err">'+esc(e.message)+'</div>');return}
const t=p.tiers,own=['exact','verified','readable','deferred','raw'].reduce((a,k)=>a+t[k].functions,0),done=t.exact.functions+t.verified.functions+t.readable.functions;
el('progressSum').textContent=fmt(done)+' of '+fmt(own)+' game functions decompiled';
put('progress','<div class="tierbar" role="img" aria-label="'+esc(TIERS.map(([k,l])=>l+' '+t[k].functions).join(', '))+'">'+TIERS.map(([k,l])=>t[k].functions?'<span class="tier-'+k+'" style="flex:'+t[k].functions+'" title="'+esc(l)+': '+fmt(t[k].functions)+' functions"></span>':'').join('')+'</div>'
+'<table style="margin-top:14px"><thead><tr><th>Tier</th><th style="text-align:right">Functions</th><th style="text-align:right">Bytes</th><th>Meaning</th></tr></thead><tbody>'
+TIERS.map(([k,l,d])=>'<tr><td><span class="tier-key tier-'+k+'"></span>'+esc(l)+'</td><td class="num">'+fmt(t[k].functions)+'</td><td class="num">'+fmt(t[k].bytes)+'</td><td class="note">'+esc(d)+'</td></tr>').join('')+'</tbody></table>'
+(p.sources&&!p.sources.diffrun_results?'<div class="note" style="margin-top:10px">The emulator check has not reported yet, so Verified is still empty.</div>':''))}
async function renderNextUp(){if(!el('nextUp')._html)put('nextUp',empty('clock','Ranking the bands…','This takes a few seconds the first time.'));let q;
try{q=await get('api/decomp/queues')}catch(e){put('nextUp','<div class="empty err">'+esc(e.message)+'</div>');return}
put('nextUp',q.length?q.map(a=>'<div class="band"><div class="cell-who">'+avatar(a.agent,'xs')+'<b>'+esc(a.agent)+'</b><span class="note mono">'+esc(a.bands.map(b=>b.join('–')).join(', '))+'</span></div>'
+(a.next.length?'<ul class="attn">'+a.next.map(f=>'<li><a href="#" class="mono" data-fn="'+esc(f.address)+'">'+esc(f.address)+'</a> '+esc(f.name||(f.file||'').replace(/\.c$/,''))+'<div class="note">'+esc(f.why)+'</div></li>').join('')+'</ul>'
:'<div class="note">'+esc(a.note||'Nothing left in this band.')+'</div>')+'</div>').join(''):empty('list','No bands yet','The coordinator gives each agent an address band.'))}
async function renderDeferred(){let d;try{d=await get('api/decomp/deferred')}catch(e){return}el('deferCount').textContent=d.length||'';
put('deferred',d.length?'<table><thead><tr><th>Function</th><th>Reason</th><th>Evidence</th></tr></thead><tbody>'+d.map(x=>'<tr><td><a href="#" class="mono" data-fn="'+esc(x.address)+'">'+esc(x.address)+'</a><div class="note">'+esc(x.name||'')+'</div></td><td><span class="tag amber">'+esc(x.reason)+'</span><div class="note">'+esc(x.author)+'</div></td><td class="note">'+esc(x.evidence)+'</td></tr>').join('')+'</tbody></table>'
:empty('check','Nothing deferred','After two failed attempts an agent sets a function aside here, with the reason and what it found.'))}
async function inspectFn(){const target=el('fnTarget').value.trim();if(!target)return;put('fnInfo',empty('search','Looking it up…',''));let f;
try{f=await get('api/decomp/function?target='+encodeURIComponent(target))}catch(e){put('fnInfo','<div class="empty err">'+esc(e.message)+'</div>');return}
const fn=f.function||{},mac=f.mac_name,src=f.source_file,m=f.match,b=f.behaviour,c=f.convention_from_bytes;
const row=(k,v)=>v?'<dt>'+esc(k)+'</dt><dd>'+v+'</dd>':'';
const note=f.names_row?String(f.names_row).split(',').slice(3).join(',').replace(/^"|"$/g,'').replace(/""/g,'"'):'';
const exact=m&&['exact','exact-relaxed','match'].includes(m.status),near=m&&m.status==='exact-but-targets-differ';
put('fnInfo','<div class="fn-head"><h3 class="mono">'+esc(fn.start||f.address)+'</h3><b>'+esc(fn.name||'unnamed')+'</b>'
+'<span class="tag '+(f.status==='readable'?'violet':'')+'">'+esc(f.status)+'</span>'+(f.library?'<span class="tag">library</span>':'')
+(m?'<span class="tag '+(exact?'green':near?'amber':'')+'">'+(exact?'exact match':near?'same bytes, different targets':esc(m.status)+(m.similarity!=null?' · '+Math.round(m.similarity*100)+'% similar':''))+'</span>':'')
+(b?'<span class="tag '+(b.verdict==='same'?'green':b.verdict==='differs'?'red':'')+'">emulator: '+esc(b.verdict)+'</span>':'')
+'<span class="note">'+esc(fn.size??'?')+' bytes · '+esc(f.category||'')+'</span></div>'
+'<dl class="fn-facts">'+row('Original name',mac?'<code>'+esc(mac.demangled)+'</code> <span class="note">'+esc(mac.confidence)+' confidence, from the Mac build</span>':'')
+row('Source file',src?'<code>'+esc(src.source_file)+'</code>'+(src.lines?' line '+esc(src.lines):'')+' <span class="note">'+esc(src.confidence)+' confidence, from the Xbox build</span>':'')
+row('Convention',c?esc(c.summary)+(c.ret_pops&&c.ret_pops.some(x=>x)?' · cleans up '+esc(c.ret_pops.join('/'))+' bytes':''):'')
+row('Decompiled file',f.src_file?'<span class="mono">'+esc(f.src_file)+'</span>':'')
+row('Deferred',f.deferred?'<span class="tag amber">'+esc(f.deferred.reason)+'</span> '+esc(f.deferred.evidence):'')
+row('Emulator',b&&b.verdict!=='same'?esc(b.reason||''):b&&b.weak?'same, but no trial ran to the end in both':'')
+row('Prototype',f.prototype?'<code>'+esc(f.prototype)+'</code>':'')+row('Team note',note?esc(note.slice(0,700)):'')+'</dl>'
+(f.similar&&f.similar.length?'<h3 class="sub-h">Near-duplicates</h3><ul class="attn">'+f.similar.map(s=>'<li><a href="#" class="mono" data-fn="'+esc(s.address)+'">'+esc(s.address)+'</a> '+esc(s.name||'')+' <span class="note">'+Math.round(s.score*100)+'% alike · '+esc(s.status)+'</span></li>').join('')+'</ul>':'')
+'<details data-k="disasm"><summary>Disassembly</summary><pre class="disasm">'+esc(f.disassembly||'')+'</pre></details>')}
async function renderTools(){let t;try{t=await get('api/decomp/tools')}catch(e){put('retools','<div class="empty err">'+esc(e.message)+'</div>');return}
const bad=t.filter(x=>!x.ok).length;el('reSum').textContent=bad?bad+' to check':'all in place';
put('retools','<table><thead><tr><th>Status</th><th>Tool</th></tr></thead><tbody>'+t.map(x=>'<tr><td><span class="tag '+(x.ok?'green':'amber')+'"><span class="dot"></span>'+(x.ok?'ok':'check')+'</span></td><td>'+esc(x.tool)+'<div class="note">'+esc(x.detail)+'</div></td></tr>').join('')+'</tbody></table>')}
el('fnForm').addEventListener('submit',e=>{e.preventDefault();inspectFn()});
el('refreshDecomp').addEventListener('click',renderDecomp);
document.addEventListener('click',e=>{const a=e.target.closest('[data-fn]');if(!a)return;e.preventDefault();if(current!=='decomp')show('decomp');el('fnTarget').value=a.dataset.fn;inspectFn();el('h-inspect').scrollIntoView({behavior:'smooth',block:'start'})});
document.addEventListener('keydown',e=>{if(e.ctrlKey||e.metaKey||e.altKey||e.target.closest('input,textarea,select,[contenteditable]')||document.querySelector('dialog[open]'))return;
const keys=Object.keys(VIEWS).filter(k=>k!=='chat');const i=Number(e.key)-1;if(i>=0&&i<keys.length){e.preventDefault();show(keys[i])}
else if(e.key==='c'){e.preventDefault();show('chat');el('text').focus()}
@@ -1208,7 +1304,7 @@ document.addEventListener('visibilitychange',()=>{if(!document.hidden){unseen=0;
const startQ=new URLSearchParams(location.search).get('q');
const onBoard=/\/board\/?$/.test(location.pathname);
show(startQ?'knowledge':(location.hash.slice(1)||(onBoard?store.get('board.view','overview'):'chat')));
refresh().then(()=>{renderDigest();if(current==='activity'){renderPulse();renderFeed()}if(current==='launch')renderLaunch();if(startQ){el('q').value=startQ;el('searchForm').requestSubmit()}});
refresh().then(()=>{renderDigest();if(current==='activity'){renderPulse();renderFeed()}if(current==='launch')renderLaunch();if(current==='decomp')renderDecomp();if(current==='health')renderTools();if(startQ){el('q').value=startQ;el('searchForm').requestSubmit()}});
setInterval(refresh,3000);
setInterval(()=>{if(current==='overview')renderDigest()},30000);
setInterval(()=>{if(current==='activity')renderPulse()},60000);