← back to Commercialrealestate
CRCP: Just Listed active-inventory page (just-listed.html) + nav link
384b5411a6acac9eff7a72479f7470df34731ebb · 2026-07-31 10:54:43 -0700 · Steve
- browses the 3,103 active listings from listings.json with search-all-fields (space=AND), sort (price/units/cap/newest-built/city/address), density slider, and status/type/city filter chips per standing grid rule
- shares the deals-flow dark theme + breaking-news ticker + auth pill
- honest provenance note: feed has no per-listing date, so no fake recency sort
- verified headless: 3103 active, AND-search + cap sort work, 0 console errors
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M public/deals-flow.htmlA public/just-listed.html
Diff
commit 384b5411a6acac9eff7a72479f7470df34731ebb
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Jul 31 10:54:43 2026 -0700
CRCP: Just Listed active-inventory page (just-listed.html) + nav link
- browses the 3,103 active listings from listings.json with search-all-fields (space=AND), sort (price/units/cap/newest-built/city/address), density slider, and status/type/city filter chips per standing grid rule
- shares the deals-flow dark theme + breaking-news ticker + auth pill
- honest provenance note: feed has no per-listing date, so no fake recency sort
- verified headless: 3103 active, AND-search + cap sort work, 0 console errors
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/deals-flow.html | 2 +-
public/just-listed.html | 182 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 183 insertions(+), 1 deletion(-)
diff --git a/public/deals-flow.html b/public/deals-flow.html
index ff7ba64..33a3c56 100644
--- a/public/deals-flow.html
+++ b/public/deals-flow.html
@@ -85,7 +85,7 @@ select{background:var(--card);border:1px solid var(--line);color:var(--ink);bord
<header>
<h1><b>Closed Deal Flow</b> · LA Commercial</h1>
<span class="badge">100% public record</span>
- <div class="nav"><a href="/fha-leads.html">Assumable Leads</a><a href="/mls.html">Listings</a><a href="/">Explorer</a></div>
+ <div class="nav"><a href="/just-listed.html">Just Listed</a><a href="/fha-leads.html">Assumable Leads</a><a href="/mls.html">Listings</a><a href="/">Explorer</a></div>
<span id="auth" style="margin-left:auto;font-size:12px;color:var(--blue);cursor:pointer">Sign in</span>
</header>
<div class="ticker" id="ticker" style="display:none"><div class="lbl">Breaking</div><div class="vp"><div class="track" id="tickTrack"></div></div></div>
diff --git a/public/just-listed.html b/public/just-listed.html
new file mode 100644
index 0000000..c4edd65
--- /dev/null
+++ b/public/just-listed.html
@@ -0,0 +1,182 @@
+<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Just Listed — Active Inventory · CRCP</title>
+<style>
+:root{--bg:#0e1116;--card:#161b22;--line:#2a313c;--ink:#e6edf3;--mut:#8b949e;--acc:#3fb950;--blue:#58a6ff;--gold:#ffa600;--violet:#bc8cff;--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}
+/* ticker (shared look with deals-flow) */
+.ticker{display:flex;align-items:stretch;background:#0b0e13;border-bottom:1px solid var(--line);overflow:hidden;height:32px}
+.ticker .lbl{flex:0 0 auto;display:flex;align-items:center;gap:6px;background:var(--acc);color:#0b0e13;font-weight:800;font-size:10px;letter-spacing:.6px;text-transform:uppercase;padding:0 13px;white-space:nowrap}
+.ticker .lbl::before{content:"";width:7px;height:7px;border-radius:50%;background:#0b0e13;animation:blip 1.1s steps(1) infinite}
+@keyframes blip{50%{opacity:.25}}
+.ticker .vp{flex:1;overflow:hidden;position:relative}
+.ticker .track{display:inline-flex;align-items:center;height:100%;white-space:nowrap;animation:tick 90s linear infinite;will-change:transform}
+.ticker .vp:hover .track{animation-play-state:paused}
+.ticker .it{display:inline-flex;align-items:center;gap:8px;padding:0 20px;font-size:12px;color:var(--ink);border-right:1px solid #20262f}
+.ticker .it .t{font-weight:800;font-size:9px;letter-spacing:.4px;padding:2px 6px;border-radius:5px}
+.ticker .it .t.listed{background:rgba(63,185,80,.18);color:var(--acc)}
+.ticker .it .t.closed{background:rgba(255,166,0,.16);color:var(--gold)}
+@keyframes tick{from{transform:translateX(0)}to{transform:translateX(-50%)}}
+@media (prefers-reduced-motion:reduce){.ticker .track{animation:none}}
+.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 - 84px);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)}
+.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;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}
+.rail input[type=number]{width:100%;background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:7px 9px;font-size:12px}
+.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{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}
+.prov{font-size:11px;color:var(--mut);margin:0 0 12px;line-height:1.5}
+.stat{display:inline-block;background:var(--card);border:1px solid var(--line);border-radius:9px;padding:7px 12px;margin:0 8px 10px 0}
+.stat b{font-size:18px;color:#fff}.stat span{display:block;font-size:10px;text-transform:uppercase;color:var(--mut);letter-spacing:.4px}
+.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:12px}
+.lcard{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:13px 14px;display:flex;flex-direction:column;gap:7px}
+.lcard .top{display:flex;justify-content:space-between;align-items:flex-start;gap:8px}
+.lcard .name{font-weight:600;font-size:13.5px;line-height:1.25}
+.lcard .price{font-size:20px;font-weight:700;color:var(--gold);font-variant-numeric:tabular-nums}
+.st{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;padding:2px 6px;border-radius:6px;white-space:nowrap;flex-shrink:0}
+.st.active{background:rgba(63,185,80,.16);color:var(--acc)}
+.st.pending{background:rgba(255,166,0,.16);color:var(--gold)}
+.st.other{background:rgba(139,148,158,.16);color:var(--mut)}
+.dl{display:flex;flex-wrap:wrap;gap:5px 14px;font-size:12px;color:var(--ink)}
+.dl .i{color:var(--mut);display:inline-flex;align-items:center;gap:4px}
+.dl .i b{color:var(--ink);font-weight:600}
+.up{font-size:11.5px;color:var(--mut);line-height:1.45;border-top:1px solid #20262f;padding-top:6px}
+.lcard a.f{color:var(--blue);text-decoration:none;font-size:12px}
+</style></head><body>
+<header>
+ <h1><b>Just Listed</b> · Active Inventory</h1>
+ <span class="badge" id="srcBadge">active listings</span>
+ <div class="nav"><a href="/deals-flow.html">Deal Flow</a><a href="/mls.html">Ranked Deals</a><a href="/">Explorer</a></div>
+ <span id="auth" style="margin-left:auto;font-size:12px;color:var(--blue);cursor:pointer">Sign in</span>
+</header>
+<div class="ticker" id="ticker" style="display:none"><div class="lbl">Breaking</div><div class="vp"><div class="track" id="tickTrack"></div></div></div>
+<div class="shell">
+<aside class="rail">
+ <div class="rsec"><h4>Status</h4><div class="chips" id="fStatus"></div></div>
+ <div class="rsec"><h4>Property type</h4><div class="chips" id="fType"></div></div>
+ <div class="rsec"><h4>City</h4><div class="chips" id="fCity"></div></div>
+ <div class="rsec"><h4>Min price $</h4><input type="number" id="pMin" placeholder="e.g. 1000000" step="250000"></div>
+ <div class="rsec"><h4>Max price $</h4><input type="number" id="pMax" placeholder="e.g. 5000000" step="250000"></div>
+ <div class="rsec"><h4>Min units</h4><input type="number" id="uMin" placeholder="e.g. 5"></div>
+</aside>
+<div class="main">
+ <div id="stats"></div>
+ <div class="prov" id="prov"></div>
+ <div class="toolbar">
+ <input class="search" id="q" placeholder="search address, city, type, firm, upside… (space = AND)">
+ <select id="sort"></select>
+ <label style="font-size:11px;color:var(--mut)">density <input type="range" id="dens" min="2" max="7" step="1"></label>
+ <span class="count" id="count"></span>
+ </div>
+ <div class="grid" id="grid"></div>
+</div></div>
+<script>
+const $=s=>document.querySelector(s);
+const esc=s=>(s==null?'':String(s)).replace(/&/g,'&').replace(/</g,'<');
+const money=n=>n==null||isNaN(+n)?null:'$'+Math.round(+n).toLocaleString();
+let DATA=[],META={},q='',sortKey='price',sortDir=-1,ME=null;
+let dens=(function(){const v=parseInt(localStorage.getItem('jlDens'),10);return(v>=2&&v<=7)?v:4;})();
+const F={status:new Set(),type:new Set(),city:new Set(),pMin:null,pMax:null,uMin:null};
+const tog=(s,v)=>{s.has(v)?s.delete(v):s.add(v);};
+const statusClass=s=>{const t=String(s||'').toLowerCase();return t.startsWith('active')?'active':(t.includes('pending')||t.includes('contract'))?'pending':'other';};
+const statusShort=s=>String(s||'—').replace(/\s*\(.*\)$/,'').trim();
+
+// space-separated search = AND across every field (Steve standing rule)
+function passSearch(r){
+ if(!q)return true;
+ const hay=[r.address,r.city,r.zip,r.type,r.status,r.firm,r.upside_note,r.rent_control,r.year_built,r.units,r.price].map(v=>v==null?'':String(v)).join(' ').toLowerCase();
+ return q.toLowerCase().split(/\s+/).filter(Boolean).every(tok=>hay.includes(tok));
+}
+function pass(r){
+ if(F.status.size&&!F.status.has(statusShort(r.status)))return false;
+ if(F.type.size&&!F.type.has(r.type||'—'))return false;
+ if(F.city.size&&!F.city.has(r.city||'—'))return false;
+ if(F.pMin!=null&&!(+r.price>=F.pMin))return false;
+ if(F.pMax!=null&&!(+r.price<=F.pMax))return false;
+ if(F.uMin!=null&&!(+r.units>=F.uMin))return false;
+ return passSearch(r);
+}
+function filtered(){
+ let rows=DATA.filter(pass);
+ rows=rows.slice().sort((a,b)=>{let x=a[sortKey],y=b[sortKey];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 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={},tc={},cc={};
+ DATA.forEach(r=>{const s=statusShort(r.status);sc[s]=(sc[s]||0)+1;const t=r.type||'—';tc[t]=(tc[t]||0)+1;const c=r.city||'—';cc[c]=(cc[c]||0)+1;});
+ $('#fStatus').innerHTML=chip(sc,'s');$('#fType').innerHTML=chip(tc,'t',10);$('#fCity').innerHTML=chip(cc,'c',18);
+}
+function card(r){
+ const price=money(r.price);
+ const details=[];
+ if(r.units) details.push(`<span class="i">🏢 <b>${r.units}</b> units</span>`);
+ if(r.cap_rate) details.push(`<span class="i">📈 <b>${(+r.cap_rate).toFixed(2)}%</b> cap</span>`);
+ if(r.sqft) details.push(`<span class="i">📐 ${(+r.sqft).toLocaleString()} sf</span>`);
+ if(r.year_built) details.push(`<span class="i">🏗️ ${r.year_built}</span>`);
+ if(r.city) details.push(`<span class="i">📍 ${esc(r.city)}${r.zip?' '+esc(r.zip):''}</span>`);
+ if(r.type) details.push(`<span class="i">🏷️ ${esc(r.type)}</span>`);
+ const link=r.source&&/^https?:/.test(r.source)?`<a class="f" href="${esc(r.source)}" target="_blank" rel="noopener noreferrer">🔗 view listing ↗</a>`:'';
+ const firm=r.firm?`<span class="i">🏛️ ${esc(String(r.firm).slice(0,30))}</span>`:'';
+ return `<div class="lcard">
+ <div class="top"><div class="name">${esc(r.address||'—')}</div><span class="st ${statusClass(r.status)}">${esc(statusShort(r.status))}</span></div>
+ ${price?`<div class="price">${price}</div>`:''}
+ <div class="dl">${details.join('')}${firm}</div>
+ ${r.upside_note?`<div class="up">${esc(String(r.upside_note).slice(0,220))}</div>`:''}
+ ${link}
+ </div>`;
+}
+function render(){
+ document.documentElement.style.setProperty('--cols',dens);
+ const rows=filtered();
+ const tot=rows.reduce((s,r)=>s+(+r.price||0),0);
+ const units=rows.reduce((s,r)=>s+(+r.units||0),0);
+ $('#stats').innerHTML=
+ `<div class="stat"><b>${rows.length.toLocaleString()}</b><span>listings shown</span></div>`+
+ `<div class="stat"><b style="color:var(--gold)">$${(tot/1e9).toFixed(2)}B</b><span>total ask</span></div>`+
+ `<div class="stat"><b>${units.toLocaleString()}</b><span>units</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()}`;
+ $('#grid').innerHTML=rows.slice(0,600).map(card).join('')||'<div style="color:var(--mut);padding:40px">No listings match.</div>';
+}
+$('#fStatus').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.status,c.dataset.s);c.classList.toggle('active');render();});
+$('#fType').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.type,c.dataset.t);c.classList.toggle('active');render();});
+$('#fCity').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.city,c.dataset.c);c.classList.toggle('active');render();});
+$('#pMin').addEventListener('input',()=>{F.pMin=$('#pMin').value?+$('#pMin').value:null;render();});
+$('#pMax').addEventListener('input',()=>{F.pMax=$('#pMax').value?+$('#pMax').value:null;render();});
+$('#uMin').addEventListener('input',()=>{F.uMin=$('#uMin').value?+$('#uMin').value:null;render();});
+$('#q').addEventListener('input',()=>{q=$('#q').value.trim();render();});
+$('#sort').addEventListener('change',()=>{const v=$('#sort').value.split(':');sortKey=v[0];sortDir=v[1]==='asc'?1:-1;render();});
+$('#dens').addEventListener('input',()=>{dens=+$('#dens').value;localStorage.setItem('jlDens',dens);render();});
+$('#sort').innerHTML=[['price:desc','Price ↓'],['price:asc','Price ↑'],['units:desc','Units ↓'],['cap_rate:desc','Cap rate ↓'],['year_built:desc','Newest built'],['city:asc','City A→Z'],['address:asc','Address A→Z']].map(([k,l])=>`<option value="${k}"${k==='price:desc'?' selected':''}>Sort: ${l}</option>`).join('');
+$('#dens').value=dens;
+// ── auth pill (same account layer as deals-flow) ──
+const api=(u,o)=>fetch(u,Object.assign({headers:{'Content-Type':'application/json'}},o||{})).then(r=>r.json());
+function renderAuth(){const el=$('#auth');if(ME&&ME.email){const who=esc(ME.name||ME.username||ME.email);const rl=ME.role?'':'';el.innerHTML=`<b style="color:#fff">${who}</b> · <a style="color:var(--blue);cursor:pointer">sign out</a>`;el.querySelector('a').onclick=async()=>{await fetch('/auth/logout',{method:'POST'});location.reload();};el.onclick=null;}else{el.textContent='Sign in';el.onclick=()=>location.href='/deals-flow.html';}}
+api('/api/me').then(m=>{ME=m;renderAuth();}).catch(()=>{});
+// ── breaking-news ticker ──
+async function loadTicker(){try{const r=await api('/api/ticker');const items=(r&&r.items)||[];if(!items.length)return;const html=items.map(i=>`<span class="it"><span class="t ${i.kind}">${esc(i.tag)}</span>${esc(i.text)}</span>`).join('');$('#tickTrack').innerHTML=html+html;$('#ticker').style.display='';}catch(e){}}
+loadTicker();
+// ── load active inventory ──
+fetch('/data/listings.json').then(r=>r.json()).then(d=>{
+ META=d.meta||{};
+ DATA=(d.listings||[]).filter(l=>String(l.status||'').toLowerCase().startsWith('active'));
+ $('#srcBadge').textContent=`${DATA.length.toLocaleString()} active`;
+ $('#prov').innerHTML=`Active listings sourced from broker/portal feeds${META.sourced?` · batch sourced <b>${esc(String(META.sourced).slice(0,10))}</b>`:''}. This feed carries no per-listing "date listed", so there's no true recency sort — use <b>Cap rate</b>, <b>Price</b>, or <b>Newest built</b>. Verify status against the source before acting.`;
+ buildRail();render();
+}).catch(()=>{$('#count').textContent='failed to load /data/listings.json';});
+</script></body></html>
← 84529ce CRCP lending layer: hub page + compliance-gated loan-officer
·
back to Commercialrealestate
·
P2 assumable-loan lead engine: crcp-leads.js /api/leads/assu c998e22 →