← 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 →