Keep expanded synopsis content in the existing page or column flow so touch users do not encounter an undiscoverable nested scroll area. Expose the toggle state to assistive technology.
Fixes#1202
Accept the author display fields already emitted by the client and include them in automatic account sync. Keep older payloads compatible with defaults and cover the client-server contract with regression tests.
Fixes#1209
Expose promotion and demotion to existing superusers while keeping shared and self targets blocked. Serialize role lifecycle changes, preserve one active superuser, and invalidate stale credentials.
Closes#1208
Size book detail layouts from usable pane width so sidebar width and browser zoom do not force inappropriate compact layouts. Keep metadata save actions reachable while secondary controls overflow safely.
Fixes#1192
Add an isPublic flag to collections allowing users to share collections across the instance. Public collections are readable by all authenticated users while write, edit, reorder, and delete operations remain restricted to the owner.
Add fileSize as a filterable rule field resolved against the book's primary file size in bytes. In the filter builder UI, allow operators to input sizes in megabytes with automatic serialization to bytes for queries.
Redesign the bulk rename UI with change-type grouping, inline character diffs, and tree previews. Add two-mode selection for large libraries, virtualize the queue, and move search to the server. Optimize execution by pausing the library watcher instead of tearing it down, and improve SSE progress reporting and run error handling.
Share one path-resolution routine between the preview and the rename so the preview cannot offer a rename the executor refuses: multi-track audiobooks whose parts collapse onto one filename, and renumbering that would overwrite the book's own files.
Replace the inline cluster cards with a dedicated side-by-side duplicate comparison interface, cluster navigation list, threshold selector menu, and review controls. Introduce the useDuplicateReview composable to manage active review state and merge decisions across clusters.
Split the monolithic browse table into focused components for the toolbar, data grid, sortable headers, and pager. Add support for compact and comfortable row densities, improve pagination navigation, and refine the entity type selector layout.
The series, author and similar endpoints never selected a read status, so
every cover in the detail shelf rendered bare. Attach the requesting user's
status in one bulk lookup per lane, and draw the badge in BookCarousel using
the same icons and placement as the library grid, plus an sr-only label.
BookRepository has no user in scope, so it now returns UnscopedBookRecommendation
and the type system forces the status on where the user exists.
Also adds the untracked scanner overlap repro for issue 1094.
Closes#1163
Replace the tree-plus-inspector split with a hero strip, one Files card and a
rail holding only the selected file and a write-back summary. The nine stacked
bands stated a file's format five times and its role three, and the 28-row
metadata dump was the largest block on a tab about files; that table now lives
behind a link to Edit Metadata. The tab reflows on its own width via container
queries, so a collapsed sidebar and a narrow window behave alike, and the rail
becomes a bottom sheet on a phone.
Complete a plural category the target locale needs from the message's own
`other` branch during sync, so one missing branch no longer costs the whole
translation. Romanian arrived with `one` and `other` for all 156 of its
plurals and rendered English for every one of them. See docs/LOCALIZATION.md.
Fail the nightly export only when a catalog loses far more than churn
explains, rewrite a Crowdin em dash to the hyphen the source uses instead
of discarding the translation, and report a rejected message as a
rejection rather than a lost one. See docs/LOCALIZATION.md.
A home-screen web app cannot follow a streaming attachment link, so it
buffers the whole file first, and buffering said nothing: a large book is
about 45 seconds of a static UI. Read the body through a reader and render
the byte count as a progress toast, keeping the Content-Type so WebKit
still names the file.
Also default the book detail cover tint to off.
Refs #1159
A web app installed to the Home Screen has no browser chrome, so navigating it at an
attachment URL swapped the app for WebKit's document preview with no way back. Neither the
download attribute nor target="_blank" is honoured there, so hand that one case a blob and
keep the streaming link everywhere the browser owns a window it can leave.
Closes#1158
Imports resolved the naming pattern with the download resolver, which keeps only the last
path segment, so the shipped default's author and series folders were dropped and every file
landed at the library root. Rename, bulk rename and move all kept them, leaving the app
disagreeing with itself about where its own import belonged.
Closes#1157
Both views are toggled from the header and read from the same ladder: one
segment per volume the series should have, coloured read, reading, in library
or missing. Gaps were previously only on the series detail page, so the list
could never say a volume was absent; a Gaps filter now decides that in SQL,
mirroring the existing gap finder branch for branch so the two surfaces can
never disagree.
The card's fanned covers are laid out from cached percentages against an
aspect-ratio box, so nothing is measured at runtime and hover spreads the fan
through one custom property instead of per-cover reactive state.
See docs/COMMIT_GUIDELINES.md for the API additions to SeriesSummary.
The two layouts are the viewMode the app already persists, so search, filters
and sort carry across them. Only 19% of a library's authors have a portrait,
so the gallery leans on monograms and the index treats the name as the content;
a sort name now prints only when it says something the display name does not.
Jump buckets, a cover fallback and two filters come from the server so the A-Z
rail and the chips cover the whole result set rather than the loaded pages.
Reconciles authors.has_photo against the image store on boot: the list resolves
portraits from disk while the filter queried the column, so "no portrait" was
over-reporting by 64.
Both pages now share the stream, entry and detail components, so a highlight
reads the same wherever you meet it. Entries split three ways - date rail,
quote at a real measure, source margin - and the library hub gains an overview
endpoint so its side rail does not recompute six aggregates per scroll.
Notes and anchor-review are server filters now; filtering the loaded window
made the counts and the pager disagree with each other.
Both tabs fetch on mount and were rebuilt on every switch, so each visit
replayed blank, skeleton and content inside 120ms. Keep them alive and
revalidate silently instead, hold the layout until the first load decides
which shape it settles into, and defer skeletons past 220ms.
Also fixes the unhandled rejection failing the server suite in CI: the
Goodreads bot-challenge test rejected before its assertion was attached.
Replace the paginated card list with a three-column ledger: a marks rail, a
grouped stream that loads on scroll, an index that regroups by chapter, colour
or day, and a chapter position band. Add chapterBreakdown and activity to the
annotation stats so those panels describe the whole book without loading every
row, and move the metadata score colour onto a shared oklch ramp.
Seed the random sort field from a per-view randomSeed on BookQuery rather
than a day counter, so a reload or a scope change draws a new order while
every page of one listing stays coherent. Callers that omit the seed keep
the daily fallback. Adds a shuffle button to the library, smart scope, and
collection toolbars when the active sort is random.
Closes#1153
Redesign the book details Files tab into a two-pane layout with an interactive file tree rail
and detailed file inspector.
Support inspecting audio tracks, embedded metadata, and write-back properties, and add filename
stem normalization helpers.
Vitals, the session ledger and attempts/records fill the pane height, with
the progress band across the row beneath, matching the details tab.
- attempt boundaries and reading stalls are rows inside the ledger, in date
order only, so sorting by length or change still works
- the band toggles between the progress curve and a trace of the stretch of
the book each session covered
- one empty state for a book with no log, in place of four empty panels
- drops the twelve-month heatmap, which was 357 empty cells on a real book
Stats gain longestSessionSeconds, longestSessionAt and backtrackCount, and
sessions gain attemptId; the client cannot derive either from one page.
Artwork and source, identity, catalog and description sit side by side, sized
by container queries on the pane rather than the viewport. Provider ids lose
their accordion for compact rows above the community ratings, and a locked
field now reads as a filled chip instead of a near-identical open padlock.
Three columns fill the pane height and a discovery shelf takes the row
beneath them, so the whole record reads without scrolling the page.
- synopsis clamps and expands inside its own block, never the column
- per-format progress folds into Editions instead of a duplicate list
- multi-track audiobooks collapse to one edition row, not one per file
- the cover frame keeps its library aspect ratio at any column height
Ports formatColorVar, the pill-format tokens and formatRelativeFromNow,
which the new cards depend on.
Multiple audiobook editions of one title resolved to the same target path, so
enabling file rename merged or skipped every narrator but the first. Add
{narrators}, plus a :max3 modifier that yields nothing above three values, and
judge an optional group by what its placeholders render so the fallback literal
stays in the user's own pattern. The five duplicate token builders collapse into
one shared helper, which also gives audio uploads the metadata tokens they never
had. Build the shared types package before server lint, since type-aware rules
degrade to unknown on a clean checkout without it.
Refs #1108
Choose Off, Problems or All per category instead of a single on/off switch, and
stop announcing scheduled scans that changed nothing. Repeat occurrences now
collapse into one row with a count, so a bulk run no longer emits one
notification per book. Category and severity move into a shared registry that
the client reads instead of keeping its own copies.
Closes#1141
Remove database records for books whose files are gone, clear cover
references with no file on disk, and delete cover folders left behind by
deleted books. Each category is reviewed in a paginated list and
re-verified against disk before anything is removed.
Closes#312
Finishing an issue offered no way onward, so the next one had to be opened from the library.
Resolve the next readable book server-side with one keyset query, offer it on the last page, and
let an opt-in setting cross over on a further page turn. The reader route now remounts on a param
change, without which it would keep rendering the book it was mounted with.
Also cast the compared value in compareSeriesIndexSql: as a bare parameter Postgres could not type
it inside the null test and rejected the statement.
Closes#998
Expose each session's originating file so the reader can restore a validated historical progress position without losing exact highlight navigation.
Closes#1045
Require explicit confirmation before row, sheet, or bulk discard deletes files from disk. Provide a separate deselect action for bulk selections.
Closes#1007
Preserve curated genres while appending unique values returned by metadata
providers, and expose merge versus replace behavior in the web interface.
Closes#1125
Allow ready Book Dock rows to refresh provider matches without depending on the
failed-only bulk retry path. Preserve bounded queueing, ownership checks, and
stale metadata cleanup.
Fixes#1118
Apply a patch to @embedpdf/plugin-zoom@2.15.0 so numeric default zoom levels release the startup
viewport gate once resize metrics are measured.
Add unit test to verify custom zoom initialization and scale calculation under EmbedPDF.
Closes#1126
Treat series positions as validated string labels so values such as 5.10 retain their exact representation across storage, metadata providers, file formats, and clients.
Closes#1119
Build provider links from one shared registry so the detail page and quick view stay aligned.\nRender ComicVine with its CV fallback when no icon is available.
Fixes#1103
Each run sized its worker pool to the whole machine, so simultaneous runs
drove the box into swap until no suite finished and tests began failing on
the 5s default timeout. Serialize runs behind a lock, cap worker counts, and
raise testTimeout. See scripts/dev/with-test-lock.mjs.
Detect the OPF rendition:layout declaration and announce pre-paginated books
as EPUB3FL instead of KEPUB, which is what selects the device's fixed-layout
renderer. Kepub conversion is left alone so KoboSpan progress sync still works.
The flag is stored per file and backfilled during the sync reconcile pass, so
existing libraries converge without a rescan.
Closes#1044
Rebuilds the users roster, metadata field rules, file naming and metadata
score panels, and replaces the libraries list with an expandable ledger.
Libraries now batch their counts and last scan into one request instead of a
per-library fan-out, and surface scan history from scan_jobs on demand.
Let each user choose which accessible libraries feed dashboard shelves and reading widgets while preserving access checks and Smart Scope filters.
Closes#1113
Read variable-font axes and named instances so readers can select real weight and style variants instead of synthesized faces. Preserve the axis default, accept the full CSS weight range, and keep saved choices stable while font catalogs load.
Require the new settings fields for complete EPUB defaults, keep per-book patches partial, and widen the persisted font-weight constraints to match CSS.
Closes#533
Editing Date Started or Date Finished sent a date-only patch that the attempt layer had nothing to
attach it to. On an unread book no attempt was created, so the request returned 200 with null dates
and the field cleared itself. On a book being read the date landed on the open attempt as an end
date with no outcome, violating reading_attempts_closed_has_outcome_chk and surfacing as a 500.
Treat a date-only patch as a lifecycle statement: a start date opens an attempt and projects
reading, a finish date closes one as completed and projects read, and endedOn is never written
without its outcome. Clearing a finish date keeps the completion, because an outcome with no
completion date is legal and is already what re-read seeding writes. Setting a finish date on an
abandoned book now completes it. Reject a status that contradicts its dates with a stable error
code the client can translate, and derive statusChanged from the resulting status so a date-driven
transition still reaches achievements and the Kobo projection.
Keep unread and want_to_read free of dates they cannot own. The attempt keeps its own, so a book
marked unread no longer projects a start and finish date once a historical attempt is logged, and
marking a finished book unread no longer strands its finish date on the projection.
Stop the Hardcover import sending dates a status cannot carry. A want-to-read row that still holds
a start date would otherwise fail its row and skip the status import entirely.
Closes#1073