import { useState } from 'react'; import { useNavigate, useParams } from 'react-router'; import { useTranslation } from 'react-i18next'; import { Window, Card, Button, TextField, SegmentedControl, Dialog, AlertDialog, Menu, MenuTrigger, MenuContent, MenuItem, IconButton, Badge, Callout, } from '@olly/modern-sk'; import { Icon } from '../../components/common/Icon'; import { LoadingSkeleton } from '../../components/common/LoadingSkeleton'; import { EmptyState } from '../../components/common/EmptyState'; import { ErrorState } from '../../components/common/ErrorState'; import { usePermissions } from '../../hooks/usePermissions'; import { useGetUsersQuery, useGetUserQuery, useCreateUserMutation, useUpdateUserMutation, useDeleteUserMutation, useResetPasswordMutation, useGetServicesQuery, useGetSourcesQuery, useReindexMutation, } from '../../api/endpoints/admin'; import type { User, ServiceStatus } from '../../api/types'; function StubPanel({ title }: { title: string }) { const { t } = useTranslation(); return (

{t('common.comingSoon')}

); } /** Role + active-status badges shared by the list and the detail view. */ function UserBadges({ user }: { user: User }) { const { t } = useTranslation(); return (
{t(`admin.users.role.${user.role}`)} {!user.isActive && ( {t('admin.users.status.blocked')} )}
); } /** `/admin/users` — user list with add / role toggle / (de)activate / reset password. */ export function AdminUsers() { const { t } = useTranslation(); const { data: users, isLoading, isError, refetch } = useGetUsersQuery(); const [createUser, { isLoading: isCreating }] = useCreateUserMutation(); const [createOpen, setCreateOpen] = useState(false); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [role, setRole] = useState<'admin' | 'user'>('user'); const [createError, setCreateError] = useState(false); const resetForm = () => { setUsername(''); setPassword(''); setRole('user'); setCreateError(false); }; const handleCreate = async (e: React.FormEvent) => { e.preventDefault(); const name = username.trim(); if (!name || !password) return; setCreateError(false); try { await createUser({ username: name, password, role }).unwrap(); setCreateOpen(false); resetForm(); } catch { setCreateError(true); } }; return (
{ setCreateOpen(open); if (!open) resetForm(); }} title={t('admin.users.newDialog.title')} trigger={ } footer={
} >
void handleCreate(e)} style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }} > setUsername(e.target.value)} placeholder={t('admin.users.newDialog.username')} autoComplete="off" /> setPassword(e.target.value)} placeholder={t('admin.users.newDialog.password')} autoComplete="new-password" /> setRole(v === 'admin' ? 'admin' : 'user')} items={[ { value: 'user', label: t('admin.users.role.user') }, { value: 'admin', label: t('admin.users.role.admin') }, ]} /> {createError && ( {t('admin.users.newDialog.error')} )}
{isLoading && } {isError && } {!isLoading && !isError && (users?.length ?? 0) === 0 && ( )} {!isLoading && !isError && users && users.length > 0 && (
{users.map((user) => ( ))}
)}
); } /** One row in the user list: identity + badges + actions menu. */ function UserRow({ user }: { user: User }) { const { t } = useTranslation(); const navigate = useNavigate(); const { user: currentUser } = usePermissions(); const isSelf = currentUser?.id === user.id; const [updateUser] = useUpdateUserMutation(); const [deleteUser] = useDeleteUserMutation(); const [resetOpen, setResetOpen] = useState(false); const [deactivateOpen, setDeactivateOpen] = useState(false); const toggleRole = () => void updateUser({ id: user.id, role: user.role === 'admin' ? 'user' : 'admin', }); const toggleActive = () => void updateUser({ id: user.id, isActive: !user.isActive }); return ( ⋯ {user.role === 'admin' ? t('admin.users.menu.makeUser') : t('admin.users.menu.makeAdmin')} setResetOpen(true)}> {t('admin.users.menu.resetPassword')} {/* Guard against locking yourself out of the instance. */} {!isSelf && ( user.isActive ? setDeactivateOpen(true) : toggleActive() } > {user.isActive ? t('admin.users.menu.deactivate') : t('admin.users.menu.activate')} )} void deleteUser(user.id)} /> ); } /** Reset-password dialog: admin sets a new password for the account. */ function ResetPasswordDialog({ user, open, onOpenChange, }: { user: User; open: boolean; onOpenChange: (open: boolean) => void; }) { const { t } = useTranslation(); const [resetPassword, { isLoading }] = useResetPasswordMutation(); const [value, setValue] = useState(''); const [done, setDone] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!value) return; await resetPassword({ id: user.id, newPassword: value }).unwrap(); setValue(''); setDone(true); }; return ( { onOpenChange(o); if (!o) { setValue(''); setDone(false); } }} title={t('admin.users.resetDialog.title', { name: user.username })} footer={
} > {done ? ( {t('admin.users.resetDialog.done')} ) : (
void handleSubmit(e)}> setValue(e.target.value)} placeholder={t('admin.users.resetDialog.newPassword')} autoComplete="new-password" /> )}
); } /** `/admin/users/:userId` — per-user detail: identity, role toggle, reset, status. */ export function AdminUserDetail() { const { t } = useTranslation(); const navigate = useNavigate(); const { userId = '' } = useParams(); const { user: currentUser } = usePermissions(); const { data: user, isLoading, isError, refetch } = useGetUserQuery(userId, { skip: !userId, }); const [updateUser] = useUpdateUserMutation(); const [resetOpen, setResetOpen] = useState(false); const isSelf = currentUser?.id === user?.id; return (
{isLoading && } {isError && } {user && (

{user.username}

{t('admin.users.added', { date: new Date(user.createdAt).toLocaleDateString(), })}
{!isSelf && ( )}
)}
); } /** `/admin/sources` — pluggable source management (creds/cookies/status). Scaffold. */ export function AdminSources() { const { t } = useTranslation(); return ; } const SERVICE_VARIANT: Record = { ok: 'lime', down: 'ember', skipped: 'neutral', }; /** `/admin/instance` — backing-service health, source status, library reindex. */ export function AdminInstance() { const { t } = useTranslation(); const { data: servicesData, isLoading: servicesLoading, isError: servicesError, refetch: refetchServices, } = useGetServicesQuery(); const { data: sourcesData, isLoading: sourcesLoading, isError: sourcesError, refetch: refetchSources, } = useGetSourcesQuery(); const [reindex, { isLoading: isReindexing }] = useReindexMutation(); const [reindexMsg, setReindexMsg] = useState< { kind: 'success' | 'danger'; text: string } | null >(null); const handleReindex = async () => { setReindexMsg(null); try { const jobs = await reindex().unwrap(); setReindexMsg({ kind: 'success', text: t('admin.instance.reindex.enqueued', { count: jobs.length }), }); } catch { // 400 when no indexable source is configured (see admin.py::trigger_reindex). setReindexMsg({ kind: 'danger', text: t('admin.instance.reindex.noSource'), }); } }; return (
{/* ── Services ─────────────────────────────────────────────── */}

{t('admin.instance.services.title')}

{servicesLoading && } {servicesError && } {servicesData && (
{(['database', 'redis', 'ml'] as const).map((key) => { const status = servicesData[key]; return ( {t(`admin.instance.services.${key}`)} {t(`admin.instance.status.${status}`)} ); })}
)}
{/* ── Sources (read-only; configured via env) ──────────────── */}

{t('admin.instance.sources.title')}

{t('admin.instance.sources.envNote')}

{sourcesLoading && } {sourcesError && } {sourcesData && sourcesData.length === 0 && ( )} {sourcesData && sourcesData.length > 0 && (
{sourcesData.map((s) => ( {s.label} {s.kind} {t( s.available ? 'admin.instance.sources.available' : 'admin.instance.sources.unavailable', )} ))}
)}
{/* ── Reindex ──────────────────────────────────────────────── */}

{t('admin.instance.reindex.title')}

{t('admin.instance.reindex.description')}

{reindexMsg && (
{reindexMsg.text}
)}
); }