# Admin Panel Design - ResolutionFlow **Date:** February 8, 2026 **Status:** Design Complete - Ready for Implementation **Scope:** Phase 1 - Essential Admin Tools ## Overview This document specifies a comprehensive admin panel for ResolutionFlow administrators to manage site-wide settings, users, and platform configuration without direct database access. ### Goals 1. **Operational Support** - Manage users, invite codes, and troubleshoot issues 2. **Growth Insights** - Track platform metrics and user activity 3. **Platform Control** - Configure plans, features, and settings ### Primary Pain Point Currently managing settings manually in database via SQL commands. This admin panel eliminates the need for direct database access. --- ## Architecture ### Route Structure ``` /admin (super_admin only) ├── /dashboard - Overview metrics and activity ├── /users - User management ├── /invite-codes - Invite code management ├── /plan-limits - Plan configuration + account overrides ├── /feature-flags - Feature flags with plan defaults ├── /settings - Platform settings (maintenance mode) ├── /audit-logs - Audit log viewer └── /categories - Global categories (NEW) /account (account_owner only) └── /categories - Team categories (NEW) ``` ### Layout Components **AdminLayout** (240px sidebar): ```typescript export function AdminLayout() { const [sidebarOpen, setSidebarOpen] = useState(true) return (
setSidebarOpen(false)} />
) } ``` **Sidebar Behavior:** - Desktop (≥1024px): Always visible, no overlay - Mobile (<1024px): Overlay with backdrop fade animation - No state persistence (always starts open on desktop) **AccountLayout** (simple wrapper): ```typescript export function AccountLayout() { return (
) } ``` ### Navigation **Admin Sidebar Items:** ```typescript const navItems = [ { path: '/admin/dashboard', label: 'Dashboard', icon: LayoutDashboard }, { path: '/admin/users', label: 'Users', icon: Users }, { path: '/admin/invite-codes', label: 'Invite Codes', icon: Mail }, { path: '/admin/plan-limits', label: 'Plan Limits', icon: Layers }, { path: '/admin/feature-flags', label: 'Feature Flags', icon: Flag }, { path: '/admin/settings', label: 'Platform Settings', icon: Settings }, { path: '/admin/audit-logs', label: 'Audit Logs', icon: FileText }, { path: '/admin/categories', label: 'Global Categories', icon: FolderTree } ] ``` **Main App Navigation:** - "Admin Panel" link (Shield icon) in user dropdown (super_admin only) - "Account Settings" dropdown menu item for account_owner (includes "Team Categories") - "Back to App" always navigates to `/trees` ### Permissions & Protection **Layout Level:** ```typescript ``` **Behavior:** - Silent redirect to `/trees` on permission denied (no toast notification) - Backend always validates permissions (defense in depth) - Error boundary catches React errors, shows fallback UI ### Routing Configuration ```typescript { path: 'admin', element: ( ), children: [ { index: true, element: }, { path: 'dashboard', element: }> }, { path: 'users', element: }> }, { path: 'invite-codes', element: }> }, { path: 'plan-limits', element: }> }, { path: 'feature-flags', element: }> }, { path: 'settings', element: }> }, { path: 'audit-logs', element: }> }, { path: 'categories', element: }> }, { path: '*', element: } ] }, { path: 'account', element: ( ), children: [ { index: true, element: }, { path: 'categories', element: }> }, { path: '*', element: } ] } ``` --- ## Reusable Components ### DataTable Generic paginated table with sorting support. ```typescript interface Column { key: string header: string render: (item: T) => ReactNode // REQUIRED - no fallback sortable?: boolean width?: string } interface DataTableProps { columns: Column[] data: T[] isLoading?: boolean emptyMessage?: string } {user.email}, sortable: true }, { key: 'role', header: 'Role', render: (user) => {user.role} } ]} data={users} isLoading={isLoading} /> ``` **Features:** - Generic type support (``) - Optional loading state (skeleton rows) - Empty state via `EmptyState` component - Sortable columns (client-side) - Responsive width control ### Pagination Smart pagination with ellipsis for large page counts. ```typescript interface PaginationProps { currentPage: number totalPages: number onPageChange: (page: number) => void itemsPerPage?: number totalItems?: number } ``` **Ellipsis Logic:** - Always show: First, Last, Current, Current±1 - Show ellipsis when gap > 1 - Example: `1 ... 5 6 [7] 8 9 ... 20` ### ActionMenu Three-dot dropdown menu for row actions. ```typescript interface MenuItem { label: string icon: LucideIcon onClick: () => void variant?: 'default' | 'destructive' disabled?: boolean } interface ActionMenuProps { items: MenuItem[] align?: 'start' | 'end' } handleEdit(item.id) }, { label: 'Delete', icon: Trash2, onClick: () => handleDelete(item.id), variant: 'destructive' } ]} /> ``` **Phase 1:** No submenus (single-level only) ### StatusBadge Consistent badge styling with 4 variants. ```typescript interface StatusBadgeProps { variant: 'success' | 'destructive' | 'warning' | 'default' children: ReactNode } Active Pending Deactivated ``` **Styling:** ```typescript const variants = { success: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400', destructive: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400', warning: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400', default: 'bg-muted text-muted-foreground' } ``` ### EmptyState Consistent empty state component. ```typescript interface EmptyStateProps { icon: LucideIcon title: string description?: string action?: { label: string onClick: () => void } } ``` ### SearchInput Debounced search input with 300ms delay. ```typescript interface SearchInputProps { value: string onChange: (value: string) => void placeholder?: string className?: string } ``` **Implementation:** ```typescript import { debounce } from 'lodash' const debouncedOnChange = useMemo( () => debounce((value: string) => onChange(value), 300), [onChange] ) useEffect(() => { return () => debouncedOnChange.cancel() }, [debouncedOnChange]) ``` ### PageHeader Consistent page header with title and optional action. ```typescript interface PageHeaderProps { title: string description?: string action?: { label: string icon?: LucideIcon onClick: () => void } } setShowCreateModal(true) }} /> ``` **Phase 1:** Single action only (no multiple actions) --- ## Page Specifications ### 1. Admin Dashboard **Route:** `/admin/dashboard` **Permission:** super_admin **Scope:** Minimal (read-only overview) **Layout:** ``` ┌─────────────────────────────────────────────┐ │ Dashboard [Refresh] │ ├─────────────────────────────────────────────┤ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │Total │ │Active│ │Paid │ │Trees │ │ │ │Users │ │ Subs │ │ Accts│ │Count │ │ │ └──────┘ └──────┘ └──────┘ └──────┘ │ │ │ │ System Status │ │ ┌──────────────────────────────────────────┐│ │ │ API: Healthy DB: Connected Cache: OK ││ │ └──────────────────────────────────────────┘│ │ │ │ Recent Activity │ │ ┌──────────────────────────────────────────┐│ │ │ • User X registered (2 min ago) ││ │ │ • Admin Y updated plan limits (5 min ago)││ │ │ • User Z started session (10 min ago) ││ │ └──────────────────────────────────────────┘│ │ │ │ Quick Links │ │ [Manage Users] [View Audit Logs] [...] │ └─────────────────────────────────────────────┘ ``` **Metrics (4 cards):** - Total Users - Active Subscriptions (status = 'active' OR 'trialing') - Paid Accounts (plan IN ('pro', 'team')) - Total Trees **System Status:** - API: Check if server is responding - Database: Check connection status - Cache: Check settings cache (if applicable) **Recent Activity:** - Last 10 audit log entries - Format: `{user_name} {action} {entity_type} ({time_ago})` - Click to view full audit logs (filtered by item) **Quick Links:** - Manage Users → `/admin/users` - View Audit Logs → `/admin/audit-logs` - Platform Settings → `/admin/settings` **API Endpoints (NEW):** ```python # GET /api/v1/admin/dashboard/metrics { "total_users": 142, "active_subscriptions": 38, "paid_accounts": 12, "total_trees": 567 } # GET /api/v1/admin/dashboard/activity [ { "id": "uuid", "user_name": "John Doe", "user_email": "john@example.com", "action": "user_role_changed", "entity_type": "user", "entity_id": "uuid", "timestamp": "2026-02-08T10:30:00Z", "details": { "old_role": "viewer", "new_role": "engineer" } } ] ``` **Features:** - Manual refresh only (no auto-refresh) - Metric cards do NOT link to filtered pages - Activity feed links to audit logs with filter --- ### 2. User Management **Route:** `/admin/users` **Permission:** super_admin **Uses:** Existing `/api/v1/admin/users/*` endpoints **Layout:** ``` ┌─────────────────────────────────────────────┐ │ Users │ │ Manage platform users and permissions │ ├─────────────────────────────────────────────┤ │ [Search users...] [Filter: All ▼] │ │ │ │ ┌──────────────────────────────────────────┐│ │ │ Email │ Name │ Role │ Status ││ │ ├──────────────────────────────────────────┤│ │ │ john@msp.com │ John │ Engineer│ Active││ │ │ jane@msp.com │ Jane │ Viewer │ Active││ │ └──────────────────────────────────────────┘│ │ │ │ < 1 2 3 ... 10 > (50 per page) │ └─────────────────────────────────────────────┘ ``` **Columns:** 1. Email (sortable, primary identifier) 2. Name (sortable) 3. Account (display_code + account_role badge) 4. Role (engineer/viewer badge) 5. Status (active/deactivated badge) 6. Last Login (sortable, time ago) 7. Actions (dropdown) **Actions (per user):** - Change Role (engineer ↔ viewer) - Toggle Team Admin (if role = engineer) - Deactivate / Activate - Move to Different Account (NEW) - View Audit Logs (filtered by user_id) **Filters:** - All / Active / Deactivated - Role: All / Engineer / Viewer - Team Admin: All / Yes / No - Search: Name or Email (debounced) **Validation Rules:** - Cannot change role of last account_owner (show error toast) - Deactivating user logs them out next token refresh - Moving user to different account requires account_id input (modal) **Pagination:** 50 per page (fixed) **No Bulk Actions in Phase 1** --- ### 3. Invite Code Management **Route:** `/admin/invite-codes` **Permission:** super_admin **System:** Single-use only (no max_uses) **Layout:** ``` ┌─────────────────────────────────────────────┐ │ Invite Codes [Create Code] │ │ Manage platform invite codes │ ├─────────────────────────────────────────────┤ │ [Search codes...] [Status: All ▼] │ │ │ │ ┌──────────────────────────────────────────┐│ │ │ Code │ Created│ Used By│ Status│ ... ││ │ ├──────────────────────────────────────────┤│ │ │ ABC12345│ Jan 1 │ - │ Active│ ... ││ │ │ XYZ67890│ Jan 2 │ john@..│ Used │ ... ││ │ └──────────────────────────────────────────┘│ └─────────────────────────────────────────────┘ ``` **Columns:** 1. Code (8-char, monospace) 2. Created By (admin name) 3. Created At (date, sortable) 4. Used By (email or "-") 5. Used At (date or "-") 6. Expires At (date or "Never") 7. Status (Available/Used/Expired badge) 8. Actions (dropdown) **Actions:** - Copy Code - Deactivate (sets expires_at to now) - Delete (if never used) - View Usage Details **Create Code Modal:** ``` Create Invite Code ┌─────────────────────────────┐ │ Expiration (Optional) │ │ [Date Picker] │ │ │ │ [Cancel] [Create] │ └─────────────────────────────┘ ``` **Code Generation:** Backend generates 8-char code (existing logic) **Filters:** - All / Available / Used / Expired - Search: Code or Used By email **Pagination:** 50 per page --- ### 4. Plan Limits Configuration **Route:** `/admin/plan-limits` **Permission:** super_admin **Tables:** `plan_limits` (existing) + `account_limit_overrides` (NEW) **Layout:** ``` ┌─────────────────────────────────────────────┐ │ Plan Limits [Edit Plans] │ │ Configure plan limits and account overrides │ ├─────────────────────────────────────────────┤ │ Plan Configuration │ │ ┌──────────────────────────────────────────┐│ │ │ Plan │ Trees│ Sessions│ Users│ Export ││ │ ├──────────────────────────────────────────┤│ │ │ Free │ 5 │ 50/mo │ 1 │ MD,Text ││ │ │ Pro │ ∞ │ 500/mo │ 5 │ All ││ │ │ Team │ ∞ │ ∞ │ ∞ │ All+API ││ │ └──────────────────────────────────────────┘│ │ │ │ Account Overrides [Create Override]│ │ ┌──────────────────────────────────────────┐│ │ │ Account │ Plan│ Override │ Note ││ │ ├──────────────────────────────────────────┤│ │ │ ABC12345 │ Free│ 50 trees │ Beta... ││ │ └──────────────────────────────────────────┘│ └─────────────────────────────────────────────┘ ``` **Plan Configuration Table:** - Read-only display of `plan_limits` table - Edit Plans button opens modal with inline editing - Shows: max_trees, max_sessions_per_month, max_users, export_formats, custom_branding, priority_support - ∞ symbol for NULL (unlimited) **Account Overrides Table:** - Columns: Account (display_code + account name), Plan (current), Override Fields, Note, Actions - Override Fields: Only show non-NULL overrides (e.g., "50 trees, ∞ sessions") - Actions: Edit, Delete **Create Override Modal:** ``` Create Account Override ┌─────────────────────────────┐ │ Account ID (display_code) │ │ [ABC12345] │ │ │ │ Override Max Trees │ │ [50] or [∞ Unlimited] │ │ │ │ Override Max Sessions/Month │ │ [500] or [∞ Unlimited] │ │ │ │ Override Max Users │ │ [10] or [∞ Unlimited] │ │ │ │ Note (Optional) │ │ [Beta partner access] │ │ │ │ [Cancel] [Create] │ └─────────────────────────────┘ ``` **Validation:** - Account display_code must exist - At least one override field must be set - Unlimited checkbox sends NULL to backend **Database Schema (NEW):** ```sql CREATE TABLE account_limit_overrides ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, override_max_trees INTEGER, -- NULL = use plan default override_max_sessions_per_month INTEGER, override_max_users INTEGER, note TEXT, created_by_id UUID NOT NULL REFERENCES users(id), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), UNIQUE(account_id) ); CREATE INDEX idx_account_limit_overrides_account ON account_limit_overrides(account_id); ``` **API Endpoints (NEW):** ```python # GET /api/v1/admin/plan-limits # PUT /api/v1/admin/plan-limits # GET /api/v1/admin/account-overrides # POST /api/v1/admin/account-overrides # PUT /api/v1/admin/account-overrides/{id} # DELETE /api/v1/admin/account-overrides/{id} ``` **Backend Logic:** ```python def get_effective_limits(account: Account) -> dict: """Get effective limits for an account (overrides take precedence).""" override = db.query(AccountLimitOverrides).filter_by(account_id=account.id).first() plan_limits = db.query(PlanLimits).filter_by(plan=account.subscription.plan).first() return { "max_trees": override.override_max_trees if override and override.override_max_trees is not None else plan_limits.max_trees, "max_sessions_per_month": override.override_max_sessions_per_month if override and override.override_max_sessions_per_month is not None else plan_limits.max_sessions_per_month, "max_users": override.override_max_users if override and override.override_max_users is not None else plan_limits.max_users, } ``` --- ### 5. Feature Flags **Route:** `/admin/feature-flags` **Permission:** super_admin **Approach:** Plan-based defaults (not global) **Layout:** ``` ┌─────────────────────────────────────────────┐ │ Feature Flags [Create Feature] │ │ Manage plan-based feature access │ ├─────────────────────────────────────────────┤ │ Feature Matrix │ │ ┌──────────────────────────────────────────┐│ │ │ Feature │ Free│ Pro │ Team│ ... ││ │ ├──────────────────────────────────────────┤│ │ │ Advanced Search │ ✗ │ ✓ │ ✓ │ ... ││ │ │ Custom Branding │ ✗ │ ✗ │ ✓ │ ... ││ │ │ API Access │ ✗ │ ✓ │ ✓ │ ... ││ │ └──────────────────────────────────────────┘│ │ │ │ Account Overrides [Create Override]│ │ ┌──────────────────────────────────────────┐│ │ │ Account │ Feature │ Enabled│ Note││ │ ├──────────────────────────────────────────┤│ │ │ ABC12345 │ API Access │ ✓ │ Beta││ │ └──────────────────────────────────────────┘│ └─────────────────────────────────────────────┘ ``` **Database Schema (NEW):** ```sql -- Feature flag definitions CREATE TABLE feature_flags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), flag_key VARCHAR(100) NOT NULL UNIQUE, -- 'advanced_search', 'api_access' display_name VARCHAR(255) NOT NULL, description TEXT, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() ); -- Plan defaults (which features each plan gets) CREATE TABLE plan_feature_defaults ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), plan VARCHAR(50) NOT NULL REFERENCES plan_limits(plan), flag_id UUID NOT NULL REFERENCES feature_flags(id) ON DELETE CASCADE, enabled BOOLEAN NOT NULL DEFAULT false, UNIQUE(plan, flag_id) ); -- Per-account exceptions CREATE TABLE account_feature_overrides ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, flag_id UUID NOT NULL REFERENCES feature_flags(id) ON DELETE CASCADE, enabled BOOLEAN NOT NULL, note TEXT, created_by_id UUID NOT NULL REFERENCES users(id), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), UNIQUE(account_id, flag_id) ); CREATE INDEX idx_plan_feature_defaults_plan ON plan_feature_defaults(plan); CREATE INDEX idx_account_feature_overrides_account ON account_feature_overrides(account_id); ``` **Feature Matrix:** - Rows: Feature flags (display_name) - Columns: Plan names (free, pro, team) - Cells: Checkmark (✓) or X (✗) - Click cell to toggle (updates plan_feature_defaults) **Account Overrides:** - Columns: Account (display_code), Feature (display_name), Enabled (toggle), Note, Actions - Actions: Edit Note, Delete Override **Create Override Modal:** ``` Override Feature for Account ┌─────────────────────────────┐ │ Account ID (display_code) │ │ [ABC12345] │ │ │ │ Feature │ │ [Advanced Search ▼] │ │ │ │ Enable Feature │ │ [Toggle Switch: ON] │ │ │ │ Note (Optional) │ │ [Beta partner access] │ │ │ │ [Cancel] [Create] │ └─────────────────────────────┘ ``` **Backend Logic:** ```python def is_feature_enabled(account: Account, flag_key: str) -> bool: """Check if a feature is enabled for an account (override > plan default).""" flag = db.query(FeatureFlag).filter_by(flag_key=flag_key).first() if not flag: return False # Check override first override = db.query(AccountFeatureOverride).filter_by( account_id=account.id, flag_id=flag.id ).first() if override: return override.enabled # Fall back to plan default plan_default = db.query(PlanFeatureDefault).filter_by( plan=account.subscription.plan, flag_id=flag.id ).first() return plan_default.enabled if plan_default else False ``` **API Endpoints (NEW):** ```python # Feature flags # GET /api/v1/admin/feature-flags # POST /api/v1/admin/feature-flags # PUT /api/v1/admin/feature-flags/{id} # DELETE /api/v1/admin/feature-flags/{id} # Plan defaults (batch update for matrix) # PUT /api/v1/admin/feature-flags/plan-defaults # Body: { "plan": "pro", "flag_id": "uuid", "enabled": true } # Account overrides # GET /api/v1/admin/feature-flags/account-overrides # POST /api/v1/admin/feature-flags/account-overrides # DELETE /api/v1/admin/feature-flags/account-overrides/{id} ``` --- ### 6. Platform Settings **Route:** `/admin/settings` **Permission:** super_admin **Scope:** Minimal - Maintenance Mode Only **Layout:** ``` ┌─────────────────────────────────────────────┐ │ Platform Settings │ │ Configure platform-wide settings │ ├─────────────────────────────────────────────┤ │ Maintenance Mode │ │ ┌──────────────────────────────────────────┐│ │ │ Enable Maintenance Mode ││ │ │ [Toggle Switch: OFF] ││ │ │ ││ │ │ Maintenance Message ││ │ │ ┌────────────────────────────────────┐ ││ │ │ │ We're performing scheduled │ ││ │ │ │ maintenance. We'll be back soon! │ ││ │ │ └────────────────────────────────────┘ ││ │ │ ││ │ │ [Save Changes] ││ │ └──────────────────────────────────────────┘│ └─────────────────────────────────────────────┘ ``` **Database Schema (NEW):** ```sql CREATE TABLE platform_settings ( setting_key VARCHAR(100) PRIMARY KEY, setting_value TEXT, -- JSON string for complex types data_type VARCHAR(20), -- 'boolean', 'integer', 'string', 'json' is_sensitive BOOLEAN DEFAULT false, -- Hide value in audit logs updated_by_id UUID REFERENCES users(id), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() ); -- Seed maintenance mode settings INSERT INTO platform_settings (setting_key, setting_value, data_type) VALUES ('maintenance_mode', 'false', 'boolean'), ('maintenance_message', 'We''re performing scheduled maintenance. We''ll be back soon!', 'string'); ``` **Settings Manager (NEW):** ```python # backend/app/core/settings_manager.py from datetime import datetime from typing import Any, Optional from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.models.platform_settings import PlatformSetting class SettingsManager: """Manage runtime platform settings with in-memory cache.""" _cache: dict[str, Any] = {} _cache_time: float = 0 CACHE_TTL = 60 # 1 minute cache (safe for single Railway instance) @classmethod async def get(cls, key: str, db: AsyncSession, default: Any = None) -> Any: """Get a setting value (cached).""" import time # Check cache if time.time() - cls._cache_time < cls.CACHE_TTL and key in cls._cache: return cls._cache[key] # Query database result = await db.execute( select(PlatformSetting).where(PlatformSetting.setting_key == key) ) setting = result.scalar_one_or_none() if not setting: return default # Parse value based on data type value = cls._parse_value(setting.setting_value, setting.data_type) # Update cache cls._cache[key] = value cls._cache_time = time.time() return value @classmethod async def set(cls, key: str, value: Any, db: AsyncSession, user_id: uuid.UUID) -> None: """Set a setting value (invalidates cache).""" result = await db.execute( select(PlatformSetting).where(PlatformSetting.setting_key == key) ) setting = result.scalar_one_or_none() if setting: setting.setting_value = str(value) setting.updated_by_id = user_id setting.updated_at = datetime.now(timezone.utc) else: setting = PlatformSetting( setting_key=key, setting_value=str(value), data_type=cls._infer_type(value), updated_by_id=user_id ) db.add(setting) # Invalidate cache cls._cache_time = 0 if key in cls._cache: del cls._cache[key] @staticmethod def _parse_value(value: str, data_type: str) -> Any: if data_type == 'boolean': return value.lower() == 'true' elif data_type == 'integer': return int(value) elif data_type == 'json': import json return json.loads(value) return value # string @staticmethod def _infer_type(value: Any) -> str: if isinstance(value, bool): return 'boolean' elif isinstance(value, int): return 'integer' elif isinstance(value, (dict, list)): return 'json' return 'string' ``` **Maintenance Mode Middleware (NEW):** ```python # backend/app/core/middleware.py from fastapi import Request from fastapi.responses import JSONResponse from app.core.settings_manager import SettingsManager from app.core.database import get_db @app.middleware("http") async def maintenance_mode_middleware(request: Request, call_next): # Skip for admin endpoints if request.url.path.startswith("/api/v1/admin"): return await call_next(request) # Check maintenance mode async for db in get_db(): is_maintenance = await SettingsManager.get("maintenance_mode", db, default=False) if is_maintenance: message = await SettingsManager.get( "maintenance_message", db, default="We're performing scheduled maintenance." ) return JSONResponse( status_code=503, content={"detail": message} ) return await call_next(request) ``` **API Endpoints (NEW):** ```python # GET /api/v1/admin/settings # PUT /api/v1/admin/settings # Body: { "maintenance_mode": true, "maintenance_message": "..." } ``` **Features:** - Toggle maintenance mode (affects all non-admin endpoints) - Edit maintenance message (markdown support) - Preview message before saving - Audit log on save **Phase 2 Settings:** - Registration settings (REQUIRE_INVITE_CODE) - Rate limit overrides - SMTP configuration - Session timeout --- ### 7. Audit Log Viewer **Route:** `/admin/audit-logs` **Permission:** super_admin **Scope:** Minimal - Essential viewing + CSV export **Layout:** ``` ┌─────────────────────────────────────────────┐ │ Audit Logs [Export CSV] │ │ View platform activity and changes │ ├─────────────────────────────────────────────┤ │ [Search...] [Action ▼] [Entity ▼] [User ▼] │ │ [Date: Last 30 days ▼] [Apply Filters]│ │ │ │ ┌──────────────────────────────────────────┐│ │ │ Time │ User │ Action│ Entity│ Details ││ │ ├──────────────────────────────────────────┤│ │ │ 10:30 │ Admin │ Role │ User │ {...} ││ │ │ 10:25 │ Admin │ Delete│ Tree │ {...} ││ │ └──────────────────────────────────────────┘│ │ │ │ < 1 2 3 ... 10 > (50 per page) │ └─────────────────────────────────────────────┘ ``` **Columns:** 1. Timestamp (date + time, sortable) 2. User (name + email, click to filter) 3. Action (badge: user_created, user_role_changed, tree_deleted, etc.) 4. Entity Type (user, tree, plan_limits, etc.) 5. Entity ID (first 8 chars, monospace) 6. IP Address 7. Details (expandable JSON viewer) **Filters:** - Action: All / User Actions / Tree Actions / Settings Changes / etc. - Entity Type: All / User / Tree / Account / Subscription / etc. - User: Dropdown (autocomplete) - Date Range: Last 7/30/90 days, Custom, All Time - Search: Entity ID or details JSON (server-side) **Auto-Apply Filters:** - Changing any filter automatically applies it (no "Apply" button needed) - URL params track filter state (`?action=user_created&entity=user`) - Pagination resets to page 1 on filter change **CSV Export:** - 10,000 row limit (show warning if more) - Respects current filters - Columns: timestamp, user_email, action, entity_type, entity_id, ip_address, details_json - Filename: `audit-logs-{date}.csv` **Backend Optimization:** ```python # N+1 query fix: LEFT JOIN to get user email in single query query = ( select(AuditLog, User.email.label('user_email')) .outerjoin(User, AuditLog.user_id == User.id) .order_by(AuditLog.created_at.desc()) ) ``` **API Endpoints (Existing + NEW):** ```python # GET /api/v1/admin/audit-logs?page=1&per_page=50&action=user_created&entity_type=user&user_id=uuid&date_from=2026-01-01&date_to=2026-02-08&search=keyword # GET /api/v1/admin/audit-logs/export (CSV, 10k limit, respects filters) ``` **Phase 2 Features:** - Real-time updates (WebSocket or polling) - JSON export - "View Related Logs" action (filter by entity_id) --- ### 8. Global Categories (NEW) **Route:** `/admin/categories` **Permission:** super_admin **Scope:** Manage global categories (account_id = NULL) **Layout:** ``` ┌─────────────────────────────────────────────┐ │ Global Categories [Create Category]│ │ Manage global decision tree categories │ ├─────────────────────────────────────────────┤ │ [Search categories...] │ │ │ │ ┌──────────────────────────────────────────┐│ │ │ Name │ Slug │ Trees│ ... ││ │ ├──────────────────────────────────────────┤│ │ │ 🌍 Networking │ networking│ 45 │ ... ││ │ │ 🌍 Security │ security │ 32 │ ... ││ │ │ 🌍 Hardware │ hardware │ 18 │ ... ││ │ └──────────────────────────────────────────┘│ └─────────────────────────────────────────────┘ ``` **Features:** - Create/Edit/Archive global categories - Globe icon (🌍) prefix for all global categories - Tree count (simple N+1 query acceptable) - Archive instead of delete (allows undo) - Can archive even if trees are using it (trees keep reference) **API Endpoints (NEW):** ```python # Global categories only (account_id IS NULL) # GET /api/v1/admin/categories/global # POST /api/v1/admin/categories/global # PUT /api/v1/admin/categories/global/{id} # DELETE /api/v1/admin/categories/global/{id} # Sets is_archived=true ``` --- ### 9. Team Categories (NEW) **Route:** `/account/categories` **Permission:** account_owner **Scope:** Manage team-specific categories (account_id = current user's account) **Layout:** ``` ┌─────────────────────────────────────────────┐ │ Team Categories [Create Category]│ │ Manage your team's decision tree categories │ ├─────────────────────────────────────────────┤ │ [Search categories...] │ │ │ │ ┌──────────────────────────────────────────┐│ │ │ Name │ Slug │ Trees│ ... ││ │ ├──────────────────────────────────────────┤│ │ │ 👥 Client ABC │ client-abc│ 12 │ ... ││ │ │ 👥 Internal IT │ internal │ 8 │ ... ││ │ └──────────────────────────────────────────┘│ └─────────────────────────────────────────────┘ ``` **Features:** - Create/Edit/Archive team categories - Users icon (👥) prefix for all team categories - Tree count (simple N+1 query acceptable) - Archive instead of delete - Can archive even if trees are using it **API Endpoints (Existing):** ```python # Team categories (account_id = current user's account) # GET /api/v1/categories (filtered by account_id) # POST /api/v1/categories # PUT /api/v1/categories/{id} # DELETE /api/v1/categories/{id} # Sets is_archived=true ``` **Navigation:** - Add "Account Settings" dropdown to user menu - Menu items: "Team Categories", "Billing" (future), "Team Members" (future) --- ## Technical Implementation ### Frontend Structure **New Files:** ``` frontend/src/ ├── components/ │ ├── admin/ │ │ ├── AdminLayout.tsx │ │ ├── AdminSidebar.tsx │ │ ├── DataTable.tsx │ │ ├── Pagination.tsx │ │ ├── ActionMenu.tsx │ │ ├── StatusBadge.tsx │ │ ├── EmptyState.tsx │ │ ├── SearchInput.tsx │ │ └── PageHeader.tsx │ └── account/ │ └── AccountLayout.tsx ├── pages/ │ ├── admin/ │ │ ├── DashboardPage.tsx │ │ ├── UsersPage.tsx │ │ ├── InviteCodesPage.tsx │ │ ├── PlanLimitsPage.tsx │ │ ├── FeatureFlagsPage.tsx │ │ ├── SettingsPage.tsx │ │ ├── AuditLogsPage.tsx │ │ └── GlobalCategoriesPage.tsx │ └── account/ │ └── TeamCategoriesPage.tsx └── api/ └── admin.ts (single file for all admin endpoints) ``` ### Backend Structure **New Files:** ``` backend/app/ ├── models/ │ ├── account_limit_overrides.py │ ├── feature_flags.py (3 models) │ └── platform_settings.py ├── api/endpoints/ │ ├── admin_audit.py (audit log endpoints) │ ├── admin_dashboard.py (dashboard metrics) │ └── admin_categories.py (global categories) └── core/ └── settings_manager.py (runtime settings cache) ``` **Updated Files:** - `backend/app/api/endpoints/admin.py` - Add move user to account endpoint - `backend/app/api/router.py` - Add new admin endpoints - `backend/app/core/middleware.py` - Add maintenance mode middleware ### Migrations **New Alembic Migrations:** 1. `024_account_limit_overrides.py` - Account-specific limit overrides 2. `025_feature_flags.py` - Feature flag system (3 tables) 3. `026_platform_settings.py` - Runtime settings table ### Dependencies **Frontend:** ```json { "lodash": "^4.17.21", // Debounce utility "@types/lodash": "^4.14.202" // TypeScript types } ``` **Backend:** - No new dependencies (uses existing FastAPI, SQLAlchemy, etc.) --- ## Security & Permissions ### Role Hierarchy ``` super_admin (is_super_admin=true) └─ Can access /admin/* routes └─ Can view/modify all accounts └─ Cannot be demoted by other admins account_owner (account_role='owner') └─ Can access /account/* routes └─ Can manage team categories └─ Cannot access admin panel engineer (role='engineer') └─ Can create/edit trees, steps └─ Cannot access admin or account settings viewer (role='viewer') └─ Can browse trees, start sessions └─ Cannot create/edit content ``` ### Permission Checks **Backend (always validates):** ```python # Existing dependency from app.api.deps import get_current_active_user, require_admin # All admin endpoints @router.get("/admin/users") async def list_users( current_user: Annotated[User, Depends(require_admin)], db: Annotated[AsyncSession, Depends(get_db)] ): ... ``` **Frontend (UI only):** ```typescript const { isSuperAdmin, isAccountOwner } = usePermissions() // Route protection // Conditional rendering {isSuperAdmin && Admin Panel} {isAccountOwner && Team Categories} ``` ### Audit Logging All admin actions must be logged: ```python from app.core.audit import log_audit await log_audit( db=db, user_id=current_user.id, action="user_role_changed", entity_type="user", entity_id=user.id, ip_address=request.client.host, details={ "old_role": old_role, "new_role": new_role } ) ``` **Logged Actions:** - User role changes - Team admin toggles - User deactivations - Account moves - Plan limit edits - Feature flag toggles - Platform settings changes - Category create/edit/archive --- ## Phase 2 Enhancements Features deferred to Phase 2: ### Dashboard - Real-time metrics (auto-refresh) - Charts/graphs (revenue trends, usage over time) - Quick actions section - Clickable metric cards (link to filtered views) ### User Management - Bulk actions (bulk role change, bulk deactivate) - Advanced filters (account search, date ranges) - User activity timeline - Export users to CSV ### Invite Codes - Multi-use codes with usage tracking - Code templates/presets - Email integration (send invite via email) ### Audit Logs - Real-time updates (WebSocket) - JSON export format - "View Related Logs" action - Advanced search (full-text on details JSON) ### Settings - Registration settings (toggle REQUIRE_INVITE_CODE) - Rate limit overrides (per-user/per-account) - SMTP configuration (email settings) - Session timeout settings ### Categories - Drag-and-drop reordering - Category icons/colors - Tree count optimization (cached/precomputed) - Nested categories (hierarchy) ### Feature Flags - A/B testing support - Gradual rollout (percentage-based) - User-level overrides (not just account-level) - Flag usage analytics ### New Pages - Account Management (list all accounts, edit plan/status) - Subscription Management (Stripe integration) - Analytics Dashboard (detailed usage metrics) - Support Tickets (if support system is added) --- ## Estimation ### Development Time | Component | Hours | |-----------|-------| | Reusable Components (7) | 8-12 | | Backend Models & Migrations (3) | 6-8 | | Backend API Endpoints (20+) | 12-16 | | Dashboard Page | 4-6 | | User Management Page | 6-8 | | Invite Codes Page | 4-6 | | Plan Limits Page | 6-8 | | Feature Flags Page | 8-10 | | Settings Page | 3-4 | | Audit Logs Page | 6-8 | | Global Categories Page | 4-6 | | Team Categories Page | 3-4 | | Settings Manager & Middleware | 4-6 | | Testing & Bug Fixes | 10-15 | | **Total** | **84-117 hours** | ### Phase Breakdown **Phase 1A - Foundation (20-25 hours):** - Reusable components - AdminLayout & AccountLayout - Backend models & migrations - Settings manager **Phase 1B - Core Pages (30-40 hours):** - Dashboard - User Management - Invite Codes - Audit Logs **Phase 1C - Configuration (25-35 hours):** - Plan Limits - Feature Flags - Platform Settings - Categories (global + team) **Phase 1D - Testing & Polish (10-15 hours):** - Integration tests - Permission checks - UI polish - Documentation --- ## Testing Strategy ### Backend Tests **Unit Tests:** - Settings manager cache behavior - Feature flag resolution logic - Effective limits calculation - Audit log helper **Integration Tests:** ```python # test_admin_users.py async def test_list_users_as_admin(client, test_admin): """Super admin can list all users.""" response = await client.get("/api/v1/admin/users", headers=auth_headers(test_admin)) assert response.status_code == 200 async def test_change_user_role(client, test_admin, test_user): """Super admin can change user roles.""" response = await client.put( f"/api/v1/admin/users/{test_user.id}/role", headers=auth_headers(test_admin), json={"role": "viewer"} ) assert response.status_code == 200 # test_admin_permissions.py async def test_non_admin_cannot_access_admin_endpoints(client, test_user): """Engineers cannot access admin endpoints.""" response = await client.get("/api/v1/admin/users", headers=auth_headers(test_user)) assert response.status_code == 403 # test_feature_flags.py async def test_feature_flag_override(db, test_account): """Account overrides take precedence over plan defaults.""" # Setup: Free plan has advanced_search = False by default # Create override: Enable advanced_search for this account # Assert: is_feature_enabled returns True # test_plan_limits.py async def test_effective_limits_with_override(db, test_account): """Account overrides take precedence over plan limits.""" # Setup: Free plan has max_trees = 5 # Create override: max_trees = 50 # Assert: get_effective_limits returns 50 ``` ### Frontend Tests **Component Tests:** - DataTable rendering & sorting - Pagination ellipsis logic - SearchInput debounce behavior - ActionMenu dropdown functionality **Integration Tests:** - Admin route protection - Permission-based UI rendering - API error handling - Form validation **E2E Tests (Cypress/Playwright):** ```javascript describe('Admin Panel', () => { it('super admin can access admin panel', () => { cy.loginAs('admin@example.com') cy.visit('/admin/dashboard') cy.contains('Dashboard').should('be.visible') }) it('engineer cannot access admin panel', () => { cy.loginAs('engineer@example.com') cy.visit('/admin/dashboard') cy.url().should('eq', '/trees') // Redirected }) it('can change user role', () => { cy.loginAs('admin@example.com') cy.visit('/admin/users') cy.get('[data-testid="user-row-1"]').find('[data-testid="action-menu"]').click() cy.contains('Change Role').click() cy.get('[data-testid="role-select"]').select('Viewer') cy.contains('Save').click() cy.contains('Role updated successfully').should('be.visible') }) }) ``` --- ## Migration Path ### Existing Users No impact - admin panel is new functionality. Existing permissions continue to work. ### Existing Data - Plan limits: Existing `plan_limits` table is read-only in UI - Categories: Existing `/categories` endpoint works for team categories - Audit logs: Existing `audit_logs` table is queried with optimizations ### Deployment Steps 1. **Backend:** ```bash cd backend alembic upgrade head # Run 3 new migrations python -m scripts.seed_plan_limits # Seed plan defaults if needed ``` 2. **Frontend:** ```bash cd frontend npm install # Install lodash npm run build ``` 3. **Verify:** - Create first super admin (manually set `is_super_admin=true` in DB) - Login as super admin - Verify `/admin/dashboard` is accessible - Test one CRUD operation per page --- ## Documentation ### For Admins Create `docs/admin-guide.md`: - How to access admin panel - Overview of each page - Common tasks (create user, change plan, enable feature) - Troubleshooting (cannot access admin, changes not applying) ### For Developers Update `CLAUDE.md`: - Add admin panel architecture section - Document settings manager usage - Add feature flag check pattern - Update API endpoints reference ### API Documentation Auto-generated via FastAPI OpenAPI at `/api/docs` - no manual updates needed. --- ## Appendix ### Database ER Diagram ``` ┌─────────────┐ ┌──────────────────┐ │ users │──────>│ accounts │ │ - id │ │ - id │ │ - email │ │ - name │ │ - is_super_admin│ │ - display_code │ └─────────────┘ └──────────────────┘ │ │ │ V │ ┌──────────────────┐ │ │ subscriptions │ │ │ - account_id │ │ │ - plan │ │ └──────────────────┘ │ │ V │ ┌─────────────┐ │ │ audit_logs │ │ │ - user_id │ │ │ - action │ │ │ - details │ │ └─────────────┘ │ │ ┌─────────────────────┤ V │ ┌──────────────────┐ │ │ plan_limits │ │ │ - plan (PK) │<────────┘ │ - max_trees │ │ - max_sessions │ └──────────────────┘ ^ │ ┌──────────────────────────┐ │ account_limit_overrides │ │ - account_id │ │ - override_max_trees │ └──────────────────────────┘ ┌──────────────────┐ ┌──────────────────────┐ │ feature_flags │<──────│ plan_feature_defaults│ │ - flag_key │ │ - plan │ │ - display_name │ │ - flag_id │ └──────────────────┘ │ - enabled │ ^ └──────────────────────┘ │ ┌──────────────────────────┐ │ account_feature_overrides│ │ - account_id │ │ - flag_id │ │ - enabled │ └──────────────────────────┘ ┌──────────────────┐ │ platform_settings│ │ - setting_key │ │ - setting_value │ │ - data_type │ └──────────────────┘ ``` ### Key Design Decisions 1. **Plan-Based Feature Flags** (not global defaults) - Aligns with SaaS model 2. **Database-Backed Settings** (not Redis) - Simpler for Phase 1, single instance 3. **60s Cache for Settings** - Acceptable latency for single Railway instance 4. **Simple N+1 for Tree Counts** - Low volume (<1k categories), acceptable 5. **Archive Instead of Delete** - All destructive actions are soft deletes 6. **Single Admin API File** - Easier to maintain than split across modules 7. **No Bulk Actions** - Deferred to Phase 2 to reduce complexity 8. **Minimal Dashboard** - Focus on configuration tools over analytics ### Related Issues - **Issue #40** - Admin panel foundation (this design) - **Issue #41** - Account management (Phase 2) - **Issue #42** - Subscription billing (Phase 2) - **Issue #43** - Analytics dashboard (Phase 2) --- ## Sign-Off **Design Status:** Complete - Ready for Implementation **Estimated Effort:** 84-117 hours (10-15 working days) **Phase 1 Scope:** All 9 pages + reusable components + backend infrastructure **Phase 2 Deferred:** Bulk actions, real-time updates, advanced analytics **Next Steps:** 1. Review this design document 2. Approve Phase 1 scope 3. Create feature branch: `feat/admin-panel` 4. Begin implementation (Foundation phase) 5. Iterative testing and refinement **Questions or Concerns:** Discuss before implementation begins.