import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { Link } from 'react-router-dom'; import { CheckCircle2, Clock, FolderKanban, Megaphone, Pencil, ShieldCheck, Trash2, Users, XCircle, } from 'lucide-react'; import { createBanner, deleteBanner, deleteProject, getAdminOverview, listAdminBanners, listAdminUsers, listProjects, setProjectApproval, setUserAdmin, updateBanner, updateProject, type AdminUser, type Banner, type ProjectEntity, } from '@/lib/api-client'; import { useAuthSession } from '@/lib/use-auth-session'; import { useDemoMode } from '@/lib/demo-mode'; import { DemoRestricted, LoadingState, useToast } from '@/shared/components'; const statLabels: Record = { users: 'Users', projects: 'Projects', pending_projects: 'Pending approval', services: 'Services', running_services: 'Running', databases: 'Databases', agents: 'Agents', templates: 'Templates', user_templates: 'User templates', cron_jobs: 'Cron jobs', deployments_total: 'Deployments', }; export function AdminPage() { const queryClient = useQueryClient(); const { showToast } = useToast(); const isDemoMode = useDemoMode(); const sessionQuery = useAuthSession({ enabled: !isDemoMode }); const myId = sessionQuery.data?.user.id; const overviewQuery = useQuery({ queryKey: ['admin-overview'], queryFn: getAdminOverview, enabled: !isDemoMode }); const usersQuery = useQuery({ queryKey: ['admin-users'], queryFn: listAdminUsers, enabled: !isDemoMode }); const projectsQuery = useQuery({ queryKey: ['admin-projects'], queryFn: () => listProjects({ limit: 100 }), enabled: !isDemoMode }); const bannersQuery = useQuery({ queryKey: ['admin-banners'], queryFn: listAdminBanners, enabled: !isDemoMode }); const [editingProject, setEditingProject] = useState(null); const [editForm, setEditForm] = useState({ name: '', description: '' }); const [bannerForm, setBannerForm] = useState({ title: '', body: '', level: 'info' as Banner['level'] }); const invalidate = () => { queryClient.invalidateQueries({ queryKey: ['admin-overview'] }); queryClient.invalidateQueries({ queryKey: ['admin-users'] }); queryClient.invalidateQueries({ queryKey: ['admin-projects'] }); queryClient.invalidateQueries({ queryKey: ['admin-banners'] }); queryClient.invalidateQueries({ queryKey: ['active-banners'] }); queryClient.invalidateQueries({ queryKey: ['projects'] }); }; const bannerCreateMutation = useMutation({ mutationFn: () => createBanner({ title: bannerForm.title.trim(), body: bannerForm.body.trim(), level: bannerForm.level, }), onSuccess: () => { showToast('success', 'Banner published'); setBannerForm({ title: '', body: '', level: 'info' }); invalidate(); }, onError: (error) => showToast('error', 'Create failed', error instanceof Error ? error.message : undefined), }); const bannerToggleMutation = useMutation({ mutationFn: ({ id, active }: { id: string; active: boolean }) => updateBanner(id, { active }), onSuccess: () => invalidate(), onError: (error) => showToast('error', 'Update failed', error instanceof Error ? error.message : undefined), }); const bannerDeleteMutation = useMutation({ mutationFn: (id: string) => deleteBanner(id), onSuccess: () => { showToast('success', 'Banner deleted'); invalidate(); }, onError: (error) => showToast('error', 'Delete failed', error instanceof Error ? error.message : undefined), }); const approvalMutation = useMutation({ mutationFn: ({ id, approved }: { id: string; approved: boolean }) => setProjectApproval(id, approved), onSuccess: (_, vars) => { showToast('success', vars.approved ? 'Project approved' : 'Project unapproved'); invalidate(); }, onError: (error) => showToast('error', 'Update failed', error instanceof Error ? error.message : undefined), }); const adminMutation = useMutation({ mutationFn: ({ id, isAdmin }: { id: string; isAdmin: boolean }) => setUserAdmin(id, isAdmin), onSuccess: (_, vars) => { showToast('success', vars.isAdmin ? 'Admin granted' : 'Admin revoked'); invalidate(); }, onError: (error) => showToast('error', 'Update failed', error instanceof Error ? error.message : undefined), }); const editMutation = useMutation({ mutationFn: () => updateProject(editingProject!.id, { name: editForm.name.trim(), description: editForm.description.trim(), }), onSuccess: () => { showToast('success', 'Project updated'); setEditingProject(null); invalidate(); }, onError: (error) => showToast('error', 'Update failed', error instanceof Error ? error.message : undefined), }); const deleteMutation = useMutation({ mutationFn: (id: string) => deleteProject(id), onSuccess: () => { showToast('success', 'Project deleted'); invalidate(); }, onError: (error) => showToast('error', 'Delete failed', error instanceof Error ? error.message : undefined), }); if (isDemoMode) { return ; } if (overviewQuery.isPending) { return ; } if (overviewQuery.isError) { return (

Admin access required

This area is restricted to platform administrators.

); } const overview = overviewQuery.data; const stats = overview?.stats ?? {}; const pending = overview?.pending_projects ?? []; const users = usersQuery.data ?? []; const allProjects = projectsQuery.data ?? []; const banners = bannersQuery.data ?? []; return (

Admin console

Platform overview, project approvals, and user management.

{Object.entries(statLabels).map(([key, label]) => (

{stats[key] ?? 0}

{label}

))}

Pending approvals

{pending.length > 0 ? ( {pending.length} ) : null}
{pending.length > 0 ? (
{pending.map((project) => (
{project.name}

{project.description || 'No description'}

))}
) : (
Nothing waiting for approval.
)}

All projects

{allProjects.map((project) => (
{project.name}

{project.stats?.service_count ?? 0} services · {project.stats?.deployment_count ?? 0} deploys

{project.isApproved ? 'public' : 'pending'}
))} {allProjects.length === 0 ? (

No projects yet.

) : null}

Announcements

setBannerForm((f) => ({ ...f, title: e.target.value }))} placeholder="Title" className="h-9 min-w-48 flex-1 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-3 text-sm focus:border-[var(--accent-primary)]" /> setBannerForm((f) => ({ ...f, body: e.target.value }))} placeholder="Body (optional)" className="h-9 min-w-48 flex-[2] rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-3 text-sm focus:border-[var(--accent-primary)]" />
{banners.map((banner) => (

{banner.title}

{banner.level} {banner.body ? ` · ${banner.body}` : ''}

))} {banners.length === 0 ? (

No announcements.

) : null}

Users

{users.map((user) => ( adminMutation.mutate({ id: user.id, isAdmin })} /> ))} {users.length === 0 ? (

No users.

) : null}
{editingProject ? (

Edit project

Admin edit — changes apply immediately.

setEditForm((f) => ({ ...f, name: e.target.value }))} className="h-10 w-full rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-3 text-sm focus:border-[var(--accent-primary)]" />