diff --git a/src/app/api/cron/reconcile/route.ts b/src/app/api/cron/reconcile/route.ts new file mode 100644 index 0000000..e51452c --- /dev/null +++ b/src/app/api/cron/reconcile/route.ts @@ -0,0 +1,45 @@ +import { ingestProject, projectsWithRepos } from "@/lib/git/ingest"; +import { jsonError, jsonOk } from "@/lib/http/json"; + +/** + * POST /api/cron/reconcile — hourly catch-up across every linked project. + * + * Exists because webhooks fail. A delivery lost while the app restarts is gone + * for good, and the dashboard stays plausible while being wrong, which is the + * worst failure available here. This is also the same code path as backfill, + * so the reliable path is the one that gets exercised constantly. + * + * Triggered by a Coolify scheduled task: + * curl -X POST -H "Authorization: Bearer $CRON_SECRET" https:///api/cron/reconcile + * + * One project failing must not stop the rest — a repository that was renamed + * should not silently freeze every other project's dashboard. + */ +export async function POST(request: Request): Promise { + const secret = process.env.CRON_SECRET; + if (!secret) { + console.error("[cron] CRON_SECRET is not set; refusing to run"); + return jsonError("Not configured", 500); + } + + if (request.headers.get("authorization") !== `Bearer ${secret}`) { + return jsonError("Unauthorized", 401); + } + + const projects = await projectsWithRepos(); + const results: Array<{ project: string; created?: number; error?: string }> = []; + + for (const project of projects) { + try { + const result = await ingestProject(project, { stopAfterKnownPages: 1 }); + results.push({ project: project.slug, created: result.created }); + } catch (error) { + const message = error instanceof Error ? error.message : "unknown error"; + console.error(`[cron] ${project.slug} failed: ${message}`); + results.push({ project: project.slug, error: message }); + } + } + + console.log(`[cron] reconciled ${projects.length} projects`); + return jsonOk({ projects: results }); +} \ No newline at end of file