Proper admin panel at /admin
Replaces the table tucked into Settings with a real instance dashboard, gated server-side to ADMIN_USERS (frontend nav entry appears for admins only). Backend (/api/admin/*, admin-token required): - GET overview: uptime, RSS/heap, data-dir bytes, account count vs MAX_USERS, default quota, campaign/character totals, live-room load - GET users: created date, usage vs effective quota, session count, admin flag - POST users/:name/revoke — log a user out everywhere - DELETE users/:name — delete account + backup + owned campaigns + published characters (admin accounts are protected from deletion) - GET/DELETE campaigns — oversight + removal incl. published characters - GET rooms — players/seats/images/idle per room; join codes never exposed Frontend: new /admin page (health cards, account management with quota editor + two-click destructive confirms, campaign table, live-room table), ShieldCheck nav entry via useIsAdmin(), Settings now links to the panel instead of embedding it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { Link, Outlet, useRouterState } from '@tanstack/react-router';
|
||||
import {
|
||||
Crown, LayoutDashboard, UserRound, Swords, Map as MapIcon, Dices, BookOpenText, RadioTower,
|
||||
LibraryBig, Settings as SettingsIcon, Search, PanelLeftClose, PanelLeftOpen, Sun, Moon, PanelRight,
|
||||
ScrollText, Drama, Target, CalendarDays, FlaskConical, Sparkles, Wand2,
|
||||
ScrollText, Drama, Target, CalendarDays, FlaskConical, Sparkles, Wand2, ShieldCheck,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { useUiStore } from '@/stores/uiStore';
|
||||
@@ -24,6 +24,7 @@ import { SignalsBell } from '@/features/assistant/SignalsBell';
|
||||
import { SyncStatusIndicator } from '@/features/cloud/SyncStatusIndicator';
|
||||
import { SessionSidebar } from '@/features/play/SessionSidebar';
|
||||
import { useSessionStore } from '@/stores/sessionStore';
|
||||
import { useIsAdmin } from '@/features/admin/useIsAdmin';
|
||||
|
||||
type NavItem = { to: string; label: string; icon: LucideIcon; exact: boolean; group: 'top' | 'play' | 'world' | 'reference' };
|
||||
const NAV: NavItem[] = [
|
||||
@@ -100,6 +101,11 @@ export function RootLayout() {
|
||||
useSessionBroadcaster(activeCampaign ?? null);
|
||||
usePlayerConnection();
|
||||
useCloudAutosave();
|
||||
// Instance admins get an extra nav entry (server re-checks every /api/admin call).
|
||||
const isAdmin = useIsAdmin();
|
||||
const nav: NavItem[] = isAdmin
|
||||
? [...NAV, { to: '/admin', label: 'Admin', icon: ShieldCheck, exact: false, group: 'reference' }]
|
||||
: NAV;
|
||||
|
||||
// Global Ctrl/Cmd+K opens the command palette.
|
||||
useEffect(() => {
|
||||
@@ -148,7 +154,7 @@ export function RootLayout() {
|
||||
{GROUPS.map((group) => (
|
||||
<div key={group.id}>
|
||||
{group.label && showLabel && <div className="smallcaps hidden px-6 pt-4 pb-1.5 sm:block" style={{ fontSize: 9.5 }}>{group.label}</div>}
|
||||
{NAV.filter((n) => n.group === group.id).map((item) => {
|
||||
{nav.filter((n) => n.group === group.id).map((item) => {
|
||||
const active = item.exact ? pathname === item.to : pathname.startsWith(item.to);
|
||||
const Ico = item.icon;
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user