You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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
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).
Boot-mode detection in hub/src/server.tsbuildApp: if required config (owner user, instance name, keys) is absent from hub.db.settingsand env, start in setup mode: serve the SPA + an unauthenticatedPOST /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.
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.
Update passwords from admin interface #115: PUT /api/admin/hub/users/:id/password (owner) + self-service PUT /api/admin/hub/me/password; AES path only; frontend form in features/hub-admin/.
SPA: auto-update on new version while preserving player + queue state #196: SPA build stamps a version (from package.json) into index.html; a lightweight poll of /api/health version triggers a "new version — reload" toast that preserves player/queue state via the existing persisted store. Scope: prompt-to-reload only, not silent hot-swap.
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:federationmust 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).
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.
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 -.-> afterImplementation plan
Phase 1 — single-container spike → adoption (#179)
hub/src/services/navidrome-supervisor.ts(new): spawn Navidrome vianode:child_processat boot whenNAVIDROME_MANAGED=true; restart with backoff on crash; forward stdout/stderr into the hub's pino logger (taggedcomponent: navidrome); kill onapp.onClose. Bind Navidrome to127.0.0.1:4533only.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 whenNAVIDROME_MANAGEDis unset (external-Navidrome escape hatch, documented).ND_*vars pass through to the child env — including theND_DEVAUTOCREATEADMINPASSWORDandND_SCANNER_PURGEMISSING=alwaysgotchas from pitfalls "Navidrome ops").docs/system-architecture.mddeployment diagram +README.mdoperations sections; checkscripts/for anything assuming thenavidromecompose service name (scripts/db-query.shtargets the hub — verify).Phase 2 — first-run setup wizard (#195)
hub/src/server.tsbuildApp: if required config (owner user, instance name, keys) is absent fromhub.db.settingsand env, start insetupmode: serve the SPA + an unauthenticatedPOST /api/setup(single-shot: atomically writes instance name, owner username+password (AES path — never hash, pitfalls "Auth"), generates Ed25519 keypair + JWT secret, then flips asetup_completesettings row). All other routes return 503 in setup mode. The endpoint must consume itself atomically (singleUPDATE … WHEREguard) 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 whenGET /api/healthbody reportssetupRequired: true. Wizard must fire no authenticated calls before setup completes (pitfalls: login-page 401 loop).docs/hub-internals.md.Phase 3 — log management (#205)
LOG_LEVELenv (defaultinfo), optional file output with rotation viapino-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 documentedlogging:block (json-file drivermax-size/max-file) indocker-compose.yml.Phase 4 — minimal observability (#3)
GET /metrics(Prometheus text viaprom-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 behindMETRICS_ENABLED=true; LAN/owner-only — reuse the LAN-gate pattern from DLNA (hub/test/lan-only.test.tsshows the precedent) or require owner JWT./api/healthbody (never the status code — pitfall Hub /api/health should reflect local Navidrome reachability #178) withnavidrome: managed|external,setupRequired, version fields.Phase 5 — quick wins
PUT /api/admin/hub/users/:id/password(owner) + self-servicePUT /api/admin/hub/me/password; AES path only; frontend form infeatures/hub-admin/.package.json) intoindex.html; a lightweight poll of/api/healthversion triggers a "new version — reload" toast that preserves player/queue state via the existing persisted store. Scope: prompt-to-reload only, not silent hot-swap.Testing plan
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:federationmust run against the single-container image — updatedocker-composefiles used by the federation suite (hub-a/b/c) so CI exercises the new topology end-to-end.hub/test/setup-mode.test.ts(new) — unconfigured boot serves only setup surface;POST /api/setupidempotency/atomicity (parallel double-submit → exactly one succeeds); post-setup boot is normal; env-precedence. Frontend wizard tests perdocs/frontend-testing.md, incl. "no authenticated fetches before completion".hub/test/metrics.test.ts— gated off by default; counters increment on a stream + a sync; LAN/owner gate enforced.hub/test/admin-routes.test.ts(password change: AES round-trip,u+t+sstill authenticates after change — proves the no-hash rule held).pnpm verifythroughout;pnpm test:federationfor Phases 1, 2, 4 (health/auth surface changes).Acceptance criteria
docker compose upwith zero env vars reaches the setup wizard; completing it yields a working single-container hub that can join a federation./metricsscrapeable when enabled;/api/healthstill 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.