← back to Wallco Ai

scripts/backfill-rooms-cron.sh

67 lines

#!/usr/bin/env bash
# backfill-rooms-cron.sh — recurring room-mockup backfill for prod.
# Lock-file gated so a long-running render can't be double-fired.
# Steve authorized recurring auto-backfill 2026-05-28 via AskUserQuestion.
set -uo pipefail
LOCK=/tmp/backfill-rooms.lock
LOG=/tmp/backfill-rooms-cron.log
cd /root/public-projects/wallco-ai || exit 1
exec 200>$LOCK
flock -n 200 || { echo "$(date -Iseconds) skip: prior run still active" >> $LOG; exit 0; }
echo "$(date -Iseconds) ==== start ====" >> $LOG
# Idempotent: /api/room refuses to re-render an already-rendered design.
# Cap each run at --limit 50 to keep spend bounded ($5/run).
CONCURRENCY=2 node scripts/backfill-rooms.js --limit 50 >> $LOG 2>&1
# Patch designs.json with any IDs that got a fresh PNG in the last 2h.
python3 <<'PY' >> $LOG 2>&1
import json, os, re, glob, time, shutil
DJ='data/designs.json'
cutoff=time.time()-7200
have=set()
for p in glob.glob('data/rooms/design_*_living_room.png'):
  if p.endswith('_hero.png'): continue
  if os.path.getmtime(p)>=cutoff:
    m=re.search(r'design_(\d+)_living_room\.png',p)
    if m: have.add(int(m.group(1)))
d=json.load(open(DJ))
t=0
for x in d:
  if x.get('id') in have:
    rm=x.get('room_mockups') or []
    if 'living_room' not in rm:
      rm.append('living_room'); x['room_mockups']=rm; t+=1
if t:
  # Catastrophe guard: never overwrite the live catalog with a suspiciously small
  # set (mirrors refresh_designs_snapshot.py's >50%-shrink refuse, which this inline
  # writer was bypassing — that's why the guard "didn't fire" on the 06:30 blackout).
  if len(d) < 1000:
    print(f'ABORT: refusing to write designs.json with only {len(d)} rows (catastrophe guard)')
  else:
    # Serialize with indent=2 to match the canonical pretty-printed format produced
    # by refresh_designs_snapshot.py. Compact json.dump(d,f,separators=(',',':')) was
    # writing a ~20% smaller file on every run (e.g. 2026-05-30 06:30 clobber dropped
    # designs.json from 25.2MB → 20.3MB while preserving row count, blackout ~4min).
    new_blob = json.dumps(d, indent=2)
    # Byte-size guard (mirrors refresh_designs_snapshot.py lines 209-216). The COUNT
    # guard above misses field-shrink clobbers. Refuse a materially smaller FILE
    # unless override flag WALLCO_FORCE_BACKFILL=1 is set.
    if os.path.exists(DJ) and os.environ.get('WALLCO_FORCE_BACKFILL') != '1':
      old_bytes = os.path.getsize(DJ)
      if old_bytes > 1000000 and len(new_blob) < old_bytes * 0.95:
        print(f'ABORT: new designs.json {len(new_blob)}B < 95% of existing {old_bytes}B (field-shrink clobber guard). Keeping existing. Override WALLCO_FORCE_BACKFILL=1.')
        raise SystemExit(0)
    shutil.copy(DJ, DJ+'.bak.'+str(int(time.time())))
    # ATOMIC write: stream to a temp file then os.replace() (atomic rename) so the
    # server NEVER reads a half-written/0-byte file. open(DJ,'w') truncated the live
    # 25MB file to zero and took seconds to refill — any read in that window saw 0
    # designs → the recurring ~4-min catalog blackout. (fix 2026-05-30)
    tmp=DJ+'.tmp'
    with open(tmp,'w') as f: f.write(new_blob)
    os.replace(tmp, DJ)
    print(f'patched {t} rows in designs.json (atomic, pretty)')
else:
  print('no new room_mockups to patch')
PY
curl -s -X POST -u admin:DWSecure2024! http://127.0.0.1:9905/admin/reload-designs >> $LOG 2>&1
echo "" >> $LOG