# 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 (
    <div className="flex h-screen overflow-hidden">
      <AdminSidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
      <div className="flex-1 flex flex-col overflow-hidden">
        <main className="flex-1 overflow-y-auto bg-muted/30 p-6">
          <div className="max-w-screen-2xl mx-auto">
            <Outlet />
          </div>
        </main>
      </div>
    </div>
  )
}
```

**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 (
    <div className="min-h-screen bg-background">
      <main className="container mx-auto py-8 px-4 max-w-screen-2xl">
        <Outlet />
      </main>
    </div>
  )
}
```

### 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
<ProtectedRoute requiredRole="super_admin">
  <ErrorBoundary>
    <AdminLayout />
  </ErrorBoundary>
</ProtectedRoute>
```

**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: (
    <ErrorBoundary>
      <ProtectedRoute requiredRole="super_admin">
        <AdminLayout />
      </ProtectedRoute>
    </ErrorBoundary>
  ),
  children: [
    { index: true, element: <Navigate to="/admin/dashboard" replace /> },
    { path: 'dashboard', element: <Suspense fallback={<PageLoader />}><DashboardPage /></Suspense> },
    { path: 'users', element: <Suspense fallback={<PageLoader />}><UsersPage /></Suspense> },
    { path: 'invite-codes', element: <Suspense fallback={<PageLoader />}><InviteCodesPage /></Suspense> },
    { path: 'plan-limits', element: <Suspense fallback={<PageLoader />}><PlanLimitsPage /></Suspense> },
    { path: 'feature-flags', element: <Suspense fallback={<PageLoader />}><FeatureFlagsPage /></Suspense> },
    { path: 'settings', element: <Suspense fallback={<PageLoader />}><SettingsPage /></Suspense> },
    { path: 'audit-logs', element: <Suspense fallback={<PageLoader />}><AuditLogsPage /></Suspense> },
    { path: 'categories', element: <Suspense fallback={<PageLoader />}><GlobalCategoriesPage /></Suspense> },
    { path: '*', element: <Navigate to="/admin/dashboard" replace /> }
  ]
},
{
  path: 'account',
  element: (
    <ErrorBoundary>
      <ProtectedRoute requiredRole="owner">
        <AccountLayout />
      </ProtectedRoute>
    </ErrorBoundary>
  ),
  children: [
    { index: true, element: <Navigate to="/account/categories" replace /> },
    { path: 'categories', element: <Suspense fallback={<PageLoader />}><TeamCategoriesPage /></Suspense> },
    { path: '*', element: <Navigate to="/account/categories" replace /> }
  ]
}
```

---

## Reusable Components

### DataTable

Generic paginated table with sorting support.

```typescript
interface Column<T> {
  key: string
  header: string
  render: (item: T) => ReactNode  // REQUIRED - no fallback
  sortable?: boolean
  width?: string
}

interface DataTableProps<T> {
  columns: Column<T>[]
  data: T[]
  isLoading?: boolean
  emptyMessage?: string
}

<DataTable
  columns={[
    {
      key: 'email',
      header: 'Email',
      render: (user) => <span className="font-medium">{user.email}</span>,
      sortable: true
    },
    {
      key: 'role',
      header: 'Role',
      render: (user) => <StatusBadge variant={getBadgeVariant(user.role)}>{user.role}</StatusBadge>
    }
  ]}
  data={users}
  isLoading={isLoading}
/>
```

**Features:**
- Generic type support (`<T>`)
- 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
}

<Pagination
  currentPage={page}
  totalPages={totalPages}
  onPageChange={setPage}
  totalItems={filteredData.length}
/>
```

**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'
}

<ActionMenu
  items={[
    { label: 'Edit', icon: Edit, onClick: () => 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
}

<StatusBadge variant="success">Active</StatusBadge>
<StatusBadge variant="warning">Pending</StatusBadge>
<StatusBadge variant="destructive">Deactivated</StatusBadge>
```

**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
  }
}

<EmptyState
  icon={Users}
  title="No users found"
  description="Try adjusting your search or filters"
  action={{ label: 'Clear Filters', onClick: clearFilters }}
/>
```

### SearchInput

Debounced search input with 300ms delay.

```typescript
interface SearchInputProps {
  value: string
  onChange: (value: string) => void
  placeholder?: string
  className?: string
}

<SearchInput
  value={searchTerm}
  onChange={setSearchTerm}
  placeholder="Search users..."
/>
```

**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
  }
}

<PageHeader
  title="Users"
  description="Manage platform users and permissions"
  action={{ label: 'Create User', icon: Plus, onClick: () => 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
<ProtectedRoute requiredRole="super_admin">
  <AdminLayout />
</ProtectedRoute>

// Conditional rendering
{isSuperAdmin && <Link to="/admin">Admin Panel</Link>}
{isAccountOwner && <Link to="/account/categories">Team Categories</Link>}
```

### 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.
