Basis: Certvia dev@a48c5fb als Fundament für Craftvia
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s

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:
2026-09-14 11:05:39 +02:00
co-authored by Claude Opus 5
commit c8e6f30a27
720 changed files with 140143 additions and 0 deletions
+126
View File
@@ -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>
);
}