Logbook lives in Harbor and keeps itself current
The board's Logbook view replaces the published page: statistics, the chart of the executable, where the code stands (with the function tiers), the trend, milestones and the ship's log, stored as JSON in the Harbor home. A server thread follows main: each new commit becomes a log entry and the statistics, chart and trend point are rebuilt with the open-seas progress tools on a scratch copy. 'harbor logbook add|stats|map|milestone|refresh'.
This commit is contained in:
1 parent
87631bec52
commit
b7e09cfbd1
7 files changed
+497
-6
No files matched your search
+119
-6
@@ -55,6 +55,37 @@ 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; }
|
||||
.lb { --lb-game: var(--blue); --lb-engine: var(--violet); --lb-platform: var(--amber); --lb-crt: var(--muted); --lb-unsorted: var(--line-2); --lb-done: var(--green); }
|
||||
.lb-head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 10px 20px; }
|
||||
.lb-head .chip { display: inline-block; font: 600 11.5px/1 var(--font, inherit); letter-spacing: .06em; text-transform: uppercase; color: var(--accent-ink); background: var(--accent-soft); border-radius: 999px; padding: 6px 10px; }
|
||||
.lb-head p { margin: 8px 0 0; color: var(--muted); max-width: 70ch; }
|
||||
.lb-stamp { font: 12px/1.5 var(--mono); color: var(--muted); text-align: right; } .lb-stamp b { color: var(--text); font-weight: 600; }
|
||||
.lb-figs { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; }
|
||||
.lb-fig { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-2, 12px); padding: 14px 16px; }
|
||||
.lb-fig .v { font: 700 26px/1.1 var(--mono); font-variant-numeric: tabular-nums; } .lb-fig .l { color: var(--muted); font-size: 13px; margin-top: 4px; } .lb-fig .n { color: var(--faint); font: 11.5px/1.35 var(--mono); margin-top: 6px; }
|
||||
canvas#lbMap { width: 100%; height: 150px; display: block; }
|
||||
.lb-axis { display: flex; justify-content: space-between; font: 11.5px var(--mono); color: var(--faint); margin-top: 6px; }
|
||||
.lb-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-top: 12px; font-size: 12.5px; color: var(--muted); }
|
||||
.lb-sw { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 6px; vertical-align: -1px; }
|
||||
.lb-stack { display: flex; height: 24px; border-radius: 7px; overflow: hidden; background: var(--surface-3); gap: 2px; margin-top: 10px; }
|
||||
.lb-stack i { display: block; height: 100%; min-width: 2px; }
|
||||
.lb-hatch { background: repeating-linear-gradient(135deg, var(--surface-3) 0 5px, var(--line-2) 5px 10px) !important; }
|
||||
.lb-cat { display: grid; grid-template-columns: 14px minmax(0, 1fr) auto; gap: 2px 10px; padding: 10px 0; border-top: 1px solid var(--line); align-items: start; }
|
||||
.lb-cat .sw { width: 12px; height: 12px; border-radius: 3px; margin-top: 4px; } .lb-cat .plan { color: var(--muted); font-size: 12.5px; }
|
||||
.lb-cat .nums { text-align: right; font: 12.5px/1.45 var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--muted); } .lb-cat .nums b { color: var(--text); }
|
||||
.lb-cat .sub { grid-column: 2 / 4; margin-top: 8px; }
|
||||
.lb-big { font: 700 26px/1.1 var(--mono); font-variant-numeric: tabular-nums; }
|
||||
svg#lbTrend { width: 100%; height: 110px; display: block; }
|
||||
.lb-ms h3 { font-size: 14px; margin: 0 0 6px; display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; } .lb-ms h3 .code { font-family: var(--mono); color: var(--accent-ink); }
|
||||
.lb-ms { padding: 10px 0; border-top: 1px solid var(--line); } .lb-ms:first-child { border-top: 0; }
|
||||
.lb-ms ul { list-style: none; margin: 6px 0 0; padding: 0; display: grid; gap: 4px; } .lb-ms li { display: grid; grid-template-columns: 16px 1fr; gap: 6px; font-size: 13px; color: var(--muted); }
|
||||
.lb-ms li::before { content: "○"; } .lb-ms li.done { color: var(--text); } .lb-ms li.done::before { content: "●"; color: var(--green); }
|
||||
.lb-phase { font-weight: 700; color: var(--accent-ink); margin-top: 10px; font-size: 13px; letter-spacing: .04em; text-transform: uppercase; }
|
||||
.lb-log { list-style: none; margin: 0; padding: 0; } .lb-log li { display: grid; grid-template-columns: 104px minmax(0, 1fr); gap: 12px; padding: 12px 0; border-top: 1px solid var(--line); }
|
||||
.lb-log li:first-child { border-top: 0; } .lb-log time { font: 11.5px/1.5 var(--mono); color: var(--faint); }
|
||||
.lb-log .kind { font: 700 10px/1 inherit; letter-spacing: .08em; text-transform: uppercase; color: var(--accent-ink); margin-right: 8px; }
|
||||
.lb-log .t { font-weight: 600; } .lb-log .d { color: var(--muted); font-size: 13px; white-space: pre-wrap; overflow-wrap: anywhere; margin-top: 4px; }
|
||||
@media (max-width: 560px) { .lb-log li { grid-template-columns: 1fr; gap: 2px; } .lb-stamp { text-align: left; } }
|
||||
.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); }
|
||||
@@ -442,6 +473,7 @@ button.chip:hover { color: var(--accent-ink); background: var(--accent-soft); }
|
||||
<nav class="nav" aria-label="Sections">
|
||||
<div class="nav-label">Board</div>
|
||||
<button class="nav-btn" data-go="overview" title="Overview (1)"><svg class="i"><use href="static/icons.svg#home"/></svg><span>Overview</span><span class="count-badge" id="navOverview" hidden></span></button>
|
||||
<button class="nav-btn" data-go="logbook" title="Logbook (9)"><svg class="i"><use href="static/icons.svg#anchor"/></svg><span>Logbook</span></button>
|
||||
<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>
|
||||
@@ -637,6 +669,43 @@ button.chip:hover { color: var(--accent-ink); background: var(--accent-soft); }
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- logbook -->
|
||||
<section class="view lb" id="view-logbook" aria-label="Logbook">
|
||||
<section class="card"><div class="body lb-head">
|
||||
<div><span class="chip" id="lbMilestone">Milestone</span>
|
||||
<p>Decompiling Sid Meier’s Pirates! (2004) into readable C, then rebuilding it in Rust, charted function by function against the PC 1.0.2 executable.</p></div>
|
||||
<div class="lb-stamp" id="lbStamp">Loading…</div></div></section>
|
||||
<div class="lb-figs" id="lbFigs"></div>
|
||||
<section class="card" aria-labelledby="h-lbmap">
|
||||
<header><h2 id="h-lbmap"><svg class="i"><use href="static/icons.svg#layout"/></svg>Chart of the executable</h2><span class="note">bytes per category along the code addresses</span></header>
|
||||
<div class="body"><canvas id="lbMap" aria-label="Address space of the executable, bytes per category"></canvas>
|
||||
<div class="lb-axis"><span id="lbLow">0x401000</span><span>code address →</span><span id="lbHigh"></span></div>
|
||||
<div class="lb-legend" id="lbLegend"></div></div>
|
||||
</section>
|
||||
<div class="cols">
|
||||
<section class="card" aria-labelledby="h-lbwhere">
|
||||
<header><h2 id="h-lbwhere"><svg class="i"><use href="static/icons.svg#trophy"/></svg>Where the code stands</h2></header>
|
||||
<div class="body" id="lbWhere"></div>
|
||||
</section>
|
||||
<section class="card" aria-labelledby="h-lbtrend">
|
||||
<header><h2 id="h-lbtrend"><svg class="i"><use href="static/icons.svg#activity"/></svg>Trend</h2></header>
|
||||
<div class="body"><svg id="lbTrend" viewBox="0 0 300 110" preserveAspectRatio="none" aria-label="Progress over time"></svg>
|
||||
<div class="lb-legend"><span><i class="lb-sw" style="background:var(--lb-game)"></i>classified</span><span><i class="lb-sw" style="background:var(--lb-platform)"></i>game code decompiled</span><span><i class="lb-sw" style="background:var(--lb-done)"></i>weighted progress</span></div>
|
||||
<div class="note" id="lbTrendNote" style="margin-top:8px"></div></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="cols">
|
||||
<section class="card" aria-labelledby="h-lbms">
|
||||
<header><h2 id="h-lbms"><svg class="i"><use href="static/icons.svg#flag"/></svg>Milestones</h2></header>
|
||||
<div class="body" id="lbMilestones"></div>
|
||||
</section>
|
||||
<section class="card" aria-labelledby="h-lblog">
|
||||
<header><h2 id="h-lblog"><svg class="i"><use href="static/icons.svg#book"/></svg>Ship’s log</h2><span class="count" id="lbLogCount"></span></header>
|
||||
<div class="body"><ul class="lb-log" id="lbLog"></ul></div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- decomp -->
|
||||
<section class="view" id="view-decomp" aria-label="Decomp">
|
||||
<section class="card" aria-labelledby="h-progress">
|
||||
@@ -757,7 +826,8 @@ const VIEWS={overview:['Overview','Who is working, what needs you, and what happ
|
||||
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'],
|
||||
decomp:['Decomp','Progress by function, the function inspector and what each agent takes next','terminal'],chat:['Chat','The team’s conversation','chat']};
|
||||
decomp:['Decomp','Progress by function, the function inspector and what each agent takes next','terminal'],
|
||||
logbook:['Logbook','Where the decomp stands, how it got here and what comes next','anchor'],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')});
|
||||
@@ -771,6 +841,7 @@ function show(view){if(!VIEWS[view])view='overview';current=view;
|
||||
if(data&&view==='activity'){renderPulse();renderFeed()}
|
||||
if(data&&view==='launch')renderLaunch();
|
||||
if(data&&view==='decomp')renderDecomp();
|
||||
if(data&&view==='logbook')renderLogbook();
|
||||
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')}
|
||||
@@ -1020,10 +1091,10 @@ 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>':''))}
|
||||
put('progress',tierBar(t)+'<div class="lb-legend">'+TIERS.map(([k,l])=>'<span><i class="tier-key tier-'+k+'"></i>'+esc(l)+' '+fmt(t[k].functions)+'</span>').join('')+'</div><div class="note" style="margin-top:10px">Bytes, categories, the trend and the ship’s log: <a href="#logbook" data-go="logbook">open the Logbook</a>.</div>');}
|
||||
function tierTable(t){return '<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>'}
|
||||
function tierBar(t){return '<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>'}
|
||||
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>'
|
||||
@@ -1055,6 +1126,48 @@ async function inspectFn(){const target=el('fnTarget').value.trim();if(!target)r
|
||||
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>')}
|
||||
const LB_CATS=[['game','Game code','Firaxis game logic: decompiled to readable C, then ported to Rust'],['engine','Engine','Gamebryo: named and documented, replaced in the port'],
|
||||
['platform','Platform','Miles, Bink, D3D9 and input wrappers: replaced in the port'],['crt','C runtime','MSVC 7.1 runtime: replaced by Rust’s standard library'],['unsorted','Unsorted','not classified yet; each lands in one of the four above']];
|
||||
let LB=null;
|
||||
const pct=n=>Number(n||0).toFixed(Number(n)>=10?1:2)+'%';
|
||||
const kb=b=>b>=1048576?(b/1048576).toFixed(2)+' MB':Math.round((b||0)/1024)+' KB';
|
||||
const cssVar=n=>getComputedStyle(el('view-logbook')).getPropertyValue(n).trim();
|
||||
async function renderLogbook(){try{LB=await get('api/logbook')}catch(e){put('lbStamp','<span class="err">'+esc(e.message)+'</span>');return}
|
||||
const s=LB.stats||{},cat=k=>(s.by_category||{})[k]||{functions:0,bytes:0};
|
||||
const cur=LB.milestones.find(m=>/progress/i.test(m.state||''));el('lbMilestone').textContent=cur?'Milestone '+cur.code+' · '+cur.title:'Milestones';
|
||||
const au=LB.auto||{};put('lbStamp',(s.updated_at?'Statistics updated <b>'+esc(central(s.updated_at))+'</b><br>main at <b>'+esc(s.commit||'–')+'</b>':'No statistics yet')+(au.last_refresh?'<br>Keeps itself current: last checked '+esc(central(au.last_refresh))+(au.last_error?' · <span class="err">'+esc(String(au.last_error).slice(0,120))+'</span>':''):''));
|
||||
put('lbFigs',[[fmt(s.functions),'functions in the original executable',fmt(s.bytes)+' bytes of code'],[pct(s.classified_percent),'classified (category known)',fmt(cat('unsorted').functions)+' still unsorted'],
|
||||
[pct(s.weighted_percent),'weighted progress','documented 20 · decompiled 40 · reimplemented 75 · verified 100'],[pct(s.game_bytes_decompiled_percent),'of game code decompiled to readable C','Rust: '+pct(s.game_bytes_reimplemented_percent)+' reimplemented']]
|
||||
.map(([v,l,n])=>'<div class="lb-fig"><div class="v">'+esc(v)+'</div><div class="l">'+esc(l)+'</div><div class="n">'+esc(n)+'</div></div>').join(''));
|
||||
put('lbLegend',LB_CATS.map(([k,n])=>'<span><i class="lb-sw'+(k==='unsorted'?' lb-hatch':'')+'" style="background:var(--lb-'+k+')"></i>'+esc(n)+'</span>').join('')+'<span><i class="lb-sw" style="background:var(--lb-done)"></i>decompiled share under each column</span>');
|
||||
drawLbMap();renderLbWhere(s);renderLbTrend(LB.history);renderLbMilestones(LB.milestones);
|
||||
el('lbLogCount').textContent=LB.log_total?LB.log_total+' entries':'';
|
||||
put('lbLog',LB.log.length?LB.log.map(e=>'<li><time>'+esc(central(e.at))+'</time><div><div class="t"><span class="kind">'+esc(e.kind)+'</span>'+esc(e.title)+'</div><div class="d">'+esc(e.detail)+'</div></div></li>').join(''):'<li class="note">No entries yet.</li>')}
|
||||
async function renderLbWhere(s){const total=s.bytes||0,cat=k=>(s.by_category||{})[k]||{functions:0,bytes:0},uns=cat('unsorted'),sorted=total?100*(total-uns.bytes)/total:0;
|
||||
let tiers='';try{const p=await get('api/decomp/progress');tiers='<h3 class="sub-h" style="margin-top:18px">Game functions by how far they have come</h3>'+tierBar(p.tiers)+tierTable(p.tiers)}catch(e){}
|
||||
put('lbWhere','<div><span class="lb-big">'+pct(sorted)+'</span> <span class="note">of the executable’s code is sorted into a category · '+fmt(uns.functions)+' functions ('+kb(uns.bytes)+') still unsorted</span></div>'
|
||||
+'<div class="lb-stack" role="img" aria-label="Code bytes by category">'+LB_CATS.map(([k,n])=>{const sh=total?100*cat(k).bytes/total:0;return '<i class="'+(k==='unsorted'?'lb-hatch':'')+'" style="flex:0 0 '+sh.toFixed(3)+'%;background:var(--lb-'+k+')" title="'+esc(n)+': '+sh.toFixed(1)+'% of bytes"></i>'}).join('')+'</div>'
|
||||
+'<div class="lb-axis"><span>0</span><span>sorted '+pct(sorted)+' · unsorted '+pct(100-sorted)+'</span><span>'+kb(total)+'</span></div>'
|
||||
+LB_CATS.map(([k,n,plan])=>{const c=cat(k),sh=total?100*c.bytes/total:0;return '<div class="lb-cat"><span class="sw'+(k==='unsorted'?' lb-hatch':'')+'" style="background:var(--lb-'+k+')"></span><div><b>'+esc(n)+'</b><div class="plan">'+esc(plan)+'</div></div><div class="nums"><b>'+sh.toFixed(1)+'%</b> of bytes<br>'+fmt(c.functions)+' fn · '+kb(c.bytes)+'</div>'
|
||||
+(k==='game'?'<div class="sub"><div class="tierbar" style="height:8px"><span class="tier-readable" style="flex:'+(s.game_bytes_decompiled_percent||0)+'"></span><span style="flex:'+Math.max(0,100-(s.game_bytes_decompiled_percent||0))+';background:transparent"></span></div><div class="note">'+pct(s.game_bytes_decompiled_percent)+' of game bytes decompiled</div></div>':'')+'</div>'}).join('')
|
||||
+'<div class="note" style="margin-top:10px">Bar and shares are by bytes of code; counts are functions. Only game code is decompiled and ported; the rest is named and replaced.</div>'+tiers)}
|
||||
function drawLbMap(){const m=LB&&LB.map,c=el('lbMap');if(!c||!m||!c.clientWidth)return;const r=window.devicePixelRatio||1,w=c.clientWidth,h=c.clientHeight;
|
||||
c.width=Math.round(w*r);c.height=Math.round(h*r);const g=c.getContext('2d');g.setTransform(r,0,0,r,0,0);g.clearRect(0,0,w,h);
|
||||
const cells=m.cells.map(x=>x.split(',').map(Number)),max=Math.max(1,...cells.map(x=>x.slice(0,5).reduce((a,b)=>a+b,0)));
|
||||
const colors=LB_CATS.map(([k])=>cssVar('--lb-'+k)),done=cssVar('--lb-done'),rule=cssVar('--line'),cw=w/cells.length,base=h-12;
|
||||
g.fillStyle=rule;g.fillRect(0,base,w,1);
|
||||
cells.forEach((x,i)=>{let y=base;for(let k=0;k<5;k++){const hh=x[k]/max*(base-4);if(hh<=0)continue;g.fillStyle=colors[k];g.fillRect(i*cw+.5,y-hh,Math.max(1,cw-1),hh);y-=hh}
|
||||
const tot=x.slice(0,5).reduce((a,b)=>a+b,0);if(tot>0&&x[5]>0){g.fillStyle=done;g.fillRect(i*cw+.5,base+4,Math.max(1,(cw-1)*Math.min(1,x[5]/tot)),6)}});
|
||||
el('lbLow').textContent=m.low;el('lbHigh').textContent=m.high}
|
||||
function renderLbTrend(points){const svg=el('lbTrend');if(!points.length){svg.innerHTML='';el('lbTrendNote').textContent='The trend starts with the second report.';return}
|
||||
const n=points.length,x=i=>n===1?150:i*300/(n-1),y=v=>104-Math.min(100,v||0)/100*96,path=k=>points.map((p,i)=>(i?'L':'M')+x(i).toFixed(1)+','+y(p[k]).toFixed(1)).join(' ');
|
||||
svg.innerHTML=[0,25,50,75,100].map(v=>'<line x1="0" x2="300" y1="'+y(v)+'" y2="'+y(v)+'" stroke="var(--line)" stroke-width="0.5"/>').join('')
|
||||
+[['classified','--lb-game'],['decompiled','--lb-platform'],['weighted','--lb-done']].map(([k,c])=>'<path d="'+path(k)+'" fill="none" stroke="var('+c+')" stroke-width="2" vector-effect="non-scaling-stroke"/>').join('');
|
||||
el('lbTrendNote').textContent=n+' reports since '+new Date(points[0].at).toLocaleDateString()+'. Scale 0–100%.'}
|
||||
function renderLbMilestones(list){let last='';put('lbMilestones',list.length?list.map(m=>{const head=m.phase&&m.phase!==last?'<div class="lb-phase">'+esc(m.phase)+'</div>':'';last=m.phase||last;
|
||||
const items=m.items||[],done=items.filter(i=>i.done).length,li=items.map(i=>'<li class="'+(i.done?'done':'')+'">'+esc(i.text)+'</li>').join(''),open=/progress/i.test(m.state||'');
|
||||
return head+'<div class="lb-ms"><h3><span class="code">'+esc(m.code)+'</span>'+esc(m.title)+'<span class="note">'+done+'/'+items.length+(m.state?' · '+esc(m.state):'')+'</span></h3>'+(open?'<ul>'+li+'</ul>':'<details data-k="ms-'+esc(m.id)+'"><summary class="note">Show '+items.length+' steps</summary><ul>'+li+'</ul></details>')+'</div>'}).join(''):empty('flag','No milestones yet',''))}
|
||||
window.addEventListener('resize',()=>{if(current==='logbook')drawLbMap()});
|
||||
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'})});
|
||||
@@ -1304,7 +1417,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(current==='decomp')renderDecomp();if(current==='health')renderTools();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==='logbook')renderLogbook();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);
|
||||
|
||||
Reference in new issue
Block a user