Phase A: external HTTP cron jobs for snapshots, standings sync, and simulation #79

Merged
chrisp merged 3 commits from feat/phase-a-cron-jobs into main 2026-06-08 07:42:15 +00:00
Owner

Summary

  • Removes the last in-process `setInterval` (`server/snapshots.ts` 24h loop) and replaces it with an external HTTP cron job via Forgejo Actions
  • Adds automated standings sync + conditional simulation: syncs every 2h, only simulates when standings actually changed (detected by comparing `gamesPlayed`/`leagueRank` before upsert)
  • Adds `GET /healthz` for Docker healthcheck (Phase B prerequisite)

What's new

Endpoint Triggered by What it does
`POST /admin/jobs/run-daily-snapshots` Forgejo schedule `5 0 * * *` Creates daily fantasy standings snapshots for all active/draft seasons
`POST /admin/jobs/sync-and-simulate` Forgejo schedule `0 */2 * * *` Syncs standings from external APIs; runs simulation only if standings changed
`GET /healthz` Docker / Traefik Returns 200 `{ok:true}` when DB reachable, 503 otherwise

Both cron endpoints are protected by `X-Cron-Secret` header (set `CRON_SECRET` in Forgejo repo secrets + production env).

Schema changes (migration 0118)

Two new nullable columns on `sports_seasons`:

  • `standings_last_changed_at` — written by `syncStandings()` when data actually changes
  • `last_simulated_at` — written by the cron job after a successful simulation run

Deployment notes

  1. Add `CRON_SECRET` to Forgejo repo secrets (generate with `openssl rand -hex 32`)
  2. Add same value to production environment
  3. Migration runs automatically via the `migrate` container on deploy

Test plan

🤖 Generated with Claude Code

## Summary - Removes the last in-process \`setInterval\` (\`server/snapshots.ts\` 24h loop) and replaces it with an external HTTP cron job via Forgejo Actions - Adds automated standings sync + conditional simulation: syncs every 2h, only simulates when standings actually changed (detected by comparing \`gamesPlayed\`/\`leagueRank\` before upsert) - Adds \`GET /healthz\` for Docker healthcheck (Phase B prerequisite) ## What's new | Endpoint | Triggered by | What it does | |---|---|---| | \`POST /admin/jobs/run-daily-snapshots\` | Forgejo schedule \`5 0 * * *\` | Creates daily fantasy standings snapshots for all active/draft seasons | | \`POST /admin/jobs/sync-and-simulate\` | Forgejo schedule \`0 */2 * * *\` | Syncs standings from external APIs; runs simulation only if standings changed | | \`GET /healthz\` | Docker / Traefik | Returns 200 \`{ok:true}\` when DB reachable, 503 otherwise | Both cron endpoints are protected by \`X-Cron-Secret\` header (set \`CRON_SECRET\` in Forgejo repo secrets + production env). ## Schema changes (migration 0118) Two new nullable columns on \`sports_seasons\`: - \`standings_last_changed_at\` — written by \`syncStandings()\` when data actually changes - \`last_simulated_at\` — written by the cron job after a successful simulation run ## Deployment notes 1. Add \`CRON_SECRET\` to Forgejo repo secrets (generate with \`openssl rand -hex 32\`) 2. Add same value to production environment 3. Migration runs automatically via the \`migrate\` container on deploy ## Test plan - [ ] \`curl -X POST https://brackt.com/admin/jobs/run-daily-snapshots -H "X-Cron-Secret: ..."\` → 200 \`{total, succeeded, errors}\` - [ ] \`curl -X POST https://brackt.com/admin/jobs/sync-and-simulate -H "X-Cron-Secret: ..."\` → 200 with \`synced\`/\`unchanged\`/\`simulated\` breakdown - [ ] \`curl https://brackt.com/healthz\` → 200 \`{ok:true}\` - [ ] Verify Forgejo workflow runs appear in Actions tab after merge - [ ] Kill web process mid-day; confirm external cron still fires (no in-process dependency) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chrisp added 3 commits 2026-06-08 07:35:38 +00:00
- Add POST /admin/jobs/run-daily-snapshots (replaces 24h setInterval in server/snapshots.ts)
- Add POST /admin/jobs/sync-and-simulate (syncs standings every 2h; only simulates when standings
  actually changed by comparing gamesPlayed + leagueRank before upserting)
- Add standingsLastChangedAt + lastSimulatedAt columns to sportsSeasons (migration 0118)
- Add GET /healthz route for Docker healthcheck (pings DB, returns 503 on failure)
- Add app/lib/cron-auth.ts shared X-Cron-Secret header auth helper
- Remove startSnapshotSystem() from server/socket.ts (no more in-process setInterval)
- Add Forgejo Actions scheduled workflows: daily-snapshots.yml (00:05 UTC) and
  sync-and-simulate.yml (every 2h) — require CRON_SECRET repo secret

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix three code review findings from Phase A cron jobs
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m45s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m29s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
52eb3abac1
- Remove `| cat` from both Forgejo workflow curl commands so HTTP 4xx/5xx
  causes the step to fail (bash pipeline exit code was always 0 via cat)
- Simplify sync-and-simulate: drop redundant pre-check DB query for
  standingsLastChangedAt; changed===true already means standings updated
  this run, so always simulate (runner's simulationStatus guard handles overlap)
- Delete server/snapshots.ts — all exports were orphaned after startSnapshotSystem
  was removed from server/socket.ts in the Phase A commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
chrisp merged commit 273742f02e into main 2026-06-08 07:42:15 +00:00
Sign in to join this conversation.
No description provided.