My line: Activity drop-down on every row, all three levels (last seen, stage, ads today, campaigns, link views, line, wallet, badges, Working/Quiet); Video Maker font override for local runs
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,7 @@ FACTS:
|
||||
- BLOG (public, instantadpay.com/blog): Marty's coaching and teaching articles on building a line, advertising that pays, and daily habits; each article has its own page and can be shared; RSS at /blog/feed.xml. Members who want to write their own articles: not offered today.
|
||||
- ACHIEVEMENT BADGES ON TELEGRAM (2026-09-13): when a member unlocks Spark/Surge/Circuit/Nexus, their personalised badge image (username on the ribbon) is posted automatically to the team's Telegram payments topic and the main group, once per badge; members cannot trigger posts themselves (the 'Post to Telegram' button is admin-only); 'Share' opens a picker (X, Facebook, Telegram, WhatsApp, LinkedIn, Massifly (copies the post and opens the feed composer), the phone's own share menu, copy link, save image) that shares the member's public badge page instantadpay.com/b/<username>/<badge>, which shows the badge and their join link.
|
||||
- PROMO TOOLKIT BY BADGE + AI COPY ENGINE (2026-09-14): Promo tools > AI Copy Engine shows the ladder: Free (links, posts, swipes, banners, wall, objections, shorts, badge pages), Spark (payouts on: one-tap campaign templates aimed at the member's link, printable handout with their QR at /handout/<username>), Surge (first qualifying buyer; the AI Copy Engine is live: posts, DMs, follow-ups, objection replies, emails, story posts, team broadcasts in the member's name with their link, honesty rules built in; 20 free generations a month), Circuit (60 free; Video Maker renders every promo video and short with the member's own end card and QR, hosted for download; split tester compares join angles by views, joins, buyers), Nexus (150 free; Leader Ops: three-level team triage with stalled flags and one-click nudges, AI-drafted team broadcasts, credit grants from the leader's earned pool to anyone in their line, co-branded join page showing the sponsor's bio, and the member's own partner code: welcome credits funded from the leader's pool at each redemption, plus a partner kit page /partners?ref=<username>&promo=CODE). After the free allowance each generation costs 10 ad credits from the earned pool. Credits are advertising, never money.
|
||||
- MY LINE ACTIVITY DROP-DOWN (2026-09-14): every row on My line, all three levels, has an Activity button that opens that person's activity: last seen, joined, stage and next step, ads viewed today and claim streak, campaigns active, link views in 30 days and joins, directs and qualifying buyers, wallet/payouts, badges, and a Working or Quiet verdict. Read-only, phone friendly.
|
||||
- GETTING STARTED CARD (2026-09-14): the top of every new member's Overview shows four steps (username, link wallet, switch on payouts, first package) with the current one explained and one button that opens the right tab and highlights the control (Wallet tab > Link wallet; then Activate payouts). Encouragement only: it can be hidden, and members who only want to view ads and earn credits are never blocked. It disappears once all four are done.
|
||||
- LEADERBOARD + REFERRAL CONTEST (2026-09-14): instantadpay.com/leaderboard ranks members by ad packages sold to people they directly sponsor (dollar value, read from the chain; a member's own linked positions and second accounts never count). Periods: this week (Monday to Sunday, Central time), this month, all time. Weekly and monthly winners are recorded automatically at rollover, announced in Telegram, and the credit prizes set by the admin (a ladder: 1st 1,000 / 2nd 500 / 3rd 250 weekly, 5,000 / 2,500 / 1,000 monthly by default) are granted automatically to those positions; the prize text is shown on the page and on the Overview's Leaderboard card, which also shows the member's own rank. Prizes are credits or packages, never cash.
|
||||
- WHAT'S NEW / ROADMAP (2026-09-14): instantadpay.com/whats-new lists release notes (what shipped, dated, tagged new/improved/fixed) and the roadmap (planned / building, with rough ETAs). The Overview has a "What's new" card with the latest three notes and what is being built; a dot marks notes since the member's last look. Written by the admin in Admin > Releases.
|
||||
|
||||
+29
-1
@@ -1408,12 +1408,40 @@
|
||||
+ (m.earnedWei && m.earnedWei !== '0' ? '+' + IAP.fmtPol(m.earnedWei) + ' POL' : '0.00 POL') + '</span>'
|
||||
+ '<span class="dt">' + new Date(m.joined).toLocaleDateString() + '</span>'
|
||||
+ (m.email ? '<button class="btn sec small chat-msg-btn" type="button" data-cemail="' + esc(m.email) + '" data-cname="' + esc(m.name) + '">Message</button>' : '')
|
||||
+ '</div>').join('')
|
||||
+ (m.ref && !m.own ? '<button class="btn sec small lin-act-btn" type="button" data-act="' + esc(m.ref) + '" aria-expanded="false" title="Activity">Activity ▾</button>' : '')
|
||||
+ '</div>' + (m.ref && !m.own ? '<div class="lin-act" data-actbox="' + esc(m.ref) + '" hidden></div>' : '')).join('')
|
||||
+ '</div>').join('');
|
||||
el.querySelectorAll('[data-cemail]').forEach(b =>
|
||||
b.addEventListener('click', () => openConvo(b.dataset.cemail, b.dataset.cname)));
|
||||
el.querySelectorAll('[data-act]').forEach(b => b.addEventListener('click', () => toggleLineActivity(b)));
|
||||
} catch (e) {}
|
||||
}
|
||||
// who is working: per-member activity drop-down under the row (any of the three levels)
|
||||
const lineActCache = {};
|
||||
async function toggleLineActivity(btn) {
|
||||
const box = document.querySelector('[data-actbox="' + btn.dataset.act + '"]'); if (!box) return;
|
||||
const open = box.hidden; box.hidden = !open; btn.setAttribute('aria-expanded', String(open)); btn.textContent = open ? 'Activity ▴' : 'Activity ▾';
|
||||
if (!open) return;
|
||||
if (!lineActCache[btn.dataset.act]) {
|
||||
box.innerHTML = '<span class="muted small">Loading…</span>';
|
||||
try { lineActCache[btn.dataset.act] = await (await fetch('/api/my/line/activity?ref=' + encodeURIComponent(btn.dataset.act))).json(); } catch (e) { lineActCache[btn.dataset.act] = { error: 'Could not load.' }; }
|
||||
}
|
||||
const a = lineActCache[btn.dataset.act];
|
||||
if (a.error) { box.innerHTML = '<span class="muted small">' + esc(a.error) + '</span>'; return; }
|
||||
const ago = t => { if (!t) return 'never'; const d = Date.now() - t; if (d < 3600e3) return Math.max(1, Math.round(d / 60e3)) + ' min ago'; if (d < 86400e3) return Math.round(d / 3600e3) + 'h ago'; return Math.round(d / 86400e3) + 'd ago'; };
|
||||
const chip = (label, val, cls) => '<span class="lin-chip' + (cls ? ' ' + cls : '') + '"><b>' + esc(label) + '</b>' + esc(val) + '</span>';
|
||||
const working = (a.viewsToday || 0) > 0 || (a.campaignsActive || 0) > 0 || (a.linkViews30 || 0) > 0 || (a.lastSeen && Date.now() - a.lastSeen < 2 * 86400e3);
|
||||
box.innerHTML = chip('Last seen', ago(a.lastSeen), a.lastSeen && Date.now() - a.lastSeen < 2 * 86400e3 ? 'on' : (a.quietDays >= 3 ? 'warn' : ''))
|
||||
+ chip('Joined', new Date(a.joined).toLocaleDateString())
|
||||
+ chip('Stage', (a.rung || 'Joined') + (a.next ? ' · next: ' + a.next : ''), a.stalled ? 'warn' : '')
|
||||
+ chip('Ads today', (a.viewsToday || 0) + '/' + (a.target || 5) + (a.claimedToday ? ' claimed' : '') + (a.streakDay ? ' · streak day ' + a.streakDay : ''), (a.viewsToday || 0) > 0 ? 'on' : '')
|
||||
+ chip('Campaigns', (a.campaignsActive || 0) + ' active of ' + (a.campaigns || 0) + (a.imps ? ' · ' + a.imps.toLocaleString() + ' views' : ''), (a.campaignsActive || 0) > 0 ? 'on' : '')
|
||||
+ chip('Link', (a.linkViews30 || 0) + ' views (30d) · ' + (a.joins || 0) + ' joined', (a.linkViews30 || 0) > 0 ? 'on' : '')
|
||||
+ chip('Line', (a.directs || 0) + ' direct' + (a.directs === 1 ? '' : 's') + ' · ' + (a.buyerCount || 0) + ' qualifying', (a.buyerCount || 0) > 0 ? 'on' : '')
|
||||
+ chip('Wallet', a.wallet ? (a.badges && a.badges.includes('payouts') ? 'linked · payouts on' : 'linked') : 'not linked', a.wallet ? '' : 'warn')
|
||||
+ (a.badges && a.badges.length ? chip('Badges', a.badges.map(b => ({ payouts: 'Spark', firstBuyer: 'Surge', level2: 'Circuit', level3: 'Nexus' }[b] || b)).join(' · ')) : '')
|
||||
+ '<span class="lin-verdict' + (working ? ' on' : '') + '">' + (working ? 'Working' : 'Quiet') + '</span>';
|
||||
}
|
||||
async function loadUplineMessages() {
|
||||
try {
|
||||
const r = await (await fetch('/api/my/messages')).json();
|
||||
|
||||
@@ -459,8 +459,14 @@ textarea{resize:vertical;font:inherit}
|
||||
.lin-row .id{font-family:var(--mono);font-size:11px;color:var(--muted)}
|
||||
.lin-row .dt{font-size:12px;color:var(--muted);text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
|
||||
.lin-row .chat-msg-btn{justify-self:end}
|
||||
.lin-row .lin-act-btn{justify-self:end;white-space:nowrap}
|
||||
.lin-act{display:flex;flex-wrap:wrap;gap:6px;align-items:center;padding:8px 10px 10px;margin:-1px 0 6px;border-bottom:1px solid var(--line);background:rgba(255,255,255,.025);border-radius:0 0 10px 10px}
|
||||
.lin-chip{font-size:12px;color:var(--muted);border:1px solid var(--line);border-radius:999px;padding:3px 10px;white-space:nowrap} .lin-chip b{font-weight:600;color:var(--ink);margin-right:5px} .lin-chip.on{border-color:rgba(67,232,195,.45);color:var(--ink)} .lin-chip.warn{border-color:rgba(255,209,92,.5)}
|
||||
.lin-verdict{margin-left:auto;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:#ffd15c} .lin-verdict.on{color:var(--mint)}
|
||||
@media (max-width:560px){.lin-chip{white-space:normal}.lin-act{padding:8px 4px 10px}}
|
||||
@media (max-width:560px){.lin-row{grid-template-columns:1fr auto;gap:2px 10px}
|
||||
.lin-row .em{grid-column:1/2}.lin-row .dt{grid-column:1/2;text-align:left}.lin-row .chat-msg-btn{grid-row:1/3;grid-column:2}}
|
||||
.lin-row .em{grid-column:1/2}.lin-row .dt{grid-column:1/2;text-align:left}.lin-row .chat-msg-btn{grid-row:1/3;grid-column:2}
|
||||
.lin-row .lin-act-btn{grid-column:2;grid-row:3/5}.lin-row:not(:has(.chat-msg-btn)) .lin-act-btn{grid-row:1/3}}
|
||||
/* ── solo composer: toolbar + contenteditable editor ── */
|
||||
.ed-bar{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px}
|
||||
.ed-bar button{background:var(--panel);color:var(--ink);border:1px solid var(--line-strong);border-radius:8px;
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
<title>Member area | InstantAdPay</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||
<link rel="icon" type="image/png" href="/logo-icon.png">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260914d">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260914e">
|
||||
</head>
|
||||
<body class="bo-body">
|
||||
|
||||
@@ -984,7 +984,7 @@
|
||||
<script src="/assets/common.js?v=20260914a"></script>
|
||||
<script src="/assets/wallet.js?v=20260911a"></script>
|
||||
<script src="/assets/promo.js?v=20260911a"></script>
|
||||
<script src="/assets/my.js?v=20260914g"></script>
|
||||
<script src="/assets/my.js?v=20260914h"></script>
|
||||
<script src="/assets/chat.js?v=20260907l"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1659,6 +1659,7 @@ const server = http.createServer(async (req, res) => {
|
||||
const out = levels.map(L => ({ level: L.level, members: L.members.map(m => ({
|
||||
memberId: m.memberId || 0,
|
||||
name: m.username ? '@' + m.username : m.memberId ? 'member #' + m.memberId : 'member',
|
||||
ref: m.code || null, // opens the activity drop-down (Clinton's ask, 2026-09-14): any of the three levels
|
||||
email: L.level === 1 ? m.email : null, // directs only
|
||||
joined: m.created,
|
||||
qualified: !!(m.memberId && qualified.has(m.memberId)),
|
||||
@@ -1672,6 +1673,24 @@ const server = http.createServer(async (req, res) => {
|
||||
}
|
||||
return json(res, 200, { levels: out, counts: out.map(L => L.members.length) });
|
||||
}
|
||||
// -- who is working: one downline member's activity, any of the three levels (Clinton, 2026-09-14)
|
||||
if (p === '/api/my/line/activity' && req.method === 'GET') {
|
||||
const s = await auth.fromRequest(req);
|
||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||
const ref = String(u.searchParams.get('ref') || '').trim().toLowerCase().slice(0, 40);
|
||||
const a = ref ? await accounts.byCode(ref) : null;
|
||||
if (!a || !(await accounts.isDownlineOf(s.email, a.email, 3))) return json(res, 404, { error: 'Not in your line.' });
|
||||
const now = Date.now();
|
||||
const out = { name: a.username ? '@' + a.username : (a.memberId ? 'member #' + a.memberId : 'member'), joined: a.created, lastSeen: a.lastSeen || 0, quietDays: Math.floor((now - Math.max(a.created || 0, a.lastSeen || 0)) / 86400000), wallet: !!a.address, memberId: a.memberId || 0 };
|
||||
try { const c = await coach.describe(a, now); out.rung = c.label; out.next = c.next; out.stalled = c.stalled; out.buyerCount = c.buyerCount || 0; } catch (e) {}
|
||||
try { const v = await ads.viewStatus(a.email); out.viewsToday = v.views; out.target = v.target; out.claimedToday = v.claimed; out.streakDay = v.claimed ? v.streakDay : Math.max(0, v.streakDay - 1); } catch (e) {}
|
||||
try { const cs = await ads.listCampaigns(a.email); out.campaigns = cs.length; out.campaignsActive = cs.filter(c => c.status === 'active').length; out.imps = cs.reduce((n, c) => n + (c.imps || 0) + (c.impsNas || 0), 0); } catch (e) {}
|
||||
try { const ls = await coach.linkStats(a.email); out.linkViews30 = (ls.angles || []).reduce((n, r) => n + (r.views30 || 0), 0); out.linkViews = ls.totalViews || 0; out.joins = ls.totalJoins || 0; } catch (e) {}
|
||||
try { out.directs = ((await accounts.downline(a.email, 1))[0] || { members: [] }).members.length; } catch (e) { out.directs = 0; }
|
||||
try { out.badges = await ads.milestonesOf(a.email); } catch (e) { out.badges = []; }
|
||||
try { out.positions = (await accounts.positions(a.email)).length; } catch (e) { out.positions = 0; }
|
||||
return json(res, 200, out);
|
||||
}
|
||||
// -- broadcast a message to your downline (1/day), on-site inbox + email
|
||||
if (p === '/api/my/broadcast' && req.method === 'POST') {
|
||||
const s = await auth.fromRequest(req);
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ function jobs() { try { return JSON.parse(fs.readFileSync(FILE(), 'utf8')); } ca
|
||||
function saveJobs(j) { try { fs.writeFileSync(FILE(), JSON.stringify(j.slice(-500))); } catch (e) {} }
|
||||
function init(refs) { R = refs; setInterval(() => tick().catch(e => console.error('videomaker', e.message)), 5000); }
|
||||
let FONT = undefined; // Alpine puts ttf-dejavu under /usr/share/fonts/ttf-dejavu (older) or /dejavu (newer): search once
|
||||
function font() { if (FONT !== undefined) return FONT; FONT = null; const walk = d => { let ents = []; try { ents = fs.readdirSync(d, { withFileTypes: true }); } catch (e) { return null; } for (const e of ents) { const f = path.join(d, e.name); if (e.isDirectory()) { const r = walk(f); if (r) return r; } else if (e.name === 'DejaVuSans-Bold.ttf') return f; } return null; }; FONT = walk('/usr/share/fonts'); return FONT; }
|
||||
function font() { if (FONT !== undefined) return FONT; if (process.env.VIDEOMAKER_FONT) { FONT = process.env.VIDEOMAKER_FONT; return FONT; } FONT = null; const walk = d => { let ents = []; try { ents = fs.readdirSync(d, { withFileTypes: true }); } catch (e) { return null; } for (const e of ents) { const f = path.join(d, e.name); if (e.isDirectory()) { const r = walk(f); if (r) return r; } else if (e.name === 'DejaVuSans-Bold.ttf') return f; } return null; }; FONT = walk('/usr/share/fonts'); return FONT; }
|
||||
// progress: 0-100 plus a short stage label, written to the jobs file so the page can poll it
|
||||
function setProgress(id, pct, stage) { const j = jobs(); const k = j.find(x => x.id === id); if (!k) return; k.pct = Math.max(k.pct || 0, Math.min(100, Math.round(pct))); if (stage) k.stage = stage; saveJobs(j); }
|
||||
function durationOf(file) {
|
||||
|
||||
Reference in New Issue
Block a user