Two follow-ups from live testing:
- "recent" is BOTH a base toolbar key and a full registry key (where it
means desc). ResolveSeriesSort's full-key passthrough ran first, so
sort=recent&dir=asc silently resolved back to desc — the direction
chevron flipped but Date Added could never sort ascending. "recent"
now composes with the direction before the passthrough, unknown bases
fall to the default entirely (a hostile dir can't ride an unknown
sort), and the composition table is unit-pinned plus an HTTP-level
row in the series filter test.
- The header search "still broken" report was a stale-cache artifact:
the fixed code shipped but main.js kept its ?v=7 stamp, so browsers
with a cached copy never refetched (fresh-profile e2e passed while
the user's browser didn't — heuristically cached module). Stamp bumped
to ?v=8 in header + docs; the e2e suite re-verified against the
rebuilt server.
Toolbar feedback round: the series search box now matches whatever the
main search matches for a group — series NAME, any book's AUTHOR, and
any book's TITLE inside the series (WHERE EXISTS per group-row, so
counts stay true); the separate author input is gone from the toolbar
(the author= param stays live for the JSON API and deep links). The
sort control becomes a base-key select (Date added/Name/Author/Books/
Progress/Last read) plus a chevron direction toggle that flips and
resubmits; sort/dir compose through ResolveSeriesSort (full registry
keys keep working for back-compat, unknown base keys fall to recent).
The search/apply button moved next to the search box as requested, and
default URLs stay clean (sort/dir only emitted once the user chooses).
The /series page rendered its grid in one fixed order (newest first)
with no way to find a series beyond eyeballing cards. The page now has
a toolbar — series-name search, author filter, viewer-progress filter
and a 12-key sort select — wired as a plain GET form (the page's
existing model: the URL is the single source of truth, and the library
switcher now carries q/author/status/sort along on switch).
Extensibility is deliberate (user request): sorts live in a registry
(services.ResolveSeriesSort) mirrored by a CASE cascade in
GetDistinctSeries and a <select> in series.templ — a new sort is one
entry in each. The sort value is a BOUND PARAM compared inside the
cascade, never interpolated; unknown keys resolve to the default.
Filters: series-name matches in WHERE (the group key, so counts stay
true); author (BOOL_OR any-book match) and status (finished/reading/
unstarted vs the viewer's progress, same 0.999 threshold as the
filter_query status arms) in HAVING so COUNT(*) keeps reporting the
whole series. GetDistinctSeriesCount shares the JOIN + HAVING so
pagination totals match. The reading_progress join can't multiply rows
(the table is UNIQUE per media_item+user).
Cards gain the viewer's finished-count ("X of N read" + progress bar),
and the empty state distinguishes "no series at all" from "no matches —
clear filters". The JSON /api/series endpoint takes the same params and
gains read_count/last_read_at additively.
Integration test pins every sort key against a three-series fixture
with distinct created/author/count/progress answers, the filter matrix,
composition, unknown-sort fallback, card read-counts and the JSON
contract — all as the fresh regular user so the live reader's data is
untouched.
The family-lens hardening: library_visibility was only enforced where
file bytes flowed (download/reader/OPDS) — metadata surfaces leaked
hidden libraries to any authenticated account, and registration was
open by default with no admin-side user creation.
- Series are grouped PER LIBRARY now (same name in two libraries =
two entries: manga adaptations / LN splits no longer merge);
/api/series aggregates only the requester's VISIBLE libraries and
/api/series/books REQUIRES a visible library_id (breaking contract;
web + app clients updated in the same change).
- /api/media-items, item detail, and the reader content endpoints
(chapters/panels/outline/THUMBNAILS — page images leaked unguarded
next to a gated GetPage) all enforce the visible set; resolveLibrary
no longer honors a hand-typed library_id outside the visible set.
- Annotation/note/bookmark mutations (update/delete/tombstone, sync
LWW paths incl. conflict resolution) carry ownership predicates:
another account's bare row id is a 404, not a silent delete.
- Admin user creation: POST /api/auth/users (AdminMiddleware) + a
Create User card in the admin Users UI — accounts stay creatable
with self-service registration OFF, which is now the schema default
(fresh installs; the first-admin bootstrap exception is unchanged).
- Tests: series contract re-pinned (per-library, visibility 403s),
new ownership + admin-create integration suites, OPDS all=1 test
now passes an explicit page size (shared-DB growth made the fresh
books fall off page 1), device-cap helper switched to the admin
endpoint.
- dashboard_service.go: Change libraryID parameter from uuid.UUID to
pgtype.UUID across GetDashboardSections, GetDashboardPreferences,
and all helper methods. pgtype.UUID{Valid: false} now signals
"no library filter" (All Libraries), which gets passed through
to sqlc.narg() in the SQL layer.
- series_service.go: Drop libraryID parameter from GetSeriesBooks
entirely. Series are not library-specific — all books in a series
are shown regardless of which library they belong to.
Create SeriesService with methods for paginated series listing, cover
path resolution, series book listing, and a conversion helper for
GetContinueSeriesItemsRow to MediaItems.
Wire the continue-series query type into DashboardService's
getCollectionItemsByQueryType switch and add its metadata to the
RestoreSystemCollection default collection map.