From 9f10e8ee6f7531f917bc637a4555e89c8e3070d5 Mon Sep 17 00:00:00 2001 From: Samuel James <143277412+SamuelSJames@users.noreply.github.com> Date: Sun, 21 Jun 2026 09:35:55 -0400 Subject: [PATCH] Group all integration node tiles by integration except Proxmox (#39) Generalizes the Uptime Kuma monitor-grouping pattern to every integration: Node Status now collapses each integration's resources into one tile (e.g. 30 EC2 instances under one "AWS" tile) instead of flooding the grid, with members listed in Node Detail on selection. Proxmox stays ungrouped since its VMs/LXCs are managed individually elsewhere in the app. Adds integrationType to the /api/integrations/resources response so the frontend can group/exclude by adapter type rather than resource kind (kind alone can't distinguish Proxmox VMs from AWS VMs, for example). Documents the grouping rule in HANDOFF.md and adds a paid-tier roadmap entry for per-integration node tabs that will show every individual node. Co-authored-by: Claude --- HANDOFF.md | 1 + ROADMAP.md | 21 +++++++++++++++++++++ backend/src/routes/integrations.ts | 4 ++-- src/lib/api.ts | 1 + src/pages/Infrastructure.tsx | 30 ++++++++++++++++++++---------- 5 files changed, 45 insertions(+), 12 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index b6057e6..0858663 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -54,6 +54,7 @@ No new feature is queued. Pick up from here: - `backend/src/routes/` — one file per route group (`auth`, `bookmarks`, `integrations`, `events`, `terminal`, `tunnels`, `files`, `docker`, `dockerSsh`, `agents`, `guacamole`, `metrics`, `transfer`, `data`). - `backend/src/routes/auth.ts` — `/api/setup` (first-run, creates the first admin user), `/api/auth/login`, `/api/auth/me` (GET/PUT), `/api/auth/password`, `/api/auth/sessions`, `/api/auth/logout`, `/api/auth/login-events` (Phase 2), plus user-management endpoints `/api/users` (GET/POST) and `/api/users/:id` (PUT/DELETE) gated by `requireAdmin` (Phase 3). - `backend/src/integrations/` — the 8 integration adapters (Proxmox, Docker, NetBird, Cloudflare, AWS, Uptime Kuma, Weather, SSH). +- **Node Status grouping rule**: `GET /api/integrations/resources` tags every resource with `integrationType` (the adapter's `IntegrationType`, e.g. `'aws'`, `'docker'`). `Infrastructure.tsx`'s Node Status tab collapses every integration's resources into **one tile per integration** — except Proxmox (`ungroupedIntegrationTypes` in `Infrastructure.tsx`), which stays ungrouped since its VMs/LXCs are managed individually elsewhere in the app. Clicking a grouped tile lists its members in the Node Detail card. This means e.g. 30 EC2 instances under one AWS integration show as a single "AWS" tile, not 30 separate tiles. See `ROADMAP.md` for the planned paid-tier per-integration tabs that will surface every individual node. - `backend/src/ssh/` — SSH-backed feature engines: terminal sessions, tunnels, file ops, host metrics collectors, host-to-host transfer, and `docker.ts` (**Docker-over-SSH** — runs the `docker` CLI on a remote SSH host; PR #31). - Docker images run on Alpine; **OpenSSL legacy provider is enabled** in `backend/Dockerfile` (`OPENSSL_CONF=/etc/ssl/openssl-legacy.cnf`) so old-format encrypted PEM keys (`BEGIN RSA PRIVATE KEY` + `DEK-Info`) still decrypt under OpenSSL 3 — don't remove this without understanding why it's there. - **Required env vars, no defaults**: `ARCHNEST_SECRET_KEY`, `ARCHNEST_JWT_SECRET`. Server refuses to start without both. Optional: `ARCHNEST_DB_PATH`, `PORT`, `ARCHNEST_GUAC_CRYPT_KEY`/`ARCHNEST_GUACD_HOST`/`ARCHNEST_GUACD_PORT`, `ARCHNEST_CORS_ORIGIN`, **`ARCHNEST_AGENT_TOKEN`** (enables the Docker agent ingest endpoint — when unset, ingest is disabled / returns 503), **`ARCHNEST_AGENT_STALE_MS`** (default 90000; when an agent report is considered stale). diff --git a/ROADMAP.md b/ROADMAP.md index 23f18f4..aafd104 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -136,6 +136,27 @@ A complementary **agent** model is planned, split across tiers: --- +## Per-integration node tabs — PAID ADD-ON + +**Status:** not built; planned as a paid-tier feature. + +Node Status on the Infrastructure page collapses every integration (except +Proxmox) into a **single tile per integration** — e.g. 30 EC2 instances under +one "AWS" tile, all of Uptime Kuma's monitors under one "Uptime" tile — with +the individual members only visible in the Node Detail card after selecting +that tile (`ungroupedIntegrationTypes` in `src/pages/Infrastructure.tsx`). +This keeps the grid usable when an integration has dozens/hundreds of +resources, but it means there's currently no way to see *all* nodes of a +given integration laid out at once. + +Planned scope (paid tier): a dedicated **tab per integration** (alongside +today's Overview/Network etc. sub-tabs) that lists every node belonging to +that integration — full grid, not just the grouped summary tile — for users +who want to browse/filter dozens of EC2 instances, Docker containers, or +Uptime Kuma monitors directly rather than drilling through Node Detail. + +--- + ## Known non-blocking stubs (cosmetic, not scheduled) Not flagged as work to do unless explicitly asked: diff --git a/backend/src/routes/integrations.ts b/backend/src/routes/integrations.ts index f535ade..9d68dee 100644 --- a/backend/src/routes/integrations.ts +++ b/backend/src/routes/integrations.ts @@ -143,7 +143,7 @@ export async function integrationRoutes(app: FastifyInstance) { app.get('/api/integrations/resources', async () => { const rows = db.prepare("SELECT * FROM integrations WHERE enabled = 1 AND status = 'connected'").all() as IntegrationRow[] - const resources: (Resource & { integration: string })[] = [] + const resources: (Resource & { integration: string; integrationType: string })[] = [] for (const row of rows) { const adapter = adapterRegistry[row.type as IntegrationType] if (!adapter.listResources) continue @@ -151,7 +151,7 @@ export async function integrationRoutes(app: FastifyInstance) { const secrets = loadSecrets(row.id) try { const found = await adapter.listResources(config, secrets) - for (const r of found) resources.push({ ...r, integration: row.name }) + for (const r of found) resources.push({ ...r, integration: row.name, integrationType: row.type }) } catch (err) { app.log.warn(`listResources failed for integration "${row.name}" (${row.type}): ${err instanceof Error ? err.message : err}`) } diff --git a/src/lib/api.ts b/src/lib/api.ts index f4c7257..dbbce05 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -443,6 +443,7 @@ export interface Resource { status: 'healthy' | 'warning' | 'critical' | 'unknown' detail?: string integration: string + integrationType: string kind?: 'vm' | 'container' | 'app' | 'host' | 'network' } diff --git a/src/pages/Infrastructure.tsx b/src/pages/Infrastructure.tsx index 652c506..b087bc9 100644 --- a/src/pages/Infrastructure.tsx +++ b/src/pages/Infrastructure.tsx @@ -68,18 +68,26 @@ 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']) +// Every integration except Proxmox collapses its resources into one tile per +// integration on Node Status (e.g. 30 EC2 instances under one "AWS" tile, all of +// Uptime Kuma's monitors under one "Uptime" tile) — the underlying items are listed +// in Node Detail once selected. Proxmox stays ungrouped since its VMs/LXCs are +// managed individually elsewhere in the app. See ROADMAP.md for the planned paid-tier +// per-integration tabs that will show every node, not just the grouped tile. +const ungroupedIntegrationTypes = new Set(['proxmox']) -const cdnIconByIntegrationKind: Record = { - app: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/uptime-kuma.png', +const cdnIconByIntegrationType: Record = { + uptime_kuma: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/uptime-kuma.png', + aws: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/aws.png', + docker: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/docker.png', + netbird: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/netbird.png', + cloudflare: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/cloudflare.png', } interface NodeGroup { isGroup: true integration: string + integrationType: string kind: string status: Resource['status'] members: Resource[] @@ -163,14 +171,15 @@ 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. + // An integration can have dozens of resources (e.g. 30 EC2 instances) — collapse + // everything except Proxmox 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)) { + if (!ungroupedIntegrationTypes.has(r.integrationType)) { const arr = groups.get(r.integration) ?? [] arr.push(r) groups.set(r.integration, arr) @@ -181,6 +190,7 @@ export default function Infrastructure() { const groupTiles: NodeGroup[] = Array.from(groups.entries()).map(([integration, members]) => ({ isGroup: true, integration, + integrationType: members[0]?.integrationType ?? '', kind: members[0]?.kind ?? '', status: members.some((m) => m.status === 'critical') ? 'critical' @@ -307,7 +317,7 @@ export default function Infrastructure() { {nodeTiles.length > 0 ? (
{nodeTiles.map((node, i) => { - const cdnIcon = cdnIconByIntegrationKind[node.kind ?? ''] + const cdnIcon = cdnIconByIntegrationType[node.integrationType] 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}`