← back to Trending Dw
scripts/tick.sh
120 lines
#!/bin/bash
# trending-dw — 10-minute HEALTH + OPPORTUNITY tick.
# DTD verdict (2026-07-06, 3/3 option A): this cron is a WATCHER, not a writer/spender.
# • probe live uptime
# • measure data staleness
# • detect ComfyUI availability (for the 13 image-gap fills)
# • when a threshold is crossed, DRAFT a GATED approval memo to pending-approval/ (idempotent)
# HARD RULE: never auto-deploy, never auto-spend (Exa refresh is metered), never write to prod.
# Everything here is $0 local — read-only probes + local file writes to the approval queue.
set -u
cd "$(dirname "$0")/.."
ROOT="$(pwd)"
# launchd runs with a minimal PATH (no /opt/homebrew/bin) — resolve node absolutely
# or the data-staleness read silently returns empty. Belt: prepend homebrew to PATH too.
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
NODE="$(command -v node || echo /opt/homebrew/bin/node)"
STATE="$ROOT/data/tick-state"; mkdir -p "$STATE"
LEDGER="$ROOT/data/tick-ledger.jsonl"
PENDING="$HOME/.claude/yolo-queue/pending-approval"; mkdir -p "$PENDING"
NOW="$(date +%Y-%m-%dT%H:%M:%S%z)"
DAY="$(date +%Y-%m-%d)"
LIVE_URL="${TRENDING_LIVE_URL:-https://trending.designerwallcoverings.com/}"
AUTH="${TRENDING_AUTH:-admin:DW2024!}"
STALE_THRESHOLD="${TRENDING_STALE_DAYS:-7}" # weekly refresh cadence (matches the original staged plist)
# ── 1. live uptime probe (read-only) ──
HTTP="$(curl -s -o /dev/null -w '%{http_code}' -u "$AUTH" --max-time 12 "$LIVE_URL" 2>/dev/null || echo 000)"
[ "$HTTP" = "200" ] && UP=1 || UP=0
# ── 2. data staleness (read local snapshot) ──
read AS_OF STALE_DAYS IMG_GAP COUNT <<<"$("$NODE" -e '
try{ const d=JSON.parse(require("fs").readFileSync("data/bestsellers.json","utf8"));
const its=d.items||[]; const sp=its.map(i=>i.spottedAt).filter(Boolean).sort();
const asOf=sp.length?sp[sp.length-1]:(d.generatedAt||"");
// staleness pinned to Steve TZ (America/Los_Angeles) + pure date arithmetic, so it matches
// the server (Kamatera/UTC) and Mac2 identically. spottedAt is a bare date with no tz.
let days=-1; { const p=String(asOf).split("-").map(Number);
if(p[0]&&p[1]&&p[2]){ const ts=new Date().toLocaleDateString("en-CA",{timeZone:"America/Los_Angeles"}).split("-").map(Number);
days=Math.round((Date.UTC(ts[0],ts[1]-1,ts[2])-Date.UTC(p[0],p[1]-1,p[2]))/86400000); } }
const gap=its.filter(i=>!i.image).length;
process.stdout.write(asOf+" "+days+" "+gap+" "+its.length);
}catch(e){ process.stdout.write("- -1 -1 0"); }' 2>/dev/null)"
# ── 3. ComfyUI availability (for image-gap fills) ──
COMFY=0
for port in ${COMFY_PORTS:-8188 8189 8000}; do
if curl -s -o /dev/null --max-time 3 "http://127.0.0.1:$port/" 2>/dev/null; then COMFY=1; COMFY_PORT=$port; break; fi
done
DRAFTED=""
# ── 4a. GATED: metered live-refresh request when stale beyond threshold (idempotent per data vintage) ──
if [ "$STALE_DAYS" -ge "$STALE_THRESHOLD" ] 2>/dev/null; then
SLUG="trending-dw-refresh-${AS_OF}.md"
# Only suppress while the memo is in the ACTIVE queue (maxdepth 1). If it was resolved/archived
# but the vintage is STILL $AS_OF (the "fix" didn't move the data), re-surface it — an unfixed
# problem must keep nagging, never go permanently silent.
if [ -z "$(find "$PENDING" -maxdepth 1 -name "$SLUG" 2>/dev/null | head -1)" ]; then
cat > "$PENDING/$SLUG" <<EOF
# GATED APPROVAL — trending-dw metered trend refresh
**Drafted:** $NOW (autonomous 10-min tick)
**Officer of record:** vp-dw-commerce
**Why:** the trending board's data is **$STALE_DAYS days old** (as of $AS_OF), past the ${STALE_THRESHOLD}-day cadence. A *trending* board is only as credible as its freshness.
## Status: wiring is BUILT — only the metered pull remains gated
The \$0 mapper (\`scripts/scout-to-bestsellers.js\`) + \`refresh.sh --scout\` path exist and are tested; a scout feed maps to the board with TODAY's date. What's left is the one metered step:
1. **GATED** — run the \`design-trend-scout\` agent (Exa, metered) → write \`data/scout-feed.json\`.
2. \$0 — \`bash ~/Projects/trending-dw/scripts/refresh.sh --scout data/scout-feed.json\` (maps + re-attaches images + reloads).
3. GATED — deploy to prod.
## The ask
Approve step 1 (the metered Exa pull). Steps 2 is \$0; step 3 is a normal deploy.
- **Cost:** metered Exa research pull — estimate a few \$ per run (show + log actual to cost-tracker before running). The tick, the mapper, and \`refresh.sh --scout\` are all \$0; only the scout pull spends.
## APPROVE / REVISE / BLOCK
- [ ] APPROVE — build the live-Exa wiring (if needed) + run the metered pull + draft the deploy
- [ ] REVISE — change cadence/threshold (currently ${STALE_THRESHOLD}d)
- [ ] BLOCK — leave the board at its last approved vintage (badge stays honest-stale)
EOF
DRAFTED="${DRAFTED}refresh:$SLUG "
fi
fi
# ── 4b. OPPORTUNITY: ComfyUI came back → the 13 image gaps can be filled to staging (idempotent on transition) ──
LAST_COMFY="$(cat "$STATE/comfy" 2>/dev/null || echo 0)"
if [ "$COMFY" = "1" ] && [ "$LAST_COMFY" != "1" ] && [ "${IMG_GAP:-0}" -gt 0 ] 2>/dev/null; then
SLUG="trending-dw-imagegap-${DAY}.md"
if [ -z "$(find "$PENDING" -maxdepth 1 -name "$SLUG" 2>/dev/null | head -1)" ]; then
cat > "$PENDING/$SLUG" <<EOF
# OPPORTUNITY — trending-dw image-gap fill (ComfyUI is back)
**Drafted:** $NOW (autonomous 10-min tick)
**Officer of record:** vp-dw-commerce / wallpapersback-agent (settlement-gated generation)
**Why:** ComfyUI is UP (port ${COMFY_PORT:-?}) and **$IMG_GAP of $COUNT** trending cards still lack an our-own image (the seam-gate wall that blocked TR generation). Now fillable to LOCAL staging.
## The ask
Generate the $IMG_GAP missing our-own designs to staging (settlement gate MANDATORY, is_published=FALSE), then attach + stage a deploy.
- **Cost:** local SDXL/ComfyUI = \$0 (local GPU); any metered fallback shown separately.
- **Guardrail:** generation runs through the settlement gate; nothing publishes without a separate go.
## APPROVE / REVISE / BLOCK
- [ ] APPROVE — generate the $IMG_GAP gaps to staging
- [ ] REVISE — subset / different lanes
- [ ] BLOCK — leave the gap cards on the 'pending' affordance
EOF
DRAFTED="${DRAFTED}imagegap:$SLUG "
fi
fi
echo "$COMFY" > "$STATE/comfy"
# ── 5. append the tick ledger ($0 local) ──
printf '{"ts":"%s","live_http":"%s","up":%s,"as_of":"%s","stale_days":%s,"img_gap":%s,"count":%s,"comfy":%s,"drafted":"%s","cost":"$0 (local)"}\n' \
"$NOW" "$HTTP" "$UP" "$AS_OF" "${STALE_DAYS:-null}" "${IMG_GAP:-null}" "${COUNT:-0}" "$COMFY" "${DRAFTED:-none}" >> "$LEDGER"
echo "tick $NOW · live=$HTTP · stale=${STALE_DAYS}d · img_gap=$IMG_GAP · comfy=$COMFY · drafted=[${DRAFTED:-none}] · cost=\$0 (local)"