diff --git a/public/admin.js b/public/admin.js
index 800103c..5356aae 100644
--- a/public/admin.js
+++ b/public/admin.js
@@ -98,12 +98,20 @@ async function loadCoaching(){
if(!d.ready){out.innerHTML='
Chain snapshot not ready โ try again in a minute.
';return}
const nm=r=>r.name?`${esc(r.name)} (#${r.id})`:`#${r.id}`;
const link=r=>`${nm(r)}`;
+ const mineRisk=d.atRisk.filter(r=>r.owned),mineRoll=d.rollForward.filter(r=>r.owned);
+ d.atRisk=d.atRisk.filter(r=>!r.owned);d.rollForward=d.rollForward.filter(r=>!r.owned);d.oneAway=d.oneAway.filter(r=>!r.owned);
+ const mineHtml=(mineRisk.length||mineRoll.length)?
+ `๐ Your positions โ your own action items
`+
+ mineRisk.map(r=>`๐
${link(r)} โ ${esc(r.levelName)}, ${fmt(r.atRiskPol)} POL forming; needs ${esc(r.neededLevelName||'qualification')} to catch it${r.earnedPol>=(r.upgradeCost||0)&&r.upgradeCost?' โ next step covered by its earnings':r.upgradeCost?` (earned ${fmt(r.earnedPol)} vs ${fmt(r.upgradeCost)} next step)`:''}.
`).join('')+
+ mineRoll.map(r=>`๐
${link(r)} โ qualified at Scintilla, Ascensus (${fmt(r.ascensusCost)} POL) covered by its ${fmt(r.earnedPol)} POL earned, whenever you choose.
`).join('')
+ :'';
const t1=d.rollForward.map(r=>`๐ฌ
${link(r)} โ qualified, still Scintilla, sitting on ${fmt(r.earnedPol)} POL (Ascensus costs ${fmt(r.ascensusCost)}). One message: โyour entry rewards already cover the upgrade that catches your team's first payments.โ
`).join('')||'Nobody stuck at qualified-Scintilla. ๐
';
const t2=d.atRisk.filter(r=>r.qualified&&r.level>1).map(r=>`โ ๏ธ
${link(r)} โ ${esc(r.levelName)}, needs ${esc(r.neededLevelName)} to catch ${fmt(r.atRiskPol)} POL forming under them (from ${r.fromIds.map(f=>'#'+f).join(', ')}).
`).join('')||'Everyone leveled is ahead of their wave. ๐
';
const t3=d.oneAway.map(r=>`๐ฏ
${link(r)} โ 1/2 directs, one placement from qualifying.
`).join('')||'Nobody stranded at 1/2.
';
const unq=d.atRisk.filter(r=>!r.qualified).map(r=>`โฐ
${link(r)} โ NOT qualified (${r.directCount}/2) with ${fmt(r.atRiskPol)} POL already forming below. Qualification is urgent for them.
`).join('');
out.innerHTML=
`Members scanned${d.scanned}
POL at risk${fmt(d.totals.atRiskPol)}
Easy wins${d.totals.rollForwardCount}
One direct away${d.totals.oneAwayCount}
`+
+ mineHtml+
`Tier 1 ยท Easy wins โ one message each
${t1}`+
`Tier 2 ยท Money at risk โ coach the upgrade
${t2}`+
(unq?`Tier 2b ยท Money forming, not yet qualified
${unq}`:'')+
diff --git a/server.js b/server.js
index 9efeff5..4a5212f 100644
--- a/server.js
+++ b/server.js
@@ -516,7 +516,8 @@ async function handleApi(req,res,pathname){
const names={};
try{for(const s of getSponsors())names[String(s.id)]=s.name;}catch(e){}
try{for(const s of readJson(SUBMISSIONS_FILE))if(s.newId&&!names[String(s.newId)])names[String(s.newId)]=s.memberName;}catch(e){}
- for(const list of [d.atRisk,d.rollForward,d.oneAway])for(const r of list)if(names[String(r.id)])r.name=names[String(r.id)];
+ const ownedIds=new Set(parseOwnerIds());
+ for(const list of [d.atRisk,d.rollForward,d.oneAway])for(const r of list){if(names[String(r.id)])r.name=names[String(r.id)];if(ownedIds.has(Number(r.id)))r.owned=true;}
}
return json(res,200,d);
}