← back to Commercialrealestate
public/parcel.html
167 lines
<!doctype html><html lang="en"><head>
<script>(function(){try{var t=localStorage.getItem('crcp-theme');document.documentElement.setAttribute('data-theme',t==='light'?'light':'dark');}catch(e){document.documentElement.setAttribute('data-theme','dark');}})();</script>
<script>/* credential-safe fetch (2026-08-18): resolve EVERY relative fetch (with or without a leading slash) against a credential-free base so creds embedded in the page URL (http://user:pass@host/...) don't poison document.baseURI and throw "Request cannot be constructed from a URL that includes credentials". */(function(){var of=window.fetch,O=location.origin;window.fetch=function(u,o){try{if(typeof u==='string'){if(/^[a-z][a-z0-9+.-]*:/i.test(u)){var x=new URL(u);if(x.username||x.password){x.username='';x.password='';u=x.href;}}else{u=new URL(u,O+location.pathname+location.search).href;}}else if(u&&typeof u.url==='string'){var y=new URL(u.url,O+location.pathname+location.search);if(y.username||y.password){y.username='';y.password='';u=new Request(y.href,u);}}}catch(e){}return of.call(this,u,o);};try{if(location.href.indexOf('@')>=0){history.replaceState(null,'',location.pathname+location.search+location.hash);}}catch(e){}})();</script>
<!-- Self-heal: nav-agent grid controls can persist a "removed"/"collapsed" state that hides the
whole grid with no obvious way back. Clear it before nav-agent runs so a grid can never stay hidden. -->
<script>(function(){try{for(var i=localStorage.length-1;i>=0;i--){var k=localStorage.key(i);if(k&&k.indexOf('navagent:')===0){var v=localStorage.getItem(k);if(v&&v.indexOf('"removed":true')>=0){localStorage.removeItem(k);}}}}catch(e){}})();</script>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Parcel Detail · CRCP</title>
<style>
:root{--bg:#0e1116;--card:#161b22;--line:#2a313c;--ink:#e6edf3;--mut:#8b949e;--acc:#3fb950;--blue:#58a6ff;--gold:#ffa600;--violet:#bc8cff;--bad:#f85149}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:-apple-system,Helvetica,Arial,sans-serif;font-size:13.5px;line-height:1.5}
header{padding:14px 20px;border-bottom:1px solid var(--line);display:flex;flex-wrap:wrap;gap:10px;align-items:center}
h1{font-size:16px;margin:0}h1 b{color:#fff}
.nav a{color:var(--blue);text-decoration:none;font-size:12px;margin-left:12px}.nav a:hover{text-decoration:underline}
main{max-width:900px;margin:0 auto;padding:22px 20px}
.crumb{font-size:12px;color:var(--mut);margin-bottom:12px}
.crumb a{color:var(--blue);text-decoration:none}.crumb a:hover{text-decoration:underline}
.h2row{display:flex;flex-wrap:wrap;align-items:baseline;gap:10px;margin:0 0 4px}
.h2row h2{font-size:22px;margin:0;color:#fff;font-weight:600}
.pill{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:3px 8px;border-radius:6px;white-space:nowrap}
.pill.county{background:rgba(63,185,80,.16);color:var(--acc)}
.pill.size{background:rgba(255,166,0,.16);color:var(--gold)}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px}
@media(max-width:680px){.grid2{grid-template-columns:1fr}}
.panel{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:15px 17px}
.panel h3{margin:0 0 10px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut)}
.kv{display:flex;gap:10px;padding:4px 0;border-bottom:1px solid #20262f}
.kv:last-child{border-bottom:0}
.kv .k{color:var(--mut);min-width:150px;flex-shrink:0}
.kv .v{color:var(--ink)}
.kv .v b{color:#fff}
a.drill{color:var(--blue);text-decoration:none;border-bottom:1px dotted rgba(88,166,255,.45)}
a.drill:hover{color:#fff;border-bottom-color:var(--blue)}
a.ext{color:var(--blue);text-decoration:none}a.ext:hover{text-decoration:underline}
.money{font-size:26px;font-weight:700;color:var(--gold);font-variant-numeric:tabular-nums}
.sig{background:rgba(88,166,255,.07);border:1px solid rgba(88,166,255,.25);border-radius:8px;padding:8px 11px;margin:6px 0;font-size:12.5px}
.sig.note{background:rgba(255,166,0,.06);border-color:rgba(255,166,0,.25)}
.ula{padding:7px 0;border-bottom:1px solid #20262f;font-size:12.5px}
.ula:last-child{border-bottom:0}
.ula b{color:var(--gold)}
.gap{color:var(--mut);font-size:12px;line-height:1.55}
.src{color:var(--mut);font-size:10.5px;margin-top:12px}
.full{grid-column:1/-1}
.loading{color:var(--mut);padding:40px 0}
.err{color:var(--bad);padding:24px 0}
</style> <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
<link rel="stylesheet" href="/crcp-theme.css"><!-- CRCP day/night tokens: load LAST so html[data-theme] wins -->
</head><body data-nav-agent="off"><!-- nav-agent off (TK-10525): single-parcel DETAIL dossier (#out), no browsable grid -->
<header>
<h1><b>Parcel Detail</b> · CRCP</h1>
<span class="pill county">100% public record</span>
<div class="nav"><a href="/deals-flow.html">← Deal Flow</a><a href="/property">🔍 Property Lookup</a><a href="/">Explorer</a></div>
</header>
<main>
<div class="crumb"><a href="/deals-flow.html">Closed Deal Flow</a> › <span id="crumbAin">parcel</span></div>
<div id="out"><div class="loading">Loading the public-record dossier…</div></div>
</main>
<script>
const esc=s=>(s==null?'':String(s)).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
const money=n=>n==null||isNaN(+n)?null:'$'+Math.round(+n).toLocaleString();
// AIN from the path (/parcel/<AIN>) or ?ain= for back-compat.
const AIN=(location.pathname.match(/\/(?:parcel|deal)\/(\d+)/)||[])[1]
|| new URLSearchParams(location.search).get('ain') || '';
// href-drill helpers: every data point links to its deeper data.
const dfLink=(field,val,label)=>val==null||val===''?esc(label==null?'':label)
:`<a class="drill" href="/deals-flow.html?${field}=${encodeURIComponent(val)}" title="See all ${esc(field)} = ${esc(val)} deals">${esc(label==null?val:label)}</a>`;
// safeHref: protocol whitelist so a hostile feed value (javascript:/data:) can never land in an href (TK-10482 contrarian BLOCKER fix; mirrors usre deals.html safeHref).
const safeHref=u=>{try{const p=new URL(String(u==null?'':u),location.origin);return (p.protocol==='http:'||p.protocol==='https:')?p.href:'#';}catch(e){return '#';}};
const ext=(url,label,title)=>`<a class="ext" href="${esc(safeHref(url))}" target="_blank" rel="noopener noreferrer" title="${esc(title||'')}">${esc(label)} ↗</a>`;
const kv=(k,v)=>(v==null||v==='')?'':`<div class="kv"><span class="k">${esc(k)}</span><span class="v">${v}</span></div>`;
async function load(){
const out=document.getElementById('out');
if(!AIN){out.innerHTML='<div class="err">No AIN in the URL. Use /parcel/<AIN>.</div>';return;}
document.getElementById('crumbAin').textContent='AIN '+AIN;
let d;
try{ const r=await fetch('/api/parcel/'+encodeURIComponent(AIN),{headers:{Accept:'application/json'}});
if(!r.ok)throw new Error('HTTP '+r.status); d=await r.json(); }
catch(e){ out.innerHTML='<div class="err">Lookup failed: '+esc(e.message)+'</div>'; return; }
if(!d||d.found===false){ out.innerHTML='<div class="err">No LA County assessor record for AIN '+esc(AIN)+'.</div>'; return; }
render(d);
}
function render(d){
const p=d.parcel||{}, deal=d.deal||null, la=d.loan_angle||{};
document.title='Parcel '+AIN+' · '+(p.address||'CRCP');
const salePrice=deal&&deal.price?deal.price:(la.est_price||null);
// ── header ──
let html=`<div class="h2row"><h2>${esc(p.address||('AIN '+AIN))}</h2>`
+`<span class="pill county">🏛️ LA County Assessor</span>`
+(la.deal_size?`<span class="pill size">${esc(la.deal_size)}</span>`:'')+`</div>`;
if(salePrice) html+=`<div class="money">${money(salePrice)} <span style="font-size:12px;color:var(--mut);font-weight:400">${deal?'est. sale (Prop-13 reassessment-derived)':'assessed'}</span></div>`;
html+=`<div class="grid2">`;
// ── Parcel identity + physical ──
html+=`<div class="panel"><h3>Parcel</h3>`
+kv('AIN', `<b>${esc(p.ain||AIN)}</b> · ${ext(d.portal_url||('https://portal.assessor.lacounty.gov/parceldetail/'+AIN),'assessor record','Open the LA County Assessor public record')}`)
+kv('Address', esc(p.address))
+kv('City', esc(((deal&&deal.deal_city)||p.city)||'')) /* deals-flow.json exposes no deal_city filter key → render city as TEXT, no silent dead-end drill (TK-10482 contrarian BLOCKER fix) */
+kv('ZIP', esc(p.zip))
+kv('Use / type', dfLink('use', (deal&&deal.use)||p.use_desc||p.use_type, p.use_desc||p.use_type||(deal&&deal.use)))
+kv('Year built', dfLink('year_built', p.year_built, p.year_built))
+kv('Sq ft', p.sqft?Number(p.sqft).toLocaleString():'')
+kv('Units', p.units)
+kv('Beds / baths', (p.bedrooms||p.bathrooms)?((p.bedrooms||'?')+' / '+(p.bathrooms||'?')):'')
+kv('Tax rate area', p.tax_rate_area)
+`</div>`;
// ── Assessed values + transfer ──
html+=`<div class="panel"><h3>Assessed value (${esc(p.assessed_as_of||p.roll_year||'roll')})</h3>`
+kv('Land', money(p.assessed_land))
+kv('Improvement', money(p.assessed_improvement))
+kv('Total', `<b>${money(p.assessed_total)||'—'}</b>`)
+kv('Roll year', p.assessed_as_of||p.roll_year)
+kv('Last transfer recorded', dfLink('year', deal&&deal.year, p.last_transfer_recorded||(deal&&deal.date)))
+(deal?kv('Transaction date', dfLink('year', deal.year, deal.date)):'')
+(deal?kv('Deal source', ext((deal.url||d.portal_url),(deal.source_label||'LA County Assessor'), deal.source||'')):'')
+(deal&&deal.confidence?kv('Confidence', esc(deal.confidence)):'')
+`</div>`;
// ── LOAN ANGLE (why Frank cares) ──
html+=`<div class="panel full"><h3>💰 Lending angle — for Frank the loan officer</h3>`;
if((la.signals||[]).length||(la.notes||[]).length){
(la.signals||[]).forEach(s=>html+=`<div class="sig">◆ ${esc(s)}</div>`);
(la.notes||[]).forEach(n=>html+=`<div class="sig note">• ${esc(n)}</div>`);
} else html+=`<div class="gap">No standout financing signal from public facts alone — pull the recorded deed for parties + exact terms.</div>`;
// buyer / seller as TEXT only
const pr=la.parties||{};
html+=`<div style="margin-top:11px">`
+kv('Buyer (text)', pr.buyer_text?esc(pr.buyer_text)+(pr.source?` <span class="gap">· ${esc(pr.source)}</span>`:''):'<span class="gap">not in free public record</span>')
+kv('Seller (text)', pr.seller_text?esc(pr.seller_text):'<span class="gap">not in free public record</span>')
+(la.price_to_assessed?kv('Sale ÷ assessed', '<b>'+la.price_to_assessed+'×</b>'):'')
+`</div>`;
if(pr.note) html+=`<div class="gap" style="margin-top:6px">${esc(pr.note)}</div>`;
html+=`</div>`;
// ── ULA candidate sales (same ZIP) ──
if((d.ula_zip_candidates||[]).length){
html+=`<div class="panel full"><h3>Recent $5.3M+ sales in ZIP ${esc(p.zip||'')} (LA City ULA · candidates)</h3>`;
d.ula_zip_candidates.slice(0,10).forEach(c=>{
const instr=c.instrument?` · <span class="gap">instr ${esc(c.instrument)}</span>`:'';
html+=`<div class="ula">${esc(c.date||'')} · <b>${money(c.price)}</b> · ${dfLink('use',c.use,c.use)}${instr}${c.basis?` <span class="gap">(${esc(c.basis)})</span>`:''}</div>`;
});
if(d.ula_note) html+=`<div class="gap" style="margin-top:7px">${esc(d.ula_note)}</div>`;
html+=`</div>`;
}
// ── Recorded-deed (gated) declaration ──
if(d.deed&&!d.deed.available){
html+=`<div class="panel full"><h3>Buyer / seller — recorded deed (gated source)</h3>`
+`<div class="gap">${esc(d.deed.reason)}</div>`;
(d.deed.paths||[]).forEach(pt=>{ html+=`<div class="ula"><b style="color:var(--blue)">${esc(pt.name)}</b> · <span class="gap">${esc(pt.cost)}</span> — ${esc(pt.gives)}</div>`; });
if(d.deed.bridge_hint) html+=`<div class="gap" style="margin-top:7px">${esc(d.deed.bridge_hint)}</div>`;
html+=`</div>`;
}
html+=`</div>`; // /grid2
html+=`<div class="src">Sources: ${esc((d.sources||[]).join(' · '))} · Public-record / address-level facts only, nothing proprietary. Company / party names are text-only (no firm link-out).</div>`;
document.getElementById('out').innerHTML=html;
}
load();
</script>
<script src="/corner-nav.js" defer></script>
<script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
<script src="/crcp-theme.js" defer></script><!-- CRCP day/night toggle (auto-injects into header) -->
</body></html>