From 1e2b03c795a008cea8192ddfb254e3790b462a8a Mon Sep 17 00:00:00 2001 From: martbost Date: Thu, 13 Aug 2026 10:36:36 -0500 Subject: [PATCH] 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 --- chain.js | 4 +-- public/admin.html | 2 +- public/admin.js | 72 ++++++++++++++++++++++++++++++++++++++--------- public/styles.css | 10 +++++++ 4 files changed, 72 insertions(+), 16 deletions(-) diff --git a/chain.js b/chain.js index edc675a..6c68fd0 100644 --- a/chain.js +++ b/chain.js @@ -332,8 +332,8 @@ function getMatrixTree() { id, tier: m.tier, tierName: tierName(m.tier), level: m.level, levelName: levelName(m.level), directCount: m.directCount || 0, earnedPol: m.earnedPol || 0, referrerId: m.referrerId, joinedAt: m.joinedAt }; - const l = node(m.l, depth + 1), r = node(m.r, depth + 1); - if (l || r) n.children = [l, r].filter(Boolean); + n.left = node(m.l, depth + 1); + n.right = node(m.r, depth + 1); return n; } const root = node(1, 0); diff --git a/public/admin.html b/public/admin.html index 29c4821..99ef2fb 100644 --- a/public/admin.html +++ b/public/admin.html @@ -5,7 +5,7 @@

Traffic & Conversions

First-touch source per visitor session (referring domain or utm_source). Funnel: bridge page → start page → join click.

SourceBridge viewsStart viewsTraining viewsJoin clicksStart → Join

Member ID Submissions

New members who confirmed their purchase on the start page. Each one was posted to your Hermes Telegram chat — add them to the rotation.

WhenName / HandleNew IDJoined underSourceOn-chain

On-Chain Member Lookup

Enter an RM Circle ID to read its registration, lineage, and every payment it has received — live from the smart contract.

-

Matrix Tree

The entire on-chain matrix from the root down — who landed where, with tier, level, directs, and earnings per position.

+

Matrix View

The entire on-chain matrix — who landed where, with tier, level, directs, and earnings per position. Click a position to drill down.

Add Sponsor

AI Chat

Paste an OpenRouter API key to switch the help chat from canned answers to AI (). Clear it to switch back.

Public Page Settings

diff --git a/public/admin.js b/public/admin.js index 18fc000..02eb1ad 100644 --- a/public/admin.js +++ b/public/admin.js @@ -66,24 +66,70 @@ document.getElementById('lookupForm').addEventListener('submit',async e=>{ out.innerHTML=facts+chain+`

Payments received by #${d.id} — ${d.income?d.income.length:0} total, newest first:

`+income; }catch(x){out.innerHTML=`
${esc(x.message)}
`} }); +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?'P':'S'} +const mtFmt=n=>Number(n||0).toLocaleString(undefined,{maximumFractionDigits:0}); +function mtCard(n){ + if(!n)return '
open
slot
'; + return ``; +} +function renderPyramid(){ + const out=document.getElementById('matrixTree'),nav=document.getElementById('matrixNav'); + const root=matrixUI.byId[matrixUI.rootId]; + if(!root){out.innerHTML='
Position not found.
';return} + const GEN=4; // root + 3 generations = up to 15 cards + const rows=[[root]]; + for(let g=1;gn?[n.left||null,n.right||null]:[null,null])); + } + out.innerHTML=`
${rows.map(r=>`
${r.map(mtCard).join('')}
`).join('')}
`; + // 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=`Viewing: `+path.map((id,i)=>i===path.length-1?`#${id}`:``).join(' › ')+ + (matrixUI.parent[matrixUI.rootId]?` `:''); + 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 `
  • #${n.id} (no data)
  • `; + const ch=[n.left,n.right].filter(Boolean); + const kids=ch.length?`
      ${ch.map(c=>node(c,depth+1)).join('')}
    `:''; + const open=depth<4?' open':''; + const label=`${mtBadge(n)} #${n.id} ${esc(n.levelName)} · ${n.directCount}/2 directs · ${mtFmt(n.earnedPol)} POL earned${n.referrerId?` · ref #${n.referrerId}`:''}`; + return ch.length?`
  • ${label}${kids}
  • `:`
  • ${label}
  • `; + }; + const unplaced=d.unplaced?`

    Not reachable from root: ${d.unplaced.map(i=>'#'+i).join(', ')}

    `:''; + out.innerHTML=`
      ${node(d.root,0)}
    ${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',`

    ${head}

    `); +} document.getElementById('treeLoadBtn').addEventListener('click',async()=>{ const out=document.getElementById('matrixTree'); - out.innerHTML='
    Building tree from the on-chain snapshot…
    '; + out.innerHTML='
    Building matrix from the on-chain snapshot…
    '; try{ const d=await api('/api/admin/matrix-tree'); if(!d.ready||!d.root){out.innerHTML='
    Snapshot not ready yet — try again in a minute.
    ';return} - const fmt=n=>Number(n).toLocaleString(undefined,{maximumFractionDigits:0}); - const node=(n,depth)=>{ - if(!n)return ''; - if(n.missing)return `
  • #${n.id} (no data)
  • `; - const badge=n.tier===2?'P':'S'; - const kids=n.children&&n.children.length?`
      ${n.children.map(c=>node(c,depth+1)).join('')}
    `:''; - const open=depth<4?' open':''; - const label=`${badge} #${n.id} ${esc(n.levelName)} · ${n.directCount}/2 directs · ${fmt(n.earnedPol)} POL earned${n.referrerId?` · ref #${n.referrerId}`:''}`; - return n.children&&n.children.length?`
  • ${label}${kids}
  • `:`
  • ${label}
  • `; - }; - const unplaced=d.unplaced?`

    Not reachable from root: ${d.unplaced.map(i=>'#'+i).join(', ')}

    `:''; - out.innerHTML=`

    ${d.memberCount} positions · snapshot ${new Date(d.snapshotAt).toISOString().replace('T',' ').slice(0,16)} UTC · P = Premium, S = Standard

      ${node(d.root,0)}
    ${unplaced}`; + matrixUI.data=d;matrixUI.byId={};matrixUI.parent={};matrixUI.rootId=d.root.id; + mtIndex(d.root,null); + renderMatrix(); }catch(x){out.innerHTML=`
    ${esc(x.message)}
    `} }); +document.getElementById('treeToggleBtn').addEventListener('click',()=>{ + matrixUI.mode=matrixUI.mode==='pyramid'?'list':'pyramid'; + if(matrixUI.data)renderMatrix(); +}); loadState(); diff --git a/public/styles.css b/public/styles.css index 2f7d8fe..9c85f41 100644 --- a/public/styles.css +++ b/public/styles.css @@ -82,3 +82,13 @@ a{color:inherit}.wrap{width:min(1160px,calc(100% - 32px));margin:auto}.nav{heigh .mt-meta{color:var(--muted);font-size:12px} .mt-badge{display:inline-block;width:16px;height:16px;line-height:16px;text-align:center;border-radius:5px;background:#183952;color:var(--teal);font-size:10px;font-weight:900} .mt-badge.mt-prem{background:rgba(243,190,67,.15);color:var(--gold)} + +/* Admin matrix pyramid view */ +.mtp{display:flex;flex-direction:column;gap:14px;overflow-x:auto;padding:6px 2px 10px} +.mtp-row{display:flex;justify-content:center;gap:8px;min-width:max-content;margin:0 auto} +.mtp-card{appearance:none;font:inherit;cursor:pointer;text-align:center;background:#0d2236;border:1px solid #2a4a66;border-radius:12px;padding:9px 10px;min-width:104px;color:var(--text);display:flex;flex-direction:column;gap:2px;align-items:center;transition:.15s ease} +.mtp-card:hover{border-color:var(--teal);transform:translateY(-1px)} +.mtp-focus{border-color:var(--gold);box-shadow:0 0 0 1px var(--gold),0 8px 22px rgba(243,190,67,.12)} +.mtp-open{background:transparent;border:1px dashed #27445e;color:#5f7891;font-size:11px;justify-content:center;cursor:default;line-height:1.3} +.mtp-lvl{font-size:11px;color:var(--teal);text-transform:uppercase;letter-spacing:.5px;font-weight:800} +.mtp-sub{font-size:11px;color:var(--muted)}