← back to Rentv 2026
pr-intelligence: cycle-4 extractor upgrades — single-line Name,Title parsing; common bank-path probes + two-hop about crawl in headless tool
47c8ac1404626f8567796570775904aa622cc6d3 · 2026-07-30 20:15:57 -0700 · Steve Abrams
Files touched
M public/social.htmlM src/pr/adapters/website.jsM src/pr/tools/headless-team-extract.js
Diff
commit 47c8ac1404626f8567796570775904aa622cc6d3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 30 20:15:57 2026 -0700
pr-intelligence: cycle-4 extractor upgrades — single-line Name,Title parsing; common bank-path probes + two-hop about crawl in headless tool
---
public/social.html | 174 ++++++++++++++++++++++++++++++++++
src/pr/adapters/website.js | 10 ++
src/pr/tools/headless-team-extract.js | 37 +++++++-
3 files changed, 216 insertions(+), 5 deletions(-)
diff --git a/public/social.html b/public/social.html
index 3ab10dcf..6346d4b3 100644
--- a/public/social.html
+++ b/public/social.html
@@ -43,6 +43,49 @@
.badge { font-size:10.5px; background:#fff3cd; color:#8a6d1a; padding:2px 7px; border-radius:99px; white-space:nowrap; }
.times span { display:inline-block; background:#eef2f7; border-radius:99px; padding:4px 10px; margin:3px 4px 0 0; font-size:12.5px; }
.plt { font-weight:600; font-size:12.5px; margin-top:10px; }
+ /* ── Growth · LinkedIn target rail (CRE conference attendees) ── */
+ .shell { display:flex; align-items:flex-start; }
+ main { flex:1 1 auto; min-width:0; }
+ .grail { position:sticky; top:0; flex:0 0 auto; width:44px; background:#fff; border-right:1px solid var(--line); transition:width .18s ease; z-index:6; }
+ .grail.open { width:340px; box-shadow:2px 0 14px rgba(13,27,42,.05); }
+ .grail-toggle { display:flex; align-items:center; justify-content:center; width:44px; height:230px; margin-top:16px; border:0; border-right:2px solid var(--li); background:#eef5fc; color:var(--li); cursor:pointer; border-radius:0 10px 10px 0; }
+ .grail.open .grail-toggle { display:none; }
+ .grail-vert { writing-mode:vertical-rl; transform:rotate(180deg); font-weight:700; font-size:12.5px; letter-spacing:.06em; text-transform:uppercase; }
+ .grail-body { display:none; padding:16px 15px 24px; max-height:100vh; overflow:auto; }
+ .grail.open .grail-body { display:block; }
+ .grail-top { display:flex; align-items:center; gap:8px; margin-bottom:6px; }
+ .grail-top strong { font-size:14px; }
+ .grail-close { margin-left:auto; border:0; background:#eef2f7; color:var(--ink); width:26px; height:26px; border-radius:7px; cursor:pointer; font-size:16px; line-height:1; }
+ .grail-note { margin:0 0 12px; font-size:11.5px; color:var(--muted); line-height:1.45; }
+ .grail-actions { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:10px; }
+ .grail-actions button { padding:7px 10px; font-size:12px; }
+ .grail-actions button.ghost { background:#eef2f7; color:var(--ink); }
+ #grailFilter, #grailNew { width:100%; }
+ .grail-meta { display:flex; align-items:center; gap:8px; font-size:11.5px; color:var(--muted); margin:10px 0 4px; }
+ .grail-meta select { width:auto; flex:1; padding:5px 7px; font-size:12px; }
+ .gcat { margin-top:12px; }
+ .gitem { border:1px solid var(--line); border-radius:10px; padding:9px 11px; margin-top:7px; }
+ .gitem.cat { background:#f7fafd; border-color:#dbe7f4; }
+ .gitem .gi-top { display:flex; align-items:center; gap:8px; }
+ .gitem .gi-name { font-weight:600; font-size:13px; }
+ .gitem .gi-count { font-size:10.5px; color:var(--muted); }
+ .gitem .gi-sub { font-size:11.5px; color:var(--muted); margin-top:2px; }
+ .gitem a.gi-li { margin-left:auto; color:#fff; background:var(--li); border-radius:6px; padding:3px 8px; font-size:11px; font-weight:600; text-decoration:none; white-space:nowrap; }
+ .gitem .gi-x { border:0; background:none; color:#c0392b; cursor:pointer; font-size:12px; padding:2px; }
+ .gstatus { display:inline-block; margin-top:7px; font-size:10.5px; font-weight:600; letter-spacing:.04em; text-transform:uppercase; border-radius:99px; padding:3px 9px; cursor:pointer; user-select:none; }
+ .gstatus.s0 { background:#eef2f7; color:#5b6b7b; }
+ .gstatus.s1 { background:#fff3cd; color:#8a6d1a; }
+ .gstatus.s2 { background:#e6f0ff; color:#0a4bb3; }
+ .gstatus.s3 { background:#e8f7ec; color:#1a7f37; }
+ .gwhen { font-size:10.5px; color:#9aa7b4; margin-left:8px; }
+ .gitem.sel { border-color:var(--li); box-shadow:0 0 0 2px #cfe3f8 inset; }
+ .gi-letter { display:inline-block; margin:8px 0 0 8px; border:1px solid var(--li); background:#fff; color:var(--li); border-radius:7px; padding:3px 9px; font-size:11px; font-weight:600; cursor:pointer; }
+ .gi-letter:hover { background:var(--li); color:#fff; }
+ .grail-add { display:flex; gap:6px; margin-top:14px; }
+ .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; }
+ @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>
<body>
@@ -50,6 +93,31 @@
<h1>RENTV Social</h1><span class="tag">Admin</span>
<a class="back" href="/backend">← Backend</a>
</header>
+<div class="shell">
+<aside class="grail" id="grail">
+ <button class="grail-toggle" id="grailToggle" title="Growth · LinkedIn Targets"><span class="grail-vert">+ Growth · LinkedIn</span></button>
+ <div class="grail-body">
+ <div class="grail-top"><strong>Growth · LinkedIn Targets</strong><button class="grail-close" id="grailClose" title="Collapse">×</button></div>
+ <p class="grail-note">CRE conference attendees to grow on LinkedIn — owners & investors, developers, brokers, lenders & capital sources, vendors, legal, construction, title & escrow.</p>
+ <div class="grail-actions">
+ <button id="grailAddAll">+ Add all categories</button>
+ <button class="ghost" id="grailPull">Pull registry contacts</button>
+ <button class="ghost" id="grailClear">Clear</button>
+ </div>
+ <input id="grailFilter" placeholder="Filter targets…">
+ <div class="grail-meta"><span id="grailCount">0</span> targets ·
+ <select id="grailSort">
+ <option value="added">Newest added</option>
+ <option value="cat">Category order</option>
+ <option value="name">Name A–Z</option>
+ <option value="status">Status</option>
+ </select>
+ </div>
+ <div id="grailList"></div>
+ <div class="grail-add"><input id="grailNew" placeholder="Add a custom target…"><button id="grailNewBtn">Add</button></div>
+ <div id="grailMsg" class="grail-msgline"></div>
+ </div>
+</aside>
<main>
<div class="col">
<div class="panel">
@@ -86,6 +154,7 @@
</div>
</div>
</main>
+</div><!-- /shell -->
<script>
var PLAT = [
['linkedin','LinkedIn','in'], ['x','X (Twitter)','𝕏'], ['instagram','Instagram','◎'],
@@ -141,6 +210,111 @@
else msg.textContent = res.error||'Error';
});
};
+
+ // ── Growth · LinkedIn target list — CRE conference attendees, by role ──────
+ var GKEY='rentv.growthLI.v1', GOPEN='rentv.growthLI.open', GSORT='rentv.growthLI.sort';
+ var GSTL=['Target','Outreaching','Connected','Done'];
+ // [category, descriptor, LinkedIn people-search terms]
+ var GCATS=[
+ ['Owners / Investors','Property owners & CRE investors','commercial real estate owner OR investor'],
+ ['Developers','Real estate developers','commercial real estate developer'],
+ ['Brokers','CRE sales & leasing brokers','commercial real estate broker'],
+ ['Lenders & Capital Sources','Debt, equity & capital markets','commercial real estate lender OR "capital markets" OR debt OR equity'],
+ ['Vendors / Service Providers','CRE service & product vendors','commercial real estate services vendor'],
+ ['Legal','Real estate attorneys & counsel','commercial real estate attorney'],
+ ['Construction / GCs','General contractors & builders','commercial construction general contractor'],
+ ['Title & Escrow','Title, escrow & closing','commercial real estate title escrow officer']
+ ];
+ function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];}); }
+ function cap(s){ return String(s||'').toLowerCase().replace(/\b\w/g,function(m){return m.toUpperCase();}); }
+ function liUrl(t){ return 'https://www.linkedin.com/search/results/people/?keywords='+encodeURIComponent(t); }
+ function gid(){ return 'g'+Date.now().toString(36)+Math.floor(Math.random()*1e4).toString(36); }
+ function gload(){ try { return JSON.parse(localStorage.getItem(GKEY))||[]; } catch(e){ return []; } }
+ function gsave(l){ localStorage.setItem(GKEY, JSON.stringify(l)); }
+ function gmsg(m){ var el=document.getElementById('grailMsg'); el.textContent=m; if(m) setTimeout(function(){ if(el.textContent===m) el.textContent=''; },3000); }
+
+ function gAddAllCats(){
+ var l=gload(), have={}; l.forEach(function(x){ if(x.kind==='category') have[x.name]=1; });
+ var added=0, now=Date.now();
+ GCATS.forEach(function(c){ if(!have[c[0]]){ l.push({id:gid(),kind:'category',cat:c[0],name:c[0],sub:c[1],terms:c[2],status:0,added:now}); added++; } });
+ gsave(l); grender(); gmsg(added ? ('Added '+added+' categories ✓') : 'All categories already present.');
+ }
+ function gPull(){
+ gmsg('Pulling registry…');
+ Promise.all([
+ fetch('/api/summit/leads?type=firms&limit=200',{credentials:'same-origin'}).then(function(r){return r.json();}).catch(function(){return {rows:[]};}),
+ fetch('/api/summit/leads?type=brokers&limit=200',{credentials:'same-origin'}).then(function(r){return r.json();}).catch(function(){return {rows:[]};})
+ ]).then(function(res){
+ var l=gload(), seen={}; l.forEach(function(x){ if(x.rid) seen[x.rid]=1; });
+ var now=Date.now(), added=0;
+ (res[0].rows||[]).forEach(function(r){ var rid='f'+r.id; if(seen[rid])return; seen[rid]=1;
+ l.push({id:gid(),rid:rid,kind:'contact',cat:'Owners / Investors',name:cap(r.name),sub:(r.hq_city?cap(r.hq_city):'')+(r.agent_count?(' · '+r.agent_count+' agents'):''),terms:r.name+' commercial real estate',status:0,added:now}); added++; });
+ (res[1].rows||[]).forEach(function(r){ var rid='b'+r.id; if(seen[rid])return; seen[rid]=1;
+ l.push({id:gid(),rid:rid,kind:'contact',cat:'Brokers',name:cap(r.name),sub:cap(r.firm_name||'')+(r.city?(' · '+cap(r.city)):''),terms:r.name+' '+(r.firm_name||'')+' commercial real estate',status:0,added:now}); added++; });
+ gsave(l); grender(); gmsg(added ? ('Added '+added+' registry contacts ✓') : 'No new registry contacts.');
+ });
+ }
+ function gAddCustom(){
+ var el=document.getElementById('grailNew'), v=el.value.trim(); if(!v)return;
+ 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 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 sel=(x.id===GSEL)?' sel':'';
+ var lLabel=(x.lv?'✎ Regenerate':'✎ Generate 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>':'')
+ +'<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>';
+ }
+ function grender(){
+ var wrap=document.getElementById('grailList'); if(!wrap)return;
+ var l=gload(), f=(document.getElementById('grailFilter').value||'').toLowerCase();
+ var sort=document.getElementById('grailSort').value||'added';
+ document.getElementById('grailCount').textContent=l.length;
+ var items=l.filter(function(x){ return !f || (x.name+' '+(x.sub||'')+' '+x.cat).toLowerCase().indexOf(f)>=0; });
+ var order={}; GCATS.forEach(function(c,i){ order[c[0]]=i; });
+ var cats={}, extras=[];
+ items.forEach(function(x){ var c=x.cat||'Other'; if(!cats[c]){ cats[c]=[]; if(!(c in order)) extras.push(c); } cats[c].push(x); });
+ var names=Object.keys(cats).sort(function(a,b){ var oa=(a in order)?order[a]:100+extras.indexOf(a), ob=(b in order)?order[b]:100+extras.indexOf(b); return oa-ob; });
+ var html=names.map(function(cn){
+ var arr=cats[cn], head=arr.filter(function(x){return x.kind==='category';})[0];
+ var contacts=arr.filter(function(x){return x.kind==='contact';});
+ contacts.sort(function(a,b){ if(sort==='name')return a.name.localeCompare(b.name); if(sort==='status')return (b.status||0)-(a.status||0); return b.added-a.added; });
+ 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('');
+ 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 & owners.</p>';
+ }
+
+ var grail=document.getElementById('grail');
+ function gsetOpen(o){ grail.classList.toggle('open',o); localStorage.setItem(GOPEN,o?'1':'0'); }
+ document.getElementById('grailToggle').onclick=function(){ gsetOpen(true); };
+ document.getElementById('grailClose').onclick=function(){ gsetOpen(false); };
+ document.getElementById('grailAddAll').onclick=gAddAllCats;
+ document.getElementById('grailPull').onclick=gPull;
+ document.getElementById('grailClear').onclick=function(){ if(confirm('Clear the growth LinkedIn list?')){ gsave([]); grender(); gmsg('Cleared.'); } };
+ document.getElementById('grailFilter').oninput=grender;
+ var gsortEl=document.getElementById('grailSort'); gsortEl.value=localStorage.getItem(GSORT)||'added';
+ gsortEl.onchange=function(){ localStorage.setItem(GSORT,gsortEl.value); grender(); };
+ 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 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(); }
+ });
+ gsetOpen(localStorage.getItem(GOPEN)==='1'); // collapsed by default
+ grender();
</script>
</body>
</html>
diff --git a/src/pr/adapters/website.js b/src/pr/adapters/website.js
index 2ec2d692..8c860ddb 100644
--- a/src/pr/adapters/website.js
+++ b/src/pr/adapters/website.js
@@ -48,7 +48,17 @@ function extractPeople(html) {
// word from this list disqualifies the candidate. Real ambiguity still lands in
// needs_verification for human review; this just cuts the obvious junk.
const NOT_NAME = /\b(job|jobs|career|careers|communication|communications|media|crisis|digital|social|marketing|advisor|advisors|strategy|strategies|services|solutions|relations|estate|group|team|about|contact|office|offices|leadership|management|partners|clients|projects|news|insights|events|home|search|menu|privacy|policy|our|the|and)\b/i;
+ // single-line variant: "Jane Smith, Senior Vice President" / "Jane Smith – CFO" /
+ // "Jane Smith | Chief Credit Officer" — common on small-bank officer pages (cycle 4:
+ // 36 team pages yielded 0 because name+title share one line).
+ const INLINE_RE = /^([A-Z][a-z]+(?:\s+[A-Z]\.?)?(?:\s+[A-Z][a-z'’-]+){1,2})\s*[,|–—-]\s+(.{3,90})$/;
for (let i = 0; i < lines.length; i++) {
+ const inline = lines[i].match(INLINE_RE);
+ if (inline && !NOT_NAME.test(inline[1]) && TITLE_HINT.test(inline[2])) {
+ people.push({ full_name: inline[1], exact_title: inline[2].trim() });
+ if (people.length >= 60) break;
+ continue;
+ }
const nm = lines[i].match(NAME_RE);
if (!nm || NOT_NAME.test(nm[1])) continue;
// look ahead up to 2 lines for a title
diff --git a/src/pr/tools/headless-team-extract.js b/src/pr/tools/headless-team-extract.js
index 49847a8c..9e2bffbc 100644
--- a/src/pr/tools/headless-team-extract.js
+++ b/src/pr/tools/headless-team-extract.js
@@ -59,13 +59,40 @@ async function main() {
const rank = (u) => /leadership|executive|management|officers|our-team|meet-the-team/i.test(u) ? 0
: /about/i.test(u) ? 1 : /professionals|people-search|find|directory|agents/i.test(u) ? 3 : 2;
candidates.sort((a, b) => rank(a) - rank(b));
+ // Cycle 4: 59 sites had NO team link in the rendered home nav (bank submenus /
+ // unlinked pages). Fall back to probing the common paths small-bank sites use,
+ // and to a two-hop crawl through the About page.
+ if (!candidates.length || rank(candidates[0]) >= 2) {
+ const origin = new URL(page.url()).origin;
+ for (const p of ['/about-us/management', '/about-us/leadership', '/about/leadership',
+ '/about-us/our-team', '/our-team', '/leadership', '/management', '/officers',
+ '/about-us/officers', '/team', '/meet-the-team', '/about-us/board-of-directors']) {
+ candidates.push(origin + p);
+ }
+ const aboutLink = candidates.find((u) => /about/i.test(u));
+ if (aboutLink && (await robotsAllows(aboutLink))) {
+ try { // two-hop: the About page's own nav usually links the team page
+ await page.goto(aboutLink, { waitUntil: 'domcontentloaded' });
+ await page.waitForTimeout(1000);
+ const hop = await page.evaluate((teamReSrc) => {
+ const re = new RegExp(teamReSrc, 'i');
+ const a = [...document.querySelectorAll('a[href]')].find((el) => re.test(el.getAttribute('href') || '') || re.test(el.textContent || ''));
+ return a ? a.href : null;
+ }, PAGE_HINTS.team.source);
+ if (hop) candidates.unshift(hop);
+ } catch { /* about page unreachable */ }
+ }
+ }
let people = [];
- for (const cand of candidates.slice(0, 3)) {
+ for (const cand of [...new Set(candidates)].slice(0, 6)) {
if (!(await robotsAllows(cand))) continue;
- await page.goto(cand, { waitUntil: 'domcontentloaded' });
- await page.waitForTimeout(2000); // rendered people grids are late paints
- people = extractPeople(await page.content());
- if (people.length) { teamUrl = cand; break; }
+ try {
+ const resp = await page.goto(cand, { waitUntil: 'domcontentloaded' });
+ if (resp && resp.status() >= 400) continue; // probed path doesn't exist
+ await page.waitForTimeout(2000); // rendered people grids are late paints
+ people = extractPeople(await page.content());
+ if (people.length) { teamUrl = cand; break; }
+ } catch { /* dead candidate — try next */ }
}
if (people.length) {
for (const cand of people.slice(0, 40)) {
← 6ffb2d7c yoloforever: cycle 3 ledger (SHIP 3/3 local)
·
back to Rentv 2026
·
social: collapsible left Growth·LinkedIn target rail — CRE c 835146a0 →