Skip to content

feat(requests): route requests by rules, request seasons, and rework request pages and admin - #1632

Merged
Quick104 merged 158 commits into
mainfrom
feat/request-revamp
Sep 29, 2026
Merged

Quick104 merged 158 commits into
mainfrom
feat/request-revamp

Conversation

@Quick104

@Quick104 Quick104 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Closes #1554
Related issue: #1201, #582, #587, #1643
Validation tasks: changes #1196 C1–C2, #1197 C1–C2 (group request policy: request approval and limits move to the account and access-group pages, #1563, and the invitation case checks the new group policy; re-validate after merge); reaches #1202, #1203, #1204, Silo-Server/silo-apple#328, Silo-Server/silo-apple#329, Silo-Server/silo-android#338, Silo-Server/silo-android#339, #1234, #1235, Silo-Server/silo-apple#336 and Silo-Server/silo-android#346 (none run yet)

Requests could only go to one Sonarr or Radarr per media type, chosen by switches on each connection. There was no way to send anime, kids' titles or 4K copies to different servers, no way to request some seasons of a series, and the request pages and admin queue did not match the rest of the app. The request lifecycle could also double-submit or stall when two actors (two admins, or an admin and the reconcile pass) acted on the same request.

This merges the request revamp branch. Its parts were reviewed and merged into feat/request-revamp as separate pull requests (#1559–#1565, #1609, #1619, #1620, #1622, #1623, #1649), and review findings on this pull request were fixed in #1644, #1653 and on the branch. The branch is up to date with main.

Approach

API changes are additive and v2-only; /api/v1 is unchanged. docs/architecture/media-requests.md and docs/architecture/api-contract.md describe the new behavior.

Validation

  • Each part had its own CI run and automated review; findings were fixed or answered in those pull requests.
  • After merging main: Go build and vet pass; go test passes for internal/requests, internal/apiv2, internal/api, internal/contractledger, internal/plugins and internal/metadata, including DB-backed tests. Four DB-backed tests outside requests fail against the shared local test database because of known fixture drift; they fail the same way on main.
  • The API contract gates pass (OpenAPI, fixtures, route inventory, web types, migration ledger, local paths), and make lint-changed is clean.
  • Web: typecheck passes and the full web suite passes (687 files).
  • The maintainer is testing the branch on a development deployment against real Sonarr and Radarr servers, including 4K servers.

Risks

Checklist

  • I read and can explain the complete diff.
  • This pull request addresses one concern. It merges a feature branch whose parts were reviewed separately.

AI Disclosure

  • Harness: Claude Code (in T3 Code)
  • Tool(s): Claude Code
  • Model(s): claude-opus-5-5 (Claude Opus 5.5, 1M context)
  • Involvement: Fully AI-generated, human verified. The maintainer set the scope and is testing the branch on a development deployment.
  • Adversarial review: each part received automated review in its own pull request; findings were fixed or answered there.

🤖 Generated with Claude Code

Note

Add rule-based request routing, season requests, and rework request UI and admin

  • Adds ordered conditional routing rules (per media type, with HD/UHD destinations, fallback routes, and Standard vs Advanced routing mode) backed by the request_routes tables, with admin CRUD, reordering, and preview endpoints plus a full settings UI in RequestsSettings.tsx and RequestRoutingList.tsx.
  • Adds per-season series requests: users can request missing seasons of a library series, and fulfillment tracks per-season library progress via new season-availability queries in seasons.go and season_availability.go.
  • Reworks the request pages: a new /requests hub with Discover and Yours tabs, /title/:mediaType/:tmdbId detail pages with request/cancel/follow actions, request follows with follower-specific notifications, and download-progress reporting and polling.
  • Reworks the admin request queue: four views (needs approval, in progress, failed, done) with counts, bulk approve, request history dialogs, and per-target download progress; retired admin tabs redirect to the new request settings and users pages.
  • Request submission is now fenced with leases, backoff, and retry, and reconciliation runs under PostgreSQL advisory locks; effective request policy resolves from account, group, and server layers.
  • Behavioral Change: v1 API create requests are forced to whole-series mode, admin moderation responses hide server details from non-admin viewers, and legacy /admin/requests?tab= URLs redirect to new locations; several SQL migrations add new tables and columns (request_routes, request_follows, request_group_limits, download-progress and submission-state columns).

Macroscope summarized ac6f140.

Quick104 and others added 30 commits September 27, 2026 00:59
Request status and outcome writes now name the states they may start from,
so two admins approving at once, or an approval racing a decline, apply
exactly one transition. Sending a request to the router plugin requires an
atomic claim on the row, so admin approval, auto-approval and the reconcile
pass on any server cannot submit it twice.

A failed submission keeps the approval: it records the error and retries with
backoff (5 minutes doubling to an hour), then marks the request failed after
ten attempts. Approve, Retry and auto-approved creates return the saved
request instead of a 500 after the approval has committed.

Retry reopens a failed request in one write, answers 409 when another account
has since requested the title, and drops failed targets for qualities the
request no longer wants (#582). Re-requesting a failed title deletes only the
requester's own failed rows, inside the create transaction and before the
quota check; it used to delete every account's failed rows for the title.

The reconcile pass takes a cluster advisory lock, rotates through candidates
by last_reconciled_at, and completes from the library only when no submission
claim is running. The admin queue offers Decline only for pending requests,
matching the server.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Requests no longer need Sonarr, Radarr or another router plugin. When no
router connection serves the media type, an approved request stays approved
and the reconcile pass completes it once the title is in the library, then
notifies the requester. Auto-approval no longer waits for a configured
connection, so an auto-approved user's request skips pending on a server
without one.

A second reconcile rotation checks pending requests, and requests that
failed in the last 30 days without delivering anything, against the library:
a pending request whose title appears needs no approval any more, and a
failed one whose title appears is complete. It uses one batched presence
lookup per media type and runs apart from the requests that need router
calls, so a backlog cannot slow router polling. The in-flight rotation now
batches its presence lookups too.

An approved request nothing has been sent for (no target, no submission in
flight) can be declined by an admin or cancelled by its owner, so a request
waiting for the library can still be closed. A router connection that
exists but cannot be used (no API key, not bound to a plugin installation)
now records the reason and retries with backoff instead of failing the
request, so fixing the connection lets it through.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Requests hub and its browse pages now take the shell's single gutter
like Recommendations and Calendar, and a title's request page collapses the
sidebar like a library item page does.

One RequestStatusBadge, built on the shared Badge variants, replaces the four
hard-coded amber colour sets on the hub, the Yours tab, poster cards and the
title page. Request pages use one vocabulary for request state: Pending,
Approved, Processing, Available, Declined, Cancelled, Failed.

Users can cancel their own requests from the Yours tab and the title page
until something has been sent for them, behind a confirmation. Approved and
declined notifications link to the title. In the search dialog, request rows
join arrow-key navigation, close the dialog when opened, and show the
request's status instead of a dead "Request" pill.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A profile that finds a title someone else already requested can now ask to
be notified when it becomes available, instead of being turned away with
"already requested". The follow is stored per title and profile, so it
survives the request failing and being requested again, and it is cleared
once the fulfilled notification has gone out. The requesting profile is
always notified and never needs one.

When a request's title reaches the library, the fulfilled notification now
goes to the requester and every follower. A follower's copy is marked so
every channel words it as a title they followed, not their own request. Each
profile's copy uses the existing per-profile unique index, so a retry is
told once, and the server-channel announcement now waits until an attempt
has reached every recipient, so a retry does not repeat it. Declining or
cancelling the request clears its title's follows.

API (v2 only; v1 is frozen):
- PUT and DELETE /api/v2/requests/follows/{media_type}/{tmdb_id}, both
  naturally idempotent. Following needs the same access as requesting and
  is refused for a title with no active request or one already in the
  library.
- Request state gains following and requested_by_viewer.
- GET /api/v2/requests/status advertises follow_supported.

The title page shows "Notify me when available" on another profile's open
request, and "Stop notifying me" once followed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Requests now carry a state field that clients can show as is: pending,
approved, processing, available, declined, cancelled or failed. The server
derives it from status, outcome and library presence, so a request whose
download finished but whose title the scan has not found yet reads as
processing rather than available. Request state on discovery, search and
detail carries it too. status and outcome stay for admin detail and older
clients.

Why a request was declined or cancelled is now stored on the request
(outcome_reason) and returned, instead of living only in the event log and
the decline notification. A migration fills it in for existing requests from
their events. The web request pages use the server's state when present and
show the reason on declined requests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Creating a request now reads the title's TMDB detail once, after the cheap
refusals (already in the library, already requested), instead of an
uncached detail read before them just to detect anime. The server's copy of
the title and year replaces the one the client sent.

The request stores a snapshot of what routing rules will match on:
TMDB genre, keyword, network and company IDs, original language, origin
countries, year and the anime flag. IDs rather than names, because names
follow the configured TMDB language. The TMDB client now carries those IDs
and origin countries, which it already received and dropped. When TMDB
cannot answer, the request is still created from the client's copy and the
facts stay uncaptured until routing needs them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Silo now decides which server each quality tier of a request goes to,
instead of leaving it to the Sonarr/Radarr plugin's one-default-per-tier
switches. That lets an admin send anime, a genre, a decade, a language, a
country, a network or studio, or one account's requests to their own
Sonarr or Radarr, with their own root folder, quality profile and tags.

Routes belong to a media type and hold conditions and a destination per
tier: a server plus overrides for its plugin config. Per tier, the first
enabled route whose conditions match and that has a destination for the
tier wins; each media type's fallback route comes last. A route can skip 4K
for the titles it matches. Conditions match the routing facts captured at
creation (fetched at routing time for older requests, retrying while TMDB
is unreachable).

A routed submission calls the plugin once per tier with only the chosen
server, marked as the tier's default and carrying the route's overrides, so
the existing plugin follows the route unchanged. Each target records the
route that sent it (v2 route_name). Status checks now go through the plugin
installation that owns each target's server rather than whichever
installation happens to be first.

The migration turns each media type's current default and default-4K
servers into its fallback route, and a default server's anime settings into
an Anime route, so routing is unchanged after upgrade. Media types with no
routes, such as Seerr-only setups, keep the plugin's routing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds the v2 administration surface for request routing, which until now
could only be seeded by the migration:

- GET /admin/request-routes lists every route in evaluation order, always
  including each media type's fallback ("Everything else"), which reads as
  revision zero until it is first saved.
- GET, PUT and DELETE /admin/request-routes/{id} read, replace and delete a
  route; replacement and deletion require If-Match on the route's revision.
  The fallback's first PUT creates it; it cannot be deleted.
- POST /admin/request-routes creates a rule after the media type's others,
  and POST /admin/request-routes/order sets their order.
- POST /admin/request-routes/preview shows which server each quality tier
  of a title would go to, from TMDB's current facts, and why a tier goes
  nowhere.

Validation keeps rules meaningful: a rule needs a condition and an effect,
a destination must be a server of the media type's kind, routing-owned
config keys cannot be overridden, and no rule can be added before the media
type's fallback has an HD server, since the first rule moves the media type
to Silo's routing and unmatched titles would otherwise have nowhere to go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Request configuration moves out of the Requests queue page into Settings ›
Requests, built on the settings shell: one save bar with an unsaved-changes
guard, If-Match on every save, and a conflict prompt that keeps the admin's
edits when someone else saved first.

- General: allow requests, approval (automatic or by an admin), the request
  limit and its window, and whether to also request a 4K copy.
- Servers: one tile per Sonarr or Radarr server showing where routing uses
  it. The editor puts the type, name, URL and API key first, then the
  plugin's settings without the switches routing now owns. Test reports the
  quality profiles and root folders it found. A server routing uses cannot be
  deleted or switched to the other type, and the editor says why.
- Routing, per media type: the default destination (HD and 4K server with
  per-tier overrides from the plugin's own options), ordered rules with an
  editor for their conditions (anime, genres, keywords, networks or studios,
  languages, countries, years) and destinations, and "Test a title", which
  shows the facts and the server each tier would go to. With requests off,
  a title can be tested by its TMDB ID.

The Requests page keeps the queue and user overrides and links to the new
page; its old ?tab=settings and ?tab=integrations URLs redirect there. ⌘K and
the settings overview find the page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A series request now names the seasons it wants. When the requester names
none, the server asks for every aired season that is not complete in the
library.

A season is complete when every aired episode has a file in an enabled
library, judged by the library's own episode metadata, so no external
service is involved. Episodes a multi-episode file spans count, and only
aired episodes count toward aired ones. A season request completes, and
notifies, when all its seasons are complete rather than when the first
episode is scanned in, and reads as partially_available while only some
are. Once the download server reports the request done, a season with any
episode present also counts, so an episode the server cannot find does not
hold the request open. Requests from before this change, and every v1
request, mean the whole series and keep the old any-episode rule.

A series partly in the library can be requested for its missing seasons
only when no download server takes series: the router plugin does not
receive seasons yet (that needs an SDK and plugin release) and would add
the whole series again. With a download server, such a series stays
already_available, as before.

The fulfilled-notification pass now stamps each request it checks without
notifying, so requests still waiting on the library rotate behind newer
completions instead of starving them. Request lists read every series'
season counts in one query.

API (v2): createRequest accepts seasons; series detail lists each regular
season with its availability (missing, partial, available) and whether the
active request covers it; requests carry seasons, season_progress and the
partially_available state; GET /requests/status advertises
season_requests_supported and missing_seasons_requestable. The TMDB client now carries a series' seasons.
v1 is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A title the server does not have used its own request page, which reused
only the hero banner and built everything else itself. It now renders
through the same detail layout, sections and components as a library item:
the metadata badges, a score row with the TMDB score, the crew line, the
action bar, the cast section and a "More Like This" row.

The action bar's primary pill is "Request movie"/"Request series", or the
request's state once there is one ("Requested", "Approved",
"Processing"), with "Cancel request" and "Notify me when available" as
secondary actions and IMDb/TMDB as links. The action bar gained optional
primary, secondary and link slots; library items render exactly as before.

Titles now live at /title/:mediaType/:tmdbId, with the same shell as
/item. /requests/:mediaType/:tmdbId redirects there, and an unknown media
type shows the unavailable page instead of silently loading a movie. A
title already in the library redirects to its /item page when the viewer
can open it, and shows a disabled "In the library" when they cannot.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Requesting a series outside the library now opens a season picker. Aired
seasons the library lacks start picked, as the server would choose on its
own; seasons already in the library or already requested show but cannot be
picked, and upcoming ones can be added. The title page also lists the
series' seasons with what the library has and what is requested.

A series already in the library offers "Request Seasons" in its More menu
when the server allows missing seasons to be requested (the
missing_seasons_requestable capability, true while no download server takes
series). The dialog loads the series' request detail only when opened, so
the series page makes no extra request on load.

My requests and the admin queue show which seasons a request asks for; a
request with only some seasons in the library reads "Partially available"
with how many are in. My requests now groups a request under "Landed in
your library" by its state, so a download the library has not scanned yet
stays in motion.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Season requests and the shared title page add request code that search and
title pages load at launch. The budget goes up until the request
administration hooks leave the launch bundle later in this branch, which
brings it back under the original budget.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The admin queue groups requests by what an admin does next: Needs
approval, In progress, Failed and Done, each tab with its count. It opens on
what needs approval, searches titles or a TMDB ID, filters by media type or
by one account (?user=), and loads more as the admin scrolls.

- Each row shows the poster, requested seasons, requester, state and, once
  sent, each server the request went to with the rule that chose it.
- Actions follow the server's rules per view: approve or decline (with a
  reason) what needs approval, cancel what nothing was sent for yet, retry or
  close what failed. Needs approval also approves or declines a selection,
  four requests at a time, and lists any that failed.
- A side sheet shows the request's details, where routing would send it now,
  and its history.
- The admin sidebar shows how many requests need approval.

Server (additive v2): the admin list takes view, q, media_type and
requested_by_user_id; GET /admin/requests/counts counts each view; GET
/admin/requests/{id}/events returns a request's history. The list reads its
targets in one query. The v2 admin cancel can now close a failed request
(v1 is unchanged), a closed request stays closed when a target reports late,
and a request's history records each change of its status or outcome once
rather than once per target.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Titles known only from TMDB (Requests rows and the Yours tab, brand pages,
"More Like This" on a title page, "Request to add" in search) used their own
card. They now use the library's card frame: the same artwork box and
missing-poster fallback, radius, hover lift, focus handling and caption, and
they follow the viewer's poster-size and caption settings. Request takes the
place of Play in the centre of the artwork; the request state badge and the
Library chip sit where the library puts its badges. Under a caption a TMDB
title always names its type and year, since a movie and a series can share a
title, and a poster that fails to load shows the title instead of an empty
box.

The artwork box, caption classes and centre-action class move into
MediaCardArtwork, shared by ItemCard, SectionItemCard and the request card;
library cards render the same markup as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Who may request, whether an admin approves, and how many titles an account
may request now live where accounts and access groups are managed, and the
admin Requests page is the queue alone.

- Accounts: the user page gets a Requests section with the account's own
  approval and limit, a line saying what applies now and where it comes
  from, and a link to the account's requests in the queue.
- Access groups: a group sets approval and a limit beside its existing
  requests switch (new v2 GET/PUT /admin/request-groups/{group_id}/limit,
  guarded by If-Match). An account's own setting wins, then its group's, then
  the server-wide default; admins never take a group's.
- One way to block: the requests switch on the account or its group (or
  requests off server-wide). The old "blocked" limit and approval modes are
  gone from the editors; a migration moves accounts blocked that way onto
  their switch. The API still honors "blocked" when written. Because the
  switch now feeds the request policy too, v1 and v2 detail and search report
  "blocked" for such an account, as creating a request already did.
- Quota: declined and failed requests give their slot back; cancelled ones
  still count, so requesting and withdrawing cannot repeat without limit.
- The User Overrides tab is removed; its old links open the Users page.

A detail page, and the discover rows, resolve the viewer's policy once per
call rather than once per page of results.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Requests page drops its marketing hero, its own search form and the
status guide, and takes the header, rows and grids of the app's other browse
pages.

- Discover: carousel rows of the discover sections, each with Explore all
  (a new paged grid at /requests/discover/:section); studio, network and
  genre tiles are links.
- Search: the header's search field opens the app's search page, where
  "Request to add" now reads like the People section, pages through TMDB
  results and follows the search scope. Old /requests?q= links redirect
  there.
- Yours: a list instead of a poster grid, grouped by what happens next
  (needs attention, on the way, in your library, cancelled). Each row shows
  the state, requested seasons and progress, dates, and the decline reason
  or error, with Cancel and Open in library. A popover explains the states.
- Request notifications refresh the list without a reload.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…access

The request queue and request access hooks sit in the module search and
title pages load at launch. The budget goes up until those hooks move to
their own admin module later in this branch, which brings it back under
the original budget.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ach decision

Server support for a simpler routing admin.

- Conditions: every list condition gets an exclude form ("original language
  is not English"), and a US content-rating ceiling ("rated PG or lower")
  matches titles rated at most that; a title with no US rating does not
  match. The rating is captured with the other routing facts from the detail
  TMDB already returns, and fetched at submission for older requests only when
  a route checks ratings.
- The preview explains a decision route by route: which conditions each
  failed and what it did for the HD and 4K copies. Try-a-title gets an admin
  TMDB search (GET /admin/request-routes/titles) that works while requests
  are off, and the preview matches rules for a chosen requester.
- Everything else with no 4K server makes no 4K copy, even when every
  request asks for 4K, instead of failing the 4K copy: "no 4K copy" now means
  the same on a rule and on the fallback.
- The first Radarr (Sonarr) server added becomes Everything else for movies
  (series), and a migration does the same for installs with exactly one usable
  server of a kind, so a single-server setup needs no routing. Deleting the
  last server of a kind takes that Everything else with it when no rule routes
  the media type.
- Validation messages use the words the admin sees ("Everything else",
  "4K copies").

All v2 changes are additive.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Settings › Requests replaces its two "Movie routing" / "Series routing"
blocks with one "Where requests go" section and a Movies | Series switch.

- Each switch shows one ordered list: rules as sentences ("When a series is
  anime → Sonarr · /tv/anime · Anime 1080p"), numbered, dragged or moved to
  reorder, turned on and off in place, and Everything else pinned last.
  Changes save as the admin goes; the save bar keeps only the general
  settings.
- "Add a rule" starts from Anime, Foreign language or Kids & family, or a
  custom rule. A preset asks only where the titles should go; the Anime
  preset also sets Sonarr's series type to Anime.
- The rule editor shows only the conditions in use, each "is any of" or "is
  none of", added from a menu. Where they go puts folder, quality and tags
  up front and the rest under More settings, for HD copies and 4K copies
  ("Same as Everything else", another server, or no 4K copy).
- Try a title works while requests are off, can act as a given account, and
  explains the decision rule by rule; the queue's side sheet shows the same
  result.
- Rows warn about rules that can never match, anime below a language rule,
  a server that is off or of the wrong kind, a Sonarr anime rule without the
  Anime series type, and force-dual with no 4K server, with a fix where one
  click will do.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The request detail (servers, where it would go, history, actions) now
opens as a centered card with its actions pinned below a scrolling body,
so it no longer covers the queue from the side. The row's details button
drops the side-panel icon.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Most installs have one Radarr and one Sonarr, and maybe a 4K copy of each,
yet the routing rules were the first thing they saw. Standard, now the
default for such installs, sends each media type to its one server and 4K
copies to its one server marked 4K, with each server's own settings; the
rules stay stored but paused. Advanced is the rule-based routing.

- request_routing holds the mode, guarded by its revision:
  GET/PUT /api/v2/admin/request-routing, which also says where Standard
  sends each media type or why it cannot be used.
- Standard needs at most one enabled normal and one 4K server per media
  type. Adding or enabling a second turns Advanced on in the same
  transaction and gives Everything else the servers Standard was using, so
  requests keep going where they went. Every server write and mode switch
  takes one advisory lock first.
- A migration puts installs whose routing Standard would change on
  Advanced, everyone else on Standard.
- Settings > Requests: a Standard/Advanced choice, one line per media type
  under Standard, a "4K server" switch on Radarr/Sonarr servers, and a
  toast when a server save turns Advanced on.
- Requests go without routing facts when TMDB is down and no rule has a
  condition, since the facts could not change where they go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TMDB's anime keyword misses about one anime series in eight and one film
in three, and routing only knew a title's US rating, so titles never rated
in the US matched no rating rule.

- Anime is Japanese animation: TMDB's anime keyword, Animation in Japanese
  or from Japan, or a title an AniDB-based list names (Kometa's Anime-IDs,
  matched by TVDB or IMDb ID). A listed series also needs a Japanese signal,
  since the list names some Western series and the flag can set Sonarr's
  series type. Chinese and Korean animation counts only when TMDB tags it.
- A "Refresh Anime List" task downloads the list daily into anime_ids, one
  server at a time under a lease, with an ETag, a size cap, a same-host
  redirect guard and a truncation check; a failed download keeps the stored
  copy. Requests only read the table.
- A title with no US rating (or only "NR") is routed on its own country's
  rating, stored as "JP:PG12". The TMDB client exposes every country's
  certifications; the US-only parental-control read is unchanged.
- On a sample of 118 anime series and 60 films from the AniDB mappings,
  detection goes from 87%/62% to 100%/95%, with no Western, Chinese or
  Japanese live-action title flagged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A settings row's label column stops growing at its maximum width, and
nothing pushed the control column to the right after it. On a wide window
a row with a short label and a narrow control (a switch, a link button)
left its control wherever the label column ended, so switches sat short
of the selects and inputs above them and link buttons landed at different
places. The control column now always sits on the row's right edge, as the
row's own contract says, on every settings page.

On Settings > Requests, a routing rule's warning fix now lines up with the
rule switches, and Try a title's search fills the row with the requester
select, now the same height, on the right edge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…card

The Related rows' outline buttons had a border and fill close to the card's
own colour. They now use the dark field and border the settings inputs and
selects use.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Everything else and rule editors borrowed the settings page's
side-by-side rows inside a narrow dialog: a floating "Send to" beside the
server select, an indented block of overrides, tag chips ragged against
the right edge, and a stray field count on More settings.

Each copy (HD, 4K) is now its own panel: a heading, the server, then Folder
on a full row, the other settings in two columns, and tags as left-aligned
chips that show a check when picked and say whose tags apply when none
are. More settings says how many are changed instead of how many exist.
Unset settings read "Server default (...)". The rule's Name field stacks
the same way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The request detail page rendered seasons in a native overflow row with a
browser scrollbar and no drag scrolling. Extract the season carousel's
Embla rail into SeasonRail and render request seasons inside it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Standard routing sent every series with the Sonarr server's own series
type, so anime got standard episode numbering unless an admin switched to
Advanced and wrote a rule. Anime series now go to the same servers with
Sonarr's anime series type, as Seerr sends them; everything else keeps the
server's settings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Search and title pages load the request hooks at launch, and that module
had grown to hold every admin hook too: the queue, request settings,
servers, routing and limits, with the admin request API behind them. Those
move to hooks/queries/admin/requests, which only the admin pages import,
taking about 3 KB brotli off the launch bundle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…anges

A request notification refreshed the request list, title pages, and
Discover, but not request search results, which also show each title's
request state and availability. A reconnect also delivers request changes
made while the socket was down as unread rows in the notifications
snapshot rather than as notification.created events, so those changes
never refreshed anything. Refresh the same request queries, now including
search, from both paths.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread internal/requests/repository.go
Comment thread internal/requests/service.go
Comment thread internal/taskmanager/tasks/refresh_request_downloads_test.go
Comment thread internal/taskmanager/tasks/refresh_request_downloads_test.go
Comment thread web/src/hooks/queries/admin/requests.ts
…erver types under lock (#1653)

* fix(requests): tell only a request's own followers when it arrives

A series can have a completed request still waiting for the library beside a
newer open request for other seasons. Follows belong to the title, so the
completed request's notification went to, and cleared, follows made for the
open request, and declining the open request deleted follows still waiting
for the completed one.

The notification now goes to the follows made before its request completed,
and a decline keeps the follows a completed, not yet notified request of the
title is waiting to tell.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(requests): recheck a server's type against its routes under the row lock

A route save and a server save each validated the other before their
transactions and rechecked only the 4K switch under the server's row lock.
Two admins saving at once could leave a movie route pointing at a server that
had just become a Sonarr, or one that no longer takes movies, and every
request it routed would fail when sent.

Both saves now recheck the server's type and media types with the row locked,
and a server save does so under Standard too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(requests): bound a request's followers by the title's previous completion

Two completed requests for one series can both wait for the library, and the
newer can arrive first. Bounding followers only by the request's own
completion time told the newer request's notification about the older
request's followers too, and cleared them.

A title has one open request at a time, so a request's followers are the
follows made after the title's previous request completed and no later than
it did. Clearing them is bounded the same way, so a profile that unfollowed
and followed again during the dispatch keeps its new follow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(requests): record the open request on each follow

Bounding a request's followers by timestamps broke when a follow committed
while a completion was under way: the completion's timestamp comes from the
start of its transaction, so it could be earlier than the follow's, and the
follow was left out of the request it had read.

A follow now records the request that was open when it was made, and a
request's notification, its follow cleanup and a decline all go by that
request. A new request takes over the follows of a failed request, or of one
its requester replaced, so a follow still survives its request failing.
Existing follows go to the title's open request, or else to its latest
completed request that has not been notified.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(requests): key follows by request so a profile can follow each one

A profile following an older completed request of a series could not follow
a newer request for other seasons: the key was per title, the insert kept the
old row, and the title showed as followed. The backfill also gave every
existing follow to the open request, even ones made for an older request.

Follows are now keyed by account, profile and request, and the "following"
state reads the follow on the title's open request. A new request takes over
the follows of the title's failed requests before any it replaces are
deleted. The backfill gives each follow the title's latest request created
before it, which is the request that was open then.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(requests): move adopted follows in place and backfill replaced requests' follows

A new request took over a failed request's follows by deleting and
re-inserting them. An unfollow running at the same moment waited on the
deleted row, could not see the new one, and returned while the profile still
followed the new request. The follows now move with an UPDATE, which the
waiting unfollow re-checks and deletes.

The backfill gave a follow the title's latest request created before it even
when that request was already closed by then, as when the followed request
was deleted by its requester's replacement. It now takes that request only if
it could still have been open, and otherwise the title's open request.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* build: pin the plugin SDK to v0.19.0

The download-progress change pinned the SDK to its pull request commit,
which was squash-merged as v0.19.0 and is no longer fetchable, so the Go
jobs could not download the module. v0.19.0 has the same contents.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(requests): backfill a failed request's follows to its replacement

A follow made for a request that later failed stayed with the failed request
when its replacement had already completed and was still waiting to notify,
since the backfill only looked for an open request. It now gives such a
follow the title's first request since the follow that still has a
notification to send.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(requests): keep a follow stamped during its request's completion

A completion's timestamp is taken when its transaction begins, so a follow
that committed while it ran can carry a later created_at than the request's
completed_at. The backfill took that as the request having closed before the
follow and moved or dropped the follow. A completed request that has not
notified now keeps such a follow when the title has no request created after
it; a follow whose request was replaced and deleted always has that
replacement after it, so the two cases stay apart.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(requests): backfill a follow to a failed replacement request

A follow whose request was replaced and deleted, where the replacement has
since failed too, was dropped: the fallback looked only at active requests.
Such a follow now stays with the title's latest failed request, for the next
request to take over.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kody-ai

kody-ai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

Quick104 and others added 2 commits September 29, 2026 02:08
The download refresh pass stopped at a target whose router features could
not be read, without moving it back in the rotation. A lasting failure kept
the same requests at the head of every batch, and downloads outside the
batch were never refreshed. The target is now settled as unanswered, like
one its server did not answer for, and the request's other targets are
still asked about.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The title page looked for the viewer's cancellable request among the
account's newest 50 active requests, so an older pending request lost its
Cancel action. The detail names the request, so it is now read directly
and kept when the viewer's account made it; a detail without the ID still
falls back to the list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread internal/requests/follows.go
Comment thread migrations/sql/20260929001953_request_follows_request_key.sql

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a2ba20701

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/notifications/request_notifier.go
Comment thread internal/requests/repository.go
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 107bbe83ea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/requests/repository.go
MarkAvailable's callers check that a request has no live target before
calling it, but a submission could queue one in between, and the title
arriving then completed a request that was still downloading. The guarded
update now also requires that no target is queued or downloading.

Retrying a failed request also takes back the follows a later request for
the title took over when that request failed too, so they are told when the
retried request arrives.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kody-ai

kody-ai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

Quick104 and others added 3 commits September 29, 2026 03:36
The request suggestions in quick search pulled their cards, grid and status
badge into the launch bundle, which put it over its budget once merged with
main. They appear only after a search runs, so they now load on first use;
their keyboard options come from the search query, so navigation is in place
before they arrive.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Moving quick search's request suggestions out of the launch bundle left it
1.4 KB under its budget; the check asks for the saving to be recorded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kody-ai

kody-ai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a846694f55

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/notifications/request_notifier.go Outdated
Comment thread web/src/components/GlobalSearch.tsx
Quick104 and others added 2 commits September 29, 2026 04:13
The server-channel announcement went out inside NotifyFulfilled, before the
followers were cleared and the request stamped as notified. If either write
failed, the next pass notified the request again: personal deliveries
deduplicate, but the announcement was posted a second time. It now goes out
from AnnounceFulfilled, which the notify pass calls only when its stamp took.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the suggestions loading lazily, their rows became keyboard options as
soon as the search returned, so while the code loaded, or after it failed to
load, arrow keys could select a title that was not shown and Enter could open
it. A signal inside their Suspense boundary now marks them shown in the same
commit that paints them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kody-ai

kody-ai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac6f140f3f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/taskmanager/tasks/reconcile_requests.go
Comment thread internal/requests/notify.go
@Quick104
Quick104 merged commit 9d63465 into main Sep 29, 2026
32 of 33 checks passed
@Quick104
Quick104 deleted the feat/request-revamp branch September 29, 2026 13:05
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.

Send requested seasons to router plugins and allow requesting missing seasons

1 participant