# Cron Setup (PM2 / self-hosted Linux) 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 | 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. | 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 Generate random secret 32 byte: ```bash openssl rand -hex 32 ``` Tambah ke file `.env` yang dibaca PM2 (atau yang pasti ter-load saat process boot): ```bash CRON_SECRET="" ``` Restart PM2 supaya proses re-load env: ```bash pm2 restart setrip --update-env ``` ### 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): ```bash crontab -e ``` Tambah baris berikut (ganti `https://your-domain.com` dan `` sesuai env yang di-set di step 1): ```cron # === 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: ```bash crontab -l ``` ### 4. Siapkan file log ```bash sudo touch /var/log/setrip-cron.log sudo chown $(whoami) /var/log/setrip-cron.log ``` 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 per cron:** | 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}` | **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. --- ## Monitoring ### 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 ``` 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 `/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` + semua baris crontab, restart PM2. **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. --- ## 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.