diff --git a/frontend/src/pages/nodes/NodeList.tsx b/frontend/src/pages/nodes/NodeList.tsx index e3dfc48..e4f0ed1 100644 --- a/frontend/src/pages/nodes/NodeList.tsx +++ b/frontend/src/pages/nodes/NodeList.tsx @@ -363,16 +363,34 @@ export default function NodeList({ ), dataIndex: 'url', ellipsis: true, - render: (_value, record) => ( - - {record.url} - - ), + render: (_value, record) => { + if (record.nodeType === 'agent') { + // Agent nodes have no panel to link to: show the host IPs the + // agent reports with its heartbeat as plain text (P13). + const ips = [record.ipv4, record.ipv6].filter(Boolean) as string[]; + return ( + + {ips.length > 0 + ? ips.map((ip) => ( + + {ip} + + )) + : '-'} + + ); + } + return ( + + {record.url} + + ); + }, }, { title: t('pages.nodes.status'), @@ -418,7 +436,12 @@ export default function NodeList({ dataIndex: 'panelVersion', align: 'center', render: (_value, record) => { + // Agent rows carry an "agent " label, not an upstream + // panel version: the update badge must never light up for them + // (the version comparator falls back to string inequality for + // unparseable versions, which would always differ) (P13). const canUpdate = + record.nodeType !== 'agent' && isUpdateEligible(record) && isPanelUpdateAvailable(latestVersion, record.panelVersion || ''); return ( diff --git a/frontend/src/schemas/node.ts b/frontend/src/schemas/node.ts index 3549b8c..c3ce052 100644 --- a/frontend/src/schemas/node.ts +++ b/frontend/src/schemas/node.ts @@ -47,6 +47,10 @@ export const NodeRecordSchema = z // P7: node kind, annotated by the backend for nodes registered by an // agent. Absent/traditional for classic panel-to-panel nodes. nodeType: z.enum(['traditional', 'agent']).optional(), + // P13: host IPv4/IPv6 an agent reports with its heartbeat telemetry. + // Rendered as plain text in the address column for agent rows. + ipv4: z.string().optional(), + ipv6: z.string().optional(), }) .loose();