[object Object]

← back to All Designerwallcoverings

live-stock: daily $ kill-switch — global per-day spend ceiling (DAILY_LIVE_CAP_USD, default $5) persisted to data/live-spend.json, survives pm2 restart; live path hard-stops at $0 before any Browserbase session once the day's total hits the cap; day_spend+cap surfaced on every metered response; align findRowBySku with the grid's dw_sku-first identity so live checks actually resolve

2c397b47347aac8d9e14b0f3045cd3df63b17a8b · 2026-07-07 10:06:41 -0700 · Steve

Files touched

Diff

commit 2c397b47347aac8d9e14b0f3045cd3df63b17a8b
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 7 10:06:41 2026 -0700

    live-stock: daily $ kill-switch — global per-day spend ceiling (DAILY_LIVE_CAP_USD, default $5) persisted to data/live-spend.json, survives pm2 restart; live path hard-stops at $0 before any Browserbase session once the day's total hits the cap; day_spend+cap surfaced on every metered response; align findRowBySku with the grid's dw_sku-first identity so live checks actually resolve
---
 .gitignore |  2 ++
 server.js  | 46 +++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 47 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index fd573d0..606bca2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,6 @@ build/
 
 # live crawl snapshot (regenerated every 10 min by scripts/crawl-microsites.js)
 data/microsites.json
+# daily live-scrape spend ledger (runtime artifact for the $ kill-switch)
+data/live-spend.json
 logs/
diff --git a/server.js b/server.js
index 86530d9..c8a2739 100644
--- a/server.js
+++ b/server.js
@@ -101,6 +101,30 @@ const liveCache = new Map();                 // UPPER(sku) → { result, at }
 const liveInflight = new Map();              // UPPER(sku) → Promise (coalesces concurrent clicks)
 const liveIpHits = new Map();                // ip → [timestamps] (per-IP rate window)
 
+// ── DAILY $ KILL-SWITCH ────────────────────────────────────────────────────────────
+// A GLOBAL per-calendar-day live-scrape spend ceiling that sits ON TOP of the per-IP +
+// concurrent-burst caps (those bound RATE, not the day's TOTAL). Every billed live scrape's
+// cost_usd is summed per UTC day and persisted to data/live-spend.json so the tally survives a
+// pm2 restart. Once the day's total reaches DAILY_LIVE_CAP_USD the live path HARD-STOPS BEFORE
+// any Browserbase session opens — returning a graceful {live_available:false, reason, cost_usd:0}
+// degrade the UI dims like any other. Cached/coalesced hits ($0, session already paid) still serve.
+const DAILY_LIVE_CAP_USD = Math.max(0.01, +(process.env.DAILY_LIVE_CAP_USD || 5) || 5);
+const SPEND_FILE = path.join(__dirname, 'data', 'live-spend.json');
+const spendDayKey = () => new Date().toISOString().slice(0, 10); // UTC calendar day
+function readSpendMap() { try { return JSON.parse(fs.readFileSync(SPEND_FILE, 'utf8')) || {}; } catch { return {}; } }
+function daySpendUSD() { return +(readSpendMap()[spendDayKey()] || 0); }
+function addDaySpend(usd) {
+  if (!(usd > 0)) return daySpendUSD();
+  const s = readSpendMap(), k = spendDayKey();
+  s[k] = +((+(s[k] || 0)) + usd).toFixed(4);
+  // prune keys older than ~14 days so the ledger file stays tiny
+  const cutoff = new Date(Date.now() - 14 * 864e5).toISOString().slice(0, 10);
+  for (const key of Object.keys(s)) if (key < cutoff) delete s[key];
+  try { fs.writeFileSync(SPEND_FILE, JSON.stringify(s)); } catch (e) { console.error('spend persist failed:', e.message); }
+  console.log(`live-spend: day ${k} → $${s[k].toFixed(4)} / cap $${DAILY_LIVE_CAP_USD.toFixed(2)}`);
+  return s[k];
+}
+
 // ── snapshot load ────────────────────────────────────────────────────────────────
 // Public-safe columns ONLY — cost_price / net_price / cost never selected.
 // The Mac2 mirror carries rollup columns (min_variant_price, online_store_published)
@@ -381,7 +405,12 @@ function loadMicrosites() {
 // caches each SKU's live result 10 min, coalesces concurrent clicks, per-IP + global-burst
 // rate-limited. PUBLIC-SAFE: only availability + our RETAIL price ship — never cost/net.
 const sendJSON = (res, code, obj) => { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };
-const findRowBySku = (sku) => { if (!sku) return null; const k = String(sku).toUpperCase(); return ROWS.find((r) => (r.sku || '').toUpperCase() === k) || null; };
+// Match on ANY identity the grid may pass — the products API serializes sku = dw_sku||variant_sku||sku,
+// so the live check can arrive with any of the three; match them all (else the adapter gate mis-dims).
+const findRowBySku = (sku) => {
+  if (!sku) return null; const k = String(sku).toUpperCase();
+  return ROWS.find((r) => (r.dw_sku || '').toUpperCase() === k || (r.variant_sku || '').toUpperCase() === k || (r.sku || '').toUpperCase() === k) || null;
+};
 
 function logBrowserbase(min, sku) {
   try {
@@ -465,6 +494,16 @@ async function handleLiveStock(req, res, u) {
     try { const r = await liveInflight.get(key); return sendJSON(res, 200, { ...r, cached: true, coalesced: true, cost_usd: 0 }); }
     catch { return sendJSON(res, 200, { ok: false, live: true, live_available: true, sku, reason: 'live scrape failed', cost_usd: 0 }); }
   }
+  // ── DAILY $ KILL-SWITCH — the TOTAL ceiling the rate limits don't provide ──
+  // Checked AFTER cache/coalesce (those are free) but BEFORE any new billable session. When the
+  // day's summed spend hits the cap, hard-stop at $0 — no Browserbase session is ever opened.
+  const daySpend = daySpendUSD();
+  if (daySpend >= DAILY_LIVE_CAP_USD) {
+    return sendJSON(res, 200, { ok: false, live: true, live_available: false, sku,
+      vendor: row ? row.vendor : null, tier: cov ? cov.tier : null,
+      reason: `daily live-stock budget reached ($${daySpend.toFixed(2)}/$${DAILY_LIVE_CAP_USD.toFixed(2)})`,
+      day_spend_usd: +daySpend.toFixed(4), day_cap_usd: DAILY_LIVE_CAP_USD, cost_usd: 0 });
+  }
   // per-IP rate window
   const ip = (String(req.headers['x-forwarded-for'] || req.socket.remoteAddress || '')).split(',')[0].trim();
   const now = Date.now();
@@ -484,6 +523,11 @@ async function handleLiveStock(req, res, u) {
   liveInflight.set(key, p);
   let result;
   try { result = await p; } catch (e) { return sendJSON(res, 200, { ok: false, live: true, live_available: true, sku, reason: 'live scrape error: ' + e.message, cost_usd: 0 }); }
+  // Accrue any real spend (a session may bill even on a failed parse) toward the daily ceiling,
+  // then surface the running day total + cap on every metered response.
+  if (result.cost_usd > 0) addDaySpend(result.cost_usd);
+  result.day_spend_usd = +daySpendUSD().toFixed(4);
+  result.day_cap_usd = DAILY_LIVE_CAP_USD;
   if (result.ok) liveCache.set(key, { result, at: Date.now() });
   return sendJSON(res, 200, result);
 }

← 875d338 live-stock verification: 4 metered scrapes ($0.234 total) pr  ·  back to All Designerwallcoverings  ·  live-stock: generic schema-public adapter (login-free schema d1be0d2 →