[object Object]

← 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

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 →