fix: Crash-Robustheit + Performance in Backend und Frontend härten
Backend: recover() in allen langlebigen Goroutinen (neues internal/safego- Paket), MIME-Multipart-Tiefenlimit gegen Stack-Overflow, IMAP-Zeilenlängen- und FETCH-Result-Limits gegen OOM, MBOX-Buffer-Aliasing-Bug (Datenkorruption beim Import), ungeprüfte Type Assertions abgesichert, Data Race im API-Key-Rate-Limiter behoben, SMTP-Session-Panic führt jetzt zu 451-Retry statt Prozessabsturz. Performance: O(n²)-String-Concat in Mailparser und IMAP-Parser durch strings.Builder ersetzt. Frontend: Error Boundaries für Root und Mail-Detailansicht ergänzt (gab es vorher nicht), zahlreiche Guards gegen nil-Slices aus dem Backend-JSON die sonst .map()/.length-Crashes/White-Screens auslösten, defekte JSON-Antworten in api/core.ts abgefangen, zwei React-Key-Bugs bei löschbaren Listen korrigiert. Verifiziert auf 192.168.1.132: Build und Tests der geänderten Pakete fehlerfrei, keine Regressionen gegenüber vorbestehendem Stand. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0fa4fad49d
commit
798cb2817c
@@ -103,9 +103,9 @@ export function ArchivingRulesTab() {
|
||||
setError("");
|
||||
Promise.all([getArchivingRules(), getTenants()])
|
||||
.then(([data, ts]) => {
|
||||
setRules(data.rules);
|
||||
setMinRetentionDays(data.min_retention_days);
|
||||
setTenants(ts);
|
||||
setRules(Array.isArray(data?.rules) ? data.rules : []);
|
||||
setMinRetentionDays(data?.min_retention_days ?? 0);
|
||||
setTenants(Array.isArray(ts) ? ts : []);
|
||||
})
|
||||
.catch(() => setError("Archivierungsregeln konnten nicht geladen werden"))
|
||||
.finally(() => setLoading(false));
|
||||
|
||||
@@ -80,9 +80,9 @@ function MailTimeseriesChart({ points }: { points: TimeseriesPoint[] }) {
|
||||
})}
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-muted-foreground font-mono">
|
||||
<span>{points[0]?.day.slice(5)}</span>
|
||||
<span>{points[Math.floor(points.length / 2)]?.day.slice(5)}</span>
|
||||
<span>{points[points.length - 1]?.day.slice(5)}</span>
|
||||
<span>{points[0]?.day?.slice(5)}</span>
|
||||
<span>{points[Math.floor(points.length / 2)]?.day?.slice(5)}</span>
|
||||
<span>{points[points.length - 1]?.day?.slice(5)}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -436,7 +436,7 @@ export function DashboardTab({
|
||||
)}
|
||||
|
||||
{/* Festplatten */}
|
||||
{systemStats.disks.length > 0 && (
|
||||
{Array.isArray(systemStats.disks) && systemStats.disks.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">Festplatten</h3>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
||||
@@ -135,7 +135,9 @@ export function ReconciliationCard() {
|
||||
|
||||
// Tages-Header aus dem ersten Quell-Eintrag ableiten (alle Quellen haben
|
||||
// dieselben Tage in gleicher Reihenfolge).
|
||||
const dayHeaders = data?.sources[0]?.points.map((p) => p.date) ?? [];
|
||||
// Backend liefert leere Listen ggf. als null — defensiv normalisieren.
|
||||
const sources = Array.isArray(data?.sources) ? data.sources : [];
|
||||
const dayHeaders = sources[0]?.points?.map((p) => p.date) ?? [];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@@ -184,7 +186,7 @@ export function ReconciliationCard() {
|
||||
Vollständigkeits-Report konnte nicht geladen werden: {error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : !data || data.sources.length === 0 ? (
|
||||
) : !data || sources.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Noch keine Reconciliation-Daten vorhanden. Der tägliche Zähl-Job
|
||||
(<code className="font-mono">archivmail reconcile</code>) hat noch
|
||||
@@ -213,7 +215,7 @@ export function ReconciliationCard() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.sources.map((s) => {
|
||||
{sources.map((s) => {
|
||||
const isImap = s.source_type === "imap";
|
||||
return (
|
||||
<TableRow key={s.source_key}>
|
||||
@@ -229,7 +231,7 @@ export function ReconciliationCard() {
|
||||
</TableCell>
|
||||
) : (
|
||||
<>
|
||||
{s.points.map((p) => (
|
||||
{(s.points ?? []).map((p) => (
|
||||
<TableCell key={p.date} className="text-right">
|
||||
<DayCell
|
||||
archived={p.archived_count}
|
||||
|
||||
@@ -121,8 +121,8 @@ export function RoutingRulesTab({ isSuperAdmin }: { isSuperAdmin: boolean }) {
|
||||
];
|
||||
Promise.all(loaders)
|
||||
.then(([rs, ts]) => {
|
||||
setRules(rs);
|
||||
setTenants(ts);
|
||||
setRules(Array.isArray(rs) ? rs : []);
|
||||
setTenants(Array.isArray(ts) ? ts : []);
|
||||
})
|
||||
.catch(() => setError("Routing-Regeln konnten nicht geladen werden"))
|
||||
.finally(() => setLoading(false));
|
||||
|
||||
@@ -77,12 +77,17 @@ export function SecurityTab({
|
||||
"SSH Root-Login": { action: "fix_ssh_root_login", label: "Auf prohibit-password setzen" },
|
||||
};
|
||||
|
||||
// Backend kann bei leerem Ergebnis null statt [] liefern.
|
||||
const checks: SecurityCheck[] = Array.isArray(securityAudit.checks)
|
||||
? securityAudit.checks
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{securityAudit.checks.map((check: SecurityCheck, i: number) => {
|
||||
{checks.map((check: SecurityCheck, i: number) => {
|
||||
const fix = check.status !== "ok" ? fixActions[check.name] : undefined;
|
||||
return (
|
||||
<Card key={i}>
|
||||
<Card key={check.name || i}>
|
||||
<CardContent className="p-4 flex items-start gap-3">
|
||||
<span className={`mt-1 h-2.5 w-2.5 flex-shrink-0 rounded-full ${
|
||||
check.status === "ok" ? "bg-green-500" :
|
||||
@@ -130,9 +135,9 @@ export function SecurityTab({
|
||||
{/* Summary */}
|
||||
<div className="mt-4 grid grid-cols-3 gap-3 text-center text-sm">
|
||||
{[
|
||||
{ label: "OK", color: "bg-green-50 text-green-700", count: securityAudit.checks.filter((c: SecurityCheck) => c.status === "ok").length },
|
||||
{ label: "Warnungen", color: "bg-yellow-50 text-yellow-700", count: securityAudit.checks.filter((c: SecurityCheck) => c.status === "warning").length },
|
||||
{ label: "Fehler", color: "bg-red-50 text-red-700", count: securityAudit.checks.filter((c: SecurityCheck) => c.status === "error").length },
|
||||
{ label: "OK", color: "bg-green-50 text-green-700", count: checks.filter((c: SecurityCheck) => c.status === "ok").length },
|
||||
{ label: "Warnungen", color: "bg-yellow-50 text-yellow-700", count: checks.filter((c: SecurityCheck) => c.status === "warning").length },
|
||||
{ label: "Fehler", color: "bg-red-50 text-red-700", count: checks.filter((c: SecurityCheck) => c.status === "error").length },
|
||||
].map((s) => (
|
||||
<div key={s.label} className={`rounded p-3 ${s.color}`}>
|
||||
<p className="text-2xl font-bold">{s.count}</p>
|
||||
|
||||
@@ -46,8 +46,9 @@ export function RestoreMailButton({ mailId, enabled }: RestoreMailButtonProps) {
|
||||
getImapAccounts()
|
||||
.then((data) => {
|
||||
if (!active) return;
|
||||
setAccounts(data);
|
||||
if (data.length > 0) setSelected(String(data[0].id));
|
||||
const list = Array.isArray(data) ? data : [];
|
||||
setAccounts(list);
|
||||
if (list.length > 0) setSelected(String(list[0].id));
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore — without accounts the button stays hidden.
|
||||
|
||||
@@ -162,7 +162,7 @@ export function SearchFilterBar(props: SearchFilterBarProps) {
|
||||
type="button"
|
||||
className="flex-1 text-left text-sm truncate"
|
||||
onClick={() => onApplySavedSearch(s)}
|
||||
title={Object.entries(s.query).map(([k, v]) => `${k}: ${v}`).join(", ")}
|
||||
title={Object.entries(s.query ?? {}).map(([k, v]) => `${k}: ${v}`).join(", ")}
|
||||
>
|
||||
{s.name}
|
||||
</button>
|
||||
|
||||
@@ -33,7 +33,7 @@ const MATCH_FIELD_LABEL: Record<SearchMatchField, string> = {
|
||||
function MatchSourceBadge({ field }: { field: SearchMatchField }) {
|
||||
return (
|
||||
<span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground whitespace-nowrap">
|
||||
{MATCH_FIELD_LABEL[field]}
|
||||
{MATCH_FIELD_LABEL[field] ?? field}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user