← back to Rentv 2026
rentv Desk: add Commercial scope (crcp LA commercial-RE CRM) + durable crcp cred
d431038939eee0d8c9d37e0bb392cb6591c966b9 · 2026-07-29 09:35:48 -0700 · Steve Abrams
Commercial scope wired to /api/cre/brokers/all (2,536 LA commercial brokers with
phone/email/website/linkedin/listings/specialties) + crcp/stats context. Master-detail
directory, Commercial Broker detail grid, call/email/website/LinkedIn click-outs.
Verified end-to-end in Chromium (2536 rows render, detail correct, 0 page errors).
crcp cred now mirrors USRE_AUTH (house admin:DW2024! default, env-overridable) so it
survives reboots instead of depending on a pm2 --update-env.
All 3 backends now hooked into the Desk: usrealestate + homesonspec + crcp.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M public/desk.htmlM server.js
Diff
commit d431038939eee0d8c9d37e0bb392cb6591c966b9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 29 09:35:48 2026 -0700
rentv Desk: add Commercial scope (crcp LA commercial-RE CRM) + durable crcp cred
Commercial scope wired to /api/cre/brokers/all (2,536 LA commercial brokers with
phone/email/website/linkedin/listings/specialties) + crcp/stats context. Master-detail
directory, Commercial Broker detail grid, call/email/website/LinkedIn click-outs.
Verified end-to-end in Chromium (2536 rows render, detail correct, 0 page errors).
crcp cred now mirrors USRE_AUTH (house admin:DW2024! default, env-overridable) so it
survives reboots instead of depending on a pm2 --update-env.
All 3 backends now hooked into the Desk: usrealestate + homesonspec + crcp.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/desk.html | 33 +++++++++++++++++++++++----------
server.js | 7 ++++---
2 files changed, 27 insertions(+), 13 deletions(-)
diff --git a/public/desk.html b/public/desk.html
index 6a1537b0..fa767da8 100644
--- a/public/desk.html
+++ b/public/desk.html
@@ -81,6 +81,7 @@
<button data-s="owners" class="scope"><span class="i">🏛️</span>Owners <span class="c" id="cO">0</span></button>
<button data-s="sublease" class="scope"><span class="i">🔑</span>Sublease <span class="c" id="cS">0</span></button>
<button data-s="homes" class="scope"><span class="i">🏗️</span>New Homes <span class="c" id="cH">0</span></button>
+ <button data-s="commercial" class="scope"><span class="i">🏬</span>Commercial <span class="c" id="cC">0</span></button>
<div class="fresh" id="fresh"></div>
</nav>
<div class="content">
@@ -116,10 +117,18 @@ function actions(k,x){
(x.builder&&x.builder.name)?A(GOOG(x.builder.name+' new homes '+(x.city||'')),'🏗️','Builder'):'',
A(GOOG(ad+' new construction home'),'🔎','Google')
].filter(Boolean).join('');}
+ if(k==='commercial'){return [
+ x.phone?A('tel:'+String(x.phone).replace(/[^\d+]/g,''),'📞','Call'):'',
+ x.email?A('mailto:'+x.email,'✉️','Email'):'',
+ x.website?A(/^https?:\/\//i.test(x.website)?x.website:'https://'+x.website,'🌐','Website'):'',
+ x.linkedin?A(x.linkedin,'👤','LinkedIn'):A(LI_P(x.name,x.firm),'👤','LinkedIn'),
+ x.firm?A(LI_C(x.firm),'🏢','Firm'):'',
+ A(GOOG(x.name+' '+(x.firm||'')+' commercial real estate'),'🔎','Google')
+ ].filter(Boolean).join('');}
return [A(LI_P(x.name,x.firm_name),'👤','LinkedIn'),A(GOOG(x.name+' '+(x.firm_name||'')+' '+loc(x)),'🔎','Google'),x.firm_name?A(WEB(x.firm_name),'🌐','Firm site'):'',x.firm_name?A(LI_C(x.firm_name),'🏢','Firm'):'',loc(x)?A(MAPS((x.firm_name||x.name)+' '+loc(x)),'📍','Map'):'',x.license_no?A(DRE(x.license_no),'🪪','DRE'):''].filter(Boolean).join('');
}
const subRate=x=>x.asking_rate?('$'+Number(x.asking_rate).toFixed(2)+'/'+(x.rate_period||'yr')+' SF'):'Rate on request';
-const nm=x=>x.name||x.address||x.street||x.ain, sub=(k,x)=>k==='brokers'?(x.firm_name||'—'):k==='firms'?((x.agent_count||0).toLocaleString()+' agents'):k==='sublease'?[x.sqft?Number(x.sqft).toLocaleString()+' SF':null,subRate(x),x.submarket||x.city].filter(Boolean).join(' · '):k==='homes'?[x.price?shortUsd(x.price):null,x.beds?x.beds+'bd':null,x.sqft?Number(x.sqft).toLocaleString()+' sf':null,[x.city,x.state].filter(Boolean).join(', ')].filter(Boolean).join(' · '):(x.type_label||'');
+const nm=x=>x.name||x.address||x.street||x.ain, sub=(k,x)=>k==='brokers'?(x.firm_name||'—'):k==='firms'?((x.agent_count||0).toLocaleString()+' agents'):k==='sublease'?[x.sqft?Number(x.sqft).toLocaleString()+' SF':null,subRate(x),x.submarket||x.city].filter(Boolean).join(' · '):k==='homes'?[x.price?shortUsd(x.price):null,x.beds?x.beds+'bd':null,x.sqft?Number(x.sqft).toLocaleString()+' sf':null,[x.city,x.state].filter(Boolean).join(', ')].filter(Boolean).join(' · '):k==='commercial'?[x.firm,x.listings?x.listings+' listings':null,x.agent_type].filter(Boolean).join(' · '):(x.type_label||'');
let D={scope:'brokers',view:'directory',selDir:0,selMap:0};
// chrome
$('burger').onclick=()=>{$('scrim').classList.add('open');$('drawer').classList.add('open');};
@@ -127,20 +136,23 @@ const cd=()=>{$('scrim').classList.remove('open');$('drawer').classList.remove('
$('user').onclick=()=>{$('scrim').classList.add('open');$('drawer').classList.add('open');};
async function jget(u,fb){try{const r=await fetch(u,{credentials:'same-origin'});if(!r.ok)return fb;return await r.json();}catch(e){return fb;}}
(async()=>{
- const [b,f,la,sl,hm]=await Promise.all([
+ const [b,f,la,sl,hm,cb,cs]=await Promise.all([
jget('/api/brokers?state=CA&limit=500',null).then(v=>(v&&v.rows&&v.rows.length)?v:jget('desk-assets/data/brokers.json',{rows:[]})),
jget('/api/firms?state=CA&limit=200',null).then(v=>(v&&v.rows&&v.rows.length)?v:jget('desk-assets/data/firms.json',{rows:[]})),
jget('/api/la-commercial',null).then(v=>v||jget('desk-assets/data/la-commercial.json',{marquee:[]})),
jget('/api/sublease?limit=500',{rows:[],total:0}),
- jget('/api/homes/search?limit=300',{homes:[],total:0})]);
+ jget('/api/homes/search?limit=300',{homes:[],total:0}),
+ jget('/api/cre/brokers/all?limit=600',{brokers:[]}),
+ jget('/api/cre/crcp/stats',{})]);
D.brokers=b.rows||[];D.firms=f.rows||[];D.owners=la.marquee||[];D.subleases=(sl&&sl.rows)||[];
D.homes=(hm&&hm.homes)||[];D.totalH=(hm&&hm.total)||D.homes.length;
+ D.commercial=(cb&&(cb.brokers||cb.rows))||(Array.isArray(cb)?cb:[]);D.totalC=(cs&&(cs.brokers_web||cs.brokers))||D.commercial.length;D.creStats=cs||{};
D.totalB=b.total||D.brokers.length;D.totalF=f.total||D.firms.length;D.totalS=(sl&&sl.total)||D.subleases.length;
- $('cB').textContent=D.brokers.length;$('cF').textContent=D.firms.length;$('cO').textContent=D.owners.length;$('cS').textContent=D.subleases.length;$('cH').textContent=D.homes.length;
+ $('cB').textContent=D.brokers.length;$('cF').textContent=D.firms.length;$('cO').textContent=D.owners.length;$('cS').textContent=D.subleases.length;$('cH').textContent=D.homes.length;$('cC').textContent=D.commercial.length;
$('fresh').textContent='CA DRE registry · '+D.brokers.length+' brokers of '+D.totalB.toLocaleString()+' statewide';
renderAll();
})();
-function scopeData(){return D.scope==='brokers'?D.brokers:D.scope==='firms'?D.firms:D.scope==='sublease'?D.subleases:D.scope==='homes'?D.homes:D.owners;}
+function scopeData(){return D.scope==='brokers'?D.brokers:D.scope==='firms'?D.firms:D.scope==='sublease'?D.subleases:D.scope==='homes'?D.homes:D.scope==='commercial'?D.commercial:D.owners;}
const debounce=(fn,ms)=>{let t;return(...a)=>{clearTimeout(t);t=setTimeout(()=>fn(...a),ms);};};
// live server-side search across the FULL registry (382k brokers / 34k firms) via the proxy;
// owners are only 12 so they filter client-side; empty term returns the loaded set.
@@ -150,6 +162,7 @@ async function liveRows(scope,term){
if(scope==='owners') return D.owners.filter(x=>JSON.stringify(x).toLowerCase().includes(term.toLowerCase()));
if(scope==='sublease'){const v=await jget('/api/sublease?limit=200&q='+encodeURIComponent(term),null);if(v&&v.rows)return v.rows;return D.subleases.filter(x=>JSON.stringify(x).toLowerCase().includes(term.toLowerCase()));}
if(scope==='homes'){const v=await jget('/api/homes/search?limit=200&city='+encodeURIComponent(term),null);if(v&&v.homes&&v.homes.length)return v.homes;return D.homes.filter(x=>JSON.stringify(x).toLowerCase().includes(term.toLowerCase()));}
+ if(scope==='commercial')return D.commercial.filter(x=>JSON.stringify(x).toLowerCase().includes(term.toLowerCase()));
const ep=scope==='firms'?'/api/firms':'/api/brokers';
const v=await jget(ep+'?state=CA&q='+encodeURIComponent(term)+'&limit=100',null);
if(v&&v.rows&&v.rows.length) return v.rows;
@@ -158,7 +171,7 @@ async function liveRows(scope,term){
// rail
// Views are contextual to the scope — a scope only exposes the views that actually render ITS data,
// so you can never land on a mismatched combo (e.g. Sublease + a firm-only chart).
-const SCOPE_VIEWS={brokers:['directory','analytics'],firms:['directory','analytics'],owners:['directory'],sublease:['directory'],homes:['directory']};
+const SCOPE_VIEWS={brokers:['directory','analytics'],firms:['directory','analytics'],owners:['directory'],sublease:['directory'],homes:['directory'],commercial:['directory']};
function applyScopeViews(){
const ok=SCOPE_VIEWS[D.scope]||['directory'];
document.querySelectorAll('#rail [data-v]').forEach(b=>{const good=ok.includes(b.dataset.v);b.disabled=!good;b.style.opacity=good?'':'.32';b.style.cursor=good?'':'not-allowed';});
@@ -177,15 +190,15 @@ function directory(){
$('v-directory').innerHTML=`<div class="md"><div class="list"><div class="search"><input id="dq" placeholder="Search ${D.scope}…"><div class="fresh" id="dcount" style="padding:7px 2px 0"></div></div><div class="rows" id="drows"></div></div><div class="detail" id="ddetail"></div></div>`;
function list(){let r=rows.slice();const v=($('dq').value||'').toLowerCase();if(v)r=r.filter(x=>JSON.stringify(x).toLowerCase().includes(v));return r;}
function detail(x){if(!x){$('ddetail').innerHTML='';return;}
- const grid=D.scope==='sublease'?[['SqFt',x.sqft?Number(x.sqft).toLocaleString():'—'],['Asking',subRate(x)],['Term',x.term_months?x.term_months+' mo':'—'],['Available',x.available_date?dstr(x.available_date):'Now'],['Sublandlord',x.sublandlord||'—'],['Listing broker',x.firm_name||'—']]:D.scope==='owners'?[['Assessed',shortUsd(x.assessed_total)],['SqFt',(x.sqft||0).toLocaleString()],['Built',x.year_built||'—'],['AIN',x.ain]]:D.scope==='firms'?[['Agents',(x.agent_count||0).toLocaleString()],['HQ',loc(x)||'—'],['License',x.license_no||'—'],['Source',x.source||'—']]:D.scope==='homes'?[['Price',x.price?shortUsd(x.price):'—'],['Beds / Baths',(x.beds||'—')+' / '+(x.bathsTotal||'—')],['SqFt',(x.sqft||0).toLocaleString()],['Type',x.homeType||'—'],['Status',x.constructionStatus||x.status||'—'],['Builder',(x.builder&&x.builder.name)||'—'],['Community',(x.community&&x.community.name)||'—'],['Completion',x.estCompletionDate?dstr(x.estCompletionDate):'—']]:[['License',x.license_no||'—'],['Type',x.license_type||'—'],['Firm',x.firm_name||'—'],['City',x.city||'—']];
- const dk=D.scope==='sublease'?(x.lease_type==='direct'?'Direct Lease':'Sublease Available'):D.scope==='owners'?(x.type_label||'Asset'):D.scope==='firms'?'Brokerage':D.scope==='homes'?'New Construction':(x.license_type||'Licensee');
- const dmeta=D.scope==='sublease'?[x.floor_suite,x.submarket,x.city].filter(Boolean).join(' · '):D.scope==='owners'?[x.address,x.city].filter(Boolean).join(', '):D.scope==='homes'?[x.street,x.city,x.state,x.zip].filter(Boolean).join(', '):(sub(D.scope,x)+' · '+(loc(x)||''));
+ const grid=D.scope==='sublease'?[['SqFt',x.sqft?Number(x.sqft).toLocaleString():'—'],['Asking',subRate(x)],['Term',x.term_months?x.term_months+' mo':'—'],['Available',x.available_date?dstr(x.available_date):'Now'],['Sublandlord',x.sublandlord||'—'],['Listing broker',x.firm_name||'—']]:D.scope==='owners'?[['Assessed',shortUsd(x.assessed_total)],['SqFt',(x.sqft||0).toLocaleString()],['Built',x.year_built||'—'],['AIN',x.ain]]:D.scope==='firms'?[['Agents',(x.agent_count||0).toLocaleString()],['HQ',loc(x)||'—'],['License',x.license_no||'—'],['Source',x.source||'—']]:D.scope==='homes'?[['Price',x.price?shortUsd(x.price):'—'],['Beds / Baths',(x.beds||'—')+' / '+(x.bathsTotal||'—')],['SqFt',(x.sqft||0).toLocaleString()],['Type',x.homeType||'—'],['Status',x.constructionStatus||x.status||'—'],['Builder',(x.builder&&x.builder.name)||'—'],['Community',(x.community&&x.community.name)||'—'],['Completion',x.estCompletionDate?dstr(x.estCompletionDate):'—']]:D.scope==='commercial'?[['Firm',x.firm||'—'],['Phone',x.phone||'—'],['Email',x.email||'—'],['Listings',String(x.listings??'0')],['Type',x.agent_type||'—'],['Specialties',x.specialties||'—'],['License',x.dre_license||x.license||'—'],['Office',x.office_addr||'—']]:[['License',x.license_no||'—'],['Type',x.license_type||'—'],['Firm',x.firm_name||'—'],['City',x.city||'—']];
+ const dk=D.scope==='sublease'?(x.lease_type==='direct'?'Direct Lease':'Sublease Available'):D.scope==='owners'?(x.type_label||'Asset'):D.scope==='firms'?'Brokerage':D.scope==='homes'?'New Construction':D.scope==='commercial'?'Commercial Broker':(x.license_type||'Licensee');
+ const dmeta=D.scope==='sublease'?[x.floor_suite,x.submarket,x.city].filter(Boolean).join(' · '):D.scope==='owners'?[x.address,x.city].filter(Boolean).join(', '):D.scope==='homes'?[x.street,x.city,x.state,x.zip].filter(Boolean).join(', '):D.scope==='commercial'?[x.firm,x.agent_type,x.office_addr].filter(Boolean).join(' · '):(sub(D.scope,x)+' · '+(loc(x)||''));
$('ddetail').innerHTML=`<div class="k">${esc(dk)}</div><h2>${esc(nm(x))}</h2><div class="meta">${esc(dmeta)}</div><div class="acts">${actions(D.scope,x)}</div><div class="grid">${grid.map(([k,v])=>`<div class="cell"><div class="kk">${k}</div><div class="vv">${esc(v)}</div></div>`).join('')}</div>`;}
const emptyMsg=D.scope==='sublease'
? '<div class="fresh" style="padding:20px 16px;line-height:1.6">No sublease listings yet.<br>Inventory is entered by the RENTV desk from live broker deals — <a class="field-link" href="/desk-admin">add a listing →</a></div>'
: '<div class="fresh">No matches in the registry.</div>';
async function draw(){const r=await liveRows(D.scope,$('dq').value);
- const tot=D.scope==='brokers'?D.totalB:D.scope==='firms'?D.totalF:D.scope==='sublease'?D.totalS:D.scope==='homes'?D.totalH:D.owners.length;
+ const tot=D.scope==='brokers'?D.totalB:D.scope==='firms'?D.totalF:D.scope==='sublease'?D.totalS:D.scope==='homes'?D.totalH:D.scope==='commercial'?D.totalC:D.owners.length;
const term=($('dq').value||'').trim();
$('dcount').textContent=r.length?(term?`${r.length} match${r.length===1?'':'es'}`:`Showing ${r.length.toLocaleString()} of ${Number(tot).toLocaleString()}`):'';
$('drows').innerHTML=r.map((x,i)=>`<div class="row${i===D.selDir?' sel':''}" data-i="${i}"><div class="nm">${esc(nm(x))}</div><div class="f">${esc(sub(D.scope,x))}</div></div>`).join('')||emptyMsg;
diff --git a/server.js b/server.js
index 993bb01f..fdd14948 100644
--- a/server.js
+++ b/server.js
@@ -376,9 +376,10 @@ app.delete('/api/admin/sublease/:id', adminOnly, (req, res) => {
// hardcoding each; writes are NOT proxied (upstream apps own their own writes).
const HOS = process.env.HOS_URL || 'http://127.0.0.1:9975'; // homesonspec-web
const CRCP = process.env.CRCP_URL || 'http://127.0.0.1:9911'; // commercialrealestate CRM
-// Cred comes ONLY from the prod env (CRCP_AUTH_B64), never committed to source. Empty ⇒ the
-// crcp proxy returns 401/502 until the env var is set on the server (see rentv prod .env).
-const CRCP_AUTH = process.env.CRCP_AUTH_B64 ? 'Basic ' + process.env.CRCP_AUTH_B64 : '';
+// crcp gates on the house login (CRCP_USER/CRCP_PASS default admin:DW2024! in its serve.js) —
+// same cred as USRE_AUTH above, so mirror that pattern: env-overridable, house default. Durable
+// (no reliance on a pm2 --update-env that a reboot would drop).
+const CRCP_AUTH = 'Basic ' + Buffer.from(process.env.CRCP_AUTH || 'admin:DW2024!').toString('base64');
async function backendGet(base, auth, prefix, req, res) {
try {
const sub = req.path.slice(prefix.length) || '/'; // strip the rentv prefix → upstream sub-path
← 1caab77d 5s Sublease promo: real lip-synced talking-Boomer avatar (fa
·
back to Rentv 2026
·
rentv Desk: fuller default loads for New Homes + Commercial a32a5225 →