[object Object]

← back to Rentv 2026

social console: /api/firm/:id proxy + firm→people drilldown, role icons on rows, RENTV-menu-grounded offer letters (real events/prices/contact)

3db0aeaf0f3090eb83e77b89ad44dc102d9cda82 · 2026-07-30 20:51:04 -0700 · Steve

Files touched

Diff

commit 3db0aeaf0f3090eb83e77b89ad44dc102d9cda82
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 30 20:51:04 2026 -0700

    social console: /api/firm/:id proxy + firm→people drilldown, role icons on rows, RENTV-menu-grounded offer letters (real events/prices/contact)
---
 public/social.html | 175 +++++++++++++++++++++++++++++++++++++++++------------
 server.js          |   1 +
 2 files changed, 138 insertions(+), 38 deletions(-)

diff --git a/public/social.html b/public/social.html
index e3f9657d..b0e78540 100644
--- a/public/social.html
+++ b/public/social.html
@@ -85,6 +85,23 @@
   .grail-add input { flex:1; }
   .grail-add button { padding:9px 12px; font-size:12.5px; }
   .grail-msgline { font-size:11.5px; color:var(--muted); margin-top:8px; min-height:14px; }
+  .gi-ic { flex:0 0 auto; width:18px; height:18px; color:var(--li); }
+  .gi-ic svg { width:18px; height:18px; display:block; }
+  .gi-ic.sm { width:15px; height:15px; } .gi-ic.sm svg { width:15px; height:15px; }
+  .gitem.cat .gi-ic { color:var(--ink); }
+  .gi-expand { border-color:var(--line) !important; color:var(--muted) !important; }
+  .gi-expand:hover { background:#eef2f7 !important; color:var(--ink) !important; }
+  .gpeople { margin:4px 0 2px 10px; border-left:2px solid #e3ebf3; padding-left:9px; }
+  .gp-firm { font-size:11px; color:var(--muted); margin:5px 0; }
+  .gp-firm a { color:var(--li); text-decoration:none; }
+  .gperson { display:flex; align-items:center; gap:7px; flex-wrap:wrap; padding:6px 3px; border-bottom:1px dashed #eef2f7; }
+  .gperson.sel { background:#f2f8ff; border-radius:6px; }
+  .gp-nm { font-weight:600; font-size:12.5px; }
+  .gp-sub { font-size:11px; color:var(--muted); }
+  .gp-a { font-size:11px; color:var(--li); text-decoration:none; border:1px solid #dbe7f4; border-radius:6px; padding:2px 6px; }
+  .gp-a:hover { background:#eef5fc; }
+  .gperson .gstatus { margin-top:0; }
+  .gperson .gp-let { margin:0; }
   @media (max-width:900px){ .grail.open { position:fixed; top:0; bottom:0; height:100vh; width:86vw; max-width:340px; } .grail.open .grail-body { max-height:100vh; } }
 </style>
 </head>
@@ -155,6 +172,7 @@
   </div>
 </main>
 </div><!-- /shell -->
+<script src="/desk-assets/role-icons.js"></script>
 <script>
   var PLAT = [
     ['linkedin','LinkedIn','in'], ['x','X (Twitter)','𝕏'], ['instagram','Instagram','◎'],
@@ -259,20 +277,34 @@
     var l=gload(); l.push({id:gid(),kind:'contact',cat:'Other',name:v,sub:'Custom target',terms:v+' commercial real estate',status:0,added:Date.now()});
     gsave(l); el.value=''; grender(); gmsg('Added ✓');
   }
+  function icoFor(cat){ return (window.ROLE_ICONS ? ROLE_ICONS.get(cat) : ''); }
   function gItemHtml(x, count){
     var when=new Date(x.added).toLocaleString(undefined,{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});
-    var isCat=x.kind==='category';
+    var isCat=x.kind==='category'||x.kind==='segment';
+    var isFirm=x.rid&&x.rid.charAt(0)==='f';
     var sel=(x.id===GSEL)?' sel':'';
-    var lLabel=(x.lv?'✎ Regenerate':'✎ Generate letter');
+    var lLabel=(x.lv?'✎ Regenerate':'✎ Letter');
     return '<div class="gitem'+(isCat?' cat':'')+sel+'">'
-      +'<div class="gi-top"><span class="gi-name">'+esc(x.name)+'</span>'
-      +(isCat&&count?'<span class="gi-count">'+count+' contacts</span>':'')
+      +'<div class="gi-top"><span class="gi-ic">'+icoFor(x.cat)+'</span><span class="gi-name">'+esc(x.name)+'</span>'
+      +(isCat&&count?'<span class="gi-count">'+count+'</span>':'')
       +'<a class="gi-li" href="'+liUrl(x.terms||x.name)+'" target="_blank" rel="noopener">in ↗</a>'
       +'<button class="gi-x" data-del="'+x.id+'" title="Remove">✕</button></div>'
       +(x.sub?'<div class="gi-sub">'+esc(x.sub)+'</div>':'')
       +'<span class="gstatus s'+(x.status||0)+'" data-st="'+x.id+'">'+GSTL[x.status||0]+'</span>'
       +'<span class="gwhen">🕓 '+when+'</span>'
-      +'<button class="gi-letter" data-letter="'+x.id+'">'+lLabel+'</button></div>';
+      +'<button class="gi-letter" data-letter="'+x.id+'">'+lLabel+'</button>'
+      +(isFirm?'<button class="gi-letter gi-expand" data-expand="'+x.id+'">'+(x.expanded?'▾ People':'▸ People'+(x.people?(' ('+x.people.length+')'):''))+'</button>':'')
+      +'</div>';
+  }
+  function gPersonHtml(p, firm){
+    var key=firm.id+'|'+p.pid, sel=(GSEL===key)?' sel':'';
+    return '<div class="gperson'+sel+'"><span class="gi-ic sm">'+(window.ROLE_ICONS?ROLE_ICONS.person:'')+'</span>'
+      +'<span class="gp-nm">'+esc(cap(p.name))+'</span>'
+      +'<span class="gp-sub">'+esc([cap(p.city||''),p.license_type||''].filter(Boolean).join(' · '))+'</span>'
+      +'<span class="gstatus s'+(p.status||0)+'" data-pst="'+key+'">'+GSTL[p.status||0]+'</span>'
+      +'<a class="gp-a" href="'+liUrl(p.name+' '+(firm.name||''))+'" target="_blank" rel="noopener">in↗</a>'
+      +(p.license_no?'<a class="gp-a" href="'+dreUrl(p.license_no)+'" target="_blank" rel="noopener">DRE</a>':'')
+      +'<button class="gi-letter gp-let" data-pletter="'+key+'">'+(p.lv?'✎ Regen':'✎ Letter')+'</button></div>';
   }
   function grender(){
     var wrap=document.getElementById('grailList'); if(!wrap)return;
@@ -291,7 +323,14 @@
       var out='<div class="gcat">';
       if(head) out+=gItemHtml(head, contacts.length);
       else out+='<div class="gitem cat"><div class="gi-top"><span class="gi-name">'+esc(cn)+'</span><span class="gi-count">'+contacts.length+' contacts</span><a class="gi-li" href="'+liUrl(cn+' commercial real estate')+'" target="_blank" rel="noopener">in ↗</a></div></div>';
-      out+=contacts.map(function(x){ return gItemHtml(x,0); }).join('');
+      out+=contacts.map(function(x){
+        var h=gItemHtml(x,0);
+        if(x.expanded && x.people){
+          var meta=(x.website||x.phone)?('<div class="gp-firm">'+(x.phone?'📞 '+esc(x.phone)+'   ':'')+(x.website?'<a href="'+(/^https?:/i.test(x.website)?x.website:'https://'+x.website)+'" target="_blank" rel="noopener">🌐 site</a>':'')+'</div>'):'';
+          h+='<div class="gpeople">'+meta+x.people.map(function(p){ return gPersonHtml(p,x); }).join('')+'</div>';
+        }
+        return h;
+      }).join('');
       return out+'</div>';
     }).join('');
     wrap.innerHTML = l.length ? html : '<p class="grail-note" style="margin-top:14px">No targets yet. Click <b>Add all categories</b> to seed the CRE conference-attendee list, then <b>Pull registry contacts</b> for named brokers &amp; owners.</p>';
@@ -310,54 +349,114 @@
   document.getElementById('grailNewBtn').onclick=gAddCustom;
   document.getElementById('grailNew').addEventListener('keydown',function(e){ if(e.key==='Enter') gAddCustom(); });
   document.getElementById('grailList').addEventListener('click',function(e){
+    var ex=e.target.closest('[data-expand]'); if(ex){ gToggleFirm(ex.getAttribute('data-expand')); return; }
+    var pl=e.target.closest('[data-pletter]'); if(pl){ genPersonLetter(pl.getAttribute('data-pletter')); return; }
+    var pst=e.target.closest('[data-pst]'); if(pst){ advancePerson(pst.getAttribute('data-pst')); return; }
     var lt=e.target.closest('[data-letter]'); if(lt){ genLetter(lt.getAttribute('data-letter')); return; }
     var st=e.target.closest('[data-st]'); if(st){ var id=st.getAttribute('data-st'); var l=gload(); l.forEach(function(x){ if(x.id===id) x.status=((x.status||0)+1)%GSTL.length; }); gsave(l); grender(); return; }
     var del=e.target.closest('[data-del]'); if(del){ var did=del.getAttribute('data-del'); gsave(gload().filter(function(x){return x.id!==did;})); grender(); }
   });
 
-  // ── Pick a name (left) → generate a personalized outreach letter (fills compose, right) ──
+  // ── Firm → people drilldown (real brokers via /api/firm/:id) ──────────────
+  function dreUrl(lic){ return 'https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=' + encodeURIComponent(lic); }
+  function gToggleFirm(id){
+    var l=gload(), it=null; l.forEach(function(x){ if(x.id===id) it=x; }); if(!it)return;
+    if(it.people){ it.expanded=!it.expanded; gsave(l); grender(); return; }
+    gmsg('Loading roster…');
+    fetch('/api/firm/'+encodeURIComponent(String(it.rid).slice(1)),{credentials:'same-origin'})
+      .then(function(r){ return r.json(); })
+      .then(function(d){
+        var ppl=(d.brokers||[]).map(function(b,i){ return {pid:String(b.id||i),name:b.name,city:b.city,license_no:b.license_no,license_type:b.license_type,status:0}; });
+        var l2=gload(); l2.forEach(function(x){ if(x.id===id){ x.people=ppl; x.expanded=true; if(d.firm){ if(d.firm.website)x.website=d.firm.website; if(d.firm.phone)x.phone=d.firm.phone; } } });
+        gsave(l2); grender(); gmsg(ppl.length ? ('Loaded '+ppl.length+' people at '+it.name) : 'No roster returned.');
+      }).catch(function(){ gmsg('Roster unavailable.'); });
+  }
+  function advancePerson(key){
+    var parts=key.split('|'), fid=parts[0], pid=parts[1], l=gload();
+    l.forEach(function(x){ if(x.id===fid&&x.people){ x.people.forEach(function(p){ if(String(p.pid)===pid) p.status=((p.status||0)+1)%GSTL.length; }); } });
+    gsave(l); grender();
+  }
+
+  // ── Offer-aware outreach letters, grounded in RENTV's real service menu ────
   var GSEL=null;
-  var GOPENERS=[
-    'I’ve been following {a} and wanted to connect.',
-    'Your work around {a} caught my eye — I wanted to reach out directly.',
-    'I’m reaching out because of {a}, and thought it was worth connecting.'
-  ];
+  var RENTV_FACTS={
+    newsletter:{ name:'The Real Estate Insider', reach:'~50,000', cadence:'twice a week' },
+    sponsorPrice:'$2,500',
+    contact:{ name:'Steve Bloom', phone:'310-213-6409', email:'sbloom@rentv.com' },
+    events:[
+      { market:'Arizona',      when:'October 2026',  venue:'the Scottsdale Resort & Spa' },
+      { market:'San Diego',    when:'January 2027',  venue:'the Westin Carlsbad' },
+      { market:'Greater LA',   when:'March 2027',    venue:'the Hilton LA/Culver City' },
+      { market:'Inland Empire',when:'May 2027',      venue:'the Embassy Suites Ontario' }
+    ]
+  };
+  var OFFERS={
+    sponsor:    { label:'Conference sponsorship',  line:function(ev){ return 'sponsor our next State of the Market conference'+(ev?(' — '+ev.market+', '+ev.when+(ev.venue?(' at '+ev.venue):'')):'')+', at '+RENTV_FACTS.sponsorPrice+'/event (panels on office, industrial, multifamily, retail and finance)'; } },
+    insider:    { label:'The Real Estate Insider ad', line:function(){ return 'place a banner or tile in '+RENTV_FACTS.newsletter.name+', our newsletter reaching '+RENTV_FACTS.newsletter.reach+' CRE professionals '+RENTV_FACTS.newsletter.cadence+' ($500–$1,500/mo, or a fixed advertorial from $350/mo)'; } },
+    spotlight:  { label:'Property Spotlight eblast', line:function(){ return 'feature a property in a dedicated Property Spotlight eblast to our '+RENTV_FACTS.newsletter.reach+'-recipient list (from $250, or nine for $1,800)'; } },
+    marketplace:{ label:'Marketplace eblast',      line:function(){ return 'promote your firm in a dedicated Marketplace eblast to our '+RENTV_FACTS.newsletter.reach+' CRE recipients (from $300, or eight for $2,000)'; } },
+    cretalk:    { label:'CRE Talk sponsorship',    line:function(){ return 'sponsor an episode of CRE Talk with Steve Bloom and Arnie Garfinkel — up to four sponsors per show, each with a 30-second spot plus logo and link placement'; } }
+  };
+  var ROLE_OFFER={
+    'Owners / Investors':'spotlight','Developers':'spotlight','Brokers':'marketplace',
+    'Lenders & Capital Sources':'sponsor','Vendors / Service Providers':'sponsor',
+    'Legal':'insider','Construction / GCs':'sponsor','Title & Escrow':'cretalk','Other':'insider'
+  };
   var GROLE={
-    'Owners / Investors':{a:['your portfolio activity across the Western U.S.','how owners are positioning for the next cycle'],cta:'a complimentary seat at our next State of the Market conference'},
-    'Developers':{a:['your current development pipeline','how developers are navigating capital and entitlements right now'],cta:'a panel seat and complimentary registration at the next RENTV summit'},
-    'Brokers':{a:['the deals moving in your market','getting your listings and closings in front of our CRE audience'],cta:'a delegate invitation to the next State of the Market conference'},
-    'Lenders & Capital Sources':{a:['where capital is flowing in Western CRE','the debt-and-equity picture heading into next quarter'],cta:'a capital-markets panel seat or a sponsorship at our next summit'},
-    'Vendors / Service Providers':{a:['reaching CRE owners, developers and brokers directly','putting your firm in front of the decision-makers who attend'],cta:'an exhibitor or sponsor package at our next conference'},
-    'Legal':{a:['the legal questions shaping CRE deals right now','sharing your expertise with our audience'],cta:'a thought-leadership panel seat at the next RENTV summit'},
-    'Construction / GCs':{a:['the projects breaking ground across the West','showcasing your build pipeline to owners and developers'],cta:'a complimentary seat at our next State of the Market conference'},
-    'Title & Escrow':{a:['keeping deals closing cleanly in this market','partnering on our deal coverage'],cta:'a partner or sponsor conversation ahead of the next summit'},
-    'Other':{a:['your work in commercial real estate','connecting around Western-U.S. CRE'],cta:'an invitation to our next State of the Market conference'}
+    'Owners / Investors':['your portfolio activity across the Western U.S.','how owners are positioning for the next cycle'],
+    'Developers':['your current development pipeline','how developers are navigating capital and entitlements right now'],
+    'Brokers':['the deals moving in your market','getting your listings and closings in front of our CRE audience'],
+    'Lenders & Capital Sources':['where capital is flowing in Western CRE','the debt-and-equity picture heading into next quarter'],
+    'Vendors / Service Providers':['reaching CRE owners, developers and brokers directly','putting your firm in front of the decision-makers who attend'],
+    'Legal':['the legal questions shaping CRE deals right now','sharing your expertise with our audience'],
+    'Construction / GCs':['the projects breaking ground across the West','showcasing your build pipeline to owners and developers'],
+    'Title & Escrow':['keeping deals closing cleanly in this market','being the title and escrow partner our audience turns to'],
+    'Other':['your work in commercial real estate','connecting around Western-U.S. CRE']
   };
-  function firstName(n){ return String(n||'').trim().split(/\s+/)[0]; }
+  function firstName(n){
+    var parts=String(n||'').trim().split(/\s+/).filter(Boolean);
+    for(var i=0;i<parts.length;i++){ if(parts[i].replace(/[^A-Za-z]/g,'').length>1) return parts[i]; } // skip initials like "A"
+    return parts[0]||'there';
+  }
   function looksFirm(n){ return /\b(inc|llc|corp|co|company|group|realty|properties|partners|capital|associates|holdings|ventures|bank|trust|advisors|management)\b/i.test(n); }
+  function roleAngle(role,v){ var a=GROLE[role]||GROLE['Other']; return a[Math.max(0,(v||1)-1)%a.length]; }
+  function nextEvent(market){
+    if(market){ var m=RENTV_FACTS.events.filter(function(e){ var a=e.market.toLowerCase(),b=market.toLowerCase(); return a.indexOf(b)>=0||b.indexOf(a)>=0; })[0]; if(m) return m; }
+    return RENTV_FACTS.events[0];
+  }
   function letterFor(x,v){
-    var R=GROLE[x.cat]||GROLE['Other'], i=Math.max(0,(v||1)-1);
-    var a=R.a[i%R.a.length], opener=GOPENERS[i%GOPENERS.length].replace('{a}',a);
-    var isCat=x.kind==='category';
+    var role=x.cat||'Other';
+    var cycle=[ROLE_OFFER[role]||'insider','insider','sponsor'];
+    var offer=OFFERS[cycle[Math.max(0,(v||1)-1)%cycle.length]]||OFFERS.insider;
+    var ev=nextEvent(x.market);
+    var isCat=(x.kind==='category'||x.kind==='segment');
     var greet=isCat?'there':(looksFirm(x.name)?(cap(x.name)+' team'):cap(firstName(x.name)));
     var text='Hi '+greet+',\n\n'
-      +'I’m Steve with RENTV — since 1998 we’ve covered the deals that move Western U.S. commercial real estate: home of CRE Talk with Steve & Arnie, the State of the Market conferences, and The REview.\n\n'
-      +opener+'\n\n'
-      +'I’d love to offer you '+R.cta+', and to connect here on LinkedIn as we cover more of the Western-U.S. market.\n\n'
-      +'Best,\nSteve\nRENTV.com';
-    return { topic:'Outreach — '+(isCat?x.cat:x.name), text:text };
+      +'I’m '+RENTV_FACTS.contact.name+' with RENTV — now in our 25th year covering the deals that move Western U.S. commercial real estate, from Texas and Arizona to California and the Pacific NW. We’re home to CRE Talk, the State of the Market conferences, and The REview.\n\n'
+      +'Given '+roleAngle(role,v)+', I wanted to reach out directly.\n\n'
+      +'I’d love to '+offer.line(ev)+'.\n\n'
+      +'Worth a quick call? I’m at '+RENTV_FACTS.contact.phone+' or '+RENTV_FACTS.contact.email+'.\n\n'
+      +'Best,\n'+RENTV_FACTS.contact.name+'\nRENTV.com';
+    return { topic:'RENTV — '+offer.label+' · '+(isCat?role:x.name), text:text };
   }
-  function genLetter(id){
-    var l=gload(), it=null; l.forEach(function(x){ if(x.id===id) it=x; });
-    if(!it)return;
-    it.lv=(it.lv||0)+1; gsave(l);              // bump variant → regenerates a different angle each click
-    var out=letterFor(it,it.lv);
+  function fillCompose(out,who){
     document.getElementById('pf').value='LinkedIn';
     document.getElementById('topic').value=out.topic;
     document.getElementById('text').value=out.text;
-    GSEL=id; grender();
-    var t=document.getElementById('text'); t.scrollIntoView({behavior:'smooth',block:'center'});
-    document.getElementById('msg').textContent='Letter ready for '+it.name+' — edit & Save draft →';
+    document.getElementById('text').scrollIntoView({behavior:'smooth',block:'center'});
+    document.getElementById('msg').textContent='Letter ready for '+who+' — edit & Save draft →';
+  }
+  function genLetter(id){
+    var l=gload(), it=null; l.forEach(function(x){ if(x.id===id) it=x; }); if(!it)return;
+    it.lv=(it.lv||0)+1; gsave(l);
+    fillCompose(letterFor(it,it.lv), it.name); GSEL=id; grender();
+  }
+  function genPersonLetter(key){
+    var parts=key.split('|'), fid=parts[0], pid=parts[1];
+    var l=gload(), firm=null; l.forEach(function(x){ if(x.id===fid) firm=x; }); if(!firm||!firm.people)return;
+    var p=firm.people.filter(function(z){ return String(z.pid)===pid; })[0]; if(!p)return;
+    p.lv=(p.lv||0)+1; gsave(l);
+    fillCompose(letterFor({kind:'contact',cat:firm.cat,name:p.name,market:firm.market},p.lv), cap(p.name)); GSEL=key; grender();
   }
 
   gsetOpen(localStorage.getItem(GOPEN)==='1'); // collapsed by default
diff --git a/server.js b/server.js
index 5df594bd..f9e31467 100644
--- a/server.js
+++ b/server.js
@@ -346,6 +346,7 @@ async function usreProxy(upstream, req, res) {
 }
 app.get('/api/brokers', adminOnly, (req, res) => usreProxy('/api/brokers', req, res));
 app.get('/api/firms', adminOnly, (req, res) => usreProxy('/api/firms', req, res));
+app.get('/api/firm/:id', adminOnly, (req, res) => usreProxy('/api/firm/' + encodeURIComponent(req.params.id), req, res)); // firm detail + broker roster (drill firm→people)
 app.get('/api/sublease', adminOnly, (req, res) => usreProxy('/api/sublease', req, res));
 
 // ── Summit Leads (growth idea #1 + #3): turn the CA registry (382k brokers / 34k firms)

← aed80d09 pr-intelligence: Cody c4 — person.rejected audit logging, co  ·  back to Rentv 2026  ·  social console F1: Role×Market×Sector facet builder — compos bd17052e →