Compare commits
2 Commits
d5842b984b
...
43ea725107
| Author | SHA1 | Date | |
|---|---|---|---|
| 43ea725107 | |||
| 1200bf85c2 |
+170
-22
@@ -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 <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](../.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 `<CRON_SECRET>` sesuai env yang di-set di step 1):
|
||||
Tambah baris berikut (ganti `https://your-domain.com` dan `<CRON_SECRET>` 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 <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
|
||||
```
|
||||
|
||||
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 <id>`.
|
||||
**Error response:**
|
||||
- **401** — `CRON_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.
|
||||
|
||||
**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 <CRON_SECRET>`).
|
||||
---
|
||||
|
||||
## 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](../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/<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:
|
||||
|
||||
```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 <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.
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "setrip",
|
||||
"version": "0.16.0",
|
||||
"version": "0.16.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "setrip",
|
||||
"version": "0.16.0",
|
||||
"version": "0.16.1",
|
||||
"dependencies": {
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/adapter-pg": "^7.7.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "setrip",
|
||||
"version": "0.16.0",
|
||||
"version": "0.16.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in New Issue
Block a user