← back to Dw Five Field Step0

drain.sh

65 lines

#!/bin/zsh
# Daily auto-drain of the 5-field auto-fixable worklist.
# Budget-aware (claims the day's leftover Shopify variant headroom via budget.cjs),
# idempotent + resumable (skips anything already in the result JSON), DELETE-nothing.
# Self-disables when the worklist is fully drained. Singleton-guarded.
set -u
export PATH="/opt/homebrew/opt/postgresql@14/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
SK="$HOME/Projects/dw-five-field-step0"
LOG="$SK/drain.log"
LOCKDIR="$SK/.drain.lock"
EXE="$SK/bulk-fivefield-exec.py"
RESULT="$SK/out/bulk-fivefield-result.json"
LABEL="com.steve.dw-fivefield-drain"
ts(){ date '+%Y-%m-%d %H:%M:%S'; }

# singleton (macOS has no flock) — atomic mkdir lock, stale after 2h
if ! mkdir "$LOCKDIR" 2>/dev/null; then
  if [ -d "$LOCKDIR" ] && [ "$(find "$LOCKDIR" -maxdepth 0 -mmin +120 2>/dev/null)" ]; then
    rmdir "$LOCKDIR" 2>/dev/null; mkdir "$LOCKDIR" 2>/dev/null || { echo "[$(ts)] locked, skip" >>"$LOG"; exit 0; }
  else
    echo "[$(ts)] already running, skip" >>"$LOG"; exit 0
  fi
fi
trap 'rmdir "$LOCKDIR" 2>/dev/null' EXIT

# executable total (worklist minus the held-no-cost reprice-zeros, minus
# internal-only Schumacher — matches the executor's load_worklist() exclusion so
# done/total stays consistent; Schumacher stays archived and off the storefront)
TOTAL=$(psql -d dw_unified -tAc "select count(*) from bulk_fivefield_worklist where status='pending' and fix_type <> 'reprice-zero' and lower(coalesce(vendor,'')) <> 'schumacher'" 2>/dev/null || echo 0)
DONE=$(node -e 'try{const r=require(process.argv[1]);console.log(r.filter(x=>x.status==="fixed"||x.status==="skipped").length)}catch(e){console.log(0)}' "$RESULT" 2>/dev/null || echo 0)
echo "[$(ts)] drain start — done=$DONE / total=$TOTAL" >>"$LOG"

if [ "$TOTAL" -gt 0 ] && [ "$DONE" -ge "$TOTAL" ]; then
  echo "[$(ts)] DRAIN COMPLETE ($DONE/$TOTAL) — self-disabling launchd job" >>"$LOG"
  launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null
  exit 0
fi

# run one SMALL per-slot batch so the day's 500 'backlog' budget spreads evenly
# across the 24 hourly slots (~21/slot) instead of bursting at once. The shared
# budget.cjs 'backlog' category still hard-caps the daily total at 500.
SLOT_MAX="${DW_BACKLOG_SLOT_MAX:-30}"

# END-OF-DAY RECLAIM (DTD 2/2 verdict C, 2026-07-02): from 21:00 budget.cjs lets
# 'backlog' sweep the day's unspent global remainder (~60/day historically) that
# would otherwise expire at midnight. Ask for more in the 21/22/23 slots so the
# sweep isn't capped by the request size; the ledger still bounds every grant.
if [ "$(date +%H)" -ge 21 ]; then
  SLOT_MAX="${DW_BACKLOG_RECLAIM_SLOT_MAX:-150}"
fi

# display_variant legacy tag backfill (Option A, DTD 3/3 2026-06-24).
# GATED OFF by default: the executor only runs the tag pass when env
# DISPLAY_VARIANT_BACKFILL=1 is present. tagsAdd is NOT a variant create -> it
# does NOT debit the variant budget. Own per-slot cap (DISPLAY_VARIANT_SLOT_MAX,
# default 30) + same inter-call sleep; runs AFTER the variant drain each slot.
# To GO LIVE: add DISPLAY_VARIANT_BACKFILL=1 to this job's environment (the
# launchd plist EnvironmentVariables block) and reload the plist. Until then the
# scheduled run logs "backfill GATED OFF" and changes no tags.
echo "[$(ts)] display_variant backfill = ${DISPLAY_VARIANT_BACKFILL:-0} (slot_max=${DISPLAY_VARIANT_SLOT_MAX:-30})" >>"$LOG"
python3 "$EXE" --max "$SLOT_MAX" >>"$LOG" 2>&1
RC=$?
DONE2=$(node -e 'try{const r=require(process.argv[1]);console.log(r.filter(x=>x.status==="fixed"||x.status==="skipped").length)}catch(e){console.log(0)}' "$RESULT" 2>/dev/null || echo 0)
echo "[$(ts)] drain end rc=$RC — now done=$DONE2 / total=$TOTAL (this run +$((DONE2-DONE)))" >>"$LOG"