← back to Commercialrealestate
public/fha-leads.html
209 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>Assumable-Loan Leads · FHA Multifamily — CRCP</title>
<style>
:root{--bg:#0e1116;--card:#161b22;--line:#2a313c;--ink:#e6edf3;--mut:#8b949e;--acc:#3fb950;--blue:#58a6ff;--warn:#d29922;--gold:#ffa600;--cols:4}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:-apple-system,Helvetica,Arial,sans-serif;font-size:13px}
header{padding:14px 18px;border-bottom:1px solid var(--line);display:flex;flex-wrap:wrap;gap:10px;align-items:center}
h1{font-size:17px;margin:0}h1 b{color:#fff}
.badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#0e1116;background:var(--acc);padding:3px 8px;border-radius:6px}
.nav a{color:var(--blue);text-decoration:none;font-size:12px;margin-left:12px}
.shell{display:flex;align-items:flex-start}
.rail{width:230px;flex:0 0 230px;border-right:1px solid var(--line);padding:12px;height:calc(100vh - 52px);overflow:auto;position:sticky;top:0}
.rsec{border-bottom:1px solid var(--line);padding:9px 0}
.rsec h4{margin:0 0 7px;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);cursor:pointer}
.chips{display:flex;flex-wrap:wrap;gap:5px}
.chip{border:1px solid var(--line);border-radius:20px;padding:3px 9px;font-size:11px;cursor:pointer;color:var(--ink);background:var(--card);user-select:none}
.chip.active{background:var(--blue);color:#0e1116;border-color:var(--blue);font-weight:600}
.chip .ct{color:var(--mut);margin-left:4px;font-size:10px}.chip.active .ct{color:#0e1116}
.main{flex:1;padding:12px 16px;min-width:0}
.toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:10px}
.search{flex:1;min-width:200px;background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:8px 11px;font-size:13px}
select,input[type=number]{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:7px 9px;font-size:12px}
.count{color:var(--mut);font-size:12px}
table{width:100%;border-collapse:collapse;font-size:12px}
th{text-align:left;color:var(--mut);text-transform:uppercase;font-size:10px;letter-spacing:.4px;padding:7px 8px;border-bottom:1px solid var(--line);cursor:pointer;white-space:nowrap}
td{padding:7px 8px;border-bottom:1px solid #20262f}
tr:hover td{background:#131922}
.pill{font-size:10px;font-weight:700;padding:2px 7px;border-radius:20px}
.pill.gold{background:rgba(255,166,0,.16);color:var(--gold)}
.spread{font-weight:700;color:var(--gold)}
.mono{font-variant-numeric:tabular-nums}
.prov{font-size:11px;color:var(--mut);margin:2px 0 12px;line-height:1.5}
.stat{display:inline-block;background:var(--card);border:1px solid var(--line);border-radius:9px;padding:8px 12px;margin:0 8px 10px 0}
.stat b{font-size:19px;color:#fff}.stat span{display:block;font-size:10px;text-transform:uppercase;color:var(--mut);letter-spacing:.4px}
/* grid/list/table view toggle + FHA lead cards (three-view) */
.viewseg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden}
.viewseg button{background:var(--card);color:var(--mut);border:0;border-right:1px solid var(--line);padding:7px 12px;font-size:12px;cursor:pointer}
.viewseg button:last-child{border-right:0}.viewseg button.active{background:var(--blue);color:#0e1116;font-weight:600}
.fha-grid{display:grid;grid-template-columns:repeat(var(--cols,4),minmax(0,1fr));gap:12px;margin-top:4px}
.fcard{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:13px 15px;display:flex;flex-direction:column;gap:8px}
.fcard:hover{border-color:var(--blue)}
.fcard-h{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}
.fcard-t{font-weight:700;color:#fff;font-size:13px;line-height:1.25}
.fcard-sub{color:var(--mut);font-size:11px;margin-top:-4px}
.fcard-spread{color:var(--gold);font-weight:700;font-size:13px}
.fcard-spread span{color:var(--mut);font-weight:400;font-size:10px;text-transform:uppercase;letter-spacing:.4px;margin-left:5px}
.fcard-kv{display:grid;grid-template-columns:1fr 1fr;gap:5px 12px;font-size:12px}
.fcard-kv div{display:flex;justify-content:space-between;gap:8px;border-bottom:1px solid #20262f;padding-bottom:3px}
.fcard-kv em{color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.3px;font-style:normal}
</style> <link rel="stylesheet" href="/three-view.css"><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>
<header>
<h1><b>Assumable-Loan Leads</b> · FHA Multifamily</h1>
<span class="badge">100% public data · resellable</span>
<div class="nav"><a href="/mls.html">← Listings</a><a href="/">Explorer</a></div>
</header>
<div class="shell">
<aside class="rail">
<div class="rsec"><h4>State</h4><div class="chips" id="fState"></div></div>
<div class="rsec"><h4>City</h4><div class="chips" id="fCity"></div></div>
<div class="rsec"><h4>Lead quality</h4><div class="chips" id="fQual">
<span class="chip" data-q="below" title="Rate is below the market average at origination — an assumable advantage">◆ Below-market only</span>
<span class="chip" data-q="historic" title="Originated in a historic-low-rate era">◆ Historic-low era</span></div></div>
<div class="rsec"><h4>Holder / lender</h4><div class="chips" id="fHolder"></div></div>
<div class="rsec"><h4>Units</h4><div style="display:flex;gap:6px"><input type="number" id="uMin" placeholder="min" style="width:70px"><input type="number" id="uMax" placeholder="max" style="width:70px"></div></div>
<div class="rsec"><h4>Max rate %</h4><input type="number" id="rMax" placeholder="e.g. 4" step="0.5" style="width:90px"></div>
</aside>
<div class="main">
<div id="stats"></div>
<div class="prov">Source: <b>HUD/FHA Multifamily Insured Mortgages (Active)</b> — U.S. government public data ($0, no license, resellable). Rate spread = market average (Freddie PMMS) at the loan's origination month − the loan's actual rate. A large spread = a below-market note a qualified buyer may be able to assume. Single-family FHA per-loan rates are NOT public (borrower privacy), so this is the commercial multifamily universe.</div>
<div class="toolbar">
<input class="search" id="q" placeholder="search property, city, lender, zip…">
<span class="viewseg tv-seg" id="viewseg"></span>
<select id="sort"></select>
<label style="font-size:11px;color:var(--mut)">density <input type="range" id="dens" min="1" max="8" step="1"></label>
<span class="count" id="count"></span>
</div>
<div id="tableView"><table><thead id="head"></thead><tbody id="body"></tbody></table></div>
<div id="gridView" class="fha-grid"></div>
<div id="listView"></div>
</div></div>
<script src="/three-view.js"></script>
<script>
const $=s=>document.querySelector(s);
const money=n=>n==null||n===''||isNaN(+n)?'—':'$'+Math.round(+n).toLocaleString();
const esc=s=>(s==null?'':String(s)).replace(/&/g,'&').replace(/</g,'<');
const COLS=[
{k:'property',l:'Property',t:'s'},{k:'city',l:'City',t:'s'},{k:'state',l:'ST',t:'s'},{k:'zip',l:'Zip',t:'s'},
{k:'units',l:'Units',t:'n'},{k:'originalAmount',l:'Orig $',t:'money'},{k:'rate',l:'Rate %',t:'pct'},
{k:'pmmsBenchmark',l:'Mkt @ orig',t:'pct'},{k:'spread',l:'Spread',t:'spread',calc:r=>(r.pmmsBenchmark!=null&&r.rate!=null)?+(r.pmmsBenchmark-r.rate).toFixed(2):null},
{k:'holder',l:'Lender',t:'s'},{k:'originationDate',l:'Originated',t:'s'},{k:'belowMarket',l:'Assumable?',t:'flag',calc:r=>r.belowMarket}
];
let DATA=[],sortKey='spread',sortDir=-1,q='',TV=null,CURRENT=[];
const F={state:new Set(),city:new Set(),holder:new Set(),qual:new Set(),uMin:null,uMax:null,rMax:null};
let dens=(function(){const v=parseInt(localStorage.getItem('fhaDens'),10);return(v>=1&&v<=8)?v:4;})();
const cval=(r,c)=>c.calc?c.calc(r):r[c.k];
const tog=(s,v)=>{s.has(v)?s.delete(v):s.add(v);};
function pass(r){
if(F.state.size&&!F.state.has(r.state))return false;
if(F.city.size&&!F.city.has(r.city))return false;
if(F.holder.size&&!F.holder.has(r.holder))return false;
if(F.qual.has('below')&&!r.belowMarket)return false;
if(F.qual.has('historic')&&!r.historicLowEra)return false;
if(F.uMin!=null&&!(+r.units>=F.uMin))return false;
if(F.uMax!=null&&!(+r.units<=F.uMax))return false;
if(F.rMax!=null&&!(+r.rate<=F.rMax))return false;
if(q){const t=q.toLowerCase();return COLS.some(c=>{const v=cval(r,c);return v!=null&&String(v).toLowerCase().includes(t);});}
return true;
}
function filtered(){
let rows=DATA.filter(pass);
const c=COLS.find(x=>x.k===sortKey)||COLS[8];
rows=rows.slice().sort((a,b)=>{let x=cval(a,c),y=cval(b,c);const xm=(x==null||x===''),ym=(y==null||y===''); if(xm&&ym)return 0;if(xm)return 1;if(ym)return -1;
if(!isNaN(+x)&&!isNaN(+y)){x=+x;y=+y;}else{x=String(x).toLowerCase();y=String(y).toLowerCase();}
return x<y?-1*sortDir:x>y?sortDir:0;});
return rows;
}
function cell(r,c){const v=cval(r,c);
if(c.t==='money')return money(v);
if(c.t==='pct')return v==null||v===''?'—':(+v).toFixed(2)+'%';
if(c.t==='n')return v==null?'—':(+v).toLocaleString();
if(c.t==='spread')return v==null?'—':`<span class="spread">▼ ${v} pts</span>`;
if(c.t==='flag')return v?`<span class="pill gold">ASSUMABLE ◆</span>`:'—';
return esc(v==null||v===''?'—':v);
}
function chip(obj,attr,lim){return Object.entries(obj).sort((a,b)=>b[1]-a[1]).slice(0,lim||99).map(([k,c])=>`<span class="chip" data-${attr}="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');}
function buildRail(){
const sc={},cc={},hc={};
DATA.forEach(r=>{if(r.state)sc[r.state]=(sc[r.state]||0)+1;if(r.city)cc[r.city]=(cc[r.city]||0)+1;if(r.holder)hc[r.holder]=(hc[r.holder]||0)+1;});
$('#fState').innerHTML=chip(sc,'st',12);$('#fCity').innerHTML=chip(cc,'cy',18);$('#fHolder').innerHTML=chip(hc,'ho',14);
}
// ── the three view renderers (delegated to by ThreeView; each receives the current filtered+sorted rows) ──
function renderTable(rows){
$('#head').innerHTML='<tr>'+COLS.map(c=>`<th data-k="${c.k}">${esc(c.l)}${sortKey===c.k?(sortDir<0?' ↓':' ↑'):''}</th>`).join('')+'</tr>';
const fs=Math.max(10,15-dens);
$('#body').innerHTML=rows.map(r=>`<tr style="font-size:${fs}px">`+COLS.map(c=>`<td class="${c.t==='money'||c.t==='n'||c.t==='pct'?'mono':''}">${cell(r,c)}</td>`).join('')+'</tr>').join('');
}
function renderGrid(rows){
const gv=$('#gridView'); gv.style.setProperty('--cols',dens);
const kv=(l,v)=>`<div><em>${l}</em><span>${v}</span></div>`;
gv.innerHTML=rows.map(r=>{
const spread=(r.pmmsBenchmark!=null&&r.rate!=null)?+(r.pmmsBenchmark-r.rate).toFixed(2):null;
return `<div class="fcard">`+
`<div class="fcard-h"><span class="fcard-t">${esc(r.property||'—')}</span>${r.belowMarket?'<span class="pill gold">ASSUMABLE ◆</span>':''}</div>`+
`<div class="fcard-sub">${esc(r.city||'')}${r.state?', '+esc(r.state):''}${r.zip?' · '+esc(r.zip):''}</div>`+
(spread!=null&&spread>0?`<div class="fcard-spread">▼ ${spread} pts<span>below market</span></div>`:'')+
`<div class="fcard-kv">`+
kv('Units',r.units!=null?(+r.units).toLocaleString():'—')+
kv('Orig $',money(r.originalAmount))+
kv('Rate',r.rate!=null?(+r.rate).toFixed(2)+'%':'—')+
kv('Mkt @ orig',r.pmmsBenchmark!=null?(+r.pmmsBenchmark).toFixed(2)+'%':'—')+
kv('Lender',esc(r.holder||'—'))+
kv('Originated',esc(r.originationDate||'—'))+
`</div></div>`;
}).join('')||'<div style="color:var(--mut);padding:30px">No matches for these filters.</div>';
}
function render(){
CURRENT=filtered();
const rows=CURRENT;
const below=rows.filter(r=>r.belowMarket).length;
const avgSpread=rows.filter(r=>r.belowMarket&&r.pmmsBenchmark!=null&&r.rate!=null);
const avg=avgSpread.length?(avgSpread.reduce((s,r)=>s+(r.pmmsBenchmark-r.rate),0)/avgSpread.length).toFixed(1):'—';
$('#stats').innerHTML=`<div class="stat"><b>${rows.length.toLocaleString()}</b><span>leads shown</span></div>`+
`<div class="stat"><b style="color:var(--gold)">${below.toLocaleString()}</b><span>below-market (assumable)</span></div>`+
`<div class="stat"><b>${avg}${avg==='—'?'':' pts'}</b><span>avg assumable spread</span></div>`+
`<div class="stat"><b>${new Set(rows.map(r=>r.city)).size}</b><span>cities</span></div>`;
$('#count').textContent=`${rows.length.toLocaleString()} of ${DATA.length.toLocaleString()}`;
if(TV) TV.render(); else renderTable(rows);
}
$('#head').addEventListener('click',e=>{const th=e.target.closest('th');if(!th)return;const k=th.dataset.k;if(sortKey===k)sortDir*=-1;else{sortKey=k;sortDir=(k==='property'||k==='city'||k==='holder'||k==='state')?1:-1;}render();});
$('#fState').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.state,c.dataset.st);c.classList.toggle('active');render();});
$('#fCity').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.city,c.dataset.cy);c.classList.toggle('active');render();});
$('#fHolder').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.holder,c.dataset.ho);c.classList.toggle('active');render();});
$('#fQual').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.qual,c.dataset.q);c.classList.toggle('active');render();});
$('#uMin').addEventListener('input',()=>{F.uMin=$('#uMin').value?+$('#uMin').value:null;render();});
$('#uMax').addEventListener('input',()=>{F.uMax=$('#uMax').value?+$('#uMax').value:null;render();});
$('#rMax').addEventListener('input',()=>{F.rMax=$('#rMax').value?+$('#rMax').value:null;render();});
$('#q').addEventListener('input',()=>{q=$('#q').value.trim();render();});
$('#sort').addEventListener('change',()=>{sortKey=$('#sort').value;sortDir=(sortKey==='property'||sortKey==='city')?1:-1;render();});
$('#dens').addEventListener('input',()=>{dens=+$('#dens').value;localStorage.setItem('fhaDens',dens);render();});
$('#sort').innerHTML=COLS.map(c=>`<option value="${c.k}"${c.k==='spread'?' selected':''}>Sort: ${esc(c.l)}</option>`).join('');
$('#dens').value=dens;
// grid / list / table toggle + compact-list view (three-view). Sort stays owned by the
// page's own #sort select + header clicks (getRows returns already-sorted rows), so we
// don't pass a sortSel — ThreeView just adds the view modes + the compact list.
TV=ThreeView.mount({
seg:'#viewseg', gridMount:'#gridView', listMount:'#listView', tableMount:'#tableView',
getRows:()=>CURRENT, // full set — three-view.js windows it via infinite scroll (count reports the full match total)
fields:COLS.filter(c=>!['property','city','state'].includes(c.k)).map(c=>({k:c.k,l:c.l,html:true,calc:r=>cell(r,c)})),
title:r=>({name:r.property||'—', sub:(r.city||'')+(r.state?', '+r.state:'')}),
badgeHtml:r=>r.belowMarket?'<span class="pill gold">ASSUMABLE ◆</span>':'',
renderGrid:renderGrid, renderTable:renderTable,
storageKey:'fha', defaultView:'table', views:['grid','list','table']
});
fetch('/data/fha-loans.json').then(r=>r.json()).then(j=>{DATA=(j.rows||[]).filter(r=>r.rate!=null);buildRail();render();})
.catch(()=>{$('#count').textContent='failed to load /data/fha-loans.json';});
</script><script src="/corner-nav.js" defer></script>
<script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
<script src="/filter-modal.js" defer></script><!-- filters → collapsed modal panels -->
<script src="/crcp-theme.js" defer></script><!-- CRCP day/night toggle (auto-injects into header) -->
</body></html>