2:I[7012,["4765","static/chunks/4765-f5afdf8061f456f3.js","9856","static/chunks/9856-3b185291364d9bef.js","6687","static/chunks/app/docs/%5B...slug%5D/page-e07536548216bee4.js"],"MarkdownRenderer"] 4:I[9856,["4765","static/chunks/4765-f5afdf8061f456f3.js","9856","static/chunks/9856-3b185291364d9bef.js","6687","static/chunks/app/docs/%5B...slug%5D/page-e07536548216bee4.js"],""] 5:I[4126,[],""] 7:I[9630,[],""] 8:I[4278,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"HeadingProvider"] 9:I[1476,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"Header"] a:I[3167,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"Sidebar"] b:I[7409,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"PageFrame"] 3:T179a9, # VoiceAssist Admin Panel - Feature Specifications **Document Version:** 1.0.0 **Last Updated:** 2025-11-21 **Status:** Final Specification **Target Release:** v1.0.0 --- ## Table of Contents 1. [Overview](#1-overview) 2. [Dashboard Features](#2-dashboard-features) 3. [Knowledge Base Management](#3-knowledge-base-management) 4. [AI Model Configuration](#4-ai-model-configuration) 5. [Analytics](#5-analytics) 6. [Integration Management](#6-integration-management) 7. [Technical Implementation](#7-technical-implementation) 8. [Testing Strategy](#8-testing-strategy) --- ## 1. Overview ### 1.1 Technology Stack The VoiceAssist Admin Panel is built with modern web technologies optimized for administrative interfaces and data visualization: **Core Framework:** - React 18.2+ with TypeScript 5.0+ - Vite 5.0+ (build tool and dev server) - React Router v6 (client-side routing) **UI Framework:** - Tailwind CSS 3.4+ (utility-first styling) - Tremor 3.x (dashboard and chart components) - Headless UI (accessible component primitives) - Heroicons (icon library) **Data Management:** - TanStack Table v8 (advanced table functionality) - TanStack Query v5 (server state management) - Zustand (client state management) **Visualization:** - Recharts 2.x (charts via Tremor) - D3.js 7.x (custom visualizations) **Real-time Communication:** - Socket.io Client 4.x (WebSocket connections) - Server-Sent Events (SSE) for updates **Form Management:** - React Hook Form 7.x - Zod (schema validation) **Development Tools:** - ESLint + Prettier - Vitest (unit testing) - Playwright (e2e testing) - MSW (API mocking) ### 1.2 Design Principles **1. Information Density** - Maximize useful information per screen - Use progressive disclosure for complex data - Implement responsive tables and charts **2. Real-time Updates** - Live metrics without page refresh - WebSocket connections for critical data - Optimistic UI updates **3. Performance** - Virtualized tables for large datasets - Lazy loading of heavy components - Code splitting by route **4. Accessibility** - WCAG 2.1 AA compliance - Keyboard navigation throughout - Screen reader optimized **5. Error Handling** - Graceful degradation - Clear error messages - Retry mechanisms ### 1.3 Architecture ``` /admin-panel ├── /src │ ├── /components │ │ ├── /dashboard # Dashboard widgets │ │ ├── /kb # KB management components │ │ ├── /ai-config # AI configuration components │ │ ├── /analytics # Analytics components │ │ ├── /integrations # Integration components │ │ └── /common # Shared components │ ├── /hooks # Custom React hooks │ ├── /services # API services │ ├── /stores # Zustand stores │ ├── /types # TypeScript types │ ├── /utils # Utility functions │ └── /pages # Route pages ├── /tests │ ├── /unit │ ├── /integration │ └── /e2e └── /public ``` --- ## 2. Dashboard Features ### 2.1 Real-time Metrics Display **Priority:** P0 (Critical) **Effort:** 5 days **Dependencies:** WebSocket connection, /api/admin/metrics endpoint #### Specification Display key system metrics updated in real-time via WebSocket: - Active sessions count - Requests per minute - Average response time - Error rate - Cost per hour **Visual Design:** - 5 metric cards in a grid layout - Large numbers with trend indicators - Sparkline charts showing 1-hour history - Color-coded status (green/yellow/red) #### Component Implementation ```typescript // src/components/dashboard/RealTimeMetrics.tsx import { useEffect, useState } from 'react'; import { Card, Metric, Text, Flex, BadgeDelta, AreaChart } from '@tremor/react'; import { useWebSocket } from '@/hooks/useWebSocket'; import type { SystemMetrics } from '@/types/metrics'; interface MetricCardProps { title: string; value: number; unit?: string; trend?: number; trendType?: 'increase' | 'decrease' | 'unchanged'; history?: number[]; format?: (value: number) => string; } const MetricCard: React.FC = ({ title, value, unit = '', trend, trendType = 'unchanged', history = [], format = (v) => v.toString(), }) => { const chartData = history.map((val, idx) => ({ time: idx, value: val, })); return ( {title} {trend !== undefined && ( {trend > 0 ? '+' : ''}{trend}% )} {format(value)}{unit} {history.length > 0 && ( )} ); }; export const RealTimeMetrics: React.FC = () => { const [metrics, setMetrics] = useState(null); const [history, setHistory] = useState>(new Map()); const { data, isConnected } = useWebSocket('metrics', { reconnect: true, reconnectInterval: 5000, }); useEffect(() => { if (data) { setMetrics(data); // Update history for sparklines setHistory((prev) => { const newHistory = new Map(prev); const updateHistory = (key: string, value: number) => { const current = newHistory.get(key) || []; const updated = [...current, value].slice(-60); // Keep last 60 points newHistory.set(key, updated); }; updateHistory('activeSessions', data.activeSessions); updateHistory('requestsPerMinute', data.requestsPerMinute); updateHistory('avgResponseTime', data.avgResponseTime); updateHistory('errorRate', data.errorRate); updateHistory('costPerHour', data.costPerHour); return newHistory; }); } }, [data]); if (!metrics) { return (
{[...Array(5)].map((_, i) => (
))}
); } return (
{!isConnected && (

Real-time updates disconnected. Attempting to reconnect...

)}
0 ? 'increase' : 'decrease'} history={history.get('activeSessions')} /> 0 ? 'increase' : 'decrease'} history={history.get('requestsPerMinute')} format={(v) => v.toFixed(1)} /> Math.round(v).toString()} /> 0 ? 'decrease' : 'increase'} history={history.get('errorRate')} format={(v) => v.toFixed(2)} /> 0 ? 'decrease' : 'increase'} history={history.get('costPerHour')} format={(v) => `$${v.toFixed(2)}`} />
); }; ``` #### Custom Hook for WebSocket ```typescript // src/hooks/useWebSocket.ts import { useEffect, useState, useRef } from "react"; import io, { Socket } from "socket.io-client"; interface UseWebSocketOptions { reconnect?: boolean; reconnectInterval?: number; } interface UseWebSocketReturn { data: T | null; isConnected: boolean; error: Error | null; emit: (event: string, data: any) => void; } export function useWebSocket(event: string, options: UseWebSocketOptions = {}): UseWebSocketReturn { const { reconnect = true, reconnectInterval = 5000 } = options; const [data, setData] = useState(null); const [isConnected, setIsConnected] = useState(false); const [error, setError] = useState(null); const socketRef = useRef(null); useEffect(() => { const socket = io(import.meta.env.VITE_WEBSOCKET_URL || "http://localhost:5056", { reconnection: reconnect, reconnectionDelay: reconnectInterval, transports: ["websocket"], }); socketRef.current = socket; socket.on("connect", () => { setIsConnected(true); setError(null); }); socket.on("disconnect", () => { setIsConnected(false); }); socket.on("error", (err: Error) => { setError(err); }); socket.on(event, (newData: T) => { setData(newData); }); return () => { socket.disconnect(); }; }, [event, reconnect, reconnectInterval]); const emit = (eventName: string, eventData: any) => { if (socketRef.current?.connected) { socketRef.current.emit(eventName, eventData); } }; return { data, isConnected, error, emit }; } ``` #### Type Definitions ```typescript // src/types/metrics.ts export interface SystemMetrics { activeSessions: number; sessionsTrend: number; requestsPerMinute: number; requestsTrend: number; avgResponseTime: number; responseTrend: number; errorRate: number; errorTrend: number; costPerHour: number; costTrend: number; timestamp: string; } ``` #### Unit Tests ```typescript // tests/unit/components/dashboard/RealTimeMetrics.test.tsx import { render, screen, waitFor } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import { RealTimeMetrics } from '@/components/dashboard/RealTimeMetrics'; import * as useWebSocketModule from '@/hooks/useWebSocket'; vi.mock('@/hooks/useWebSocket'); describe('RealTimeMetrics', () => { it('should render loading state initially', () => { vi.mocked(useWebSocketModule.useWebSocket).mockReturnValue({ data: null, isConnected: false, error: null, emit: vi.fn(), }); render(); const cards = screen.getAllByRole('article'); expect(cards).toHaveLength(5); expect(cards[0]).toHaveClass('animate-pulse'); }); it('should render metrics when data is available', async () => { const mockMetrics = { activeSessions: 42, sessionsTrend: 5.2, requestsPerMinute: 123.5, requestsTrend: -2.1, avgResponseTime: 145, responseTrend: 3.4, errorRate: 0.05, errorTrend: -0.02, costPerHour: 2.45, costTrend: 1.2, timestamp: '2025-11-21T10:00:00Z', }; vi.mocked(useWebSocketModule.useWebSocket).mockReturnValue({ data: mockMetrics, isConnected: true, error: null, emit: vi.fn(), }); render(); await waitFor(() => { expect(screen.getByText('42')).toBeInTheDocument(); expect(screen.getByText('123.5')).toBeInTheDocument(); expect(screen.getByText('145ms')).toBeInTheDocument(); expect(screen.getByText('0.05%')).toBeInTheDocument(); expect(screen.getByText('$2.45')).toBeInTheDocument(); }); }); it('should show disconnection warning', () => { vi.mocked(useWebSocketModule.useWebSocket).mockReturnValue({ data: null, isConnected: false, error: null, emit: vi.fn(), }); render(); expect(screen.getByText(/Real-time updates disconnected/i)).toBeInTheDocument(); }); }); ``` #### Accessibility Notes - All metric cards have proper ARIA labels - Trend indicators use semantic colors and text - Loading states announced to screen readers - Keyboard navigation between cards - Focus indicators on interactive elements --- ### 2.2 System Health Indicators **Priority:** P0 (Critical) **Effort:** 3 days **Dependencies:** /api/admin/health endpoint #### Specification Visual indicators for critical system components: - API server status - Database connection - Redis cache - Vector database - External integrations (Nextcloud, email) **Visual Design:** - Status badges with icons (check/warning/error) - Last check timestamp - Response time for each component - Click to view detailed logs #### Component Implementation ```typescript // src/components/dashboard/SystemHealthIndicators.tsx import { useState } from 'react'; import { Card, Badge, Table, TableHead, TableRow, TableHeaderCell, TableBody, TableCell, Button } from '@tremor/react'; import { CheckCircleIcon, ExclamationCircleIcon, XCircleIcon, ArrowPathIcon } from '@heroicons/react/24/outline'; import { useQuery } from '@tanstack/react-query'; import { adminApi } from '@/services/api'; import type { HealthStatus } from '@/types/health'; interface HealthIndicatorProps { status: 'healthy' | 'degraded' | 'down'; size?: 'sm' | 'md' | 'lg'; } const HealthIndicator: React.FC = ({ status, size = 'md' }) => { const config = { healthy: { icon: CheckCircleIcon, color: 'green', label: 'Healthy' }, degraded: { icon: ExclamationCircleIcon, color: 'yellow', label: 'Degraded' }, down: { icon: XCircleIcon, color: 'red', label: 'Down' }, }; const { icon: Icon, color, label } = config[status]; const iconSize = size === 'sm' ? 'h-4 w-4' : size === 'md' ? 'h-5 w-5' : 'h-6 w-6'; return ( {label} ); }; export const SystemHealthIndicators: React.FC = () => { const [showDetails, setShowDetails] = useState(null); const { data: health, isLoading, refetch, isRefetching } = useQuery({ queryKey: ['system-health'], queryFn: () => adminApi.getSystemHealth(), refetchInterval: 30000, // Refresh every 30 seconds }); if (isLoading) { return (
{[...Array(5)].map((_, i) => (
))}
); } if (!health) { return (

Unable to load health status

); } const overallStatus = health.components.every(c => c.status === 'healthy') ? 'healthy' : health.components.some(c => c.status === 'down') ? 'down' : 'degraded'; return (

System Health

Component Status Response Time Last Check Actions {health.components.map((component) => (

{component.name}

{component.description}

{component.responseTime}ms {new Date(component.lastCheck).toLocaleTimeString()}
))}
{showDetails && (

{health.components.find(c => c.name === showDetails)?.name} Details

            {JSON.stringify(
              health.components.find(c => c.name === showDetails)?.details,
              null,
              2
            )}
          
)}
); }; ``` #### API Service ```typescript // src/services/api/admin.ts import { apiClient } from "./client"; import type { HealthStatus } from "@/types/health"; export const adminApi = { async getSystemHealth(): Promise { const response = await apiClient.get("/api/admin/health"); return response.data; }, async runHealthCheck(component: string): Promise { await apiClient.post(`/api/admin/health/${component}/check`); }, }; ``` #### Type Definitions ```typescript // src/types/health.ts export type ComponentStatus = "healthy" | "degraded" | "down"; export interface ComponentHealth { name: string; description: string; status: ComponentStatus; responseTime: number; lastCheck: string; details?: Record; } export interface HealthStatus { overall: ComponentStatus; components: ComponentHealth[]; timestamp: string; } ``` #### Unit Tests ```typescript // tests/unit/components/dashboard/SystemHealthIndicators.test.tsx import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, it, expect, vi } from 'vitest'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { SystemHealthIndicators } from '@/components/dashboard/SystemHealthIndicators'; import * as adminApi from '@/services/api/admin'; vi.mock('@/services/api/admin'); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); describe('SystemHealthIndicators', () => { it('should render all components', async () => { const mockHealth = { overall: 'healthy' as const, components: [ { name: 'API Server', description: 'FastAPI backend', status: 'healthy' as const, responseTime: 45, lastCheck: '2025-11-21T10:00:00Z', }, { name: 'Database', description: 'PostgreSQL', status: 'healthy' as const, responseTime: 12, lastCheck: '2025-11-21T10:00:00Z', }, ], timestamp: '2025-11-21T10:00:00Z', }; vi.mocked(adminApi.adminApi.getSystemHealth).mockResolvedValue(mockHealth); render(, { wrapper }); await waitFor(() => { expect(screen.getByText('API Server')).toBeInTheDocument(); expect(screen.getByText('Database')).toBeInTheDocument(); }); }); it('should show details when clicked', async () => { const user = userEvent.setup(); const mockHealth = { overall: 'healthy' as const, components: [ { name: 'API Server', description: 'FastAPI backend', status: 'healthy' as const, responseTime: 45, lastCheck: '2025-11-21T10:00:00Z', details: { version: '1.0.0', uptime: 3600 }, }, ], timestamp: '2025-11-21T10:00:00Z', }; vi.mocked(adminApi.adminApi.getSystemHealth).mockResolvedValue(mockHealth); render(, { wrapper }); await waitFor(() => { expect(screen.getByText('API Server')).toBeInTheDocument(); }); const detailsButton = screen.getByText('Details'); await user.click(detailsButton); expect(screen.getByText(/version/i)).toBeInTheDocument(); }); }); ``` --- ### 2.3 Active Sessions Monitor **Priority:** P1 (High) **Effort:** 4 days **Dependencies:** WebSocket connection, /api/admin/sessions endpoint #### Specification Real-time table of active user sessions: - Session ID - User ID / Anonymous - Start time - Duration - Current activity - Messages sent - Token usage - Actions (view details, terminate) **Visual Design:** - Paginated table with live updates - Color-coded by activity level - Expandable rows for session details - Bulk actions (terminate multiple) #### Component Implementation ```typescript // src/components/dashboard/ActiveSessionsMonitor.tsx import { useMemo, useState } from 'react'; import { createColumnHelper, flexRender, getCoreRowModel, useReactTable, getPaginationRowModel, getSortedRowModel, type SortingState, } from '@tanstack/react-table'; import { Card, Badge, Button, TextInput } from '@tremor/react'; import { MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/outline'; import { useWebSocket } from '@/hooks/useWebSocket'; import { formatDuration } from '@/utils/time'; import type { ActiveSession } from '@/types/sessions'; const columnHelper = createColumnHelper(); export const ActiveSessionsMonitor: React.FC = () => { const [sorting, setSorting] = useState([]); const [filter, setFilter] = useState(''); const [selectedSessions, setSelectedSessions] = useState>(new Set()); const { data: sessions, emit } = useWebSocket('active-sessions', { reconnect: true, }); const filteredSessions = useMemo(() => { if (!sessions) return []; if (!filter) return sessions; return sessions.filter( (s) => s.sessionId.toLowerCase().includes(filter.toLowerCase()) || s.userId?.toLowerCase().includes(filter.toLowerCase()) || s.currentActivity.toLowerCase().includes(filter.toLowerCase()) ); }, [sessions, filter]); const columns = useMemo( () => [ columnHelper.display({ id: 'select', header: ({ table }) => ( ), cell: ({ row }) => ( ), }), columnHelper.accessor('sessionId', { header: 'Session ID', cell: (info) => ( {info.getValue().slice(0, 8)}... ), }), columnHelper.accessor('userId', { header: 'User', cell: (info) => { const userId = info.getValue(); return userId ? ( {userId} ) : ( Anonymous ); }, }), columnHelper.accessor('startTime', { header: 'Duration', cell: (info) => { const duration = Date.now() - new Date(info.getValue()).getTime(); return {formatDuration(duration)}; }, }), columnHelper.accessor('currentActivity', { header: 'Activity', cell: (info) => { const activity = info.getValue(); const color = activity === 'idle' ? 'gray' : activity === 'chatting' ? 'green' : activity === 'searching' ? 'blue' : 'purple'; return {activity}; }, }), columnHelper.accessor('messagesSent', { header: 'Messages', cell: (info) => ( {info.getValue()} ), }), columnHelper.accessor('tokenUsage', { header: 'Tokens', cell: (info) => ( {info.getValue().toLocaleString()} ), }), columnHelper.display({ id: 'actions', header: 'Actions', cell: ({ row }) => (
), }), ], [] ); const table = useReactTable({ data: filteredSessions, columns, state: { sorting, }, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), initialState: { pagination: { pageSize: 10, }, }, }); const handleViewDetails = (sessionId: string) => { // Open modal or navigate to session details console.log('View session:', sessionId); }; const handleTerminate = (sessionId: string) => { if (confirm('Are you sure you want to terminate this session?')) { emit('terminate-session', { sessionId }); } }; const handleBulkTerminate = () => { const sessionIds = Array.from(selectedSessions); if ( confirm( `Are you sure you want to terminate ${sessionIds.length} session(s)?` ) ) { emit('terminate-sessions', { sessionIds }); setSelectedSessions(new Set()); } }; return (

Active Sessions ({filteredSessions.length})

{selectedSessions.size > 0 && ( )}
setFilter(e.target.value)} />
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))} {table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( ))} ))}
{flexRender( header.column.columnDef.header, header.getContext() )} {header.column.getIsSorted() && ( {header.column.getIsSorted() === 'asc' ? '↑' : '↓'} )}
{flexRender(cell.column.columnDef.cell, cell.getContext())}
{table.getPageCount() > 1 && (
Page {table.getState().pagination.pageIndex + 1} of{' '} {table.getPageCount()}
)}
); }; ``` #### Type Definitions ```typescript // src/types/sessions.ts export interface ActiveSession { sessionId: string; userId: string | null; startTime: string; currentActivity: "idle" | "chatting" | "searching" | "listening"; messagesSent: number; tokenUsage: number; ipAddress?: string; userAgent?: string; } ``` #### Utility Functions ```typescript // src/utils/time.ts export function formatDuration(ms: number): string { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } } ``` --- ### 2.4 API Usage Graphs **Priority:** P1 (High) **Effort:** 3 days **Dependencies:** /api/admin/usage endpoint #### Specification Interactive charts showing API usage patterns: - Requests per hour (last 24h) - Requests by endpoint - Requests by status code - Requests by client type **Visual Design:** - Area chart for time series - Bar chart for endpoint comparison - Donut chart for status codes - Time range selector (1h, 6h, 24h, 7d) #### Component Implementation ```typescript // src/components/dashboard/APIUsageGraphs.tsx import { useState } from 'react'; import { Card, Title, AreaChart, BarChart, DonutChart, TabGroup, TabList, Tab, TabPanels, TabPanel, Select, SelectItem, } from '@tremor/react'; import { useQuery } from '@tanstack/react-query'; import { adminApi } from '@/services/api'; import type { APIUsageData } from '@/types/usage'; type TimeRange = '1h' | '6h' | '24h' | '7d' | '30d'; export const APIUsageGraphs: React.FC = () => { const [timeRange, setTimeRange] = useState('24h'); const { data: usage, isLoading } = useQuery({ queryKey: ['api-usage', timeRange], queryFn: () => adminApi.getAPIUsage(timeRange), refetchInterval: 60000, // Refresh every minute }); if (isLoading || !usage) { return (
); } const timeSeriesData = usage.timeSeries.map((point) => ({ time: new Date(point.timestamp).toLocaleTimeString(), requests: point.count, errors: point.errors, })); const endpointData = usage.byEndpoint.map((item) => ({ endpoint: item.endpoint, requests: item.count, })); const statusCodeData = usage.byStatusCode.map((item) => ({ name: `${item.code} (${item.label})`, value: item.count, })); return (
API Usage
Requests Over Time By Endpoint Status Codes `${value} req`} showLegend={true} showGridLines={true} showXAxis={true} showYAxis={true} /> `${value} req`} showLegend={false} showGridLines={true} showXAxis={true} showYAxis={true} layout="vertical" />
`${value} req`} showLabel={true} />

Status Code Summary

{statusCodeData.map((item) => (
{item.name} {item.value.toLocaleString()}
))}
Total Requests {statusCodeData .reduce((sum, item) => sum + item.value, 0) .toLocaleString()}
); }; ``` #### API Service Extension ```typescript // src/services/api/admin.ts (extension) export const adminApi = { // ... existing methods async getAPIUsage(timeRange: string): Promise { const response = await apiClient.get(`/api/admin/usage?range=${timeRange}`); return response.data; }, }; ``` #### Type Definitions ```typescript // src/types/usage.ts export interface TimeSeriesPoint { timestamp: string; count: number; errors: number; } export interface EndpointUsage { endpoint: string; count: number; avgResponseTime: number; } export interface StatusCodeUsage { code: number; label: string; count: number; } export interface APIUsageData { timeSeries: TimeSeriesPoint[]; byEndpoint: EndpointUsage[]; byStatusCode: StatusCodeUsage[]; totalRequests: number; totalErrors: number; avgResponseTime: number; } ``` --- ### 2.5 Cost Tracking **Priority:** P1 (High) **Effort:** 4 days **Dependencies:** /api/admin/costs endpoint #### Specification Real-time cost monitoring across all services: - OpenAI API costs (by model) - ElevenLabs TTS costs - Vector database costs - Total daily/monthly costs - Cost projections - Budget alerts **Visual Design:** - Cost breakdown donut chart - Trend line chart - Cost per user metric - Budget progress bar - Cost optimization suggestions #### Component Implementation ```typescript // src/components/dashboard/CostTracking.tsx import { Card, Title, DonutChart, LineChart, ProgressBar, Callout, Badge } from '@tremor/react'; import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; import { useQuery } from '@tanstack/react-query'; import { adminApi } from '@/services/api'; import type { CostData } from '@/types/costs'; export const CostTracking: React.FC = () => { const { data: costs, isLoading } = useQuery({ queryKey: ['costs'], queryFn: () => adminApi.getCosts(), refetchInterval: 300000, // Refresh every 5 minutes }); if (isLoading || !costs) { return (
); } const serviceBreakdown = [ { name: 'OpenAI API', value: costs.openai, color: 'blue' }, { name: 'ElevenLabs TTS', value: costs.elevenlabs, color: 'purple' }, { name: 'Vector DB', value: costs.vectorDb, color: 'green' }, { name: 'Infrastructure', value: costs.infrastructure, color: 'gray' }, ]; const trendData = costs.dailyTrend.map((point) => ({ date: new Date(point.date).toLocaleDateString(), cost: point.total, projected: point.projected, })); const budgetUsage = (costs.monthToDate / costs.monthlyBudget) * 100; const isOverBudget = budgetUsage > 100; const isNearBudget = budgetUsage > 80 && budgetUsage <= 100; return (
Cost Overview - {new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}

Today

${costs.today.toFixed(2)}

Month to Date

${costs.monthToDate.toFixed(2)}

Projected Monthly

${costs.projectedMonthly.toFixed(2)}

{(isOverBudget || isNearBudget) && ( You have used {budgetUsage.toFixed(1)}% of your monthly budget ( ${costs.monthlyBudget.toFixed(2)}). {isOverBudget && ' Consider reviewing usage or increasing budget.'} )}
Monthly Budget ${costs.monthToDate.toFixed(2)} / ${costs.monthlyBudget.toFixed(2)}
Cost Breakdown `$${value.toFixed(2)}`} colors={['blue', 'purple', 'green', 'gray']} showLabel={true} />
{serviceBreakdown.map((service) => (
{service.name} ${service.value.toFixed(2)}
))}
Daily Trend & Projection `$${value.toFixed(2)}`} showLegend={true} showGridLines={true} showXAxis={true} showYAxis={true} />
Cost Optimization Suggestions
{costs.suggestions.map((suggestion, index) => (
{suggestion.category}

{suggestion.title}

{suggestion.description}

Potential savings: ${suggestion.potentialSavings.toFixed(2)}/month

))}
); }; ``` #### Type Definitions ```typescript // src/types/costs.ts export interface DailyCostPoint { date: string; total: number; projected: number; byService: Record; } export interface CostSuggestion { category: string; title: string; description: string; potentialSavings: number; } export interface CostData { today: number; monthToDate: number; projectedMonthly: number; monthlyBudget: number; openai: number; elevenlabs: number; vectorDb: number; infrastructure: number; dailyTrend: DailyCostPoint[]; suggestions: CostSuggestion[]; } ``` --- ### 2.6 Alert Notifications **Priority:** P2 (Medium) **Effort:** 3 days **Dependencies:** WebSocket connection, /api/admin/alerts endpoint #### Specification Real-time alert notification system: - System errors - Performance degradation - Budget warnings - Security alerts - Custom alerts **Visual Design:** - Toast notifications for new alerts - Alert center with history - Severity levels (info, warning, error, critical) - Acknowledge/dismiss actions - Alert filtering #### Component Implementation ```typescript // src/components/dashboard/AlertNotifications.tsx import { useState, useEffect } from 'react'; import { Card, Title, Badge, Button, Select, SelectItem } from '@tremor/react'; import { BellIcon, CheckCircleIcon, XMarkIcon, FunnelIcon, } from '@heroicons/react/24/outline'; import { useWebSocket } from '@/hooks/useWebSocket'; import { useToast } from '@/hooks/useToast'; import type { Alert } from '@/types/alerts'; type AlertSeverity = 'info' | 'warning' | 'error' | 'critical'; export const AlertNotifications: React.FC = () => { const [alerts, setAlerts] = useState([]); const [filter, setFilter] = useState('all'); const { showToast } = useToast(); const { data: newAlert } = useWebSocket('new-alert'); useEffect(() => { if (newAlert) { setAlerts((prev) => [newAlert, ...prev]); // Show toast notification showToast({ title: newAlert.title, message: newAlert.message, severity: newAlert.severity, duration: 5000, }); } }, [newAlert]); const filteredAlerts = alerts.filter( (alert) => filter === 'all' || alert.severity === filter ); const handleAcknowledge = (alertId: string) => { setAlerts((prev) => prev.map((alert) => alert.id === alertId ? { ...alert, acknowledged: true } : alert ) ); }; const handleDismiss = (alertId: string) => { setAlerts((prev) => prev.filter((alert) => alert.id !== alertId)); }; const unacknowledgedCount = alerts.filter((a) => !a.acknowledged).length; const severityConfig = { info: { color: 'blue', label: 'Info' }, warning: { color: 'yellow', label: 'Warning' }, error: { color: 'orange', label: 'Error' }, critical: { color: 'red', label: 'Critical' }, }; return (
Alerts {unacknowledgedCount > 0 && ( {unacknowledgedCount} new )}
{filteredAlerts.length === 0 ? (

No alerts to display

) : (
{filteredAlerts.map((alert) => { const config = severityConfig[alert.severity]; return (
{config.label} {new Date(alert.timestamp).toLocaleString()} {alert.acknowledged && ( Acknowledged )}

{alert.title}

{alert.message}

{alert.details && (
View details
                            {JSON.stringify(alert.details, null, 2)}
                          
)}
{!alert.acknowledged && ( )}
); })}
)}
); }; ``` #### Toast Hook ```typescript // src/hooks/useToast.ts import { create } from "zustand"; interface Toast { id: string; title: string; message: string; severity: "info" | "warning" | "error" | "critical"; duration?: number; } interface ToastStore { toasts: Toast[]; addToast: (toast: Omit) => void; removeToast: (id: string) => void; } export const useToastStore = create((set) => ({ toasts: [], addToast: (toast) => { const id = Math.random().toString(36).substring(7); set((state) => ({ toasts: [...state.toasts, { ...toast, id }] })); if (toast.duration) { setTimeout(() => { set((state) => ({ toasts: state.toasts.filter((t) => t.id !== id), })); }, toast.duration); } }, removeToast: (id) => set((state) => ({ toasts: state.toasts.filter((t) => t.id !== id) })), })); export function useToast() { const { addToast, removeToast } = useToastStore(); return { showToast: addToast, hideToast: removeToast, }; } ``` #### Type Definitions ```typescript // src/types/alerts.ts export interface Alert { id: string; severity: "info" | "warning" | "error" | "critical"; title: string; message: string; timestamp: string; acknowledged: boolean; details?: Record; source?: string; } ``` --- ### 2.7 Quick Actions Panel **Priority:** P2 (Medium) **Effort:** 2 days **Dependencies:** Various admin endpoints #### Specification Common administrative actions in one place: - Restart services - Clear caches - Reindex knowledge base - Run health checks - Export reports - System maintenance mode **Visual Design:** - Grid of action cards - Confirmation modals for destructive actions - Progress indicators - Recent action history #### Component Implementation ```typescript // src/components/dashboard/QuickActionsPanel.tsx import { useState } from 'react'; import { Card, Title, Button, Grid, Col } from '@tremor/react'; import { ArrowPathIcon, TrashIcon, MagnifyingGlassIcon, HeartIcon, DocumentArrowDownIcon, WrenchScrewdriverIcon, } from '@heroicons/react/24/outline'; import { useMutation } from '@tanstack/react-query'; import { adminApi } from '@/services/api'; import { useToast } from '@/hooks/useToast'; import { ConfirmModal } from '@/components/common/ConfirmModal'; interface QuickAction { id: string; title: string; description: string; icon: typeof ArrowPathIcon; color: string; requiresConfirm: boolean; confirmMessage?: string; action: () => Promise; } export const QuickActionsPanel: React.FC = () => { const [confirmAction, setConfirmAction] = useState(null); const { showToast } = useToast(); const { mutate: executeAction, isPending } = useMutation({ mutationFn: (action: QuickAction) => action.action(), onSuccess: (_, action) => { showToast({ title: 'Action completed', message: `${action.title} completed successfully`, severity: 'info', }); setConfirmAction(null); }, onError: (error, action) => { showToast({ title: 'Action failed', message: `${action.title} failed: ${error.message}`, severity: 'error', }); }, }); const actions: QuickAction[] = [ { id: 'restart-services', title: 'Restart Services', description: 'Restart all backend services', icon: ArrowPathIcon, color: 'blue', requiresConfirm: true, confirmMessage: 'This will restart all services. Active sessions may be interrupted.', action: () => adminApi.restartServices(), }, { id: 'clear-cache', title: 'Clear Cache', description: 'Clear Redis cache', icon: TrashIcon, color: 'orange', requiresConfirm: true, confirmMessage: 'This will clear all cached data.', action: () => adminApi.clearCache(), }, { id: 'reindex-kb', title: 'Reindex KB', description: 'Rebuild knowledge base index', icon: MagnifyingGlassIcon, color: 'purple', requiresConfirm: true, confirmMessage: 'This may take several minutes and affect search performance.', action: () => adminApi.reindexKnowledgeBase(), }, { id: 'health-check', title: 'Run Health Check', description: 'Check all system components', icon: HeartIcon, color: 'green', requiresConfirm: false, action: () => adminApi.runHealthCheck(), }, { id: 'export-report', title: 'Export Report', description: 'Generate usage report', icon: DocumentArrowDownIcon, color: 'blue', requiresConfirm: false, action: async () => { const blob = await adminApi.exportReport(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `report-${new Date().toISOString()}.pdf`; a.click(); }, }, { id: 'maintenance-mode', title: 'Maintenance Mode', description: 'Enable/disable maintenance', icon: WrenchScrewdriverIcon, color: 'yellow', requiresConfirm: true, confirmMessage: 'This will make the system unavailable to users.', action: () => adminApi.toggleMaintenanceMode(), }, ]; const handleActionClick = (action: QuickAction) => { if (action.requiresConfirm) { setConfirmAction(action); } else { executeAction(action); } }; return ( <> Quick Actions {actions.map((action) => (

{action.title}

{action.description}

))}
{confirmAction && ( executeAction(confirmAction)} onCancel={() => setConfirmAction(null)} confirmText="Execute" confirmColor="blue" /> )} ); }; ``` #### Confirm Modal Component ```typescript // src/components/common/ConfirmModal.tsx import { Dialog, Transition } from '@headlessui/react'; import { Fragment } from 'react'; import { Button } from '@tremor/react'; import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; interface ConfirmModalProps { isOpen: boolean; title: string; message: string; onConfirm: () => void; onCancel: () => void; confirmText?: string; cancelText?: string; confirmColor?: 'blue' | 'red' | 'green'; } export const ConfirmModal: React.FC = ({ isOpen, title, message, onConfirm, onCancel, confirmText = 'Confirm', cancelText = 'Cancel', confirmColor = 'blue', }) => { return (
{title} {message}
); }; ``` --- ### 2.8 System Announcements **Priority:** P2 (Medium) **Effort:** 2 days **Dependencies:** /api/admin/announcements endpoint #### Specification Create and manage system-wide announcements: - Scheduled maintenance notices - Feature announcements - System updates - Emergency alerts **Visual Design:** - Banner display on all pages - Create/edit announcement form - Schedule future announcements - Track view/acknowledgment stats #### Component Implementation ```typescript // src/components/dashboard/SystemAnnouncements.tsx import { useState } from 'react'; import { Card, Title, Button, TextInput, Textarea, DatePicker, Select, SelectItem, Badge } from '@tremor/react'; import { PlusIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { adminApi } from '@/services/api'; import type { Announcement } from '@/types/announcements'; export const SystemAnnouncements: React.FC = () => { const [isCreating, setIsCreating] = useState(false); const [editingId, setEditingId] = useState(null); const queryClient = useQueryClient(); const { data: announcements } = useQuery({ queryKey: ['announcements'], queryFn: () => adminApi.getAnnouncements(), }); const createMutation = useMutation({ mutationFn: (data: Partial) => adminApi.createAnnouncement(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['announcements'] }); setIsCreating(false); }, }); const deleteMutation = useMutation({ mutationFn: (id: string) => adminApi.deleteAnnouncement(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['announcements'] }); }, }); return (
System Announcements
{isCreating && ( createMutation.mutate(data)} onCancel={() => setIsCreating(false)} /> )}
{announcements?.map((announcement) => (
{announcement.type} {announcement.scheduled && ( Scheduled for {new Date(announcement.scheduledFor!).toLocaleString()} )}

{announcement.title}

{announcement.message}

{announcement.views} views {announcement.acknowledgedBy} acknowledged
))}
); }; interface AnnouncementFormProps { announcement?: Announcement; onSubmit: (data: Partial) => void; onCancel: () => void; } const AnnouncementForm: React.FC = ({ announcement, onSubmit, onCancel, }) => { const [formData, setFormData] = useState({ title: announcement?.title || '', message: announcement?.message || '', type: announcement?.type || 'update', scheduled: announcement?.scheduled || false, scheduledFor: announcement?.scheduledFor || null, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSubmit(formData); }; return (
setFormData({ ...formData, title: e.target.value })} required />