[object Object]

← back to Nationalrealestate

feat: Parcel Explorer national coverage overview map + lint/refactor, v0.17.0 (session close)

956d71906d7e18b9e80780f562aa5ef658494393 · 2026-08-10 21:17:02 -0700 · Steve Abrams

- overview map: /api/parcels/coverage now returns per-county AVG centroid (clat/clng);
  parcels.html plots a marker per covered county (sized by count, gold-ringed=priced),
  click to drill in + '‹ All counties' back button.
- security hardening (lint): safeHref() strips javascript:/data: URIs on DB-sourced
  hrefs; client-side validFips()/sid guards on deep-links.
- refactor: renamed shadowed 'map' var in headerSort; split jammed const decl.
tsc clean, JS syntax clean, verified render (32 counties mapped). Local pm2 restarted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 956d71906d7e18b9e80780f562aa5ef658494393
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 10 21:17:02 2026 -0700

    feat: Parcel Explorer national coverage overview map + lint/refactor, v0.17.0 (session close)
    
    - overview map: /api/parcels/coverage now returns per-county AVG centroid (clat/clng);
      parcels.html plots a marker per covered county (sized by count, gold-ringed=priced),
      click to drill in + '‹ All counties' back button.
    - security hardening (lint): safeHref() strips javascript:/data: URIs on DB-sourced
      hrefs; client-side validFips()/sid guards on deep-links.
    - refactor: renamed shadowed 'map' var in headerSort; split jammed const decl.
    tsc clean, JS syntax clean, verified render (32 counties mapped). Local pm2 restarted.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 package.json          |  2 +-
 public/parcels.html   | 39 +++++++++++++++++++++++++++++++++------
 src/server/parcels.ts |  6 +++++-
 3 files changed, 39 insertions(+), 8 deletions(-)

diff --git a/package.json b/package.json
index 7f02df9..f12099c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "nationalrealestate",
-  "version": "0.16.1",
+  "version": "0.17.0",
   "private": true,
   "type": "module",
   "description": "USRealEstate — national U.S. residential market explorer. Free-data v1 (Redfin Data Center, Zillow Research, Census ACS, FHFA). Internal analyst tool behind basic auth.",
diff --git a/public/parcels.html b/public/parcels.html
index 42c9fd3..e55f035 100644
--- a/public/parcels.html
+++ b/public/parcels.html
@@ -104,6 +104,7 @@
     <div id="map"></div>
     <div id="tablewrap"><div class="empty">Pick a county on the left to explore its parcels.</div></div>
     <div id="status">
+      <button id="backbtn" style="display:none">‹ All counties</button>
       <span id="count">—</span>
       <div id="colmenu"><button id="colbtn">Columns ▾</button><div id="colpop"></div></div>
       <button id="prev" disabled>‹ Prev</button><button id="next" disabled>Next ›</button>
@@ -121,6 +122,8 @@ const $ = s => document.querySelector(s);
 const fmt = n => n==null?'—':Number(n).toLocaleString();
 const money = n => n==null?'—':'$'+Number(n).toLocaleString(undefined,{maximumFractionDigits:0});
 const esc = s => (s==null?'':String(s)).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+const safeHref = u => { try{ const p=new URL(String(u||'')); return (p.protocol==='http:'||p.protocol==='https:')?esc(p.href):'#'; }catch(e){ return '#'; } };
+const validFips = f => { const t=String(f||'').trim(); return /^\d{5}$/.test(t)?t:null; };
 
 // columns: key, label, render, numeric, defaultOn
 const COLS = [
@@ -177,6 +180,26 @@ async function loadMap(){
   $('#mapnote').textContent = d.points.length?`${fmt(d.points.length)} plotted${d.capped?' (capped)':''}`:'no coordinates';
   if(pts.length){ try{ map.fitBounds(L.latLngBounds(pts).pad(.05),{maxZoom:13}); }catch(e){} }
 }
+// national coverage overview — one marker per covered county (sized by count, gold-ringed if priced)
+function showOverview(){
+  ptLayer.clearLayers(); state.county=null; state.selSid=null;
+  document.querySelectorAll('.cty.sel').forEach(x=>x.classList.remove('sel'));
+  $('#tablewrap').innerHTML='<div class="empty">Pick a county — on the map above or in the list — to explore its parcels.</div>';
+  $('#count').textContent='—'; $('#prev').disabled=$('#next').disabled=true; $('#backbtn').style.display='none';
+  const cs=(state.counties||[]).filter(c=>c.clat!=null&&c.clng!=null);
+  const pts=[];
+  const maxP=Math.max(1,...cs.map(c=>c.parcels));
+  for(const c of cs){
+    const rad=5+18*Math.sqrt(c.parcels/maxP);
+    const m=L.circleMarker([c.clat,c.clng],{radius:rad,weight:c.priced>0?2:1,
+      color:c.priced>0?'#c8a24b':'#3a4658',fillColor:c.priced>0?'#c8a24b':'#5a9be0',fillOpacity:.45});
+    m.on('click',()=>selectCounty(c.fips));
+    m.bindTooltip(`<span class="nm">${esc(c.name)}</span> · <span class="vl">${fmt(c.parcels)}</span>${c.priced>0?' ◆ '+fmt(c.priced)+' priced':''}`,{className:'county-tip'});
+    m.addTo(ptLayer); pts.push([c.clat,c.clng]);
+  }
+  $('#mapnote').textContent=`${cs.length} counties mapped · click one`;
+  if(pts.length){ try{ map.fitBounds(L.latLngBounds(pts).pad(.12),{maxZoom:7}); }catch(e){} }
+}
 
 // ── sidebar coverage ──
 async function loadCoverage(){
@@ -194,12 +217,15 @@ async function loadCoverage(){
     el.onclick=()=>selectCounty(c.fips,el);
     box.appendChild(el);
   }
+  state.counties=d.counties||[];
+  if(!state.county) showOverview();
 }
 function selectCounty(fips,el){
   document.querySelectorAll('.cty.sel').forEach(x=>x.classList.remove('sel'));
   (el||document.querySelector(`.cty[data-fips="${fips}"]`))?.classList.add('sel');
-  state.county=fips; state.offset=0; loadTable(); loadMap();
+  state.county=fips; state.offset=0; $('#backbtn').style.display=''; loadTable(); loadMap();
 }
+$('#backbtn').onclick=()=>{ showOverview(); };
 
 // ── table ──
 function renderCols(){
@@ -241,8 +267,8 @@ function draw(){
   if(window.ColResize) ColResize.refresh();
 }
 function headerSort(k){
-  const map={total_value:'value_desc',last_sale_price:'price_desc',last_sale_date:'recent',address:'address',source_id:'',added:'added'};
-  if(k in map){ state.sort=map[k]; $('#sort').value=map[k]; state.offset=0; loadTable(); }
+  const sortKeys={total_value:'value_desc',last_sale_price:'price_desc',last_sale_date:'recent',address:'address',source_id:'',added:'added'};
+  if(k in sortKeys){ state.sort=sortKeys[k]; $('#sort').value=sortKeys[k]; state.offset=0; loadTable(); }
 }
 
 // ── detail drawer ──
@@ -277,14 +303,14 @@ async function openDetail(fips,sid){
         (det.grantor?'<div class="row"><span>Grantor</span>'+esc(det.grantor)+'</div>':'')+
         (det.grantee?'<div class="row"><span>Grantee</span>'+esc(det.grantee)+'</div>':'')+
         (e.doc_number?'<div class="row"><span>Doc #</span>'+esc(e.doc_number)+'</div>':'')+
-        (e.source_url?'<div class="row"><span>Source</span><a href="'+esc(e.source_url)+'" target="_blank" rel="noopener">record ↗</a></div>':'')+
+        (e.source_url?'<div class="row"><span>Source</span><a href="'+safeHref(e.source_url)+'" target="_blank" rel="noopener">record ↗</a></div>':'')+
         '</div>';
     }
   } else {
     deeds='<div class="badge">Coverage-only — no recorded sale price (CA AB-1785)</div>';
   }
   let links='';
-  if(d.links&&d.links.length){ links='<div style="margin-top:12px">'+d.links.map(l=>'<a href="'+esc(l.url)+'" target="_blank" rel="noopener">'+esc(l.label||l.kind)+' ↗</a>').join(' · ')+'</div>'; }
+  if(d.links&&d.links.length){ links='<div style="margin-top:12px">'+d.links.map(l=>'<a href="'+safeHref(l.url)+'" target="_blank" rel="noopener">'+esc(l.label||l.kind)+' ↗</a>').join(' · ')+'</div>'; }
   $('#detailBody').innerHTML=`<h2>${esc(p.address||p.source_id)}</h2><div class="sub">${esc(p.county_name||p.county_fips)} ${esc(p.state_code||'')} · ${esc(p.source_id)}</div>${kv}${deeds}${links}`;
 }
 function closeDetail(){ $('#detail').classList.remove('open'); state.selSid=null; document.querySelectorAll('tbody tr.sel').forEach(t=>t.classList.remove('sel')); }
@@ -307,7 +333,8 @@ document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeDetail(); });
   initMap(); renderCols(); loadCoverage();
   // deep-link ?county=06059&sid=...
   const p=new URLSearchParams(location.search);
-  if(p.get('county')){ state.county=p.get('county'); loadCoverage().then(()=>selectCounty(state.county)); if(p.get('sid')) setTimeout(()=>openDetail(state.county,p.get('sid')),600); }
+  const dlCounty=validFips(p.get('county'));
+  if(dlCounty){ state.county=dlCounty; const sid=p.get('sid')?String(p.get('sid')).trim().slice(0,64):null; loadCoverage().then(()=>selectCounty(dlCounty)); if(sid) setTimeout(()=>openDetail(dlCounty,sid),600); }
 })();
 </script>
 </body>
diff --git a/src/server/parcels.ts b/src/server/parcels.ts
index 3fb9670..fb217db 100644
--- a/src/server/parcels.ts
+++ b/src/server/parcels.ts
@@ -32,6 +32,8 @@ export function mountParcels(app: Express) {
                 COUNT(p.last_sale_price)::bigint AS priced,
                 COUNT(p.lat)::bigint        AS mapped,
                 COUNT(p.total_value)::bigint AS valued,
+                AVG(p.lat) FILTER (WHERE p.lat IS NOT NULL) AS clat,
+                AVG(p.lng) FILTER (WHERE p.lng IS NOT NULL) AS clng,
                 r.name, r.state_code
            FROM parcel p
            LEFT JOIN region r ON r.fips = p.county_fips AND r.region_type = 'county'
@@ -40,6 +42,7 @@ export function mountParcels(app: Express) {
       const counties = r.rows.map((x) => ({
         fips: x.county_fips, name: x.name || x.county_fips, state: x.state_code || null,
         parcels: Number(x.parcels), priced: Number(x.priced), mapped: Number(x.mapped), valued: Number(x.valued),
+        clat: x.clat != null ? +(+x.clat).toFixed(4) : null, clng: x.clng != null ? +(+x.clng).toFixed(4) : null,
       }));
       const totals = counties.reduce((a, c) => ({ parcels: a.parcels + c.parcels, priced: a.priced + c.priced, counties: a.counties + 1 }), { parcels: 0, priced: 0, counties: 0 });
       coverageCache = { at: Date.now(), rows: counties, totals };
@@ -57,7 +60,8 @@ export function mountParcels(app: Express) {
       const q = String(req.query.q || '').trim().slice(0, 80);
       const pricedOnly = req.query.priced === '1';
 
-      const where: string[] = ['p.county_fips = $1']; const params: any[] = [cf];
+      const where: string[] = ['p.county_fips = $1'];
+      const params: any[] = [cf];
       if (pricedOnly) where.push('p.last_sale_price IS NOT NULL');
       if (q) {
         const esc = q.replace(/[%_\\]/g, '\\$&');

← d6f64ff TK-16: add Parcel Explorer web viewer (/parcels.html + /api/  ·  back to Nationalrealestate  ·  auto-data-snapshot: 2026-08-10T21:18:09 (1 data files) — pac cd7be34 →