[object Object]

← back to Rentv

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

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  ·  rentv Desk: fuller default loads for New Homes + Commercial a32a5225 →