Skip to content

feat(lumen): caching, admin request tools, saved settings and branding uploads - #596

Merged
snapetech merged 7 commits into
snapetech-org:mainfrom
ncbudd:consolidate/next
Nov 7, 2026
Merged

snapetech merged 7 commits into
snapetech-org:mainfrom
ncbudd:consolidate/next

Conversation

@ncbudd

@ncbudd ncbudd commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Lumen: caching, admin request tools, saved settings and branding

Seven commits on top of main (f7bc03b), consolidated from tester feedback collected in ncbudd/lumen (mirror PR ncbudd/lumen#2).

1. Keep recent answers instead of asking SeerrNG every time

  • Anything loaded in the last 3 minutes is reused without a new call. Anything up to a day old shows at once while it refreshes in the background.
  • Saved per account in localStorage, so a reopened tab or app starts from what it last showed. Signing out or switching accounts deletes it.
  • Any request or other change marks everything for refresh, so nothing keeps showing a title as not requested. Requests, quota, sign-in and public settings are always checked again.
  • Books, Audiobooks, Movies and Series show placeholder posters while the first page loads, instead of an empty page.
  • The paged lists keep fix(lumen): large screens, list retry, clearer address names and a bug pass #595's stale-response guards.

Discover's second visit makes 1 call instead of 23.

2. Requests from everyone, Request as, library badge, saved settings

  • Everyone tab on Requests for admins and request managers (Admin or Manage Requests). It shows waiting requests first, then the newest, each with who asked, plus Approve and Decline. The tab shows how many are waiting. Nobody else gets the tab or the calls.
  • Request as in the request sheet and Advanced options. Shown only to people SeerrNG lets choose the requester: Admin, or Manage Users together with Manage Requests. It sends userId.
  • Library badge on title pages: the poster gets the same corner glyph as posters elsewhere. A check means it's in the library; a clock means it's requested.
  • Save "Lumen settings for everyone" from the admin page. A small nginx script (njs, already in the nginx image) checks with SeerrNG's /auth/me that the person is an admin. It then writes lumen-config.json into a mounted /lumen-data folder.
    • It only accepts JSON objects up to 64 KB, sent as JSON, by PUT.
    • The saved file wins over a mounted lumen-config.json.
    • Without the folder, the page keeps the download button and explains how to add the folder.
    • The Dockerfile loads the njs module. /lumen-data is deliberately not created in the image, so settings are never written into a container that an update would replace.

3. Show downloads that have started, and call 0% ones Queued

  • With a big download queue, the request actually downloading could sit far past the newest requests Lumen lists, so only queued titles at 0% showed. Both Requests lists now also pull in every request whose download has started, using filter=processing (up to two pages, kept only when progress is above 0).
  • Radarr and Sonarr report everything in the client's queue as downloading, so anything at 0% now says Queued, on request rows and on posters.

4. Change, Retry and Remove on the Everyone tab

  • Change works on waiting requests, and on failed movie or series requests. It opens a sheet with server, quality, folder, who the request is for and, for a series, its seasons, starting from what the request has now (PUT /request/:id). A waiting request is then approved or just saved; a failed one is saved and retried.
  • Retry works on failed requests (POST /request/:id/retry).
  • Remove works on any request (DELETE /request/:id), confirmed by a second click on the same button rather than a browser pop-up.
  • The Back-closes-the-sheet handling moves into useBackCloses, so both sheets share it.

5. Upload the logo and tab icon

  • With /lumen-data mounted, the admin settings page has Upload buttons (PUT /lumen/branding/logo|icon, admins only).
  • The image is checked by its first bytes: PNG, JPEG, WebP, SVG or ICO, up to 2 MB.
  • It's kept in /lumen-data/branding under a new name each time, so no browser shows an old copy, and earlier uploads of the same kind are deleted.
  • Its address is saved for everyone straight away. Other unsaved edits on the page stay unsaved.
  • /branding/ serves uploads first, then a mounted branding folder. It has a CSP that lets nothing there run scripts, including an SVG opened on its own.

6. Logo shape and a custom media server name

  • branding.logoShape: rounded (default), circle, or raw. Raw is the file as it is: transparent, uncropped, and as wide as it is, up to 160px. It's picked on the admin page with a live preview.
  • Sign-in logo fix: the logo CSS now beats the play mark's own rules. Those gave a custom logo the accent square on the sign-in card.
  • mediaServerName replaces Plex / Jellyfin / Emby in every label, filter and the sign-in page (up to 40 characters). Leaving it empty keeps the real name.

7. Everyone shows every request: filters and Show more

  • With hundreds of requests, the Everyone tab showed only waiting ones, started downloads and the 20 newest.
  • It now has SeerrNG's own filters with their counts from /request/count: All, Waiting, Downloading, Failed and Available.
  • A Show more button loads 20 more at a time, in SeerrNG pages of up to 100.
  • All still puts waiting requests and started downloads first, and Downloading puts started ones first.

Compose files and docs/using-seerr/lumen.md cover /lumen-data (mkdir lumen-data && sudo chown 101:101 lumen-data), uploads, logoShape and mediaServerName.

Testing

  • tsc --noEmit, build, and unit tests: 22 passed, including the real-image proxy tests. These caught two njs syntax limits (spread and for...of) before anything shipped.
  • Playwright on desktop, iPhone, Android and tablet: 327 passed, 13 skipped (device-only cases). New tests cover:
    • The cache: reuse, asking again after a request, and dropping it on sign-out.
    • Everyone with Approve and Decline, and who can see it.
    • Request as, and who gets it.
    • The title-page badge.
    • Save for everyone: saving, and the read-only folder note.
    • Started downloads, and Queued.
    • Change, Retry and Remove.
    • Uploads, logo shapes on the sign-in card, the shape picker, and the media server name.
    • Everyone paging through 45 requests, and its filters.
  • The real Lumen image against a lab SeerrNG 3.72.16 and Jellyfin, with real admin and family accounts:
    • Saving:
      • No folder, a read-only folder and a writable one each report correctly.
      • It's refused when signed out (401), for a family member (403), for a form post (403), for bad JSON or an array (400), and over 64 KB (413).
      • An admin save writes the file and serves it from then on.
    • Uploads:
      • Refused when signed out (401), for a family member (403), for a text file posing as a PNG (415), at 3 MB (413), and for an unknown kind (404).
      • A second logo upload replaced and deleted the first.
      • Uploads are served as images with the no-scripts CSP, and a mounted branding folder still works.
    • Everyone tab: it loaded in 2.3 s cold. Approve moved a family request out of waiting, and Change then Approve kept it in the family member's name. Changing a series' seasons saved, and Remove deleted the request.
    • Layout: no horizontal overflow on desktop or phone.
  • Running on a production install (SeerrNG behind a Cloudflare tunnel, stock nginx with the template mounted).

Lumen asked SeerrNG again for everything on every visit: going back to
Discover repeated all its calls, including one detail lookup per title in
"New on <server>". Besides being slow (SeerrNG's first lookup of a title
can take seconds), every call counts against SeerrNG's per-account rate
limit, so quick browsing could hit 429s.

- apiCache.ts: answers younger than 3 minutes are reused without asking;
  older ones (up to a day) show at once while a new answer loads. Saved
  per account in localStorage so a reopened app starts from what it last
  showed; signing out or switching account drops it.
- Any request or other change (POST/PUT/DELETE, another narrator) marks
  everything stale, so nothing keeps showing a title as not requested.
  Requests, quota, sign-in and public settings are always re-checked.
- useApi, the Discover rows, "New on <server>" (the finished row is saved
  too) and list pages use it. A failed refresh keeps the saved answer up.
- Books, Audiobooks, Movies and Series show placeholder posters while the
  first page loads, so a slow answer doesn't look like an empty page.

Checked in the SeerrNG Android app (emulator, Android 16): Movies and
Back, and a full close and reopen, ask SeerrNG for none of Discover's rows
again; Back still behaves.
…ttings

Tester feedback:

- Requests gets an Everyone tab for admins and request managers: every
  waiting request first, then the newest, each with who asked, and
  Approve / Decline on waiting ones. The tab shows how many are waiting.
- The request sheet and the Advanced options get "Request as" for people
  SeerrNG lets choose the requester (Admin, or Manage Users together with
  Manage Requests).
- Title pages show the posters' corner glyph on their poster: the check
  when it's in the library, the clock when requested.
- "Lumen settings for everyone" can be saved from the admin page. A small
  nginx script (njs, already in the image) checks with SeerrNG that the
  person is an admin, then writes lumen-config.json into a mounted
  /lumen-data folder. Without that folder the page keeps the download.
With a big download queue, the request that was actually downloading
could be far down the history, past the newest requests Lumen lists, so
the Requests page showed only queued titles at 0%. Both Requests lists
now also pull in every request whose download has started (from
SeerrNG's in-progress requests); the Everyone tab puts them right after
the ones waiting for approval, furthest along first.

Radarr and Sonarr report everything in the download client's queue as
"downloading". Requests and posters now say Queued while none of it is
here yet.
Admins and request managers get SeerrNG's request actions in Lumen:

- Change (waiting requests, and failed movie or series requests): a sheet
  with server, quality, folder, who the request is for and, for a series,
  its seasons, starting from what the request has now. A waiting request
  is then approved or just saved; a failed one is saved and tried again.
- Retry on failed requests.
- Remove on any request, confirmed by a second click on the same button
  (no browser pop-up).

The Back-closes-the-sheet handling moves into useBackCloses so both
sheets share it.
With the /lumen-data folder mounted, "Lumen settings for everyone" gets an
Upload button next to the logo and the tab icon. The image is checked
(PNG, JPEG, WebP, SVG or ICO by its first bytes, up to 2 MB, SeerrNG
admins only), kept in /lumen-data/branding under a new name each time so
no browser shows an old copy, and its address is saved for everyone
straight away. Other unsaved changes on the page stay unsaved. Older
uploads of the same kind are deleted.

/branding/ now serves uploads first, then a branding folder mounted into
the app, with a CSP that lets nothing there run scripts.

The save script also avoids syntax njs doesn't support (spread, for...of);
the image's proxy tests caught it.
Tester feedback (Tatsu, ncbudd/lumen#4):

- branding.logoShape frames the custom logo: "rounded" (default),
  "circle", or "raw" (the file as it is: transparent, uncropped, as wide
  as it is up to 160px). The admin settings page picks it, with a live
  preview. The logo CSS now beats the play mark's own rules, which gave a
  custom logo the accent square on the sign-in card.
- mediaServerName replaces Plex / Jellyfin / Emby in every label, filter
  and the sign-in page (up to 40 characters). Set on the admin page;
  empty keeps the real name.
The Everyone tab listed waiting requests, started downloads and only the
20 newest requests, so a big library (hundreds of requests) showed a
small slice. It now has SeerrNG's own filters with their counts (All,
Waiting, Downloading, Failed, Available) and a Show more button that
loads 20 more at a time (in SeerrNG pages of up to 100). All still puts
waiting requests and started downloads first; Downloading puts started
ones first.
@snapetech
snapetech merged commit a03a2a0 into snapetech-org:main Nov 7, 2026
@snapetech

Copy link
Copy Markdown
Collaborator

Merged with contributor ancestry preserved in a03a2a04a, pushed to main and the GitLab mirror. This is not yet a new stable release.

Reviewed the nginx/njs writes and authentication, SVG serving protections, client account/cache boundaries, request permissions and controls. I patched three verified defects before merging:

  • A second upload deleted the live logo before its replacement config was saved. Failed saves/concurrent edits could break branding. Old images are retained and filenames use nginx request IDs to prevent collisions.
  • Cache expiry was checked only on initial storage load, so open tabs could retain entries beyond a day; future-dated entries could also remain fresh. Expiration now applies on reads.
  • Everyone represented an API failure as an empty history. It now gives an explicit error and Retry.

Evidence: 327 browser cases passed, with 13 viewport-specific skips; 8 matching baseline visual cases; final retry/sign-out checks passed across desktop, phone and tablet. The final native suite passes 27 cases, including an actual built nginx container that checks anonymous/non-admin denial, invalid/oversized config, persistent config readback, upload validation, SVG CSP/sandbox/nosniff and preservation of the current logo. Failed baselines are retained. Tests use isolated fixtures, not household requests.

Detailed review and limits. Root release qualification is still pending.

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.

2 participants