import { useEffect, useState } from "react"; import { ArrowLeft, Eye, EyeOff, Flag, Inbox, LayoutDashboard, Mail, Send, ShieldAlert, SlidersHorizontal, Users } from "lucide-react"; import { api, getToken } from "../lib/api.ts"; import { subscribeAccessLive, wsUrl } from "../lib/accessWs.ts"; import { AdminPanel } from "./AdminPanel.tsx"; import { AccessRequestsPanel } from "./AccessRequests.tsx"; import { ReportsPanel } from "./ReportsPanel.tsx"; import { Button, CopyButton, FlowerMark, Input, LoadingScreen, Spinner, Switch } from "./ui.tsx"; import { cn } from "../lib/cn.ts"; import { useT } from "../lib/i18n.ts"; type Tab = "accounts" | "access" | "reports" | "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"; } export function AdminCenter() { const { t } = useT(); const [allowed, setAllowed] = useState(null); const [tab, setTab] = useState("accounts"); const [settings, setSettings] = useState<{ registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean } | null>(null); const [error, setError] = useState(""); const [saving, setSaving] = useState(false); const [openAccessCount, setOpenAccessCount] = useState(0); const [openReportCount, setOpenReportCount] = useState(0); const TABS: { id: Tab; label: string; icon: typeof Users }[] = [ { id: "accounts", label: t("center.tabAccounts"), icon: Users }, { id: "access", label: t("center.tabAccess"), icon: Inbox }, { id: "reports", label: t("center.tabReports"), icon: Flag }, { id: "features", label: t("center.tabFeatures"), icon: SlidersHorizontal }, { id: "mail", label: t("center.tabMail"), icon: Mail }, ]; 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 : t("admin.loadFailed"))); }) .catch(() => setAllowed(false)); }, []); // Live badges on the Access/Reports tabs: the admin firehose pushes every access // event, so the open counts stay fresh with zero polling (reports re-fetch // alongside). Resync on (re)connect covers events missed while offline. useEffect(() => { if (!allowed) return; async function fetchCount() { try { const res = await api.adminAccessList(); setOpenAccessCount( res.requests.filter((r) => r.status === "pending" || r.status === "reviewing").length, ); } catch { // Transient failure: keep last count. } try { const res = await api.adminReportList(); setOpenReportCount( res.reports.filter((r) => (r.status === "open" || r.status === "reviewing") && r.reporter_shadowbanned !== 1).length, ); } catch { // Transient failure: keep last count. } } fetchCount(); const token = getToken(); if (!token) return; return subscribeAccessLive(wsUrl(`/api/admin/ws?token=${encodeURIComponent(token)}`), { onEvent: (evt) => { if (evt.type !== "pong") fetchCount(); }, onSync: () => { fetchCount(); }, }); }, [allowed]); 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 : t("common.saveFailed")); } finally { setSaving(false); } } if (allowed === null) { return ; } if (!allowed) { return (

{t("center.locked")}

{t("center.lockedSub")}

{t("common.backToLogin")}
); } return (

{t("center.title")}

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

{error}

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

{t("center.regTitle")}

{t("center.regDesc")}

toggle({ registrationEnabled: v })} />

{t("center.accTitle")}

{settings?.registrationEnabled ? t("center.accDescOn") : t("center.accDescOff")}

toggle({ accessRequestEnabled: v })} />

{t("center.ocrTitle")}

{t("center.ocrDesc")}

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

{t("common.saving")}

} {!settings && !error &&

{t("common.loading")}

}
)} {tab === "mail" && }
); } type SmtpInfo = { enabled: boolean; host: string; port: number; secure: boolean; user: string; pass: string; hasPass: boolean; from: string; appUrl: string; }; function MailPanel() { const { t } = useT(); const [smtp, setSmtp] = useState(null); const [error, setError] = useState(""); const [showPass, setShowPass] = useState(false); const [to, setTo] = useState(""); const [verifying, setVerifying] = useState(false); const [sending, setSending] = useState(false); const [result, setResult] = useState<{ ok: boolean; text: string } | null>(null); useEffect(() => { api.adminMailStatus().then((r) => setSmtp(r.smtp)).catch((e) => setError(e instanceof Error ? e.message : t("admin.loadFailed"))); }, []); async function verify() { setVerifying(true); setResult(null); try { await api.adminMailVerify(); setResult({ ok: true, text: t("center.connOk") }); } catch (e) { setResult({ ok: false, text: e instanceof Error ? e.message : t("center.verifyFailed") }); } finally { setVerifying(false); } } async function sendTest() { setSending(true); setResult(null); try { const r = await api.adminMailTest(to.trim()); setResult({ ok: true, text: t("center.testSent", { to: to.trim(), tail: r.messageId ? ` (${r.messageId})` : "" }) }); } catch (e) { setResult({ ok: false, text: e instanceof Error ? e.message : t("center.mailSendFailed") }); } finally { setSending(false); } } function Row({ label, children }: { label: string; children: React.ReactNode }) { return (
{label} {children}
); } return (

{t("center.mailTitle")}

{smtp == null ? t("center.mailChecking") : smtp.enabled ? t("center.mailFrom", { from: smtp.from }) : t("center.mailOff")}

{smtp?.enabled ? t("center.on") : t("center.off")}
{error &&

{error}

} {!smtp && !error &&

{t("common.loading")}

} {smtp && (
{smtp.host || {t("center.notSet")}} {smtp.port} · {smtp.secure ? t("center.tlsImplicit") : t("center.tlsStarttls")} {smtp.user || {t("center.noAuth")}} {!smtp.hasPass ? ( {t("center.notSet")} ) : showPass ? ( {smtp.pass} ) : ( •••••••••• )} {smtp.hasPass && ( <> {showPass && } )} {smtp.from} {smtp.appUrl}
)}

{t("center.credsNote")}

{t("center.mailTestTitle")}

{t("center.mailTestDesc")}

setTo(e.target.value)} className="flex-1" />
{result && (

{result.text}

)}
); }