[object Object]

← back to Rentv Sheet Enrich Refine

Add 'Sheet Name' as first column on every tab, populated with the tab title on every record (non-destructive insert); phone cells tap-to-call (tel: links); composer phone option = full spoken phone pitch

eb71907b9b0a9b71fdfd29e5e7a28782be612152 · 2026-08-13 14:31:30 -0700 · Steve Abrams

Files touched

Diff

commit eb71907b9b0a9b71fdfd29e5e7a28782be612152
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 13 14:31:30 2026 -0700

    Add 'Sheet Name' as first column on every tab, populated with the tab title on every record (non-destructive insert); phone cells tap-to-call (tel: links); composer phone option = full spoken phone pitch
---
 add_sheetname.py       | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++
 public_live/index.html | 15 ++++++++++++--
 2 files changed, 66 insertions(+), 2 deletions(-)

diff --git a/add_sheetname.py b/add_sheetname.py
new file mode 100644
index 0000000..d8f4ce2
--- /dev/null
+++ b/add_sheetname.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+"""
+add_sheetname.py — insert a "Sheet Name" column as the FIRST column (A) on every tab and
+fill every data record with that tab's title, so each row carries its source sheet.
+Idempotent: skips a tab that already has a "Sheet Name" column. One insert + one values
+write per tab (quota-safe). Values written under the tab's real header row (title rows above
+the header stay blank).
+"""
+import lib, json, urllib.request, urllib.parse, urllib.error, time
+
+def req(method, url, tok, body=None):
+    r = urllib.request.Request(url, data=(None if body is None else json.dumps(body).encode()),
+        method=method, headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"})
+    try:
+        return json.load(urllib.request.urlopen(r))
+    except urllib.error.HTTPError as e:
+        raise RuntimeError(f"{e.code}: {e.read().decode()[:200]}")
+
+def header_row(rows):
+    best, bi = -1, 0
+    for i in range(min(4, len(rows))):
+        n = sum(1 for c in rows[i] if str(c).strip())
+        if n > best:
+            best, bi = n, i
+    return bi
+
+def main():
+    tok = lib.access_token()
+    for s in lib.get_meta(tok)["sheets"]:
+        gid = s["properties"]["sheetId"]; title = s["properties"]["title"]
+        rows = lib.read_tab(tok, title)
+        if not rows:
+            print(f"  {title[:34]:34s} -> empty, skipped"); continue
+        if any(str(c).strip().lower() == "sheet name" for r in rows[:4] for c in r):
+            print(f"  {title[:34]:34s} -> already has Sheet Name"); continue
+        hr = header_row(rows)
+        # 1) insert a new column at position 0
+        req("POST", f"{lib.API}/{lib.SID}:batchUpdate", tok, {"requests": [{"insertDimension": {
+            "range": {"sheetId": gid, "dimension": "COLUMNS", "startIndex": 0, "endIndex": 1},
+            "inheritFromBefore": False}}]})
+        # 2) fill column A: "Sheet Name" header at the header row, the title on every data row
+        n = len(rows)
+        col = [[""] for _ in range(n)]
+        col[hr] = ["Sheet Name"]
+        for i in range(hr + 1, n):
+            col[i] = [title]
+        rangeA1 = urllib.parse.quote(f"'{title}'!A1:A{n}", safe="")
+        req("PUT", f"{lib.API}/{lib.SID}/values/{rangeA1}?valueInputOption=RAW", tok, {"values": col})
+        print(f"  {title[:34]:34s} -> inserted Sheet Name (A), filled {n-hr-1} rows")
+        time.sleep(1.2)  # stay under the 60 writes/min quota
+
+if __name__ == "__main__":
+    main()
diff --git a/public_live/index.html b/public_live/index.html
index 3be16bd..d836c0e 100644
--- a/public_live/index.html
+++ b/public_live/index.html
@@ -305,7 +305,17 @@ function cardsHTML(rows,mode){
   return h+'</div>';
 }
 function badge(ic,v){return `<span class="badge${v?' ok':''}">${ic}${v?' ✓':''}</span>`}
-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 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>`;
+  if(/\d{3}[-.\s]?\d{3}[-.\s]?\d{4}/.test(v))return phoneLinkify(v);   // phone numbers -> tap-to-call
+  return esc(v)}
+function phoneLinkify(v){
+  const re=/(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g; let out='',last=0,m;
+  while((m=re.exec(v))){ out+=esc(v.slice(last,m.index));
+    let d=m[0].replace(/\D/g,''); if(d.length===10)d='1'+d;
+    out+=`<a href="tel:+${esc(d)}" title="Click to call">${esc(m[0])}</a>`; last=m.index+m[0].length; }
+  out+=esc(v.slice(last)); return out||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(){
@@ -387,7 +397,8 @@ function buildDraft(c,chan,purpose,tone){const first=(c.first&&/^[A-Za-z]/.test(
     return{subject:subj,body:`${hi}\n\n${val}\n\nWould you be open to a 15-minute call this week?\n\n${close}`,target:c.email||'(no email)'}}
   if(chan==='linkedin'){const note=`${hi.replace(/ —$/,'')} — I run partnerships at RENTV (CRE video news). ${purpose==='sponsorship'?"We're selecting sponsors for our CRE conference and "+co+' is a great fit.':purpose==='speaker'?"I'd love to invite "+co+' onto a panel at our CRE conference.':"I'd love to connect about ways RENTV and "+co+' could work together.'} Open to a quick chat?`;
     return{subject:'',body:note.slice(0,600),target:(c.liC||c.mvpL||c.liCo)||'(no LinkedIn)'}}
-  return{subject:'',target:c.phone||'(no phone)',body:`CALL SCRIPT — ${co}${c.name?' ('+c.name+')':''}\n\n• Opener: "Hi${c.name?' '+first:''}, this is Steve Bloom with RENTV, the commercial real estate news network."\n• Reason: ${val}\n• Ask: "Any 15 minutes this week to walk through it?"\n• Gatekeeper: ask for ${c.mvpN||'the marketing or partnerships lead'}.`}
+  return{subject:'',target:c.phone||'(no phone on file)',
+    body:`📞 PHONE PITCH — ${co}${c.name?' · '+c.name:''}\n\nOPEN\n"Hi${c.name?', '+first:''} — this is Steve Bloom, President of RENTV, the commercial real estate news network. Do you have a quick minute?"\n\nPITCH\n"${val} We reach roughly 50,000 CRE professionals — investors, developers, brokers and capital sources — twice a week, and I think ${co} would land really well with that audience."\n\nASK\n"I'd love to grab 15 minutes this week to walk you through a couple of options tailored to ${co}. What does later this week look like?"\n\nIF A GATEKEEPER ANSWERS\n"Who handles marketing or partnerships for ${co}?${c.mvpN?' I believe it may be '+c.mvpN+'.':''} Happy to send a quick email first${c.email?' — is '+c.email+' best?':'.'}"\n\nLEAVE-BEHIND\n"No problem if now isn't ideal — I'll follow up by email so you have it in writing."`}
 }
 async function doEnhance(){const b=$('#enhance'),o=b.textContent;b.textContent='✨ thinking…';b.disabled=true;
   try{const r=await (await fetch('/api/enhance',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt:`Rewrite this ${curChan} outreach so it sounds natural, concise, professional. Keep it truthful, sender "Steve Bloom, RENTV". Return ONLY the message.\n\n---\n${$('#d-body').value}`})})).json();

← 43cf368 Console layout: FREEZE header row (border-collapse:separate  ·  back to Rentv Sheet Enrich Refine  ·  Console: default column order pulls First Name + Last Name u 0f87d75 →