Move completed design/implementation docs from docs/plans/ to docs/archive/ to keep the plans folder focused on active and future work. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1704 lines
56 KiB
Markdown
1704 lines
56 KiB
Markdown
# 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.
|