fix email sender all flow

This commit is contained in:
2026-05-20 15:25:32 +07:00
parent 306396ae43
commit cb03967deb
20 changed files with 1450 additions and 62 deletions
+80 -2
View File
@@ -5,6 +5,7 @@ import { authOptions } from "@/lib/auth";
import { isAdminEmail } from "@/lib/admin";
import { prisma } from "@/lib/prisma";
import { systemHealthService } from "@/server/services/system-health.service";
import { emailRepo } from "@/server/repositories/email.repo";
interface JobSummary {
jobName: string;
@@ -80,20 +81,22 @@ export default async function AdminSystemPage() {
);
}
const [summaries, recentRuns, stale] = await Promise.all([
const [summaries, recentRuns, stale, emailStats] = await Promise.all([
Promise.all(TRACKED_JOBS.map(getJobSummary)),
prisma.cronRun.findMany({
orderBy: { startedAt: "desc" },
take: 20,
}),
systemHealthService.detectStale(),
emailRepo.stats(),
]);
const hasAnyStale =
stale.stalePaymentsCount > 0 ||
stale.awaitingPayPastDepartureCount > 0 ||
stale.overduePayoutsCount > 0 ||
stale.stuckRefundsCount > 0;
stale.stuckRefundsCount > 0 ||
emailStats.deadLetter > 0;
return (
<div className="mx-auto max-w-5xl px-4 py-8 sm:py-12">
@@ -152,6 +155,19 @@ export default async function AdminSystemPage() {
</Link>
</li>
)}
{emailStats.deadLetter > 0 && (
<li>
<strong>{emailStats.deadLetter}</strong> email gagal kirim &
sudah habis 5 attempt cron berhenti retry, perlu retry
manual.{" "}
<Link
href="/admin/emails?tab=failed"
className="font-semibold text-amber-700 hover:underline"
>
Lihat email gagal
</Link>
</li>
)}
</ul>
</section>
)}
@@ -236,6 +252,37 @@ export default async function AdminSystemPage() {
</div>
</section>
<section className="mb-8">
<h2 className="mb-3 text-xs font-bold uppercase tracking-wide text-neutral-500">
Email
</h2>
<div className="grid gap-3 sm:grid-cols-3">
<EmailStat
label="Antri dikirim"
value={emailStats.queued}
tone={emailStats.queued > 0 ? "amber" : "ok"}
/>
<EmailStat
label="Gagal 24 jam"
value={emailStats.failed24h}
tone={emailStats.failed24h > 0 ? "red" : "ok"}
/>
<EmailStat
label="Perlu aksi manual"
value={emailStats.deadLetter}
tone={emailStats.deadLetter > 0 ? "red" : "ok"}
/>
</div>
<p className="mt-2 text-xs text-neutral-500">
<Link
href="/admin/emails"
className="font-semibold text-primary-600 hover:underline"
>
Buka Email Log
</Link>
</p>
</section>
<section>
<h2 className="mb-3 text-xs font-bold uppercase tracking-wide text-neutral-500">
Recent Runs (20 terakhir)
@@ -301,6 +348,37 @@ function truncate(s: string, max: number): string {
return s.length > max ? `${s.slice(0, max)}` : s;
}
function EmailStat({
label,
value,
tone,
}: {
label: string;
value: number;
tone: "ok" | "amber" | "red";
}) {
const cls =
tone === "red"
? "border-red-200 bg-red-50/60"
: tone === "amber"
? "border-amber-200 bg-amber-50/60"
: "border-emerald-200 bg-emerald-50/50";
const valueCls =
tone === "red"
? "text-red-700"
: tone === "amber"
? "text-amber-700"
: "text-emerald-700";
return (
<div className={`rounded-2xl border p-4 shadow-sm ${cls}`}>
<p className="text-[10px] font-semibold uppercase tracking-wide text-neutral-500">
{label}
</p>
<p className={`mt-1 text-2xl font-bold ${valueCls}`}>{value}</p>
</div>
);
}
function StatusBadge({ value }: { value: string }) {
const cls =
value === "SUCCESS"