From 4f57c84d43a39e5ccb22177350d74ba77a212173 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sun, 8 Feb 2026 01:54:09 -0500 Subject: [PATCH] docs: add comprehensive admin panel design document Co-Authored-By: Claude Sonnet 4.5 --- docs/plans/2026-02-08-admin-panel-design.md | 1703 +++++++++++++++++++ 1 file changed, 1703 insertions(+) create mode 100644 docs/plans/2026-02-08-admin-panel-design.md diff --git a/docs/plans/2026-02-08-admin-panel-design.md b/docs/plans/2026-02-08-admin-panel-design.md new file mode 100644 index 00000000..ce2bcd81 --- /dev/null +++ b/docs/plans/2026-02-08-admin-panel-design.md @@ -0,0 +1,1703 @@ +# 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.