From 6395ad3a7ec589d37c460e178c30fb6db8449d53 Mon Sep 17 00:00:00 2001 From: martbost Date: Sat, 12 Sep 2026 16:03:43 -0500 Subject: [PATCH] Dashboard: community toasts for joins, tank arrivals, adoptions, purchases, payouts and qualifications A second, quieter toast (bottom-left, one per 4 s) shows site-wide activity from the live feed for every signed-in member; personal payout toasts are unchanged. The server pushes Joined (at first username, with tank flag) and Adopted events onto the feed. Co-Authored-By: Claude Fable 5.1 --- public/assets/my.js | 28 +++++++++++++++++++++++++++- public/my.html | 2 +- server.js | 3 +++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/public/assets/my.js b/public/assets/my.js index 75b8c06..ca8460d 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -275,8 +275,34 @@ clearTimeout(liveRefreshT); liveRefreshT = setTimeout(() => { loadDashboard(); try { loadLineage(); } catch (e) {} }, 600); } + // community toasts (Marty, 2026-09-12: keep the dashboard feeling alive): everyone else's joins, + // purchases, payouts, qualifications, tank arrivals and adoptions, in a second, quieter toast so + // they never replace a personal one. At most one every 4 s; a burst shows the latest. + let liveT = 0, liveQ = null; + function communityToast(msg) { + const now = Date.now(); + if (now - liveT < 4000) { liveQ = msg; if (!communityToast._q) communityToast._q = setTimeout(() => { communityToast._q = null; const m = liveQ; liveQ = null; if (m) communityToast(m); }, 4200 - (now - liveT)); return; } + liveT = now; + let el = document.getElementById('liveToast'); + if (!el) { el = document.createElement('div'); el.id = 'liveToast'; el.setAttribute('role', 'status'); el.style.cssText = 'position:fixed;left:16px;bottom:16px;z-index:90;max-width:min(360px,calc(100vw - 32px));background:var(--panel-solid);border:1px solid var(--line-strong);border-left:3px solid var(--mint);border-radius:12px;padding:10px 14px;font-size:14px;box-shadow:0 10px 30px rgba(0,0,0,.4);transition:opacity .3s'; document.body.appendChild(el); } + el.textContent = msg; el.hidden = false; el.style.opacity = '1'; + clearTimeout(communityToast._t); communityToast._t = setTimeout(() => { el.style.opacity = '0'; setTimeout(() => { el.hidden = true; }, 350); }, 6000); + } function handleLiveEvent(ev) { - if (!MYID || !ev || !ev.type) return; + if (!ev || !ev.type) return; + const nm = id => (ev.names && ev.names[id]) ? '@' + ev.names[id] : 'member #' + id; + // site-wide activity (not about me): joins, tank, adoptions, purchases, payouts, qualifications + if (ev.type === 'Joined') { communityToast('๐Ÿ‘‹ ' + ev.name + ' just joined' + (ev.tank ? ' and is waiting for a sponsor in the holding tank' : '')); liveRefresh(); return; } + if (ev.type === 'Adopted') { communityToast('๐Ÿค ' + ev.sponsor + ' picked up ' + ev.member + ' from the holding tank'); liveRefresh(); return; } + const mine = MYID && (ev.recipientId === MYID || ev.toId === MYID || ev.sponsorId === MYID || ev.skippedId === MYID || ev.buyerId === MYID); + if (!mine) { + if (ev.type === 'Purchase' && ev.buyerId) communityToast('๐Ÿงพ ' + nm(ev.buyerId) + ' just bought a $' + Math.round((ev.priceCents || 0) / 100) + ' package'); + else if (ev.type === 'TierPaid' && ev.recipientId) communityToast('๐Ÿ’ธ ' + nm(ev.recipientId) + ' just got paid ' + IAP.fmtPol(ev.amountWei) + ' POL'); + else if (ev.type === 'BuyerCounted' && ev.sponsorId) communityToast('๐ŸŽฏ ' + nm(ev.sponsorId) + ' now has ' + ev.newCount + ' qualifying buyer' + (ev.newCount === 1 ? '' : 's')); + else if (ev.type === 'MemberActivated' && ev.id) communityToast('โšก ' + nm(ev.id) + ' switched on payouts'); + return; + } + if (!MYID) return; let toast = null, kind = 'ok'; let sound = null; if (ev.type === 'TierPaid' && ev.recipientId === MYID) { toast = '๐Ÿ’ธ You earned a level-' + ev.tier + ' payout of ' + IAP.fmtPol(ev.amountWei) + ' POL!'; sound = 'chaching'; } diff --git a/public/my.html b/public/my.html index 6f68605..858a7d7 100644 --- a/public/my.html +++ b/public/my.html @@ -915,7 +915,7 @@ - + diff --git a/server.js b/server.js index 50c6d6e..5d02156 100644 --- a/server.js +++ b/server.js @@ -1130,6 +1130,7 @@ const server = http.createServer(async (req, res) => { const b = await readBody(req); const r = await tank.adopt(s.email, b.who, b.note); if (r.ok) { // tell the payments topic who picked whom up (Marty, 2026-09-12) + pushFeed({ type: 'Adopted', sponsor: r.adopterName, member: r.adopteeName, ts: Date.now() }); try { const sc = siteConfig(); const clean = t => String(t || '').replace(/[<>&]/g, ''); @@ -1235,6 +1236,8 @@ const server = http.createServer(async (req, res) => { if (!r.error && before && !before.username && b.username) { const acct = await accounts.byEmail(s.email); notifyNewReferral(acct && acct.sponsorRef, acct).catch(() => {}); + // everyone's dashboard: "@name just joined" (and whether they are waiting in the tank) + if (acct && acct.username) pushFeed({ type: 'Joined', name: '@' + acct.username, tank: !acct.sponsorRef && !acct.memberId, ts: Date.now() }); } return json(res, r.error ? 400 : 200, r); }