[object Object]

← 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

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: '&copy; OpenStreetMap &copy; 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 →