← back to Rentv Sheet Enrich
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
A add_sheetname.pyM public_live/index.html
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=>({'&':'&','<':'<','>':'>','"':'"'}[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
·
Console: default column order pulls First Name + Last Name u 0f87d75 →