[object Object]

← back to Trending Dw

trending: 10-min health+opportunity tick + launchd cron (DTD verdict A)

4fe938fa9ee1baf92576fa028a2113437962e030 · 2026-07-06 21:09:33 -0700 · Steve Abrams

- scripts/tick.sh: deterministic $0 watcher — probes live uptime, measures data
  staleness, detects ComfyUI; drafts a GATED refresh memo when stale>threshold (7d)
  and an image-gap opportunity memo when ComfyUI returns. Idempotent (find-based guard).
  NEVER auto-deploys, NEVER auto-spends (metered Exa refresh stays Steve-gated).
- ops/com.steve.trending-dw-tick.plist: StartInterval 600 (every 10 min), RunAtLoad.
- resolves node absolutely (launchd minimal-PATH fix) + hardened PATH.
- ledger at data/tick-ledger.jsonl (gitignored); state in data/tick-state/.

Files touched

Diff

commit 4fe938fa9ee1baf92576fa028a2113437962e030
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 6 21:09:33 2026 -0700

    trending: 10-min health+opportunity tick + launchd cron (DTD verdict A)
    
    - scripts/tick.sh: deterministic $0 watcher — probes live uptime, measures data
      staleness, detects ComfyUI; drafts a GATED refresh memo when stale>threshold (7d)
      and an image-gap opportunity memo when ComfyUI returns. Idempotent (find-based guard).
      NEVER auto-deploys, NEVER auto-spends (metered Exa refresh stays Steve-gated).
    - ops/com.steve.trending-dw-tick.plist: StartInterval 600 (every 10 min), RunAtLoad.
    - resolves node absolutely (launchd minimal-PATH fix) + hardened PATH.
    - ledger at data/tick-ledger.jsonl (gitignored); state in data/tick-state/.
---
 .gitignore                           |   3 +
 ops/com.steve.trending-dw-tick.plist |  13 +++++
 scripts/tick.sh                      | 108 +++++++++++++++++++++++++++++++++++
 3 files changed, 124 insertions(+)

diff --git a/.gitignore b/.gitignore
index 421f4d2..982b07b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,6 @@ build/
 data/shopify-queue.json
 
 .gapvenv/
+
+data/tick-state/
+data/tick-ledger.jsonl
diff --git a/ops/com.steve.trending-dw-tick.plist b/ops/com.steve.trending-dw-tick.plist
new file mode 100644
index 0000000..8c0075b
--- /dev/null
+++ b/ops/com.steve.trending-dw-tick.plist
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0"><dict>
+  <key>Label</key><string>com.steve.trending-dw-tick</string>
+  <key>ProgramArguments</key><array>
+    <string>/bin/bash</string>
+    <string>/Users/macstudio3/Projects/trending-dw/scripts/tick.sh</string>
+  </array>
+  <key>StartInterval</key><integer>600</integer>
+  <key>RunAtLoad</key><true/>
+  <key>StandardOutPath</key><string>/tmp/trending-dw-tick.log</string>
+  <key>StandardErrorPath</key><string>/tmp/trending-dw-tick.log</string>
+</dict></plist>
diff --git a/scripts/tick.sh b/scripts/tick.sh
new file mode 100755
index 0000000..61d52e8
--- /dev/null
+++ b/scripts/tick.sh
@@ -0,0 +1,108 @@
+#!/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||"");
+    const days=asOf?Math.floor((Date.now()-Date.parse(asOf))/86400000):-1;
+    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"
+  # skip if a memo for THIS data vintage already exists anywhere in the queue (pending or resolved)
+  if [ -z "$(find "$PENDING" -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.
+
+## The ask
+Approve a metered **design-trend-scout (Exa)** refresh to re-pull current bestsellers, then re-attach our-own images and stage a deploy.
+
+- **Cost:** metered Exa research pull — estimate a few \$ per run (show + log actual to cost-tracker before running). This is the ONLY spend; the tick itself is \$0.
+- **Command (run after approval):** \`bash ~/Projects/trending-dw/scripts/refresh.sh\` once wired to live Exa (currently re-seeds committed data — the live wiring is the gated step).
+
+## APPROVE / REVISE / BLOCK
+- [ ] APPROVE — run the metered refresh + draft the deploy
+- [ ] REVISE — change cadence/threshold (currently ${STALE_THRESHOLD}d)
+- [ ] BLOCK — leave the board at its last approved vintage
+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" -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)"

← ab31ac0 trending: data-freshness badge + per-card spotted timestamp  ·  back to Trending Dw  ·  trending: fix contrarian-caught issues (onerror injection, h 955e9e4 →