diff --git a/docs/CRON_SETUP.md b/docs/CRON_SETUP.md index f9cfe2b..bff8a47 100644 --- a/docs/CRON_SETUP.md +++ b/docs/CRON_SETUP.md @@ -2,14 +2,22 @@ Aplikasi punya beberapa endpoint cron yang harus di-trigger periodik dari luar. Karena deploy pakai PM2 di VPS Linux (bukan Vercel), trigger schedule pakai **system crontab** — zero dependency, OS-native. +> **Audit trail otomatis:** semua cron yang di-wrap `runCron()` helper auto-log ke tabel `CronRun` (start/finish/error). Cek hasilnya real-time di `/admin/system` — link "System" di sidebar admin. Tidak perlu tail log untuk monitoring rutin. + +--- + ## Daftar cron job -| Endpoint | Schedule | Tujuan | -|---|---|---| -| `GET /api/cron/auto-complete-trips` | `0 18 * * *` (18:00 UTC = 01:00 WIB) | Flip trip yang sudah lewat tanggal selesai dari `OPEN`/`FULL` ke `COMPLETED`. | -| `GET /api/cron/process-email-jobs` | `*/5 * * * *` (setiap 5 menit) | Drain retry queue email — pick `EmailJob` status PENDING/FAILED (attempts<5), retry via Resend. | +| # | Endpoint | Schedule | Frekuensi | Tujuan | +|---|---|---|---|---| +| 1 | `GET /api/cron/auto-complete-trips` | `0 18 * * *` | Daily 01:00 WIB (18:00 UTC) | Flip trip yang sudah lewat tanggal selesai dari `OPEN`/`FULL` ke `COMPLETED`. Setelah itu, release payout HELD yang sudah lewat `heldUntil`. | +| 2 | `GET /api/cron/process-email-jobs` | `*/5 * * * *` | Setiap 5 menit | Drain retry queue email — pick `EmailJob` status `PENDING`/`FAILED` (attempts<5), retry via Resend dengan exponential backoff. | -## Setup di server +Semua cron pakai pola yang sama: header `Authorization: Bearer ${CRON_SECRET}`, idempotent, auto-log ke `CronRun`. Tambah cron baru = tambah baris di tabel ini + tabel `TRACKED_JOBS` di [app/admin/system/page.tsx](../app/admin/system/page.tsx). + +--- + +## Setup di server (one-time) ### 1. Set `CRON_SECRET` di env production @@ -31,7 +39,17 @@ Restart PM2 supaya proses re-load env: pm2 restart setrip --update-env ``` -### 2. Daftarkan crontab +### 2. Set env opsional untuk fitur lain yang di-trigger cron + +| Env | Dibutuhkan oleh | Akibat kalau kosong | +|---|---|---| +| `RESEND_API_KEY` | `process-email-jobs` cron | Email tetap di-queue di DB; cron skip dengan warning. Set nanti dan cron akan auto-drain queue. | +| `EMAIL_FROM` | `process-email-jobs` cron | Pakai default `SeTrip ` (cocok untuk dev/test). | +| `ADMIN_ALERT_WEBHOOK_URL` | `runCron` (semua cron) | Tidak ada Discord push notif saat cron FAILED. Admin tetap bisa cek manual di `/admin/system`. | + +Semua env ini ada di [.env.example](../.env.example) dengan instruksi setup masing-masing. + +### 3. Daftarkan crontab Edit crontab user yang punya akses ke env (biasanya user yang menjalankan PM2): @@ -39,11 +57,16 @@ Edit crontab user yang punya akses ke env (biasanya user yang menjalankan PM2): crontab -e ``` -Tambah baris (ganti `https://your-domain.com` dan `` sesuai env yang di-set di step 1): +Tambah baris berikut (ganti `https://your-domain.com` dan `` sesuai env yang di-set di step 1): ```cron -# Setrip — auto-complete trips harian (jam 01:00 WIB) +# === Setrip cron jobs === + +# 1. Auto-complete trip + release payout (daily 01:00 WIB) 0 18 * * * curl -fsS -H "Authorization: Bearer " https://your-domain.com/api/cron/auto-complete-trips >> /var/log/setrip-cron.log 2>&1 + +# 2. Drain email retry queue (setiap 5 menit) +*/5 * * * * curl -fsS -H "Authorization: Bearer " https://your-domain.com/api/cron/process-email-jobs >> /var/log/setrip-cron.log 2>&1 ``` Verifikasi crontab tersimpan: @@ -52,57 +75,182 @@ Verifikasi crontab tersimpan: crontab -l ``` -### 3. Siapkan file log +### 4. Siapkan file log ```bash sudo touch /var/log/setrip-cron.log sudo chown $(whoami) /var/log/setrip-cron.log ``` -## Test manual +Optional — logrotate supaya log tidak menggemuk: + +```bash +sudo nano /etc/logrotate.d/setrip-cron +``` + +Isi: + +``` +/var/log/setrip-cron.log { + weekly + rotate 4 + compress + missingok + notifempty +} +``` + +--- + +## Test manual (sanity check setelah deploy) Sebelum tunggu schedule, panggil endpoint langsung untuk verifikasi: ```bash +# Test cron 1 curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://your-domain.com/api/cron/auto-complete-trips + +# Test cron 2 +curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://your-domain.com/api/cron/process-email-jobs ``` -**Expected response:** +**Expected response per cron:** -- Belum ada trip yang lewat: `{"ok":true,"completed":0,"ids":[]}` -- Ada trip yang lewat: `{"ok":true,"completed":2,"ids":["clx...","cly..."]}` +| Cron | Sukses kosong | Sukses ada pekerjaan | +|---|---|---| +| `auto-complete-trips` | `{"ok":true,"completed":0,"ids":[],"payoutsReleased":[]}` | `{"ok":true,"completed":2,"ids":["clx...","cly..."],"payoutsReleased":["..."]}` | +| `process-email-jobs` | `{"ok":true,"picked":0,"succeeded":0,"failed":0}` | `{"ok":true,"picked":5,"succeeded":5,"failed":0}` | -**Kalau dapat 401:** `CRON_SECRET` di env tidak match dengan header. Cek `pm2 env `. +**Error response:** +- **401** — `CRON_SECRET` di env tidak match dengan header. Cek `pm2 env `. +- **500 "Server misconfigured"** — `CRON_SECRET` belum di-set di env. +- **500 lain** — cek log app atau `/admin/system` untuk detail error. -**Kalau dapat 500:** `CRON_SECRET` belum di-set di env. +--- ## Monitoring -Tail log cron: +### Cara utama: `/admin/system` (admin panel) + +Buka [/admin/system](/admin/system) — tampilkan: +- **Per-job summary card**: last run, last success, count 7 hari, error count 7 hari, health badge (🟢 OK / 🟡 STALE / 🔴 FAILED). +- **Recent runs table**: 20 cron run terakhir lintas semua job (waktu, status, payload, error). +- **Stale state alerts**: banner kuning kalau ada Payment AWAITING > 25h / Payout HELD overdue / Refund APPROVED > 7d. + +Cek setiap pagi sebelum mulai kerja — kalau semua 🟢 OK, tidak ada incident. + +### Cara backup: tail log file ```bash tail -f /var/log/setrip-cron.log ``` -Cek log app PM2 (untuk `console.log` dari endpoint): +PM2 log untuk `console.log` dari endpoint: ```bash pm2 logs setrip --lines 100 | grep cron ``` +### Discord push notif (opsional) + +Kalau env `ADMIN_ALERT_WEBHOOK_URL` di-set ke Discord webhook URL, `runCron` otomatis kirim 🚨 message saat cron FAILED — admin bisa react langsung tanpa harus cek dashboard. + +Cara setup: Discord channel → Edit Channel → Integrations → Webhooks → New → copy URL → set di env. + +--- + ## Troubleshooting **Cron jalan tapi tidak ada efek di DB:** -- Cek `pm2 logs setrip` untuk error. -- Verifikasi waktu server: `date` (output harus UTC kalau pakai schedule UTC). +- Cek `/admin/system` — kalau status SUCCESS dengan `payload: { completed: 0 }`, memang tidak ada pekerjaan saat ini. +- Cek `pm2 logs setrip` untuk error runtime. +- Verifikasi waktu server: `date -u` (output harus UTC kalau pakai schedule UTC). **Cron tidak jalan sama sekali:** - Cek service cron aktif: `systemctl status cron` (Debian/Ubuntu) atau `systemctl status crond` (RHEL/CentOS). - Cek crontab terdaftar di user yang benar: `sudo crontab -u $(whoami) -l`. +- Cek `/admin/system` — kalau "Last run" jauh dari ekspektasi (mis. > 25 jam untuk daily cron), schedule mungkin tidak ke-trigger. + +**`process-email-jobs` SUCCESS tapi email tidak terkirim:** +- Cek `RESEND_API_KEY` di env. Tanpa env, cron return early dengan warning di log. +- Cek dashboard Resend untuk delivery status / bounce. +- Cek tabel `EmailJob` di DB: status `PENDING`/`FAILED` + `lastError` field. + +**`auto-complete-trips` SUCCESS tapi trip masih OPEN:** +- Cek `Trip.endDate` (kalau ada) atau `Trip.date` — harus lewat cutoff (`utcStartOfDay(now)`). +- Trip dengan status `CLOSED` sengaja tidak di-touch (organizer eksplisit batalkan). **Secret bocor:** -- Generate ulang `CRON_SECRET`, update di `.env` + crontab line, restart PM2. +- Generate ulang `CRON_SECRET`, update di `.env` + semua baris crontab, restart PM2. -## Hari kalau pindah ke Vercel / PaaS lain +**Cron FAILED berturut-turut:** +- `/admin/system` akan tampilkan badge 🔴 FAILED. +- Kalau env `ADMIN_ALERT_WEBHOOK_URL` di-set, Discord channel akan dapat notif. +- Klik "Last error" di card cron untuk lihat stack trace, atau cek tabel `CronRun.errorMessage` langsung. -Tinggal hapus crontab line + bikin `vercel.json` (atau equivalent platform). Endpoint sudah platform-agnostic — proteksinya sama (header `Authorization: Bearer `). +--- + +## Saat menambah cron baru (developer note) + +Checklist: + +1. Buat route handler di `app/api/cron//route.ts` dengan pola standar (CRON_SECRET check + `runCron(jobName, fn)` wrapper). +2. Tambah entry di tabel **Daftar cron job** di doc ini. +3. Tambah baris di `TRACKED_JOBS` di [app/admin/system/page.tsx](../app/admin/system/page.tsx) supaya muncul di health card. +4. Brief ops: tambah baris di server crontab dengan schedule yang sesuai. + +Pattern minimal cron handler: + +```ts +// app/api/cron//route.ts +import { NextRequest, NextResponse } from "next/server"; +import { runCron } from "@/lib/cron-runner"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +export async function GET(req: NextRequest) { + const secret = process.env.CRON_SECRET; + if (!secret) { + return NextResponse.json({ error: "Server misconfigured" }, { status: 500 }); + } + if (req.headers.get("authorization") !== `Bearer ${secret}`) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const outcome = await runCron("", async () => { + // ... actual work; return value masuk ke CronRun.payload + return { processedCount: 0 }; + }); + + if (!outcome.ok) { + return NextResponse.json({ error: outcome.error }, { status: 500 }); + } + return NextResponse.json({ ok: true, ...outcome.payload }); +} +``` + +--- + +## Kalau pindah ke Vercel / PaaS lain + +Bikin `vercel.json` di root: + +```json +{ + "crons": [ + { + "path": "/api/cron/auto-complete-trips", + "schedule": "0 18 * * *" + }, + { + "path": "/api/cron/process-email-jobs", + "schedule": "*/5 * * * *" + } + ] +} +``` + +Vercel Cron otomatis kirim header `Authorization: Bearer ` — sesuaikan logic auth check di route handler (atau pakai env yang sama). Endpoint sudah platform-agnostic — tidak ada code change yang diperlukan. + +> **Catatan:** Vercel Cron free tier limit 2 cron/project + minimum schedule 1 jam. Untuk `process-email-jobs` yang 5 menit, perlu upgrade Vercel Pro atau pertahankan VPS untuk cron.