fix: crons do Pro entram no vercel.ts, nao num json ao lado

O deploy anterior caiu com 'Multiple configuration files found'. A lista completa fica no vercel.ts, que ja era o arquivo canonico.
This commit is contained in:
Ian Couto
2026-09-13 10:03:04 -03:00
parent e825a1a77b
commit a55416dc31
4 changed files with 40 additions and 138 deletions
+3 -2
View File
@@ -2,10 +2,11 @@
## Plano Pro
No Pro o relógio nativo é o `vercel.json` da raiz — a mesma cadência do
No Pro o relógio nativo é o `vercel.ts` da raiz — a mesma cadência do
`docker/scheduler/entrypoint.sh` (follow-up, dreno, dispatcher, agenda…).
Expressões de minuto **quebram o deploy Hobby**. Este runbook é só para quem
está no plano gratuito e precisa de um cron de fora.
está no plano gratuito e precisa de um cron de fora. Não crie `vercel.json`
ao lado do `vercel.ts`: a Vercel recusa os dois.
## Por que existe (Hobby)
+7 -16
View File
@@ -30,7 +30,7 @@ const DIR_CRON = join(RAIZ, "app", "api", "v1", "cron");
// do cron à internet da VPS. A cerca continua a mesma; só a fonte da verdade do
// "o que roda" mudou de arquivo.
const CRONTAB = join(RAIZ, "docker", "scheduler", "entrypoint.sh");
const VERCEL_JSON = join(RAIZ, "vercel.json");
const VERCEL_TS = join(RAIZ, "vercel.ts");
/** As rotas que existem, lidas do disco — não de uma lista mantida à mão. */
function rotasNoCodigo(): string[] {
@@ -78,24 +78,15 @@ describe("rotas de cron × agendamento no self-host", () => {
});
});
function rotasNoVercelJson(): string[] {
const raw = JSON.parse(readFileSync(VERCEL_JSON, "utf8")) as {
crons?: { path: string }[];
};
return [
...new Set(
(raw.crons ?? []).map((c) => {
const nome = c.path.match(/\/api\/v1\/cron\/([a-z0-9-]+)/)?.[1];
if (!nome) throw new Error(`cron path fora do contrato: ${c.path}`);
return nome;
}),
),
].sort();
function rotasNoVercelTs(): string[] {
const fonte = readFileSync(VERCEL_TS, "utf8");
const achadas = fonte.matchAll(/path:\s*"\/api\/v1\/cron\/([a-z0-9-]+)"/g);
return [...new Set([...achadas].map((m) => m[1]!))].sort();
}
describe("rotas de cron × agendamento no Vercel Pro", () => {
it("vercel.json agenda as mesmas rotas do scheduler", () => {
expect(rotasNoVercelJson()).toEqual(rotasAgendadas());
it("vercel.ts agenda as mesmas rotas do scheduler", () => {
expect(rotasNoVercelTs()).toEqual(rotasAgendadas());
});
it("documenta o CRON_SECRET da Vercel — senão o Pro agenda e a rota responde 403", () => {
-103
View File
@@ -1,103 +0,0 @@
{
"crons": [
{
"path": "/api/v1/cron/agent-dispatcher",
"schedule": "* * * * *"
},
{
"path": "/api/v1/cron/followup-flow-worker",
"schedule": "* * * * *"
},
{
"path": "/api/v1/cron/event-log-drain",
"schedule": "* * * * *"
},
{
"path": "/api/v1/cron/routing-worker",
"schedule": "* * * * *"
},
{
"path": "/api/v1/cron/recover-stuck-messages",
"schedule": "* * * * *"
},
{
"path": "/api/v1/cron/storage-redaction",
"schedule": "*/5 * * * *"
},
{
"path": "/api/v1/cron/snooze-watcher",
"schedule": "*/5 * * * *"
},
{
"path": "/api/v1/cron/attendant-heartbeat",
"schedule": "*/5 * * * *"
},
{
"path": "/api/v1/cron/webhook-log-retention",
"schedule": "*/5 * * * *"
},
{
"path": "/api/v1/cron/channel-health",
"schedule": "*/5 * * * *"
},
{
"path": "/api/v1/cron/agenda-google-push",
"schedule": "*/5 * * * *"
},
{
"path": "/api/v1/cron/agenda-reminder",
"schedule": "*/5 * * * *"
},
{
"path": "/api/v1/cron/contact-avatars",
"schedule": "*/10 * * * *"
},
{
"path": "/api/v1/cron/agenda-google-refresh",
"schedule": "*/10 * * * *"
},
{
"path": "/api/v1/cron/agenda-google-sync",
"schedule": "*/15 * * * *"
},
{
"path": "/api/v1/cron/risk-watcher",
"schedule": "*/15 * * * *"
},
{
"path": "/api/v1/cron/contact-phones",
"schedule": "*/30 * * * *"
},
{
"path": "/api/v1/cron/contact-proposals-watcher",
"schedule": "17 * * * *"
},
{
"path": "/api/v1/cron/lgpd-sla-watcher",
"schedule": "0 12 * * *"
},
{
"path": "/api/v1/cron/kb-conversations-batch",
"schedule": "30 3 * * *"
},
{
"path": "/api/v1/cron/sync-model-catalog",
"schedule": "15 4 * * *"
},
{
"path": "/api/v1/cron/data-retention",
"schedule": "40 4 * * *"
}
],
"functions": {
"app/api/v1/cron/**/route.ts": {
"maxDuration": 120
},
"app/api/v1/system/relogio/tick/route.ts": {
"maxDuration": 60
},
"app/api/internal/agents/run/route.ts": {
"maxDuration": 300
}
}
}
+30 -17
View File
@@ -1,32 +1,45 @@
/**
* Vercel project config (canonical TS form).
* Configuração do projeto na Vercel.
*
* Crons placeholder; lista final virá da Spec 08 (Operações & Workers).
* Os 7 crons abaixo refletem os jobs derivados das specs herdadas:
* - recover-stuck-messages (WAHA)
* - sync-sessions (WAHA)
* - process-pending-webhooks (event_log)
* - dispatch-webhooks (deliveries / outbound webhooks)
* - lgpd-data-request-worker (D+7 SLA)
* - nuvemshop-sync-incremental
* - audit-log-archive (cold storage)
* Os crons abaixo são a MESMA cadência de `docker/scheduler/entrypoint.sh`.
* Plano Pro: minuto a minuto vale. Hobby: expressões mais frequentes que
* 1×/dia derrubam o deploy — nesse caso deixe só o `lgpd-sla-watcher` e use
* o relógio HTTP (`docs/runbooks/vercel-hobby-relogio.md`).
*
* Auth de cron: header `Authorization: Bearer ${INTERNAL_SECRET}` validado em cada handler.
* Auth: Vercel Cron manda Bearer CRON_SECRET; em produção `lib/env.ts` copia
* isso para INTERNAL_CRON_SECRET. INTERNAL_SECRET continua valendo nas rotas.
*/
import type { VercelConfig } from "@vercel/config/v1";
const config: VercelConfig = {
// Plano Hobby só aceita cron ≤1×/dia. Os jobs */1 (agent-dispatcher,
// routing-worker, event-log-drain) ficam de fora até Pro — no self-host
// eles rodam no container `scheduler`. Reative-os ao subir de plano.
crons: [
{ path: "/api/v1/cron/agent-dispatcher", schedule: "* * * * *" },
{ path: "/api/v1/cron/followup-flow-worker", schedule: "* * * * *" },
{ path: "/api/v1/cron/event-log-drain", schedule: "* * * * *" },
{ path: "/api/v1/cron/routing-worker", schedule: "* * * * *" },
{ path: "/api/v1/cron/recover-stuck-messages", schedule: "* * * * *" },
{ path: "/api/v1/cron/storage-redaction", schedule: "*/5 * * * *" },
{ path: "/api/v1/cron/snooze-watcher", schedule: "*/5 * * * *" },
{ path: "/api/v1/cron/attendant-heartbeat", schedule: "*/5 * * * *" },
{ path: "/api/v1/cron/webhook-log-retention", schedule: "*/5 * * * *" },
{ path: "/api/v1/cron/channel-health", schedule: "*/5 * * * *" },
{ path: "/api/v1/cron/agenda-google-push", schedule: "*/5 * * * *" },
{ path: "/api/v1/cron/agenda-reminder", schedule: "*/5 * * * *" },
{ path: "/api/v1/cron/contact-avatars", schedule: "*/10 * * * *" },
{ path: "/api/v1/cron/agenda-google-refresh", schedule: "*/10 * * * *" },
{ path: "/api/v1/cron/agenda-google-sync", schedule: "*/15 * * * *" },
{ path: "/api/v1/cron/risk-watcher", schedule: "*/15 * * * *" },
{ path: "/api/v1/cron/contact-phones", schedule: "*/30 * * * *" },
{ path: "/api/v1/cron/contact-proposals-watcher", schedule: "17 * * * *" },
{ path: "/api/v1/cron/lgpd-sla-watcher", schedule: "0 12 * * *" },
{ path: "/api/v1/cron/kb-conversations-batch", schedule: "30 3 * * *" },
{ path: "/api/v1/cron/sync-model-catalog", schedule: "15 4 * * *" },
{ path: "/api/v1/cron/data-retention", schedule: "40 4 * * *" },
],
functions: {
// EPIC-13 S-13.08: ToolLoopAgent runtime can issue multiple tool calls per
// step. 300s max keeps Fluid Compute within bounds; the runtime's own
// step/token/cost guards usually finish much earlier.
"app/api/v1/cron/**/route.ts": { maxDuration: 120 },
"app/api/v1/system/relogio/tick/route.ts": { maxDuration: 60 },
"app/api/internal/agents/run/route.ts": { maxDuration: 300 },
},
};