Render the Readarr downloads table with the cancel download prompt and
the update downloads prompt, and dispatch it from ReadarrUi so the tab
is reachable.
The snapshot suite covers every block in DOWNLOADS_BLOCKS plus loading
and empty states. A second set of snapshots seeds a two row fixture and
selects the second row. Its rows deliberately have different completion
ratios, so that inverting the percentage calculation is caught rather
than cancelling out against a half finished download.
Render the Readarr blocklist table with the item details popup, the
remove item prompt and the clear blocklist prompt, and dispatch it from
ReadarrUi so the tab is reachable.
The snapshot suite covers every block in BLOCKLIST_BLOCKS plus loading
and empty states. A second set of snapshots seeds a two row fixture with
distinct values in every column and selects the second row, so that
reading items[0] instead of the current selection cannot pass.
The overview was rendered second of twelve into a fixed height block
with no scrolling, so free text of any length pushed status, genres,
rating, path and the statistics off the bottom where they could not be
reached. Every author in a real library has an overview and the median
runs several lines, so those fields were usually invisible.
Rendering it last keeps the structured fields on screen and lets the
overview take whatever space is left.
Renders the edit author prompt with a snapshot per step of the
selection state machine, seeding distinct quality and metadata profile
values so a swap between the two dropdowns cannot go unnoticed.
Renders the author search box, the search results table, and the
multi-step add confirmation prompt with its five dropdown popups.
The prompt's per-field highlighting is conveyed purely by style, which
render_to_string_with_app discards, so the highlight mapping is extracted
into add_author_prompt_highlights and unit tested per selection step.
Enriches the fully populated author search fixture with a second result
that is absent from the library, so both branches of the in-library
marker render in one snapshot and every table column carries a value.
Handles the delete book prompt opened from the author details view,
toggling whether to delete files on disk and whether to add a list
exclusion before confirming.
Backing out of the prompt resets both toggles, so preferences from an
abandoned delete cannot leak into the next one.
Handles the delete author prompt, toggling whether to delete files on
disk and whether to add a list exclusion before confirming.
Backing out of the prompt resets both toggles, so preferences from an
abandoned delete cannot leak into the next one.
Handles the multi-step edit prompt covering monitored, monitor new
items, quality profile, metadata profile, path, tags, and confirmation.
The prompt can be opened from either the author list or the author
details view, so the originating block is threaded through to the
sub-block pushes and Esc returns to the view the user came from.
Handles the add author search input, search results, and the multi-step
add prompt covering root folder, monitor, monitor new items, quality
profile, metadata profile, tags, and confirmation.
Selecting a search result whose foreign author id already exists in the
library routes to the already-in-library popup instead of the add
prompt, so a duplicate add cannot be submitted.
Span::style assigns rather than patches, so chaining it after bold()
discarded the modifier that bold() had just set and every detail popup
label rendered plain. Apply the modifier to the style itself instead.
Snapshots compare rendered text and cannot observe styling, so this is
pinned by asserting the label span's modifier directly.
Every line of the edition details popup was parsed as a label and a value,
so a line carrying no colon had one appended to it. A multi-line overview
therefore rendered with a spurious colon at the end of each continuation
line, and Goodreads sourced descriptions, which use CRLF, turned their
blank lines into a lone colon.
Split each line on the first colon only, leave a line without one
unlabelled, and trim the trailing carriage return so a CRLF blank line
renders as a real paragraph break.
Building the line is now a named function so its branches can be asserted
directly. Rendered text discards the carriage return, so a snapshot alone
cannot tell the trimming apart from leaving it in place.
A continuation line that happens to contain a colon is still styled as a
label. Telling those apart needs the details to be built as styled lines
rather than flattened to text and parsed back.
Pressing Del on the Editions tab opened a delete prompt operating on the
currently selected book file, which is not visible from that tab. On the
File tab, where the book files are listed, Del did nothing even though the
tab advertised a "delete book file" hint.
Gate the delete on the File tab instead and drop the now inaccurate hint
from the Editions tab clue set.
A Readarr edition carries no file id, unlike a Lidarr track, so the book
files live in their own table and the delete cannot be driven from the
editions table.
Draw the edition details popup over the book details view. The popup body is
the scrollable text the book details handler builds from the already loaded
edition row, so it needs no tabs and no network call.
The text styling is extracted into a named function so both branches are unit
tested, and the shared test fixture now holds the real field lines instead of a
placeholder, which exercises the scroll offset and the values that themselves
contain a colon.
Draw the book details popup over the author details view, with the editions
table, book history, the book file info tab, and the manual search releases
table, plus the automatic search, delete book file, download release, and
history details prompts.
Edition row styling is extracted into a named function so each branch is unit
tested; text snapshots discard colour and cannot see it. The shared test
fixture now populates the edition format, ISBN13, ASIN, and publisher, and the
book file media info, so the rendered columns are exercised rather than blank.
The book details handler already accepted the book file info block, but the
tab state declared only three tabs, so left and right could never reach it.
Add the File tab between History and Manual Search, backed by a new context
clue static covering refresh, delete book file, and esc.
Add EditionDetailsHandler, the single block popup opened from the editions
table. It owns the arrow, Home and End scrolling over its ScrollableText body,
and the Esc that pops the popup and clears the edition details modal.
The handler is nested under BookDetailsHandler, which claims the block ahead
of its own tables. It never sees the keypress that opens it, so it performs no
build, and it dispatches no network events: the popup is assembled from an
already loaded row.
Scrolling is implemented rather than stubbed. The body is mostly the edition
overview, which routinely overflows the popup, so empty scroll methods would
leave the arrow keys dead on the longest text in the view.
Add BookDetailsHandler, covering the editions, history and manual search
tables, the automatic search, manual download and delete book file prompts,
and tab movement across the book details modal.
Selecting an edition builds the EditionDetailsModal client side from the
already loaded row and pushes the popup block. Readarr has no edition detail
endpoint, so there is no network request and no event to dispatch.
The delete book file prompt is guarded on the book files table being
populated. Unlike Lidarr, where a track row carries its own file id, an
edition carries none and the file id comes from a separate table that can be
empty while editions are present.
Add AuthorDetailsUi: the author description pane plus the three author info
tabs (Books, History, and Author Search), along with the history item details,
automatic search, update and scan, and manual search confirmation popups.
LibraryUi now accepts and draws it over the authors table.
The description pane omits the Disambiguation and Type lines that the Lidarr
equivalent renders. GET /author returns neither key, so both lines would be
blank on every record. The books table drops the Type and Duration columns for
the same reason, since books have no type and no duration, and renders Files as
the book file count over the total book count.
Land create_history_event_details together with this first caller so it is not
committed as an unused function.
Extract decorate_book_row_with_style rather than inlining the row styling in
the table closure, matching decorate_author_row_with_style in the sibling
library view. Rendering to a string discards colour, so the styling branches
are only reachable by tests through a named function.
The stats, downloads, and logo panes of the Readarr context row had no test
exercising them. Add snapshots for the populated, loading, and empty variants
of the context row, plus loading and empty variants of the tab view.
Add LibraryHandler, which owns the authors table and its sort, search,
and filter overlays through TableHandlingConfig, along with the add,
edit, toggle monitoring, update all, and refresh key actions and the
delete prompt. Dispatch into it from ReadarrHandler and add the main tab
left and right movement helper.
authors_sorting_options offers eight sorts: name, status, quality
profile, metadata profile, books, size, monitored, and tags. It omits
the type and tracks sorts that the artists table has. GET /author never
returns authorType, so that column would be empty on every row, and
author statistics carry no track count.
Also cover the Readarr arm of handle_events, which was previously
unreachable from any test: neutering the arm entirely left the suite
green.
Give dispatch_by_readarr_block a real match so each view requests what it
needs, and refresh the shared metadata on first render and on every poll
tick. Until now the Readarr tab rendered but never fetched anything.
Guard the conditional arms the way the other servarrs do: the manual
author search only fetches when no releases are cached, book history only
when the books table is populated, and the manual book search only when
the book details modal exists with no cached releases.
Add the selection id helpers the arms read from, named to avoid colliding
with the equivalents the other servarrs already define on App.
Book edition details gets no arm on purpose. There is no endpoint to fetch
a single edition, and the popup is fully populated before it opens.
The fetched settings are only written into app state when nothing is
already held there, so a refresh landing while the user has the settings
prompt open cannot overwrite edits in progress.
The fixtures give every field a distinct value. Settings whose fields
all share one value cannot detect two of them being mapped to each
other's keys.
Results come back keyed only by indexer id, so each one is joined
against the cached indexer list to recover a display name, and its
validation failures are flattened into a single line for the results
table.
The endpoint reports failures with a non-2xx status, so the request
ignores the status code and parses the body either way. When the
request itself fails the results table is replaced with an empty one
rather than being left showing the previous run.
Fetch the indexer's full details, then post them back verbatim to the
test endpoint. The payload round-trips through serde_json::Value so the
fields Indexer does not model, such as infoLink and the per-field label
and type metadata, survive the round trip intact.
A successful test returns an empty JSON object and clears
indexer_test_errors; a failure returns an array whose first entry
carries the error message. The test endpoint reports failures with a
non-2xx status, so the request ignores the status code and parses the
body either way.
The delete flags travel as query parameters rather than a request body,
and Readarr spells the exclusion key addImportListExclusion, not the
addImportExclusion that Radarr uses.
The PUT body round-trips through serde_json::Value rather than the typed
Book, so the fields Readarr returns but Book does not declare survive the
toggle instead of being silently dropped. BOOK_JSON now carries a few of
those undeclared fields so a regression to a typed round-trip fails the
tests rather than passing unnoticed.