β 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
M public/index.htmlM server.js
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 β