Files
rm-circle-team-router/daysnap.js
T
martbost a95633c6ca Day page: a past date with no activity is not found, and never written to disk
Probing any old date returned an empty page with a 200 and left a junk file
behind for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 05:46:00 -05:00

240 lines
15 KiB
JavaScript

// The day page (Marty, 2026-09-23): everything that happened across the WHOLE contract on one
// Central calendar day, at a dated address that keeps working forever.
//
// Why it exists: the daily Telegram snapshot outgrew Telegram. Past 1,000 members the post hit
// the 4,096-character limit and was rejected outright for two mornings, and capping the lists to
// fit meant deciding what to throw away. Manson caught the cost of that: the level-up list was
// ordered by time, so the cap hid 2 Culmen and 13 Fabrica behind 24 routine Ascensus upgrades.
// Now the post carries the headline and the climbs, and everything else lives here with no cap.
//
// Today is computed live from the chain index. A past day is read from a file written once when
// that day closed, because the index only keeps the most recent few hundred payouts (about four
// days at current volume) and an older day could never be rebuilt from it.
'use strict';
const fs = require('fs');
const path = require('path');
let DATA_DIR = null;
const LEVELS = ['Scintilla', 'Ascensus', 'Fabrica', 'Culmen', 'Apex', 'Fastigium', 'Vertex', 'Corona'];
const lname = n => (n >= 1 && n <= 8) ? LEVELS[n - 1] : 'L' + n;
function init(opts) { DATA_DIR = opts.dataDir; try { fs.mkdirSync(path.join(DATA_DIR, 'days'), { recursive: true }); } catch (e) {} }
const indexFile = () => path.join(DATA_DIR, 'chain-index.json');
const dayFile = key => path.join(DATA_DIR, 'days', key + '.json');
// Central calendar day, the same key the rest of the estate uses
const dayKey = ts => new Date(ts === undefined ? Date.now() : ts).toLocaleDateString('en-CA', { timeZone: 'America/Chicago' });
const isKey = k => /^\d{4}-\d{2}-\d{2}$/.test(String(k || ''));
// midnight-to-midnight in Central, found by bisection so it is right on both clock-change days
function bounds(key) {
const probe = Date.parse(key + 'T12:00:00Z');
let lo = probe - 36 * 3600000, hi = probe;
while (hi - lo > 1000) { const mid = Math.floor((lo + hi) / 2); if (dayKey(mid) < key) lo = mid; else hi = mid; }
const start = hi;
lo = start; hi = start + 36 * 3600000;
while (hi - lo > 1000) { const mid = Math.floor((lo + hi) / 2); if (dayKey(mid) <= key) lo = mid; else hi = mid; }
return { start, end: hi };
}
function readIndex() { try { return JSON.parse(fs.readFileSync(indexFile(), 'utf8')); } catch (e) { return null; } }
// everything that happened on one Central day, company-wide: no team or org filter anywhere
function compute(key) {
const idx = readIndex();
if (!idx) return null;
const { start, end } = bounds(key);
const s = Math.floor(start / 1000), e = Math.floor(end / 1000);
const members = idx.members || {};
const all = idx.payouts || [];
const joins = Object.keys(members)
.map(i => ({ id: Number(i), m: members[i] }))
.filter(x => { const t = Number(x.m.joinedAt || 0); return t >= s && t < e; })
.sort((a, b) => Number(a.m.joinedAt) - Number(b.m.joinedAt))
.map(x => ({ id: x.id, referrerId: Number(x.m.referrerId || 0) || null, tier: x.m.tier || null, level: Number(x.m.level || 0) || null }));
const pays = all.filter(p => { const t = Number(p.ts || 0); return t >= s && t < e; })
.map(p => ({ toId: Number(p.toId || 0), fromId: Number(p.fromId || 0), kind: p.kind || null, level: Number(p.level || 0) || null,
pol: Number(p.pol || 0), tx: p.tx || null, ts: Number(p.ts || 0), upgrade: p.upgrade || null }))
.sort((a, b) => b.pol - a.pol);
const climbSeen = new Map();
for (const p of pays) if (p.upgrade) climbSeen.set(p.upgrade.id + ':' + p.upgrade.newLevel, { id: Number(p.upgrade.id), lvl: Number(p.upgrade.newLevel), ts: p.ts });
const climbs = [...climbSeen.values()].sort((a, b) => b.lvl - a.lvl || a.ts - b.ts);
const recruited = {};
for (const j of joins) if (j.referrerId) (recruited[j.referrerId] = recruited[j.referrerId] || []).push(j.id);
const recruiters = Object.keys(recruited).map(r => ({
id: Number(r), brought: recruited[r], directs: Number((members[r] || {}).directCount || 0),
})).sort((a, b) => b.brought.length - a.brought.length || a.id - b.id);
const polMoved = pays.reduce((n, p) => n + p.pol, 0);
// the index keeps a rolling payout window; if a day fills it the totals are a floor, not the whole day
const clipped = pays.length >= all.length && all.length > 0;
return {
key, computedAt: Date.now(), clipped,
totals: { joins: joins.length, climbs: climbs.length, payouts: pays.length, polMoved: Math.round(polMoved * 1e6) / 1e6 },
contract: { members: Object.keys(members).length, lifetimePol: Number((idx.totals || {}).pol || 0), lifetimePayouts: Number((idx.totals || {}).count || 0) },
climbs, joins, recruiters, payouts: pays,
biggest: pays[0] || null,
};
}
// a finished day is written once and read from disk forever after
function get(key) {
if (!isKey(key)) return null;
const today = dayKey();
if (key !== today) {
try { return JSON.parse(fs.readFileSync(dayFile(key), 'utf8')); } catch (e) {}
if (key > today) return null;
}
const d = compute(key);
if (!d) return null;
if (key !== today) {
// a past day with nothing in it is either before the contract or beyond the index's reach:
// say not found rather than serving an empty page, and never write a file for it
if (d.totals.payouts + d.totals.joins === 0) return null;
save(d);
}
return d;
}
function save(d) { try { fs.writeFileSync(dayFile(d.key), JSON.stringify(d)); } catch (e) {} }
function stored() { try { return fs.readdirSync(path.join(DATA_DIR, 'days')).filter(f => f.endsWith('.json')).map(f => f.slice(0, -5)).sort().reverse(); } catch (e) { return []; } }
// hourly: close out any finished day that has not been written yet, so the archive never depends
// on someone opening the page before the payout window rolls past it
function tick() {
const today = dayKey();
for (let back = 1; back <= 3; back++) {
const k = dayKey(Date.now() - back * 86400000);
if (k === today) continue;
if (fs.existsSync(dayFile(k))) continue;
const d = compute(k);
if (d && d.totals.payouts + d.totals.joins > 0) { save(d); console.log('daysnap: stored ' + k); }
}
}
// ---- the page ---------------------------------------------------------------------------------
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const n0 = v => Number(v || 0).toLocaleString('en-US', { maximumFractionDigits: 0 });
const n2 = v => Number(v || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const SCAN = 'https://polygonscan.com/tx/';
const pretty = key => { const [y, m, d] = key.split('-').map(Number); return new Date(Date.UTC(y, m - 1, d)).toLocaleDateString('en-US', { timeZone: 'UTC', weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }); };
function render(key, opts) {
const o = opts || {};
const d = get(key);
const today = dayKey();
const prev = dayKey(bounds(key).start - 3600000);
const nextKey = dayKey(bounds(key).end + 3600000);
const title = 'The Circle on ' + pretty(key);
if (!d) return { status: 404, html: shell(title, '<p class="muted">No figures for that date yet.</p>', key, prev, null) };
const usd = Number(o.polUsd) || 0;
const dol = p => usd ? ' <span class="muted">(~$' + n0(p * usd) + ')</span>' : '';
const high = d.climbs.filter(c => c.lvl >= 3);
const routine = d.climbs.length - high.length;
const head = '<div class="dayhead">'
+ tile(n0(d.totals.joins), 'new members')
+ tile(n0(d.totals.climbs), 'upgrades')
+ tile(n0(d.totals.polMoved) + ' POL', 'moved on chain' + (usd ? ' · ~$' + n0(d.totals.polMoved * usd) : ''))
+ tile(n0(d.contract.members), 'on the contract')
+ '</div>';
// grouped by rung, highest first: the ladder is the story, and a two-column table of the same
// thing wastes half the page and buries a Culmen among two dozen Ascensus rows
const byLevel = new Map();
for (const c of d.climbs) { if (!byLevel.has(c.lvl)) byLevel.set(c.lvl, []); byLevel.get(c.lvl).push(c.id); }
const climbRows = d.climbs.length
? [...byLevel.keys()].sort((a, b) => b - a).map(lv => '<div class="climbrow' + (lv >= 3 ? ' big' : '') + '">'
+ '<div class="cl">' + esc(lname(lv)) + ' <span class="muted">' + byLevel.get(lv).length + '</span></div>'
+ '<div class="ci">' + byLevel.get(lv).map(i => '<span class="pid">#' + i + '</span>').join('') + '</div></div>').join('')
: '<p class="muted">No upgrades on this day.</p>';
const joinRows = d.joins.length
? '<table class="daytbl"><tr><th>Position</th><th>Invited by</th><th>Entry</th></tr>'
+ d.joins.map(j => '<tr><td class="mono">#' + j.id + '</td><td class="mono">' + (j.referrerId ? '#' + j.referrerId : '—') + '</td><td>' + esc(j.tier === 2 || j.tier === 'Premium' ? 'Premium' : 'Standard') + '</td></tr>').join('')
+ '</table>'
: '<p class="muted">No new members on this day.</p>';
const recRows = d.recruiters.length
? '<table class="daytbl"><tr><th>Position</th><th>Brought in</th><th>Who</th><th>Directs</th></tr>'
+ d.recruiters.map(r => '<tr><td class="mono">#' + r.id + '</td><td>' + r.brought.length + '</td><td class="mono small">'
+ r.brought.map(x => '#' + x).join(' ') + '</td><td>' + (r.directs >= 2 ? '<b>qualified</b>' : r.directs + '/2') + '</td></tr>').join('')
+ '</table>'
: '<p class="muted">Nobody added a personal referral on this day.</p>';
const payRows = d.payouts.length
? '<table class="daytbl"><tr><th>POL</th><th>To</th><th>From</th><th>Why</th><th>Proof</th></tr>'
+ d.payouts.map(p => '<tr><td class="mono"><b>' + n2(p.pol) + '</b>' + dol(p.pol) + '</td><td class="mono">#' + p.toId + '</td><td class="mono">'
+ (p.fromId ? '#' + p.fromId : '—') + '</td><td>' + esc(p.upgrade ? 'upgrade to ' + lname(p.upgrade.newLevel) : (p.kind || 'payment'))
+ '</td><td>' + (p.tx ? '<a href="' + SCAN + esc(p.tx) + '" target="_blank" rel="noopener">Polygonscan ↗</a>' : '—') + '</td></tr>').join('')
+ '</table>'
: '<p class="muted">No payments on this day.</p>';
const body = head
+ (high.length ? '<p class="lead">Highest climb of the day: <b>' + esc(lname(high[0].lvl)) + '</b>, reached by '
+ high.filter(c => c.lvl === high[0].lvl).map(c => '#' + c.id).join(', ') + '.</p>' : '')
+ (d.biggest ? '<p class="lead">Biggest single payment: <b>' + n2(d.biggest.pol) + ' POL</b>' + dol(d.biggest.pol)
+ ', caught by <span class="mono">#' + d.biggest.toId + '</span>'
+ (d.biggest.tx ? ' · <a href="' + SCAN + esc(d.biggest.tx) + '" target="_blank" rel="noopener">Polygonscan ↗</a>' : '') + '.</p>' : '')
+ (d.clipped ? '<p class="warnline">These payment totals are a floor: the index window filled for this day.</p>' : '')
+ sec('Every climb', (high.length ? high.length + ' above Ascensus' : '') + (routine ? (high.length ? ' · ' : '') + routine + ' reached Ascensus' : ''), climbRows)
+ sec('Every payment', d.totals.payouts + ' on chain, each one verifiable', payRows)
+ sec('Every new member', d.totals.joins + ' joined', joinRows)
+ sec('Recruiters', d.recruiters.length + ' added someone', recRows);
return { status: 200, html: shell(title, body, key, prev, nextKey <= today && nextKey !== key ? nextKey : null) };
}
const tile = (v, l) => '<div class="daytile"><div class="dv">' + v + '</div><div class="dl">' + l + '</div></div>';
const sec = (h, sub, inner) => '<section class="daysec"><h2>' + esc(h) + (sub ? ' <span class="muted">' + esc(sub) + '</span>' : '') + '</h2>'
+ (/^<table/.test(inner) ? '<div class="dayscroll">' + inner + '</div>' : inner) + '</section>';
function shell(title, body, key, prev, next) {
return '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">'
+ '<title>' + esc(title) + ' — RM Circle</title>'
+ '<meta name="description" content="Every upgrade, every new position and every payment across The RM Circle contract on ' + esc(pretty(key)) + '. All on Polygon, all verifiable.">'
+ '<meta property="og:type" content="website"><meta property="og:site_name" content="RM Circle Team Build">'
+ '<meta property="og:title" content="' + esc(title) + '"><meta property="og:description" content="Every upgrade, position and payment for the day, each one verifiable on Polygon.">'
+ '<link rel="canonical" href="https://rmcircle.team/day/' + esc(key) + '">'
+ '<link rel="icon" type="image/png" href="/favicon.png"><link rel="stylesheet" href="/styles.css">'
+ '<style>'
+ '.dayhead{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:18px 0 26px}'
+ '.daytile{border:1px solid rgba(212,175,55,.28);border-radius:14px;padding:14px 16px;background:rgba(212,175,55,.05)}'
+ '.daytile .dv{font-size:26px;font-weight:800;color:#f3c34a;line-height:1.1}'
+ '.daytile .dl{font-size:12px;color:#9fb0c2;margin-top:4px}'
+ '.daysec{margin:30px 0}.daysec h2{font-size:19px;margin:0 0 10px}'
+ '.daysec h2 .muted{font-size:13px;font-weight:400}'
+ '.dayscroll{overflow-x:auto;-webkit-overflow-scrolling:touch}'
+ '.daytbl{width:100%;min-width:460px;border-collapse:collapse;font-size:14px}'
+ '.climbrow{display:grid;grid-template-columns:150px 1fr;gap:10px;align-items:baseline;padding:9px 0;border-bottom:1px solid rgba(255,255,255,.07)}'
+ '.climbrow .cl{font-weight:700}.climbrow.big .cl{color:#f3c34a;font-size:17px}'
+ '.climbrow .ci{display:flex;flex-wrap:wrap;gap:6px}'
+ '.pid{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:13px;border:1px solid rgba(255,255,255,.14);border-radius:7px;padding:2px 8px}'
+ '.climbrow.big .pid{border-color:rgba(243,195,74,.45);color:#f3c34a}'
+ '@media(max-width:560px){.climbrow{grid-template-columns:1fr;gap:6px}}'
+ '.daytbl th{text-align:left;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:#9fb0c2;padding:7px 12px 7px 0;border-bottom:1px solid rgba(255,255,255,.12)}'
+ '.daytbl td{padding:7px 12px 7px 0;border-bottom:1px solid rgba(255,255,255,.06)}'
+ '.mono{font-family:ui-monospace,Menlo,Consolas,monospace}.muted{color:#9fb0c2}.small{font-size:12px}'
+ '.daynav{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:22px 0 0}'
+ '.warnline{color:#f3c34a;font-size:13px}'
+ '</style></head><body>'
+ '<header class="wrap nav"><a class="brand" href="/"><img class="brand-mark" src="/logo.jpg" alt="The RM Circle" width="42" height="42"><span>RM Circle</span></a></header>'
+ '<main><section class="section"><div class="wrap" style="max-width:1000px">'
+ '<div class="eyebrow">The whole contract, one day</div>'
+ '<h1 style="font-size:clamp(28px,4.4vw,44px);margin:8px 0 6px">' + esc(title) + '</h1>'
+ '<p class="muted" style="margin:0 0 6px">Midnight to midnight, US Central. Every figure below is read from the Polygon contract, company-wide.</p>'
+ body
+ '<div class="daynav">'
+ (prev ? '<a class="btn ghost" href="/day/' + esc(prev) + '">← ' + esc(pretty(prev)) + '</a>' : '')
+ (next ? '<a class="btn ghost" href="/day/' + esc(next) + '">' + esc(pretty(next)) + ' →</a>' : '')
+ '<a class="btn" href="/">Join The RM Circle</a>'
+ '</div>'
+ '<p class="muted small" style="margin-top:22px">Positions are shown by their contract id. No income is guaranteed and cryptocurrency carries risk of loss.</p>'
+ '</div></section></main></body></html>';
}
module.exports = { init, dayKey, isKey, compute, get, save, stored, tick, render, bounds, lname };