chore: consolidate platform split checks
Dependency Audit / dependency-audit (push) Has been cancelled
Module Matrix / module-matrix (push) Has been cancelled

This commit is contained in:
2026-07-10 12:51:19 +02:00
parent 150b720f12
commit 635d25c74c
216 changed files with 23336 additions and 4077 deletions
+311 -149
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "react-router-dom";
import type { ApiSettings, AuthInfo, MailProfilesUiCapability } from "../../types";
import type { ApiSettings, AuthInfo, FilesConnectorsUiCapability, MailProfilesUiCapability, UserUiPreferences, UserUiTheme } from "../../types";
import Card from "../../components/Card";
import FormField from "../../components/FormField";
import PasswordField from "../../components/PasswordField";
@@ -11,30 +11,47 @@ import { apiFetch } from "../../api/client";
import { updateProfile } from "../../api/auth";
import ModuleSubnav, { type ModuleSubnavGroup } from "../../layout/ModuleSubnav";
import DismissibleAlert from "../../components/DismissibleAlert";
import { useUnsavedChanges, useUnsavedDraftGuard } from "../../components/UnsavedChangesGuard";
import { usePlatformUiCapability } from "../../platform/ModuleContext";
import { hasAnyScope, hasScope } from "../../utils/permissions";
import { usePlatformLanguage } from "../../i18n/LanguageContext";
type SettingsSection = "profile" | "mail-profiles" | "interface" | "workspace" | "local-connection" | "notifications";
type SettingsSection = "profile" | "mail-profiles" | "file-connectors" | "interface" | "workspace" | "local-connection" | "notifications";
function settingsGroups(canUseMailProfiles: boolean): ModuleSubnavGroup<SettingsSection>[] {
const DEFAULT_UI_PREFERENCES: UserUiPreferences = {
compact_tables: false,
show_inline_help_hints: true,
reduce_motion: false,
sticky_section_sidebars: true,
theme: "system"
};
const UI_THEME_OPTIONS: Array<{ value: UserUiTheme; label: string }> = [
{ value: "system", label: "i18n:govoplan-core.system_default.1f06f3ed" },
{ value: "light", label: "i18n:govoplan-core.light_theme.7878f1fa" },
{ value: "dark", label: "i18n:govoplan-core.dark_theme.164a90d9" }
];
function settingsGroups(canUseMailProfiles: boolean, canUseFileConnectors: boolean): ModuleSubnavGroup<SettingsSection>[] {
return [
{
title: "ACCOUNT",
items: [
{ id: "profile", label: "My profile" },
...(canUseMailProfiles ? [{ id: "mail-profiles" as const, label: "Mail profiles" }] : [])
]
},
{
title: "UI SETTINGS",
items: [
{ id: "interface", label: "Interface" },
{ id: "workspace", label: "Workspace" },
{ id: "local-connection", label: "Local connection" },
{ id: "notifications", label: "Notifications" }
]
}
];
{
title: "i18n:govoplan-core.account.f967543b",
items: [
{ id: "profile", label: "i18n:govoplan-core.my_profile.2f3df0a9" },
...(canUseMailProfiles ? [{ id: "mail-profiles" as const, label: "i18n:govoplan-core.mail_profiles.8a8018b7" }] : []),
...(canUseFileConnectors ? [{ id: "file-connectors" as const, label: "i18n:govoplan-core.file_connections.1e362326" }] : [])]
},
{
title: "i18n:govoplan-core.ui_settings.9e9cc5ea",
items: [
{ id: "interface", label: "i18n:govoplan-core.interface.7b4db7ef" },
{ id: "workspace", label: "i18n:govoplan-core.workspace.4ca0a75c" },
{ id: "local-connection", label: "i18n:govoplan-core.local_connection.42fba65a" },
{ id: "notifications", label: "i18n:govoplan-core.notifications.753a22b2" }]
}];
}
export default function SettingsPage({
@@ -42,49 +59,101 @@ export default function SettingsPage({
auth,
onSettingsChange,
onAuthChange
}: {
settings: ApiSettings;
auth: AuthInfo;
onSettingsChange: (settings: ApiSettings) => void;
onAuthChange: (auth: AuthInfo | null, accessToken?: string) => void;
}) {
}: {settings: ApiSettings;auth: AuthInfo;onSettingsChange: (settings: ApiSettings) => void;onAuthChange: (auth: AuthInfo | null, accessToken?: string) => void;}) {
const [searchParams, setSearchParams] = useSearchParams();
const { requestNavigation } = useUnsavedChanges();
const mailProfilesUi = usePlatformUiCapability<MailProfilesUiCapability>("mail.profiles");
const fileConnectorsUi = usePlatformUiCapability<FilesConnectorsUiCapability>("files.connectors");
const { language, languageLabel, selectableLanguages, availableLanguages, enabledLanguages, setLanguage } = usePlatformLanguage();
const MailProfileScopeManager = mailProfilesUi?.MailProfileScopeManager ?? null;
const FileConnectorScopeManager = fileConnectorsUi?.FileConnectorScopeManager ?? null;
const canUseMailProfiles = Boolean(MailProfileScopeManager) && hasAnyScope(auth, ["mail_servers:read", "mail_servers:write", "mail_servers:manage_credentials", "admin:policies:read", "admin:policies:write"]);
const settingsSubnav = useMemo(() => settingsGroups(canUseMailProfiles), [canUseMailProfiles]);
const canUseFileConnectors = Boolean(FileConnectorScopeManager) && hasAnyScope(auth, ["files:file:read", "files:file:admin", "admin:settings:read", "admin:settings:write"]);
const settingsSubnav = useMemo(() => settingsGroups(canUseMailProfiles, canUseFileConnectors), [canUseFileConnectors, canUseMailProfiles]);
const requestedSection = searchParams.get("section") as SettingsSection | null;
const [active, setActive] = useState<SettingsSection>(settingsSectionAvailable(settingsSubnav, requestedSection) ? requestedSection : "interface");
const active: SettingsSection = settingsSectionAvailable(settingsSubnav, requestedSection) ? requestedSection : "interface";
const currentUiPreferences = normalizeUiPreferences(auth.user.ui_preferences);
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState("");
const [compactTables, setCompactTables] = useState(false);
const [showHelpHints, setShowHelpHints] = useState(true);
const [reduceMotion, setReduceMotion] = useState(false);
const [stickySections, setStickySections] = useState(true);
const [testResultTone, setTestResultTone] = useState<"success" | "warning">("success");
const [compactTables, setCompactTables] = useState(currentUiPreferences.compact_tables);
const [showHelpHints, setShowHelpHints] = useState(currentUiPreferences.show_inline_help_hints);
const [reduceMotion, setReduceMotion] = useState(currentUiPreferences.reduce_motion);
const [stickySections, setStickySections] = useState(currentUiPreferences.sticky_section_sidebars);
const [theme, setTheme] = useState<UserUiTheme>(currentUiPreferences.theme);
const [uiBusy, setUiBusy] = useState(false);
const [uiResult, setUiResult] = useState("");
const [uiResultTone, setUiResultTone] = useState<"success" | "warning">("success");
const [profileName, setProfileName] = useState(auth.user.display_name || "");
const [tenantProfileName, setTenantProfileName] = useState(auth.user.tenant_display_name || "");
const [profileBusy, setProfileBusy] = useState(false);
const [profileResult, setProfileResult] = useState("");
const [profileResultTone, setProfileResultTone] = useState<"success" | "warning">("success");
const profileDirty = profileName !== (auth.user.display_name || "") || tenantProfileName !== (auth.user.tenant_display_name || "");
const uiDirty =
compactTables !== currentUiPreferences.compact_tables ||
showHelpHints !== currentUiPreferences.show_inline_help_hints ||
reduceMotion !== currentUiPreferences.reduce_motion ||
stickySections !== currentUiPreferences.sticky_section_sidebars ||
theme !== currentUiPreferences.theme;
useUnsavedDraftGuard({
dirty: active === "profile" && profileDirty,
onSave: saveProfile,
onDiscard: () => {
setProfileName(auth.user.display_name || "");
setTenantProfileName(auth.user.tenant_display_name || "");
}
});
useUnsavedDraftGuard({
dirty: (active === "interface" || active === "workspace") && uiDirty,
onSave: saveUiPreferences,
onDiscard: resetUiPreferences
});
useEffect(() => {
if (settingsSectionAvailable(settingsSubnav, requestedSection)) {
setActive(requestedSection);
} else if (!settingsSectionAvailable(settingsSubnav, active)) {
setActive("interface");
if (requestedSection && !settingsSectionAvailable(settingsSubnav, requestedSection)) {
const next = new URLSearchParams(searchParams);
next.set("section", "interface");
setSearchParams(next, { replace: true });
}
}, [active, requestedSection, settingsSubnav]);
}, [requestedSection, searchParams, setSearchParams, settingsSubnav]);
useEffect(() => {
setProfileName(auth.user.display_name || "");
setTenantProfileName(auth.user.tenant_display_name || "");
}, [auth.user.display_name, auth.user.tenant_display_name]);
useEffect(() => {
setCompactTables(currentUiPreferences.compact_tables);
setShowHelpHints(currentUiPreferences.show_inline_help_hints);
setReduceMotion(currentUiPreferences.reduce_motion);
setStickySections(currentUiPreferences.sticky_section_sidebars);
setTheme(currentUiPreferences.theme);
}, [
currentUiPreferences.compact_tables,
currentUiPreferences.show_inline_help_hints,
currentUiPreferences.reduce_motion,
currentUiPreferences.sticky_section_sidebars,
currentUiPreferences.theme
]);
function selectSection(section: SettingsSection) {
setActive(section);
setSearchParams(section === "interface" ? {} : { section });
if (section === active) return;
requestNavigation(() => {
const next = new URLSearchParams(searchParams);
next.set("section", section);
setSearchParams(next);
});
}
async function saveProfile() {
async function saveProfile(): Promise<boolean> {
setProfileBusy(true);
setProfileResult("");
try {
@@ -93,21 +162,63 @@ export default function SettingsPage({
tenant_display_name: tenantProfileName.trim() || null
});
onAuthChange(next);
setProfileResult("Profile saved. The account menu has been updated.");
setProfileResultTone("success");
setProfileResult("i18n:govoplan-core.profile_saved_the_account_menu_has_been_updated.aee56076");
return true;
} catch (err) {
setProfileResultTone("warning");
setProfileResult(err instanceof Error ? err.message : String(err));
return false;
} finally {
setProfileBusy(false);
}
}
function resetUiPreferences() {
setCompactTables(currentUiPreferences.compact_tables);
setShowHelpHints(currentUiPreferences.show_inline_help_hints);
setReduceMotion(currentUiPreferences.reduce_motion);
setStickySections(currentUiPreferences.sticky_section_sidebars);
setTheme(currentUiPreferences.theme);
}
function uiPreferencePayload(): UserUiPreferences {
return {
compact_tables: compactTables,
show_inline_help_hints: showHelpHints,
reduce_motion: reduceMotion,
sticky_section_sidebars: stickySections,
theme
};
}
async function saveUiPreferences(): Promise<boolean> {
setUiBusy(true);
setUiResult("");
try {
const next = await updateProfile(settings, { ui_preferences: uiPreferencePayload() });
onAuthChange(next);
setUiResultTone("success");
setUiResult("i18n:govoplan-core.preferences_saved.c8cd3501");
return true;
} catch (err) {
setUiResultTone("warning");
setUiResult(err instanceof Error ? err.message : String(err));
return false;
} finally {
setUiBusy(false);
}
}
async function testConnection() {
setTesting(true);
setTestResult("");
try {
await apiFetch<unknown>(settings, "/health");
setTestResult("Connection successful. The backend health endpoint responded.");
setTestResultTone("success");
setTestResult("i18n:govoplan-core.connection_successful_the_backend_health_endpoin.14eb9295");
} catch (err) {
setTestResultTone("warning");
setTestResult(err instanceof Error ? err.message : String(err));
} finally {
setTesting(false);
@@ -121,177 +232,228 @@ export default function SettingsPage({
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle>Settings</PageTitle>
<p>Your profile, personal WebUI preferences and local browser connection settings. Tenant-wide administration lives in Admin.</p>
<PageTitle>i18n:govoplan-core.settings.c7f73bb5</PageTitle>
<p>i18n:govoplan-core.your_profile_personal_webui_preferences_and_loca.beda6d56</p>
</div>
</div>
{active === "profile" && (
<div className="dashboard-grid settings-dashboard-grid">
<Card title="My profile">
{active === "profile" &&
<div className="dashboard-grid settings-dashboard-grid">
<Card title="i18n:govoplan-core.my_profile.2f3df0a9">
<div className="form-grid">
<FormField label="Account display name" help="This global name is shown in the title bar and follows you across tenant memberships.">
<FormField label="i18n:govoplan-core.account_display_name.03ed8fc5" help="i18n:govoplan-core.this_global_name_is_shown_in_the_title_bar_and_f.1d46240e">
<input value={profileName} onChange={(event) => setProfileName(event.target.value)} placeholder={auth.user.email} />
</FormField>
<FormField label="Tenant display name" help="Optional local alias for the active tenant. It does not replace the global account name in the title bar.">
<input value={tenantProfileName} onChange={(event) => setTenantProfileName(event.target.value)} placeholder="Use account display name" />
<FormField label="i18n:govoplan-core.tenant_display_name.9ae4495e" help="i18n:govoplan-core.optional_local_alias_for_the_active_tenant_it_do.edbb7b14">
<input value={tenantProfileName} onChange={(event) => setTenantProfileName(event.target.value)} placeholder="i18n:govoplan-core.use_account_display_name.570926b0" />
</FormField>
<FormField label="Email">
<FormField label="i18n:govoplan-core.email.84add5b2">
<input value={auth.user.email} disabled />
</FormField>
<div className="button-row compact-actions">
<Button variant="primary" onClick={() => void saveProfile()} disabled={profileBusy}>{profileBusy ? "Saving…" : "Save profile"}</Button>
<Button variant="primary" onClick={() => void saveProfile()} disabled={profileBusy}>{profileBusy ? "i18n:govoplan-core.saving.56a2285c" : "i18n:govoplan-core.save_profile.f597c0e8"}</Button>
</div>
{profileResult && <DismissibleAlert tone={profileResult.startsWith("Profile saved") ? "success" : "warning"} resetKey={profileResult} floating>{profileResult}</DismissibleAlert>}
{profileResult && <DismissibleAlert tone={profileResultTone} resetKey={profileResult} floating>{profileResult}</DismissibleAlert>}
</div>
</Card>
<Card title="Account context">
<Card title="i18n:govoplan-core.account_context.5bb0a918">
<dl className="detail-list compact-detail-list">
<div><dt>Account ID</dt><dd>{auth.user.account_id}</dd></div>
<div><dt>Active tenant</dt><dd>{auth.active_tenant?.name || auth.tenant.name}</dd></div>
<div><dt>Tenant roles</dt><dd>{auth.roles.filter((role) => role.level !== "system").map((role) => role.name).join(", ") || "None"}</dd></div>
<div><dt>System roles</dt><dd>{auth.roles.filter((role) => role.level === "system").map((role) => role.name).join(", ") || "None"}</dd></div>
<div><dt>i18n:govoplan-core.account_id.c5e0db28</dt><dd>{auth.user.account_id}</dd></div>
<div><dt>i18n:govoplan-core.active_tenant.39b16ee9</dt><dd>{auth.active_tenant?.name || auth.tenant.name}</dd></div>
<div><dt>i18n:govoplan-core.tenant_roles.51aca82d</dt><dd>{auth.roles.filter((role) => role.level !== "system").map((role) => role.name).join(", ") || "i18n:govoplan-core.none.6eef6648"}</dd></div>
<div><dt>i18n:govoplan-core.system_roles.a9461aa6</dt><dd>{auth.roles.filter((role) => role.level === "system").map((role) => role.name).join(", ") || "i18n:govoplan-core.none.6eef6648"}</dd></div>
</dl>
<p className="muted small-note">Email changes and password management require dedicated identity-verification workflows and are intentionally not part of this first self-service profile editor.</p>
<p className="muted small-note">i18n:govoplan-core.email_changes_and_password_management_require_de.f7443b69</p>
</Card>
</div>
)}
}
{active === "mail-profiles" && MailProfileScopeManager && (
<MailProfileScopeManager
settings={settings}
scopeType="user"
scopeId={auth.user.id}
profileTitle="My mail server profiles"
policyTitle="My mail profile policy"
canWriteProfiles={hasScope(auth, "mail_servers:write")}
canManageCredentials={hasScope(auth, "mail_servers:manage_credentials")}
canWritePolicy={hasAnyScope(auth, ["admin:policies:write", "mail_servers:write"])}
/>
)}
{active === "mail-profiles" && MailProfileScopeManager &&
<MailProfileScopeManager
settings={settings}
scopeType="user"
scopeId={auth.user.id}
profileTitle="i18n:govoplan-core.my_mail_server_profiles.c5830798"
policyTitle="i18n:govoplan-core.my_mail_profile_policy.6e480f23"
canWriteProfiles={hasScope(auth, "mail_servers:write")}
canManageCredentials={hasScope(auth, "mail_servers:manage_credentials")}
canWritePolicy={hasAnyScope(auth, ["admin:policies:write", "mail_servers:write"])} />
{active === "interface" && (
<div className="dashboard-grid settings-dashboard-grid">
<Card title="Interface preferences">
}
{active === "file-connectors" && FileConnectorScopeManager &&
<FileConnectorScopeManager
settings={settings}
scopeType="user"
scopeId={auth.user.id}
title="i18n:govoplan-core.my_file_connections.00252f2a"
canWrite={hasAnyScope(auth, ["files:file:admin", "admin:settings:write"])} />
}
{active === "interface" &&
<div className="dashboard-grid settings-dashboard-grid">
<Card title="i18n:govoplan-core.interface_preferences.b82b39a7">
<div className="form-grid">
<ToggleSwitch
label="Compact tables"
help="Prepared UI preference for denser tables. The current table layout remains unchanged until this is wired globally."
checked={compactTables}
onChange={setCompactTables}
/>
label="i18n:govoplan-core.compact_tables.c755d9ee"
help="i18n:govoplan-core.prepared_ui_preference_for_denser_tables_the_cur.45698d83"
checked={compactTables}
onChange={setCompactTables} />
<ToggleSwitch
label="Show inline help hints"
help="Controls contextual UI help markers once persisted user preferences are available."
checked={showHelpHints}
onChange={setShowHelpHints}
/>
label="i18n:govoplan-core.show_inline_help_hints.47cf5aaa"
help="i18n:govoplan-core.controls_contextual_ui_help_markers_once_persist.0eaef9c4"
checked={showHelpHints}
onChange={setShowHelpHints} />
<ToggleSwitch
label="Reduce motion"
help="Prepared preference for users who prefer fewer animations."
checked={reduceMotion}
onChange={setReduceMotion}
/>
label="i18n:govoplan-core.reduce_motion.25a5aef5"
help="i18n:govoplan-core.prepared_preference_for_users_who_prefer_fewer_a.b288e8ab"
checked={reduceMotion}
onChange={setReduceMotion} />
<div className="button-row compact-actions">
<Button variant="primary" onClick={() => void saveUiPreferences()} disabled={uiBusy || !uiDirty}>{uiBusy ? "i18n:govoplan-core.saving.56a2285c" : "i18n:govoplan-core.save_preferences.0f1a7e44"}</Button>
</div>
{uiResult && <DismissibleAlert tone={uiResultTone} resetKey={uiResult} floating>{uiResult}</DismissibleAlert>}
</div>
</Card>
<Card title="Theme and language">
<dl className="detail-list compact-detail-list">
<div><dt>Theme</dt><dd>System default for now</dd></div>
<div><dt>Accent color</dt><dd>Default brand accent</dd></div>
<div><dt>Language</dt><dd>Browser/application default</dd></div>
<div><dt>Density</dt><dd>{compactTables ? "Compact preview" : "Comfortable"}</dd></div>
</dl>
<Card title="i18n:govoplan-core.theme_and_language.60a8cf59">
<div className="form-grid">
<FormField label="i18n:govoplan-core.language.89b86ab0">
<select value={language} onChange={(event) => setLanguage(event.target.value)} disabled={selectableLanguages.length <= 1}>
{selectableLanguages.map((item) =>
<option key={item.code} value={item.code}>{item.code.toUpperCase()} - {item.nativeLabel || item.label}</option>
)}
</select>
</FormField>
<FormField label="i18n:govoplan-core.theme.a797e309">
<select value={theme} onChange={(event) => setTheme(event.target.value as UserUiTheme)}>
{UI_THEME_OPTIONS.map((item) =>
<option key={item.value} value={item.value}>{item.label}</option>
)}
</select>
</FormField>
<dl className="detail-list compact-detail-list">
<div><dt>i18n:govoplan-core.theme.a797e309</dt><dd>{themeLabel(theme)}</dd></div>
<div><dt>i18n:govoplan-core.accent_color.e49578ed</dt><dd>i18n:govoplan-core.default_brand_accent.606ae693</dd></div>
<div><dt>i18n:govoplan-core.language.89b86ab0</dt><dd>{languageLabel}</dd></div>
<div><dt>i18n:govoplan-core.enabled.df174a3f</dt><dd>{enabledLanguages.map((item) => item.code.toUpperCase()).join(", ")}</dd></div>
<div><dt>i18n:govoplan-core.available.7c62a142</dt><dd>{availableLanguages.map((item) => item.code.toUpperCase()).join(", ")}</dd></div>
<div><dt>i18n:govoplan-core.density.f9160c22</dt><dd>{compactTables ? "i18n:govoplan-core.compact_preview.3e06901d" : "i18n:govoplan-core.comfortable.2313707a"}</dd></div>
</dl>
</div>
</Card>
</div>
)}
}
{active === "workspace" && (
<div className="dashboard-grid settings-dashboard-grid">
<Card title="Campaign workspace">
{active === "workspace" &&
<div className="dashboard-grid settings-dashboard-grid">
<Card title="i18n:govoplan-core.campaign_workspace.c345580f">
<div className="form-grid">
<ToggleSwitch
label="Sticky section sidebars"
help="Keeps campaign and admin section navigation in view while scrolling."
checked={stickySections}
onChange={setStickySections}
/>
label="i18n:govoplan-core.sticky_section_sidebars.399c92d4"
help="i18n:govoplan-core.keeps_campaign_and_admin_section_navigation_in_v.f3602938"
checked={stickySections}
onChange={setStickySections} />
<ToggleSwitch
label="Keep page shell visible while loading"
help="The current UI already keeps the section shell visible and overlays loading indicators during refresh."
checked
disabled
onChange={() => undefined}
/>
label="i18n:govoplan-core.keep_page_shell_visible_while_loading.17fc142a"
help="i18n:govoplan-core.the_current_ui_already_keeps_the_section_shell_v.c9bbc227"
checked
disabled
onChange={() => undefined} />
<div className="button-row compact-actions">
<Button variant="primary" onClick={() => void saveUiPreferences()} disabled={uiBusy || !uiDirty}>{uiBusy ? "i18n:govoplan-core.saving.56a2285c" : "i18n:govoplan-core.save_preferences.0f1a7e44"}</Button>
</div>
{uiResult && <DismissibleAlert tone={uiResultTone} resetKey={uiResult} floating>{uiResult}</DismissibleAlert>}
</div>
</Card>
<Card title="Editor behavior">
<Card title="i18n:govoplan-core.editor_behavior.99581bc1">
<div className="placeholder-stack">
<span>Manual save with unsaved-change guard</span>
<span>Readable chooser fields for file/path selection</span>
<span>Field-type-aware recipient data inputs</span>
<span>Template placeholder chips and preview overlays</span>
<span>i18n:govoplan-core.manual_save_with_unsaved_change_guard.ef42e803</span>
<span>i18n:govoplan-core.readable_chooser_fields_for_file_path_selection.d3aee1a7</span>
<span>i18n:govoplan-core.field_type_aware_recipient_data_inputs.2892f175</span>
<span>i18n:govoplan-core.template_placeholder_chips_and_preview_overlays.11634d55</span>
</div>
</Card>
</div>
)}
}
{active === "local-connection" && (
<div className="dashboard-grid settings-dashboard-grid">
<Card title="Local API connection">
{active === "local-connection" &&
<div className="dashboard-grid settings-dashboard-grid">
<Card title="i18n:govoplan-core.local_api_connection.bfb02921">
<div className="form-grid">
<FormField label="API base URL" help="Leave empty to use the same origin. In Vite dev, /api is proxied to the FastAPI backend.">
<FormField label="i18n:govoplan-core.api_base_url.1358fba4" help="i18n:govoplan-core.leave_empty_to_use_the_same_origin_in_vite_dev_a.9a1c25d7">
<input value={settings.apiBaseUrl} onChange={(e) => onSettingsChange({ ...settings, apiBaseUrl: e.target.value })} placeholder="https://example.org or empty" />
</FormField>
<FormField label="Automation API key" help="Used only when there is no browser session token. Browser login remains the preferred interactive mode.">
<FormField label="i18n:govoplan-core.automation_api_key.5d4e2e6e" help="i18n:govoplan-core.used_only_when_there_is_no_browser_session_token.9d399e70">
<PasswordField
value={settings.apiKey}
autoComplete="off"
onValueChange={(apiKey) => onSettingsChange({ ...settings, apiKey })}
/>
value={settings.apiKey}
autoComplete="off"
onValueChange={(apiKey) => onSettingsChange({ ...settings, apiKey })} />
</FormField>
<div className="button-row compact-actions">
<Button variant="primary" onClick={testConnection} disabled={testing}>{testing ? "Testing…" : "Test connection"}</Button>
<Button variant="primary" onClick={testConnection} disabled={testing}>{testing ? "i18n:govoplan-core.testing.95c4564a" : "i18n:govoplan-core.test_connection.ccf66f07"}</Button>
</div>
{testResult && <DismissibleAlert tone={testResult.startsWith("Connection successful") ? "success" : "warning"} resetKey={testResult} floating>{testResult}</DismissibleAlert>}
{testResult && <DismissibleAlert tone={testResultTone} resetKey={testResult} floating>{testResult}</DismissibleAlert>}
</div>
</Card>
<Card title="Session state">
<Card title="i18n:govoplan-core.session_state.b7a3d0f4">
<dl className="detail-list compact-detail-list">
<div><dt>Browser session</dt><dd>Active via HttpOnly cookie</dd></div>
<div><dt>Automation key</dt><dd>{settings.apiKey ? "Configured" : "Not configured"}</dd></div>
<div><dt>Backend mode</dt><dd>{settings.apiBaseUrl ? "Explicit API URL" : "Same-origin / proxied"}</dd></div>
<div><dt>i18n:govoplan-core.browser_session.cc021a31</dt><dd>i18n:govoplan-core.active_via_httponly_cookie.9d05e6ba</dd></div>
<div><dt>i18n:govoplan-core.automation_key.78f1ddbc</dt><dd>{settings.apiKey ? "i18n:govoplan-core.configured.668c5fff" : "i18n:govoplan-core.not_configured.811931bb"}</dd></div>
<div><dt>i18n:govoplan-core.backend_mode.c863723a</dt><dd>{settings.apiBaseUrl ? "i18n:govoplan-core.explicit_api_url.b117b4b8" : "i18n:govoplan-core.same_origin_proxied.c39e6e2b"}</dd></div>
</dl>
<p className="muted small-note">Tenant, user, mail-server and policy administration has moved to Admin. This page keeps browser-local configuration.</p>
<p className="muted small-note">i18n:govoplan-core.tenant_user_mail_server_and_policy_administratio.2f551271</p>
</Card>
</div>
)}
}
{active === "notifications" && (
<div className="dashboard-grid settings-dashboard-grid">
<Card title="Notification preferences">
<p className="muted">Prepared for later personal notification preferences. Backend and browser notification wiring are not active yet.</p>
{active === "notifications" &&
<div className="dashboard-grid settings-dashboard-grid">
<Card title="i18n:govoplan-core.notification_preferences.0ead6c12">
<p className="muted">i18n:govoplan-core.prepared_for_later_personal_notification_prefere.3fe73f86</p>
<div className="placeholder-stack">
<span>In-app completion notices</span>
<span>Email summary preferences</span>
<span>Failure and warning alerts</span>
<span>i18n:govoplan-core.in_app_completion_notices.b68f2f4c</span>
<span>i18n:govoplan-core.email_summary_preferences.b6c1dfb1</span>
<span>i18n:govoplan-core.failure_and_warning_alerts.939d21c2</span>
</div>
</Card>
<Card title="Quiet UI mode">
<Card title="i18n:govoplan-core.quiet_ui_mode.1b0bd558">
<div className="placeholder-stack">
<span>Mute non-critical banners</span>
<span>Batch repetitive notices</span>
<span>Keep validation and send warnings visible</span>
<span>i18n:govoplan-core.mute_non_critical_banners.27b23a4a</span>
<span>i18n:govoplan-core.batch_repetitive_notices.cc893559</span>
<span>i18n:govoplan-core.keep_validation_and_send_warnings_visible.9d6e0cf4</span>
</div>
</Card>
</div>
)}
}
</div>
</section>
</div>
);
</div>);
}
function settingsSectionAvailable(groups: ModuleSubnavGroup<SettingsSection>[], section: SettingsSection | null | undefined): section is SettingsSection {
return Boolean(section && groups.some((group) => group.items.some((item) => "id" in item && item.id === section)));
}
function normalizeUiPreferences(value: Partial<UserUiPreferences> | null | undefined): UserUiPreferences {
const theme = value?.theme === "light" || value?.theme === "dark" || value?.theme === "system" ? value.theme : "system";
return {
compact_tables: Boolean(value?.compact_tables ?? DEFAULT_UI_PREFERENCES.compact_tables),
show_inline_help_hints: Boolean(value?.show_inline_help_hints ?? DEFAULT_UI_PREFERENCES.show_inline_help_hints),
reduce_motion: Boolean(value?.reduce_motion ?? DEFAULT_UI_PREFERENCES.reduce_motion),
sticky_section_sidebars: Boolean(value?.sticky_section_sidebars ?? DEFAULT_UI_PREFERENCES.sticky_section_sidebars),
theme
};
}
function themeLabel(value: UserUiTheme): string {
return UI_THEME_OPTIONS.find((item) => item.value === value)?.label ?? UI_THEME_OPTIONS[0].label;
}