feat: document and surface node p2p telemetry

This commit is contained in:
lpf
2026-03-09 00:56:32 +08:00
parent f441972c56
commit c0fe977bce
9 changed files with 571 additions and 50 deletions

View File

@@ -4,6 +4,14 @@ import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
import StatCard from '../components/StatCard';
function formatRuntimeTime(value: unknown) {
const raw = String(value || '').trim();
if (!raw || raw === '0001-01-01T00:00:00Z') return '-';
const ts = Date.parse(raw);
if (Number.isNaN(ts)) return raw;
return new Date(ts).toLocaleString();
}
const Dashboard: React.FC = () => {
const { t } = useTranslation();
const {
@@ -15,6 +23,7 @@ const Dashboard: React.FC = () => {
skills,
cfg,
nodeP2P,
nodeDispatchItems,
taskQueueItems,
ekgSummary,
} = useAppContext();
@@ -45,6 +54,35 @@ const Dashboard: React.FC = () => {
const p2pRetryCount = Array.isArray(nodeP2P?.nodes)
? nodeP2P.nodes.reduce((sum: number, session: any) => sum + Number(session?.retry_count || 0), 0)
: 0;
const p2pNodeSessions = useMemo(() => {
if (!Array.isArray(nodeP2P?.nodes)) return [];
return [...nodeP2P.nodes]
.map((session: any) => ({
node: String(session?.node || '-'),
status: String(session?.status || 'unknown'),
retryCount: Number(session?.retry_count || 0),
lastError: String(session?.last_error || '').trim(),
lastReadyAt: formatRuntimeTime(session?.last_ready_at),
lastAttempt: formatRuntimeTime(session?.last_attempt),
createdAt: formatRuntimeTime(session?.created_at),
}))
.sort((a, b) => a.node.localeCompare(b.node));
}, [nodeP2P]);
const recentNodeDispatches = useMemo(() => {
return [...nodeDispatchItems]
.slice(0, 8)
.map((item: any, index: number) => ({
id: `${item?.time || 'dispatch'}-${index}`,
time: formatRuntimeTime(item?.time),
node: String(item?.node || '-'),
action: String(item?.action || '-'),
usedTransport: String(item?.used_transport || '-'),
fallbackFrom: String(item?.fallback_from || '').trim(),
durationMs: Number(item?.duration_ms || 0),
ok: Boolean(item?.ok),
error: String(item?.error || '').trim(),
}));
}, [nodeDispatchItems]);
return (
<div className="p-4 md:p-6 xl:p-8 w-full space-y-6 xl:space-y-8">
@@ -148,6 +186,119 @@ const Dashboard: React.FC = () => {
</div>
</div>
</div>
<div className="brand-card rounded-[30px] border border-zinc-800/80 p-6">
<div className="flex items-center justify-between gap-3 mb-5 flex-wrap">
<div>
<div className="flex items-center gap-2 text-zinc-200">
<Workflow className="w-5 h-5 text-zinc-400" />
<h2 className="text-lg font-medium">{t('dashboardNodeP2PSessions')}</h2>
</div>
<div className="text-xs text-zinc-500 mt-1">
{t('dashboardNodeP2PDetail', { transport: p2pTransport, sessions: p2pSessions, retries: p2pRetryCount })}
</div>
</div>
<div className="text-xs text-zinc-500">
{`${p2pConfiguredIce} ICE · ${p2pConfiguredStun} STUN`}
</div>
</div>
{p2pNodeSessions.length === 0 ? (
<div className="text-sm text-zinc-500 text-center py-8">{t('dashboardNodeP2PSessionsEmpty')}</div>
) : (
<div className="grid grid-cols-1 xl:grid-cols-2 gap-3">
{p2pNodeSessions.map((session) => {
const isOpen = session.status.toLowerCase() === 'open';
const isConnecting = session.status.toLowerCase() === 'connecting';
return (
<div key={session.node} className="brand-card-subtle rounded-2xl border border-zinc-800 p-4">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-sm font-medium text-zinc-100 truncate">{session.node}</div>
<div className="text-xs text-zinc-500 mt-1">
{t('dashboardNodeP2PSessionCreated')}: {session.createdAt}
</div>
</div>
<div className={`shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium ${isOpen ? 'bg-emerald-500/10 text-emerald-300' : isConnecting ? 'bg-amber-500/10 text-amber-300' : 'bg-rose-500/10 text-rose-300'}`}>
{session.status}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-4 text-xs">
<div>
<div className="text-zinc-400">{t('dashboardNodeP2PSessionRetries')}</div>
<div className="text-zinc-200 mt-1">{session.retryCount}</div>
</div>
<div>
<div className="text-zinc-400">{t('dashboardNodeP2PSessionReady')}</div>
<div className="text-zinc-200 mt-1">{session.lastReadyAt}</div>
</div>
<div>
<div className="text-zinc-400">{t('dashboardNodeP2PSessionAttempt')}</div>
<div className="text-zinc-200 mt-1">{session.lastAttempt}</div>
</div>
<div>
<div className="text-zinc-400">{t('dashboardNodeP2PSessionError')}</div>
<div className={`mt-1 break-all ${session.lastError ? 'text-rose-300' : 'text-zinc-500'}`}>
{session.lastError || '-'}
</div>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
<div className="brand-card rounded-[30px] border border-zinc-800/80 p-6">
<div className="flex items-center justify-between gap-3 mb-5 flex-wrap">
<div>
<div className="flex items-center gap-2 text-zinc-200">
<Activity className="w-5 h-5 text-zinc-400" />
<h2 className="text-lg font-medium">{t('dashboardNodeDispatches')}</h2>
</div>
<div className="text-xs text-zinc-500 mt-1">{t('dashboardNodeDispatchesHint')}</div>
</div>
</div>
{recentNodeDispatches.length === 0 ? (
<div className="text-sm text-zinc-500 text-center py-8">{t('dashboardNodeDispatchesEmpty')}</div>
) : (
<div className="grid grid-cols-1 xl:grid-cols-2 gap-3">
{recentNodeDispatches.map((item) => (
<div key={item.id} className="brand-card-subtle rounded-2xl border border-zinc-800 p-4">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-sm font-medium text-zinc-100 truncate">{`${item.node} · ${item.action}`}</div>
<div className="text-xs text-zinc-500 mt-1">{item.time}</div>
</div>
<div className={`shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium ${item.ok ? 'bg-emerald-500/10 text-emerald-300' : 'bg-rose-500/10 text-rose-300'}`}>
{item.ok ? 'ok' : 'error'}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-4 text-xs">
<div>
<div className="text-zinc-400">{t('dashboardNodeDispatchTransport')}</div>
<div className="text-zinc-200 mt-1">{item.usedTransport}</div>
</div>
<div>
<div className="text-zinc-400">{t('dashboardNodeDispatchFallback')}</div>
<div className="text-zinc-200 mt-1">{item.fallbackFrom || '-'}</div>
</div>
<div>
<div className="text-zinc-400">{t('dashboardNodeDispatchDuration')}</div>
<div className="text-zinc-200 mt-1">{`${item.durationMs}ms`}</div>
</div>
<div>
<div className="text-zinc-400">{t('dashboardNodeDispatchError')}</div>
<div className={`mt-1 break-all ${item.error ? 'text-rose-300' : 'text-zinc-500'}`}>
{item.error || '-'}
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};

View File

@@ -227,6 +227,14 @@ function formatStreamTime(ts?: number): string {
return new Date(ts).toLocaleTimeString([], { hour12: false });
}
function formatRuntimeTimestamp(value?: string): string {
const raw = `${value || ''}`.trim();
if (!raw || raw === '0001-01-01T00:00:00Z') return '-';
const ts = Date.parse(raw);
if (Number.isNaN(ts)) return raw;
return new Date(ts).toLocaleString();
}
function summarizePreviewText(value?: string, limit = 180): string {
const compact = `${value || ''}`.replace(/\s+/g, ' ').trim();
if (!compact) return '(empty)';
@@ -360,7 +368,7 @@ function GraphCard({
const Subagents: React.FC = () => {
const { t } = useTranslation();
const { q, nodeTrees, subagentRuntimeItems, subagentRegistryItems } = useAppContext();
const { q, nodeTrees, nodeP2P, nodeDispatchItems, subagentRuntimeItems, subagentRegistryItems } = useAppContext();
const ui = useUI();
const [items, setItems] = useState<SubagentTask[]>([]);
@@ -510,6 +518,26 @@ const Subagents: React.FC = () => {
return acc;
}, {});
}, [items]);
const p2pSessionByNode = useMemo(() => {
const out: Record<string, any> = {};
const sessions = Array.isArray(nodeP2P?.nodes) ? nodeP2P.nodes : [];
sessions.forEach((session: any) => {
const nodeID = normalizeTitle(session?.node, '');
if (!nodeID) return;
out[nodeID] = session;
});
return out;
}, [nodeP2P]);
const recentDispatchByNode = useMemo(() => {
const out: Record<string, any> = {};
const rows = Array.isArray(nodeDispatchItems) ? nodeDispatchItems : [];
rows.forEach((row: any) => {
const nodeID = normalizeTitle(row?.node, '');
if (!nodeID || out[nodeID]) return;
out[nodeID] = row;
});
return out;
}, [nodeDispatchItems]);
const topologyGraph = useMemo(() => {
const scale = topologyZoom;
const originX = 56;
@@ -645,6 +673,9 @@ const Subagents: React.FC = () => {
remoteClusters.forEach((cluster, treeIndex) => {
const { tree, root: treeRoot, children } = cluster;
const branch = `node:${normalizeTitle(tree.node_id, `remote-${treeIndex}`)}`;
const nodeID = normalizeTitle(tree.node_id, '');
const p2pSession = p2pSessionByNode[nodeID];
const recentDispatch = recentDispatchByNode[nodeID];
const rootX = remoteOffsetX + Math.max(0, (cluster.width - cardWidth) / 2);
if (!treeRoot) return;
const rootCard: GraphCardSpec = {
@@ -662,10 +693,15 @@ const Subagents: React.FC = () => {
meta: [
`status=${tree.online ? t('online') : t('offline')}`,
`transport=${normalizeTitle(treeRoot.transport, 'node')} type=${normalizeTitle(treeRoot.type, 'router')}`,
`p2p=${normalizeTitle(nodeP2P?.transport, 'disabled')} session=${normalizeTitle(p2pSession?.status, 'unknown')}`,
`last_transport=${normalizeTitle(recentDispatch?.used_transport, '-')}${recentDispatch?.fallback_from ? ` fallback=${normalizeTitle(recentDispatch?.fallback_from, '-')}` : ''}`,
`last_ready=${formatRuntimeTimestamp(p2pSession?.last_ready_at)}`,
`retry=${Number(p2pSession?.retry_count || 0)}`,
`${t('error')}=${normalizeTitle(p2pSession?.last_error, '-')}`,
`source=${normalizeTitle(treeRoot.managed_by, tree.source || '-')}`,
t('remoteTasksUnavailable'),
],
accent: tree.online ? 'bg-fuchsia-400' : 'bg-zinc-500',
accent: !tree.online ? 'bg-zinc-500' : normalizeTitle(p2pSession?.status, '').toLowerCase() === 'open' ? 'bg-emerald-400' : normalizeTitle(p2pSession?.status, '').toLowerCase() === 'connecting' ? 'bg-amber-400' : 'bg-fuchsia-400',
clickable: true,
scale,
onClick: () => {
@@ -695,10 +731,15 @@ const Subagents: React.FC = () => {
subtitle: `${normalizeTitle(child.agent_id, '-')} · ${normalizeTitle(child.role, '-')}`,
meta: [
`transport=${normalizeTitle(child.transport, 'node')} type=${normalizeTitle(child.type, 'worker')}`,
`p2p=${normalizeTitle(nodeP2P?.transport, 'disabled')} session=${normalizeTitle(p2pSession?.status, 'unknown')}`,
`last_transport=${normalizeTitle(recentDispatch?.used_transport, '-')}${recentDispatch?.fallback_from ? ` fallback=${normalizeTitle(recentDispatch?.fallback_from, '-')}` : ''}`,
`last_ready=${formatRuntimeTimestamp(p2pSession?.last_ready_at)}`,
`retry=${Number(p2pSession?.retry_count || 0)}`,
`${t('error')}=${normalizeTitle(p2pSession?.last_error, '-')}`,
`source=${normalizeTitle(child.managed_by, 'remote_webui')}`,
t('remoteTasksUnavailable'),
],
accent: 'bg-violet-400',
accent: normalizeTitle(p2pSession?.status, '').toLowerCase() === 'open' ? 'bg-emerald-400' : normalizeTitle(p2pSession?.status, '').toLowerCase() === 'connecting' ? 'bg-amber-400' : 'bg-violet-400',
clickable: true,
scale,
onClick: () => {
@@ -787,7 +828,7 @@ const Subagents: React.FC = () => {
}));
return { width, height, cards: decoratedCards, lines: decoratedLines };
}, [parsedNodeTrees, registryItems, taskStats, recentTaskByAgent, selectedTopologyBranch, topologyFilter, t, topologyZoom, nodeOverrides]);
}, [parsedNodeTrees, registryItems, taskStats, recentTaskByAgent, selectedTopologyBranch, topologyFilter, t, topologyZoom, nodeOverrides, nodeP2P, p2pSessionByNode, recentDispatchByNode]);
const fitView = () => {
const viewport = topologyViewportRef.current;