diff --git a/dashboards/admin-web/src/app/(dashboard)/debug-sessions/[id]/page.tsx b/dashboards/admin-web/src/app/(dashboard)/debug-sessions/[id]/page.tsx new file mode 100644 index 00000000..3eea5909 --- /dev/null +++ b/dashboards/admin-web/src/app/(dashboard)/debug-sessions/[id]/page.tsx @@ -0,0 +1,559 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Separator } from '@/components/ui/separator'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + ArrowLeft, + Clock, + Download, + StopCircle, + Plus, + MoreHorizontal, + Calendar, + User, + Smartphone, + Activity, + Network, + Camera, + FileText, +} from 'lucide-react'; +import { + createDiagnosticsClient, + type DebugSession, + type TraceSpan, + type LogEntry, +} from '@/lib/diagnostics-client'; + +const statusColors: Record = { + pending: 'bg-yellow-500', + active: 'bg-green-500', + paused: 'bg-orange-500', + completed: 'bg-blue-500', + cancelled: 'bg-red-500', +}; + +const levelColors: Record = { + debug: 'text-gray-500', + info: 'text-blue-500', + warn: 'text-yellow-500', + error: 'text-red-500', + fatal: 'text-red-700 font-bold', +}; + +export default function SessionDetailPage() { + const params = useParams(); + const router = useRouter(); + const sessionId = params.id as string; + + const [session, setSession] = useState(null); + const [traces, setTraces] = useState([]); + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState('timeline'); + const [logLevelFilter, setLogLevelFilter] = useState('all'); + + const getAuthToken = () => { + if (typeof window === 'undefined') return null; + return localStorage.getItem('admin_access_token'); + }; + + const client = createDiagnosticsClient({ + baseUrl: process.env.NEXT_PUBLIC_PLATFORM_SERVICE_URL || 'http://localhost:4003', + productId: 'lysnrai', + getAuthToken, + }); + + const fetchSession = async () => { + try { + const data = await client.getSession(sessionId); + setSession(data); + } catch (error) { + console.error('Failed to fetch session:', error); + } + }; + + const fetchTraces = async () => { + try { + const result = await client.getTraces({ + sessionId, + productId: 'lysnrai', + limit: 100, + }); + setTraces(result.traces); + } catch (error) { + console.error('Failed to fetch traces:', error); + } + }; + + const fetchLogs = async () => { + try { + const result = await client.getLogs({ + sessionId, + productId: 'lysnrai', + levels: + logLevelFilter === 'all' + ? undefined + : [logLevelFilter as 'debug' | 'info' | 'warn' | 'error' | 'fatal'], + limit: 100, + }); + setLogs(result.logs); + } catch (error) { + console.error('Failed to fetch logs:', error); + } + }; + + useEffect(() => { + const loadData = async () => { + setLoading(true); + await fetchSession(); + await Promise.all([fetchTraces(), fetchLogs()]); + setLoading(false); + }; + loadData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sessionId, activeTab, logLevelFilter]); + + const handleExtend = async () => { + if (!session) return; + try { + await client.updateSession(sessionId, { + maxDurationMinutes: session.maxDurationMinutes + 30, + }); + fetchSession(); + } catch (error) { + console.error('Failed to extend session:', error); + } + }; + + const handleStop = async () => { + try { + await client.cancelSession(sessionId, 'Stopped by admin'); + fetchSession(); + } catch (error) { + console.error('Failed to stop session:', error); + } + }; + + const handleDownload = () => { + // TODO: Implement download of session data as JSON + console.log('Download session data'); + }; + + if (loading) { + return ( +
+
Loading session...
+
+ ); + } + + if (!session) { + return ( +
+
Session not found
+
+ ); + } + + const formatDuration = (minutes: number) => { + if (minutes < 60) return `${minutes}m`; + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; + }; + + // Mock breadcrumbs for timeline (would come from API in real implementation) + const breadcrumbs = [ + { timestamp: session.createdAt, category: 'session', message: 'Session created' }, + ...(session.startedAt + ? [{ timestamp: session.startedAt, category: 'session', message: 'Session activated' }] + : []), + ]; + + // Mock network requests for demo + const networkRequests = [ + { + id: '1', + method: 'GET', + url: '/api/user/profile', + status: 200, + durationMs: 145, + timestamp: new Date().toISOString(), + }, + { + id: '2', + method: 'POST', + url: '/api/diagnostics/ingest', + status: 200, + durationMs: 89, + timestamp: new Date().toISOString(), + }, + ]; + + return ( +
+ {/* Header */} +
+
+ +
+

{session.id}

+ {session.status} +
+

+ Created {new Date(session.createdAt).toLocaleString()} +

+
+
+ {session.status === 'active' && ( + <> + + + + )} + +
+
+ + {/* Session Info Cards */} +
+ + + + + Target User + + + +
{session.targetUserId || 'Anonymous'}
+
+
+ + + + + Device + + + +
{session.targetDeviceId || 'Unknown'}
+
+
+ + + + + Duration + + + +
{formatDuration(session.maxDurationMinutes)}
+
+
+ + + + + Collection + + + +
{session.collectionLevel}
+
+ {session.captureLogs && 'Logs '} + {session.captureNetwork && 'Network '} + {session.captureScreenshots && 'Screenshots'} +
+
+
+
+ + {/* Tabs */} + + + + + Timeline + + + + Logs + + + + Network + + + + Traces + + + + Screenshots + + + + {/* Timeline Tab */} + + + + Session Timeline + + +
+ {breadcrumbs.map((crumb, index) => ( +
+
+
+ {index < breadcrumbs.length - 1 && ( +
+ )} +
+
+
{crumb.message}
+
+ {new Date(crumb.timestamp).toLocaleString()} +
+ + {crumb.category} + +
+
+ ))} +
+ + + + + {/* Logs Tab */} + + + +
+ Log Entries ({logs.length}) + +
+
+ +
+
+ {logs.length === 0 ? ( +
No logs yet
+ ) : ( + logs.map(log => ( +
+
+
+ + {log.level} + + {log.module} +
+ + {new Date(log.timestamp).toLocaleTimeString()} + +
+

{log.message}

+ {log.file && ( +

+ {log.file}:{log.line} +

+ )} +
+ )) + )} +
+
+
+
+
+ + {/* Network Tab */} + + + + Network Requests ({networkRequests.length}) + + + + + + Method + URL + Status + Duration + Time + + + + {networkRequests.length === 0 ? ( + + + No network requests yet + + + ) : ( + networkRequests.map(req => ( + + + + {req.method} + + + + {req.url} + + + + {req.status} + + + {req.durationMs}ms + + {new Date(req.timestamp).toLocaleTimeString()} + + + )) + )} + +
+
+
+
+ + {/* Traces Tab */} + + + + Trace Spans ({traces.length}) + + +
+
+ {traces.length === 0 ? ( +
No traces yet
+ ) : ( + traces.map(trace => ( +
+
+
+ {trace.name} + + {trace.status} + +
+ + {trace.durationMs?.toFixed(2)}ms + +
+
+ {new Date(trace.startTime).toLocaleTimeString()} +
+ {trace.statusMessage && ( +

{trace.statusMessage}

+ )} +
+ )) + )} +
+
+
+
+
+ + {/* Screenshots Tab */} + + + + Screenshots ({session.screenshotCount}) + + + {session.screenshotCount === 0 ? ( +
+ +

No screenshots captured yet

+

+ Screenshots are captured automatically when errors occur (if enabled) +

+
+ ) : ( +
+ {/* Screenshot thumbnails would go here */} +
+ Screenshot placeholder +
+
+ )} +
+
+
+ +
+ ); +}