Polish UI, harden company creation, and add error pages
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user