## 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)
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #79
29 lines
993 B
TypeScript
29 lines
993 B
TypeScript
import { database } from "~/database/context";
|
|
import { eq, or } from "drizzle-orm";
|
|
import * as schema from "~/database/schema";
|
|
import { createDailySnapshot } from "~/models/standings";
|
|
import { requireCronSecret } from "~/lib/cron-auth";
|
|
|
|
export async function action({ request }: { request: Request }) {
|
|
requireCronSecret(request);
|
|
|
|
const db = database();
|
|
const activeSeasons = await db.query.seasons.findMany({
|
|
where: or(eq(schema.seasons.status, "active"), eq(schema.seasons.status, "draft")),
|
|
});
|
|
|
|
let succeeded = 0;
|
|
const errors: { id: string; error: string }[] = [];
|
|
|
|
for (const season of activeSeasons) {
|
|
try {
|
|
await createDailySnapshot(season.id, db);
|
|
succeeded++;
|
|
} catch (err) {
|
|
errors.push({ id: season.id, error: err instanceof Error ? err.message : String(err) });
|
|
}
|
|
}
|
|
|
|
const status = errors.length > 0 && succeeded === 0 ? 500 : 200;
|
|
return Response.json({ total: activeSeasons.length, succeeded, errors }, { status });
|
|
}
|