Skip to content

Poster artwork and service logos - #52

Merged
joshdev8 merged 4 commits into
mainfrom
feat/dashboard-posters
Jul 29, 2026
Merged

Poster artwork and service logos#52
joshdev8 merged 4 commits into
mainfrom
feat/dashboard-posters

Conversation

@joshdev8

@joshdev8 joshdev8 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Every image in the dashboard was a placeholder: media panels drew striped rectangles, and the sidebar and Launcher drew two-letter monograms. This replaces both, using only sources the stack already has.

Poster artwork

Each panel's art comes from whichever service already holds it. The route is /api/poster?src=…&ref=…, and sources/posters.ts is the only place that turns a reference into bytes.

Panel Source Needs internet
Now Playing Plex, via Tautulli's pms_image_proxy no
Upcoming Sonarr's cached mediacover no
Downloads Sonarr/Radarr mediacover, via the queue match no
Requests TMDb yes

The *arrs matter more than they look: their images arrays only carry remoteUrl pointing at thetvdb, but each keeps a local copy under mediacover — which is what keeps Upcoming and most downloads offline-safe.

TMDb is the single outbound dependency, confined to Requests, where there is no alternative: Seerr keeps no local copy, its /imageproxy/ is absent on some builds, and a pending request is in no *arr yet. It fails to the placeholder, so an install with no route out sees exactly what it saw before.

Why a proxy at all

The browser can't fetch any of these itself — three sit behind API keys that must not leave the server, and the fourth is off-box. Every ref is produced server-side, travels to the client, and comes back, so each source re-validates its own shape on the way in. Refs are deliberately not interchangeable: a Plex path offered as a TMDb one is refused, since it would otherwise reach a different host entirely.

Every failure is a 404, never a 5xx — the tile falls back to the monogram it already shows before an image loads.

Service logos

Vendored, for the same reason Inter and Phosphor are: an icon set that only renders with internet access is worse than none.

Runtime favicons were tried first and rejected on evidence. Of the 18 services with a web UI, only 6 return a usable image:

Result Services
real icon plex, seerr, tautulli, maintainerr, requestrr, tracearr
redirect to a login HTML page radarr, prowlarr, grafana
401, even with its API key sonarr
404 checkrr, flaresolverr, prometheus, portainer, node-exporter, cadvisor
unreachable bazarr, transmission

Radarr and Prowlarr hand back 10 KB of login HTML, which renders as a broken tile. That would have looked worse than the monograms it replaced.

So 20 SVGs from homarr-labs/dashboard-icons (Apache 2.0), with provenance and the trademark position in NOTICE.md beside the assets. They're globbed by filename, so adding one is just dropping <service id>.svg in the folder. Six services have no upstream icon and keep their monograms — the fallback already existed.

Queue pagination fix

Download artwork rides on the existing torrent→*arr queue match, which turned out to request pageSize: 100. Real queues here run to 314 and 638 records, so anything past the first page silently fell back to OTHER — indistinguishable from a torrent no *arr tracks. queue() now pages to the end, bounded so a queue misreporting its total can't spin it.

Verification

Against a live stack:

  • All four sources serve real bytes through the route — Sonarr 188 KB, Radarr 210 KB, Plex 181 KB, TMDb 54 KB
  • Nine malformed refs all 404: cross-source substitution both ways, traversal, absolute and protocol-relative URLs, command injection, unknown source, missing params
  • In-browser, every poster and every logo loaded with naturalWidth > 0; the six uncovered services render monograms
  • An episode resolves to its show poster, not the episode still

47/47 tests, typecheck, lint and build clean.

Known, not addressed

Transmission's torrent-get across ~9,100 torrents measured 34s against a 6s upstream timeout, so /api/downloads intermittently reports unavailable. Environmental rather than caused here, and it degrades correctly — but note the hint it produces (check LOCAL_NETWORK) names the wrong fix for a slow-but-reachable daemon. Worth a follow-up.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added poster artwork across the dashboard (Now Playing, Downloads, Upcoming, Requests) with a shared renderer and graceful placeholders.
    • Introduced a server-side poster proxy endpoint (GET /api/poster) with browser caching.
  • Bug Fixes
    • Improved poster selection rules (episodes vs movies) and robust handling for missing/invalid/unavailable artwork.
    • Enhanced artwork attribution for transmission items and improved Sonarr/Radarr queue pagination.
  • UI Improvements
    • Service icons now prefer matching logos by service identifier, falling back to monograms when unavailable.
  • Tests
    • Expanded coverage for poster validation, source attribution, and episode/movie poster behavior.

Now Playing rendered a monogram placeholder because no endpoint ever
returned an image. Posters now come from Plex through Tautulli's
pms_image_proxy, so artwork needs no outbound internet — matching the
self-hosted assumption that keeps Inter and Phosphor vendored.

The browser cannot call Tautulli directly: it may have no route to it,
and the API key must not leave the server. A new /api/poster route
proxies the bytes instead. The `img` value round-trips through the
client, so it is re-validated against Plex's metadata path shape on the
way back in rather than trusted because we emitted it — that check is
the whole trust boundary, and it keeps the route from being usable as a
general-purpose proxy.

Every failure is a 404, not a 5xx: an item with no poster, a booting
Tautulli, or an undiscovered key all fall back to the monogram tile the
design already specifies, which is also what renders before the image
loads. Episodes use the show's poster rather than the episode still.

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

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@joshdev8, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f2126ad-0535-42c0-bbde-df16acd049cd

📥 Commits

Reviewing files that changed from the base of the PR and between adee8b4 and de9b9e2.

📒 Files selected for processing (3)
  • dashboard/server/src/sources/seerr.ts
  • dashboard/server/src/sources/sources.test.ts
  • dashboard/web/src/components/ServiceIcon.tsx
📝 Walkthrough

Walkthrough

The dashboard adds validated poster URL generation and proxying for Plex, Sonarr, Radarr, and TMDb, propagates nullable poster fields through server and web models, renders poster fallbacks, and loads vendored service SVG logos with monogram fallback behavior.

Changes

Poster artwork pipeline

Layer / File(s) Summary
Poster URL and proxy infrastructure
dashboard/server/src/config.ts, dashboard/server/src/sources/posters.ts, dashboard/server/src/index.ts
Validated source references produce internal poster URLs; upstream images are checked and served through cached GET /api/poster.
Source poster propagation
dashboard/server/src/sources/arr.ts, dashboard/server/src/sources/tautulli.ts, dashboard/server/src/sources/seerr.ts, dashboard/server/src/sources/transmission.ts
Streams, episodes, requests, and downloads now carry nullable poster URLs, with Arr pagination and torrent attribution updates.
Contracts and validation
dashboard/web/src/types.ts, dashboard/server/src/sources/sources.test.ts
Web models include poster fields, with tests covering validation, source selection, attribution, and unavailable artwork.
Command Center rendering
dashboard/web/src/components/Poster.tsx, dashboard/web/src/views/CommandCenter.tsx
A shared lazy-loading component displays poster images and switches to monogram placeholders after failures.

Service logo rendering

Layer / File(s) Summary
Service logo lookup and fallback
dashboard/web/src/components/ServiceIcon.tsx, dashboard/web/src/assets/services/NOTICE.md
Service identifiers map to vendored SVG assets; unmatched services retain tinted monogram tiles, with asset attribution documented.
Service logo call-site wiring
dashboard/web/src/app/Sidebar.tsx, dashboard/web/src/components/CommandSearch.tsx, dashboard/web/src/views/Launcher.tsx
Service identifiers are passed to ServiceIcon from sidebar, search, and launcher views.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SourceLoader
  participant CommandCenter
  participant Poster
  participant PosterAPI
  participant MediaSource
  SourceLoader->>CommandCenter: provide nullable poster URL
  CommandCenter->>Poster: render poster
  Poster->>PosterAPI: request validated poster URL
  PosterAPI->>MediaSource: fetch image bytes
  MediaSource-->>PosterAPI: image response or failure
  PosterAPI-->>Poster: cached image or 404
  Poster-->>CommandCenter: image or monogram fallback
Loading

Possibly related PRs

  • joshdev8/AutoPlexx#49: Scaffolds the dashboard UI and shared ServiceIcon call sites extended by this change.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title captures the two main themes of the PR: poster artwork and added service logos.
✨ 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-posters

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.

Now Playing was the only panel with real art. The rest kept their
placeholders, which reads as broken rather than intentional once one
panel has covers.

Each panel's art comes from whichever service already holds it, so the
route generalises from `?img=` to `?src=…&ref=…`, and posters.ts becomes
the single place that knows how to turn a reference into bytes. Doing
this now rather than as a follow-up keeps the URL shape from changing
under anyone twice.

  plex    Tautulli's pms_image_proxy  (Now Playing)
  sonarr  the *arr's own cached cover (Upcoming, downloads it grabbed)
  radarr  likewise                    (downloads it grabbed)
  tmdb    image.tmdb.org              (Requests)

The *arrs matter more than they look: their `images` arrays only carry
`remoteUrl` pointing at thetvdb, but each keeps a local copy under
`mediacover`, so Upcoming and most downloads stay offline-safe.

TMDb is the one outbound dependency and is confined to Requests, where
there is no alternative — Seerr holds no local copy and its /imageproxy/
is absent on some builds, and a pending request is in no *arr yet. It
fails to the placeholder, so an install with no route out sees exactly
what it saw before.

Every `ref` still round-trips through the browser, so each source
re-validates its own shape on the way in. Refs are not interchangeable:
a Plex path offered as a TMDb one is refused, since it would otherwise
reach a different host entirely.

Download art rides on the existing queue match, which turned out to be
capped at one page. Both queues here run past 300 records, so anything
beyond the first 100 silently fell back to OTHER — indistinguishable
from a torrent no *arr tracks. queue() now pages to the end, bounded so
a queue that misreports its total can't spin it.

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.

🧹 Nitpick comments (1)
dashboard/server/src/sources/seerr.ts (1)

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

Consider covering describe()'s poster branch in tests.

Every other poster source in this PR (posters.isValidRef/url, tautulli.toStream, transmission.attribute/indexQueues) has dedicated test coverage for its poster derivation, but describe() — which is the only place tmdbPoster is exercised against real Seerr detail data — doesn't appear to have an equivalent test for the happy path, missing tmdbId, or detail-fetch failure.

🤖 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 85 - 121, Add focused
tests for the Seerr describe function covering successful detail data with a
poster, requests without tmdbId returning UNKNOWN, and detail-fetch failures
returning UNKNOWN. Mock the Seerr detail request and verify title/poster
derivation, including tmdbPoster behavior, without changing describe’s existing
error handling.
🤖 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.

Nitpick comments:
In `@dashboard/server/src/sources/seerr.ts`:
- Around line 85-121: Add focused tests for the Seerr describe function covering
successful detail data with a poster, requests without tmdbId returning UNKNOWN,
and detail-fetch failures returning UNKNOWN. Mock the Seerr detail request and
verify title/poster derivation, including tmdbPoster behavior, without changing
describe’s existing error handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c7730eb-3719-4861-bbe0-791b0dee10ae

📥 Commits

Reviewing files that changed from the base of the PR and between 6f48006 and 0550376.

📒 Files selected for processing (11)
  • dashboard/server/src/config.ts
  • dashboard/server/src/index.ts
  • dashboard/server/src/sources/arr.ts
  • dashboard/server/src/sources/posters.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/web/src/components/Poster.tsx
  • dashboard/web/src/types.ts
  • dashboard/web/src/views/CommandCenter.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • dashboard/web/src/components/Poster.tsx

The sidebar and Launcher stood in two-letter monograms for every
service. Now that panels carry real poster art, uniform monograms read
as unfinished rather than as a deliberate style.

Logos are vendored rather than fetched, for the same reason Inter and
Phosphor are: an icon set that only renders with internet access is
worse than none on a stack that may have no route out.

Pulling each service's own favicon at runtime was tried first and
rejected on evidence — of the 18 services with a web UI, only 6 return
a usable image. Radarr, Prowlarr and Grafana redirect to a login page
and would render 10KB of HTML as a broken tile; Sonarr's is behind form
auth and 401s even with its API key; six 404; two were unreachable. The
result would have looked worse than the monograms it replaced.

So: 20 SVGs from homarr-labs/dashboard-icons (Apache 2.0), with
provenance and the trademark position recorded in a NOTICE beside them.
They are globbed by filename rather than listed, so adding a logo is
just dropping `<service id>.svg` in the folder. The set has nothing for
checkrr, watchlistarr, decluttarr, cleanarr, cadvisor or node-exporter,
and those keep their monograms — the fallback already existed, so the
gap needs no handling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@joshdev8 joshdev8 changed the title Show real poster artwork for active streams Poster artwork and service logos Jul 29, 2026

@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/web/src/components/ServiceIcon.tsx`:
- Around line 39-54: Update the logo branch in ServiceIcon so the monogram
remains mounted behind the image while it loads or if loading fails. Track image
load state, reveal the image only after its load event, and hide it on error
while preserving the existing sizing and styling.
🪄 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: 302c4ec2-c53b-4e3b-9a97-808d2c46537f

📥 Commits

Reviewing files that changed from the base of the PR and between 0550376 and adee8b4.

⛔ Files ignored due to path filters (20)
  • dashboard/web/src/assets/services/bazarr.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/docker-socket-proxy.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/flaresolverr.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/grafana.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/maintainerr.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/plex.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/portainer.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/prometheus.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/prowlarr.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/radarr.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/redis.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/requestrr.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/seerr.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/sonarr.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/tautulli.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/telegraf.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/timescale.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/tracearr.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/transmission.svg is excluded by !**/*.svg
  • dashboard/web/src/assets/services/watchtower.svg is excluded by !**/*.svg
📒 Files selected for processing (5)
  • dashboard/web/src/app/Sidebar.tsx
  • dashboard/web/src/assets/services/NOTICE.md
  • dashboard/web/src/components/CommandSearch.tsx
  • dashboard/web/src/components/ServiceIcon.tsx
  • dashboard/web/src/views/Launcher.tsx

Comment thread dashboard/web/src/components/ServiceIcon.tsx Outdated
ServiceIcon rendered only <img> for a matched logo, with no fallback for
the fetch itself failing or being slow — unlike Poster, which already
handles this for media artwork. The monogram now stays mounted
underneath until the logo's onLoad fires, and takes over again on
onError, rather than a blank tile appearing in the meantime.

Also adds test coverage for seerr.ts's describe(), the one poster
source that had none: the happy path pairing a title with its TMDb
poster, a request with no tmdbId yet (must not even attempt the
lookup), and a failed detail fetch — all mirroring the failure-mode
coverage the other three poster sources already have.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@joshdev8
joshdev8 merged commit ea4a961 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