← 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
A .deploy.confA public/index.htmlM scripts/ingest-assets.mjsA server.js
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 →