Render admin matrix as a drill-down pyramid with open slots
Matrix View now defaults to a generation-by-generation pyramid (root + 3 generations of position cards, empty placements shown as dashed open slots). Clicking a position re-roots the pyramid there; a breadcrumb navigates back up. The collapsible list stays behind a toggle. Tree payload now carries explicit left/right children so an only-child lands on the correct side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+59
-13
@@ -66,24 +66,70 @@ document.getElementById('lookupForm').addEventListener('submit',async e=>{
|
||||
out.innerHTML=facts+chain+`<p style="font-size:12px;color:#8498aa;margin:14px 0 0">Payments received by #${d.id} — ${d.income?d.income.length:0} total, newest first:</p>`+income;
|
||||
}catch(x){out.innerHTML=`<div class="empty" style="color:var(--danger)">${esc(x.message)}</div>`}
|
||||
});
|
||||
const matrixUI={data:null,byId:{},parent:{},rootId:1,mode:'pyramid'};
|
||||
function mtIndex(n,par){if(!n)return;matrixUI.byId[n.id]=n;if(par)matrixUI.parent[n.id]=par.id;mtIndex(n.left,n);mtIndex(n.right,n)}
|
||||
function mtBadge(n){return n.tier===2?'<span class="mt-badge mt-prem">P</span>':'<span class="mt-badge">S</span>'}
|
||||
const mtFmt=n=>Number(n||0).toLocaleString(undefined,{maximumFractionDigits:0});
|
||||
function mtCard(n){
|
||||
if(!n)return '<div class="mtp-card mtp-open">open<br>slot</div>';
|
||||
return `<button class="mtp-card${n.id===matrixUI.rootId?' mtp-focus':''}" data-mid="${n.id}">${mtBadge(n)} <span class="mt-id">#${n.id}</span><span class="mtp-lvl">${esc(n.levelName)}</span><span class="mtp-sub">${n.directCount}/2 · ${mtFmt(n.earnedPol)} POL</span></button>`;
|
||||
}
|
||||
function renderPyramid(){
|
||||
const out=document.getElementById('matrixTree'),nav=document.getElementById('matrixNav');
|
||||
const root=matrixUI.byId[matrixUI.rootId];
|
||||
if(!root){out.innerHTML='<div class="empty">Position not found.</div>';return}
|
||||
const GEN=4; // root + 3 generations = up to 15 cards
|
||||
const rows=[[root]];
|
||||
for(let g=1;g<GEN;g++){
|
||||
rows.push(rows[g-1].flatMap(n=>n?[n.left||null,n.right||null]:[null,null]));
|
||||
}
|
||||
out.innerHTML=`<div class="mtp">${rows.map(r=>`<div class="mtp-row">${r.map(mtCard).join('')}</div>`).join('')}</div>`;
|
||||
// breadcrumb: path from tree root down to current focus
|
||||
const path=[];let cur=matrixUI.rootId;
|
||||
while(cur){path.unshift(cur);cur=matrixUI.parent[cur]}
|
||||
nav.classList.remove('hidden');
|
||||
nav.innerHTML=`<span class="micro" style="margin:0">Viewing:</span> `+path.map((id,i)=>i===path.length-1?`<strong class="mt-id">#${id}</strong>`:`<button class="btn btn-secondary btn-sm" data-mid="${id}">#${id}</button>`).join(' <span style="color:var(--muted)">›</span> ')+
|
||||
(matrixUI.parent[matrixUI.rootId]?` <button class="btn btn-secondary btn-sm" data-mid="${matrixUI.parent[matrixUI.rootId]}">↑ Up one</button>`:'');
|
||||
out.querySelectorAll('[data-mid]').forEach(b=>b.addEventListener('click',()=>{matrixUI.rootId=Number(b.dataset.mid);renderPyramid()}));
|
||||
nav.querySelectorAll('[data-mid]').forEach(b=>b.addEventListener('click',()=>{matrixUI.rootId=Number(b.dataset.mid);renderPyramid()}));
|
||||
}
|
||||
function renderList(){
|
||||
const out=document.getElementById('matrixTree'),d=matrixUI.data;
|
||||
document.getElementById('matrixNav').classList.add('hidden');
|
||||
const node=(n,depth)=>{
|
||||
if(!n)return '';
|
||||
if(n.missing)return `<li class="mt-node"><span class="mt-id">#${n.id}</span> <span style="color:var(--danger)">(no data)</span></li>`;
|
||||
const ch=[n.left,n.right].filter(Boolean);
|
||||
const kids=ch.length?`<ul class="mt-kids">${ch.map(c=>node(c,depth+1)).join('')}</ul>`:'';
|
||||
const open=depth<4?' open':'';
|
||||
const label=`${mtBadge(n)} <span class="mt-id">#${n.id}</span> <span class="mt-meta">${esc(n.levelName)} · ${n.directCount}/2 directs · ${mtFmt(n.earnedPol)} POL earned${n.referrerId?` · ref #${n.referrerId}`:''}</span>`;
|
||||
return ch.length?`<li class="mt-node"><details${open}><summary>${label}</summary>${kids}</details></li>`:`<li class="mt-node mt-leaf">${label}</li>`;
|
||||
};
|
||||
const unplaced=d.unplaced?`<p class="micro" style="color:var(--danger)">Not reachable from root: ${d.unplaced.map(i=>'#'+i).join(', ')}</p>`:'';
|
||||
out.innerHTML=`<ul class="mt-tree">${node(d.root,0)}</ul>${unplaced}`;
|
||||
}
|
||||
function renderMatrix(){
|
||||
const d=matrixUI.data;
|
||||
const head=`${d.memberCount} positions · snapshot ${new Date(d.snapshotAt).toISOString().replace('T',' ').slice(0,16)} UTC · P = Premium, S = Standard`;
|
||||
document.getElementById('treeToggleBtn').classList.remove('hidden');
|
||||
document.getElementById('treeToggleBtn').textContent=matrixUI.mode==='pyramid'?'List view':'Pyramid view';
|
||||
if(matrixUI.mode==='pyramid')renderPyramid();else renderList();
|
||||
const out=document.getElementById('matrixTree');
|
||||
out.insertAdjacentHTML('afterbegin',`<p class="micro" style="margin:0 0 10px">${head}</p>`);
|
||||
}
|
||||
document.getElementById('treeLoadBtn').addEventListener('click',async()=>{
|
||||
const out=document.getElementById('matrixTree');
|
||||
out.innerHTML='<div class="empty">Building tree from the on-chain snapshot…</div>';
|
||||
out.innerHTML='<div class="empty">Building matrix from the on-chain snapshot…</div>';
|
||||
try{
|
||||
const d=await api('/api/admin/matrix-tree');
|
||||
if(!d.ready||!d.root){out.innerHTML='<div class="empty">Snapshot not ready yet — try again in a minute.</div>';return}
|
||||
const fmt=n=>Number(n).toLocaleString(undefined,{maximumFractionDigits:0});
|
||||
const node=(n,depth)=>{
|
||||
if(!n)return '';
|
||||
if(n.missing)return `<li class="mt-node"><span class="mt-id">#${n.id}</span> <span style="color:var(--danger)">(no data)</span></li>`;
|
||||
const badge=n.tier===2?'<span class="mt-badge mt-prem">P</span>':'<span class="mt-badge">S</span>';
|
||||
const kids=n.children&&n.children.length?`<ul class="mt-kids">${n.children.map(c=>node(c,depth+1)).join('')}</ul>`:'';
|
||||
const open=depth<4?' open':'';
|
||||
const label=`${badge} <span class="mt-id">#${n.id}</span> <span class="mt-meta">${esc(n.levelName)} · ${n.directCount}/2 directs · ${fmt(n.earnedPol)} POL earned${n.referrerId?` · ref #${n.referrerId}`:''}</span>`;
|
||||
return n.children&&n.children.length?`<li class="mt-node"><details${open}><summary>${label}</summary>${kids}</details></li>`:`<li class="mt-node mt-leaf">${label}</li>`;
|
||||
};
|
||||
const unplaced=d.unplaced?`<p class="micro" style="color:var(--danger)">Not reachable from root: ${d.unplaced.map(i=>'#'+i).join(', ')}</p>`:'';
|
||||
out.innerHTML=`<p class="micro" style="margin:0 0 8px">${d.memberCount} positions · snapshot ${new Date(d.snapshotAt).toISOString().replace('T',' ').slice(0,16)} UTC · P = Premium, S = Standard</p><ul class="mt-tree">${node(d.root,0)}</ul>${unplaced}`;
|
||||
matrixUI.data=d;matrixUI.byId={};matrixUI.parent={};matrixUI.rootId=d.root.id;
|
||||
mtIndex(d.root,null);
|
||||
renderMatrix();
|
||||
}catch(x){out.innerHTML=`<div class="empty" style="color:var(--danger)">${esc(x.message)}</div>`}
|
||||
});
|
||||
document.getElementById('treeToggleBtn').addEventListener('click',()=>{
|
||||
matrixUI.mode=matrixUI.mode==='pyramid'?'list':'pyramid';
|
||||
if(matrixUI.data)renderMatrix();
|
||||
});
|
||||
loadState();
|
||||
|
||||
Reference in New Issue
Block a user