"use client"; import React, { useCallback, useEffect, useState } from "react"; import { RefreshCw } from "lucide-react"; import { useTranslations } from "../../../../i18n/provider"; /** * The outbound queue, read from the database. * * This screen used to be a fixture: two invented jobs ("job-101", * user@example.com -> partner@remotehost.com) held in component state, whose * Retry and Freeze buttons only edited that local array. It showed the same two * rows on every deployment, never displayed a real stuck message, and reported * success for actions that had not happened. /api/v1/admin/queue was already * serving the real thing. */ /** Mirrors the outbound_jobs row shape returned by the queue endpoint. */ interface QueueJob { id: string; envelope_from: string; envelope_to: string; destination_domain: string; status: string; attempt: number; next_attempt_at: string | null; last_smtp_code: number | null; last_error: string | null; } interface QueueSummary { by_status: Record; recent: QueueJob[]; } /** Only failure states earn a warning colour; the rest are ordinary progress. */ function statusClass(status: string): string { if (status === "BOUNCED" || status === "FROZEN") return "badge-danger"; if (status === "DEFERRED") return "badge-warning"; return "badge-verified"; } export default function AdminQueuePage() { const t = useTranslations(); const [summary, setSummary] = useState(null); const [message, setMessage] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const load = useCallback(async () => { setLoading(true); try { const res = await fetch("/api/v1/admin/queue"); if (!res.ok) throw new Error(); setSummary(await res.json()); setError(null); } catch { // Left null so the table renders empty rather than showing stale or // invented rows while the service is unreachable. setSummary(null); setError(t("errors.loadFailed")); } finally { setLoading(false); } }, [t]); useEffect(() => { void load(); }, [load]); /** * Retry and cancel are the two actions the service actually implements. * The old "Freeze" button named a third that has never existed anywhere in * the backend, so pressing it changed nothing but the colour of a cell. */ const act = async (id: string, action: "retry" | "cancel") => { setMessage(null); setError(null); const res = await fetch(`/api/v1/admin/queue/${encodeURIComponent(id)}/${action}`, { method: "POST", }); if (!res.ok) { setError(t("errors.serverError")); return; } setMessage(t(action === "retry" ? "admin.jobRetried" : "admin.jobCancelled", { id })); // Re-read rather than patching local state, so what is on screen is what // the queue now holds. await load(); }; const jobs = summary?.recent ?? []; return (

{t("admin.queueTitle")}

{t("admin.queueSubtitle")}

{/* Counts per status, straight from the by_status aggregate. */} {summary && Object.keys(summary.by_status).length > 0 && (
{Object.entries(summary.by_status).map(([status, count]) => ( {count} {status} ))}
)} {message && (
{message}
)} {error && (
{error}
)}
{t("admin.queueTitle")} ({jobs.length})
{jobs.length === 0 && !loading ? (
{t("admin.queueEmpty")}
) : (
{jobs.map((j) => ( ))}
{t("ui.jobId")} {t("ui.sender")} {t("ui.recipient")} {t("ui.destination")} {t("ui.attempts")} {t("ui.lastError")} {t("common.status")} {t("ui.actions")}
{j.id.slice(0, 8)} {j.envelope_from} {j.envelope_to} {j.destination_domain} {j.attempt} {j.last_error ? `${j.last_smtp_code ? `${j.last_smtp_code} ` : ""}${j.last_error}` : t("common.none")} {j.status}
)}
); }