[object Object]

← back to Dw Staged Active Viewer

Read-only viewer of cadence staged-for-active SKU backlog (matches --gate; sort+density+date-chip)

6c5fa3764198a48458706ea5d85a3436445d84f5 · 2026-06-23 11:35:48 -0700 · Steve Studio

Files touched

Diff

commit 6c5fa3764198a48458706ea5d85a3436445d84f5
Author: Steve Studio <steve@designerwallcoverings.com>
Date:   Tue Jun 23 11:35:48 2026 -0700

    Read-only viewer of cadence staged-for-active SKU backlog (matches --gate; sort+density+date-chip)
---
 .gitignore        |   9 +++
 package.json      |  11 +++
 public/index.html | 213 +++++++++++++++++++++++++++++++++++++++++++++++++++
 server.js         | 225 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 458 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4a0b3ea
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules/
+.env
+.env.*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..5b5384c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,11 @@
+{
+  "name": "dw-staged-active-viewer",
+  "version": "1.0.0",
+  "private": true,
+  "description": "Read-only local viewer of every SKU staged to go Active via the DW metered cadence. Reuses cadence-import.js gate logic so counts match --gate.",
+  "main": "server.js",
+  "scripts": {
+    "start": "node server.js"
+  },
+  "license": "UNLICENSED"
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..f2599de
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,213 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>DW — Staged for Active (cadence backlog)</title>
+<style>
+  :root { --cols: 5; --bg:#0f1115; --card:#191c23; --ink:#e7e9ee; --mut:#8b91a0; --line:#2a2e38; --accent:#6ea8fe; }
+  * { box-sizing: border-box; }
+  body { margin:0; font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; background:var(--bg); color:var(--ink); }
+  header { position:sticky; top:0; z-index:10; background:#12151c; border-bottom:1px solid var(--line); padding:12px 18px; }
+  h1 { font-size:16px; margin:0 0 8px; font-weight:650; }
+  .totals { color:var(--mut); font-size:13px; margin-bottom:10px; }
+  .totals b { color:var(--ink); }
+  .vendorbar { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:10px; }
+  .vchip { cursor:pointer; border:1px solid var(--line); background:#1b1f28; color:var(--ink); padding:4px 9px; border-radius:14px; font-size:12px; }
+  .vchip.active { border-color:var(--accent); background:#22304a; color:#fff; }
+  .vchip .n { color:var(--mut); margin-left:5px; }
+  .vchip.active .n { color:#bcd2ff; }
+  .controls { display:flex; align-items:center; gap:18px; flex-wrap:wrap; }
+  .controls label { color:var(--mut); font-size:12px; display:flex; align-items:center; gap:6px; }
+  select, input[type=range] { accent-color:var(--accent); }
+  select { background:#1b1f28; color:var(--ink); border:1px solid var(--line); border-radius:7px; padding:5px 8px; font-size:13px; }
+  main { padding:16px 18px 60px; }
+  .grid { display:grid; grid-template-columns:repeat(var(--cols), minmax(0,1fr)); gap:12px; }
+  .card { background:var(--card); border:1px solid var(--line); border-radius:10px; overflow:hidden; display:flex; flex-direction:column; }
+  .thumb { aspect-ratio:1/1; background:#0c0e13 center/cover no-repeat; border-bottom:1px solid var(--line); }
+  .thumb.empty { display:flex; align-items:center; justify-content:center; color:#3b414f; font-size:11px; }
+  .body { padding:9px 10px 11px; display:flex; flex-direction:column; gap:4px; }
+  .ttl { font-size:13px; font-weight:600; line-height:1.3; }
+  .sub { color:var(--mut); font-size:11.5px; }
+  .sku { font-family:ui-monospace,Menlo,monospace; font-size:11px; color:#aab3c5; }
+  .price { font-size:12.5px; }
+  .price b { color:#9be59b; }
+  .price .map { color:#ffd479; font-size:10px; border:1px solid #5c4a17; border-radius:4px; padding:0 4px; margin-left:5px; }
+  .price .smp { color:var(--mut); }
+  .when { color:#9aa3b5; font-size:10.5px; display:flex; align-items:center; gap:4px; }
+  .line-badge { display:inline-block; font-size:10px; color:#cdb6ff; background:#241b38; border:1px solid #3a2c5c; border-radius:4px; padding:0 5px; }
+  .more { text-align:center; margin:22px 0; }
+  .more button { background:#22304a; color:#fff; border:1px solid var(--accent); border-radius:8px; padding:9px 22px; cursor:pointer; font-size:13px; }
+  .loading { color:var(--mut); text-align:center; padding:30px; }
+</style>
+</head>
+<body>
+<header>
+  <h1>Staged for Active — metered cadence backlog <span style="color:var(--mut);font-weight:400;font-size:12px">(read-only · matches <code>cadence-import.js --gate</code>)</span></h1>
+  <div class="totals" id="totals">Loading…</div>
+  <div class="vendorbar" id="vendorbar"></div>
+  <div class="controls">
+    <label>Sort
+      <select id="sort">
+        <option value="newest">Newest</option>
+        <option value="oldest">Oldest</option>
+        <option value="color">Color</option>
+        <option value="style">Style</option>
+        <option value="sku">SKU A→Z</option>
+        <option value="title">Title A→Z</option>
+        <option value="priceup">Price ↑</option>
+        <option value="pricedown">Price ↓</option>
+      </select>
+    </label>
+    <label>Density
+      <input type="range" id="density" min="2" max="9" step="1" value="5">
+      <span id="densval" style="color:var(--ink)">5</span>
+    </label>
+  </div>
+</header>
+<main>
+  <div class="grid" id="grid"></div>
+  <div class="loading" id="loading" style="display:none">Loading…</div>
+  <div class="more"><button id="loadmore" style="display:none">Load more</button></div>
+</main>
+
+<script>
+const LS = {
+  get sort(){ return localStorage.getItem('dwsav.sort') || 'newest'; },
+  set sort(v){ localStorage.setItem('dwsav.sort', v); },
+  get density(){ return localStorage.getItem('dwsav.density') || '5'; },
+  set density(v){ localStorage.setItem('dwsav.density', v); },
+  get vendor(){ return localStorage.getItem('dwsav.vendor') || ''; },
+  set vendor(v){ localStorage.setItem('dwsav.vendor', v); },
+};
+
+const PAGE = 200;
+let state = { vendor: LS.vendor, offset: 0, rows: [], done: false };
+
+const $ = s => document.querySelector(s);
+const grid = $('#grid'), totalsEl = $('#totals'), barEl = $('#vendorbar');
+
+// ---- created date+time chip (Steve admin rule) ----
+function fmtWhen(iso){
+  if (!iso) return { txt:'no date', title:'' };
+  const d = new Date(iso.replace(' ', 'T'));
+  if (isNaN(d)) return { txt:'no date', title:String(iso) };
+  return {
+    txt: d.toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' }),
+    title: d.toISOString(),
+  };
+}
+
+// ---- sort buckets ----
+const COLOR_BUCKETS = [
+  ['black', /black|onyx|ebony|charcoal|jet/i],['white', /white|ivory|alabaster|chalk|snow/i],
+  ['warm', /red|orange|terracotta|rust|gold|amber|brass|copper|sienna|coral|peach/i],
+  ['green', /green|olive|sage|emerald|moss|celadon|verde/i],
+  ['blue', /blue|navy|teal|indigo|azure|cobalt|denim/i],
+  ['purple', /purple|violet|plum|mauve|lilac|aubergine/i],
+  ['neutral', /grey|gray|greige|taupe|beige|oatmeal|linen|stone|sand|cream|natural/i],
+];
+function colorKey(r){
+  const s = `${r.color_name||''} ${r.pattern_name||''}`;
+  for (let i=0;i<COLOR_BUCKETS.length;i++) if (COLOR_BUCKETS[i][1].test(s)) return i;
+  return 99;
+}
+const STYLE_BUCKETS = [
+  ['floral', /floral|flower|botanic|rose|peony|leaf|foliage|garden/i],
+  ['damask', /damask|scroll|baroque|medallion/i],
+  ['geometric', /geometric|stripe|grid|chevron|trellis|herringbone|dot/i],
+  ['toile', /toile|chinoiserie|scenic|mural/i],
+  ['texture', /grasscloth|linen|silk|weave|texture|plain|solid/i],
+];
+function styleKey(r){
+  const s = `${r.pattern_name||''} ${r.collection||''}`;
+  for (let i=0;i<STYLE_BUCKETS.length;i++) if (STYLE_BUCKETS[i][1].test(s)) return i;
+  return 99;
+}
+function titleOf(r){
+  const p = r.pattern_name || r.dw_sku;
+  const c = r.color_name ? ' ' + r.color_name : '';
+  return `${p}${c}`;
+}
+function sortRows(rows, mode){
+  const a = rows.slice();
+  switch(mode){
+    case 'newest': a.sort((x,y)=>String(y.staged_at||'').localeCompare(String(x.staged_at||''))); break;
+    case 'oldest': a.sort((x,y)=>String(x.staged_at||'').localeCompare(String(y.staged_at||''))); break;
+    case 'color': a.sort((x,y)=>colorKey(x)-colorKey(y)||titleOf(x).localeCompare(titleOf(y))); break;
+    case 'style': a.sort((x,y)=>styleKey(x)-styleKey(y)||titleOf(x).localeCompare(titleOf(y))); break;
+    case 'sku': a.sort((x,y)=>String(x.dw_sku).localeCompare(String(y.dw_sku))); break;
+    case 'title': a.sort((x,y)=>titleOf(x).localeCompare(titleOf(y))); break;
+    case 'priceup': a.sort((x,y)=>(x.retail||0)-(y.retail||0)); break;
+    case 'pricedown': a.sort((x,y)=>(y.retail||0)-(x.retail||0)); break;
+  }
+  return a;
+}
+
+function card(r){
+  const w = fmtWhen(r.staged_at);
+  const lineBadge = r.line && r.line !== r.vendor ? `<span class="line-badge">${esc(r.line)}</span>` : '';
+  const thumb = r.image_url
+    ? `<div class="thumb" style="background-image:url('${esc(r.image_url)}')"></div>`
+    : `<div class="thumb empty">no image</div>`;
+  return `<div class="card">
+    ${thumb}
+    <div class="body">
+      <div class="ttl">${esc(titleOf(r))}</div>
+      <div class="sub">${esc(r.vendor)}${lineBadge?' · ':''}${lineBadge}${r.collection?` · ${esc(r.collection)}`:''}</div>
+      <div class="sku">${esc(r.dw_sku)}</div>
+      <div class="price">cost ${r.cost!=null?'$'+r.cost.toFixed(2):'—'} → <b>$${r.retail!=null?r.retail.toFixed(2):'—'}</b>${r.priced_at_map?'<span class="map">MAP</span>':''} <span class="smp">· smpl $${r.sample_price}</span></div>
+      <div class="when" title="${esc(w.title)}">🕓 ${esc(w.txt)}</div>
+    </div>
+  </div>`;
+}
+function esc(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
+
+function render(){
+  grid.innerHTML = sortRows(state.rows, LS.sort).map(card).join('');
+}
+
+async function loadSummary(){
+  const r = await fetch('/api/summary'); const j = await r.json();
+  if (j.error){ totalsEl.textContent = 'ERROR: '+j.error; return; }
+  const tres = (j.vendors.find(v=>v.subLines)?.subLines||[]).find(s=>/tres tintas/i.test(s.name));
+  totalsEl.innerHTML = `<b>${j.grandTotal.toLocaleString()}</b> SKUs staged for active across <b>${j.vendors.length}</b> READY vendors`
+    + (tres?` · incl Tres Tintas <b>${tres.count.toLocaleString()}</b> (under Newwall)`:'')
+    + ` · <span style="font-size:11px">${new Date(j.generatedAt).toLocaleString()}</span>`;
+  barEl.innerHTML = `<span class="vchip ${state.vendor===''?'active':''}" data-v="">All<span class="n">${j.grandTotal.toLocaleString()}</span></span>`
+    + j.vendors.map(v=>`<span class="vchip ${state.vendor===v.vendor?'active':''}" data-v="${esc(v.vendor)}">${esc(v.vendor)}<span class="n">${v.count.toLocaleString()}</span></span>`).join('');
+  barEl.querySelectorAll('.vchip').forEach(el=>el.onclick=()=>{
+    state.vendor = el.dataset.v; LS.vendor = state.vendor;
+    state.offset = 0; state.rows = []; state.done = false;
+    barEl.querySelectorAll('.vchip').forEach(c=>c.classList.toggle('active', c.dataset.v===state.vendor));
+    grid.innerHTML=''; loadPage();
+  });
+}
+
+async function loadPage(){
+  if (state.done) return;
+  $('#loading').style.display='block'; $('#loadmore').style.display='none';
+  const qs = new URLSearchParams({ limit:String(PAGE), offset:String(state.offset) });
+  if (state.vendor) qs.set('vendor', state.vendor);
+  const r = await fetch('/api/skus?'+qs); const j = await r.json();
+  $('#loading').style.display='none';
+  if (j.error){ grid.insertAdjacentHTML('beforeend', `<div style="color:#ff8080">ERROR: ${esc(j.error)}</div>`); return; }
+  state.rows = state.rows.concat(j.rows);
+  state.offset += j.rows.length;
+  if (j.rows.length < PAGE) state.done = true;
+  render();
+  $('#loadmore').style.display = state.done ? 'none' : 'inline-block';
+}
+
+// ---- controls (persist to localStorage) ----
+function applyDensity(v){ document.documentElement.style.setProperty('--cols', v); $('#densval').textContent=v; }
+$('#sort').value = LS.sort;
+$('#sort').onchange = e => { LS.sort = e.target.value; render(); };
+$('#density').value = LS.density; applyDensity(LS.density);
+$('#density').oninput = e => { LS.density = e.target.value; applyDensity(e.target.value); };
+$('#loadmore').onclick = loadPage;
+
+loadSummary().then(loadPage);
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..67b2652
--- /dev/null
+++ b/server.js
@@ -0,0 +1,225 @@
+#!/usr/bin/env node
+/**
+ * DW Staged-for-Active Viewer  (READ-ONLY)
+ * ----------------------------------------
+ * Serves a local admin product grid listing EVERY SKU staged to go ACTIVE via the
+ * metered cadence (com.steve.dw-cadence-hourly). "Staged for active" is defined by
+ * reusing the cadence's OWN gate logic so counts match `cadence-import.js --gate`:
+ *
+ *   vendor in vendors.js  AND  vendor_registry.discount_confirmed = true
+ *   AND vendor NOT in cadence DENY_VENDORS
+ *   per SKU:  shopify_product_id empty  AND  (costExpr)::numeric > 0  AND  dedup_skip IS NOT TRUE
+ *
+ * Pricing per DW rules:  retail = round(cost/0.65/0.85, 2)  unless the vendor cfg has a
+ * sellExpr (MAP — Kravet family), in which case sell = sellExpr. Sample = $4.25.
+ *
+ * HARD read-only: this process only ever runs SELECT. No INSERT/UPDATE/DELETE, no Shopify.
+ */
+const http = require('http');
+const path = require('path');
+const fs = require('fs');
+const { execFileSync } = require('child_process');
+
+const PORT = process.env.PORT || 9846;
+const DB = 'dw_unified';
+
+// Reuse the cadence's verified vendor config + denylist verbatim so the staged set
+// is identical to what the cadence will actually create.
+const VENDORS = require(path.join(
+  process.env.HOME,
+  'Projects/Designer-Wallcoverings/shopify/scripts/cadence/vendors.js'
+));
+// DENY_VENDORS mirrors cadence-import.js (kept in sync; see that file's comment block).
+const DENY_VENDORS = new Set([
+  'Nicolette Mayer','Phillip Jeffries','Et Cie','Spoonflower','Colefax & Fowler','Carlisle',
+  'Paul Montgomery','Cowtan & Tout','Rebel Walls','Koroseal','Newmor','Wolf Gordon',
+  'Scalamandre','Desima','York Wallcoverings',
+]);
+
+const RETAIL = c => Math.round((c / 0.65 / 0.85) * 100) / 100;
+const SAMPLE_PRICE = '4.25';
+
+// ---- read-only psql (same invocation the cadence uses; -d dw_unified, no DSN) ----
+function psql(sql) {
+  return execFileSync('psql', ['-At', '-F', '\t', '-d', DB, '-c', sql],
+    { encoding: 'utf8', maxBuffer: 1 << 28 }).trim();
+}
+
+// ---- catalog-column detection (cached) ----
+const _colCache = {};
+function hasCol(table, col) {
+  const key = `${table}.${col}`;
+  if (key in _colCache) return _colCache[key];
+  const t = String(table).replace(/[^a-z0-9_]/gi, '');
+  const c = String(col).replace(/[^a-z0-9_]/gi, '');
+  let has = false;
+  try { has = (psql(`SELECT 1 FROM information_schema.columns WHERE table_name='${t}' AND column_name='${c}' LIMIT 1;`) || '').trim() === '1'; } catch { has = false; }
+  return (_colCache[key] = has);
+}
+function dedupSkipClause(table) {
+  return hasCol(table, 'dedup_skip') ? ' AND dedup_skip IS NOT TRUE' : '';
+}
+// Per-table "staged date" = first available of created_at / imported_at / updated_at / last_scraped.
+function stagedDateExpr(table) {
+  const cands = ['created_at', 'imported_at', 'updated_at', 'last_scraped'];
+  const present = cands.filter(c => hasCol(table, c));
+  if (!present.length) return 'NULL::timestamp';
+  return 'COALESCE(' + present.map(c => `${c}::timestamp`).join(', ') + ')';
+}
+
+let _confirmed = null;
+function confirmedDiscountVendors() {
+  if (_confirmed) return _confirmed;
+  const out = psql(`SELECT vendor_name FROM vendor_registry WHERE discount_confirmed IS TRUE;`);
+  return (_confirmed = new Set(out ? out.split('\n') : []));
+}
+
+// Vendors that are actually READY (config + confirmed discount + not denied).
+function readyVendors() {
+  const confirmed = confirmedDiscountVendors();
+  const out = [];
+  for (const [vendor, cfg] of Object.entries(VENDORS)) {
+    if (DENY_VENDORS.has(vendor)) continue;
+    if (!confirmed.has(vendor)) continue;
+    out.push({ vendor, cfg });
+  }
+  return out;
+}
+
+const WHERE = cfg => `coalesce(shopify_product_id::text,'')='' AND (${cfg.costExpr})::numeric > 0${dedupSkipClause(cfg.table)}`;
+
+// ---- per-vendor counts (matches `--gate`) ----
+function vendorCounts() {
+  const rows = [];
+  for (const { vendor, cfg } of readyVendors()) {
+    let n = 0;
+    try { n = parseInt(psql(`SELECT count(*) FROM ${cfg.table} WHERE ${WHERE(cfg)};`) || '0', 10); } catch (e) { n = 0; }
+    rows.push({ vendor, table: cfg.table, count: n });
+  }
+  rows.sort((a, b) => b.count - a.count);
+  return rows;
+}
+
+// Newwall true sub-lines (e.g. Tres Tintas) when the catalog carries vendor_name.
+function subLines(vendor, cfg) {
+  if (!hasCol(cfg.table, 'vendor_name')) return null;
+  try {
+    const out = psql(`SELECT vendor_name, count(*) FROM ${cfg.table} WHERE ${WHERE(cfg)} GROUP BY vendor_name ORDER BY 2 DESC;`);
+    if (!out) return null;
+    return out.split('\n').map(l => { const [name, c] = l.split('\t'); return { name: name || vendor, count: parseInt(c, 10) }; });
+  } catch { return null; }
+}
+
+// ---- paged SKU rows for one vendor ----
+function vendorRows(vendor, cfg, limit, offset) {
+  const t = cfg.table;
+  const lineSel = hasCol(t, 'vendor_name') ? 'vendor_name' : `'${vendor.replace(/'/g, "''")}'`;
+  const ov = cfg.colOverrides || {};
+  const col = name => ov[name] ? ov[name] : (hasCol(t, name) ? name : `NULL`);
+  const dateExpr = stagedDateExpr(t);
+  const sellSel = cfg.sellExpr ? `(${cfg.sellExpr})::numeric` : 'NULL::numeric';
+  const sql = `SELECT
+      dw_sku,
+      ${lineSel} AS line,
+      ${col('pattern_name')} AS pattern_name,
+      ${col('color_name')} AS color_name,
+      ${col('collection')} AS collection,
+      ${col('image_url')} AS image_url,
+      (${cfg.costExpr})::numeric AS cost,
+      ${sellSel} AS sell,
+      ${dateExpr} AS staged_at
+    FROM ${t}
+    WHERE ${WHERE(cfg)}
+    ORDER BY ${dateExpr} DESC NULLS LAST, dw_sku
+    LIMIT ${parseInt(limit, 10)} OFFSET ${parseInt(offset, 10)};`;
+  const out = psql(sql);
+  if (!out) return [];
+  return out.split('\n').map(line => {
+    const [dw_sku, lineName, pattern_name, color_name, collection, image_url, cost, sell, staged_at] = line.split('\t');
+    const c = parseFloat(cost) || 0;
+    const sellNum = sell && sell !== '' ? parseFloat(sell) : null;
+    const retail = sellNum && sellNum > 0 ? Math.round(sellNum * 100) / 100 : RETAIL(c);
+    return {
+      dw_sku,
+      vendor,
+      line: lineName && lineName !== '' ? lineName : vendor,
+      pattern_name: pattern_name === '' ? null : pattern_name,
+      color_name: color_name === '' ? null : color_name,
+      collection: collection === '' ? null : collection,
+      image_url: image_url === '' ? null : image_url,
+      cost: c ? Math.round(c * 100) / 100 : null,
+      retail,
+      sample_price: SAMPLE_PRICE,
+      priced_at_map: !!(sellNum && sellNum > 0),
+      staged_at: staged_at && staged_at !== '' ? staged_at : null,
+    };
+  });
+}
+
+// ---- HTTP ----
+function send(res, code, body, type = 'application/json') {
+  res.writeHead(code, { 'Content-Type': type, 'Cache-Control': 'no-store' });
+  res.end(body);
+}
+
+const server = http.createServer((req, res) => {
+  const u = new URL(req.url, `http://127.0.0.1:${PORT}`);
+
+  // Hard read-only: refuse anything but GET.
+  if (req.method !== 'GET') return send(res, 405, JSON.stringify({ error: 'read-only viewer; GET only' }));
+
+  if (u.pathname === '/api/summary') {
+    try {
+      const counts = vendorCounts();
+      const grand = counts.reduce((s, r) => s + r.count, 0);
+      const out = counts.map(r => {
+        const cfg = VENDORS[r.vendor];
+        const subs = subLines(r.vendor, cfg);
+        return { ...r, mapPriced: !!cfg.sellExpr, soldBy: cfg.soldBy, subLines: subs };
+      });
+      return send(res, 200, JSON.stringify({ grandTotal: grand, vendors: out, generatedAt: new Date().toISOString() }));
+    } catch (e) {
+      return send(res, 500, JSON.stringify({ error: String(e.message).slice(0, 300) }));
+    }
+  }
+
+  if (u.pathname === '/api/skus') {
+    try {
+      const vendor = u.searchParams.get('vendor') || '';
+      const limit = Math.min(parseInt(u.searchParams.get('limit') || '200', 10) || 200, 500);
+      const offset = Math.max(parseInt(u.searchParams.get('offset') || '0', 10) || 0, 0);
+      const ready = readyVendors();
+      const targets = vendor ? ready.filter(v => v.vendor === vendor) : ready;
+      if (vendor && !targets.length) return send(res, 400, JSON.stringify({ error: `unknown/non-ready vendor: ${vendor}` }));
+      // For "all vendors" we page across the union ordered by vendor then date; simplest correct
+      // approach for an admin tool is to require a vendor filter for deep paging, but still support
+      // a blended first-page view. We fetch limit rows from each target then globally sort+slice.
+      let rows = [];
+      for (const { vendor: v, cfg } of targets) {
+        rows = rows.concat(vendorRows(v, cfg, vendor ? limit : Math.min(limit, 60), vendor ? offset : 0));
+      }
+      if (!vendor) {
+        rows.sort((a, b) => String(b.staged_at || '').localeCompare(String(a.staged_at || '')));
+        rows = rows.slice(offset, offset + limit);
+      }
+      return send(res, 200, JSON.stringify({ vendor: vendor || null, limit, offset, count: rows.length, rows }));
+    } catch (e) {
+      return send(res, 500, JSON.stringify({ error: String(e.message).slice(0, 300) }));
+    }
+  }
+
+  // static
+  let p = u.pathname === '/' ? '/index.html' : u.pathname;
+  const file = path.join(__dirname, 'public', p);
+  if (!file.startsWith(path.join(__dirname, 'public'))) return send(res, 403, 'forbidden', 'text/plain');
+  fs.readFile(file, (err, data) => {
+    if (err) return send(res, 404, 'not found', 'text/plain');
+    const ext = path.extname(file);
+    const ct = ext === '.html' ? 'text/html' : ext === '.js' ? 'text/javascript' : ext === '.css' ? 'text/css' : 'application/octet-stream';
+    send(res, 200, data, ct);
+  });
+});
+
+server.listen(PORT, '127.0.0.1', () => {
+  console.log(`DW Staged-for-Active viewer (READ-ONLY) → http://127.0.0.1:${PORT}`);
+});

(oldest)  ·  back to Dw Staged Active Viewer  ·  Add launchd agent plist (KeepAlive, RunAtLoad, port 9846) fo ec00425 →