Skip to content

feat(ops): operator-experience milestone — single container, setup wizard, logs, metrics (umbrella) #246

Description

@benders

FROM @claude: Proposal from a codebase + issue-backlog analysis (2026-07-08). Plan written for execution by a coding agent (phases are independently executable).

Problem

Operator experience has fallen behind features. For software whose premise is "friends run their own hubs," first-run setup and diagnosability are adoption blockers, not polish:

This umbrella sequences the highest-leverage subset into one milestone, anchored on the single-container consolidation, which shrinks the surface everything else has to manage.

Related issues (umbrella)

Target deployment

flowchart LR
    subgraph before ["Today: 2 services"]
        H1[hub container] --- N1[navidrome container]
    end
    subgraph after ["After #179: 1 service"]
        direction TB
        S[poutine container]
        S --> P[hub process :3000]
        P --> NV[navidrome child process :4533<br/>localhost only, supervised]
        P --> W[first-run setup wizard<br/>when unconfigured]
        P --> L[rotating logs + /metrics]
    end
    before -.-> after
Loading

Implementation plan

Phase 1 — single-container spike → adoption (#179)

  • hub/src/services/navidrome-supervisor.ts (new): spawn Navidrome via node:child_process at boot when NAVIDROME_MANAGED=true; restart with backoff on crash; forward stdout/stderr into the hub's pino logger (tagged component: navidrome); kill on app.onClose. Bind Navidrome to 127.0.0.1:4533 only.
  • Dockerfile: multi-stage — copy the Navidrome binary (pin version; it's a static Go binary) into the hub image. docker-compose.yml: collapse to one service; keep the two-service topology working when NAVIDROME_MANAGED is unset (external-Navidrome escape hatch, documented).
  • Preserve existing env contract (ND_* vars pass through to the child env — including the ND_DEVAUTOCREATEADMINPASSWORD and ND_SCANNER_PURGEMISSING=always gotchas from pitfalls "Navidrome ops").
  • Update docs/system-architecture.md deployment diagram + README.md operations sections; check scripts/ for anything assuming the navidrome compose service name (scripts/db-query.sh targets the hub — verify).

Phase 2 — first-run setup wizard (#195)

  • Boot-mode detection in hub/src/server.ts buildApp: if required config (owner user, instance name, keys) is absent from hub.db.settings and env, start in setup mode: serve the SPA + an unauthenticated POST /api/setup (single-shot: atomically writes instance name, owner username+password (AES path — never hash, pitfalls "Auth"), generates Ed25519 keypair + JWT secret, then flips a setup_complete settings row). All other routes return 503 in setup mode. The endpoint must consume itself atomically (single UPDATE … WHERE guard) so it cannot be re-run — same TOCTOU discipline as invitations.
  • frontend/src/features/hub-admin/SetupWizard.tsx (new) + route /setup; SPA redirects to it when GET /api/health body reports setupRequired: true. Wizard must fire no authenticated calls before setup completes (pitfalls: login-page 401 loop).
  • Existing env-var config keeps precedence for automated deploys; wizard only fills gaps. Document precedence in docs/hub-internals.md.

Phase 3 — log management (#205)

  • Structured logs already go through Fastify/pino. Add: LOG_LEVEL env (default info), optional file output with rotation via pino-roll (LOG_FILE, LOG_MAX_SIZE, LOG_MAX_FILES) for the native/macOS deployment (macOS: proper packaging for native hub (launchd, auto-start, log rotation) #183); Docker deployments keep stdout + a documented logging: block (json-file driver max-size/max-file) in docker-compose.yml.
  • Supervised Navidrome logs (Phase 1) inherit the same sink for free.

Phase 4 — minimal observability (#3)

  • GET /metrics (Prometheus text via prom-client): process defaults + counters for streams by source kind, sync runs/failures, merge duration, orphan-audit counts (from the merge-hardening issue), peer gossip errors, art-cache hit rate. Gate behind METRICS_ENABLED=true; LAN/owner-only — reuse the LAN-gate pattern from DLNA (hub/test/lan-only.test.ts shows the precedent) or require owner JWT.
  • Enrich /api/health body (never the status code — pitfall Hub /api/health should reflect local Navidrome reachability #178) with navidrome: managed|external, setupRequired, version fields.

Phase 5 — quick wins

Testing plan

  • Phase 1: hub/test/navidrome-supervisor.test.ts (new) — spawn a fake child script: crash→backoff restart, clean shutdown on close, log forwarding. Full-stack proof: pnpm test:federation must run against the single-container image — update docker-compose files used by the federation suite (hub-a/b/c) so CI exercises the new topology end-to-end.
  • Phase 2: hub/test/setup-mode.test.ts (new) — unconfigured boot serves only setup surface; POST /api/setup idempotency/atomicity (parallel double-submit → exactly one succeeds); post-setup boot is normal; env-precedence. Frontend wizard tests per docs/frontend-testing.md, incl. "no authenticated fetches before completion".
  • Phase 3: log-config unit tests (level/file wiring); manual rotation check documented in the PR.
  • Phase 4: hub/test/metrics.test.ts — gated off by default; counters increment on a stream + a sync; LAN/owner gate enforced.
  • Phase 5: extend hub/test/admin-routes.test.ts (password change: AES round-trip, u+t+s still authenticates after change — proves the no-hash rule held).
  • pnpm verify throughout; pnpm test:federation for Phases 1, 2, 4 (health/auth surface changes).

Acceptance criteria

  • docker compose up with zero env vars reaches the setup wizard; completing it yields a working single-container hub that can join a federation.
  • One container image is the only deploy artifact; external-Navidrome mode still documented and tested.
  • Logs bounded on disk; /metrics scrapeable when enabled; /api/health still always-200.

Agent execution notes

Read docs/system-architecture.md, docs/pitfalls.md ("Navidrome ops", "Auth"), docs/authentication.md (Phase 2/5), docs/hub-internals.md (env vars) first. Each phase is its own feature branch + Draft PR (no stacked PRs); Phase 1 lands before Phase 2. Do not run against the live instance — use the test compose stacks; no DB resets on the running deployment.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions