← back to Build Feature Coverage

build-viewer.py

98 lines

import json, os
DEST=os.path.expanduser("~/Projects/build-feature-coverage")
d=json.load(open(f"{DEST}/matrix.json"))
feats=d["axis_features"]; mat=d["matrix"]; meta=d["site_meta"]; ev=d["evidence"]
fnames=[f["name"] for f in feats]
grp_of={f["name"]:f["group"] for f in feats}
# order features by group then adoption
adopt={f:sum(1 for s in mat if mat[s][f] is True) for f in fnames}
GROUPS=["drop-in UX primitives","_shared components","storefront standing features","SEO / meta / page"]
ordered=sorted(fnames,key=lambda f:(GROUPS.index(grp_of[f]),-adopt[f]))
sites=sorted(mat.keys(),key=lambda s:-sum(1 for f in fnames if mat[s][f] is True))

rows=[]
for s in sites:
    cells=[]
    cnt=sum(1 for f in fnames if mat[s][f] is True)
    for f in ordered:
        v=mat[s][f]
        state="yes" if v is True else ("nm" if v is None else "no")
        title=ev.get(s,{}).get(f,"") if v is True else ("no local repo" if v is None else "")
        cells.append({"f":f,"state":state,"t":title})
    rows.append({"site":s,"live":meta[s]["live"],"count":cnt,"cells":cells})

payload={"generatedAt":d["generatedAt"],"features":[{"name":f,"group":grp_of[f],"adopt":adopt[f]} for f in ordered],
         "rows":rows,"nSites":len(sites),"nFeat":len(fnames)}

html="""<!doctype html><html lang=en><head><meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>Build-Feature Coverage</title>
<style>
:root{--bg:#0f1115;--fg:#e8e8ea;--mut:#9aa0aa;--yes:#16a34a;--no:#242731;--nm:#3a2a12;--line:#242731;--accent:#00a8ff;--cell:15px}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:13px/1.4 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}
header{padding:14px 16px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--bg);z-index:5}
h1{margin:0 0 4px;font-size:16px}.sub{color:var(--mut);font-size:12px}
.controls{display:flex;gap:16px;align-items:center;flex-wrap:wrap;margin-top:10px}
.controls label{color:var(--mut);font-size:12px;display:flex;gap:6px;align-items:center}
select,input[type=range]{accent-color:var(--accent)}
.legend{display:flex;gap:12px;font-size:11px;color:var(--mut);align-items:center}
.sw{display:inline-block;width:11px;height:11px;border-radius:2px;vertical-align:-1px;margin-right:3px}
.wrap{overflow:auto;max-width:100vw}
table{border-collapse:collapse;margin:8px}
th,td{padding:0;text-align:center}
th.col{writing-mode:vertical-rl;transform:rotate(180deg);white-space:nowrap;font-weight:500;font-size:10px;color:var(--mut);height:120px;vertical-align:bottom;padding-bottom:4px}
th.col .g0{color:#ff7ab6}th.col .g1{color:#7ab6ff}th.col .g2{color:#ffd479}th.col .g3{color:#8fe388}
td.site{position:sticky;left:0;background:var(--bg);text-align:right;padding:0 8px;white-space:nowrap;font-size:11px;border-right:1px solid var(--line);z-index:2}
td.cnt{padding:0 6px;color:var(--mut);font-variant-numeric:tabular-nums}
.cell{width:var(--cell);height:var(--cell);border:1px solid var(--bg)}
.yes{background:var(--yes)}.no{background:var(--no)}.nm{background:var(--nm)}
.dead{opacity:.5}
.grouphdr td{background:#161922;color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.5px;text-align:left;padding:3px 8px}
</style></head><body>
<header>
<h1>Build-Feature Coverage Matrix <span class=sub id=meta></span></h1>
<div class=sub>TK-12025 · which fleet properties run which build-feature skills · read-only distribution audit</div>
<div class=controls>
 <label>Sort <select id=sort>
   <option value=count>Feature count ↓</option>
   <option value=name>Site A→Z</option>
   <option value=live>Live state</option>
 </select></label>
 <label>Density <input type=range id=dens min=9 max=26 value=15></label>
 <div class=legend><span><span class="sw yes"></span>runs</span><span><span class="sw no"></span>absent</span><span><span class="sw nm"></span>not-measured</span></div>
</div></header>
<div class=wrap><table id=t></table></div>
<script>
const D=__PAYLOAD__;
document.getElementById('meta').textContent='· '+D.nSites+' sites × '+D.nFeat+' features · '+D.generatedAt;
const GLAB={'drop-in UX primitives':'g0','_shared components':'g1','storefront standing features':'g2','SEO / meta / page':'g3'};
function render(){
 const sort=document.getElementById('sort').value;
 let rows=[...D.rows];
 if(sort==='name')rows.sort((a,b)=>a.site.localeCompare(b.site));
 else if(sort==='live')rows.sort((a,b)=>(a.live||'').localeCompare(b.live||'')||b.count-a.count);
 else rows.sort((a,b)=>b.count-a.count);
 let h='<thead><tr><th class=site>site</th><th class=cnt>#</th>';
 for(const f of D.features){h+='<th class=col title="'+f.group+' · '+f.adopt+'/'+D.nSites+'"><span class="'+GLAB[f.group]+'">'+f.name+' ('+f.adopt+')</span></th>';}
 h+='</tr></thead><tbody>';
 for(const r of rows){
   const dead=r.count===0?' dead':'';
   h+='<tr class="'+dead+'"><td class=site title="'+r.live+'">'+r.site+'</td><td class=cnt>'+r.count+'</td>';
   for(const c of r.cells){h+='<td><div class="cell '+c.state+'" title="'+r.site+' · '+c.f+(c.t?' · '+c.t:'')+'"></div></td>';}
   h+='</tr>';
 }
 h+='</tbody>';
 document.getElementById('t').innerHTML=h;
}
document.getElementById('sort').onchange=render;
document.getElementById('dens').oninput=e=>{document.documentElement.style.setProperty('--cell',e.target.value+'px');};
try{const s=localStorage.getItem('bfc-sort');if(s){document.getElementById('sort').value=s;}
    const dv=localStorage.getItem('bfc-dens');if(dv){document.getElementById('dens').value=dv;document.documentElement.style.setProperty('--cell',dv+'px');}}catch(e){}
document.getElementById('sort').addEventListener('change',e=>{try{localStorage.setItem('bfc-sort',e.target.value)}catch(e){}});
document.getElementById('dens').addEventListener('change',e=>{try{localStorage.setItem('bfc-dens',e.target.value)}catch(e){}});
render();
</script></body></html>"""
html=html.replace("__PAYLOAD__",json.dumps(payload))
open(f"{DEST}/coverage-matrix.html","w").write(html)
print("wrote coverage-matrix.html", os.path.getsize(f"{DEST}/coverage-matrix.html"),"bytes")