Repository navigation
Add AutoPlexx Command Center dashboard (phase 1: scaffold, launcher, health) - #49
Conversation
Introduces the repo's first application: a unified dashboard over the stack, implementing the design handoff from issue #48. This is phase 1 of three — the app shell, service launcher and live container health. The Command Center widgets and the Upcoming / Now Playing pages follow. A backend is unavoidable here: API keys must not reach the browser, the *arr APIs and Tautulli send no CORS headers, and several services are only reachable on internal bridge networks. So the app is a backend-for-frontend (Fastify) serving a React SPA from one container and one origin. Built to need no setup, since this is a public repo people clone and run: - The image is published to GHCR and pulled, not built locally, so a fresh clone needs no Node toolchain. `build:` remains for contributors. - DASHBOARD_PORT is the only new variable and it defaults to 8090. - Read-only /discover mounts are declared so the dashboard can later read each service's API key from the config file that service already writes, rather than asking users to paste keys. Container status comes from docker-socket-proxy scoped to CONTAINERS=1, not a socket mount. `:ro` on a socket only affects the file node and does not make the Docker API read-only, so mounting it would have added a second root-equivalent exposure alongside Portainer's. Services absent from the host are excluded from the health denominator, so a user who trims services from docker-compose.yml doesn't see a count that can never reach 100%. Both containers are named autoplexx-* because bare `dashboard` is generic enough to collide with an existing container, and a container_name collision fails `docker compose up` outright. The vendored Nocturne stylesheet self-hosts Inter and Phosphor icons instead of fetching them from Google Fonts and unpkg, since a self-hosted media stack may have no outbound internet. Refs #48 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (16)
🚧 Files skipped from review as they are similar to previous changes (14)
📝 WalkthroughWalkthroughAdds an AutoPlexx Dashboard with a Fastify backend, React/Vite frontend, Docker health reporting, Compose integration, production packaging, CI/release workflows, and updated operational documentation. ChangesDashboard backend and health integration
Dashboard frontend
Delivery and documentation
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Browser
participant DashboardServer
participant DockerSocketProxy
Browser->>DashboardServer: Request dashboard API data
DashboardServer->>DockerSocketProxy: Query container status
DockerSocketProxy-->>DashboardServer: Return container status
DashboardServer-->>Browser: Return service catalog and health report
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
CLAUDE.md (1)
59-59: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winCorrect the Tracearr scope and fail-fast variable description.
dashboard/is also an application, anddocker-compose.ymlrequires Transmission’sOPENVPN_*variables with${VAR:?must be set}in addition to Tracearr’s variables. Describe Tracearr as the only multi-container subsystem and list both sets of required variables.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@CLAUDE.md` at line 59, Update the Tracearr description in CLAUDE.md to call it the only multi-container subsystem rather than the only app, while preserving its three-container details. Expand the fail-fast variable description to include Transmission’s required OPENVPN_* variables alongside Tracearr’s DB_PASSWORD, JWT_SECRET, and COOKIE_SECRET.Source: Coding guidelines
🧹 Nitpick comments (1)
dashboard/web/src/styles/nocturne.css (1)
147-210: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftUse the Nocturne spacing tokens consistently.
Replace raw component spacing with
var(--space-*)tokens (or token-basedcalc()where needed) so the dashboard remains on the declared design scale.
dashboard/web/src/styles/nocturne.css#L147-L210: replace raw componentgap,padding, and margin spacing values with the defined spacing tokens.dashboard/web/src/app/Sidebar.tsx#L91-L91: replacegap: 2with a spacing token.dashboard/web/src/components/StackHealth.tsx#L37-L43: replacemarginTop: 2with a spacing token.dashboard/web/src/views/Launcher.tsx#L57-L70: replace raw margin and gap spacing values with spacing tokens.As per coding guidelines, “Use the design tokens from
dashboard/web/src/styles/nocturne.cssfor colors, spacing, radii, and shadows instead of hard-coded values.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@dashboard/web/src/styles/nocturne.css` around lines 147 - 210, Replace hard-coded component gap, padding, and margin values in the .btn, .field > label, .radio, and .seg-opt styles with the declared --space-* tokens or token-based calc() expressions. In dashboard/web/src/app/Sidebar.tsx lines 91-91, replace gap: 2 with the appropriate spacing token; in dashboard/web/src/components/StackHealth.tsx lines 37-43, replace marginTop: 2 with a spacing token; and in dashboard/web/src/views/Launcher.tsx lines 57-70, replace raw margin and gap values with spacing tokens. Preserve existing layout behavior while applying the design scale consistently.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/dashboard-ci.yml:
- Line 25: Update the actions/checkout@v4 step in the dashboard CI workflow to
disable persisted checkout credentials by setting persist-credentials to false.
In `@dashboard/server/src/config.ts`:
- Around line 8-10: Update the port configuration parsing around num so
dashboard and Grafana ports use a dedicated validator requiring finite integers
in the inclusive range 1..65535, while retaining num’s existing positive-number
behavior for HEALTH_TTL_MS.
In `@dashboard/server/src/services.ts`:
- Around line 194-196: Update the service catalog response near the Grafana
entry to derive port from the configured GRAFANA_PORT environment value at
request time, falling back to 3000 when unset, and return the resolved numeric
port instead of the hardcoded value. Preserve the existing Grafana metadata and
launcher behavior.
In `@dashboard/server/src/sources/docker.ts`:
- Around line 78-87: Update the error fallback in the Docker health-fetch
function to preserve the last successful services and totals while setting only
reachable to false. Track or reuse the most recent successful report, and return
the all-absent SERVICES fallback only when no successful report exists yet; keep
up, total, and attention from the cached report during outages.
In `@dashboard/web/src/app/Sidebar.tsx`:
- Around line 148-165: Update VpnCard so the secured value is not derived from
transmission.state or presented as verified VPN protection. Use a neutral
container-status label and matching non-affirmative icon state based on the
existing Transmission status, unless verified VPN connectivity telemetry is
available; remove the “Transmission secured” wording while preserving the
not-running behavior.
In `@dashboard/web/src/components/StackHealth.tsx`:
- Around line 54-82: Update the StackHealth component’s health-status logic
around allUp and the rendered status text to handle health.total === 0
separately. Display “No managed services discovered” for an empty report instead
of “0 / 0 up” or “All services nominal,” while preserving the existing healthy
and attention states for non-empty reports.
In `@dashboard/web/src/hooks/usePolled.ts`:
- Around line 22-37: Update the usePolled hook’s load callback and its
interval/visibility refresh paths to track the latest request, using a sequence
ID or abort mechanism alongside cancelled. Only the current request may commit
setData, setError, or setLoading, while preserving cleanup cancellation and
existing fetch error handling.
In `@dashboard/web/src/styles/autoplexx.css`:
- Around line 10-32: Replace the local literal status color values in
autoplexx.css lines 10-32 with matching shared --color-* tokens from
nocturne.css. Also replace raw gap and padding values in autoplexx.css lines
71-85 with matching --space-* tokens, and replace the subtitle margin in
Header.tsx line 29 with the appropriate --space-* token.
In `@dashboard/web/vite.config.ts`:
- Around line 11-17: Update the Vite server proxy configuration to target the
port configured by DASHBOARD_PORT, reusing the existing configuration source
used by dashboard/server/src/config.ts instead of hardcoding 8090. Ensure both
the /api and /healthz proxy entries remain synchronized with the configured
dashboard port.
In `@README.md`:
- Around line 68-70: Update the README dashboard documentation around the
startup URL and referenced service-table sections to describe 8090 as the
default, explain that the URL and published port use the configured
DASHBOARD_PORT value, and replace any claim that it reads only one variable with
wording consistent with the documented runtime variables and API-key overrides.
- Line 228: Update the dashboard deployment documentation around the
port-publishing guidance to explicitly require binding the published port only
to the intended local interface or restricting it with firewall rules, so direct
access cannot bypass reverse-proxy authentication. Keep the existing LAN-only
and reverse-proxy recommendations consistent with the Compose port
configuration.
---
Outside diff comments:
In `@CLAUDE.md`:
- Line 59: Update the Tracearr description in CLAUDE.md to call it the only
multi-container subsystem rather than the only app, while preserving its
three-container details. Expand the fail-fast variable description to include
Transmission’s required OPENVPN_* variables alongside Tracearr’s DB_PASSWORD,
JWT_SECRET, and COOKIE_SECRET.
---
Nitpick comments:
In `@dashboard/web/src/styles/nocturne.css`:
- Around line 147-210: Replace hard-coded component gap, padding, and margin
values in the .btn, .field > label, .radio, and .seg-opt styles with the
declared --space-* tokens or token-based calc() expressions. In
dashboard/web/src/app/Sidebar.tsx lines 91-91, replace gap: 2 with the
appropriate spacing token; in dashboard/web/src/components/StackHealth.tsx lines
37-43, replace marginTop: 2 with a spacing token; and in
dashboard/web/src/views/Launcher.tsx lines 57-70, replace raw margin and gap
values with spacing tokens. Preserve existing layout behavior while applying the
design scale consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8a275667-73ca-4e90-a3c0-f560ffe2018d
⛔ Files ignored due to path filters (1)
dashboard/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (39)
.env.example.github/workflows/dashboard-ci.yml.github/workflows/dashboard-release.yml.gitignoreCLAUDE.mdREADME.mddashboard/.dockerignoredashboard/Dockerfiledashboard/README.mddashboard/eslint.config.jsdashboard/package.jsondashboard/server/package.jsondashboard/server/src/cache.tsdashboard/server/src/config.tsdashboard/server/src/index.tsdashboard/server/src/services.tsdashboard/server/src/sources/docker.test.tsdashboard/server/src/sources/docker.tsdashboard/server/tsconfig.build.jsondashboard/server/tsconfig.jsondashboard/tsconfig.base.jsondashboard/web/index.htmldashboard/web/package.jsondashboard/web/src/app/App.tsxdashboard/web/src/app/Header.tsxdashboard/web/src/app/Sidebar.tsxdashboard/web/src/components/ServiceIcon.tsxdashboard/web/src/components/StackHealth.tsxdashboard/web/src/components/StatusDot.tsxdashboard/web/src/hooks/usePolled.tsdashboard/web/src/hooks/useTheme.tsdashboard/web/src/main.tsxdashboard/web/src/styles/autoplexx.cssdashboard/web/src/styles/nocturne.cssdashboard/web/src/types.tsdashboard/web/src/views/Launcher.tsxdashboard/web/tsconfig.jsondashboard/web/vite.config.tsdocker-compose.yml
| // Host port is ${GRAFANA_PORT:-3000}; resolved from env at request time. | ||
| port: 3000, | ||
| blurb: 'Metrics visualization', |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Resolve Grafana’s configured host port instead of publishing a fixed one.
The comment says ${GRAFANA_PORT:-3000} is resolved at request time, but this catalog always returns 3000. A non-default Grafana port will produce a broken launcher URL. Resolve the configured value in the server response, or document this as a fixed port.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@dashboard/server/src/services.ts` around lines 194 - 196, Update the service
catalog response near the Grafana entry to derive port from the configured
GRAFANA_PORT environment value at request time, falling back to 3000 when unset,
and return the resolved numeric port instead of the hardcoded value. Preserve
the existing Grafana metadata and launcher behavior.
| :root { | ||
| --ap-green: oklch(78% 0.14 165); | ||
| --ap-green-dim: oklch(46% 0.09 165); | ||
| --ap-amber: oklch(82% 0.13 78); | ||
| --ap-amber-dim: oklch(50% 0.09 78); | ||
| --ap-red: oklch(72% 0.16 22); | ||
| --ap-red-dim: oklch(46% 0.12 22); | ||
| --ap-cyan: oklch(80% 0.11 220); | ||
| --ap-cyan-dim: oklch(48% 0.09 220); | ||
| --ap-violet: var(--color-accent-400); | ||
| --ap-violet-dim: var(--color-accent-700); | ||
| } | ||
|
|
||
| [data-theme='light'] { | ||
| --color-bg: #f3f5fe; | ||
| --color-surface: #ffffff; | ||
| --color-text: #292b31; | ||
| --color-divider: color-mix(in srgb, #292b31 14%, transparent); | ||
| --ap-green: oklch(58% 0.15 165); | ||
| --ap-amber: oklch(62% 0.15 78); | ||
| --ap-red: oklch(56% 0.19 22); | ||
| --ap-cyan: oklch(58% 0.13 230); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use the shared Nocturne token system rather than local literal values.
dashboard/web/src/styles/autoplexx.css#L10-L32: map semantic status colors to existing--color-*tokens rather than raw OKLCH/hex values.dashboard/web/src/styles/autoplexx.css#L71-L85: replace raw gap and padding values with matching--space-*tokens.dashboard/web/src/app/Header.tsx#L29-L29: replace the raw subtitle margin with a matching--space-*token.
As per coding guidelines, dashboard colors and spacing must come from dashboard/web/src/styles/nocturne.css.
📍 Affects 2 files
dashboard/web/src/styles/autoplexx.css#L10-L32(this comment)dashboard/web/src/styles/autoplexx.css#L71-L85dashboard/web/src/app/Header.tsx#L29-L29
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@dashboard/web/src/styles/autoplexx.css` around lines 10 - 32, Replace the
local literal status color values in autoplexx.css lines 10-32 with matching
shared --color-* tokens from nocturne.css. Also replace raw gap and padding
values in autoplexx.css lines 71-85 with matching --space-* tokens, and replace
the subtitle margin in Header.tsx line 29 with the appropriate --space-* token.
Source: Coding guidelines
Eleven fixes from review. The two substantive ones: Container health no longer forgets what it knew. A failed call to the socket proxy replaced every service with `absent` and, because that resolved successfully, memoize cached the emptiness — so a momentary blip blanked the sidebar and the health tile for the full TTL. That directly contradicted this repo's own rule that a failed refresh keeps the last good data. The last successful report is now retained and only flagged unreachable, with the all-absent result reserved for a failure before any successful poll. Both paths are tested. The VPN card stopped claiming something it can't know. A running container is not proof the tunnel came up or that egress is protected; haugene's image does gate traffic on the tunnel, but this card can only observe container state. It now says so and claims nothing about protection. Also: - usePolled tracks a request sequence, so a slow earlier response can't overwrite newer data when the interval and the visibility handler overlap (or when StrictMode double-mounts). - An empty health report renders "No services found" rather than "0 / 0 up · all systems nominal", which read as healthy. - Ports are validated as integers in 1..65535; the generic positive-number parser accepted 8090.5 and 70000, which would stop Fastify binding or produce an unresolvable launcher link. - Grafana's configured port resolves through one shared helper instead of being duplicated across two routes, and the comment now points at where that happens. - Vite's dev proxy follows DASHBOARD_PORT rather than hardcoding 8090. - Both workflows check out with persist-credentials: false, since npm ci runs dependency lifecycle scripts. - DASHBOARD_BIND makes the reverse-proxy advice actionable: a port published on all interfaces lets anyone bypass proxy authentication, so binding to loopback is now a documented one-liner rather than a caveat. - README no longer hardcodes 8090 or claims one variable is all that's read; CLAUDE.md no longer calls Tracearr the only app and now lists both groups of fail-fast variables. Two suggestions were not taken. Mapping the --ap-* status hues onto --color-* tokens isn't possible: Nocturne is a declared mono palette with no green/amber/red to map to. Replacing raw spacing with --space-* tokens in the vendored nocturne.css would edit a file kept verbatim for re-syncing, and the 0.7x scale (2.8/5.6/8.4px) has no member matching the 4px values the handoff specifies, so it would change the design rather than align it. Refs #48 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Addressed in a748a80. Eleven of the thirteen findings applied; two declined with reasoning below. Applied
Two notes on the fixes: The health report issue was the most valuable catch — it directly contradicted this repo's own stated rule that a failed refresh keeps the last good data, and The VPN card point applies to phase 2 as well, where the card had been rewired to real RPC reachability. A responding RPC is no more proof of a working tunnel than a running container is, so #50 was updated to match rather than inheriting the same overclaim. Not appliedMap Replace raw spacing with 🤖 Generated with Claude Code |
Implements phase 1 of the dashboard from the design handoff in #48 — the app shell, service launcher, and live container health. This is the repo's first application; the Command Center widgets and the Upcoming / Now Playing pages follow in two more PRs.
Why there's a backend
The prototype is static, but the real thing can't be. API keys must not reach the browser, the *arr APIs and Tautulli send no CORS headers, and several services are only reachable on internal bridge networks. So this is a backend-for-frontend: Fastify holds the credentials and fans out, serving a React SPA from one container on one origin.
Zero-config, because this is a public repo people clone and run
build:stays for contributors. A fresh clone needs no Node toolchain.DASHBOARD_PORTis the only one and it defaults to8090./discovermounts are declared so the dashboard can read each service's API key from the config file that service already writes. Consumed in phase 2.Security
Container status comes from
docker-socket-proxyscoped toCONTAINERS=1, not a socket mount.:roon a socket only affects the file node — it does not make the Docker API read-only — so mounting it would have added a second root-equivalent exposure alongside Portainer's. Verified:/containers/json→ 200,/images/json→ 403,exec→ 403.The dashboard is read-only and ships no auth; documented as LAN-only.
Verification
Tested against a live Docker daemon, not just built:
(healthy)/(unhealthy)/(health: starting)suffixes parsed correctlynode, Docker healthcheck reacheshealthy, image is 180MB/api/*404s correctly while client routes fall through to the shellreachable: falserather than erroringtypecheck,lint, 9 tests,npm run buildanddocker compose configall pass;npm auditcleanNotes for review
autoplexx-*. Baredashboardis generic enough to collide with an existing container, and acontainer_namecollision failsdocker compose upoutright.ghcr.io/tecnativa/...rather than Docker Hub, so it pulls anonymously and avoids Hub rate limits.pathsfilter — one would apply to tag pushes too and silently skip publishing a tagged release.Maintainer step after merge
The first release run publishes
ghcr.io/joshdev8/autoplexx-dashboard. Make that package public (Packages → autoplexx-dashboard → Package settings → Change visibility) so anonymousdocker compose pullworks. Until then users fall back to thebuild:stanza.Refs #48
🤖 Generated with Claude Code
Summary by CodeRabbit
DASHBOARD_BIND.DASHBOARD_PORT) and reverse-proxy guidance.