"use client"; import { localeLabels, locales } from "@/lib/i18n/config"; import { useMemo, useState } from "react"; import { Settings } from "lucide-react"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { useTranslations } from "@/lib/i18n/context"; import { Input } from "@/components/ui/input"; import { NativeSelect } from "@/components/ui/native-select"; import { SettingsToggle, useAdminSettings, useUpdateSettings } from "./_shared"; import { listSupportedTimezones } from "@/lib/tz/format"; // Document-vault limits: bytes on the wire, MB / GB in the UI. The bounds // mirror the server-side schema (per-file cap hard-clamped to 100 MiB — // single-shot GCM + bounded in-memory reads are load-bearing past that). const MIB = 1_048_576; const GIB = 1_073_741_824; const MAX_FILE_MB_MIN = 1; const MAX_FILE_MB_MAX = 100; const QUOTA_GB_MIN = 0.1; const QUOTA_GB_MAX = 1024; function clamp(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value)); } // v1.4.27 — the in-file `NATIVE_SELECT_CLASS` constant // retired; the shared `` primitive owns the visual // contract now. The admin copy previously diverged on the // `focus-visible:ring-[3px]` weight; the shared primitive pins // `focus-visible:ring-2` so admin selects match the rest of the form // surface. export function GeneralSettingsSection() { const { t } = useTranslations(); const { data: settings } = useAdminSettings(); const updateSettings = useUpdateSettings(); // Memoised — `listSupportedTimezones()` calls the engine's // `Intl.supportedValuesOf('timeZone')` which returns ~400 strings. const zones = useMemo(() => listSupportedTimezones(), []); const currentDefaultTz = settings?.defaultUserTimezone ?? ""; // Draft-while-editing pattern for the two limit inputs: null = not // editing (show the stored value), string = the in-progress draft. // Commit clamps to the schema bounds and converts to bytes. const [maxFileDraft, setMaxFileDraft] = useState(null); const [quotaDraft, setQuotaDraft] = useState(null); const storedMaxFileMb = settings?.documentMaxFileBytes != null ? String(Math.round(settings.documentMaxFileBytes / MIB)) : ""; const storedQuotaGb = settings?.documentQuotaBytes != null ? // eslint-disable-next-line healthlog/no-unlocalised-decimal -- value of a type="number" input, which only takes a full stop String(Math.round((settings.documentQuotaBytes / GIB) * 10) / 10) : ""; const commitMaxFile = () => { if (maxFileDraft === null) return; const parsed = Number(maxFileDraft); setMaxFileDraft(null); if (!Number.isFinite(parsed) || maxFileDraft.trim() === "") return; const mb = clamp(Math.round(parsed), MAX_FILE_MB_MIN, MAX_FILE_MB_MAX); const bytes = mb * MIB; if (bytes === settings?.documentMaxFileBytes) return; updateSettings.mutate({ documentMaxFileBytes: bytes }); }; const commitQuota = () => { if (quotaDraft === null) return; const parsed = Number(quotaDraft); setQuotaDraft(null); if (!Number.isFinite(parsed) || quotaDraft.trim() === "") return; const gb = clamp(parsed, QUOTA_GB_MIN, QUOTA_GB_MAX); const bytes = Math.round(gb * GIB); if (bytes === settings?.documentQuotaBytes) return; updateSettings.mutate({ documentQuotaBytes: bytes }); }; return (
updateSettings.mutate({ registrationEnabled: checked }) } disabled={updateSettings.isPending} /> {/* Honest nudge: registration stays open after the first admin so a household can be invited, but on an internet-exposed instance that means anyone who can reach the URL can sign up. Surface a quiet reminder while it is open — no behaviour change, just a signal. */} {(settings?.registrationEnabled ?? true) && (

{t("admin.registrationOpenHint")}

)} {/* v1.23 — instance-wide "require a second factor" policy. Every account without an active factor is sent to forced enrollment after sign-in. This is a web-session nudge, not an API wall: direct API / native (Bearer) access for a not-yet-enrolled account is not blocked. See docs/ops/mfa-enforcement.md. */} updateSettings.mutate({ mfaRequired: checked }) } disabled={updateSettings.isPending} /> {/* v1.4.27 — match the SettingsToggle stacking contract: stack on `

{t("admin.defaultLanguage")}

{t("admin.defaultLanguageDescription")}

updateSettings.mutate({ defaultLocale: e.target.value }) } disabled={updateSettings.isPending} className="self-end sm:w-auto sm:self-auto" > {/* Every shipped locale, not a hand-kept pair: a new language that ships without being offered here was the drift. */} {locales.map((loc) => ( ))}
{/* v1.4.25 — server-wide default timezone for new signups. Existing accounts are NOT touched; this drives only the registration handler's fallback when the client doesn't send a browser-detected zone. */}

{t("admin.defaultUserTimezone")}

{t("admin.defaultUserTimezoneDescription")}

updateSettings.mutate({ defaultUserTimezone: e.target.value }) } disabled={updateSettings.isPending} className="sm:w-auto" > {/* Preserve the stored value even if it's not in the current engine's IANA list (rare — but defensive). */} {currentDefaultTz && !zones.includes(currentDefaultTz) && ( )} {zones.map((zone) => ( ))}
{/* Document vault — the two admin-tunable limits (per-file cap + per-user storage quota). Everything else about the vault is committed behaviour; these are deliberately the only knobs. */}

{t("admin.documentMaxFile")}

{t("admin.documentMaxFileDescription")}

setMaxFileDraft(e.target.value)} onBlur={commitMaxFile} onKeyDown={(e) => { if (e.key === "Enter") commitMaxFile(); }} disabled={updateSettings.isPending || settings == null} /> MB

{t("admin.documentQuota")}

{t("admin.documentQuotaDescription")}

setQuotaDraft(e.target.value)} onBlur={commitQuota} onKeyDown={(e) => { if (e.key === "Enter") commitQuota(); }} disabled={updateSettings.isPending || settings == null} /> GB
); }