diff --git a/chain.js b/chain.js
index 8d6bb7d..9434c16 100644
--- a/chain.js
+++ b/chain.js
@@ -194,6 +194,7 @@ async function tail() {
for (const lg of logs) {
const ev = decodeLog(lg);
if (!ev) continue;
+ ev.ts = Date.now(); // indexed-at time: powers honest time-series charts
state.events.push(ev);
tally(ev);
if (onEvent) { try { onEvent(ev); } catch (e) { console.error('chain onEvent', e.message); } }
diff --git a/public/assets/my.js b/public/assets/my.js
index 606fb64..f928a3e 100644
--- a/public/assets/my.js
+++ b/public/assets/my.js
@@ -26,6 +26,86 @@
if (d.buyerCount < 5) return 'Level 2 is open. ' + (5 - d.buyerCount) + ' more qualifying buyer(s) unlock level 3 and the full three-level flow.';
return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.';
}
+ // ── overview v3 charts: hand-rolled SVG/CSS, real data only ──
+ const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
+ const CH = { mint: '#43e8c3', cyan: '#54ccff', violet: '#9d7dff', amber: '#ffb238', track: 'rgba(139,166,156,.18)' };
+ function bars(el, xel, data) { // data: [{v,label,tip,alt}]
+ if (!el) return;
+ const max = Math.max(1, ...data.map(d => d.v));
+ el.innerHTML = data.length
+ ? data.map(d => '
').join('')
+ : '
'.repeat(8);
+ if (xel) xel.innerHTML = data.map(d => '' + esc(d.label) + ' ').join('');
+ }
+ function donut(el, segs, center) { // segs sum to the ring; r=15.9155 → circumference 100
+ if (!el) return;
+ const total = segs.reduce((s, x) => s + x.v, 0);
+ let off = 25, out = ' ';
+ if (total > 0) for (const s of segs) {
+ const len = s.v / total * 100;
+ if (len <= 0 || s.color === 'none') { off -= Math.max(0, len); continue; }
+ out += ' ';
+ off -= len;
+ }
+ out += '' + esc(center.big) + ' '
+ + '' + esc(center.small) + ' ';
+ el.innerHTML = out;
+ }
+ function legend(el, rows) {
+ if (el) el.innerHTML = rows.map(r => ' '
+ + esc(r.label) + ' ' + esc(r.v) + '
').join('');
+ }
+ async function loadCharts(d) {
+ // credit composition donut + today's viewing ring (live balances)
+ try {
+ const st = await (await fetch('/api/my/earn')).json();
+ if (!st.error) {
+ const purchased = d.credits || 0, earned = st.earned || 0, wc = Math.min(d.welcomeCredits || 0, earned);
+ $('dbCredits').textContent = (purchased + earned).toLocaleString();
+ $('dbCreditsSub').textContent = purchased.toLocaleString() + ' purchased · ' + earned.toLocaleString() + ' earned';
+ donut($('chDonut'),
+ [{ v: purchased, color: CH.mint }, { v: wc, color: CH.amber }, { v: earned - wc, color: CH.cyan }],
+ { big: (purchased + earned).toLocaleString(), small: 'spendable' });
+ legend($('chDonutLegend'), [
+ { color: CH.mint, label: 'Purchased', v: purchased.toLocaleString() },
+ { color: CH.amber, label: 'Welcome', v: wc.toLocaleString() },
+ { color: CH.cyan, label: 'Earned by viewing', v: Math.max(0, earned - wc).toLocaleString() }]);
+ const done = Math.min(st.views || 0, st.target || 5), left = Math.max(0, (st.target || 5) - done);
+ donut($('chRing'), [{ v: done, color: st.claimed ? CH.mint : CH.cyan }, { v: left, color: 'none' }],
+ { big: done + '/' + (st.target || 5), small: st.claimed ? 'claimed' : 'ads viewed' });
+ legend($('chRingLegend'), [
+ { color: st.claimed ? CH.mint : CH.cyan, label: 'Viewed today', v: done },
+ { color: CH.track, label: 'To go', v: left },
+ { color: CH.amber, label: 'Claim pays', v: '+' + (st.claimCredits || 0) + ' credits' }]);
+ }
+ } catch (e) {}
+ // recent on-chain payouts, sized to scale, oldest→newest
+ try {
+ let evs = [];
+ if (d.memberId) {
+ const a = await (await fetch('/api/my/activity')).json();
+ evs = (a.earnings || []).filter(e => e.amountWei).sort((x, y) => x.block - y.block).slice(-12);
+ }
+ bars($('chEarnBars'), $('chEarnX'), evs.map(e => ({
+ v: Number(BigInt(e.amountWei) / 1000000000000n) / 1e6,
+ label: e.type === 'AwardPaid' ? 'award' : 'L' + (e.tier || 1),
+ tip: IAP.fmtPol(e.amountWei) + ' POL', alt: e.type === 'AwardPaid' })));
+ if (!evs.length) $('chEarnSub').textContent = d.memberId
+ ? 'no payouts yet — share your link' : 'activate a package to start earning';
+ } catch (e) {}
+ // campaign delivery: impressions per campaign
+ try {
+ const r = await (await fetch('/api/my/campaigns')).json();
+ const cs = (r.campaigns || []).slice(0, 8);
+ bars($('chCampBars'), $('chCampX'), cs.map(c => ({
+ v: c.imps || 0, label: String(c.name || c.type).slice(0, 9),
+ tip: (c.name || c.type) + ': ' + (c.imps || 0) + ' imps · ' + (c.clicks || 0) + ' clicks',
+ alt: c.type === 'text' })));
+ if (!cs.length) $('chCampSub').textContent = 'no campaigns yet — place your first ad';
+ } catch (e) {}
+ }
async function loadDashboard() {
try {
const d = await (await fetch('/api/my/dashboard')).json();
@@ -36,6 +116,17 @@
$('dbEarned').textContent = IAP.fmtPol(d.earnedWei || '0');
$('dbBuyers').textContent = d.buyerCount || 0;
$('dbTeam').textContent = (d.referrals || []).length;
+ // trend chips: only real, computable facts
+ const ec = $('dbEarnedChip');
+ if (ec && d.earnCount) { ec.hidden = false; ec.textContent = d.earnCount + ' instant payout' + (d.earnCount > 1 ? 's' : ''); }
+ const bc = $('dbBuyersChip');
+ if (bc && d.memberId) {
+ bc.hidden = false;
+ bc.textContent = (d.buyerCount || 0) >= 5 ? 'level 3 open' : (d.buyerCount || 0) >= 2 ? 'level 2 open' : 'level 2 at 2 buyers';
+ }
+ const tc = $('dbTeamChip'), wk = (d.referrals || []).filter(r => Date.now() - new Date(r.joined) < 6048e5).length;
+ if (tc && wk) { tc.hidden = false; tc.textContent = '+' + wk + ' this week'; }
+ loadCharts(d);
$('nextMove').textContent = nextMove(d);
$('qualFill').style.width = Math.min(100, (d.buyerCount || 0) * 20) + '%';
const wrap = $('rosterWrap');
diff --git a/public/assets/site.css b/public/assets/site.css
index e77ea42..83943b4 100644
--- a/public/assets/site.css
+++ b/public/assets/site.css
@@ -258,7 +258,43 @@ input:focus,select:focus{border-color:var(--mint)}
#boBurger{display:block}
.bo-content{padding:18px}
}
-/* back-office accent family: green leads, cyan/violet/amber season the cards */
+/* ── overview v3: stat cards w/ plates+chips, hand-rolled charts ── */
+.statx{display:flex;gap:14px;align-items:flex-start;background:var(--panel);border:1px solid var(--line);
+ border-radius:var(--radius);padding:18px;position:relative;overflow:hidden}
+.statx .pl{flex:0 0 auto;width:46px;height:46px;border-radius:13px;display:grid;place-items:center;
+ background:rgba(67,232,195,.1);border:1px solid rgba(67,232,195,.3)}
+.statx .pl svg{width:22px;height:22px;stroke:var(--mint);fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
+.statx.c-cyan .pl{background:rgba(84,204,255,.1);border-color:rgba(84,204,255,.3)}
+.statx.c-cyan .pl svg{stroke:var(--cyan)}
+.statx.c-violet .pl{background:rgba(157,125,255,.12);border-color:rgba(157,125,255,.32)}
+.statx.c-violet .pl svg{stroke:var(--violet)}
+.statx.c-amber .pl{background:rgba(255,178,56,.1);border-color:rgba(255,178,56,.3)}
+.statx.c-amber .pl svg{stroke:var(--amber)}
+.statx .nv{font-family:var(--disp);font-size:26px;font-weight:800;font-variant-numeric:tabular-nums;line-height:1.1}
+.statx .lb{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-top:2px}
+.chip{display:inline-block;margin-top:7px;font-size:11.5px;font-weight:700;border-radius:999px;padding:2px 10px;
+ background:rgba(67,232,195,.1);color:var(--mint)}
+.chip.flat{background:rgba(139,166,156,.12);color:var(--muted)}
+.chip.warm{background:rgba(255,178,56,.12);color:var(--amber)}
+.card-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
+.card-head .sub{font-size:12px;color:var(--muted)}
+/* bar chart (divs, real data) */
+.barchart{display:flex;align-items:flex-end;gap:6px;height:120px;margin-top:14px}
+.barchart .bar{flex:1;min-width:6px;background:linear-gradient(180deg,var(--mint),rgba(67,232,195,.25));
+ border-radius:5px 5px 2px 2px;position:relative;transition:height .5s ease}
+.barchart .bar.alt{background:linear-gradient(180deg,var(--violet),rgba(157,125,255,.25))}
+.barchart .bar:hover::after{content:attr(data-v);position:absolute;bottom:calc(100% + 4px);left:50%;
+ transform:translateX(-50%);background:var(--panel-solid);border:1px solid var(--line-strong);border-radius:7px;
+ padding:2px 8px;font-family:var(--mono);font-size:11px;white-space:nowrap;z-index:3}
+.barchart .bar.empty{background:rgba(139,166,156,.15);height:4px!important}
+.chart-x{display:flex;gap:6px;margin-top:6px}
+.chart-x span{flex:1;text-align:center;font-family:var(--mono);font-size:10px;color:var(--muted);overflow:hidden;white-space:nowrap}
+/* donut + ring (SVG) */
+.donut-wrap{display:flex;gap:22px;align-items:center;margin-top:10px;flex-wrap:wrap}
+.donut-legend{display:flex;flex-direction:column;gap:8px;font-size:13px}
+.donut-legend i{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:8px}
+.donut-center{font-family:var(--disp);font-weight:800}
+/* ── back-office accent family: green leads, cyan/violet/amber season the cards ── */
.bo .stats .stat:nth-child(2) .n{color:var(--cyan)}
.bo .stats .stat:nth-child(2)::before{background:linear-gradient(90deg,transparent,var(--cyan),transparent)}
.bo .stats .stat:nth-child(3) .n{color:var(--violet)}
diff --git a/public/contract.html b/public/contract.html
index 2885673..a40972f 100644
--- a/public/contract.html
+++ b/public/contract.html
@@ -5,7 +5,7 @@
The contract | InstantAdPay
-
+
@@ -129,8 +129,8 @@
Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.
-
-
-
+
+
+