Files
2026-05-22 14:52:22 +07:00

9.4 KiB

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.
3 GET /api/cron/cleanup-trip-images 30 18 * * * Daily 01:30 WIB (18:30 UTC) Hapus file gambar trip yatim — foto yang di-upload di form create-trip tapi trip-nya batal dibuat. Hanya file >24 jam yang tak direferensikan TripImage.

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.


Setup di server (one-time)

1. Set CRON_SECRET di env production

Generate random secret 32 byte:

openssl rand -hex 32

Tambah ke file .env yang dibaca PM2 (atau yang pasti ter-load saat process boot):

CRON_SECRET="<hasil-openssl-tadi>"

Restart PM2 supaya proses re-load env:

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 <onboarding@resend.dev> (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 dengan instruksi setup masing-masing.

3. Daftarkan crontab

Edit crontab user yang punya akses ke env (biasanya user yang menjalankan PM2):

crontab -e

Tambah baris berikut (ganti https://your-domain.com dan <CRON_SECRET> sesuai env yang di-set di step 1):

# === Setrip cron jobs ===

# 1. Auto-complete trip + release payout (daily 01:00 WIB)
0 18 * * * curl -fsS -H "Authorization: Bearer <CRON_SECRET>" 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 <CRON_SECRET>" https://your-domain.com/api/cron/process-email-jobs >> /var/log/setrip-cron.log 2>&1

# 3. Bersihkan gambar trip yatim (daily 01:30 WIB)
30 18 * * * curl -fsS -H "Authorization: Bearer <CRON_SECRET>" https://your-domain.com/api/cron/cleanup-trip-images >> /var/log/setrip-cron.log 2>&1

Verifikasi crontab tersimpan:

crontab -l

4. Siapkan file log

sudo touch /var/log/setrip-cron.log
sudo chown $(whoami) /var/log/setrip-cron.log

Optional — logrotate supaya log tidak menggemuk:

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:

# 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}
cleanup-trip-images {"ok":true,"scanned":0,"deleted":0} {"ok":true,"scanned":12,"deleted":3}

Error response:

  • 401CRON_SECRET di env tidak match dengan header. Cek pm2 env <id>.
  • 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 — 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

tail -f /var/log/setrip-cron.log

PM2 log untuk console.log dari endpoint:

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/<name>/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 supaya muncul di health card.
  4. Brief ops: tambah baris di server crontab dengan schedule yang sesuai.

Pattern minimal cron handler:

// app/api/cron/<name>/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("<jobName>", 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:

{
  "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 <VERCEL_CRON_SECRET> — 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.