diff --git a/frontend/src/pages/Stats.jsx b/frontend/src/pages/Stats.jsx index e1dadf5b..2be60ed3 100644 --- a/frontend/src/pages/Stats.jsx +++ b/frontend/src/pages/Stats.jsx @@ -96,6 +96,20 @@ const VODCard = ({ vodContent }) => { // Get poster/logo URL const posterUrl = metadata.logo_url || logo; + // Transform VOD connections to match table data structure + const connectionData = useMemo(() => { + return (vodContent.connections || []).map((connection, index) => ({ + id: `${connection.client_id}-${index}`, + ip_address: connection.client_ip, + client_id: connection.client_id, + user_agent: connection.user_agent || 'Unknown', + connected_since: connection.duration || 0, + connected_at: connection.connected_at, + m3u_profile: connection.m3u_profile, + ...connection, + })); + }, [vodContent.connections]); + // Format duration const formatDuration = (seconds) => { if (!seconds) return 'Unknown'; @@ -136,7 +150,7 @@ const VODCard = ({ vodContent }) => { }; // Calculate duration for connection - const calculateConnectionDuration = (connection) => { + const calculateConnectionDuration = useCallback((connection) => { // If duration is provided by API, use it if (connection.duration && connection.duration > 0) { return dayjs.duration(connection.duration, 'seconds').humanize(); @@ -158,31 +172,184 @@ const VODCard = ({ vodContent }) => { } return 'Unknown duration'; - }; + }, []); // Get connection start time for tooltip - const getConnectionStartTime = (connection) => { - if (connection.connected_at) { - return dayjs(connection.connected_at * 1000).format( - `${dateFormat} HH:mm:ss` - ); - } - - // Fallback: calculate from client_id timestamp - if (connection.client_id && connection.client_id.startsWith('vod_')) { - try { - const parts = connection.client_id.split('_'); - if (parts.length >= 2) { - const clientStartTime = parseInt(parts[1]); - return dayjs(clientStartTime).format(`${dateFormat} HH:mm:ss`); - } - } catch (e) { - // Ignore parsing errors + const getConnectionStartTime = useCallback( + (connection) => { + if (connection.connected_at) { + return dayjs(connection.connected_at * 1000).format( + `${dateFormat} HH:mm:ss` + ); } - } - return 'Unknown'; - }; + // Fallback: calculate from client_id timestamp + if (connection.client_id && connection.client_id.startsWith('vod_')) { + try { + const parts = connection.client_id.split('_'); + if (parts.length >= 2) { + const clientStartTime = parseInt(parts[1]); + return dayjs(clientStartTime).format(`${dateFormat} HH:mm:ss`); + } + } catch (e) { + // Ignore parsing errors + } + } + + return 'Unknown'; + }, + [dateFormat] + ); + + // Define table columns similar to ChannelCard + const vodConnectionsColumns = useMemo( + () => [ + { + id: 'expand', + size: 20, + }, + { + header: 'IP Address', + accessorKey: 'ip_address', + cell: ({ cell }) => {cell.getValue()}, + }, + { + id: 'connected', + header: 'Connected', + accessorFn: (row) => { + return getConnectionStartTime(row); + }, + cell: ({ cell }) => ( + + {cell.getValue()} + + ), + }, + { + id: 'duration', + header: 'Duration', + accessorFn: (row) => { + return calculateConnectionDuration(row); + }, + cell: ({ cell, row }) => { + const exactDuration = row.original.duration; + return ( + + {cell.getValue()} + + ); + }, + }, + ], + [getConnectionStartTime, calculateConnectionDuration] + ); + + // Table configuration similar to ChannelCard + const vodConnectionsTable = useTable({ + ...TableHelper.defaultProperties, + columns: vodConnectionsColumns, + data: connectionData, + allRowIds: connectionData.map((connection) => connection.id), + tableCellProps: () => ({ + padding: 4, + borderColor: '#444', + color: '#E0E0E0', + fontSize: '0.85rem', + }), + headerCellRenderFns: { + ip_address: ({ header }) => ( + + + {header?.column?.columnDef?.header || 'IP Address'} + + + ), + connected: ({ header }) => ( + + + {header?.column?.columnDef?.header || 'Connected'} + + + ), + duration: ({ header }) => ( + + + {header?.column?.columnDef?.header || 'Duration'} + + + ), + }, + expandedRowRenderer: ({ row }) => { + return ( + + + + + Client ID: + + + {row.original.client_id} + + + + {row.original.user_agent && + row.original.user_agent !== 'Unknown' && ( + + + User Agent: + + + {row.original.user_agent.length > 60 + ? `${row.original.user_agent.substring(0, 60)}...` + : row.original.user_agent} + + + )} + + {row.original.m3u_profile && + (row.original.m3u_profile.profile_name || + row.original.m3u_profile.account_name) && ( + + + M3U Profile: + + + {row.original.m3u_profile.account_name || 'Unknown Account'}{' '} + →{' '} + {row.original.m3u_profile.profile_name || 'Default Profile'} + + + )} + + + ); + }, + mantineExpandButtonProps: ({ row }) => ({ + size: 'xs', + style: { + transform: row.getIsExpanded() ? 'rotate(180deg)' : 'rotate(-90deg)', + transition: 'transform 0.2s', + }, + }), + displayColumnDefOptions: { + 'mrt-row-expand': { + size: 15, + header: '', + }, + }, + }); return ( { { {/* Subtitle/episode info */} {getSubtitle() && ( - + {getSubtitle()} @@ -254,7 +421,7 @@ const VODCard = ({ vodContent }) => { )} {/* Content information badges */} - + {contentType.toUpperCase()} @@ -314,79 +481,8 @@ const VODCard = ({ vodContent }) => { - {/* Connection details table */} - - - Active Connections: - - - {vodContent.connections.map((connection, index) => ( - - - - - {connection.client_ip} - - (Client: {connection.client_id.slice(0, 12)}...) - - - - - - - - {calculateConnectionDuration(connection)} - - - - - - {/* M3U Profile Information */} - {connection.m3u_profile && - (connection.m3u_profile.profile_name || - connection.m3u_profile.account_name) && ( - - - - M3U:{' '} - {connection.m3u_profile.account_name || - 'Unknown Account'}{' '} - →{' '} - {connection.m3u_profile.profile_name || - 'Default Profile'} - - - )} - - {/* User Agent info */} - {connection.user_agent && - connection.user_agent !== 'Unknown' && ( - - - {connection.user_agent.length > 60 - ? `${connection.user_agent.substring(0, 60)}...` - : connection.user_agent} - - - )} - - ))} - - + {/* Connection details table - similar to ChannelCard */} + ); @@ -1213,22 +1309,24 @@ const ChannelsPage = () => { // Combine active streams and VOD connections into a single mixed list const combinedConnections = useMemo(() => { - const activeStreams = Object.values(channelHistory).map(channel => ({ + const activeStreams = Object.values(channelHistory).map((channel) => ({ type: 'stream', data: channel, id: channel.channel_id, - sortKey: channel.uptime || 0 // Use uptime for sorting streams + sortKey: channel.uptime || 0, // Use uptime for sorting streams })); - const vodItems = vodConnections.map(vodContent => ({ + const vodItems = vodConnections.map((vodContent) => ({ type: 'vod', data: vodContent, id: `${vodContent.content_type}-${vodContent.content_uuid}`, - sortKey: Date.now() / 1000 // Use current time as fallback for VOD + sortKey: Date.now() / 1000, // Use current time as fallback for VOD })); // Combine and sort by newest connections first (higher sortKey = more recent) - return [...activeStreams, ...vodItems].sort((a, b) => b.sortKey - a.sortKey); + return [...activeStreams, ...vodItems].sort( + (a, b) => b.sortKey - a.sortKey + ); }, [channelHistory, vodConnections]); return ( @@ -1238,7 +1336,10 @@ const ChannelsPage = () => { Active Connections - {Object.keys(channelHistory).length} stream{Object.keys(channelHistory).length !== 1 ? 's' : ''} • {vodConnections.length} VOD connection{vodConnections.length !== 1 ? 's' : ''} + {Object.keys(channelHistory).length} stream + {Object.keys(channelHistory).length !== 1 ? 's' : ''} •{' '} + {vodConnections.length} VOD connection + {vodConnections.length !== 1 ? 's' : ''} Refresh Interval (seconds): @@ -1312,10 +1413,7 @@ const ChannelsPage = () => { ); } else if (connection.type === 'vod') { return ( - + ); } return null;