From a9a58d8aa25242faee0f932f21efda79603f43d9 Mon Sep 17 00:00:00 2001 From: null Date: Sat, 4 Jul 2026 21:17:16 -0500 Subject: [PATCH] refactor(ts): convert CleanupPanel + UsersTable admin cards to TSX (B17) CleanupPanel (CleanupForm/CleanupStatus/CleanupResult; keyed toggle array typed [keyof CleanupForm,string][]). UsersTable (AdminUser type exported + reused by AdminPage's users state; ResetForm; typed row handlers). typecheck 0, build green. Co-Authored-By: Claude Opus 4.8 --- .../{CleanupPanel.jsx => CleanupPanel.tsx} | 62 ++++++++++++++----- .../admin/{UsersTable.jsx => UsersTable.tsx} | 58 +++++++++++------ client/pages/AdminPage.tsx | 5 +- 3 files changed, 89 insertions(+), 36 deletions(-) rename client/components/admin/{CleanupPanel.jsx => CleanupPanel.tsx} (78%) rename client/components/admin/{UsersTable.jsx => UsersTable.tsx} (86%) diff --git a/client/components/admin/CleanupPanel.jsx b/client/components/admin/CleanupPanel.tsx similarity index 78% rename from client/components/admin/CleanupPanel.jsx rename to client/components/admin/CleanupPanel.tsx index b9f4f97..6a0e849 100644 --- a/client/components/admin/CleanupPanel.jsx +++ b/client/components/admin/CleanupPanel.tsx @@ -1,7 +1,8 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Play, Wrench } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; +import { errMessage } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; @@ -9,9 +10,42 @@ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { FieldRow, Toggle, formatDateTime } from './adminShared'; +interface CleanupForm { + import_sessions_enabled: boolean; + temp_exports_enabled: boolean; + temp_export_max_age_hours: number; + backup_partials_enabled: boolean; + import_history_enabled: boolean; + import_history_max_age_days: number; +} + +interface CleanupTask { + pruned?: number; + removed?: number; +} + +interface CleanupResult { + import_sessions?: CleanupTask; + temp_export_files?: CleanupTask; + backup_partials?: CleanupTask; + import_history?: CleanupTask; +} + +interface CleanupStatus { + last_run_at?: string | null; + last_result?: CleanupResult | null; + settings?: CleanupForm; +} + +const TASK_TOGGLES: [keyof CleanupForm, string][] = [ + ['import_sessions_enabled', 'Prune expired import sessions (24h TTL)'], + ['temp_exports_enabled', 'Remove stale SQLite export temp files'], + ['backup_partials_enabled', 'Remove orphaned backup .partial / .upload files'], +]; + export default function CleanupPanel() { - const [status, setStatus] = useState(null); - const [form, setForm] = useState({ + const [status, setStatus] = useState(null); + const [form, setForm] = useState({ import_sessions_enabled: true, temp_exports_enabled: true, temp_export_max_age_hours: 2, @@ -25,11 +59,11 @@ export default function CleanupPanel() { const load = useCallback(async () => { try { - const data = await api.adminCleanup(); + const data = await api.adminCleanup() as CleanupStatus; setStatus(data); if (data.settings) setForm(data.settings); } catch (err) { - toast.error(err.message || 'Failed to load cleanup settings.'); + toast.error(errMessage(err, 'Failed to load cleanup settings.')); } finally { setLoading(false); } @@ -37,16 +71,16 @@ export default function CleanupPanel() { useEffect(() => { load(); }, [load]); - const set = (k, v) => setForm(prev => ({ ...prev, [k]: v })); + const set = (k: keyof CleanupForm, v: boolean | number) => setForm(prev => ({ ...prev, [k]: v }) as CleanupForm); async function handleSave() { setSaving(true); try { - const next = await api.saveAdminCleanup(form); + const next = await api.saveAdminCleanup(form) as CleanupForm | null; if (next) setForm(next); toast.success('Cleanup settings saved.'); } catch (err) { - toast.error(err.message || 'Failed to save cleanup settings.'); + toast.error(errMessage(err, 'Failed to save cleanup settings.')); } finally { setSaving(false); } @@ -55,7 +89,7 @@ export default function CleanupPanel() { async function handleRunNow() { setRunning(true); try { - const result = await api.runAdminCleanup(); + const result = await api.runAdminCleanup() as { ran_at?: string; tasks?: CleanupResult }; setStatus(prev => ({ ...prev, last_run_at: result.ran_at, @@ -63,13 +97,13 @@ export default function CleanupPanel() { })); toast.success('Cleanup tasks completed.'); } catch (err) { - toast.error(err.message || 'Cleanup run failed.'); + toast.error(errMessage(err, 'Cleanup run failed.')); } finally { setRunning(false); } } - function resultLine(label, task, countKey) { + function resultLine(label: string, task: CleanupTask | undefined, countKey: keyof CleanupTask): string | null { if (!task || task[countKey] == null) return null; return `${label}: ${task[countKey]}`; } @@ -137,11 +171,7 @@ export default function CleanupPanel() {

Task Settings

- {[ - ['import_sessions_enabled', 'Prune expired import sessions (24h TTL)'], - ['temp_exports_enabled', 'Remove stale SQLite export temp files'], - ['backup_partials_enabled', 'Remove orphaned backup .partial / .upload files'], - ].map(([key, label]) => ( + {TASK_TOGGLES.map(([key, label]) => ( set(key, v)} label={label} /> diff --git a/client/components/admin/UsersTable.jsx b/client/components/admin/UsersTable.tsx similarity index 86% rename from client/components/admin/UsersTable.jsx rename to client/components/admin/UsersTable.tsx index 2c74c27..844560e 100644 --- a/client/components/admin/UsersTable.jsx +++ b/client/components/admin/UsersTable.tsx @@ -1,7 +1,7 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { toast } from 'sonner'; import { api } from '@/api'; -import { cn } from '@/lib/utils'; +import { cn, errMessage } from '@/lib/utils'; import { Button, buttonVariants } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; @@ -11,19 +11,41 @@ import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, } from '@/components/ui/alert-dialog'; +import type { User } from '@/hooks/useAuth'; -export default function UsersTable({ users, onRefresh, currentUser }) { - const [resetForms, setResetForms] = useState({}); - const [deleting, setDeleting] = useState(null); - const [resetting, setResetting] = useState(null); - const [roleUpdating, setRoleUpdating] = useState(null); - const [activeUpdating, setActiveUpdating] = useState(null); - const [deleteTarget, setDeleteTarget] = useState(null); +export interface AdminUser { + id: number; + username: string; + role?: string; + is_default_admin?: boolean; + active?: boolean | number; + must_change_password?: boolean; + [key: string]: unknown; +} - const setReset = (id, v) => setResetForms(p => ({ ...p, [id]: { ...(p[id] || {}), ...v } })); - const getForm = (id) => resetForms[id] || { pw: '', open: false }; +interface ResetForm { + pw?: string; + open?: boolean; +} - const handleReset = async (user) => { +interface UsersTableProps { + users?: AdminUser[]; + onRefresh: () => void; + currentUser?: User | null; +} + +export default function UsersTable({ users, onRefresh, currentUser }: UsersTableProps) { + const [resetForms, setResetForms] = useState>({}); + const [deleting, setDeleting] = useState(null); + const [resetting, setResetting] = useState(null); + const [roleUpdating, setRoleUpdating] = useState(null); + const [activeUpdating, setActiveUpdating] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); + + const setReset = (id: number, v: ResetForm) => setResetForms(p => ({ ...p, [id]: { ...(p[id] || {}), ...v } })); + const getForm = (id: number): ResetForm => resetForms[id] || { pw: '', open: false }; + + const handleReset = async (user: AdminUser) => { const form = getForm(user.id); if (!form.pw || form.pw.length < 8) { toast.error('Password must be at least 8 characters.'); return; } setResetting(user.id); @@ -32,7 +54,7 @@ export default function UsersTable({ users, onRefresh, currentUser }) { toast.success(`Password reset for ${user.username}.`); setReset(user.id, { pw: '', open: false }); } catch (err) { - toast.error(err.message || 'Failed to reset password.'); + toast.error(errMessage(err, 'Failed to reset password.')); } finally { setResetting(null); } @@ -47,13 +69,13 @@ export default function UsersTable({ users, onRefresh, currentUser }) { setDeleteTarget(null); onRefresh(); } catch (err) { - toast.error(err.message || 'Failed to delete user.'); + toast.error(errMessage(err, 'Failed to delete user.')); } finally { setDeleting(null); } }; - const handleRoleChange = async (user, role) => { + const handleRoleChange = async (user: AdminUser, role: string) => { if (user.role === role) return; setRoleUpdating(user.id); try { @@ -61,20 +83,20 @@ export default function UsersTable({ users, onRefresh, currentUser }) { toast.success(`${user.username} is now ${role === 'admin' ? 'an admin' : 'a user'}.`); onRefresh(); } catch (err) { - toast.error(err.message || 'Failed to update user role.'); + toast.error(errMessage(err, 'Failed to update user role.')); } finally { setRoleUpdating(null); } }; - const handleActiveChange = async (user, active) => { + const handleActiveChange = async (user: AdminUser, active: boolean) => { setActiveUpdating(user.id); try { await api.updateUserActive(user.id, { active }); toast.success(`${user.username} is now ${active ? 'active' : 'inactive'}.`); onRefresh(); } catch (err) { - toast.error(err.message || 'Failed to update user status.'); + toast.error(errMessage(err, 'Failed to update user status.')); } finally { setActiveUpdating(null); } diff --git a/client/pages/AdminPage.tsx b/client/pages/AdminPage.tsx index 9d7e904..5580813 100644 --- a/client/pages/AdminPage.tsx +++ b/client/pages/AdminPage.tsx @@ -12,6 +12,7 @@ import UsersTable from '@/components/admin/UsersTable'; import AddUserCard from '@/components/admin/AddUserCard'; import BackupManagementCard from '@/components/admin/BackupManagementCard'; import CleanupPanel from '@/components/admin/CleanupPanel'; +import type { AdminUser } from '@/components/admin/UsersTable'; import type { User } from '@/hooks/useAuth'; export default function AdminPage() { @@ -20,7 +21,7 @@ export default function AdminPage() { const [me, setMe] = useState(null); const [hasUsers, setHasUsers] = useState(null); const [loadError, setLoadError] = useState(''); - const [users, setUsers] = useState([]); + const [users, setUsers] = useState([]); const [authMode, setAuthMode] = useState('multi'); const loadMe = useCallback(async () => { @@ -34,7 +35,7 @@ export default function AdminPage() { const loadUsers = useCallback(async () => { try { - const d = await api.adminUsers() as { users?: unknown[] } | unknown[]; + const d = await api.adminUsers() as { users?: AdminUser[] } | AdminUser[]; setUsers(Array.isArray(d) ? d : (d.users || [])); } catch { /* ignore */ } }, []);