Skip to content

Add header search, Seerr request link, and alert bell (phase 3) - #51

Merged
joshdev8 merged 2 commits into
mainfrom
feat/dashboard-header
Jul 29, 2026
Merged

Add header search, Seerr request link, and alert bell (phase 3)#51
joshdev8 merged 2 commits into
mainfrom
feat/dashboard-header

Conversation

@joshdev8

@joshdev8 joshdev8 commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Phase 3 of the dashboard from #48: the header controls that #49 deliberately shipped as empty space, because they'd have been dead until phase 2's data landed.

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

The three controls

Control What it does Why it's built this way
Search Filters the service catalog, opens what you pick. / or Ctrl/Cmd+K to focus Only services with a published port are offered as results — opening one is all a result does. Matches with no web UI are named in a footer line instead of becoming rows that do nothing on Enter
Request Deep-links to Seerr The dashboard is read-only and ships no auth. A request endpoint here would let anyone who can reach the LAN page add to the library under Seerr's credentials, with no record of who did it. Seerr already has accounts and approval rules
Alerts What needs attention, from container state + integrations Derived in the browser from data already on screen, so no new endpoint re-polls the same two sources to produce what the client can assemble for free

The avatar from the design stays out — there's no auth for it to represent.

What deliberately doesn't raise an alert

  • absent services. A user who trimmed services out of their compose file would otherwise get a permanent list of alerts for things they chose not to run.
  • waiting integrations. That's the normal state on a clean install, where a service simply hasn't written its config file yet. Alerting would mean a first boot opens with a full inbox that clears itself.
  • An unreachable socket proxy makes every service read absent, which would render the loudest possible problem as total silence. That case short-circuits to a single alert naming the proxy.

Also here

  • The integrations poll moves up to App — the bell and Setup now share one request instead of each running their own.
  • The header title truncates before the controls shrink. Without it the search box was the thing that collapsed on a narrow window, down to just its own icon.

Verification

Driven in a browser against the live stack (18/18 up), not just built:

  • Search ranking confirmed: so returns Sonarr (name prefix) ahead of cAdvisor and FlareSolverr, with aria-activedescendant tracking the highlight
  • watch returns no openable rows and correctly reports "Also matched, no web UI: Watchlistarr" — Watchtower, a system service, stays out of search entirely
  • Bell reads "nothing needs attention" on the healthy stack, with all six integrations waiting — confirming waiting is not an alert
  • With container state doctored to an unhealthy Radarr, a stopped Tautulli and a blocked Seerr: 3 alerts, ordered attn → down → blocked, each carrying Docker's own status line ("Exited (137) 2 minutes ago") rather than a generic label
  • The blocked alert's Setup action closes the popover, switches views, and lands on the matching integration row with its hint
  • Header verified at 1440px and at 900px, where the subtitle ellipsizes and the search stays usable
  • typecheck, lint, 40 tests and build all clean

Note for review

web/ still has no test runner, so alerts.ts and the search ranking in CommandSearch.tsx are verified in the browser rather than by unit test, despite both being pure functions written to be testable. Adding one is web infrastructure rather than a feature, so it belongs with the responsive pass rather than in here — happy to do it either way.

Refs #48

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added header search for quickly finding and opening services.
    • Added a Request button that links to Seerr when available.
    • Added an Alerts menu showing service and integration issues, with setup and open actions.
    • Added a Setup view displaying integration connection status, hints, and configuration sources.
    • Added dashboard views for command center, launcher, and setup, with live metrics and VPN status.
  • Documentation

    • Expanded widget guidance and documented header controls and alert visibility conditions.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 53e91ecc-e859-4cd8-9d22-76e43d4533ce

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds polling for dashboard metrics, VPN, and integrations; introduces command search, Seerr request linking, alert notifications, view switching, and an integration setup view; and documents header behavior and alert suppression states.

Changes

Dashboard experience

Layer / File(s) Summary
Dashboard shell and view wiring
dashboard/web/src/app/App.tsx
Adds metrics, VPN, and integration polling, derives alerts, wires dashboard data into the sidebar and header, and switches among command, launcher, and setup views.
Header search, requests, and alerts
dashboard/web/src/alerts.ts, dashboard/web/src/app/Header.tsx, dashboard/web/src/components/CommandSearch.tsx, dashboard/web/src/components/Notifications.tsx, dashboard/web/src/hooks/useDismissable.ts, dashboard/web/src/styles/autoplexx.css
Adds ranked service search, conditional Seerr request linking, ordered alert derivation, notification popovers, dismiss behavior, and header popover styling.
Setup view and dashboard documentation
dashboard/web/src/views/Setup.tsx, dashboard/README.md
Adds integration status cards with loading, connection, hint, and environment-override states, and documents header controls and alert suppression behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant deriveAlerts
  participant Header
  participant Notifications
  App->>deriveAlerts: Pass health and integrations
  deriveAlerts-->>App: Return ordered alerts
  App->>Header: Pass services, groups, and alerts
  Header->>Notifications: Render alert controls
  Notifications-->>App: Request setup view
Loading

Possibly related PRs

  • joshdev8/AutoPlexx#50: Introduces the integrations data and hint/state shapes consumed by the Setup and header flows.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.33% 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 summarizes the main header additions: search, Seerr request link, and alert bell.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dashboard-header

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.

@joshdev8
joshdev8 force-pushed the feat/dashboard-header branch from 5f4d2ed to 27c716b Compare July 28, 2026 21:10
Phase 3 of the dashboard from #48 — the header controls that phase 1
deliberately left out because the data behind them hadn't landed yet.

Search filters the service catalog and opens what you pick, focused with
"/" or Ctrl/Cmd+K. Only services with a published port are offered as
results, since opening one is all a result does; matches without a web UI
are named in a footer line rather than becoming rows that do nothing on
Enter.

Request deep-links to Seerr instead of posting. The dashboard is
read-only and ships no auth, so a request endpoint here would let anyone
who can reach the page add to the library under Seerr's credentials with
no record of who did it.

Alerts are derived in the browser from /api/health plus /api/integrations
— both already polled — so no new endpoint re-fetches what the client can
assemble for free. The integrations poll moves up to App so one request
feeds both the bell and Setup.

Two states deliberately stay quiet: `absent` services, so trimming
compose doesn't produce permanent alerts, and `waiting` integrations,
which are normal on a clean install and would otherwise open a first boot
with an inbox that clears itself. An unreachable socket proxy makes every
service read `absent`, so that case short-circuits to one alert naming
the proxy rather than reporting nothing at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@joshdev8
joshdev8 changed the base branch from feat/dashboard-widgets to main July 29, 2026 14:58
@joshdev8
joshdev8 force-pushed the feat/dashboard-header branch from 27c716b to 87c9a6f Compare July 29, 2026 14:58
@joshdev8

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@joshdev8
joshdev8 changed the base branch from main to feat/dashboard-widgets July 29, 2026 15:06
@joshdev8
joshdev8 changed the base branch from feat/dashboard-widgets to main July 29, 2026 15:06

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

🧹 Nitpick comments (3)
dashboard/server/src/discovery.ts (1)

127-227: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

No unit coverage for discoverArr/discoverTautulli/discoverSeerr/discoverOne.

discovery.test.ts only exercises xmlTag/iniValue via __test. The higher-level per-source discovery functions (waiting/blocked/live transitions, env-override precedence) are untested, even though DISCOVER_ROOT is already overridable via env, making them straightforward to test against a temp fixture directory.

🤖 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/discovery.ts` around lines 127 - 227, Add unit coverage
for discoverArr, discoverTautulli, discoverSeerr, and discoverOne using a
temporary DISCOVER_ROOT fixture directory. Test missing and incomplete
configuration waiting states, Tautulli’s blocked state when the API is disabled,
valid live discovery for each source, Seerr parse-retry behavior, and
discoverOne’s environment-variable override taking precedence over discovered
credentials.
dashboard/web/src/components/Gauges.tsx (1)

20-57: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Gauges and VpnCard don't propagate polling failures or upstream reasons, unlike CommandCenter's PanelBody-driven panels. Both widgets were built without the error-prop/PanelEmpty pattern that PanelBody (Panel.tsx lines 101-114) already establishes for every other panel in this PR, so a persistent transport failure or an upstream-declined reason/hint is silently lost on the resource strip and VPN card.

  • dashboard/web/src/components/Gauges.tsx#L20-L57: add an error?: string | null prop and render PanelEmpty (reason "Dashboard API unreachable" + error as hint) on the first card when data is null and error is set, mirroring PanelBody.
  • dashboard/web/src/app/App.tsx#L78-L140: forward vpn.error into <Sidebar> and metrics.error into <Gauges> so the new prop above has data to work with.
  • dashboard/web/src/app/Sidebar.tsx#L160-L214: in VpnCard, surface vpn.reason/vpn.hint when vpn.available === false instead of collapsing that case into the generic "RPC unreachable" text, and account for a transport-level vpn.error distinctly from an upstream decline.
🤖 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/components/Gauges.tsx` around lines 20 - 57, Update Gauges
to accept an optional error prop and render PanelEmpty on the first placeholder
when data is null and error is present, using “Dashboard API unreachable” as the
reason and error as the hint. In dashboard/web/src/app/App.tsx lines 78-140,
forward vpn.error to Sidebar and metrics.error to Gauges. In
dashboard/web/src/app/Sidebar.tsx lines 160-214, update VpnCard to display
vpn.reason and vpn.hint for unavailable upstream responses, while handling
transport-level vpn.error separately from an upstream decline.
dashboard/web/src/styles/autoplexx.css (1)

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

Use design tokens for .ap-badge spacing/radius, not hard-coded pixel values.

Colors correctly use var(--ap-amber)/var(--color-bg), but the offsets, size, radius, and font-size (-4px, 16px, 8px, 10px) are all hard-coded. As per coding guidelines, "Use the var(--*) design tokens from web/src/styles/nocturne.css for colors, spacing, radii, and shadows instead of hard-coded values."

♻️ Example using nearest tokens (adjust to whatever nocturne.css actually exposes)
 .ap-badge {
   position: absolute;
-  top: -4px;
-  right: -4px;
-  min-width: 16px;
-  height: 16px;
-  padding: 0 4px;
-  border-radius: 8px;
+  top: calc(-1 * var(--space-1));
+  right: calc(-1 * var(--space-1));
+  min-width: var(--space-4);
+  height: var(--space-4);
+  padding: 0 var(--space-1);
+  border-radius: var(--radius-full, 8px);
   background: var(--ap-amber);
   color: var(--color-bg);
-  font-size: 10px;
   font-weight: 600;
-  line-height: 16px;
+  line-height: var(--space-4);
   text-align: center;
 }

Separately, worth confirming badge text contrast in the light theme, where --ap-amber becomes a mid-lightness oklch(62% 0.15 78) and --color-bg becomes a near-white #f3f5fe — the dark-theme pairing (light amber + presumably dark --color-bg) likely has more headroom than this one.

🤖 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 110 - 125, Update the
`.ap-badge` styles to replace hard-coded offsets, dimensions, padding, radius,
line-height, and font-size with the nearest available `var(--*)` design tokens
from `nocturne.css`. Preserve the existing layout, badge appearance, and color
variables, and verify the selected tokens cover all spacing and sizing values
called out in the review.

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 `@dashboard/README.md`:
- Around line 73-86: Make the file path reference in the widget instructions
consistent by updating the `hintFor()` example to include the same `server/src/`
prefix used for the source-module path, without changing the surrounding
guidance.

In `@dashboard/server/src/http.ts`:
- Around line 56-69: Update the memoize() cache-refresh logic so a loader result
replaces the cached value and expiresAt only when the existing cached result is
available; retain the last successful result when a refresh returns Unavailable,
while preserving normal caching behavior for successful loads.

In `@dashboard/server/src/sources/prometheus.ts`:
- Around line 28-36: Update the Prometheus query helper query to catch getJson
failures and return null, preserving its existing null behavior for unsuccessful
responses or missing/invalid samples so one failed parallel query does not
reject load's Promise.all.

In `@dashboard/server/src/sources/seerr.ts`:
- Around line 163-169: Update getRequests in
dashboard/server/src/sources/seerr.ts (lines 163-169), the corresponding
Tautulli flow in dashboard/server/src/sources/tautulli.ts (lines 149-158), and
the Sonarr flow in dashboard/server/src/sources/upcoming.ts (lines 41-47) to add
and use a hintFor(reason) helper, mirroring transmission.ts. Pass the generated
hint to safely(load, hint) or post-process the unavailable result so
authentication rejection and connection failures receive distinct, actionable
advice.

---

Nitpick comments:
In `@dashboard/server/src/discovery.ts`:
- Around line 127-227: Add unit coverage for discoverArr, discoverTautulli,
discoverSeerr, and discoverOne using a temporary DISCOVER_ROOT fixture
directory. Test missing and incomplete configuration waiting states, Tautulli’s
blocked state when the API is disabled, valid live discovery for each source,
Seerr parse-retry behavior, and discoverOne’s environment-variable override
taking precedence over discovered credentials.

In `@dashboard/web/src/components/Gauges.tsx`:
- Around line 20-57: Update Gauges to accept an optional error prop and render
PanelEmpty on the first placeholder when data is null and error is present,
using “Dashboard API unreachable” as the reason and error as the hint. In
dashboard/web/src/app/App.tsx lines 78-140, forward vpn.error to Sidebar and
metrics.error to Gauges. In dashboard/web/src/app/Sidebar.tsx lines 160-214,
update VpnCard to display vpn.reason and vpn.hint for unavailable upstream
responses, while handling transport-level vpn.error separately from an upstream
decline.

In `@dashboard/web/src/styles/autoplexx.css`:
- Around line 110-125: Update the `.ap-badge` styles to replace hard-coded
offsets, dimensions, padding, radius, line-height, and font-size with the
nearest available `var(--*)` design tokens from `nocturne.css`. Preserve the
existing layout, badge appearance, and color variables, and verify the selected
tokens cover all spacing and sizing values called out in the review.
🪄 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: 104678cb-6163-453e-b0b4-d9f7177e6e04

📥 Commits

Reviewing files that changed from the base of the PR and between cd0694b and 87c9a6f.

📒 Files selected for processing (29)
  • 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/alerts.ts
  • dashboard/web/src/app/App.tsx
  • dashboard/web/src/app/Header.tsx
  • dashboard/web/src/app/Sidebar.tsx
  • dashboard/web/src/components/CommandSearch.tsx
  • dashboard/web/src/components/Gauges.tsx
  • dashboard/web/src/components/Notifications.tsx
  • dashboard/web/src/components/Panel.tsx
  • dashboard/web/src/hooks/useDismissable.ts
  • dashboard/web/src/styles/autoplexx.css
  • dashboard/web/src/types.ts
  • dashboard/web/src/views/CommandCenter.tsx
  • dashboard/web/src/views/Setup.tsx

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

🧹 Nitpick comments (3)
dashboard/server/src/discovery.ts (1)

127-227: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

No unit coverage for discoverArr/discoverTautulli/discoverSeerr/discoverOne.

discovery.test.ts only exercises xmlTag/iniValue via __test. The higher-level per-source discovery functions (waiting/blocked/live transitions, env-override precedence) are untested, even though DISCOVER_ROOT is already overridable via env, making them straightforward to test against a temp fixture directory.

🤖 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/discovery.ts` around lines 127 - 227, Add unit coverage
for discoverArr, discoverTautulli, discoverSeerr, and discoverOne using a
temporary DISCOVER_ROOT fixture directory. Test missing and incomplete
configuration waiting states, Tautulli’s blocked state when the API is disabled,
valid live discovery for each source, Seerr parse-retry behavior, and
discoverOne’s environment-variable override taking precedence over discovered
credentials.
dashboard/web/src/components/Gauges.tsx (1)

20-57: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Gauges and VpnCard don't propagate polling failures or upstream reasons, unlike CommandCenter's PanelBody-driven panels. Both widgets were built without the error-prop/PanelEmpty pattern that PanelBody (Panel.tsx lines 101-114) already establishes for every other panel in this PR, so a persistent transport failure or an upstream-declined reason/hint is silently lost on the resource strip and VPN card.

  • dashboard/web/src/components/Gauges.tsx#L20-L57: add an error?: string | null prop and render PanelEmpty (reason "Dashboard API unreachable" + error as hint) on the first card when data is null and error is set, mirroring PanelBody.
  • dashboard/web/src/app/App.tsx#L78-L140: forward vpn.error into <Sidebar> and metrics.error into <Gauges> so the new prop above has data to work with.
  • dashboard/web/src/app/Sidebar.tsx#L160-L214: in VpnCard, surface vpn.reason/vpn.hint when vpn.available === false instead of collapsing that case into the generic "RPC unreachable" text, and account for a transport-level vpn.error distinctly from an upstream decline.
🤖 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/components/Gauges.tsx` around lines 20 - 57, Update Gauges
to accept an optional error prop and render PanelEmpty on the first placeholder
when data is null and error is present, using “Dashboard API unreachable” as the
reason and error as the hint. In dashboard/web/src/app/App.tsx lines 78-140,
forward vpn.error to Sidebar and metrics.error to Gauges. In
dashboard/web/src/app/Sidebar.tsx lines 160-214, update VpnCard to display
vpn.reason and vpn.hint for unavailable upstream responses, while handling
transport-level vpn.error separately from an upstream decline.
dashboard/web/src/styles/autoplexx.css (1)

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

Use design tokens for .ap-badge spacing/radius, not hard-coded pixel values.

Colors correctly use var(--ap-amber)/var(--color-bg), but the offsets, size, radius, and font-size (-4px, 16px, 8px, 10px) are all hard-coded. As per coding guidelines, "Use the var(--*) design tokens from web/src/styles/nocturne.css for colors, spacing, radii, and shadows instead of hard-coded values."

♻️ Example using nearest tokens (adjust to whatever nocturne.css actually exposes)
 .ap-badge {
   position: absolute;
-  top: -4px;
-  right: -4px;
-  min-width: 16px;
-  height: 16px;
-  padding: 0 4px;
-  border-radius: 8px;
+  top: calc(-1 * var(--space-1));
+  right: calc(-1 * var(--space-1));
+  min-width: var(--space-4);
+  height: var(--space-4);
+  padding: 0 var(--space-1);
+  border-radius: var(--radius-full, 8px);
   background: var(--ap-amber);
   color: var(--color-bg);
-  font-size: 10px;
   font-weight: 600;
-  line-height: 16px;
+  line-height: var(--space-4);
   text-align: center;
 }

Separately, worth confirming badge text contrast in the light theme, where --ap-amber becomes a mid-lightness oklch(62% 0.15 78) and --color-bg becomes a near-white #f3f5fe — the dark-theme pairing (light amber + presumably dark --color-bg) likely has more headroom than this one.

🤖 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 110 - 125, Update the
`.ap-badge` styles to replace hard-coded offsets, dimensions, padding, radius,
line-height, and font-size with the nearest available `var(--*)` design tokens
from `nocturne.css`. Preserve the existing layout, badge appearance, and color
variables, and verify the selected tokens cover all spacing and sizing values
called out in the review.

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 `@dashboard/README.md`:
- Around line 73-86: Make the file path reference in the widget instructions
consistent by updating the `hintFor()` example to include the same `server/src/`
prefix used for the source-module path, without changing the surrounding
guidance.

In `@dashboard/server/src/http.ts`:
- Around line 56-69: Update the memoize() cache-refresh logic so a loader result
replaces the cached value and expiresAt only when the existing cached result is
available; retain the last successful result when a refresh returns Unavailable,
while preserving normal caching behavior for successful loads.

In `@dashboard/server/src/sources/prometheus.ts`:
- Around line 28-36: Update the Prometheus query helper query to catch getJson
failures and return null, preserving its existing null behavior for unsuccessful
responses or missing/invalid samples so one failed parallel query does not
reject load's Promise.all.

In `@dashboard/server/src/sources/seerr.ts`:
- Around line 163-169: Update getRequests in
dashboard/server/src/sources/seerr.ts (lines 163-169), the corresponding
Tautulli flow in dashboard/server/src/sources/tautulli.ts (lines 149-158), and
the Sonarr flow in dashboard/server/src/sources/upcoming.ts (lines 41-47) to add
and use a hintFor(reason) helper, mirroring transmission.ts. Pass the generated
hint to safely(load, hint) or post-process the unavailable result so
authentication rejection and connection failures receive distinct, actionable
advice.

---

Nitpick comments:
In `@dashboard/server/src/discovery.ts`:
- Around line 127-227: Add unit coverage for discoverArr, discoverTautulli,
discoverSeerr, and discoverOne using a temporary DISCOVER_ROOT fixture
directory. Test missing and incomplete configuration waiting states, Tautulli’s
blocked state when the API is disabled, valid live discovery for each source,
Seerr parse-retry behavior, and discoverOne’s environment-variable override
taking precedence over discovered credentials.

In `@dashboard/web/src/components/Gauges.tsx`:
- Around line 20-57: Update Gauges to accept an optional error prop and render
PanelEmpty on the first placeholder when data is null and error is present,
using “Dashboard API unreachable” as the reason and error as the hint. In
dashboard/web/src/app/App.tsx lines 78-140, forward vpn.error to Sidebar and
metrics.error to Gauges. In dashboard/web/src/app/Sidebar.tsx lines 160-214,
update VpnCard to display vpn.reason and vpn.hint for unavailable upstream
responses, while handling transport-level vpn.error separately from an upstream
decline.

In `@dashboard/web/src/styles/autoplexx.css`:
- Around line 110-125: Update the `.ap-badge` styles to replace hard-coded
offsets, dimensions, padding, radius, line-height, and font-size with the
nearest available `var(--*)` design tokens from `nocturne.css`. Preserve the
existing layout, badge appearance, and color variables, and verify the selected
tokens cover all spacing and sizing values called out in the review.
🪄 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: 104678cb-6163-453e-b0b4-d9f7177e6e04

📥 Commits

Reviewing files that changed from the base of the PR and between cd0694b and 87c9a6f.

📒 Files selected for processing (29)
  • 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/alerts.ts
  • dashboard/web/src/app/App.tsx
  • dashboard/web/src/app/Header.tsx
  • dashboard/web/src/app/Sidebar.tsx
  • dashboard/web/src/components/CommandSearch.tsx
  • dashboard/web/src/components/Gauges.tsx
  • dashboard/web/src/components/Notifications.tsx
  • dashboard/web/src/components/Panel.tsx
  • dashboard/web/src/hooks/useDismissable.ts
  • dashboard/web/src/styles/autoplexx.css
  • dashboard/web/src/types.ts
  • dashboard/web/src/views/CommandCenter.tsx
  • dashboard/web/src/views/Setup.tsx
🛑 Comments failed to post (4)
dashboard/README.md (1)

73-86: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Inconsistent file path reference.

Line 75 uses the full path server/src/sources/, but line 83 shortens the same reference to sources/transmission.ts, dropping the server/src/ prefix within the same section.

-fixes the problem. See `hintFor()` in `sources/transmission.ts` — an auth
+fixes the problem. See `hintFor()` in `server/src/sources/transmission.ts` — an auth
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

## Adding a widget

1. Add a source module under `server/src/sources/`. It must export a `memoize`d
   function returning `Result<T>` — use `safely()` so a failure becomes
   `{ available: false, reason, hint }` rather than a rejection.
2. Register a route in `server/src/index.ts`.
3. Add the payload type to `web/src/types.ts` and render it with `<PanelBody>`,
   which handles the loading, unavailable and empty cases for you.

The `hint` is the part that matters: it should name the one concrete step that
fixes the problem. See `hintFor()` in `server/src/sources/transmission.ts` — an auth
failure and an unreachable host need different advice, and a generic hint sends
people looking in the wrong place.
🧰 Tools
🪛 LanguageTool

[style] ~82-~82: Consider using a different verb for a more formal wording.
Context: ... should name the one concrete step that fixes the problem. See hintFor() in `source...

(FIX_RESOLVE)

🤖 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/README.md` around lines 73 - 86, Make the file path reference in
the widget instructions consistent by updating the `hintFor()` example to
include the same `server/src/` prefix used for the source-module path, without
changing the surrounding guidance.
dashboard/server/src/http.ts (1)

56-69: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect memoize() to see if it retains the previous value on a failed/Unavailable refresh.
fd -a cache.ts dashboard/server/src
cat -n dashboard/server/src/cache.ts 2>/dev/null
ast-grep run --pattern 'function memoize($$$) { $$$ }' --lang typescript dashboard/server/src/cache.ts 2>/dev/null

Repository: joshdev8/AutoPlexx

Length of output: 1250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Result type definitions:"
rg -n "type Result|interface Result|export .*Result|Unavailable|unavailable" dashboard/server/src -g '*.ts' -g '*.tsx'

echo
echo "Memoized source usages:"
rg -n "memoize\\(.*=\\>.*safely|memoize\\(.*load|safely\\(load" dashboard/server/src -g '*.ts'

echo
echo "cache.ts imports/usages in server source:"
rg -n "from ['\"].*cache|memoize\\(" dashboard/server/src -g '*.ts' -g '*.tsx'

Repository: joshdev8/AutoPlexx

Length of output: 4016


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "cache.ts (for reference):"
cat -n dashboard/server/src/cache.ts 2>/dev/null || true

echo
echo "http.ts unavailable/result helpers:"
sed -n '1,90p' dashboard/server/src/http.ts | cat -n

echo
echo "activity.ts loader route around memoized function:"
sed -n '50,95p' dashboard/server/src/sources/activity.ts | cat -n

echo
echo "prometheus.ts loader route around memoized function:"
sed -n '90,125p' dashboard/server/src/sources/prometheus.ts | cat -n

echo
echo "seerr.ts loader route around memoized function:"
sed -n '150,175p' dashboard/server/src/sources/seerr.ts | cat -n

echo
echo "tautulli.ts loader route around memoized function:"
sed -n '140,165p' dashboard/server/src/sources/tautulli.ts | cat -n

echo
echo "transmission.ts loader route around memoized function:"
sed -n '170,190p' dashboard/server/src/sources/transmission.ts | cat -n

echo
echo "upcoming.ts loader route around memoized function:"
sed -n '35,55p' dashboard/server/src/sources/upcoming.ts | cat -n

Repository: joshdev8/AutoPlexx

Length of output: 10098


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from dataclasses import dataclass
from typing import Optional

TTL_MS = 10_000

`@dataclass`
class Unavailable:
    available: bool
    reason: str
    hint: Optional[str] = None

class State:
    def __init__(self):
        self.value: Optional[object] = None
        self.expires_at = 0
        self.in_flight: Optional[object] = None

def memoize(fn: object, ttl_ms: int, state: State):
    async def wrapper():
        if state.value is not None and state.expires_at > (clock := state.time()):
            return state.value
        if state.in_flight is not None:
            return state.values[state.in_flight]()
        result_future = state.time_future(fn())
        state.in_flight = result_future
        result = await result_future
        state.value = result
        state.expires_at = state.time() + ttl_ms
        state.in_flight = None
        return result
    return wrapper

class CacheClock(State):
    def __init__(self):
        super().__init__()
        self.now = 0
        self.values = {}
    def time(self):
        return self.now
    def advance(self, ms):
        self.now += ms
    def time_future(self, future):
        idx = len(self.values)
        self.values[idx] = future
        return idx
    async def await_value(self, idx):
        return self.values[idx]

async def main():
    clock = CacheClock()
    get = lambda payload: {**payload, "available": True}
    memo = memoize(get({"items": ["last good item"]}), TTL_MS, clock)

    # Successful cache populate.
    v = await memo()
    assert v == get({"items": ["last good item"]}), "initial failed"

    clock.advance(TTL_MS // 2)
    # Miss after expiry -> fetch Unavailable.
    clock.time_future(Unavailable(False, "fetch failed"))
    v = await memo()
    assert v == Unavailable(False, "fetch failed"), "failed fetch failed"

    # Request during stale failure TTL.
    clock.advance(TTL_MS // 2)
    v = await memo()
    assert v.available is False, "failed TTL should not return last good"
    assert v == Unavailable(False, "fetch failed"), "stale failed failed"

    # Successful fetch after TTL.
    clock.time_future(get({"items": ["new item"]}))
    v = await memo()
    assert v == get({"items": ["new item"]}), "new fetch failed"
    clock.time_future(Unavailable(False, "failed refresh"))
    v = await memo()
    assert v.available is True, "failed refresh should not overwrite"
    assert v == get({"items": ["new item"]}), "overwrite logic failed"

PY

Repository: joshdev8/AutoPlexx

Length of output: 156


Preserve last-good results on stale TTL hits.

memoize() caches the loader’s successful result, but if the next loader resolves to Unavailable, this value + expiresAt get overwritten. The widget cache should only refresh when the last cached result is available, so a failed poll does not blank the widget for the remainder of the TTL.

🤖 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/http.ts` around lines 56 - 69, Update the memoize()
cache-refresh logic so a loader result replaces the cached value and expiresAt
only when the existing cached result is available; retain the last successful
result when a refresh returns Unavailable, while preserving normal caching
behavior for successful loads.

Source: Coding guidelines

dashboard/server/src/sources/prometheus.ts (1)

28-36: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

One failing PromQL query blanks the whole gauges panel.

query() doesn't catch getJson failures, so if any single one of the 8 parallel queries throws (bad PromQL, transient HTTP error), Promise.all in load() rejects and safely() marks the entire metrics widget unavailable — even though the other 7 metrics may be healthy. query() already tolerates "soft" failures (non-success status, missing sample) by returning null; extending that to thrown errors keeps a single flaky metric from taking down gauges that would otherwise render.

🛡️ Proposed fix
 async function query(expr: string): Promise<number | null> {
   const url = `${config.upstream.prometheus}/api/v1/query?query=${encodeURIComponent(expr)}`;
-  const body = await getJson<PromResponse>(url);
+  const body = await getJson<PromResponse>(url).catch(() => null);
+  if (!body) return null;
   if (body.status !== 'success') return null;
   const raw = body.data?.result?.[0]?.value?.[1];
   if (raw === undefined) return null;
   const parsed = Number(raw);
   return Number.isFinite(parsed) ? parsed : null;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

async function query(expr: string): Promise<number | null> {
  const url = `${config.upstream.prometheus}/api/v1/query?query=${encodeURIComponent(expr)}`;
  const body = await getJson<PromResponse>(url).catch(() => null);
  if (!body) return null;
  if (body.status !== 'success') return null;
  const raw = body.data?.result?.[0]?.value?.[1];
  if (raw === undefined) return null;
  const parsed = Number(raw);
  return Number.isFinite(parsed) ? parsed : null;
}
🤖 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/sources/prometheus.ts` around lines 28 - 36, Update the
Prometheus query helper query to catch getJson failures and return null,
preserving its existing null behavior for unsuccessful responses or
missing/invalid samples so one failed parallel query does not reject load's
Promise.all.
dashboard/server/src/sources/seerr.ts (1)

163-169: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing differentiated hints on post-credential-live failures. All three sources call safely(load) with no hint argument once their credential is already live, so a rejected API key and an unreachable host both surface the same bare reason — unlike transmission.ts's hintFor(), which is the pattern the coding guidelines call for ("rejected credentials and unreachable hosts must receive different advice rather than a generic hint").

  • dashboard/server/src/sources/seerr.ts#L163-L169: add a hintFor(reason) helper (mirroring transmission.ts) and pass its result into safely(load, hint) or post-process the Unavailable result the way transmission.ts's getDownloads does.
  • dashboard/server/src/sources/tautulli.ts#L149-L158: same — differentiate an "authentication rejected" reason from a "connection refused"/"host not found"/"upstream timed out" reason with distinct hints.
  • dashboard/server/src/sources/upcoming.ts#L41-L47: same — Sonarr auth failures and unreachable-host failures should get distinct hints instead of none.

As per coding guidelines, "Hints generated by hintFor() must identify the actual fix; rejected credentials and unreachable hosts must receive different advice rather than a generic hint."

📍 Affects 3 files
  • dashboard/server/src/sources/seerr.ts#L163-L169 (this comment)
  • dashboard/server/src/sources/tautulli.ts#L149-L158
  • dashboard/server/src/sources/upcoming.ts#L41-L47
🤖 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/sources/seerr.ts` around lines 163 - 169, Update
getRequests in dashboard/server/src/sources/seerr.ts (lines 163-169), the
corresponding Tautulli flow in dashboard/server/src/sources/tautulli.ts (lines
149-158), and the Sonarr flow in dashboard/server/src/sources/upcoming.ts (lines
41-47) to add and use a hintFor(reason) helper, mirroring transmission.ts. Pass
the generated hint to safely(load, hint) or post-process the unavailable result
so authentication rejection and connection failures receive distinct, actionable
advice.

Source: Coding guidelines

Two findings from the review that fall inside this PR's diff:

The alert badge failed contrast in the light theme. It is the one place
amber sits behind text rather than serving as a dot, border or bar, and
light's --ap-amber gave only 3.4:1 against --color-bg — under the 4.5:1
AA asks for at 10px. Dark theme was already fine at 9.9:1. Rather than
move --ap-amber and disturb every tag and gauge that reads it, the badge
now takes its own --ap-badge token, which light overrides to
oklch(52% 0.15 78) for 5.1:1.

The badge's geometry was also four independent magic numbers that had to
agree. nocturne's spacing scale is a layout scale — 2.8 / 5.6 / 8.4 /
11.2px — and no step reaches the 16px an icon-corner badge needs, so
tokenising the sizes outright would have changed the design. Instead the
size is stated once and the offset, radius and line-height derive from
it. Computed values are unchanged: 16x16, radius 8px, offset -4px.

Also makes the README's hintFor() path carry the same server/src/ prefix
the surrounding instructions use.

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

Copy link
Copy Markdown
Owner Author

Note on the previous review's scope: it ran while this PR's merge-base was stale, so it saw the whole 29-file stack rather than this PR's 9 files. Most of its findings were against #50 code that has since merged to main.

Addressed here (both inside this diff):

  • autoplexx.css.ap-badge contrast and hard-coded geometry. Confirmed the contrast concern was real: light theme measured 3.40:1 against --color-bg, under the 4.5:1 AA needs at 10px (dark was fine at 9.95:1). Rather than move --ap-amber and disturb every tag and gauge reading it, the badge takes its own --ap-badge token, overridden to oklch(52% 0.15 78) in light for 5.14:1. Verified in-browser: computed geometry is unchanged at 16x16 / radius 8px / offset -4px.
  • dashboard/README.mdhintFor() path now carries the same server/src/ prefix as the surrounding text.

Not addressed here — all against files this PR doesn't touch, and all now merged on main. Worth a follow-up PR rather than widening this one:

  • http.tsmemoize() should retain the last good result when a refresh returns Unavailable
  • prometheus.tsquery() should catch getJson failures so one failed parallel query can't reject load's Promise.all
  • seerr.ts / tautulli.ts / upcoming.ts — add hintFor() helpers mirroring transmission.ts
  • discovery.ts — unit coverage for the per-source discovery functions
  • Gauges.tsx / Sidebar.tsx — propagate polling errors and upstream reasons like PanelBody does

On the spacing-token suggestion specifically: nocturne's scale is a layout scale (2.8 / 5.6 / 8.4 / 11.2px) and no step reaches the 16px an icon-corner badge needs, so mapping the sizes onto it as proposed would have shrunk the badge. The size is stated once and the offset/radius/line-height derive from it instead, which addresses the keep-in-sync concern without changing the design.

@joshdev8

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@joshdev8
joshdev8 merged commit 4659e92 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