← back to Re Flyer Aggregator
href-drill: add drill primitives to re-flyer-aggregator (TK-10903)
96c06fd7812f0edfd31b6ef2a300d7ce8a6f3b82 · 2026-08-27 04:44:53 -0700 · Steve
Files touched
A public/drill.jsM public/flyers/index.htmlM public/index.htmlM server.js
Diff
commit 96c06fd7812f0edfd31b6ef2a300d7ce8a6f3b82
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Aug 27 04:44:53 2026 -0700
href-drill: add drill primitives to re-flyer-aggregator (TK-10903)
---
public/drill.js | 36 ++++++++++++++++++++++++++++++++++++
public/flyers/index.html | 27 +--------------------------
public/index.html | 12 +++++++++++-
server.js | 19 ++++++++++++++++++-
4 files changed, 66 insertions(+), 28 deletions(-)
diff --git a/public/drill.js b/public/drill.js
new file mode 100644
index 0000000..e63cdd3
--- /dev/null
+++ b/public/drill.js
@@ -0,0 +1,36 @@
+// ── href-to-deeper-data primitives — SINGLE SOURCE OF TRUTH (HARD rule, 2026-07-31) ──
+// Every displayed data point must be an href to its deeper (filtered) data. These are the
+// reusable atoms; a viewer's index.html loads this file, then defines FIELDS, FIL, $, esc,
+// and applyFilters (which these reference at call time via the shared classic-script global
+// scope). Canonical copy: ~/Projects/_shared/web/drill.js → each viewer's public/drill.js.
+// Memory: all-data-points-href-to-deeper-data. Reference viewers: astek-landing, schumacher-internal.
+
+// Build the URL for a given filter set — any count/atom becomes a real, shareable link.
+function qstr(fil, q, pat) {
+ const u = new URLSearchParams();
+ for (const [k] of FIELDS) { if (fil[k]) u.set(k, fil[k]); }
+ if (q) u.set('q', q); if (pat) u.set('pat', pat);
+ const s = u.toString(); return s ? ('?' + s) : location.pathname;
+}
+// Read the URL's filter state back into FIL + the search inputs (deep-link / back-button).
+function readURL() {
+ const u = new URLSearchParams(location.search);
+ for (const [k] of FIELDS) FIL[k] = u.get(k) || '';
+ $('#q').value = u.get('q') || ''; $('#pattern').value = u.get('pat') || '';
+}
+// Reflect the current filter state into the URL (push = new history entry, else replace).
+function writeURL(push) {
+ const url = qstr(FIL, $('#q').value.trim(), $('#pattern').value.trim());
+ history[push ? 'pushState' : 'replaceState']({}, '', url);
+}
+// drill(field,value,label,cls) → an <a> whose href IS the filtered view for that value.
+// cmd/ctrl/middle-click opens it in a new tab; plain click filters in place (delegated listener).
+function drill(field, val, label, cls) {
+ if (val == null || val === '') return '';
+ const href = qstr({ ...FIL, [field]: String(val) }, $('#q').value.trim(), $('#pattern').value.trim());
+ return `<a class="drill ${cls || ''}" href="${href}" data-filter="${field}" data-v="${esc(val)}" title="Filter to ${esc(val)}">${label != null ? esc(label) : esc(val)}</a>`;
+}
+// Atom drill target: SET the filter to this value (drill in), vs pickF which TOGGLES.
+function setF(k, v) { if (FIL[k] === String(v)) return; FIL[k] = String(v); applyFilters('push'); }
+
+window.qstr = qstr; window.readURL = readURL; window.writeURL = writeURL; window.drill = drill; window.setF = setF;
diff --git a/public/flyers/index.html b/public/flyers/index.html
index 5c8060a..6be2a9d 100644
--- a/public/flyers/index.html
+++ b/public/flyers/index.html
@@ -1,29 +1,4 @@
-<!doctype html><html lang="en"><head>
-<script>
-// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
-// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
-// https://user:pass@host/…), the browser poisons document.baseURI, so any
-// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
-// from a URL that includes credentials" and callers silently fall to an empty
-// state. Resolve every non-absolute request URL against the credential-free
-// location instead of baseURI. Placed first so it wraps window.fetch before any
-// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
-(function () {
- var _fetch = window.fetch.bind(window);
- var cleanBase = function () { return location.origin + location.pathname; };
- window.fetch = function (input, init) {
- try {
- if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
- input = new URL(input, cleanBase()).href;
- } else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
- input = new Request(new URL(input.url, cleanBase()).href, input);
- }
- } catch (_) { /* fall through to native */ }
- return _fetch(input, init);
- };
-})();
-</script>
-<meta charset="utf-8">
+<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Flyers — Abrams | CRE deal flyers</title>
<style>
diff --git a/public/index.html b/public/index.html
index 5f196e8..b74733e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,4 +1,5 @@
<!doctype html><html lang="en"><head>
+<script src="/drill.js"></script>
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
@@ -45,6 +46,7 @@
</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>
+<input type="hidden" id="q" value=""><input type="hidden" id="pattern" value="">
<div class="controls">
<div><label>Sort</label><select id="sort">
<option value="price">Price ↓</option><option value="date">Newest</option>
@@ -56,6 +58,13 @@
</div>
<div class="wrap" id="grid"></div>
<script>
+// href-drill primitives (TK-10903)
+var FIELDS=[['city','City'],['ctype','Type'],['county_name','County']];
+var FIL={city:'',ctype:'',county_name:''};
+var $=function(s){return document.querySelector(s);};
+function esc(s){return String(s||'').replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]));}
+function applyFilters(push){if(push)writeURL(true);render();}
+
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());
@@ -67,7 +76,7 @@ function assetLi(a){const[bg,lbl]=BADGE[a.rights_basis]||['#6B7280',(a.rights_ba
<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>
+ <div class="sub">${drill('city',d.city,tc(d.city))}${d.county_name?', '+drill('county_name',d.county_name,tc(d.county_name)+' County'):''}${d.ctype?' · '+drill('ctype',d.ctype,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,
@@ -83,5 +92,6 @@ 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();
+readURL();
fetch('/api/deal-assets').then(r=>r.json()).then(j=>{DEALS=j.deals||[];render();});
</script></body></html>
diff --git a/server.js b/server.js
index 82b7863..6830e55 100644
--- a/server.js
+++ b/server.js
@@ -5,7 +5,7 @@
import http from 'node:http';
import { execFileSync } from 'node:child_process';
-import { readFileSync } from 'node:fs';
+import { readFileSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
@@ -15,6 +15,17 @@ const USER = process.env.BASIC_USER || 'admin';
const PASS = process.env.BASIC_PASS || 'DW2024!';
const DB = process.env.USRE_DB || 'usre';
+// TK-10708 (decision B): the consolidated normalized cross-source flyer feed.
+// Built by scripts/build-normalized-flyers.mjs into data/flyers-normalized.json in the
+// TK-10708 schema {source_build,listing_id,title,address,flyer_url,generated_at,asset_type,
+// rights_basis,tier}. Served read-only; every row carries rights_basis + tier for the
+// compliance tiering. This surface NEVER re-hosts a file — flyer_url only.
+const NORMALIZED = join(ROOT, 'data', 'flyers-normalized.json');
+function normalizedFlyers() {
+ if (!existsSync(NORMALIZED)) return { count: 0, flyers: [], note: 'run scripts/build-normalized-flyers.mjs first' };
+ return JSON.parse(readFileSync(NORMALIZED, 'utf8'));
+}
+
const q = sql => execFileSync('psql', [DB, '-t', '-A', '-c', sql], { encoding: 'utf8' }).trim();
function dealAssets() {
@@ -48,6 +59,12 @@ const server = http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' });
return res.end(JSON.stringify({ count: data.length, deals: data }));
}
+ // TK-10708 unified normalized cross-source flyer feed (additive; deal-assets route unchanged)
+ if (req.url.startsWith('/api/flyers')) {
+ const data = normalizedFlyers();
+ res.writeHead(200, { 'content-type': 'application/json' });
+ return res.end(JSON.stringify(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')));
← 384325f auto-data-snapshot: 2026-08-27T00:22:48 (6 data files) — dat
·
back to Re Flyer Aggregator
·
auto-data-snapshot: 2026-08-27T05:52:15 (6 data files) — dat f6bd2a4 →