Polish UI, harden company creation, and add error pages

This commit is contained in:
cesnimda
2026-03-23 19:34:29 +01:00
parent 8f5eab2fe4
commit fcafda6f52
38 changed files with 2293 additions and 1269 deletions
+62 -23
View File
@@ -7,10 +7,12 @@ import {
Chip,
Paper,
Stack,
TextField,
Typography,
} from "@mui/material";
import { api } from "../api";
import { useI18n } from "../i18n/I18nProvider";
type SummarizerMetrics = {
healthy: boolean;
@@ -111,10 +113,15 @@ function DetailRow({ label, value }: { label: string; value: React.ReactNode })
}
export default function AdminSystemPage() {
const { t } = useI18n();
const [status, setStatus] = useState<SystemStatus | null>(null);
const [loading, setLoading] = useState(false);
const [runningProbe, setRunningProbe] = useState(false);
const [error, setError] = useState<string | null>(null);
const [testEmailTo, setTestEmailTo] = useState("");
const [testEmailSubject, setTestEmailSubject] = useState("Jobbjakt SMTP test");
const [testEmailMessage, setTestEmailMessage] = useState("This is a test email from the Jobbjakt system panel.");
const [sendingTestEmail, setSendingTestEmail] = useState(false);
const load = async () => {
setLoading(true);
@@ -148,12 +155,27 @@ export default function AdminSystemPage() {
return "success" as const;
}, [status]);
const sendTestEmail = async () => {
setSendingTestEmail(true);
try {
await api.post("/users/send-test-email", {
toEmail: testEmailTo.trim() || null,
subject: testEmailSubject.trim() || null,
message: testEmailMessage.trim() || null,
});
} catch (e: any) {
setError(e?.response?.data || e?.message || "Failed to send test email.");
} finally {
setSendingTestEmail(false);
}
};
return (
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}>
<Box>
<Typography variant="h5" sx={{ fontWeight: 950 }}>System status</Typography>
<Typography sx={{ color: "text.secondary" }}>Production diagnostics for runtime, database, auth, email, and summarizer health.</Typography>
<Typography variant="h5" sx={{ fontWeight: 950 }}>{t("adminSystemTitle")}</Typography>
<Typography sx={{ color: "text.secondary" }}>{t("adminSystemSubtitle")}</Typography>
</Box>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
<Button
@@ -172,10 +194,10 @@ export default function AdminSystemPage() {
}}
disabled={loading || runningProbe}
>
{runningProbe ? "Running probe..." : "Run probe now"}
{runningProbe ? t("adminSystemRunningProbe") : t("adminSystemRunProbe")}
</Button>
<Button variant="contained" onClick={() => void load()} disabled={loading}>
{loading ? "Refreshing..." : "Refresh"}
{loading ? t("adminSystemRefreshing") : t("adminSystemRefresh")}
</Button>
</Box>
</Box>
@@ -186,37 +208,37 @@ export default function AdminSystemPage() {
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "repeat(4, 1fr)" }, gap: 2 }}>
<SummaryCard
title="Environment"
title={t("adminSystemEnvironment")}
value={status?.environment ?? "-"}
subtitle={`Version ${displayMetadata(status?.version)} · Commit ${displayMetadata(status?.commitSha)}`}
/>
<SummaryCard
title="Database"
value={status ? (status.database.canConnect ? "Connected" : "Offline") : "-"}
title={t("adminSystemDatabase")}
value={status ? (status.database.canConnect ? t("adminSystemConnected") : t("adminSystemOffline")) : "-"}
subtitle={status ? `${status.database.provider} · ${status.database.target || "No target"}` : "-"}
tone={dbTone}
/>
<SummaryCard
title="SMTP"
value={status?.email.enabled ? "Enabled" : "Disabled"}
subtitle={status?.email.host || "No SMTP host configured"}
title={t("adminSystemSmtp")}
value={status?.email.enabled ? t("adminSystemEnabled") : t("adminSystemDisabled")}
subtitle={status?.email.host || t("adminSystemNoSmtpHost")}
tone={status?.email.enabled ? "success" : "default"}
/>
<SummaryCard
title="Summarizer"
value={status?.summarizer.healthy ? "Healthy" : "Offline"}
title={t("adminSystemSummarizer")}
value={status?.summarizer.healthy ? t("adminSystemHealthy") : t("adminSystemOffline")}
subtitle={status?.summarizer.probeLatencyMs != null
? `${status.summarizer.probeLatencyMs} ms probe · ${status.summarizer.device || "unknown device"}`
: status?.summarizer.healthLatencyMs != null
? `${status.summarizer.healthLatencyMs} ms health · ${status.summarizer.device || "unknown device"}`
: "No latency data"}
: t("adminSystemNoLatencyData")}
tone={summarizerTone}
/>
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", lg: "1.2fr 1fr" }, gap: 2 }}>
<Paper sx={{ p: 2, borderRadius: 3 }}>
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>Database and storage</Typography>
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemDatabaseStorage")}</Typography>
<Stack spacing={0.75}>
<DetailRow label="Provider" value={status?.database.provider || "-"} />
<DetailRow label="Target" value={status?.database.target || "-"} />
@@ -234,7 +256,7 @@ export default function AdminSystemPage() {
</Paper>
<Paper sx={{ p: 2, borderRadius: 3 }}>
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>Runtime and auth</Typography>
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemRuntimeAuth")}</Typography>
<Stack spacing={0.75}>
<DetailRow label="Framework" value={status?.runtime.framework || "-"} />
<DetailRow label="OS" value={status?.runtime.osDescription || "-"} />
@@ -252,7 +274,7 @@ export default function AdminSystemPage() {
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", lg: "1fr 1fr" }, gap: 2 }}>
<Paper sx={{ p: 2, borderRadius: 3 }}>
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>Email configuration</Typography>
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemEmailConfig")}</Typography>
<Stack spacing={0.75}>
<DetailRow label="Enabled" value={status?.email.enabled ? "Yes" : "No"} />
<DetailRow label="From" value={status?.email.from || "-"} />
@@ -264,7 +286,7 @@ export default function AdminSystemPage() {
</Paper>
<Paper sx={{ p: 2, borderRadius: 3 }}>
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>Summarizer runtime</Typography>
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemSummarizerRuntime")}</Typography>
<Stack spacing={0.75}>
<DetailRow label="Model" value={status?.summarizer.model || "-"} />
<DetailRow label="Device" value={status?.summarizer.device || "-"} />
@@ -280,7 +302,24 @@ export default function AdminSystemPage() {
</Box>
<Paper sx={{ p: 2, borderRadius: 3 }}>
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>Summarizer telemetry</Typography>
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemSmtpTest")}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>
{t("adminSystemSmtpTestBody")}
</Typography>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
<TextField label={t("adminSystemRecipientEmail")} value={testEmailTo} onChange={(e) => setTestEmailTo(e.target.value)} placeholder={t("adminSystemRecipientPlaceholder")} />
<TextField label={t("adminSystemSubject")} value={testEmailSubject} onChange={(e) => setTestEmailSubject(e.target.value)} />
<TextField label={t("adminSystemMessage")} multiline minRows={3} value={testEmailMessage} onChange={(e) => setTestEmailMessage(e.target.value)} sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }} />
</Box>
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 1.5 }}>
<Button variant="contained" disabled={sendingTestEmail} onClick={() => void sendTestEmail()}>
{sendingTestEmail ? t("adminSystemSending") : t("adminSystemSendTestEmail")}
</Button>
</Box>
</Paper>
<Paper sx={{ p: 2, borderRadius: 3 }}>
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemSummarizerTelemetry")}</Typography>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr 1fr", md: "repeat(6, 1fr)" }, gap: 2 }}>
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Requests</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.summarizer.requests ?? 0}</Typography></Box>
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Cache hits</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.summarizer.cacheHits ?? 0}</Typography></Box>
@@ -290,11 +329,11 @@ export default function AdminSystemPage() {
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Avg latency</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.summarizer.averageLatencyMs != null ? `${status.summarizer.averageLatencyMs} ms` : "-"}</Typography></Box>
</Box>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 2 }}>
<Chip label={status?.database.canConnect ? "Database connected" : "Database issue"} color={status?.database.canConnect ? "success" : "error"} size="small" />
<Chip label={status?.auth.required ? "Auth enforced" : "Auth optional"} color={status?.auth.required ? "success" : "warning"} size="small" />
<Chip label={status?.auth.googleConfigured ? "Google sign-in ready" : "Google sign-in off"} variant="outlined" size="small" />
<Chip label={status?.auth.gmailConfigured ? "Gmail ready" : "Gmail incomplete"} variant="outlined" size="small" />
<Chip label={status?.summarizer.gpuAvailable ? "GPU visible" : "CPU mode"} color={status?.summarizer.gpuAvailable ? "success" : "default"} size="small" />
<Chip label={status?.database.canConnect ? t("adminSystemDatabaseConnected") : t("adminSystemDatabaseIssue")} color={status?.database.canConnect ? "success" : "error"} size="small" />
<Chip label={status?.auth.required ? t("adminSystemAuthEnforced") : t("adminSystemAuthOptional")} color={status?.auth.required ? "success" : "warning"} size="small" />
<Chip label={status?.auth.googleConfigured ? t("adminSystemGoogleReady") : t("adminSystemGoogleOff")} variant="outlined" size="small" />
<Chip label={status?.auth.gmailConfigured ? t("adminSystemGmailReady") : t("adminSystemGmailIncomplete")} variant="outlined" size="small" />
<Chip label={status?.summarizer.gpuAvailable ? t("adminSystemGpuVisible") : t("adminSystemCpuMode")} color={status?.summarizer.gpuAvailable ? "success" : "default"} size="small" />
</Box>
</Paper>
</Box>