fix(admin): consolidate route headings and descriptions

This commit is contained in:
cesnimda
2026-11-09 22:16:59 +01:00
parent e585561a02
commit c7e8627ca7
8 changed files with 41 additions and 42 deletions
+4
View File
@@ -161,6 +161,10 @@ function titleFor(path: string, t: (k: any) => string): string {
}
function subtitleFor(path: string, t: (k: any) => string): string | undefined {
if (path.startsWith("/admin/audit")) return t("adminAuditSubtitle");
if (path.startsWith("/admin/users")) return t("adminUsersSubtitle");
if (path.startsWith("/admin/system")) return t("adminSystemSubtitle");
if (path.startsWith("/admin/processing")) return t("processingSubtitle");
if (/^\/settings\/?$/.test(path)) return t("settingsPageDescription");
if (path === "/dashboard") return t("dashboardPageSubtitle");
if (path.startsWith("/analytics")) return t("analyticsPageSubtitle");
+3 -3
View File
@@ -37,8 +37,8 @@ test("[P7-10 audit] retains shared heading hierarchy and a named keyboard scroll
mockedApi.get.mockResolvedValue({ data: [row] } as any);
renderPage();
await screen.findByRole("button", { name: "Undo" });
expect(screen.getByRole("heading", { name: "Audit log", level: 2 })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: translations.en.adminAuditEvents, level: 3 })).toBeInTheDocument();
expect(screen.queryByRole("heading", { name: /^Audit log$/ })).not.toBeInTheDocument();
expect(screen.getByRole("heading", { name: translations.en.adminAuditEvents, level: 2 })).toBeInTheDocument();
const region = screen.getByRole("region", { name: "Audit log" });
expect(region).toHaveAttribute("tabindex", "0");
expect(within(region).getByRole("table")).toHaveAccessibleName("Audit log");
@@ -104,7 +104,7 @@ test("distinguishes an unavailable audit log from an empty audit log and retries
mockedApi.get.mockRejectedValueOnce(new Error("offline"));
renderPage();
expect(screen.getByRole("heading", { name: "Audit log", level: 2 })).toBeInTheDocument();
expect(screen.getByRole("region", { name: /^Audit log$/ })).toBeInTheDocument();
expect(await screen.findByText("Unable to load audit events")).toBeInTheDocument();
expect(screen.queryByText("No events.")).not.toBeInTheDocument();
@@ -176,7 +176,8 @@ describe('AdminSystemPage', () => {
it('uses contextual system headings and keeps metric values out of heading navigation', async () => {
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
await screen.findByText('Production');
expect(screen.getByRole('heading', { name: 'System status', level: 2 })).toBeInTheDocument();
expect(screen.getByRole('region', { name: 'System status' })).toBeInTheDocument();
expect(screen.queryByRole('heading', { name: 'System status' })).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: 'Production' })).not.toBeInTheDocument();
const levels = screen.getAllByRole('heading').map(heading => Number(heading.tagName.slice(1)));
let previous = 1;
+1 -1
View File
@@ -96,7 +96,7 @@ test('disposed create acknowledgement cannot start a new private list read', asy
test("requires explicit confirmation before removing your own admin role", async () => {
renderPage([{ id: "me", email: "me@example.com", userName: "me", roles: ["Admin"], emailConfirmed: true, isCurrentUser: true, canRemoveAdmin: true }]);
expect(screen.getByRole("heading", { name: "Users", level: 2 })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Create user", level: 2 })).toBeInTheDocument();
fireEvent.click(await screen.findByRole("button", { name: "Remove admin" }));
expect(await screen.findByText(/you will immediately lose access/i)).toBeInTheDocument();
+2 -3
View File
@@ -18,7 +18,7 @@ import { useToast } from "../toast";
import { useI18n } from "../i18n/I18nProvider";
import { useViewResource } from "../hooks/useViewResource";
import ViewStateNotice from "../components/ViewStateNotice";
import { PageHeader, PageSection } from "../layout/PageLayout";
import { PageSection } from "../layout/PageLayout";
import { checkSessionCookie, getSessionContext, isSessionInvalidated } from "../sessionBoundary";
type AuditItem = {
@@ -86,8 +86,7 @@ export default function AdminAuditPage() {
return (
<Box sx={{ display: "grid", gap: "var(--app-section-gap)", minWidth: 0 }}>
<PageHeader level="h2" title={t("adminAuditTitle")} description={t("adminAuditSubtitle")} />
<PageSection level="h3" title={t("adminAuditEvents")}>
<PageSection title={t("adminAuditEvents")}>
<Box ref={body} role="region" aria-label={t("adminAuditTitle")} tabIndex={0} aria-busy={busyId!==null || auditResource.loading || auditResource.refreshing} sx={{ minWidth: 0, overflow: "auto" }}>
<ViewStateNotice
@@ -6,7 +6,7 @@ import { AUTH_USER_CHANGED_EVENT, getAuthUserKey } from '../auth';
import { isSessionInvalidated, SESSION_INVALIDATED_EVENT } from '../sessionBoundary';
import { useI18n } from '../i18n/I18nProvider';
import type { TranslationKey } from '../i18n/translations';
import { PageHeader, PageSection } from '../layout/PageLayout';
import { PageSection } from '../layout/PageLayout';
type Item = {
id: string; owner: { id: string; name: string | null; email: string | null }; taskType: string; status: string; stage: string | null;
@@ -139,7 +139,6 @@ export default function AdminProcessingPage() {
const resume = () => { pausedRef.current = false; setPaused(false); setBefore(null); setRefresh(value => value + 1); };
const visibleListing = identity === getAuthUserKey() ? listing : null;
return <Box sx={{ display: 'grid', gap: 3, minWidth: 0, '& p, & td, & th': { overflowWrap: 'anywhere' }, '& button': { minHeight: 44 } }}>
<PageHeader level="h2" title={t('processingTitle')} description={t('processingSubtitle')} />
<Stack direction="row" useFlexGap flexWrap="wrap" spacing={1} alignItems="center">
<Button onClick={() => { if (paused) resume(); else { pausedRef.current = true; setPaused(true); } }}>{t(paused ? 'processingResume' : 'processingPause')}</Button>
<Button onClick={() => setRefresh(value => value + 1)} disabled={busy}>{t('refresh')}</Button>
+26 -29
View File
@@ -17,7 +17,7 @@ import {
import { api, getApiErrorMessage } from "../api";
import { useI18n } from "../i18n/I18nProvider";
import { PageHeader, PageSection } from "../layout/PageLayout";
import { PageSection } from "../layout/PageLayout";
import type { TranslationKey } from "../i18n/translations";
import { checkSessionCookie, getSessionContext, isSessionInvalidated } from "../sessionBoundary";
@@ -250,7 +250,6 @@ export default function AdminSystemPage() {
const passwordRevision = useRef(0);
const savedRevision = useRef(0);
const bodyRef = useRef<HTMLDivElement>(null);
const headingRef = useRef<HTMLHeadingElement>(null);
const restoreFocus = useRef<HTMLElement | null>(null);
const navigationId = useId();
@@ -302,7 +301,6 @@ export default function AdminSystemPage() {
restoreFocus.current = null;
if (document.activeElement !== bodyRef.current) return;
if (target.isConnected && !target.matches(":disabled")) target.focus();
else headingRef.current?.focus();
}, [loading, action]);
const rememberFocus = () => {
@@ -422,21 +420,20 @@ export default function AdminSystemPage() {
return (
<Box ref={bodyRef} role="region" aria-label={t("adminSystemTitle")} aria-busy={locked} tabIndex={-1} sx={{ display: "flex", flexDirection: "column", gap: "var(--app-section-gap)", minWidth: 0, overflowWrap: "anywhere", "& .MuiButton-root": { whiteSpace: "normal", overflowWrap: "anywhere", maxWidth: "100%", minHeight: { xs: 44, sm: 36 }, "@media (pointer: coarse)": { minHeight: 44 } }, "& .MuiChip-root": { maxWidth: "100%", height: "auto" }, "& .MuiChip-label": { whiteSpace: "normal", overflowWrap: "anywhere", py: 0.5 } }}>
<PageHeader level="h2" titleRef={headingRef} title={t("adminSystemTitle")} description={t("adminSystemSubtitle")} actions={
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
<Button
variant="outlined"
onClick={async () => {
await runAction("probe", async (active) => { await api.post("/admin/system/ai/probe"); if (active()) await load("status", true); }, "adminSystemProbeCompleted");
}}
disabled={locked || !status || !!readErrors.status}
>
{action === "probe" ? t("adminSystemRunningProbe") : t("adminSystemRunProbe")}
</Button>
<Button variant="contained" onClick={() => refresh()} disabled={locked}>
{loading ? t("adminSystemRefreshing") : t("adminSystemRefresh")}
</Button>
</Box> } />
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
<Button
variant="outlined"
onClick={async () => {
await runAction("probe", async (active) => { await api.post("/admin/system/ai/probe"); if (active()) await load("status", true); }, "adminSystemProbeCompleted");
}}
disabled={locked || !status || !!readErrors.status}
>
{action === "probe" ? t("adminSystemRunningProbe") : t("adminSystemRunProbe")}
</Button>
<Button variant="contained" onClick={() => refresh()} disabled={locked}>
{loading ? t("adminSystemRefreshing") : t("adminSystemRefresh")}
</Button>
</Box>
<Tabs value={tab} aria-label={t("adminSystemTitle")} onChange={(_, value) => setTab(value)} variant="scrollable" scrollButtons="auto" allowScrollButtonsMobile>
<Tab id={`${navigationId}-tab-0`} aria-controls={`${navigationId}-panel-0`} label={t("adminSystemStatusTab")} />
@@ -484,7 +481,7 @@ export default function AdminSystemPage() {
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", lg: "1.2fr 1fr" }, gap: 2 }}>
<PageSection level="h3" title={t("adminSystemDatabaseStorage")}>
<PageSection title={t("adminSystemDatabaseStorage")}>
<Stack spacing={0.75}>
<DetailRow label={t("adminSystemProvider")} value={status?.database.provider || "-"} />
<DetailRow label={t("adminSystemTarget")} value={status?.database.target || "-"} />
@@ -501,7 +498,7 @@ export default function AdminSystemPage() {
</Stack>
</PageSection>
<PageSection level="h3" title={t("adminSystemRuntimeAuth")}>
<PageSection title={t("adminSystemRuntimeAuth")}>
<Stack spacing={0.75}>
<DetailRow label={t("adminSystemFramework")} value={status?.runtime.framework || "-"} />
<DetailRow label={t("adminSystemOs")} value={status?.runtime.osDescription || "-"} />
@@ -518,7 +515,7 @@ export default function AdminSystemPage() {
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", lg: "1fr 1fr" }, gap: 2 }}>
<PageSection level="h3" title={t("adminSystemEmailConfig")}>
<PageSection title={t("adminSystemEmailConfig")}>
<Stack spacing={0.75}>
<DetailRow label={t("adminSystemEnabled")} value={status?.email.enabled ? t("yes") : t("noWord")} />
<DetailRow label={t("adminSystemFrom")} value={status?.email.from || "-"} />
@@ -529,7 +526,7 @@ export default function AdminSystemPage() {
</Stack>
</PageSection>
<PageSection level="h3" title={t("adminSystemSummarizerRuntime")}>
<PageSection title={t("adminSystemSummarizerRuntime")}>
<Stack spacing={0.75}>
<DetailRow label={t("adminSystemModel")} value={status?.ai.model || "-"} />
<DetailRow label={t("adminSystemDevice")} value={status?.ai.device || "-"} />
@@ -550,7 +547,7 @@ export default function AdminSystemPage() {
</PageSection>
</Box>
<PageSection level="h3" title={t("adminSystemSummarizerTelemetry")}>
<PageSection title={t("adminSystemSummarizerTelemetry")}>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr 1fr", md: "repeat(8, 1fr)" }, gap: 2 }}>
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>{t("adminSystemRequests")}</Typography><Typography component="p" variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.requests ?? 0}</Typography></Box>
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>{t("adminSystemCacheHits")}</Typography><Typography component="p" variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.cacheHits ?? 0}</Typography></Box>
@@ -577,7 +574,7 @@ export default function AdminSystemPage() {
</Box>
</PageSection>
<PageSection level="h3" title={t("adminBenchmarkTitle")}>
<PageSection title={t("adminBenchmarkTitle")}>
<Box sx={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 2, flexWrap: "wrap", mb: 1 }}>
<Box>
<Typography variant="body2" sx={{ color: "text.secondary", mt: 0.25 }}>
@@ -619,7 +616,7 @@ export default function AdminSystemPage() {
<Box sx={{ mt: 2.5, display: "grid", gridTemplateColumns: { xs: "1fr", xl: "1.1fr 0.9fr" }, gap: 2 }}>
<Box sx={{ minWidth: 0, pt: 1.5, borderTop: 1, borderColor: "divider" }}>
<Typography component="h4" variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkTopFindings")}</Typography>
<Typography component="h3" variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkTopFindings")}</Typography>
<Stack spacing={1}>
{benchmarkFindings.length > 0 ? benchmarkFindings.map((finding) => (
<Box key={`${finding.file}:${finding.issue}`} sx={{ py: 1, overflowWrap: "anywhere", "& + &": { borderTop: 1, borderColor: "divider" } }}>
@@ -631,7 +628,7 @@ export default function AdminSystemPage() {
</Box>
<Box sx={{ minWidth: 0, pt: 1.5, borderTop: 1, borderColor: "divider" }}>
<Typography component="h4" variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkWeakest")}</Typography>
<Typography component="h3" variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkWeakest")}</Typography>
<Stack spacing={1}>
{weakestEntries.map((entry) => (
<Box key={entry.Slug} sx={{ py: 1, overflowWrap: "anywhere", "& + &": { borderTop: 1, borderColor: "divider" } }}>
@@ -649,7 +646,7 @@ export default function AdminSystemPage() {
</Box>
<Box sx={{ mt: 2, pt: 1.5, borderTop: 1, borderColor: "divider", maxHeight: 280, overflow: "auto" }}>
<Typography component="h4" variant="subtitle2" sx={{ fontWeight: 800, mb: 1 }}>{t("adminBenchmarkLatestSummary")}</Typography>
<Typography component="h3" variant="subtitle2" sx={{ fontWeight: 800, mb: 1 }}>{t("adminBenchmarkLatestSummary")}</Typography>
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap", fontFamily: "ui-monospace, SFMono-Regular, monospace" }}>
{benchmarkStatus?.reportMarkdown || "-"}
</Typography>
@@ -668,7 +665,7 @@ export default function AdminSystemPage() {
<Box role="tabpanel" id={`${navigationId}-panel-1`} aria-labelledby={`${navigationId}-tab-1`} hidden={tab !== 1} sx={{ minWidth: 0 }}>
<Box sx={{ display: "grid", gap: 2, minWidth: 0 }}>
{readNotice("email")}
{emailSettings ? <PageSection surface level="h3" title={t("adminSystemEmailSettingsTitle")}>
{emailSettings ? <PageSection surface title={t("adminSystemEmailSettingsTitle")}>
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>{t("adminSystemEmailSettingsBody")}</Typography>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
<FormControlLabel
@@ -694,7 +691,7 @@ export default function AdminSystemPage() {
</Box>
</PageSection> : null}
<PageSection surface level="h3" title={t("adminSystemSmtpTest")}>
<PageSection surface title={t("adminSystemSmtpTest")}>
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>
{t("adminSystemSmtpTestBody")}
</Typography>
+2 -3
View File
@@ -23,7 +23,7 @@ import { useDialogActions } from "../dialogs";
import { useI18n } from "../i18n/I18nProvider";
import { useViewResource } from "../hooks/useViewResource";
import ViewStateNotice from "../components/ViewStateNotice";
import { PageHeader, PageSection } from "../layout/PageLayout";
import { PageSection } from "../layout/PageLayout";
import { checkSessionCookie, getSessionContext, isSessionInvalidated } from "../sessionBoundary";
type UserDto = {
@@ -222,10 +222,9 @@ export default function AdminUsersPage() {
return (
<Box ref={body} role="group" aria-label={t("adminUsersTitle")} tabIndex={-1} aria-busy={busy} sx={{ display: "grid", gap: "var(--app-section-gap)", minWidth: 0, "& button": { whiteSpace: "normal", overflowWrap: "anywhere", "@media (pointer: coarse)": { minHeight: 44 } } }}>
<PageHeader level="h2" title={t("adminUsersTitle")} description={t("adminUsersSubtitle")} />
<Alert severity="info" sx={{ mb: 2 }}>{t("adminUsersSafetyNotice")}</Alert>
<PageSection level="h3" surface title={t("adminUsersCreateUser")}>
<PageSection surface title={t("adminUsersCreateUser")}>
<Box component="form" onSubmit={event => { event.preventDefault(); void createUser(); }}>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
<TextField label={t("profileEmail")} value={newEmail} onChange={(e) => setNewEmail(e.target.value)} fullWidth />