fix(templates): respect reverse-proxy path prefix in hardcoded JS/URLs - #80
Open
jeromelefeuvre wants to merge 319 commits into
Open
fix(templates): respect reverse-proxy path prefix in hardcoded JS/URLs#80jeromelefeuvre wants to merge 319 commits into
jeromelefeuvre wants to merge 319 commits into
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses final code-review notes: rename the in-process cache wrapper
key 'status' -> 'result' (it holds the whole {status, latencyMs} struct,
not just the word), and cover the CACHE_TTL cache-hit path with a test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lth chips Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stream summary strip, Recently Added section, and live-card polish (HDR/SDR badge + codec-transition transcode detail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7 tasks: stream-summary strip (DRY extraction), Recently Added (client+normalizer+endpoint+section), and live-card polish (HDR/SDR badge + codec-transition transcode detail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the dashboard widget's stream-summary block into a shared partial and render it above Now Playing on the Tautulli page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read-only get_recently_added wrapper with a strict allow-list normalizer (count clamped 1..50; no ids/guids/paths/timestamps). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the sanitization test to the normalizer docblock, which lists added_at among the intentionally-omitted fields. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fail-open GET /tautulli/api/recently-added rendering clickable rows that open the existing info modal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New stacked section hydrated once on load, with en/fr strings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… skeleton The injected fragment already wraps rows in .plex-sessions; keep the class on the inner div only, matching the History section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Allow-list stream/source video+audio codecs and dynamic range for the card HDR badge and codec-transition transcode detail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the absent-stream fallback case for dynamicRange and align the sanitization assertion with testStripsPrivateFields. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stream cards (page + dashboard widget) now show the dynamic range and, when transcoding, the source→target codec instead of just the decision word. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary strip, Recently Added section, and HDR/SDR + codec-transition card polish. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…test Remove the now-unused _icons import from _plex_activity (its only use moved to the shared _plex_summary partial), and add the spec-required controller smoke test for the recently-added fragment endpoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove Recently Added, dense History grid, slim Libraries, drop Total series; add Media/Stream plays toggle, hour+day-of-week activity, and platform x stream-type problem-clients graphs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6 tasks: remove Recently Added; client chart feeds + drop Total; chart endpoints; dense History grid + slim Libraries; Media/Stream toggle + activity & problem-client graphs; docs + CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The *arr apps already surface recently added; drop the section, its endpoint, client method + normalizer, tests, and i18n keys. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…otal normalizePlaysByDate now drops any aggregate "Total" series. New read-only chart commands share a playsChart() helper and the existing normalizer; getPlaysByDate refactored onto it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aysByDate fail-open Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
apiPlays gains a media|stream mode; three new fail-open JSON endpoints feed the hour-of-day, day-of-week, and platform x stream-type charts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
History renders as a responsive multi-column grid (poster + title + user/when, no progress bar); Libraries becomes a slim single-column list. Both are now full-width sections. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… graphs Generalize the chart JS into drawStackedBar; add a Media Type/Stream Type toggle on the plays chart and new hour-of-day, day-of-week, and platform x stream-type charts, all driven by the range toggle. Fix the history grid item-count regex. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the un-shipped Recently Added note; record the declutter (dense History grid, slim Libraries) and the new graphs (Media/Stream toggle, hour-of-day, day-of-week, platform x stream-type). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These are brainstorming specs and implementation plans — internal scaffolding, same category as the already-ignored CLAUDE.md / PROGRESSION.md. Removed from the public repo (kept locally) and added to .gitignore so they no longer clutter the fork or a future PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Was '63 files / ~8,900 insertions' (stale, and inflated by the now-untracked docs/superpowers specs). Actual code footprint vs upstream/main is 56 files / ~4,250 insertions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The device block was five nested cards, each a .datagrid that collapses to a single column inside col-xxl-3 — seven stacked label/value rows per card, so five devices took ~1100px to show 35 values, with the fifth card orphaned on its own row. These are homogeneous records with identical fields, which is a table: same data in ~200px, columns that can be compared down their length (scanning a RAM column is the actual job), one less card nesting level, and the same vocabulary as the two RF tables directly below it. Model and role move onto a muted sub-line under the device name, which buys back two columns. Numeric columns are right-aligned with tabular figures. CPU/RAM/retry colour only above a threshold, so a quiet console doesn't render as a wall of green. Drops the Satisfaction column: the console reports -1 per radio on this firmware, so it was a permanently empty column paying for itself in width on the widest table of the page. The reader still maps the field, so restoring the column is a one-line change if firmware ever fixes it. RF split goes 8/4 since the radio table carries nearly twice the columns. Neighbour table: signal no longer wraps mid-value, a hidden SSID says so instead of showing a bare dash that reads as a failed parse, and a truncated vendor keeps a title= so it stays inspectable. Also prefers a gateway's lan_ip over ip. Gateways report their WAN address there, so the inventory was showing a public address beside four 192.168.x ones — wrong for a LAN table and needless exposure in a screenshot. Devices without lan_ip are unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The chart axis labels were .62rem (~9.9px) with no small-screen bump, which breaks this project's own mobile floor of nothing under 11px below the lg breakpoint. The dashboard bumps its equivalent axis to .6875rem in a media query; the inline-style copy in the history fragment couldn't, because a style attribute can't carry one. Moves the chart rules into a page-scoped <style> in the shell's stylesheets block — the same pattern dashboard/index.html.twig already uses, so still no new CSS file — and adds the missing media query. The fragment goes from six repeated inline style attributes to none. It has to live in the shell rather than the fragment: fragments are re-injected on every poll, so a <style> tag inside one would accumulate duplicates. Also DRYs the repeated tabular-figure rule into .unifi-nums, and drops the two translation keys the table redesign orphaned (unifi.infra.model and unifi.infra.satisfaction) rather than leaving them for translators to translate into a UI that never shows them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RF split goes back to 7/5. The 8/4 I'd just introduced clipped the neighbour table's Vendor column clean off at desktop width — the reasoning was backwards, since the radio table had simultaneously LOST its satisfaction column and so needed less room, not more. Confirmed against the rendered page: at 390px the neighbour table showed all four columns, at 1440px it did not. The 7-day traffic axis read "Sat … Sat". One format feeds both the per-point hover labels and the two axis labels, and seven days apart is the same weekday, so both ends printed identically and all 24 points in a day shared one tooltip. Now 'D j H:i' — the date disambiguates the axis, the hour makes hovering a spike informative. A network with no vlan id now says "untagged" rather than showing a bare dash, which read as missing data. Same reasoning as the hidden-SSID label. Live-verified on :beta: gateway row now shows its LAN address (192.168.68.1, was the public WAN address), the Networks table is populated with real VLAN ids and subnets, the poller coalesces into one ?p=live,infra request per due tick, and polling stops dead after a Turbo navigation away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An admin-only network-operations page over the classic UniFi Network API, built on a two-method UnifiFetcher seam so the shipped dashboard widget cannot regress. Three readers own their own endpoint group and cadence (live 10s / infra 60s / history 300s), so every endpoint is hit once per cycle no matter how many panels consume it, and a page-scoped coalescing poller batches every due region into a single request per tick. Panels: WAN/gateway/client tiles, 7-day traffic and 30-day speedtest trends as server-rendered SVG, a device inventory, RF environment with AP/radio and neighbour tables, VLAN inventory, wireless clients, live talkers, top clients and DHCP-reservation mismatches. Read-only throughout; every action answers 200 with an empty state rather than letting a sick console 500 the page. Field map verified against the live console: gateway-only temperatures[], radio width from bw, radio satisfaction of -1 normalised to null, neighbours via POST stat/rogueap, and gateway lan_ip preferred over the WAN address in ip. Live-verified on :beta — dark and light, desktop and 390px, poller coalescing confirmed and polling confirmed to stop after a Turbo navigation.
…shuo#17) fix(qbittorrent): guard against stale pagination responses
This was referenced Jul 25, 2026
New download-client page alongside qBittorrent — setup wizard step, admin settings, sidebar badge/poll, health circuit breaker — modeled on the same conventions but with its own JSON-RPC transport, since Transmission's session handshake (expected HTTP 409 carrying a fresh X-Transmission-Session-Id to cache and retry with) is genuinely novel versus qBittorrent's cookie auth. Also fixes three bugs found while smoke-testing the new client: - SetupController's Downloads step defaults were non-empty, so qBittorrent showed as "configured" even when untouched. - DashboardController's services-health labels lacked a Transmission entry, so its chip never appeared. - A user-entered URL with a redundant /transmission suffix doubled up with the client's own /transmission/rpc path; normalizeBaseUrl() strips it. - torrent-get on a large library (this user has ~1,900 torrents) is multiple MB of uncompressed JSON, slow enough over a remote link to time out mid-transfer and silently render as "0 torrents". Requesting gzip via CURLOPT_ENCODING shrinks that response ~14x. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The contributed commit blanked qbittorrent_url and qbittorrent_user in the Downloads step's default map. Unrelated to adding Transmission and it changes onboarding for a live feature, so it goes the same way as the stray DashboardController hunk.
Guards HealthService::TOGGLEABLE_SERVICES, the settings.html.twig service_meta/groupings/kill-switch/TEST_FIELDS lists, and base.html.twig's sidebar/poller/turbo:before-render cleanup entries for transmission, plus a UniFi survival check since Task 1's rebase conflicted on the same lists.
Replaces the contributed qBittorrent-derived template. Deluge is the closer semantic match (no Gluetun panel, label categories, session-wide pause/resume) and the route sets are identical, so the fetch layer ports by prefix swap. Adopts _stat_tiles on top of the three partials Deluge already includes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
getTorrentDetail() read $t['rateDownload']/$t['rateUpload'] for properties.dl_speed/up_speed, but DETAIL_FIELDS never asked Transmission's torrent-get RPC for either field — since Transmission only returns requested fields, both always coalesced to 0 and the detail modal's General tab showed 0 B/s regardless of actual speed. Add a reflection-based test asserting DETAIL_FIELDS contains every field getTorrentDetail() reads off the torrent object. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ControllersSmokeTest requests /transmission but ServiceRouteGuardSubscriber redirects app_transmission_* to app_setup_downloads whenever transmission_url is unset (always true under AbstractWebTestCase) — the smoke test only asserts status < 500 || 503, so the 302 passes and the template body is never parsed. Four raw dotted translation keys nearly shipped in the rebuilt template as a result; only a hand check caught it. Seed transmission_url, swap in a mocked TransmissionClient covering every state normalizeState() can emit, render in both app locales, and assert the HTML contains no unresolved qbittorrent./transmission./deluge. translation key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…keys per file Follows the Deluge precedent: a second torrent client reuses the qBittorrent translation namespace and adds only client-specific keys.
…elp text qbittorrent.page_title is also 'Downloads', so with both clients configured two sidebar entries opened pages with an identical heading and indistinguishable browser tabs. The refresh setting's help text described the page's own polling, which is hardcoded; the setting drives the sidebar badge and completion toasts.
Absorbs jeromelefeuvre's Transmission contribution (fork PR Shoshuo#18), rebased onto post-UniFi main with the contributed client and controller carried as authored. The page template is rebuilt on the Deluge base plus the shared partials rather than the contributed qBittorrent-derived copy; qBittorrent and Deluge are untouched. Adds guard tests for the registration points and Deluge/Transmission route parity, plus a render test — nothing in CI previously rendered any torrent template, because ServiceRouteGuardSubscriber redirects to the setup wizard under AbstractWebTestCase and the smoke test only asserts status < 500. 956 tests, live-verified on :beta against Transmission 4.1.3.
`startRefresh()` cleared a closure-scoped `refreshTimer`, but Turbo Drive
re-executes the incoming page's script with a fresh closure while the
outgoing page's closure — and its live 3s setInterval — survive. All three
torrent pages render the same element IDs (qbt-list, qbt-stat-total, ...),
so an orphaned poller's updateList()/updateStats() guards still passed on
its successor's DOM and it kept writing its own client's torrents over the
page the user was actually looking at. Two pollers then fought at 3s each,
which reads as data corruption ("Transmission is showing my qBittorrent
torrents"). Pre-existing between qBittorrent and Deluge; Transmission just
added a third page to the cycle.
Hoist the handle to window._prismarrTorrentPagePollTimer — one name serves
all three, since only one torrent page is ever mounted at a time — so the
incoming page's startRefresh() reaches and kills the outgoing page's timer.
Add it to base.html.twig's turbo:before-render cleanup as well, so leaving
for a NON-torrent page (which re-executes no torrent script, and so would
otherwise never clear it) stops the interval too.
Verified in a browser against a real image, A/B against the unfixed
templates: on HEAD, sitting on /transmission after a Turbo visit from
/qbittorrent logged 10 qBittorrent + 9 Transmission polls over 32s; with
the fix, 0 qBittorrent + 9 Transmission. A torrent -> dashboard visit nulls
the handle and torrent polling stops entirely.
Known residue, deliberately not addressed here: refreshCtrl/refreshSeq are
still closure-scoped, so a fetch already in flight when navigation starts
can still apply once into the incoming page's DOM before that page's own
poller corrects it 3s later.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CHANGELOG gains the poller fix under Unreleased/Fixed. README and docs/FORK-CHANGES had never been updated for the Transmission tab (merged 2026-07-25 in 79e8ce3) — FORK-CHANGES still read "Last updated: 2026-07-11" and neither file mentioned Transmission anywhere, so the fork record and the feature list both understated what main ships. Backfilled: - README: Transmission in the Downloads feature bullet, Requirements, wizard config list and the credentials-storage note, plus a fork-section entry. Deliberately no bugfix line — a README describes capabilities, not fixes. - FORK-CHANGES: a Transmission section (RPC 409 handshake, the reuse-the-qbittorrent-namespace convention, and the CI hole where no torrent template was ever rendered) and a poller-fix section, both under "shipped in the fork, not yet proposed upstream" since each is an upstream PR candidate — the poller bug affects the upstream-origin qBittorrent template too. - Refreshed two stale claims in FORK-CHANGES: the "Last updated" date and the "~700 tests" gate figure, now 967. Docs-only; no code touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nsmission) Hoists the three torrent pages' 3s poll timer from a per-page closure onto one shared window global, so a Turbo navigation can actually clear the outgoing page's interval. Previously the orphaned poller kept writing its own client's torrents into the next page's identically-id'd DOM, two pollers fighting at 3s each. Also adds the handle to base.html.twig's turbo:before-render cleanup for torrent -> non-torrent navigations. 967/967 tests, CI + GHCR green, live-verified on :beta (Deluge 54 torrents vs Transmission 0: zero cross-writes in either direction). Docs: CHANGELOG entry, plus README/FORK-CHANGES backfilled for the Transmission tab, which had never been documented.
Three of the four merges from 2026-07-25 had never reached the docs. The previous commit caught Transmission; this catches the other two. - UniFi Network tab (/unifi, merge a7daffe) — documented in all three files. Only the 2026-07-05 dashboard *widget* was described anywhere, so the docs understated UniFi as a single dashboard section when main also ships a full admin-only ops page (SVG traffic/speedtest trends, device inventory, RF environment, VLANs, wireless clients, live talkers, top clients, DHCP mismatches; three readers on 10s/60s/300s cadences behind a coalescing poller; read-only and fail-soft per panel). - qBittorrent stale-pagination guard (Shoshuo#17, merge 48d30f3) — CHANGELOG entry and a FORK-CHANGES section, credited to @jeromelefeuvre. Kept as its own entry rather than folded into the poller fix, so the attribution is clear. Both are filed under "shipped in the fork, not yet proposed upstream": the pagination guard touches the upstream-origin qbittorrent template, and the UniFi tab is a poor upstream fit but nothing in it is fork-specific. Also moved the UniFi tab entry into FORK-CHANGES' 2026-07-25 cluster — section 3 is otherwise strictly date-ordered, and grouping it with the widget it builds on broke that. Docs-only; no code touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…guard Docs-only follow-up. Three of the four 2026-07-25 merges had never reached README / CHANGELOG / FORK-CHANGES; the poller-fix merge covered Transmission, this covers the /unifi tab (a7daffe) and the Shoshuo#17 stale-pagination guard (48d30f3, credited to @jeromelefeuvre).
jeromelefeuvre
force-pushed
the
fix/hardcoded-prefix-paths
branch
3 times, most recently
from
July 28, 2026 03:51
d71b393 to
ce1adc8
Compare
…-widget fix(media): collapse the download queue widget by default on Radarr/S…
Inline JS and href/action attributes across /medias, /decouverte,
/jellyseerr, /prowlarr and /setup templates hardcoded absolute paths
(fetch('/medias/...'), href="/jellyseerr/...", etc.), which ignore the
X-Forwarded-Prefix header set by a reverse proxy stripping a path
prefix (e.g. Traefik serving the app under /prismarr/). Symfony's
Request::getBasePath() already reflects that header, so prefix each
hardcoded literal with app.request.getBasePath() (or, inside an
already-open Twig expression, with the ~ concat operator) instead of
relying on instance_path()/path() helpers that aren't used everywhere.
Each affected template now computes the prefix once into a local
`var BASE` (matching the existing convention in qbittorrent/index and
deluge/index) instead of repeating the Twig call inline at every call
site, shrinking the diff and keeping a single point of change per
template.
The initial sweep missed the app-wide quick-look modal (_quicklook.html.twig,
shared by the dashboard, calendar, Explorer and top-bar search), the
top-bar search's own quicklook URLs in base.html.twig, the Plex info
modal (dashboard/_plex_info_modal.html.twig) and the entire Tautulli
dashboard page (tautulli/index.html.twig) — none of those were under
/medias, /decouverte, /jellyseerr, /prowlarr or /setup, so they kept
hardcoded absolute paths that broke behind a stripped path prefix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
jeromelefeuvre
force-pushed
the
fix/hardcoded-prefix-paths
branch
from
July 30, 2026 03:51
ce1adc8 to
894e5ce
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When Prismarr is served behind a reverse proxy that strips a path prefix (e.g. Traefik routing
https://host/prismarr/...to the container root, withX-Forwarded-Prefix: /prismarrforwarded), server-generated URLs are already prefix-safe viainstance_path()/path()(which go through the Symfony router, itself driven byRequest::getBasePath()— already trusted viatrusted_headers: [..., x-forwarded-prefix]inconfig/packages/framework.yaml).However, ~276 places across 67 Twig templates build absolute paths as raw string literals inside inline
<script>blocks orhref/action attributes (fetch('/medias/' + slug + ...),href="/jellyseerr/...", etc.). These literals ignore the prefix entirely, sinceX-Forwarded-Prefixonly affects server-side URL generation, not raw JS strings.Fix
<script>block or HTML attribute): prefix the literal with{{ app.request.getBasePath() }}.{% include ... with { save_url_edit: ... } %}or a ternary): use the Twig concat operatorapp.request.getBasePath() ~ '...'instead, since{{ }}doesn't interpolate inside an already-parsed Twig expression./api/health/services) was switched to the named route via{{ path('api_health_services') }}instead, which is cleaner and needed no raw-path handling.Request::getBasePath()already reflectsX-Forwarded-Prefix(Symfony 5.3+), so this resolves to/prismarrbehind the proxy and''on a root deployment — no extra config needed.Affected roots:
/medias,/decouverte,/jellyseerr,/prowlarr,/setup.Test plan
make lint-twig(php bin/console lint:twig templates) — all 149 Twig files pass syntax validation~instead of{{ }}