[object Object]

← back to Re Flyer Aggregator

consumable surface: read-only deal-assets viewer/API over usre.deal_assets_v (basic-auth, sort+density grid), pm2 re-flyers-viewer :9860; promoted canonical to usre (TK-10708, gate approved)

24fe2f49ff7ba417553ee842c0dc85d76dd59466 · 2026-08-19 10:30:49 -0700 · Steve Abrams

Files touched

Diff

commit 24fe2f49ff7ba417553ee842c0dc85d76dd59466
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 19 10:30:49 2026 -0700

    consumable surface: read-only deal-assets viewer/API over usre.deal_assets_v (basic-auth, sort+density grid), pm2 re-flyers-viewer :9860; promoted canonical to usre (TK-10708, gate approved)
---
 .deploy.conf              |  5 ++++
 public/index.html         | 63 +++++++++++++++++++++++++++++++++++++++++++++++
 scripts/ingest-assets.mjs |  2 +-
 server.js                 | 58 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 127 insertions(+), 1 deletion(-)

diff --git a/.deploy.conf b/.deploy.conf
new file mode 100644
index 0000000..2b9ed2b
--- /dev/null
+++ b/.deploy.conf
@@ -0,0 +1,5 @@
+PROJECT_NAME=re-flyers-viewer
+DEPLOY_PATH=/root/public-projects/re-flyer-aggregator
+HEALTH_URL=http://127.0.0.1:9860/health
+# NOTE: Kamatera deploy also requires promoting external_marketing_asset + asset_subject_link
+# + deal_assets_v + the 15 validated rows into Kamatera's usre first (db/001 + db/002 + ingest).
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..15a92a0
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,63 @@
+<!doctype html><html lang="en"><head><meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Deal marketing assets — RE flyer aggregator</title>
+<style>
+ :root{--cols:2}
+ body{font-family:Inter,Helvetica,Arial,sans-serif;margin:0;background:#f4f6fb;color:#141414}
+ header{background:#0E2350;color:#fff;padding:18px 28px}
+ header h1{margin:0;font-size:20px} header .s{color:#E8A81C;font-size:12px;margin-top:3px}
+ .controls{display:flex;gap:18px;align-items:center;flex-wrap:wrap;padding:14px 28px;background:#fff;border-bottom:1px solid #e6e9f2;position:sticky;top:0;z-index:5}
+ .controls label{font-size:12px;color:#6B7280;margin-right:6px}
+ select,input[type=range]{vertical-align:middle}
+ .wrap{padding:20px 28px;display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:16px}
+ .card{background:#fff;border-radius:10px;padding:16px 18px;box-shadow:0 2px 10px rgba(0,0,0,.06)}
+ .price{float:right;font-weight:800;color:#0E7C4A;font-size:18px}
+ .card h2{margin:0 0 2px;font-size:17px;color:#16357A}
+ .sub{color:#6B7280;font-size:12px;margin-bottom:10px}
+ ul{list-style:none;padding:0;margin:0} li{padding:5px 0;border-top:1px solid #eef0f6;font-size:13px}
+ a{color:#16357A;text-decoration:none} a:hover{text-decoration:underline}
+ .b{color:#fff;font-size:9px;font-weight:700;padding:2px 6px;border-radius:4px;letter-spacing:.4px;margin-right:5px}
+ .meta{color:#6B7280;font-size:11px}
+</style></head><body>
+<header><h1>Deal marketing assets — aggregated (usre · deal_assets_v)</h1>
+<div class="s">Read-only consumer surface · TK-10708 · GATED marketplace assets excluded</div></header>
+<div class="controls">
+  <div><label>Sort</label><select id="sort">
+    <option value="price">Price ↓</option><option value="date">Newest</option>
+    <option value="assets">Most assets</option><option value="conf">Best match</option>
+    <option value="city">City A→Z</option>
+  </select></div>
+  <div><label>Density</label><input type="range" id="dens" min="1" max="4" step="1" value="2"></div>
+  <div class="meta" id="stat"></div>
+</div>
+<div class="wrap" id="grid"></div>
+<script>
+const BADGE={self_generated:['#16357A','RECAP'],broker_owned:['#0E7C4A','BROKER'],first_party:['#8A5A00','FIRST-PARTY']};
+const money=n=>'$'+(+n).toLocaleString('en-US');
+const tc=s=>(s||'').toLowerCase().replace(/\b\w/g,c=>c.toUpperCase());
+let DEALS=[];
+function assetLi(a){const[bg,lbl]=BADGE[a.rights_basis]||['#6B7280',(a.rights_basis||'').toUpperCase()];
+  const href=a.source_landing_url||(a.local_path?a.local_path.replace(/^out\//,'out/'):'#');
+  return `<li><span class="b" style="background:${bg}">${lbl}</span>
+   <a href="${href}" target="_blank">${a.title||a.source_name}</a>
+   <span class="meta">· ${a.asset_type}${a.match_confidence!=null?' · '+a.match_confidence:''}</span></li>`;}
+function card(d){return `<div class="card"><span class="price">${money(d.sale_price)}</span>
+  <h2>${tc(d.address)}</h2>
+  <div class="sub">${tc(d.city)}${d.county_name?', '+tc(d.county_name)+' County':''} · ${tc(d.ctype||'')} · sold ${d.sale_date} · doc ${d.doc_number}</div>
+  <ul>${(d.assets||[]).map(assetLi).join('')}</ul></div>`;}
+function sortKey(m){return{
+  price:(a,b)=>b.sale_price-a.sale_price,
+  date:(a,b)=>(b.sale_date||'').localeCompare(a.sale_date||''),
+  assets:(a,b)=>b.assets.length-a.assets.length,
+  conf:(a,b)=>Math.max(0,...b.assets.map(x=>+x.match_confidence||0))-Math.max(0,...a.assets.map(x=>+x.match_confidence||0)),
+  city:(a,b)=>(a.city||'').localeCompare(b.city||'')}[m];}
+function render(){const m=sort.value;localStorage.reflyersSort=m;
+  document.getElementById('grid').innerHTML=[...DEALS].sort(sortKey(m)).map(card).join('');
+  document.getElementById('stat').textContent=DEALS.length+' deals · '+DEALS.reduce((n,d)=>n+d.assets.length,0)+' assets';}
+function dens(){document.documentElement.style.setProperty('--cols',document.getElementById('dens').value);localStorage.reflyersDens=document.getElementById('dens').value;}
+sort.onchange=render; document.getElementById('dens').oninput=dens;
+if(localStorage.reflyersSort)sort.value=localStorage.reflyersSort;
+if(localStorage.reflyersDens)document.getElementById('dens').value=localStorage.reflyersDens;
+dens();
+fetch('/api/deal-assets').then(r=>r.json()).then(j=>{DEALS=j.deals||[];render();});
+</script></body></html>
diff --git a/scripts/ingest-assets.mjs b/scripts/ingest-assets.mjs
index 9b8a245..14f5b1a 100644
--- a/scripts/ingest-assets.mjs
+++ b/scripts/ingest-assets.mjs
@@ -14,7 +14,7 @@ import { fileURLToPath } from 'node:url';
 import { dirname, join } from 'node:path';
 
 const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
-const DB = 'reflyers';
+const DB = process.env.REFLYERS_DB || 'reflyers';  // promote to usre via REFLYERS_DB=usre
 const psql = sql => execFileSync('psql', [DB, '-t', '-A', '-c', sql], { encoding: 'utf8' }).trim();
 const lit = v => (v === null || v === undefined || v === '') ? 'NULL' : `'${String(v).replace(/'/g, "''")}'`;
 const litB = v => (v === null || v === undefined) ? 'NULL' : (v ? 'true' : 'false');
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..82b7863
--- /dev/null
+++ b/server.js
@@ -0,0 +1,58 @@
+#!/usr/bin/env node
+// TK-10708  Read-only deal-assets viewer/API (the consumable surface CRCP/RENTV read,
+// per codex). Serves usre.deal_assets_v joined to recent_commercial_deals. Zero deps
+// (node http + psql). Basic-auth admin/DW2024! per the DW viewer standard.
+
+import http from 'node:http';
+import { execFileSync } from 'node:child_process';
+import { readFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+
+const ROOT = dirname(fileURLToPath(import.meta.url));
+const PORT = process.env.PORT || 9860;
+const USER = process.env.BASIC_USER || 'admin';
+const PASS = process.env.BASIC_PASS || 'DW2024!';
+const DB = process.env.USRE_DB || 'usre';
+
+const q = sql => execFileSync('psql', [DB, '-t', '-A', '-c', sql], { encoding: 'utf8' }).trim();
+
+function dealAssets() {
+  // deals that HAVE at least one consumer-visible asset, newest+priciest first, assets nested
+  const deals = q(`SELECT row_to_json(t) FROM (
+      SELECT DISTINCT d.county_fips,d.doc_number,d.address,d.city,d.county_name,
+             d.sale_price::bigint AS sale_price,d.sale_date,d.ctype,d.sqft
+      FROM recent_commercial_deals d
+      JOIN deal_assets_v v ON v.county_fips=d.county_fips AND v.doc_number=d.doc_number
+      ORDER BY sale_price DESC) t`).split('\n').filter(Boolean).map(JSON.parse);
+  const assets = q(`SELECT row_to_json(t) FROM (
+      SELECT county_fips,doc_number,asset_type,tier,rights_basis,title,source_name,
+             source_landing_url,local_path,match_confidence
+      FROM deal_assets_v) t`).split('\n').filter(Boolean).map(JSON.parse);
+  const byKey = {};
+  for (const a of assets) (byKey[a.county_fips + '|' + a.doc_number] ||= []).push(a);
+  return deals.map(d => ({ ...d, assets: byKey[d.county_fips + '|' + d.doc_number] || [] }));
+}
+
+const server = http.createServer((req, res) => {
+  // basic auth gate (the 401 is the healthy gate)
+  const hdr = req.headers.authorization || '';
+  const [, b64] = hdr.split(' ');
+  const [u, p] = Buffer.from(b64 || '', 'base64').toString().split(':');
+  if (u !== USER || p !== PASS) {
+    res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="re-flyers"' }); return res.end('Auth required');
+  }
+  try {
+    if (req.url.startsWith('/api/deal-assets')) {
+      const data = dealAssets();
+      res.writeHead(200, { 'content-type': 'application/json' });
+      return res.end(JSON.stringify({ count: data.length, deals: data }));
+    }
+    if (req.url === '/health') { res.writeHead(200); return res.end('ok'); }
+    res.writeHead(200, { 'content-type': 'text/html' });
+    return res.end(readFileSync(join(ROOT, 'public', 'index.html')));
+  } catch (e) {
+    res.writeHead(500, { 'content-type': 'text/plain' }); res.end('err: ' + e.message);
+  }
+});
+server.listen(PORT, () => console.log(`re-flyers viewer on :${PORT} (basic-auth ${USER})`));

← 7c6e855 aggregation store: local reflyers staging DB + ingest + deal  ·  back to Re Flyer Aggregator  ·  auto-data-snapshot: 2026-08-19T11:14:55 (1 data files) — RUN 348247d →