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 <noreply@anthropic.com>
This commit is contained in:
Samuel James 2026-06-21 09:35:55 -04:00 committed by GitHub
parent 9a1d2803a2
commit 9f10e8ee6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 45 additions and 12 deletions

View file

@ -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).

View file

@ -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:

View file

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

View file

@ -443,6 +443,7 @@ export interface Resource {
status: 'healthy' | 'warning' | 'critical' | 'unknown'
detail?: string
integration: string
integrationType: string
kind?: 'vm' | 'container' | 'app' | 'host' | 'network'
}

View file

@ -68,18 +68,26 @@ const nodeStatusColor: Record<string, string> = {
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<string, string> = {
app: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/uptime-kuma.png',
const cdnIconByIntegrationType: Record<string, string> = {
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<NodeTile[]>(() => {
if (!resources) return []
const groups = new Map<string, Resource[]>()
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 ? (
<div className="scrollbar-ghost grid min-h-0 flex-1 grid-cols-5 content-start gap-2" style={{ overflowY: 'auto' }}>
{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}`