Skip to content

Add Command Center widgets and API key auto-discovery (phase 2) - #50

Merged
joshdev8 merged 3 commits into
mainfrom
feat/dashboard-widgets
Jul 29, 2026
Merged

joshdev8 merged 3 commits into
mainfrom
feat/dashboard-widgets

Conversation

@joshdev8

@joshdev8 joshdev8 commented Jul 28, 2026 •

Copy link
Copy Markdown
Owner

Phase 2 of the dashboard from #48: the six Command Center panels, the resource gauges, and the discovery layer that makes them work without anyone pasting an API key.

Based on feat/dashboard-scaffold (#49) — review that one first; this PR retargets to main once it merges.

Auto-discovery is what keeps this zero-config

Every service already writes its API key to a file this stack bind-mounts. The dashboard reads those through the read-only /discover mounts rather than asking anyone to paste anything:

Service File Field
Sonarr / Radarr / Prowlarr / Bazarr config.xml <ApiKey>
Tautulli config.ini [General] api_key
Seerr settings.json main.apiKey

Resolution is env var → discovered file → unconfigured, so an override still works for anyone running a service outside this stack.

Discovery re-runs on a TTL rather than once at boot. On a clean install those files don't exist yet, so a panel has to connect itself once the service behind it starts.

Widgets

Panel Source Notes
Resource gauges Prometheus / node-exporter No key needed — works on a bare stack
Now Playing Tautulli get_activity Transcode / direct stream / direct play hues
Downloads Transmission RPC + *arr queues Queue match labels each download's source
Requests Seerr An approved request that's processing reads as "Downloading"
Upcoming Sonarr calendar Downloaded / airing / missing
Activity Merged Sonarr + Radarr history Time-ordered; one dead contributor doesn't empty it
Setup Discovery state What's connected, and the one step for what isn't

Failure is a first-class state

Every widget route returns Result<T> and answers 200 even when its upstream failed, carrying a reason and a hint rather than an error status. That's what stops one dead upstream blanking the page.

Hints are specific to the failure. A rejected Transmission credential and an unreachable Transmission need different advice — a generic hint sends people looking in the wrong place.

Where the design mocked data the stack can't prove

  • VPN card drops the invented latency (unknowable outside the container) and reports reachability plus the provider/server already in .env.
  • Network gauge shows an unfilled ring rather than dividing throughput by a ceiling that doesn't exist.

Verification

Against live services, not just built:

  • Gauges match the host exactly — 20 cores, 31Gi total / ~8Gi used, 72T of 101T (confirmed against nproc, free, df)
  • Sonarr calendar, Seerr requests and the merged activity feed all return real data
  • Runtime re-discovery proven: an integration went waiting → live 33s after its config file appeared, with no restart
  • Env override confirmed to win over a discovered key; Tautulli's api_enabled = 0 correctly reports blocked with the specific fix
  • Transmission returned 401 on the test host and degraded with the credential hint — exactly the intended behaviour
  • No API key appears in any response body (scanned every endpoint against the real keys)
  • 38 tests, typecheck, lint, build and npm audit all clean

Refs #48

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Command Center view with panels for Now Playing, Downloads, Upcoming, Requests, Activity, plus a Prometheus-backed resource gauge strip.
    • Added an integrations/setup status view with auto-updating connection state.
    • Added VPN reachability status for Transmission in the dashboard sidebar.
  • Documentation
    • Expanded zero-configuration and environment setup guidance (discovery cadence, upstream timeouts, and widget-implementation steps), including expected limitations and hints behavior.
  • Bug Fixes
    • Improved resilience: widgets render stable empty/unavailable states with actionable hints instead of failing the page.
  • Tests
    • Added automated coverage for discovery parsing and widget helper behavior.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e44eaea4-131d-46bf-97d5-cc7d9da30288

📥 Commits

Reviewing files that changed from the base of the PR and between 4c09ddb and cd0694b.

📒 Files selected for processing (2)
  • dashboard/server/src/sources/seerr.ts
  • dashboard/server/src/sources/sources.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • dashboard/server/src/sources/seerr.ts
  • dashboard/server/src/sources/sources.test.ts

📝 Walkthrough

Walkthrough

The dashboard adds automatic credential discovery, resilient widget results, new upstream integrations and API routes, Prometheus gauges, VPN status reporting, and three switchable web views: Command Center, Launcher, and Setup.

Changes

Dashboard Expansion

Layer / File(s) Summary
Configuration, discovery, and resilient results
CLAUDE.md, README.md, dashboard/README.md, dashboard/server/src/config.ts, dashboard/server/src/discovery.ts, dashboard/server/src/discovery.test.ts, dashboard/server/src/http.ts
Adds environment-driven configuration, cached credential discovery, typed unavailable results, timeout-aware requests, and related documentation and tests.
Upstream service sources
dashboard/server/src/sources/*, dashboard/server/src/sources/*.test.ts
Adds normalized Servarr, Tautulli, Transmission, Prometheus, Seerr, upcoming, and activity sources with caching, formatting, VPN reachability, and helper coverage.
Dashboard API wiring
dashboard/server/src/index.ts
Exposes widget endpoints for metrics, streams, downloads, requests, upcoming items, activity, VPN status, and integrations.
Web dashboard views and rendering
dashboard/web/src/types.ts, dashboard/web/src/components/*, dashboard/web/src/views/*, dashboard/web/src/app/*
Adds shared widget types, panel states, gauges, Command Center feeds, Setup integration cards, VPN presentation, and view switching.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: new Command Center widgets plus API key auto-discovery for phase 2.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dashboard-widgets

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Phase 2 of the dashboard: the six Command Center panels, the resource
gauges, and the discovery layer that makes them work without anyone
pasting an API key.

Discovery is the piece that keeps the dashboard zero-config. Every service
already writes its key to a file the stack bind-mounts — config.xml for the
*arrs, config.ini for Tautulli, settings.json for Seerr — so the dashboard
reads those through the read-only /discover mounts instead of asking. An
env var still wins where one is set, for anyone running a service outside
this stack.

Discovery re-runs on a TTL rather than once at boot. On a clean install
those files don't exist yet, so a panel has to be able to connect itself
once the service behind it starts. Verified end to end: an integration went
from waiting to live 33s after its config appeared, with no restart.

Every widget route returns Result<T> and answers 200 even when its upstream
failed, carrying a reason and a hint instead of an error status. This is
what keeps one dead upstream from blanking the page. Hints are specific to
the failure — a rejected Transmission credential and an unreachable
Transmission need different advice, and a generic hint sends people
looking in the wrong place.

Two places where the design mocked data the stack can't actually prove:
the VPN card drops the invented latency and reports reachability plus the
provider/server already in .env, and a throughput gauge shows an unfilled
ring rather than dividing by a ceiling that doesn't exist.

Verified against live services: gauges match the host (20 cores, 31Gi RAM,
72T of 101T), the Sonarr calendar, Seerr requests and the merged activity
feed all return real data, and no API key appears in any response body.

Refs #48

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@joshdev8
joshdev8 changed the base branch from feat/dashboard-scaffold to main July 28, 2026 19:09
@joshdev8
joshdev8 force-pushed the feat/dashboard-widgets branch from 485a831 to 85ac3df Compare July 28, 2026 19:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
dashboard/web/src/views/CommandCenter.tsx (1)

52-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Widget payload shapes are hand-inlined here instead of imported.

Each usePolled<Result<{...}>>() call reconstructs the server's payload shape by hand. See consolidated comment for the cross-file fix.

🤖 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/views/CommandCenter.tsx` around lines 52 - 59, Update the
polling declarations in CommandCenter around streams, downloads, upcoming,
requests, and activity to use the shared imported payload/result types instead
of hand-inlined object shapes. Reuse the established type symbols for each
endpoint and preserve the existing polling intervals and usePolled behavior.
dashboard/web/src/types.ts (2)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Export composite widget payload types from types.ts instead of inlining them at each call site. types.ts only exports the entity-level interfaces (Stream, Download, RequestItem, UpcomingItem, ActivityItem, Gauge), not the composite shapes the server's get* functions actually return ({ streams, count, bandwidth, transcodes }, { downloads, active }, { requests, pending }, { items: UpcomingItem[] }, { items: ActivityItem[] }). CommandCenter.tsx compensates by re-declaring each shape inline, which will silently drift if a server payload changes.

  • dashboard/web/src/types.ts#L64-143: add and export named interfaces (e.g. StreamsPayload, DownloadsPayload, RequestsPayload, UpcomingPayload, ActivityPayload) mirroring the server's source modules.
  • dashboard/web/src/views/CommandCenter.tsx#L52-59: replace each inline Result<{...}> generic argument with the corresponding imported payload type.
🤖 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/types.ts` at line 1, Export named composite payload
interfaces from types.ts for the streams, downloads, requests, upcoming, and
activity response shapes, matching the server contracts. Update CommandCenter’s
get* Result generics to use the corresponding imported payload types instead of
inline object definitions, while leaving the existing entity interfaces and
result handling unchanged.

64-143: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Payload types aren't exported alongside the entity types.

Only Stream, Download, RequestItem, UpcomingItem, ActivityItem, Gauge are exported — the composite widget payload shapes ({ streams, count, bandwidth, transcodes }, { downloads, active }, etc.) aren't, forcing consumers to re-declare them inline. See consolidated comment for the cross-file details and suggested fix.

🤖 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/types.ts` around lines 64 - 143, Export named composite
payload types in dashboard/web/src/types.ts for each widget response, including
the stream, download, request, upcoming, activity, gauge, VPN, and integration
payload shapes, using the existing entity types and Result wrapper where
applicable. Keep the payload fields aligned with the server responses so
consumers can import these types instead of redeclaring them inline.
🤖 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 `@dashboard/server/src/sources/seerr.ts`:
- Around line 101-131: Update load() so pending is obtained from a separate
Seerr request using filter=pending and a small take, rather than filtering the 8
most-recent results. Read the pending count from that response’s
pageInfo.results while preserving the existing request list mapping.

In `@dashboard/web/src/app/Sidebar.tsx`:
- Around line 7-13: Thread VPN loading state through Sidebar: add a vpnLoading
prop to Props, pass vpn.loading from the App.tsx Sidebar call site, and update
VpnCard’s responding/detail unavailable-state logic to require !vpnLoading
before showing RPC unreachable. Preserve the existing confirmed-unavailable
behavior once loading completes.

In `@dashboard/web/src/components/Panel.tsx`:
- Around line 82-107: Update PanelBody to accept an error prop and render the
existing error result when a transport failure occurs before the “No response
yet” fallback; preserve loading and successful-data behavior. Update every
PanelBody caller, including CommandCenter, to pass the corresponding usePolled
error state such as streams.error.

---

Nitpick comments:
In `@dashboard/web/src/types.ts`:
- Line 1: Export named composite payload interfaces from types.ts for the
streams, downloads, requests, upcoming, and activity response shapes, matching
the server contracts. Update CommandCenter’s get* Result generics to use the
corresponding imported payload types instead of inline object definitions, while
leaving the existing entity interfaces and result handling unchanged.
- Around line 64-143: Export named composite payload types in
dashboard/web/src/types.ts for each widget response, including the stream,
download, request, upcoming, activity, gauge, VPN, and integration payload
shapes, using the existing entity types and Result wrapper where applicable.
Keep the payload fields aligned with the server responses so consumers can
import these types instead of redeclaring them inline.

In `@dashboard/web/src/views/CommandCenter.tsx`:
- Around line 52-59: Update the polling declarations in CommandCenter around
streams, downloads, upcoming, requests, and activity to use the shared imported
payload/result types instead of hand-inlined object shapes. Reuse the
established type symbols for each endpoint and preserve the existing polling
intervals and usePolled behavior.
🪄 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: c683d73e-400f-4d1a-99da-1050cebfe265

📥 Commits

Reviewing files that changed from the base of the PR and between 18e268a and 85ac3df.

📒 Files selected for processing (23)
  • CLAUDE.md
  • README.md
  • dashboard/README.md
  • dashboard/server/src/config.ts
  • dashboard/server/src/discovery.test.ts
  • dashboard/server/src/discovery.ts
  • dashboard/server/src/http.ts
  • dashboard/server/src/index.ts
  • dashboard/server/src/sources/activity.ts
  • dashboard/server/src/sources/arr.ts
  • dashboard/server/src/sources/prometheus.ts
  • dashboard/server/src/sources/seerr.ts
  • dashboard/server/src/sources/sources.test.ts
  • dashboard/server/src/sources/tautulli.ts
  • dashboard/server/src/sources/transmission.ts
  • dashboard/server/src/sources/upcoming.ts
  • dashboard/web/src/app/App.tsx
  • dashboard/web/src/app/Sidebar.tsx
  • dashboard/web/src/components/Gauges.tsx
  • dashboard/web/src/components/Panel.tsx
  • dashboard/web/src/types.ts
  • dashboard/web/src/views/CommandCenter.tsx
  • dashboard/web/src/views/Setup.tsx

Comment thread dashboard/server/src/sources/seerr.ts
Comment thread dashboard/web/src/app/Sidebar.tsx Outdated
Comment thread dashboard/web/src/components/Panel.tsx
Three fixes from review, each a case of the UI claiming more than it knew.

The Seerr pending count was derived from the 8 most-recent requests, so a
household with more requests than that — or with older ones still waiting
behind newer arrivals — saw a count that silently capped itself. It now
comes from its own filter=pending query, reading pageInfo.results, and
falls back to the visible rows if that call fails so a failed count can't
cost the panel its list.

VpnCard treated "the first /api/vpn response hasn't arrived" as "the RPC
is unreachable", so it rendered a warning on every page load before
anything had actually failed. Loading is now its own state, and the
warning is reserved for a confirmed problem — an icon that clears itself
a moment later teaches people to stop reading the card.

PanelBody reported a transport failure as "No response yet", implying a
request was still in flight when it had already failed. It now takes the
polling error and names it, distinct from an upstream declining to answer
(which arrives as a successful response carrying `available: false`).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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 `@dashboard/server/src/sources/seerr.ts`:
- Around line 110-116: Update pendingCount to runtime-validate
page.pageInfo?.results before returning it: accept only a finite, non-negative
number, and return fallback for missing, non-numeric, negative, or non-finite
values. Preserve the existing API request and fallback error behavior.
🪄 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: c95d849b-904b-42bf-91a0-e29a6a05ad38

📥 Commits

Reviewing files that changed from the base of the PR and between 85ac3df and 4c09ddb.

📒 Files selected for processing (6)
  • dashboard/server/src/sources/seerr.ts
  • dashboard/server/src/sources/sources.test.ts
  • dashboard/web/src/app/App.tsx
  • dashboard/web/src/app/Sidebar.tsx
  • dashboard/web/src/components/Panel.tsx
  • dashboard/web/src/views/CommandCenter.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • dashboard/web/src/app/Sidebar.tsx
  • dashboard/web/src/components/Panel.tsx
  • dashboard/web/src/app/App.tsx
  • dashboard/web/src/views/CommandCenter.tsx

Comment thread dashboard/server/src/sources/seerr.ts Outdated
getJson types the response body but doesn't validate it, so a successful
but malformed reply — pageInfo.results as a string, a negative, a NaN —
flowed straight through to the panel as the pending count. Check for a
finite non-negative number and fall back to the visible rows otherwise,
matching how the count already degrades on a failed request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@joshdev8
joshdev8 merged commit d1a8dd2 into main Jul 29, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant