A page per day: the whole contract, midnight to midnight
The daily snapshot outgrew Telegram. Past 1,000 members the post hit the 4,096-character limit and was rejected outright two mornings running, and capping the lists to fit meant choosing what to throw away. Manson caught what that cost: the level-up list was ordered by time, so the cap hid two Culmen and thirteen Fabrica behind two dozen routine Ascensus upgrades. /day/<date> now carries every climb, every payment with its Polygonscan link, every new position and every recruiter, with no cap on any of it. Company-wide, no team filter, keyed to the Central calendar day so a link posted on Monday still shows Monday next year. Today is computed live; a finished day is written to disk once, because the chain index only keeps a few hundred payouts and an older day could never be rebuilt from it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+233
@@ -0,0 +1,233 @@
|
||||
// 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 && key !== today) 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 => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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 };
|
||||
@@ -4,6 +4,7 @@ const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { URL } = require('url');
|
||||
const chain = require('./chain');
|
||||
const daysnap = require('./daysnap'); // /day/<date>: the whole contract, one Central day, kept forever
|
||||
const messages = require('./messages');
|
||||
const profiles = require('./profiles');
|
||||
const placement = require('./placement');
|
||||
@@ -1847,6 +1848,14 @@ const server=http.createServer(async(req,res)=>{
|
||||
const u=new URL(req.url,`http://${req.headers.host||'localhost'}`),pathname=decodeURIComponent(u.pathname);
|
||||
// /p/<id> = member Page Builder pages: dynamic HTML, so it must reach the
|
||||
// API handler rather than the static-file path (which 404s it).
|
||||
if(pathname==='/day'||/^\/day\/\d{4}-\d{2}-\d{2}$/.test(pathname)){
|
||||
const key = pathname==='/day' ? daysnap.dayKey() : pathname.slice(5);
|
||||
const r = daysnap.render(key, { polUsd: await getPolUsd() });
|
||||
const fresh = key === daysnap.dayKey();
|
||||
res.writeHead(r.status, securityHeaders({ 'Content-Type':'text/html; charset=utf-8',
|
||||
'Cache-Control': fresh ? 'public, max-age=120' : 'public, max-age=86400' }));
|
||||
return res.end(r.html);
|
||||
}
|
||||
if(pathname==='/health'||pathname==='/announce.ics'||pathname.startsWith('/api/')||pathname.startsWith('/tv/')||/^\/p\/\d{1,15}(\/[a-z0-9-]{1,24})?$/.test(pathname))return await handleApi(req,res,pathname);
|
||||
if(req.method!=='GET'&&req.method!=='HEAD')return send(res,405,'Method Not Allowed',{'Content-Type':'text/plain; charset=utf-8'});
|
||||
// Members-area gate: the Circle Method lessons 2-10 + the e-gift cash-out
|
||||
@@ -1925,6 +1934,11 @@ function checkUpgradeAlerts(){
|
||||
}catch(e){ console.error('upgrade alert check', e.message); }
|
||||
}
|
||||
setInterval(checkUpgradeAlerts, 5*60*1000).unref();
|
||||
// close out finished days so the archive never depends on someone opening the page before the
|
||||
// chain index's rolling payout window scrolls past that day
|
||||
daysnap.init({ dataDir: DATA_DIR });
|
||||
setTimeout(() => { try { daysnap.tick(); } catch (e) { console.error('daysnap', e.message); } }, 20000).unref();
|
||||
setInterval(() => { try { daysnap.tick(); } catch (e) { console.error('daysnap', e.message); } }, 60 * 60 * 1000).unref();
|
||||
setTimeout(checkUpgradeAlerts, 30000).unref();
|
||||
chain.startIndexer(evt=>{
|
||||
// Any on-chain event makes cached member views stale (the new member, the
|
||||
|
||||
Reference in New Issue
Block a user