Commit Graph
1074 Commits
Author SHA1 Message Date
Dark-Alex-17 b9d96ce235 test(readarr): give each history fixture row a distinct quality
Two of the three rows carried the quality EPUB, so sorting by quality left them tied and any reordering confined to that pair was invisible. The rows now use EPUB, AZW3 and MOBI, matching the vocabulary the history UI fixture already uses.

This one is defensive: no natural mutation of the quality comparator flipped from surviving to failing, because two distinct values already separated the cross-field cases. The tie is removed on principle rather than to close a demonstrated hole.
2026-09-09 16:46:50 -06:00
Dark-Alex-17 3e414f9f0c test: give the CLI indexer settings tests distinct field values
The Radarr, Sonarr and Lidarr tests for editing all indexer settings expected every numeric field to be 1 and returned every previous field as 2. Because the fields were mutually equal, transposing any two of availability_delay, maximum_size, minimum_age, retention and rss_sync_interval in the CLI parameter builder produced an identical request and no test could tell. Radarr additionally expected both boolean flags to hold the same value, hiding a transposition of those two.

Readarr already used distinct values and was the model followed here.

Verified by transposing minimum_age and retention in all four CLI parameter builders at once: before this change only the two Readarr tests failed, while Radarr, Sonarr and Lidarr stayed green. After it, the same mutation is caught by five tests across the three trees.
2026-09-09 16:43:25 -06:00
Dark-Alex-17 f99d4f61f4 test: give the tree-local IndexerSettings fixtures distinct field values
Sonarr and Lidarr each keep their own copy of the shared fixture, both with minimum_age and retention set to 1, so transposing those two fields stayed invisible in either tree. Radarr built its fixture from IndexerSettings::default(), leaving availability_delay, maximum_size, minimum_age and retention all 0: four mutually transposable fields, none of which could distinguish a threaded value from a hardcoded 0.

The no-op guard test seeded IndexerSettings::default() as its sentinel, which cannot show whether the guard held or the field was simply never written. It now seeds a distinct stale_indexer_settings(), mirroring the Readarr convention.

Verified by transposing minimum_age and retention in the Lidarr indexer settings UI, now caught by its snapshot, and by inverting the Radarr no-op guard, now caught by both GetAllIndexerSettings tests.
2026-09-09 16:38:36 -06:00
Dark-Alex-17 d08f7d4b4e test: give the shared IndexerSettings fixture distinct field values
minimum_age and retention were both 1, so transposing them in any code that reads both rendered identically and no test could tell the difference. id was also 1, which cannot distinguish a correctly threaded value from a hardcode.

Verified by transposing minimum_age and retention in the Sonarr indexer settings UI: that mutation survived the full suite before this change and is now caught by the settings snapshot.
2026-09-09 16:31:05 -06:00
Dark-Alex-17 cef1ad13af test(readarr): pin table search, filter and selection behaviour
Readarr handler tests never asserted which block the search and filter keys push, so transposing searching_block and filtering_block in a TableHandlingConfig went undetected. History already had both tests; add the five missing search key tests and the three missing filter key tests, covering the authors, books, author history, editions and book history tables. The books and editions tables configure no filtering_block, so they get no filter test.

No test moved a table or list selection off index 0 either, which made current_selection() and items[0] indistinguishable. Select a non-zero index in the tests covering every handler that reads a selected row, adding a decoy entry at index 0 so existing expected values stay meaningful. The profile, monitor and root folder lists inside the add author, edit author and add root folder prompts are a separate gap that a table-only sweep misses, so those are covered too, as are the book files, editions and releases tables held on the book details modal.

Several fixtures also asserted against a default value, so the expected result was indistinguishable from an unset field. The add author, edit author and add root folder bodies now expect non-default monitor options, and the quality and metadata profile ids are drawn from disjoint id pairs so the two lookups cannot be transposed.
2026-09-09 16:22:51 -06:00
Dark-Alex-17 f141e53f30 test(lidarr): pin table search, filter and selection behaviour
The search and filter blocks a table configures were asserted nowhere, so
transposing them was invisible. Selections were also left at index 0 in
every test, which made current_selection() and items[0] indistinguishable,
including for the profile lists inside the edit and add prompts.
2026-09-09 15:51:10 -06:00
Dark-Alex-17 18a50ab97c test(sonarr): pin table search, filter and selection behaviour
Sonarr handler tests never asserted which block the search and filter keys
push, so transposing searching_block and filtering_block in a
TableHandlingConfig went undetected. Add a search key test for each of the
six searchable tables and a filter key test for each of the four filterable
ones, covering library, history, series details and season details.

No test moved a table or list selection off index 0 either, which made
current_selection() and items[0] indistinguishable. Select a non-zero index
in the tests covering every handler that reads a selected row, adding a
decoy entry at index 0 so existing expected values stay meaningful. The
modal list selections in the add and edit series prompts are a separate
gap that a table-only sweep misses, so those are covered too.

Several fixtures also asserted against a default value, so the expected
result was indistinguishable from an unset field. The system tasks now use
two distinct non-default task names, the season monitoring and manual
search expectations use distinct non-zero ids, and the add and edit series
bodies expect a non-default series type and monitor with distinct quality
and language profile ids so the two lookups cannot be transposed.
2026-09-09 11:25:34 -06:00
Dark-Alex-17 4cbec79334 test(radarr): pin table search, filter and selection behaviour
Radarr handler tests never asserted which block the search and filter keys
push, so transposing searching_block and filtering_block in a
TableHandlingConfig went undetected. Add a search key and a filter key test
for each of the three handlers that configure them: library, collections and
history.

No test moved a table or list selection off index 0 either, which made
current_selection() and items[0] indistinguishable. Select a non-zero index
in the tests covering every handler that reads a selected row, enriching
fixtures with a second, distinct entry where only one existed.

Several fixtures also asserted against a default value, so the expected
result was indistinguishable from an unset field. The system details tasks
now use two distinct non-default task names, and the add movie body expects
a non-default monitor and minimum availability.
2026-09-09 10:47:07 -06:00
Dark-Alex-17 c9244b0459 feat(radarr): make the collection movie overview popup scrollable
A collection movie's overview renders in a popup that does not scroll,
so a long overview overflows with no way to read the rest. The popup now
scrolls.

The popup owns its own ScrollableText, built from the selected collection
movie's overview when it opens and torn down on Esc so a reopened popup
starts at the top. Scrolling mutates only that owned state. The
background renders the Collection record's overview and the movies
table, neither of which carries a scroll offset, so they cannot scroll
along with the popup. The popup also gains the loading and absent
fallbacks the rest of the tree already has.

Give the collection movie fixture an overview distinct from its
collection's, and a multi-paragraph one, so the snapshots can tell the
two records apart and a scrolled popup is distinguishable from an
unscrolled one.
2026-09-08 22:07:22 -06:00
Dark-Alex-17 b3ce15a564 feat(sonarr): add a scrollable overview popup to series details
A series' overview renders as one line of a description box that does
not scroll, so a long overview overflows with no way to read the rest.
Pressing v on the series details Seasons tab now opens a popup holding
the full overview.

The popup owns its own ScrollableText, built fresh from the series'
overview when it opens and torn down on Esc so a reopened popup starts
at the top. Scrolling mutates only that owned state. The background
paragraph renders the Series record's overview, which carries no scroll
offset, so it cannot scroll along with the popup.

Cite the existing view keybinding from the series details context clues
and give the popup its own clue set so it does not inherit the hints of
the view underneath.

Enrich the shared series fixture to a multi-paragraph overview, and the
raw response fixture that encodes the same series, so a scrolled popup
is distinguishable from an unscrolled one.
2026-09-08 21:31:25 -06:00
Dark-Alex-17 ca04498717 feat(lidarr): add a scrollable overview popup to artist details
An artist's overview renders as one line of a description box that does
not scroll, so a long overview overflows with no way to read the rest.
Pressing v on the artist details Albums tab now opens a popup holding
the full overview.

The popup owns its own ScrollableText, built fresh from the artist's
overview when it opens and torn down on Esc so a reopened popup starts
at the top. Scrolling mutates only that owned state. The background
paragraph renders the Artist record's overview, which carries no scroll
offset, so it cannot scroll along with the popup.

Cite the existing view keybinding from the artist details context clues
and give the popup its own clue set so it does not inherit the hints of
the view underneath.

Enrich the shared artist fixture to a multi-paragraph overview, and the
blocklist response fixtures that embed the same artist, so a scrolled
popup is distinguishable from an unscrolled one.
2026-09-08 21:05:50 -06:00
Dark-Alex-17 27c458307f feat(readarr): add a scrollable overview popup to author details
An author's overview renders as the last line of a description box that
does not scroll, so a long overview overflows with no way to read the
rest. Pressing v on the author details Books tab now opens a popup
holding the full overview.

The popup owns its own ScrollableText, built fresh from the author's
overview when it opens and torn down on Esc so a reopened popup starts
at the top. Scrolling mutates only that owned state. The background
paragraph renders the Author record's overview, which carries no scroll
offset, so it cannot scroll along with the popup.

Add a view keybinding bound to v, cite it from the author details
context clues, and give the popup its own clue set so it does not
inherit the hints of the view underneath.
2026-09-08 18:05:52 -06:00
Dark-Alex-17 ee6035c75d test(readarr): cover the top level UI dispatch for Readarr
The top level ui() dispatch had no Readarr coverage, so removing its match arm let every test in the suite pass while the Readarr views rendered nothing at all.

Mirrors the Radarr set, which is the most complete of the three: it is the only one that also covers the notification failure popup.
2026-09-08 16:18:00 -06:00
Dark-Alex-17 3fd0a61737 fix: show confirmation prompt clues on delete prompts
Delete confirmation prompts had no arm in any of the context clue providers, so they fell through to the main tab's contextual help and displayed the library tab's hints, such as add, sort and edit, while the user was looking at a delete confirmation.

Map the delete block arrays in all four servarrs to the shared confirmation prompt clues, matching how the other prompts and selection modals are already wired.
2026-09-08 16:07:19 -06:00
Dark-Alex-17 720584ea17 fix: drop redundant references in format arguments
The borrows were passed straight to format!, which already takes its
arguments by reference. Clears the last of the compiler warnings.
2026-09-08 15:56:38 -06:00
Dark-Alex-17 59346973fc feat(readarr): add the indexers UI
Render the indexers table with its delete and test popups, the edit
indexer prompt with separate torrent and usenet field grids, the prompt
for configuring all indexer settings, and the test all indexers results
table. All four are dispatched from the Readarr UI.

Extract indexer_flag_text, decorate_test_result_row_with_style and the
two prompt highlight builders as named functions so their branches can
be unit tested. Rendering to a string discards colour, so a snapshot
cannot tell a success style from a failure style.
2026-09-08 10:38:52 -06:00
Dark-Alex-17 a7178a481b feat(readarr): add the indexers handlers
Add IndexersHandler and its three sub-handlers, EditIndexerHandler, IndexerSettingsHandler and TestAllIndexersHandler, and dispatch to them from ReadarrHandler.

IndexersHandler::accepts ORs in all three children, so the parent and the children land together. Splitting would leave eighteen blocks accepted but inert, with Esc falling through to handle_clear_errors instead of popping.

TestAllIndexersHandler::accepts compares the block for equality rather than consulting an array, since TestAllIndexers appears in no block array.
2026-09-08 10:10:25 -06:00
Dark-Alex-17 70441259a0 feat(readarr): add the system UI
The system tab renders the tasks table, the queued events table and the log list, and overlays the log details, tasks, queued events, task start confirmation and updates popups.

SystemUi and SystemDetailsUi ship together because SystemUi::accepts covers every block that only SystemDetailsUi draws. Landing them apart would advertise five blocks that nothing renders.

Log level extraction is pulled into a named function so it can be unit tested directly: the level only ever reaches the output as a colour, and the rendered snapshots are plain strings that discard it.
2026-09-08 09:51:55 -06:00
Dark-Alex-17 24c5dedd0c feat(readarr): add the system handlers
SystemHandler owns the System tab: tab movement, the refresh key, and the
five keys that open the queued events, logs, tasks, and updates views. It
delegates every system detail block to SystemDetailsHandler, which scrolls
the log, task, queued event, and update views and drives the start task
confirmation prompt.

Both land together because SystemHandler::accepts covers the system detail
blocks that only SystemDetailsHandler implements, so landing the parent
alone would accept those blocks while doing nothing with them.
2026-09-08 09:33:26 -06:00
Dark-Alex-17 58fe7d79ae feat(readarr): add the root folders UI
Renders the root folders table, the delete prompt and the add root
folder modal. The modal conveys its current step purely through
styling, which rendered output discards, so the step highlights are
computed by a separate function whose branches are asserted directly.
2026-09-08 09:15:37 -06:00
Dark-Alex-17 5e1f3bb7e3 feat(readarr): add the add root folder handler 2026-09-04 12:22:35 -06:00
Dark-Alex-17 a4a3c77586 feat(readarr): add the root folders handler 2026-09-04 12:11:00 -06:00
Dark-Alex-17 ee07a0289f feat(readarr): add the history UI
Renders the history table, its sort, search and filter prompts, both
empty result popups and the item details view. Snapshots cover a
non-default selection so that reading a row from a fixed index rather
than the current selection cannot pass unnoticed.
2026-09-04 09:49:36 -06:00
Dark-Alex-17 91c2613e65 feat(readarr): add the downloads UI
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.
2026-09-04 00:39:33 -06:00
Dark-Alex-17 f73959b73f feat(readarr): add the blocklist UI
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.
2026-09-04 00:34:46 -06:00
Dark-Alex-17 bad43d1383 feat(readarr): add the history handler 2026-09-04 00:22:04 -06:00
Dark-Alex-17 adf0896b7d feat(readarr): add the downloads handler 2026-09-04 00:07:33 -06:00
Dark-Alex-17 bd96c4ab23 feat(readarr): add the blocklist handler 2026-09-03 23:54:49 -06:00
Dark-Alex-17 e9d3851db8 fix(readarr): stop a long author overview hiding the other fields
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.
2026-09-03 22:05:11 -06:00
Dark-Alex-17 773719dbd2 feat(readarr): add the delete book UI 2026-09-03 21:58:35 -06:00
Dark-Alex-17 2b07ad96f1 feat(readarr): add the delete author UI 2026-09-03 21:51:21 -06:00
Dark-Alex-17 7fa091cb44 feat(readarr): add the edit author UI
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.
2026-09-03 21:38:37 -06:00
Dark-Alex-17 7d1ff86c68 feat(readarr): add the add author UI
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.
2026-09-03 18:03:12 -06:00
Dark-Alex-17 7500e2da24 feat(readarr): add the delete book handler
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.
2026-09-03 17:29:31 -06:00
Dark-Alex-17 6a7f9fe0da feat(readarr): add the delete author handler
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.
2026-09-03 17:18:48 -06:00
Dark-Alex-17 dc4b11cf1b feat(readarr): add the edit author handler
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.
2026-09-03 17:07:53 -06:00
Dark-Alex-17 ab9645dee9 feat(readarr): add the add author handler
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.
2026-09-03 16:52:39 -06:00
Dark-Alex-17 da6c9e7b66 fix(ui): render detail popup labels in bold
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.
2026-09-03 16:31:12 -06:00
Dark-Alex-17 a311e2b895 fix(readarr): stop appending stray colons to the edition overview
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.
2026-09-03 13:20:21 -06:00
Dark-Alex-17 61a18cb222 fix(readarr): move the book file delete to the File tab
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.
2026-09-03 13:11:51 -06:00
Dark-Alex-17 280e691a94 feat(readarr): render the edition details popup
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.
2026-09-03 12:18:56 -06:00
Dark-Alex-17 b4c6994362 feat(readarr): render the book details popup
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.
2026-09-03 12:12:03 -06:00
Dark-Alex-17 d97c20d5ac feat(readarr): reach the book file info tab from the book details tabs
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.
2026-09-03 12:00:40 -06:00
Dark-Alex-17 751ff6ecf5 feat(readarr): scroll and dismiss the edition details popup
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.
2026-09-03 11:51:20 -06:00
Dark-Alex-17 14deed6b81 feat(readarr): handle the book details tabs and open the edition details popup
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.
2026-09-03 11:43:49 -06:00
Dark-Alex-17 baf74d95e1 fix(readarr): render the update all authors confirmation prompt 2026-09-03 01:49:28 -06:00
Dark-Alex-17 5480514b12 fix(readarr): drop the always-empty author type column from the authors table 2026-09-03 01:46:44 -06:00
Dark-Alex-17 a8d13ae5c8 feat(readarr): render the author details view
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.
2026-09-03 01:41:01 -06:00
Dark-Alex-17 d1d5033710 test(readarr): snapshot the Readarr context row, loading, and empty states
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.
2026-09-03 01:05:43 -06:00
Dark-Alex-17 fb465b06f8 feat(readarr): handle key events for the author details view 2026-09-03 00:55:36 -06:00