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}`