From d5863f3b521eaf4a3ae7058b23eb12addaa19479 Mon Sep 17 00:00:00 2001 From: Stephanie Gredell Date: Tue, 9 Dec 2025 20:34:19 -0800 Subject: [PATCH] Add admin connection stats page --- .../ConnectionTracker/ConnectionTracker.tsx | 203 ++++++++++++++++++ frontend/src/pages/AdminPage.tsx | 15 ++ frontend/src/pages/StatsAdminPage.tsx | 23 ++ 3 files changed, 241 insertions(+) create mode 100644 frontend/src/components/ConnectionTracker/ConnectionTracker.tsx create mode 100644 frontend/src/pages/StatsAdminPage.tsx diff --git a/frontend/src/components/ConnectionTracker/ConnectionTracker.tsx b/frontend/src/components/ConnectionTracker/ConnectionTracker.tsx new file mode 100644 index 0000000..c4e5780 --- /dev/null +++ b/frontend/src/components/ConnectionTracker/ConnectionTracker.tsx @@ -0,0 +1,203 @@ +import { useState, useEffect } from 'react'; +import { settingsApi } from '../../services/apiClient'; + +const formatTime = (minutes: number): string => { + if (minutes < 1) { + return `${Math.round(minutes * 60)}s`; + } + if (minutes < 60) { + return `${Math.round(minutes)}m`; + } + const hours = Math.floor(minutes / 60); + const mins = Math.round(minutes % 60); + if (mins === 0) { + return `${hours}h`; + } + return `${hours}h ${mins}m`; +}; + +interface Connection { + sessionId: string; + userId?: number; + username?: string; + route?: string; + videoTitle?: string; + videoChannel?: string; + timeUsed?: number; + dailyLimit?: number; + lastHeartbeat: number; + connectedAt: number; +} + +interface ConnectionStats { + total: number; + authenticated: number; + anonymous: number; + connections?: Connection[]; +} + +export function ConnectionTracker() { + const [stats, setStats] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchStats = async () => { + try { + setIsLoading(true); + setError(null); + const response = await settingsApi.getConnectionStats(); + setStats(response.data); + } catch (err: any) { + setError(err.error?.message || 'Failed to load connection stats'); + console.error('Error fetching connection stats:', err); + } finally { + setIsLoading(false); + } + }; + + // Fetch stats on mount and periodically + useEffect(() => { + fetchStats(); + + // Refresh every 10 seconds + const interval = setInterval(fetchStats, 10000); + + return () => clearInterval(interval); + }, []); + + if (isLoading && !stats) { + return ( +
+
+

Active Connections

+

Loading...

+
+
+ ); + } + + return ( +
+
+

Active Connections

+

+ Number of users currently connected to the app +

+
+ + {error && ( +
+ {error} +
+ )} + + {stats && ( +
+
+
+
+
+ {stats.total} +
+
+ Total Active Users +
+
+
+ +
+
+
+ {stats.authenticated} +
+
+ Authenticated +
+
+
+ +
+
+
+ {stats.anonymous} +
+
+ Anonymous +
+
+
+
+ +
+ +
+ + {stats.connections && stats.connections.length > 0 && ( +
+

Connection Details

+
+ {stats.connections.map((conn) => { + const timeAgo = Math.floor((Date.now() - conn.lastHeartbeat) / 1000); + const timeAgoText = timeAgo < 60 ? `${timeAgo}s ago` : `${Math.floor(timeAgo / 60)}m ago`; + + return ( +
+
+
+
+ {conn.username ? ( + + {conn.username} + + ) : ( + + Anonymous + + )} + + ({conn.sessionId.substring(0, 8)}...) + +
+
+ Route: {conn.route || '/'} +
+ {conn.videoTitle && ( +
+ Video: {conn.videoTitle} +
+ )} + {conn.videoChannel && ( +
+ Channel: {conn.videoChannel} +
+ )} + {conn.timeUsed !== undefined && conn.dailyLimit !== undefined && (conn.route === '/videos' || conn.videoTitle) && ( +
+ Video Time Used: {formatTime(conn.timeUsed)} / {formatTime(conn.dailyLimit)} ({Math.round((conn.timeUsed / conn.dailyLimit) * 100)}%) +
+ )} +
+
+ {timeAgoText} +
+
+
+ ); + })} +
+
+ )} +
+ )} +
+ ); +} diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx index caaf8a2..f7bcd54 100644 --- a/frontend/src/pages/AdminPage.tsx +++ b/frontend/src/pages/AdminPage.tsx @@ -42,6 +42,21 @@ export function AdminPage() { Manage word groups for speech sound practice

+ + +
+
+ 📊 +
+
+

Connection Stats

+

+ View active user connections and routes +

+ ); diff --git a/frontend/src/pages/StatsAdminPage.tsx b/frontend/src/pages/StatsAdminPage.tsx new file mode 100644 index 0000000..949f80c --- /dev/null +++ b/frontend/src/pages/StatsAdminPage.tsx @@ -0,0 +1,23 @@ +import { Link } from 'react-router-dom'; +import { ConnectionTracker } from '../components/ConnectionTracker/ConnectionTracker'; + +export function StatsAdminPage() { + return ( +
+
+ + ← Back to Admin + +

Connection Statistics

+

View active user connections and their current routes

+
+ +
+ +
+
+ ); +}