[object Object]

← back to AbramsEgo

feat(revenue): dated income ledger + POST /api/revenue/record (auth) + per-window P&L + snapshot race fix (coalesce in-flight build). Recorded income now flips engine live and moves self-funding % in real time (task 03)

df0f579b9f5169721e0d9df089239ae1232f6203 · 2026-07-01 10:50:30 -0700 · Steve (AbramsEgo)

Files touched

Diff

commit df0f579b9f5169721e0d9df089239ae1232f6203
Author: Steve (AbramsEgo) <steve@designerwallcoverings.com>
Date:   Wed Jul 1 10:50:30 2026 -0700

    feat(revenue): dated income ledger + POST /api/revenue/record (auth) + per-window P&L + snapshot race fix (coalesce in-flight build). Recorded income now flips engine live and moves self-funding % in real time (task 03)
---
 build-queue/.loop.lock/pid |   1 -
 build-queue/STOP           |   0
 server.js                  | 115 ++++++++++++++++++++++++++++++++-------------
 3 files changed, 83 insertions(+), 33 deletions(-)

diff --git a/build-queue/.loop.lock/pid b/build-queue/.loop.lock/pid
deleted file mode 100644
index 8f1a099..0000000
--- a/build-queue/.loop.lock/pid
+++ /dev/null
@@ -1 +0,0 @@
-32374
diff --git a/build-queue/STOP b/build-queue/STOP
new file mode 100644
index 0000000..e69de29
diff --git a/server.js b/server.js
index e161591..4b831db 100644
--- a/server.js
+++ b/server.js
@@ -30,6 +30,7 @@ const HOME = os.homedir();
 const DATA_DIR = path.join(__dirname, 'data');
 const SNAPSHOT = path.join(DATA_DIR, 'data.json');
 const REVENUE_FILE = path.join(DATA_DIR, 'revenue.json');
+const REVENUE_LEDGER = path.join(DATA_DIR, 'revenue-ledger.jsonl');
 const VERSION = require('./package.json').version;
 const START_TS = Date.now();
 
@@ -294,17 +295,45 @@ function defaultRevenue() {
     ],
   };
 }
+// dated income rows: {ts, engine, amount, source} appended by POST /api/revenue/record.
+// Summed per-engine (all-time) and per-window so the P&L reflects REAL income by
+// today/week/month rather than a flat total.
+function readRevenueLedger() {
+  const now = Date.now();
+  const wins = { today: 24 * 3.6e6, week: 7 * 24 * 3.6e6, month: 30 * 24 * 3.6e6 };
+  const windowSums = { today: 0, week: 0, month: 0 };
+  const byEngine = {};
+  try {
+    const lines = fs.readFileSync(REVENUE_LEDGER, 'utf8').split('\n');
+    for (const ln of lines) {
+      if (!ln.trim()) continue;
+      let j; try { j = JSON.parse(ln); } catch (e) { continue; }
+      const t = Date.parse(j.ts); const a = Number(j.amount) || 0;
+      if (isNaN(t)) continue;
+      byEngine[j.engine] = (byEngine[j.engine] || 0) + a;
+      for (const [k, ms] of Object.entries(wins)) if (t >= now - ms) windowSums[k] += a;
+    }
+  } catch (e) {}
+  return { windowSums, byEngine };
+}
 function collectRevenue() {
-  try { return JSON.parse(fs.readFileSync(REVENUE_FILE, 'utf8')); }
-  catch (e) { const d = defaultRevenue(); try { fs.writeFileSync(REVENUE_FILE, JSON.stringify(d, null, 2)); } catch (e2) {} return d; }
+  let base;
+  try { base = JSON.parse(fs.readFileSync(REVENUE_FILE, 'utf8')); }
+  catch (e) { base = defaultRevenue(); try { fs.writeFileSync(REVENUE_FILE, JSON.stringify(base, null, 2)); } catch (e2) {} }
+  const led = readRevenueLedger();
+  // fold real income into each engine; an engine with income is de-facto "live"
+  base.engines = (base.engines || []).map((e) => ({ ...e, amount: round(led.byEngine[e.key] || 0, 2), status: (led.byEngine[e.key] || 0) > 0 ? 'live' : e.status }));
+  base.windowSums = led.windowSums;
+  base.total = round(Object.values(led.byEngine).reduce((s, v) => s + v, 0), 2);
+  return base;
 }
 
 function buildPnL(cost, revenue) {
   const out = {};
   for (const win of ['today', 'week', 'month']) {
     const c = (cost[win] && cost[win].total) || 0;
-    // revenue is a running total; for windows we currently only have live totals → attribute all to "month", 0 else until engines log dated income.
-    const rev = win === 'month' ? (revenue.engines || []).reduce((s, e) => s + (e.amount || 0), 0) : (revenue.engines || []).reduce((s, e) => s + (e.amount || 0), 0);
+    // real dated income per window from the revenue ledger (0 until engines earn).
+    const rev = round((revenue.windowSums && revenue.windowSums[win]) || 0, 4);
     const net = round(rev - c, 4);
     out[win] = { cost: round(c, 4), revenue: round(rev, 4), net, selfFundingPct: c > 0 ? round((rev / c) * 100, 1) : null };
   }
@@ -325,37 +354,43 @@ function buildPnL(cost, revenue) {
 // ---------------------------------------------------------------------------
 // snapshot builder + stale-while-revalidate cache
 // ---------------------------------------------------------------------------
-let SNAP = { builtAt: null, building: false };
-async function buildSnapshot() {
-  if (SNAP.building) return SNAP;
-  SNAP.building = true;
+let SNAP = { builtAt: null };
+let BUILDING = null; // in-flight build promise — callers coalesce onto it so an
+                     // awaited /api/refresh (or a post-write rebuild) always
+                     // returns FRESH data, never a stale in-flight snapshot.
+async function doBuild() {
   const wrap = async (fn) => { try { return await fn(); } catch (e) { return { error: e.message }; } };
-  try {
-    const [system, localFleet, kamatera, canaries, crons] = await Promise.all([
-      wrap(collectSystem), wrap(collectLocalFleet), wrap(collectKamateraFleet), wrap(collectCanaries), wrap(collectCrons),
-    ]);
-    const [sessions, wins, officers] = await Promise.all([
-      wrap(() => fetchJSON(`${CNCP}/api/sessions`)),
-      wrap(() => fetchJSON(`${CNCP}/api/wins`)),
-      wrap(() => fetchJSON(`${CNCP}/api/officers`)),
-    ]);
-    const cost = wrap(collectCost) instanceof Promise ? await wrap(collectCost) : collectCost();
-    const revenue = collectRevenue();
-    const pnl = buildPnL(cost, revenue);
-    SNAP = {
-      builtAt: new Date().toISOString(),
-      building: false,
-      version: VERSION,
-      system, localFleet, kamatera, canaries, crons,
-      sessions: summarizeSessions(sessions),
-      wins: summarizeWins(wins),
-      officers: summarizeOfficers(officers),
-      cost, revenue, pnl,
-    };
-    try { fs.writeFileSync(SNAPSHOT, JSON.stringify(SNAP)); } catch (e) {}
-  } finally { SNAP.building = false; }
+  const [system, localFleet, kamatera, canaries, crons] = await Promise.all([
+    wrap(collectSystem), wrap(collectLocalFleet), wrap(collectKamateraFleet), wrap(collectCanaries), wrap(collectCrons),
+  ]);
+  const [sessions, wins, officers] = await Promise.all([
+    wrap(() => fetchJSON(`${CNCP}/api/sessions`)),
+    wrap(() => fetchJSON(`${CNCP}/api/wins`)),
+    wrap(() => fetchJSON(`${CNCP}/api/officers`)),
+  ]);
+  const cost = collectCost();
+  const revenue = collectRevenue();
+  const pnl = buildPnL(cost, revenue);
+  SNAP = {
+    builtAt: new Date().toISOString(),
+    version: VERSION,
+    system, localFleet, kamatera, canaries, crons,
+    sessions: summarizeSessions(sessions),
+    wins: summarizeWins(wins),
+    officers: summarizeOfficers(officers),
+    cost, revenue, pnl,
+  };
+  try { fs.writeFileSync(SNAPSHOT, JSON.stringify(SNAP)); } catch (e) {}
   return SNAP;
 }
+function buildSnapshot() {
+  if (BUILDING) return BUILDING;
+  BUILDING = doBuild().finally(() => { BUILDING = null; });
+  return BUILDING;
+}
+// force a build that reflects state written AFTER any in-flight build started
+// (used by the money-path write so recorded income shows up immediately).
+async function rebuildFresh() { if (BUILDING) { try { await BUILDING; } catch (e) {} } return buildSnapshot(); }
 function summarizeSessions(s) {
   const arr = Array.isArray(s) ? s : (s && s.sessions) || [];
   return { count: arr.length, recent: arr.slice(0, 12) };
@@ -404,6 +439,22 @@ for (const key of ['system', 'localFleet', 'kamatera', 'canaries', 'crons', 'ses
   app.get(`/api/${key}`, (req, res) => res.json(SNAP[key] || {}));
 }
 
+// Record real income against a revenue engine (auth-gated by the middleware
+// above). This is the ONLY money-touching write — it just logs income that
+// actually landed; it does NOT charge anyone. Turning an engine's *mechanism*
+// on (Stripe, ads, affiliate) stays gated to Steve.
+const REVENUE_ENGINES = ['sell_product', 'affiliate', 'billable_work', 'ads'];
+app.post('/api/revenue/record', async (req, res) => {
+  const b = req.body || {};
+  if (!REVENUE_ENGINES.includes(b.engine)) return res.status(400).json({ error: `engine must be one of ${REVENUE_ENGINES.join(', ')}` });
+  const amount = Number(b.amount);
+  if (!isFinite(amount)) return res.status(400).json({ error: 'amount must be a number' });
+  const row = { ts: new Date().toISOString(), engine: b.engine, amount: round(amount, 2), source: (b.source || '').toString().slice(0, 200) };
+  try { fs.appendFileSync(REVENUE_LEDGER, JSON.stringify(row) + '\n'); } catch (e) { return res.status(500).json({ error: 'write failed' }); }
+  await rebuildFresh();
+  res.json({ ok: true, recorded: row, pnl: SNAP.pnl && SNAP.pnl.today });
+});
+
 // AI chat — grounded in the current snapshot. Uses local Ollama if reachable
 // (unmetered, $0); otherwise returns a deterministic grounded summary. No paid
 // API is called from here without Steve wiring it — cost line always shown.

← fd8b534 landed: 7d cost-trend + per-model panel (task 01), energy-at  ·  back to AbramsEgo  ·  queue: mark tasks 01-03 done (completed by hand during sessi 2c7b504 →