[object Object]

← back to Rentv Sheet Enrich

All LinkedIn + Website cells href to exact site: cellRender links any URL/domain (not just http-prefixed); snapshot reads FORMULA render to recover URLs from =HYPERLINK(...,'LinkedIn') label cells (533/533 MVP LinkedIn now linked). Columns: resize-drag on every column + double-click-to-autofit + hover-title full content

c6e12e859ffb9804b9f038b334fd8d0e44a5a817 · 2026-08-13 14:15:48 -0700 · Steve Abrams

Files touched

Diff

commit c6e12e859ffb9804b9f038b334fd8d0e44a5a817
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 13 14:15:48 2026 -0700

    All LinkedIn + Website cells href to exact site: cellRender links any URL/domain (not just http-prefixed); snapshot reads FORMULA render to recover URLs from =HYPERLINK(...,'LinkedIn') label cells (533/533 MVP LinkedIn now linked). Columns: resize-drag on every column + double-click-to-autofit + hover-title full content
---
 __pycache__/snapshot.cpython-314.pyc | Bin 7775 -> 8411 bytes
 public_live/index.html               |  22 ++++++++++++++++++----
 snapshot.py                          |  17 +++++++++++++----
 3 files changed, 31 insertions(+), 8 deletions(-)

diff --git a/__pycache__/snapshot.cpython-314.pyc b/__pycache__/snapshot.cpython-314.pyc
index d8d224f..8a27397 100644
Binary files a/__pycache__/snapshot.cpython-314.pyc and b/__pycache__/snapshot.cpython-314.pyc differ
diff --git a/public_live/index.html b/public_live/index.html
index 45de2e1..2f9e668 100644
--- a/public_live/index.html
+++ b/public_live/index.html
@@ -145,7 +145,12 @@ function pk(){return 'p'+GID}  // per-tab localStorage namespace
 function findCol(preds){for(const p of preds){const i=DATA.headers.findIndex(h=>p.test(h));if(i>=0)return i}return -1}
 function detectRoles(){ROLE={name:findCol([/^contact name$/i,/contact name/i,/\bname\b/i]),company:findCol([/company\/venue/i,/^company$/i,/venue/i,/firm/i]),position:findCol([/^position$/i,/title/i]),email:findCol([/^email$/i]),email2:findCol([/updated email/i]),phone:findCol([/^phone 1$/i,/^phone\(s\)$/i,/^phone$/i]),cphone:findCol([/company phone/i]),liC:findCol([/linkedin \(contact\)/i]),liCo:findCol([/linkedin \(company\)/i]),mvpN:findCol([/marketing\/vp name/i]),mvpP:findCol([/marketing\/vp position/i]),mvpL:findCol([/marketing\/vp linkedin/i])}}
 const g=(r,i)=> (i>=0&&i<r.length)?String(r[i]||'').trim():'';
-function hrefOf(v){const m=String(v).match(/https?:\/\/[^\s"')]+/i);return m?m[0]:(/linkedin\.com/i.test(v)?('https://'+v.replace(/^https?:\/\//,'')):'')}
+function hrefOf(v){ v=String(v||'').trim();
+  const m=v.match(/https?:\/\/[^\s"')]+/i); if(m) return m[0].replace(/[),.]+$/,'');
+  // bare domain or linkedin/site path (no spaces) -> add https:// so it's clickable
+  if(/^(www\.)?[a-z0-9-]+(\.[a-z0-9-]+)+(\/\S*)?$/i.test(v)) return 'https://'+v;
+  return '';
+}
 
 // ---- load (from OUR local copy) ---------------------------------------------
 async function loadMeta(){
@@ -269,8 +274,8 @@ function tableHTML(rows){
   for(const [r,ri] of rows){
     if(GROUP!==''){ const gv=g(r,+GROUP)||'(blank)'; if(gv!==lastG){ lastG=gv; h+=`<tr class="grouphdr"><td colspan="${cols.length+1}">${esc(gv)}</td></tr>`; } }
     h+=`<tr><td><button class="rowbtn" data-r="${ri}">✍</button></td>`;
-    for(const c of cols){ const gr=GREENSET.has(ri+','+c.i), isN=c.i===ROLE.name;
-      h+=`<td data-r="${ri}" data-c="${c.i}" contenteditable="true" class="${[isN?'namecell':'',gr?'enriched':''].filter(Boolean).join(' ')}">${cellRender(g(r,c.i))}</td>`; }
+    for(const c of cols){ const gr=GREENSET.has(ri+','+c.i), isN=c.i===ROLE.name, val=g(r,c.i);
+      h+=`<td data-r="${ri}" data-c="${c.i}" contenteditable="true" title="${esc(val)}" class="${[isN?'namecell':'',gr?'enriched':''].filter(Boolean).join(' ')}">${cellRender(val)}</td>`; }
     h+='</tr>';
   }
   return h+'</tbody></table>';
@@ -284,7 +289,7 @@ function cardsHTML(rows,mode){
   return h+'</div>';
 }
 function badge(ic,v){return `<span class="badge${v?' ok':''}">${ic}${v?' ✓':''}</span>`}
-function cellRender(v){const u=hrefOf(v);if(u&&/^https?:/i.test(v))return `<a href="${esc(u)}" target="_blank" rel="noopener">${esc(v.length>40?u.replace(/^https?:\/\/(www\.)?/,''):v)}</a>`;return esc(v)}
+function cellRender(v){v=String(v==null?'':v);const u=hrefOf(v);if(u)return `<a href="${esc(u)}" target="_blank" rel="noopener">${esc(v.length>44?u.replace(/^https?:\/\/(www\.)?/,''):v)}</a>`;return esc(v)}
 function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m]))}
 function contact(r){const nm=g(r,ROLE.name),co=g(r,ROLE.company);return{name:nm,first:(nm.split(/[\s,]+/)[0]||'').trim(),company:co,position:g(r,ROLE.position),email:g(r,ROLE.email)||g(r,ROLE.email2),phone:g(r,ROLE.phone)||g(r,ROLE.cphone),liC:hrefOf(g(r,ROLE.liC)),liCo:hrefOf(g(r,ROLE.liCo)),mvpN:g(r,ROLE.mvpN),mvpP:g(r,ROLE.mvpP),mvpL:hrefOf(g(r,ROLE.mvpL))}}
 function wireGrid(){
@@ -301,6 +306,15 @@ function wireGrid(){
     const up=()=>{document.removeEventListener('mousemove',mv);document.removeEventListener('mouseup',up);LS.set('colw.'+GID,COLW);};
     document.addEventListener('mousemove',mv);document.addEventListener('mouseup',up);
   }));
+  // double-click a resize handle to auto-fit the column to its content
+  $$('#grid th .rz').forEach(rz=>rz.addEventListener('dblclick',e=>{
+    e.preventDefault();e.stopPropagation();
+    const ci=+rz.dataset.c, cols=visCols(), idx=cols.findIndex(c=>c.i===ci);
+    const col=document.querySelectorAll('#grid colgroup col')[idx+1];
+    let max=String(DATA.headers[ci]||'').length;
+    document.querySelectorAll('#grid tbody tr').forEach(tr=>{const t=tr.children[idx+1]?.textContent||'';if(t.length>max)max=t.length;});
+    const w=Math.min(600,Math.max(70,Math.round(max*7.1)+26));col.style.width=w+'px';COLW[ci]=w;LS.set('colw.'+GID,COLW);
+  }));
   $$('td[contenteditable]').forEach(td=>{td.addEventListener('focus',()=>{editing=true;td._o=td.textContent});td.addEventListener('blur',()=>saveCell(td));td.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();td.blur()}})});
 }
 async function saveCell(td){ editing=false; const v=td.textContent.trim(); if(v===String(td._o||'').trim())return;
diff --git a/snapshot.py b/snapshot.py
index 9ca09ad..09dcc86 100644
--- a/snapshot.py
+++ b/snapshot.py
@@ -18,10 +18,19 @@ def build(tok=None):
     tok = tok or lib.access_token()
     sheets = [(s["properties"]["sheetId"], s["properties"]["title"])
               for s in lib.get_meta(tok)["sheets"]]
-    # 1) all values in ONE call
+    # 1) all values in ONE call — FORMULA render so =HYPERLINK("url","label") keeps the URL
+    #    (FORMATTED would collapse those to the label "LinkedIn" and lose the link)
     qs = "&".join(rng(t) for _, t in sheets)
-    vals = api(f"{lib.API}/{lib.SID}/values:batchGet?{qs}&majorDimension=ROWS", tok)
+    vals = api(f"{lib.API}/{lib.SID}/values:batchGet?{qs}&majorDimension=ROWS&valueRenderOption=FORMULA", tok)
     value_ranges = vals.get("valueRanges", [])
+    import re as _re
+    _hl = _re.compile(r'=HYPERLINK\("([^"]+)"', _re.I)
+    def unlink(v):
+        if isinstance(v, str) and v.startswith("="):
+            m = _hl.match(v)
+            if m:
+                return m.group(1)      # recover the real URL from the hyperlink formula
+        return v
     # 2) all green (enriched) cell backgrounds in ONE call
     G = lib.GREEN
     def is_green(bg):
@@ -48,8 +57,8 @@ def build(tok=None):
             continue
         hr = header_row(rows)
         width = max((len(r) for r in rows), default=0)
-        headers = [(rows[hr][i].strip() if i < len(rows[hr]) else "") for i in range(width)]
-        body = [[(r[i] if i < len(r) else "") for i in range(width)] for r in rows[hr + 1:]]
+        headers = [(str(rows[hr][i]).strip() if i < len(rows[hr]) else "") for i in range(width)]
+        body = [[unlink(r[i]) if i < len(r) else "" for i in range(width)] for r in rows[hr + 1:]]
         green = []
         for ri, row in enumerate(green_rows.get(gid, [])):
             if ri <= hr:  # header + any title rows above it

← 6f48a2b Console: resizable columns (drag edge, persists per tab via  ·  back to Rentv Sheet Enrich  ·  Console layout: FREEZE header row (border-collapse:separate 43cf368 →