← back to Rentv Sheet Enrich Refine
rentv :9797: paint EXACT original sheet cell colors in the grid (restore lost color-coding)
ca6b2ac9390085a7e2a80190551e49bf8da2b99f · 2026-08-13 18:05:13 -0700 · Steve Abrams
snapshot.py now captures every non-white, non-enrichment-green cell's exact hex per
tab (colors map); live_server tab()/combined() pass it through; cellTD paints each
cell its real sheet background (bg-only on link/mail/phone, bg+auto-contrast text on
plain). No legend names needed, no sheet write — the human color-coding is visible again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M live_server.pyM public_live/index.htmlM snapshot.py
Diff
commit ca6b2ac9390085a7e2a80190551e49bf8da2b99f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 13 18:05:13 2026 -0700
rentv :9797: paint EXACT original sheet cell colors in the grid (restore lost color-coding)
snapshot.py now captures every non-white, non-enrichment-green cell's exact hex per
tab (colors map); live_server tab()/combined() pass it through; cellTD paints each
cell its real sheet background (bg-only on link/mail/phone, bg+auto-contrast text on
plain). No legend names needed, no sheet write — the human color-coding is visible again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
live_server.py | 54 +++++++++++++++++++++++++++++++++++++++++++-------
public_live/index.html | 20 ++++++++++++-------
snapshot.py | 14 ++++++++-----
3 files changed, 69 insertions(+), 19 deletions(-)
diff --git a/live_server.py b/live_server.py
index 7872d0e..e679192 100644
--- a/live_server.py
+++ b/live_server.py
@@ -83,7 +83,8 @@ def meta():
def tab(gid):
t = SNAP["_by_gid"][int(gid)]
return {"gid": t["gid"], "title": t["title"], "headers": t["headers"],
- "rows": t["rows"], "green": t["green"], "generated": SNAP["generated"]}
+ "rows": t["rows"], "green": t["green"], "colors": t.get("colors", {}),
+ "generated": SNAP["generated"]}
def green_map(gid):
"""Enriched (green) cells for a tab — precomputed in the local snapshot."""
@@ -102,10 +103,11 @@ def combined(gids):
if "Sheet Name" in order:
order.remove("Sheet Name"); order.insert(0, "Sheet Name")
colidx = {h: i for i, h in enumerate(order)}
- rows = []; green = []
+ rows = []; green = []; colors = {}
for t in tabs:
hmap = [colidx.get(h) for h in t["headers"]] # tab column -> unified column
gset = set(t.get("green", []))
+ cmap = t.get("colors", {})
base = len(rows)
for ri, r in enumerate(t["rows"]):
newrow = [""] * len(order)
@@ -115,12 +117,43 @@ def combined(gids):
newrow[ui] = val
rows.append(newrow)
for ci in range(len(t["headers"])):
+ ui = hmap[ci]
+ if ui is None:
+ continue
if f"{ri},{ci}" in gset:
- ui = hmap[ci]
- if ui is not None:
- green.append(f"{base+ri},{ui}")
+ green.append(f"{base+ri},{ui}")
+ hx = cmap.get(f"{ri},{ci}")
+ if hx:
+ colors[f"{base+ri},{ui}"] = hx
return {"gid": "combined", "title": f"{len(tabs)} sheets combined",
- "headers": order, "rows": rows, "green": green, "generated": SNAP["generated"]}
+ "headers": order, "rows": rows, "green": green, "colors": colors,
+ "generated": SNAP["generated"]}
+
+def resolve_combined(gids, crow, ucol):
+ """Reverse-map a combined-view (row, union-col) back to the SOURCE (gid, tab_row0, tab_col0).
+ Mirrors combined()'s row/col construction EXACTLY so a write lands on the right sheet cell.
+ Returns (gid, tab_row0, tab_col0); tab_col0 == -1 means that union column doesn't exist on
+ the row's source tab (not editable in combined view)."""
+ tabs = [SNAP["_by_gid"][int(g)] for g in gids if str(g).strip() and int(g) in SNAP["_by_gid"]]
+ order = []; seen = set()
+ for t in tabs:
+ for h in t["headers"]:
+ if h and h not in seen:
+ seen.add(h); order.append(h)
+ if "Sheet Name" in order:
+ order.remove("Sheet Name"); order.insert(0, "Sheet Name")
+ header = order[ucol] if 0 <= ucol < len(order) else None
+ base = 0
+ for t in tabs:
+ n = len(t["rows"])
+ if crow < base + n:
+ try:
+ tcol = t["headers"].index(header) if header is not None else -1
+ except ValueError:
+ tcol = -1
+ return (t["gid"], crow - base, tcol)
+ base += n
+ return None
def write_cell(gid, row0, col0, value, formula=False):
# 1) LIVE write to the sheet (row0 is 0-based over DATA rows; +1 for the header)
@@ -233,7 +266,14 @@ class H(BaseHTTPRequestHandler):
n = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(n) or b"{}")
if self.path == "/api/write":
- self._json(write_cell(body["gid"], body["row0"], body["col0"],
+ gid, row0, col0 = body["gid"], body["row0"], body["col0"]
+ if gid == "combined": # reverse-map the combined (row, union-col) to the real source cell
+ res = resolve_combined(body.get("gids", []), int(row0), int(col0))
+ if not res or res[2] < 0:
+ self._json({"ok": False, "error": "That column isn't editable in the combined view for this row's sheet — open the single sheet to edit."})
+ return
+ gid, row0, col0 = res
+ self._json(write_cell(gid, row0, col0,
body.get("value", ""), body.get("formula", False)))
elif self.path == "/api/enhance":
self._json(enhance(body.get("prompt", "")))
diff --git a/public_live/index.html b/public_live/index.html
index e840887..b25fd2e 100644
--- a/public_live/index.html
+++ b/public_live/index.html
@@ -208,7 +208,7 @@
<script>
const $=s=>document.querySelector(s), $$=s=>document.querySelectorAll(s);
const LS={get:(k,d)=>{try{return JSON.parse(localStorage.getItem('rc2.'+k))??d}catch(e){return d}},set:(k,v)=>localStorage.setItem('rc2.'+k,JSON.stringify(v))};
-let DATA={gid:0,title:'',headers:[],rows:[],green:[]}, GREENSET=new Set(), ROLE={}, META=null;
+let DATA={gid:0,title:'',headers:[],rows:[],green:[]}, GREENSET=new Set(), COLORS={}, ROLE={}, META=null;
// color-coded Status field pulled from the spreadsheet (value->color) + the canonical value list
let STATUS_FIELD=null, STATUS_COLORS={}, STATUS_VALUES=[];
const STATUS_DEFAULTS={'Active / Contacted':'#cfe2f3','Needs Email / Outreach Pending':'#fff2a8','Prospect / Follow-up':'#d9ead3','In Progress / Left Message':'#fce5cd','On Hold / Inactive':'#e6e6e6','Flag / Priority':'#f4cccc','Section / Category':'#c9daf8'};
@@ -258,7 +258,7 @@ async function loadData(){
if(gids.length<=1){ GID=gids[0]??GID; LS.set('gid',GID); d=await (await fetch('/api/tab?gid='+GID)).json(); }
else { d=await (await fetch('/api/combined?gids='+gids.join(','))).json(); }
LS.set('sheets',[...SELECTED]);
- DATA=d; GREENSET=new Set(d.green||[]); detectRoles();
+ DATA=d; GREENSET=new Set(d.green||[]); COLORS=d.colors||{}; detectRoles();
FILTERS={}; SEARCH=''; $('#search')&&($('#search').value='');
const K=curKey();
HIDDEN=new Set(LS.get('hidden.'+K,[]));
@@ -559,6 +559,8 @@ function closeStatusPickerOutside(e){ if(!e.target.closest('#statusmenu')&&!e.ta
function cellTD(ri,ci,val,isN,gr){
const cls=[isN?'namecell':'',gr?'enriched':''].filter(Boolean).join(' ');
const s=String(val==null?'':val);
+ const xc=COLORS[ri+','+ci]; // EXACT original sheet cell color, if any -> paint it back in
+ const xbg=xc?` style="background:${xc}"`:'', xsty=xc?` style="background:${xc};color:${txtOn(xc)}"`:'';
if(isStatusCol(ci)){ // color-coded status chip + date/time under it + click-to-set picker
const lm=lmCol(), dt=(lm>=0 && DATA.rows[ri] && DATA.rows[ri][lm])||'';
const dtHtml=dt?`<div class="chipdate" title="Last modified">🕓 ${esc(String(dt))}</div>`:'';
@@ -567,13 +569,13 @@ function cellTD(ri,ci,val,isN,gr){
return `<td data-r="${ri}" data-c="${ci}" contenteditable="false" class="statuscell ${cls}" title="Click to change status"><span class="statuschip" style="background:${col};color:${txtOn(col)}">${esc(s)}</span>${dtHtml}</td>`;
}
const em=emailOf(s);
- if(em){ return `<td data-r="${ri}" data-c="${ci}" contenteditable="false" data-mail="${esc(em)}" title="Email ${esc(em)}" class="mailcell ${cls}"><span class="cicon">${ICON.mail}</span><span class="linkval">${esc(em)}</span></td>`; }
+ if(em){ return `<td data-r="${ri}" data-c="${ci}" contenteditable="false" data-mail="${esc(em)}" title="Email ${esc(em)}" class="mailcell ${cls}"${xbg}><span class="cicon">${ICON.mail}</span><span class="linkval">${esc(em)}</span></td>`; }
const u=hrefOf(s);
if(u){ const li=/linkedin\.com/i.test(u), disp=s.replace(/^https?:\/\/(www\.)?/i,'').replace(/\/+$/,'');
- return `<td data-r="${ri}" data-c="${ci}" contenteditable="false" data-href="${esc(u)}" title="Open ${esc(u)}" class="linkcell ${li?'li':'web'} ${cls}"><span class="cicon">${li?ICON.li:ICON.web}</span><span class="linkval">${esc(disp)}</span></td>`; }
+ return `<td data-r="${ri}" data-c="${ci}" contenteditable="false" data-href="${esc(u)}" title="Open ${esc(u)}" class="linkcell ${li?'li':'web'} ${cls}"${xbg}><span class="cicon">${li?ICON.li:ICON.web}</span><span class="linkval">${esc(disp)}</span></td>`; }
const d=telDigits(s);
- if(d){ return `<td data-r="${ri}" data-c="${ci}" contenteditable="false" data-tel="+${d}" title="Call ${esc(s)}" class="callcell ${cls}"><span class="cicon">${ICON.phone}</span><span class="linkval">${esc(s)}</span></td>`; }
- return `<td data-r="${ri}" data-c="${ci}" contenteditable="true" title="${esc(s)}" class="${cls}">${esc(s)}</td>`;
+ if(d){ return `<td data-r="${ri}" data-c="${ci}" contenteditable="false" data-tel="+${d}" title="Call ${esc(s)}" class="callcell ${cls}"${xbg}><span class="cicon">${ICON.phone}</span><span class="linkval">${esc(s)}</span></td>`; }
+ return `<td data-r="${ri}" data-c="${ci}" contenteditable="true" title="${esc(s)}" class="${cls}"${xsty}>${esc(s)}</td>`;
}
function cellRender(v){v=String(v==null?'':v);const u=hrefOf(v);
if(u){ const li=/linkedin\.com/i.test(u); // value stays plain text; icon opens the link
@@ -670,7 +672,11 @@ function wireGrid(){
}));
$$('td[contenteditable="true"]').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()}})});
}
-function postWrite(r,c,value){return fetch('/api/write',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({gid:GID,row0:r,col0:c,value})}).then(x=>x.json())}
+function postWrite(r,c,value){
+ const body={gid:DATA.gid,row0:r,col0:c,value}; // DATA.gid is 'combined' in multi-sheet view
+ if(DATA.gid==='combined') body.gids=[...SELECTED]; // let the server reverse-map to the source cell
+ return fetch('/api/write',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(x=>x.json());
+}
async function saveCell(td){ editing=false; const v=td.textContent.trim(); if(v===String(td._o||'').trim())return;
const r=+td.dataset.r,c=+td.dataset.c;
try{const res=await postWrite(r,c,v);
diff --git a/snapshot.py b/snapshot.py
index d6675e1..10a38c6 100644
--- a/snapshot.py
+++ b/snapshot.py
@@ -76,7 +76,7 @@ def build(tok=None):
for (gid, title), vr in zip(sheets, value_ranges):
rows = vr.get("values", [])
if not rows:
- tabs.append({"gid": gid, "title": title, "headers": [], "rows": [], "green": []})
+ tabs.append({"gid": gid, "title": title, "headers": [], "rows": [], "green": [], "colors": {}})
continue
hr = header_row(rows)
width = max((len(r) for r in rows), default=0)
@@ -85,17 +85,21 @@ def build(tok=None):
sidx = find_status(headers) # color-coded status column on this tab
if sidx is not None and status_field is None:
status_field = headers[sidx]
- green = []
+ green = []; colors = {}
for ri, row in enumerate(green_rows.get(gid, [])):
if ri <= hr: # header + any title rows above it
continue
+ di = ri - hr - 1
for ci, cell in enumerate(row.get("values", []) or []):
bg = (cell.get("effectiveFormat") or {}).get("backgroundColor")
if is_green(bg):
- green.append(f"{ri-hr-1},{ci}")
+ green.append(f"{di},{ci}")
+ else:
+ hx0 = hexc(bg) # EXACT human cell color (non-white, non-enrichment) -> painted in the build
+ if hx0:
+ colors[f"{di},{ci}"] = hx0
if sidx is not None and ci == sidx: # learn the value->color coding from the sheet
hx = hexc(bg)
- di = ri - hr - 1
if hx and 0 <= di < len(body) and ci < len(body[di]):
val = str(body[di][ci]).strip()
if val:
@@ -106,7 +110,7 @@ def build(tok=None):
v = str(r[sidx]).strip() if sidx < len(r) else ""
if v:
svalue_tally[v] = svalue_tally.get(v, 0) + 1
- tabs.append({"gid": gid, "title": title, "headers": headers, "rows": body, "green": green})
+ tabs.append({"gid": gid, "title": title, "headers": headers, "rows": body, "green": green, "colors": colors})
status_colors = {v: max(cnts, key=cnts.get) for v, cnts in scolor_tally.items()}
status_values = [v for v, _ in sorted(svalue_tally.items(), key=lambda kv: -kv[1])]
return {"generated": int(time.time()), "tabs": tabs,
← 95c5ba3 Live console: show Last Modified date+time directly under th
·
back to Rentv Sheet Enrich Refine
·
FIX critical data-corruption: combined/multi-sheet view writ b159bca →