import Link from "next/link"; import { redirect } from "next/navigation"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth"; import { isAdminEmail } from "@/lib/admin"; import { prisma } from "@/lib/prisma"; import { systemHealthService } from "@/server/services/system-health.service"; interface JobSummary { jobName: string; lastRun: { at: Date; status: string; errorMessage: string | null } | null; lastSuccess: Date | null; totalRuns7d: number; failedRuns7d: number; } async function getJobSummary(jobName: string): Promise { const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); const [lastRun, lastSuccessRow, totalRuns7d, failedRuns7d] = await Promise.all([ prisma.cronRun.findFirst({ where: { jobName }, orderBy: { startedAt: "desc" }, select: { startedAt: true, status: true, errorMessage: true }, }), prisma.cronRun.findFirst({ where: { jobName, status: "SUCCESS" }, orderBy: { startedAt: "desc" }, select: { startedAt: true }, }), prisma.cronRun.count({ where: { jobName, startedAt: { gte: sevenDaysAgo } }, }), prisma.cronRun.count({ where: { jobName, status: "FAILED", startedAt: { gte: sevenDaysAgo }, }, }), ]); return { jobName, lastRun: lastRun ? { at: lastRun.startedAt, status: lastRun.status, errorMessage: lastRun.errorMessage, } : null, lastSuccess: lastSuccessRow?.startedAt ?? null, totalRuns7d, failedRuns7d, }; } // Daftar cron yang dipantau. Tambah entry baru saat menambah cron route handler. const TRACKED_JOBS = ["auto-complete-trips"] as const; function healthOf(summary: JobSummary): "ok" | "stale" | "failed" { if (summary.lastRun?.status === "FAILED") return "failed"; if (!summary.lastSuccess) return "stale"; const hoursSince = (Date.now() - summary.lastSuccess.getTime()) / (1000 * 60 * 60); // Asumsi cron daily — > 25 jam dianggap stale. if (hoursSince > 25) return "stale"; return "ok"; } export default async function AdminSystemPage() { const session = await getServerSession(authOptions); if (!session?.user) redirect("/login?callbackUrl=/admin/system"); if (!isAdminEmail(session.user.email)) { return (

Halaman ini hanya untuk admin SeTrip.

); } const [summaries, recentRuns, stale] = await Promise.all([ Promise.all(TRACKED_JOBS.map(getJobSummary)), prisma.cronRun.findMany({ orderBy: { startedAt: "desc" }, take: 20, }), systemHealthService.detectStale(), ]); const hasAnyStale = stale.stalePaymentsCount > 0 || stale.awaitingPayPastDepartureCount > 0 || stale.overduePayoutsCount > 0 || stale.stuckRefundsCount > 0; return (

System Health

Status cron job otomatis. Refresh halaman ini setelah trigger cron manual atau saat investigasi.

{hasAnyStale && (

⚠️ Stale State Alerts

    {stale.stalePaymentsCount > 0 && (
  • {stale.stalePaymentsCount} Payment MIDTRANS AWAITING > 25 jam — webhook mungkin tertunda. Cek manual lalu reconcile.
  • )} {stale.awaitingPayPastDepartureCount > 0 && (
  • {stale.awaitingPayPastDepartureCount} Booking AWAITING_PAY tapi trip sudah lewat tanggal berangkat — peserta lupa bayar, butuh cleanup.
  • )} {stale.overduePayoutsCount > 0 && (
  • {stale.overduePayoutsCount} Payout HELD lewat heldUntil > 1 hari — cron release mungkin tidak jalan, cek cron history di bawah.{" "} Lihat HELD →
  • )} {stale.stuckRefundsCount > 0 && (
  • {stale.stuckRefundsCount} Refund APPROVED > 7 hari belum di-process.{" "} Lihat APPROVED →
  • )}
)}

Cron Jobs

{summaries.map((s) => { const health = healthOf(s); const cls = health === "ok" ? "border-emerald-200 bg-emerald-50/50" : health === "stale" ? "border-amber-200 bg-amber-50/50" : "border-red-200 bg-red-50/50"; const badge = health === "ok" ? { label: "🟢 OK", cls: "bg-emerald-100 text-emerald-800" } : health === "stale" ? { label: "🟡 STALE", cls: "bg-amber-100 text-amber-800", } : { label: "🔴 FAILED", cls: "bg-red-100 text-red-800", }; return (

Job

{s.jobName}

{badge.label}
Last run:
{" "}
{s.lastRun ? `${formatDateTime(s.lastRun.at)} · ${s.lastRun.status}` : "Belum pernah"}
Last success:
{" "}
{s.lastSuccess ? formatDateTime(s.lastSuccess) : "Belum pernah"}
7 hari terakhir:
{" "}
{s.totalRuns7d} run, {s.failedRuns7d} failed
{s.lastRun?.errorMessage && (
Error terakhir: {s.lastRun.errorMessage}
)}
); })}

Recent Runs (20 terakhir)

{recentRuns.length === 0 ? (

Belum ada cron run tercatat. Setelah cron berikutnya jalan, baris pertama akan muncul di sini.

) : (
{recentRuns.map((r) => ( ))}
Job Started Finished Status Note
{r.jobName} {formatDateTime(r.startedAt)} {r.finishedAt ? formatDateTime(r.finishedAt) : "—"} {r.errorMessage ?? (r.payload ? truncate(JSON.stringify(r.payload), 80) : "—")}
)}
); } function formatDateTime(d: Date): string { return d.toLocaleString("id-ID", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", }); } function truncate(s: string, max: number): string { return s.length > max ? `${s.slice(0, max)}…` : s; } function StatusBadge({ value }: { value: string }) { const cls = value === "SUCCESS" ? "bg-emerald-100 text-emerald-800" : value === "FAILED" ? "bg-red-100 text-red-800" : "bg-amber-100 text-amber-800"; return ( {value} ); }