Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
127 lines
4.3 KiB
TypeScript
127 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
import { useActionState } from "react";
|
|
import { Mail } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Pill } from "@/components/mockup-ui";
|
|
import { sendTestMail, type TestMailState } from "@/server/actions/mail";
|
|
|
|
/**
|
|
* SEC1 §9 — Betriebsanzeige der Mail-Strecke + Testversand.
|
|
*
|
|
* Der Testversand geht immer an die eigene Adresse des angemeldeten
|
|
* Plattform-Admins (siehe src/server/actions/mail.ts) — deshalb gibt es hier
|
|
* bewusst kein Empfängerfeld.
|
|
*/
|
|
|
|
type MailStatus = {
|
|
configured: boolean;
|
|
reason?: string;
|
|
host?: string;
|
|
from?: string;
|
|
queue: "bullmq" | "inline";
|
|
recent: {
|
|
id: string;
|
|
to: string;
|
|
template: string;
|
|
status: string;
|
|
error: string | null;
|
|
createdAt: Date;
|
|
}[];
|
|
};
|
|
|
|
const STATUS_TONE: Record<string, "ok" | "warn" | "risk" | "mut"> = {
|
|
sent: "ok",
|
|
pending: "warn",
|
|
failed: "risk",
|
|
bounced: "risk",
|
|
suppressed: "mut",
|
|
};
|
|
|
|
export function MailStatusPanel({ status }: { status: MailStatus }) {
|
|
const [state, action, pending] = useActionState<TestMailState, FormData>(
|
|
async () => sendTestMail(),
|
|
{ status: "idle" },
|
|
);
|
|
|
|
return (
|
|
<div className="shadow-card mt-5 rounded-xl border bg-card p-5">
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<p className="font-heading text-sm font-semibold">E-Mail-Versand</p>
|
|
<p className="mt-0.5 text-[12px] text-muted-foreground">
|
|
{status.configured
|
|
? `SMTP ${status.host} · Absender ${status.from} · ${
|
|
status.queue === "bullmq"
|
|
? "asynchron über die Queue (Worker)"
|
|
: "inline (kein Redis konfiguriert — kein Retry)"
|
|
}`
|
|
: (status.reason ?? "SMTP ist nicht konfiguriert.")}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Pill tone={status.configured ? "ok" : "warn"}>
|
|
{status.configured ? "Konfiguriert" : "Nicht konfiguriert"}
|
|
</Pill>
|
|
<form action={action}>
|
|
<Button type="submit" variant="outline" size="sm" disabled={!status.configured || pending}>
|
|
<Mail className="size-4" /> {pending ? "Sende…" : "Test-Mail senden"}
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{state.status !== "idle" && (
|
|
<p
|
|
role="status"
|
|
className={`mt-3 rounded-lg px-3 py-2 text-[13px] ${
|
|
state.status === "ok"
|
|
? "bg-[rgba(57,192,127,0.14)] text-[var(--ok)]"
|
|
: "bg-[rgba(255,107,107,0.16)] text-[var(--risk)]"
|
|
}`}
|
|
>
|
|
{state.message}
|
|
</p>
|
|
)}
|
|
|
|
{status.recent.length > 0 && (
|
|
<div className="mt-4 overflow-x-auto">
|
|
<table className="w-full text-[12.5px]">
|
|
<thead className="text-muted-foreground">
|
|
<tr className="text-left">
|
|
<th className="py-1.5 pr-3 font-semibold">Zeitpunkt</th>
|
|
<th className="py-1.5 pr-3 font-semibold">Empfänger</th>
|
|
<th className="py-1.5 pr-3 font-semibold">Template</th>
|
|
<th className="py-1.5 pr-3 font-semibold">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{status.recent.map((row) => (
|
|
<tr key={row.id} className="border-t border-[var(--panel-brd)]">
|
|
<td className="py-1.5 pr-3 whitespace-nowrap">
|
|
{new Intl.DateTimeFormat("de-DE", {
|
|
dateStyle: "short",
|
|
timeStyle: "short",
|
|
}).format(new Date(row.createdAt))}
|
|
</td>
|
|
<td className="py-1.5 pr-3">{row.to}</td>
|
|
<td className="py-1.5 pr-3 font-mono text-[11.5px]">{row.template}</td>
|
|
<td className="py-1.5 pr-3">
|
|
<Pill tone={STATUS_TONE[row.status] ?? "mut"}>{row.status}</Pill>
|
|
{row.error && (
|
|
<span className="ml-2 text-[11px] text-muted-foreground" title={row.error}>
|
|
{row.error.slice(0, 60)}
|
|
{row.error.length > 60 ? "…" : ""}
|
|
</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|