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();