[object Object]

← back to All Designerwallcoverings

livestock: one-click 'Check Live πŸ”„' β€” metered vendor-portal scrape endpoint + card control

bbcfd96e67d72a06e52e3271116f8a8edc1bc70e Β· 2026-07-07 08:08:06 -0700 Β· Steve Abrams

/api/livestock?sku=&live=1 spawns scripts/live-scrape.js (reuses browserbase romo-lib
session helper + documented WallQuest portal login), returns PUBLIC-SAFE availability +
our-retail price only (never cost/net). Self-gates to wired adapters (Malibu/PS Removable
-> WallQuest); dims for all others. 10-min per-SKU cache, concurrent-click coalescing,
per-IP + global-burst rate limits. Free stored-snapshot path stays $0. Card shows live
result + actual $ cost (or 'cached Β· $0'). Proven once live: DWQW-58220 OutOfStock, $0.0975 (logged).

Files touched

Diff

commit bbcfd96e67d72a06e52e3271116f8a8edc1bc70e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 7 08:08:06 2026 -0700

    livestock: one-click 'Check Live πŸ”„' β€” metered vendor-portal scrape endpoint + card control
    
    /api/livestock?sku=&live=1 spawns scripts/live-scrape.js (reuses browserbase romo-lib
    session helper + documented WallQuest portal login), returns PUBLIC-SAFE availability +
    our-retail price only (never cost/net). Self-gates to wired adapters (Malibu/PS Removable
    -> WallQuest); dims for all others. 10-min per-SKU cache, concurrent-click coalescing,
    per-IP + global-burst rate limits. Free stored-snapshot path stays $0. Card shows live
    result + actual $ cost (or 'cached Β· $0'). Proven once live: DWQW-58220 OutOfStock, $0.0975 (logged).
---
 public/index.html |  49 ++++++++++++++++++++++-
 server.js         | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 163 insertions(+), 1 deletion(-)

diff --git a/public/index.html b/public/index.html
index 46b376c..8e9e39b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -99,6 +99,16 @@
   .stockpop[hidden] { display:none; }
   .stockpop .sdot.in { color:#7fc98a; } .stockpop .sdot.out { color:#e08b8b; }
   .stockpop .sline { color:var(--txt); margin-top:2px; } .stockpop .sline.mut { color:var(--mut); }
+  /* METERED live-check control + popover (amber = money, distinct from the free green stock) */
+  .acts .im.livebtn, td.acts-td .im.livebtn { border-color:#4a341a; color:#e0b46a; }
+  .acts .im.livebtn:hover, td.acts-td .im.livebtn:hover { border-color:#e0b46a; }
+  .livepop { margin-top:2px; padding:6px 8px; border-radius:5px; background:#1c140c; border:1px solid #4a341a; color:var(--txt); font-size:calc(10.5px*var(--fs)); }
+  .livepop[hidden] { display:none; }
+  .livepop .sdot.in { color:#7fc98a; } .livepop .sdot.out { color:#e08b8b; }
+  .livepop .sline { margin-top:2px; } .livepop .sline.mut { color:var(--mut); }
+  .livepop .lcost { color:var(--accent); }
+  .lspin { display:inline-block; animation:lspin 1s linear infinite; }
+  @keyframes lspin { to { transform:rotate(360deg); } }
   /* list-view compact link cluster */
   td.acts-td { white-space:nowrap; }
   td.acts-td a, td.acts-td .intbtn { display:inline-block; font-size:11px; font-weight:600; padding:3px 8px; border-radius:5px; text-decoration:none; border:1px solid var(--line); margin:0 4px 3px 0; background:transparent; color:var(--accent); cursor:pointer; font-family:inherit; }
@@ -475,10 +485,15 @@ function actButtonsHTML(r) {
   const item = r.internal_product_url
     ? `<a class="im" href="${r.internal_product_url}" target="_blank" rel="noopener noreferrer">This Item β†—</a>`
     : `<a class="im disabled" title="No internal product page for this SKU yet">This Item</a>`;
+  // Check Stock = FREE stored snapshot ($0, local). Check Live = METERED real-time portal
+  // login+scrape (only enabled for vendors with a wired live adapter; dims otherwise).
   const stock = r.stock
     ? `<button class="im stockbtn" type="button">Check Stock</button>`
     : `<button class="im disabled" type="button" title="No stored stock snapshot β€” run /livestock ${esc(r.sku || '')}">Check Stock</button>`;
-  return `${web}<button class="intbtn" type="button">Internal β–Ύ</button><div class="intpanel" hidden>${admin}${viewer}${item}${stock}${stockPopHTML(r.stock)}</div>`;
+  const live = r.live_capable
+    ? `<button class="im livebtn" type="button" data-sku="${esc(r.sku || '')}" title="Metered vendor-portal scrape (~$0.15–0.30 Β· cached 10 min)">Check Live πŸ”„</button>`
+    : `<button class="im disabled" type="button" title="live check unavailable for this vendor">Check Live πŸ”„</button>`;
+  return `${web}<button class="intbtn" type="button">Internal β–Ύ</button><div class="intpanel" hidden>${admin}${viewer}${item}${stock}${live}${stockPopHTML(r.stock)}<div class="livepop" hidden></div></div>`;
 }
 
 // Delegated within an .acts container (which stops propagation so the card's own
@@ -488,6 +503,38 @@ function handleActsClick(e) {
   if (intbtn) { const p = intbtn.parentElement.querySelector('.intpanel'); if (p) { p.hidden = !p.hidden; intbtn.textContent = p.hidden ? 'Internal β–Ύ' : 'Internal β–΄'; } return; }
   const sb = e.target.closest('.stockbtn');
   if (sb) { const pop = sb.closest('.intpanel').querySelector('.stockpop'); if (pop) pop.hidden = !pop.hidden; return; }
+  const lb = e.target.closest('.livebtn');
+  if (lb) { fireLiveCheck(lb); return; }
+}
+
+// One-click METERED live check. Fires immediately (no confirm β€” server rate-limits + caches).
+// Shows a spinner, then the live result + the ACTUAL $ cost (or "cached Β· $0") inline.
+const fmtWhen = (s) => { const t = Date.parse(s); return isNaN(t) ? String(s || '') : fmtDate(t); };
+function renderLive(pop, d) {
+  if (d.rate_limited) { pop.innerHTML = `<span class="sdot out">●</span> ${esc(d.reason || 'rate limited')}${d.retry_after_s ? ` Β· retry in ${d.retry_after_s}s` : ''}`; return; }
+  if (d.live_available === false) { pop.innerHTML = `<span class="sline mut">πŸ”’ ${esc(d.reason || 'live check unavailable for this vendor')}</span>`; return; }
+  if (!d.ok) { pop.innerHTML = `<span class="sdot out">●</span> ${esc(d.reason || 'live scrape failed')}${d.cost_usd ? ` Β· <b class="lcost">$${Number(d.cost_usd).toFixed(2)} live</b>` : ''}`; return; }
+  const dot = d.in_stock === true ? '<span class="sdot in">●</span> In stock (live)'
+    : d.in_stock === false ? '<span class="sdot out">●</span> Out of stock (live)'
+    : `<span class="sdot">●</span> Availability: ${esc(d.availability || 'unknown')}`;
+  const bits = [];
+  if (d.lead_time) bits.push('Lead ' + esc(d.lead_time));
+  if (d.price != null) bits.push('$' + Number(d.price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }));
+  const cost = d.cached ? 'cached Β· $0' : ('$' + Number(d.cost_usd || 0).toFixed(2) + ' live');
+  const when = (d.cached ? 'cached Β· as of ' : 'fresh Β· ') + esc(fmtWhen(d.as_of));
+  pop.innerHTML = `${dot}${bits.length ? '<div class="sline">' + bits.join(' Β· ') + '</div>' : ''}<div class="sline mut">${when} Β· <b class="lcost">${cost}</b></div>`;
+}
+function fireLiveCheck(lb) {
+  if (lb.dataset.busy === '1') return;            // client debounce β€” no double-fire
+  const panel = lb.closest('.intpanel'); const pop = panel.querySelector('.livepop');
+  const sku = lb.dataset.sku;
+  pop.hidden = false; pop.className = 'livepop';
+  pop.innerHTML = '<span class="lspin">β—Œ</span> Checking live vendor portal…';
+  lb.dataset.busy = '1'; const orig = lb.textContent; lb.textContent = 'Checking…';
+  fetch('/api/livestock?sku=' + encodeURIComponent(sku) + '&live=1')
+    .then((r) => r.json()).then((d) => renderLive(pop, d))
+    .catch(() => { pop.innerHTML = '<span class="sdot out">●</span> Live check failed β€” try again'; })
+    .finally(() => { lb.dataset.busy = ''; lb.textContent = orig; });
 }
 
 function card(r) {
diff --git a/server.js b/server.js
index b3394c3..e78924b 100644
--- a/server.js
+++ b/server.js
@@ -227,6 +227,9 @@ function deriveRow(r) {
       return (base && r.handle) ? `${base}product/${encodeURIComponent(r.handle)}` : null;
     })(),
     stock: (sku && STOCK.get(String(sku).toUpperCase())) || null,
+    // live_capable = this SKU's display-vendor has a wired portal adapter (WallQuest-backed).
+    // Drives the card's "Check Live πŸ”„" enable/dim; the endpoint self-gates again server-side.
+    live_capable: !!liveAdapter(r.vendor),
     created: r.created_at_shopify ? new Date(r.created_at_shopify).getTime() : 0,
     updated: r.updated_at_shopify ? new Date(r.updated_at_shopify).getTime() : 0,
     // one lowercase haystack per row so search is a single .includes() pass
@@ -332,6 +335,116 @@ function loadMicrosites() {
   return micrositesCache;
 }
 
+// ── LIVE stock endpoint β€” /api/livestock?sku=<sku>[&live=1] ─────────────────────────
+// FREE default: the stored STOCK snapshot ($0, never a portal hit). live=1: the METERED
+// vendor-portal scrape (spawns scripts/live-scrape.js). Self-gates on a wired adapter,
+// 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; };
+
+function logBrowserbase(min, sku) {
+  try {
+    execFile('node', [COST_LOG, '--api', 'browserbase', '--units', `${min.toFixed(2)}:session_min`,
+      '--app', 'all-dw-livestock', '--note', `live stock scrape ${sku}`], () => {});
+  } catch { /* ledger is best-effort β€” never block the response */ }
+}
+
+// Spawn the live-scrape worker for ONE SKU; measure the session, estimate + LOG the
+// Browserbase cost, and return a PUBLIC-SAFE result. Never throws.
+function runLiveScrape(sku) {
+  return new Promise((resolve) => {
+    const t0 = Date.now();
+    execFile('node', [LIVE_WORKER, sku], { timeout: LIVE_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024, env: process.env },
+      (err, stdout) => {
+        const elapsedMin = Math.max(0.2, (Date.now() - t0) / 60000);
+        let parsed = null;
+        try { parsed = JSON.parse((stdout || '').trim().split('\n').filter(Boolean).pop()); } catch { /* no JSON */ }
+        if (!parsed) {
+          return resolve({ ok: false, live: true, live_available: true, sku,
+            reason: err ? ('live worker ' + (err.killed ? 'timed out' : 'error')) : 'no result from live worker', cost_usd: 0 });
+        }
+        if (parsed.available === false) {
+          // A Browserbase session may have opened before failing β†’ that IS billable, so log it.
+          if (parsed.session_opened) {
+            const cost = +(elapsedMin * BB_RATE_PER_MIN).toFixed(4);
+            logBrowserbase(elapsedMin, sku);
+            return resolve({ ok: false, live: true, live_available: true, sku, reason: parsed.reason || 'live scrape incomplete', cost_usd: cost, session_min: +elapsedMin.toFixed(2) });
+          }
+          return resolve({ ok: false, live: true, live_available: false, sku, reason: parsed.reason || 'live check unavailable for this vendor', cost_usd: 0 });
+        }
+        const cost = +(elapsedMin * BB_RATE_PER_MIN).toFixed(4);
+        logBrowserbase(elapsedMin, sku);
+        resolve({
+          ok: true, live: true, cached: false, sku, vendor: parsed.vendor || null,
+          in_stock: parsed.in_stock ?? null, lead_time: parsed.lead_time || null,
+          price: parsed.price ?? null, availability: parsed.raw_stock_label || null,
+          as_of: parsed.as_of || new Date().toISOString(), source: parsed.source || 'vendor-live',
+          cost_usd: cost, session_min: +elapsedMin.toFixed(2),
+        });
+      });
+  });
+}
+
+async function handleLiveStock(req, res, u) {
+  const sku = (u.searchParams.get('sku') || '').trim();
+  const wantLive = u.searchParams.get('live') === '1';
+  if (!sku) return sendJSON(res, 400, { ok: false, reason: 'sku required' });
+
+  const row = findRowBySku(sku);
+  const stored = STOCK.get(sku.toUpperCase()) || (row && row.stock) || null;
+
+  // FREE stored-snapshot path (default) β€” never a portal hit, $0.
+  if (!wantLive) {
+    return sendJSON(res, 200, {
+      ok: true, sku, live: false, cached: false, source: stored ? (stored.source || 'stored snapshot') : 'stored snapshot',
+      in_stock: stored ? stored.in_stock : null, quantity: stored ? (stored.quantity ?? null) : null,
+      lead_time: stored ? (stored.lead_time || null) : null, as_of: stored ? (stored.as_of || null) : null,
+      has_snapshot: !!stored, cost_usd: 0,
+    });
+  }
+
+  // ── LIVE (metered) path ──
+  const adapter = liveAdapter(row ? row.vendor : null);
+  if (!adapter) {
+    return sendJSON(res, 200, { ok: false, live: true, live_available: false, sku,
+      vendor: row ? row.vendor : null, reason: 'live check unavailable for this vendor', cost_usd: 0 });
+  }
+  const key = sku.toUpperCase();
+
+  // cache: repeat within the 10-min window returns the cached live result, $0.
+  const hit = liveCache.get(key);
+  if (hit && (Date.now() - hit.at) < LIVE_CACHE_MS) {
+    return sendJSON(res, 200, { ...hit.result, cached: true, cost_usd: 0, cache_age_s: Math.round((Date.now() - hit.at) / 1000) });
+  }
+  // coalesce: a scrape already running for this SKU β†’ ride it (no second bill).
+  if (liveInflight.has(key)) {
+    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 }); }
+  }
+  // per-IP rate window
+  const ip = (String(req.headers['x-forwarded-for'] || req.socket.remoteAddress || '')).split(',')[0].trim();
+  const now = Date.now();
+  const hits = (liveIpHits.get(ip) || []).filter((t) => now - t < LIVE_IP_WIN_MS);
+  if (hits.length >= LIVE_IP_MAX) {
+    return sendJSON(res, 429, { ok: false, live: true, rate_limited: true, sku,
+      reason: 'too many live checks β€” wait a moment', retry_after_s: Math.ceil((LIVE_IP_WIN_MS - (now - hits[0])) / 1000), cost_usd: 0 });
+  }
+  // global burst cap on concurrent portal sessions (protects spend + the vendor portal)
+  if (liveInflight.size >= LIVE_MAX_INFLIGHT) {
+    return sendJSON(res, 429, { ok: false, live: true, rate_limited: true, sku,
+      reason: 'live-check queue full β€” try again shortly', retry_after_s: 8, cost_usd: 0 });
+  }
+  hits.push(now); liveIpHits.set(ip, hits);
+
+  const p = runLiveScrape(sku).finally(() => liveInflight.delete(key));
+  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 }); }
+  if (result.ok) liveCache.set(key, { result, at: Date.now() });
+  return sendJSON(res, 200, result);
+}
+
 // Basic Auth gate (Steve 2026-07-02: all. is published but un/pw-gated).
 // Override with BASIC_AUTH=user:pass in .env; /healthz stays open for probes.
 const BASIC_AUTH = process.env.BASIC_AUTH || 'admin:DW2024!';
@@ -395,6 +508,8 @@ const server = http.createServer((req, res) => {
     return;
   }
 
+  if (u.pathname === '/api/livestock') { handleLiveStock(req, res, u); return; }
+
   if (u.pathname === '/api/vendors') { // legacy vendor directory API (vendors.html)
     try {
       res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'public, max-age=300' });

← ef3b79c all-dw chips: add 'This Item β†—' link β€” internal microsite pr  Β·  back to All Designerwallcoverings  Β·  livestock: self-contained Browserbase backend (lib/romo-lib. 435af85 β†’