← back to Nationalrealestate
auto-save: 2026-07-26T16:14:26 (17 files) — public/admin.html public/brokers.html public/commercial-brokers.html public/commercial-feed.html public/commercial-property.html
3d8003594ede4d08117755a6a9cfc56abc47cd93 · 2026-07-26 16:14:27 -0700 · Steve Abrams
Files touched
M public/admin.htmlM public/brokers.htmlM public/commercial-brokers.htmlM public/commercial-feed.htmlM public/commercial-property.htmlM public/commercial.htmlM public/compare.htmlM public/index.htmlM public/listings.htmlM public/market.htmlM public/markets.htmlA public/nav-drawer.cssA public/nav-drawer.jsM public/property.htmlM public/sources.htmlM public/watchlist.htmlM src/ingest/parcels/miami_dade.ts
Diff
commit 3d8003594ede4d08117755a6a9cfc56abc47cd93
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 26 16:14:27 2026 -0700
auto-save: 2026-07-26T16:14:26 (17 files) — public/admin.html public/brokers.html public/commercial-brokers.html public/commercial-feed.html public/commercial-property.html
---
public/admin.html | 2 ++
public/brokers.html | 2 ++
public/commercial-brokers.html | 2 ++
public/commercial-feed.html | 2 ++
public/commercial-property.html | 2 ++
public/commercial.html | 2 ++
public/compare.html | 2 ++
public/index.html | 3 +++
public/listings.html | 2 ++
public/market.html | 2 ++
public/markets.html | 2 ++
public/nav-drawer.css | 34 ++++++++++++++++++++++++
public/nav-drawer.js | 56 ++++++++++++++++++++++++++++++++++++++++
public/property.html | 2 ++
public/sources.html | 2 ++
public/watchlist.html | 2 ++
src/ingest/parcels/miami_dade.ts | 14 +++++++---
17 files changed, 129 insertions(+), 4 deletions(-)
diff --git a/public/admin.html b/public/admin.html
index 67d0ed9..8459c79 100644
--- a/public/admin.html
+++ b/public/admin.html
@@ -34,6 +34,7 @@
.card .hash{font-size:10px;color:var(--muted);margin-top:5px;font-family:ui-monospace,Menlo,monospace;word-break:break-all;}
.empty{color:var(--muted);padding:18px;text-align:center;}
</style>
+<link rel="stylesheet" href="/nav-drawer.css">
</head>
<body>
<div id="topbar">
@@ -102,5 +103,6 @@ renderBtns(false);
loadRuns();
setInterval(loadRuns, 30000);
</script>
+<script src="/nav-drawer.js"></script>
</body>
</html>
diff --git a/public/brokers.html b/public/brokers.html
index 982fe4e..7489d64 100644
--- a/public/brokers.html
+++ b/public/brokers.html
@@ -53,6 +53,7 @@
.usre-fd .fd-brokers{display:flex;flex-wrap:wrap;gap:4px 12px;margin-top:4px;max-height:120px;overflow:auto;}
.usre-fd .fd-brokers span{color:var(--muted);font-size:12px;white-space:nowrap;}
</style>
+<link rel="stylesheet" href="/nav-drawer.css">
</head>
<body>
<div id="topbar">
@@ -242,5 +243,6 @@ document.getElementById('pgNext').addEventListener('click', () => { S.offset +=
loadStats();
loadPage();
</script>
+<script src="/nav-drawer.js"></script>
</body>
</html>
diff --git a/public/commercial-brokers.html b/public/commercial-brokers.html
index 2d4c379..187d8ef 100644
--- a/public/commercial-brokers.html
+++ b/public/commercial-brokers.html
@@ -31,6 +31,7 @@
.roster div{padding:2px 0;}
#loading{padding:40px;text-align:center;color:var(--muted);}
</style>
+<link rel="stylesheet" href="/nav-drawer.css">
</head>
<body>
<div id="topbar">
@@ -96,5 +97,6 @@ $('#city').onchange=()=>{state.city=$('#city').value;load();};
$('#q').oninput=()=>{state.q=$('#q').value;clearTimeout(window._t);window._t=setTimeout(load,250);};
load();
</script>
+<script src="/nav-drawer.js"></script>
</body>
</html>
diff --git a/public/commercial-feed.html b/public/commercial-feed.html
index f1f09ba..4b340f7 100644
--- a/public/commercial-feed.html
+++ b/public/commercial-feed.html
@@ -35,6 +35,7 @@
.story .f{font-size:11px;color:var(--muted);margin-top:8px;}
#loading{padding:40px;text-align:center;color:var(--muted);}
</style>
+<link rel="stylesheet" href="/nav-drawer.css">
</head>
<body>
<div id="topbar">
@@ -112,5 +113,6 @@ async function load(){
}
(async()=>{ await loadTypes(); await loadCities(); await load(); })();
</script>
+<script src="/nav-drawer.js"></script>
</body>
</html>
diff --git a/public/commercial-property.html b/public/commercial-property.html
index e384298..f47933c 100644
--- a/public/commercial-property.html
+++ b/public/commercial-property.html
@@ -30,6 +30,7 @@
#loading{padding:50px;text-align:center;color:var(--muted);}
.back{font-size:13px;color:var(--muted);text-decoration:none;} .back:hover{color:var(--gold);}
</style>
+<link rel="stylesheet" href="/nav-drawer.css">
</head>
<body>
<div id="topbar">
@@ -84,5 +85,6 @@ function dstr(d){return d?new Date(d).toLocaleDateString(undefined,{year:'numeri
`;
})();
</script>
+<script src="/nav-drawer.js"></script>
</body>
</html>
diff --git a/public/commercial.html b/public/commercial.html
index 78eea53..0cac07b 100644
--- a/public/commercial.html
+++ b/public/commercial.html
@@ -40,6 +40,7 @@
.when{color:var(--muted);}
#loading{padding:50px;text-align:center;color:var(--muted);}
</style>
+<link rel="stylesheet" href="/nav-drawer.css">
</head>
<body>
<div id="topbar">
@@ -131,5 +132,6 @@ $('#q').oninput=()=>{state.q=$('#q').value;clearTimeout(window._t);window._t=set
$('#sort').onchange=()=>{state.sort=$('#sort').value;localStorage.setItem('cre_sort',state.sort);load();};
(async()=>{ await loadTypes(); await loadCities(); await load(); })();
</script>
+<script src="/nav-drawer.js"></script>
</body>
</html>
diff --git a/public/compare.html b/public/compare.html
index 2120a25..ea3daa5 100644
--- a/public/compare.html
+++ b/public/compare.html
@@ -37,6 +37,7 @@
.lgd span::before{content:'▬ ';font-size:13px;}
.empty{color:var(--muted);padding:50px;text-align:center;}
</style>
+<link rel="stylesheet" href="/nav-drawer.css">
</head>
<body>
<div id="topbar">
@@ -194,5 +195,6 @@ async function render(){
loadRegions().then(render);
</script>
+<script src="/nav-drawer.js"></script>
</body>
</html>
diff --git a/public/index.html b/public/index.html
index 65e7f98..56f3db9 100644
--- a/public/index.html
+++ b/public/index.html
@@ -40,6 +40,7 @@
.leaflet-control-attribution{background:rgba(17,21,29,.8)!important;color:var(--muted)!important;}
.leaflet-control-attribution a{color:var(--muted)!important;}
</style>
+<link rel="stylesheet" href="/nav-drawer.css">
</head>
<body>
<div id="topbar">
@@ -70,6 +71,7 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
+<script src="/nav-drawer.js"></script>
<script>
const METRICS = [
{ key: 'zhvi', label: 'ZHVI', fmt: usd0 },
@@ -89,6 +91,7 @@ let currentMetric = localStorage.getItem('usre.metric');
if (!METRICS.some(m => m.key === currentMetric)) currentMetric = 'zhvi';
const map = L.map('map', { zoomControl: true, minZoom: 3, maxZoom: 11 }).setView([38.5, -96.5], 4);
+map.zoomControl.setPosition('topright'); // clear the upper-left for the hamburger
const tiles = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', {
attribution: '© OpenStreetMap © CARTO', subdomains: 'abcd', className: 'basemap-dark',
}).addTo(map);
diff --git a/public/listings.html b/public/listings.html
index 41f63f7..8ac5959 100644
--- a/public/listings.html
+++ b/public/listings.html
@@ -39,6 +39,7 @@
.stat{color:var(--fg);} .stat b{color:var(--gold);}
.src-tag{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--muted);}
</style>
+<link rel="stylesheet" href="/nav-drawer.css">
</head>
<body>
<div id="topbar">
@@ -186,5 +187,6 @@ loadCoverage();
loadFilters();
load();
</script>
+<script src="/nav-drawer.js"></script>
</body>
</html>
diff --git a/public/market.html b/public/market.html
index 25537a9..5657600 100644
--- a/public/market.html
+++ b/public/market.html
@@ -41,6 +41,7 @@
.u-legend{color:var(--muted);font-size:11px;}
.u-legend .u-label{color:var(--muted);}
</style>
+<link rel="stylesheet" href="/nav-drawer.css">
</head>
<body>
<div id="topbar">
@@ -217,5 +218,6 @@ function chart(elId, entries, fmt){
}
})();
</script>
+<script src="/nav-drawer.js"></script>
</body>
</html>
diff --git a/public/markets.html b/public/markets.html
index a55eb3e..a82d865 100644
--- a/public/markets.html
+++ b/public/markets.html
@@ -28,6 +28,7 @@
#loading{padding:60px;text-align:center;color:var(--muted);}
.score{font-weight:700;color:var(--gold);}
</style>
+<link rel="stylesheet" href="/nav-drawer.css">
</head>
<body>
<div id="topbar">
@@ -152,5 +153,6 @@ document.getElementById('typeSeg').addEventListener('click', e => {
});
load(TYPE);
</script>
+<script src="/nav-drawer.js"></script>
</body>
</html>
diff --git a/public/nav-drawer.css b/public/nav-drawer.css
new file mode 100644
index 0000000..61b1560
--- /dev/null
+++ b/public/nav-drawer.css
@@ -0,0 +1,34 @@
+/* nav-drawer.css — collapses the page's #topbar into a hamburger drawer pinned
+ * upper-left. Shared across every USRE page so the nav is identical everywhere.
+ * Inherits the host page's --bg/--panel/--line/--fg/--muted/--gold CSS vars. */
+#navBurger{position:fixed;top:12px;left:12px;z-index:1200;width:44px;height:44px;border-radius:10px;
+ display:flex;align-items:center;justify-content:center;
+ background:rgba(17,21,29,.94);border:1px solid var(--line,#222936);color:var(--gold,#c8a24b);
+ font-size:20px;line-height:1;cursor:pointer;backdrop-filter:blur(6px);box-shadow:0 2px 10px rgba(0,0,0,.4);}
+#navBurger:hover{border-color:var(--gold,#c8a24b);color:var(--fg,#e8ecf2);}
+#navBurger.open{color:var(--fg,#e8ecf2);}
+
+#navBackdrop{position:fixed;inset:0;z-index:1150;background:rgba(0,0,0,.45);
+ opacity:0;visibility:hidden;transition:opacity .2s;}
+#navBackdrop.open{opacity:1;visibility:visible;}
+
+#navDrawer{position:fixed;top:0;left:0;height:100%;width:280px;max-width:84vw;z-index:1180;
+ background:rgba(13,17,24,.98);border-right:1px solid var(--line,#222936);backdrop-filter:blur(8px);
+ transform:translateX(-100%);transition:transform .22s ease;overflow-y:auto;
+ box-shadow:2px 0 22px rgba(0,0,0,.55);}
+#navDrawer.open{transform:translateX(0);}
+#navDrawerBody{display:flex;flex-direction:column;gap:16px;padding:66px 18px 26px;}
+
+/* re-flow whatever came out of the topbar into a clean vertical stack */
+#navDrawerBody .brand{font-size:16px;font-weight:700;white-space:normal;}
+#navDrawerBody nav{display:flex;flex-direction:column;gap:2px;font-size:15px;}
+#navDrawerBody nav a{padding:9px 11px;border-radius:8px;text-decoration:none;color:var(--muted,#8a93a3);}
+#navDrawerBody nav a:hover,#navDrawerBody nav a.active{background:rgba(200,162,75,.12);color:var(--gold,#c8a24b);}
+/* neutralize the old flex-row spacers so children stack full-width */
+#navDrawerBody #metrics,#navDrawerBody #typeSeg{margin-left:0;}
+#navDrawerBody #metrics{display:flex;flex-direction:column;align-items:stretch;gap:6px;}
+#navDrawerBody #metrics button{width:100%;text-align:left;}
+#navDrawerBody #typeSeg{display:inline-flex;width:100%;}
+#navDrawerBody #typeSeg button{flex:1;}
+#navDrawerBody #exportcsv,#navDrawerBody #freshness,#navDrawerBody #thinToggle{margin-left:0;}
+#navDrawerBody .cg-sec-label{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted,#8a93a3);}
diff --git a/public/nav-drawer.js b/public/nav-drawer.js
new file mode 100644
index 0000000..f46c5b2
--- /dev/null
+++ b/public/nav-drawer.js
@@ -0,0 +1,56 @@
+/* nav-drawer.js — turns the page's #topbar into an upper-left hamburger drawer.
+ * It MOVES the topbar's existing child nodes into the drawer (same DOM nodes), so
+ * every id and event binding is preserved and page scripts that populate #metrics /
+ * wire #typeSeg keep working. Include on any page that has a <div id="topbar">. */
+(function () {
+ function init() {
+ if (document.getElementById('navBurger')) return; // idempotent
+ var bar = document.getElementById('topbar');
+
+ var burger = document.createElement('button');
+ burger.id = 'navBurger';
+ burger.type = 'button';
+ burger.setAttribute('aria-label', 'Menu');
+ burger.setAttribute('aria-expanded', 'false');
+ burger.textContent = '☰'; // ☰
+
+ var backdrop = document.createElement('div');
+ backdrop.id = 'navBackdrop';
+
+ var drawer = document.createElement('div');
+ drawer.id = 'navDrawer';
+ var body = document.createElement('div');
+ body.id = 'navDrawerBody';
+ drawer.appendChild(body);
+
+ // Relocate the whole topbar into the drawer (preserve nodes/ids/bindings).
+ if (bar) {
+ while (bar.firstChild) body.appendChild(bar.firstChild);
+ bar.parentNode && bar.parentNode.removeChild(bar);
+ }
+
+ document.body.appendChild(backdrop);
+ document.body.appendChild(drawer);
+ document.body.appendChild(burger);
+
+ function open() {
+ drawer.classList.add('open'); backdrop.classList.add('open'); burger.classList.add('open');
+ burger.textContent = '✕'; // ✕
+ burger.setAttribute('aria-expanded', 'true');
+ }
+ function close() {
+ drawer.classList.remove('open'); backdrop.classList.remove('open'); burger.classList.remove('open');
+ burger.textContent = '☰';
+ burger.setAttribute('aria-expanded', 'false');
+ }
+ burger.addEventListener('click', function () {
+ drawer.classList.contains('open') ? close() : open();
+ });
+ backdrop.addEventListener('click', close);
+ // clicking a nav link navigates away — close so it isn't left open on the next page
+ body.addEventListener('click', function (e) { if (e.target.closest('nav a')) close(); });
+ document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
+ }
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
+ else init();
+})();
diff --git a/public/property.html b/public/property.html
index b8bfad2..936e0a4 100644
--- a/public/property.html
+++ b/public/property.html
@@ -61,6 +61,7 @@
.leaflet-container{background:var(--bg);}
#foot{margin-top:22px;color:var(--muted);font-size:11px;}
</style>
+<link rel="stylesheet" href="/nav-drawer.css">
</head>
<body>
<div id="topbar">
@@ -352,5 +353,6 @@ initCounties();
const loc = qs.get('loc');
if (loc) load(loc);
</script>
+<script src="/nav-drawer.js"></script>
</body>
</html>
diff --git a/public/sources.html b/public/sources.html
index dbb1c7c..3cf067d 100644
--- a/public/sources.html
+++ b/public/sources.html
@@ -31,6 +31,7 @@
.empty{color:var(--muted);padding:30px;text-align:center;}
.foot{color:var(--muted);font-size:12px;margin-top:26px;font-style:italic;}
</style>
+<link rel="stylesheet" href="/nav-drawer.css">
</head>
<body>
<div id="topbar">
@@ -127,5 +128,6 @@ const int = n => (+n).toLocaleString();
<td class="num">${int(s.brokers)}</td></tr>`).join('') : '<tr><td colspan="2" class="empty">none</td></tr>';
})();
</script>
+<script src="/nav-drawer.js"></script>
</body>
</html>
diff --git a/public/watchlist.html b/public/watchlist.html
index 57961f3..41a16d4 100644
--- a/public/watchlist.html
+++ b/public/watchlist.html
@@ -37,6 +37,7 @@
.alert-row .when{color:var(--muted);font-size:11px;margin-left:auto;white-space:nowrap;}
.empty{color:var(--muted);padding:18px;text-align:center;}
</style>
+<link rel="stylesheet" href="/nav-drawer.css">
</head>
<body>
<div id="topbar">
@@ -159,5 +160,6 @@ async function loadAlerts(){
loadRegions().then(()=>{ loadList(); loadAlerts(); });
</script>
+<script src="/nav-drawer.js"></script>
</body>
</html>
diff --git a/src/ingest/parcels/miami_dade.ts b/src/ingest/parcels/miami_dade.ts
index 5463565..9e72e1e 100644
--- a/src/ingest/parcels/miami_dade.ts
+++ b/src/ingest/parcels/miami_dade.ts
@@ -151,15 +151,21 @@ export async function ingestMiami(): Promise<{ upserted: number }> {
// ── in-DB bulk-sale flag: a deed (latest_deed) spanning >1 folio is an assemblage;
// its price is the bundle total, not per-parcel. Null the price + flag, in one pass.
+ // Resolve bulk parcels to source_id first, then UPDATE joining on the PKEY
+ // (county_fips, source_id) — NOT the unindexable JSONB expr extra->>'latest_deed',
+ // which seq-scanned the 5M-row prod table for ~53min on a sibling county (TK-00037).
const b = await query<{ n: string }>(
- `WITH bulk AS (
+ `WITH deeds AS MATERIALIZED (
SELECT extra->>'latest_deed' AS deed, COUNT(*)::int AS cnt
FROM parcel WHERE county_fips=$1 AND extra->>'latest_deed' IS NOT NULL
AND extra->>'latest_deed' !~ '^0+-0+$'
- GROUP BY 1 HAVING COUNT(*) > 1)
+ GROUP BY 1 HAVING COUNT(*) > 1),
+ targets AS MATERIALIZED (
+ SELECT p.source_id, d.cnt FROM parcel p JOIN deeds d ON p.extra->>'latest_deed' = d.deed
+ WHERE p.county_fips=$1)
UPDATE parcel p SET last_sale_price = NULL,
- extra = p.extra || jsonb_build_object('bulk_sale', true, 'bulk_deed_folio_count', bulk.cnt)
- FROM bulk WHERE p.county_fips=$1 AND p.extra->>'latest_deed' = bulk.deed
+ extra = p.extra || jsonb_build_object('bulk_sale', true, 'bulk_deed_folio_count', t.cnt)
+ FROM targets t WHERE p.county_fips=$1 AND p.source_id = t.source_id
RETURNING 1 AS n`, [FIPS]);
const bulk = b.rowCount ?? 0;
← 4d66223 USRE Markets: left-rail filters now cover ALL data points (d
·
back to Nationalrealestate
·
USRE: collapse topbar into an upper-left hamburger drawer si 896ac96 →