Basis: Certvia dev@a48c5fb als Fundament für Craftvia
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>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user