diff --git a/backend/src/integrations/uptimeKuma.ts b/backend/src/integrations/uptimeKuma.ts index 7130da9..2ef162e 100644 --- a/backend/src/integrations/uptimeKuma.ts +++ b/backend/src/integrations/uptimeKuma.ts @@ -99,11 +99,6 @@ export const uptimeKuma: IntegrationAdapter = { // single "done" signal, so give it a short window to arrive. await new Promise((resolve) => setTimeout(resolve, 2500)) - console.log(`[uptimeKuma] received ${monitors.size} monitor(s), ${lastHeartbeat.size} heartbeat(s)`) - for (const m of monitors.values()) { - console.log(`[uptimeKuma] monitor ${m.id} "${m.name}" active=${m.active} heartbeat=${JSON.stringify(lastHeartbeat.get(m.id))}`) - } - const resources: Resource[] = [] for (const monitor of monitors.values()) { if (!monitor.active) continue diff --git a/src/pages/Infrastructure.tsx b/src/pages/Infrastructure.tsx index 2dcc872..652c506 100644 --- a/src/pages/Infrastructure.tsx +++ b/src/pages/Infrastructure.tsx @@ -68,6 +68,25 @@ const nodeStatusColor: Record = { const integrationPalette = ['#C8A434', '#E67E22', '#2ECC71', '#7A7D85', '#3B82F6', '#8B5E3C'] +// Kinds that represent a monitoring/aggregator app with many sub-items (e.g. Uptime +// Kuma's individual monitors) collapse into one tile per integration on Node Status, +// with the underlying items shown in Node Detail once selected. +const groupedKinds = new Set(['app']) + +const cdnIconByIntegrationKind: Record = { + app: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/uptime-kuma.png', +} + +interface NodeGroup { + isGroup: true + integration: string + kind: string + status: Resource['status'] + members: Resource[] +} + +type NodeTile = Resource | NodeGroup + const kindIcon: Record = { vm: MonitorSmartphone, container: Box, @@ -116,7 +135,7 @@ export default function Infrastructure() { const [resources, setResources] = useState(null) const [integrations, setIntegrations] = useState(null) const [events, setEvents] = useState(null) - const [selectedNode, setSelectedNode] = useState(null) + const [selectedNode, setSelectedNode] = useState(null) const navigate = useNavigate() useEffect(() => { @@ -144,6 +163,37 @@ export default function Infrastructure() { return Array.from(byIntegration.entries()).map(([name, value], i) => ({ name, value, color: integrationPalette[i % integrationPalette.length] })) }, [resources]) + // Kinds like Uptime Kuma's monitors can number in the hundreds — collapse them into + // one tile per integration on Node Status, with members listed in Node Detail. + const nodeTiles = useMemo(() => { + if (!resources) return [] + const groups = new Map() + const singles: Resource[] = [] + for (const r of resources) { + if (r.kind && groupedKinds.has(r.kind)) { + const arr = groups.get(r.integration) ?? [] + arr.push(r) + groups.set(r.integration, arr) + } else { + singles.push(r) + } + } + const groupTiles: NodeGroup[] = Array.from(groups.entries()).map(([integration, members]) => ({ + isGroup: true, + integration, + kind: members[0]?.kind ?? '', + status: members.some((m) => m.status === 'critical') + ? 'critical' + : members.some((m) => m.status === 'warning') + ? 'warning' + : members.every((m) => m.status === 'healthy') + ? 'healthy' + : 'unknown', + members, + })) + return [...singles, ...groupTiles] + }, [resources]) + return ( <> {/* Sub-tabs + Add Resource — hero banner is rendered at the layout level behind this */} @@ -254,14 +304,17 @@ export default function Infrastructure() {

Node Status

- {resources && resources.length > 0 ? ( + {nodeTiles.length > 0 ? (
- {resources.map((node, i) => { + {nodeTiles.map((node, i) => { + const cdnIcon = cdnIconByIntegrationKind[node.kind ?? ''] const NodeIcon = kindIcon[node.kind ?? ''] ?? Server + const label = 'isGroup' in node ? node.integration : node.name + const tooltip = 'isGroup' in node ? `${node.integration}: ${node.members.length} monitored` : `${node.name}: ${node.detail ?? node.status}` return (
setSelectedNode(node)} className="cursor-pointer transition-colors" style={{ @@ -276,9 +329,13 @@ export default function Infrastructure() { >
- + {cdnIcon ? ( + + ) : ( + + )}
- {node.name} + {label}
) })} @@ -322,9 +379,42 @@ export default function Infrastructure() { {/* Node Detail — shows the node selected up in Node Status */}
-
+

Node Detail

- {selectedNode ? ( + {selectedNode && 'isGroup' in selectedNode ? ( +
+
+ + {selectedNode.integration} + ({selectedNode.members.length} monitored) +
+
+ {selectedNode.members.map((m, i) => ( +
+ + {m.name} + {m.detail ?? m.status} +
+ ))} +
+
+ ) : selectedNode ? (