feat(network): add session status card

Display server health metrics from the unused /session endpoint:
- Connected clients count (live from useClientStore)
- Server uptime since last restart
- Last client connection time (relative)
- Last integration request time (relative)
- Server timezone
- Cloud latency (Docker only) with warning threshold

Adds useSessionStats hook with 60s refetch interval, new NetworkStatus
component as first card in Network panel, and navigation menu entries
for Server status and Network interfaces sections.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrhcEgY8mqaxZi8veMPrrx
This commit is contained in:
Claude
2026-08-01 18:28:15 +00:00
parent 46e4ac94f1
commit 4e2baf1fa1
6 changed files with 146 additions and 49 deletions
+1
View File
@@ -15,6 +15,7 @@ export const RUNDOWN = ['rundown'];
export const CURRENT_RUNDOWN_QUERY_KEY = ['rundown', 'current'];
export const getRundownQueryKey = (rundownId: string) => ['rundown', rundownId];
export const RUNTIME = ['runtimeStore'];
export const SESSION_STATS = ['sessionStats'];
export const URL_PRESETS = ['urlpresets'];
export const VIEW_SETTINGS = ['viewSettings'];
export const CSS_OVERRIDE = ['cssOverride'];
+9 -1
View File
@@ -1,11 +1,19 @@
import axios from 'axios';
import { GetInfo, LinkOptions } from 'ontime-types';
import { GetInfo, LinkOptions, SessionStats } from 'ontime-types';
import { apiEntryUrl } from './constants';
import type { RequestOptions } from './requestOptions';
const sessionPath = `${apiEntryUrl}/session`;
/**
* HTTP request to retrieve statistics of the running session
*/
export async function getSessionStats(options?: RequestOptions): Promise<SessionStats> {
const res = await axios.get(sessionPath, { signal: options?.signal });
return res.data;
}
/**
* HTTP request to retrieve application info
*/
@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import { SessionStats } from 'ontime-types';
import { queryRefetchInterval } from '../../ontimeConfig';
import { SESSION_STATS } from '../api/constants';
import { getSessionStats } from '../api/session';
export default function useSessionStats() {
const { data, status, isError, refetch } = useQuery<SessionStats>({
queryKey: SESSION_STATS,
queryFn: ({ signal }) => getSessionStats({ signal }),
placeholderData: (previousData, _previousQuery) => previousData,
refetchInterval: queryRefetchInterval,
});
return { data, status, isError, refetch };
}
@@ -1,21 +1,14 @@
import { MessageTag } from 'ontime-types';
import { useEffect } from 'react';
import Tag from '../../../../common/components/tag/Tag';
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import { usePing } from '../../../../common/hooks/useSocket';
import { sendSocket } from '../../../../common/utils/socket';
import { isDocker, isOntimeCloud } from '../../../../externals';
import { isOntimeCloud } from '../../../../externals';
import type { PanelBaseProps } from '../../panel-list/PanelList';
import * as Panel from '../../panel-utils/PanelUtils';
import ClientControlPanel from './client-control/ClientControlPanel';
import InfoNif from './NetworkInterfaces';
import LogExport from './NetworkLogExport';
/** ping values above this are flagged to the user (ms) */
const slowPingThreshold = 100;
import NetworkStatus from './NetworkStatus';
export default function NetworkLogPanel({ location }: PanelBaseProps) {
const statusRef = useScrollIntoView<HTMLDivElement>('status', location);
const interfacesRef = useScrollIntoView<HTMLDivElement>('interfaces', location);
const clientsRef = useScrollIntoView<HTMLDivElement>('clients', location);
const logRef = useScrollIntoView<HTMLDivElement>('log', location);
@@ -23,7 +16,9 @@ export default function NetworkLogPanel({ location }: PanelBaseProps) {
return (
<>
<Panel.Header>Network</Panel.Header>
{isDocker && <OntimeCloudStats />}
<div ref={statusRef}>
<NetworkStatus />
</div>
{!isOntimeCloud && (
<div ref={interfacesRef}>
<Panel.Section>
@@ -48,40 +43,3 @@ export default function NetworkLogPanel({ location }: PanelBaseProps) {
</>
);
}
function OntimeCloudStats() {
const ping = usePing();
/**
* Send immediate ping request, and keep sending on an interval
*/
useEffect(() => {
sendSocket(MessageTag.Ping, new Date());
const doPing = setInterval(() => {
sendSocket(MessageTag.Ping, new Date());
}, 5000);
return () => {
clearInterval(doPing);
};
}, []);
return (
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Ontime cloud</Panel.SubHeader>
<Panel.Divider />
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field
title='Connection to the cloud server'
description='Time for a message to travel to the server and back. Lower is better'
/>
<Tag variant={ping > slowPingThreshold ? 'warning' : 'default'}>{ping}ms</Tag>
</Panel.ListItem>
</Panel.ListGroup>
</Panel.Card>
</Panel.Section>
);
}
@@ -0,0 +1,109 @@
import { MaybeString, MessageTag } from 'ontime-types';
import { MILLIS_PER_MINUTE } from 'ontime-utils';
import { useEffect } from 'react';
import Tag from '../../../../common/components/tag/Tag';
import useSessionStats from '../../../../common/hooks-query/useSessionStats';
import { usePing } from '../../../../common/hooks/useSocket';
import { useClientStore } from '../../../../common/stores/clientStore';
import { formatDuration } from '../../../../common/utils/time';
import { sendSocket } from '../../../../common/utils/socket';
import { isDocker } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
/** ping values above this are flagged to the user (ms) */
const slowPingThreshold = 100;
/** how often we ask the server for a ping (ms) */
const pingInterval = 5000;
/**
* Presents a date as the time elapsed since it happened
*/
function timeSince(date: MaybeString): string {
if (date === null) {
return 'Never';
}
const elapsed = Date.now() - new Date(date).getTime();
if (elapsed < MILLIS_PER_MINUTE) {
return 'Just now';
}
return `${formatDuration(elapsed)} ago`;
}
export default function NetworkStatus() {
const { data, status, isError } = useSessionStats();
// the client list is kept up to date over websocket, unlike the polled session stats
const clients = useClientStore((store) => store.clients);
return (
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Server status</Panel.SubHeader>
<Panel.Loader isLoading={status === 'pending'} />
{isError && <Panel.Error>Failed to load session data</Panel.Error>}
<Panel.Divider />
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field title='Connected clients' description='Clients currently connected to this server' />
<Tag>{Object.keys(clients).length}</Tag>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field title='Server running for' description='Time since the Ontime server was started' />
<Tag>{data ? formatDuration(Date.now() - new Date(data.startedAt).getTime()) : '...'}</Tag>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field title='Last client connection' description='When a client last connected to the server' />
<Tag>{data ? timeSince(data.lastConnection) : '...'}</Tag>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field
title='Last integration request'
description='When the server last received a request from an integration'
/>
<Tag>{data ? timeSince(data.lastRequest) : '...'}</Tag>
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field title='Server time zone' description='Time zone used by the machine running Ontime' />
<Tag>{data ? data.timezone : '...'}</Tag>
</Panel.ListItem>
{isDocker && <CloudPing />}
</Panel.ListGroup>
</Panel.Card>
</Panel.Section>
);
}
/**
* Ontime cloud users are not in the same network as the server
* we show the round trip time to help them qualify the connection
*/
function CloudPing() {
const ping = usePing();
/**
* Send immediate ping request, and keep sending on an interval
*/
useEffect(() => {
sendSocket(MessageTag.Ping, new Date());
const doPing = setInterval(() => {
sendSocket(MessageTag.Ping, new Date());
}, pingInterval);
return () => {
clearInterval(doPing);
};
}, []);
return (
<Panel.ListItem>
<Panel.Field
title='Ontime cloud latency'
description='Time for a message to travel to the server and back. Lower is better'
/>
<Tag variant={ping > slowPingThreshold ? 'warning' : 'default'}>{ping}ms</Tag>
</Panel.ListItem>
);
}
@@ -67,6 +67,10 @@ const staticOptions = [
id: 'network',
label: 'Network',
secondary: [
{
id: 'network__status',
label: 'Server status',
},
{
id: 'network__interfaces',
label: 'Network interfaces',