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;