import { useEffect, useState } from "react"; import { ArrowLeft, Inbox, LayoutDashboard, Mail, ShieldAlert, SlidersHorizontal, Users } from "lucide-react"; import { api } from "../lib/api.ts"; import { AdminPanel } from "./AdminPanel.tsx"; import { AccessRequestsPanel } from "./AccessRequests.tsx"; import { FlowerMark, LoadingScreen, Spinner, Switch } from "./ui.tsx"; import { cn } from "../lib/cn.ts"; type Tab = "accounts" | "access" | "features" | "mail"; function applyTheme(theme: string) { const root = document.documentElement; const dark = theme === "dark" || (theme !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches); root.classList.toggle("dark", dark); root.style.colorScheme = dark ? "dark" : "light"; } const TABS: { id: Tab; label: string; icon: typeof Users }[] = [ { id: "accounts", label: "Accounts", icon: Users }, { id: "access", label: "Access", icon: Inbox }, { id: "features", label: "Features", icon: SlidersHorizontal }, { id: "mail", label: "Mail", icon: Mail }, ]; export function AdminCenter() { const [allowed, setAllowed] = useState(null); const [tab, setTab] = useState("accounts"); const [settings, setSettings] = useState<{ registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean } | null>(null); const [mail, setMail] = useState<{ recovery: boolean; from?: string } | null>(null); const [error, setError] = useState(""); const [saving, setSaving] = useState(false); useEffect(() => { api.verify() .then((r) => { if (!r.user.isAdmin) { setAllowed(false); return; } setAllowed(true); applyTheme(r.user.theme); api.adminGetSettings().then((s) => setSettings(s.settings)).catch((e) => setError(e instanceof Error ? e.message : "Load failed")); api.methods().then((m) => setMail(m)).catch(() => {}); }) .catch(() => setAllowed(false)); }, []); async function toggle(patch: { registrationEnabled?: boolean; accessRequestEnabled?: boolean; ocrEnabled?: boolean }) { setSaving(true); setError(""); try { const res = await api.adminPatchSettings(patch); setSettings(res.settings); } catch (e) { setError(e instanceof Error ? e.message : "Save failed"); } finally { setSaving(false); } } if (allowed === null) { return ; } if (!allowed) { return (

Admins only

Log in with the admin account to open the Admin Center.

Back to login
); } return (

Admin Center

{TABS.map((t) => ( ))}
{error &&

{error}

} {tab === "accounts" && (
{}} />
)} {tab === "access" && (
)} {tab === "features" && (

Public registration

Show the Register button on the login page. Off = admin-created accounts only. Turning it on retires the wishlist below.

toggle({ registrationEnabled: v })} />

Access requests (wishlist)

{settings?.registrationEnabled ? "Unavailable while public registration is on — it replaces the wishlist." : "Show Request access on the login page. Visitors reserve a username and plead their case; you triage them in the Access tab."}

toggle({ accessRequestEnabled: v })} />

OCR tool

Server-side image transcription for attachments. Requires the tesseract binary.

toggle({ ocrEnabled: v })} />
{saving &&

Saving…

} {!settings && !error &&

Loading…

}
)} {tab === "mail" && (

Key recovery emails

{mail == null ? "Checking…" : mail.recovery ? `Enabled — sent from ${mail.from ?? "the configured sender"}` : "Disabled — set SMTP_HOST on the server to enable"}

{mail?.recovery ? "On" : "Off"}

SMTP credentials live in the server environment (see docs/OPERATIONS.md), never in this UI. Users set their recovery address in profile settings or at account creation.

)}
); }