Commit Graph

757 Commits

Author SHA1 Message Date
neon adab5c8783 fix(scanner): handle legacy Book Dock series indexes
Normalize numeric JSON snapshots at the Book Dock boundary and reject malformed runtime values without aborting filing.

Closes #1241
2026-09-02 00:58:39 +00:00
neon 43a7c671d9 fix(server): resolve numeric-prefixed Hardcover slugs
Require the complete metadata value to be a valid GraphQL integer before using an ID lookup. Keep slug matching and sync change detection consistent through one parser.

Fixes #1243
2026-09-02 00:48:55 +00:00
neon 19afacb0f9 fix(server): handle Prowlarr torznab endpoint responses
Preserve Prowlarr's REST feed route and accept diagnostic responses so a server URL reports a useful Torznab mismatch instead of an opaque 406.

Fixes #1244
2026-09-02 00:32:47 +00:00
neon 3f06d4eee9 fix(cover): respect PDF crop box during extraction
Render the visible page region so wraparound covers do not appear in generated thumbnails.

Fixes #1242
2026-09-02 00:18:51 +00:00
neon e3779c1d7e feat(metadata): restrict autofill to stored provider ids
Prevent automatic metadata refreshes from searching alternate identities when a book already has trusted provider IDs. Preserve discovery and manual search behavior, and avoid dirtying descriptions on empty refreshes.

Closes #1222
2026-09-01 20:55:36 +00:00
wmTJc9IK0Q 04ec495c25 feat(reader): add PDF highlights and annotations on web only (#1213)
* feat(reader): add PDF highlights and annotations

Persist and render PDF highlights through the same annotation system as
EPUB. The annotation_positions table already supported a 'pdf' format, so
this wires the web create path, response, and reader UI to it.

Backend:
- create-annotation DTO accepts a pdf position (page + segment rects) as an
  alternative to a cfi; exactly one is required
- repository createPdf() persists a format='pdf' position (geometry in pos0,
  pageno in extras); selectWithCfi exposes the pdf geometry and status
- response DTO parses the pdf position and derives positionStatus

Frontend (pdf-v4 reader):
- usePdfAnnotations REST store and usePdfHighlights orchestration render
  persisted highlights via the EmbedPDF annotation plugin (autoCommit off,
  DB stays source of truth)
- selection popup (colour/style/note/copy/delete), note dialog, and a
  Highlights sidebar tab with search/colour/notes filters, navigate, delete
- NoteDialog relocated to reader/shared for reuse by both readers

Shared types gain AnnotationRect/AnnotationPdfPosition and an optional pdf
field on AnnotationItem.

* fix(reader): preserve input on failed PDF annotation save; convert mixed PDF+CFI for KOReader

- usePdfHighlights: createFromSelection/restyleExisting now return success;
  applyHighlight and saveNote only clear the selection, dismiss the popup, and
  close the note dialog on success. On failure the mark, popup, and typed note
  are preserved so the user can retry without losing input.
- koreader annotation exchange: the PDF push-down guard now skips only PDF-only
  annotations (no EPUB anchor). A mixed PDF+CFI annotation still converts its CFI
  to xpointer instead of being dropped.
- Tests for both paths.

* fix(reader): label PDF selection buttons and dedupe multi-page highlight retries

- PdfSelectionPopup: add aria-labels to the copy, highlight, note, and delete
  icon-only buttons so screen readers can identify them.
- usePdfHighlights: createFromSelection keeps only the pages that failed, so a
  retry after a partial multi-page failure never duplicates already-saved pages.
- Test for the multi-page partial-failure retry path.

* fix(reader): harden PDF annotation store, validation, and create logging

- usePdfAnnotations: catch rejected api() calls (session expiry / network) so
  load() sets loadError and mutations return null/false instead of rejecting.
- create-annotation DTO: require positive rect width/height so degenerate PDF
  geometry is rejected; tests for zero/negative dimensions.
- annotation.service: add [annotation.create] start/end/fail lifecycle logs with
  IDs, durationMs, and sanitized error for the multi-step create.

* test(reader): cover PDF annotations in the hub and add missing docstrings

Adds a real-DB e2e proving a PDF highlight persists as a pdf position and surfaces in the library-wide Annotations hub with the exact fields the page deep link needs (jumpFileFormat=pdf, cfi=null, pageno). Adds a hub-service unit test for the pdf row mapping. Documents the PDF render helpers, the annotation deep-link builder, and the createPdf repository method.

* fix(annotations): make PDF highlights reliable across reloads

Reconcile persisted highlights after EmbedPDF finishes loading its annotation state. Keep file-scoped pagination bounded and prevent PDF-only positions from starving device sync.

Ref #1195

* fix(annotations): keep pdf annotations stable during reload

Use authenticated native pagination fetches and discard responses that overlap successful mutations. Align shared dialog styling and annotation tests with project conventions.

Ref #1195

---------

Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
2026-09-01 09:16:01 -06:00
neon f41a58ac58 fix(stats): stop estimating sessions from KOReader progress
Keep KOSync limited to position synchronization and rely on measured page timings from the BookOrbit plugin for KOReader reading sessions. Remove legacy estimates at startup and rebuild affected daily totals.

Fixes #1233
2026-09-01 05:05:52 +00:00
neon 2912429ca6 feat(reader): add epub typography controls
Preserve publisher typography by default while allowing explicit spacing and indentation overrides across reader clients.

Ref #1206
2026-09-01 00:15:28 +00:00
neon 53cfe16b04 feat(reader): add epub paragraph spacing
Preserve publisher spacing at the minimum while allowing synchronized
format defaults and per-book overrides.

Closes #1206
2026-08-31 23:04:55 +00:00
Gary J. Espitia S. 2498e09083 fix(scanner): invalidate dir scan state on book deletion (#1226)
* fix(scanner): invalidate dir scan state on book deletion

Deleting a book row left its library_dir_scan_state entries intact, so the incremental scanner kept skipping the unchanged directory and never re-created the book on rescan. Invalidate the deleted books' folder paths and ancestors after deleteByIds so the next scan re-reads them and re-discovers the records.

* fix(scanner): make scan-state invalidation atomic

Prevent deleted books and concurrent scans from restoring stale directory state by versioning each library folder and coordinating writes under a database transaction.

* test(server): repair full-suite failures

Update the Book Dock expectation to the unit-record processor contract and bound the watchdog cutoff against timestamps captured around the sweep.

---------

Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
2026-08-31 15:23:57 -06:00
neon fc6e6f03d5 feat(book-requests): add automated request fulfillment
Let users request books, route approvals, search configured sources, and import fulfilled releases through the Book Dock. Include scoped permissions, automation, download-client integrations, plugin support, requester visibility, and one consolidated schema migration.

Closes #248
Ref #1115
2026-08-31 16:52:43 +00:00
neon 0f5429131a feat(smart-scope): filter by community review count
Allow provider-aware review count criteria so rating thresholds can be qualified by community review volume.

Closes #1041
2026-08-31 04:54:00 +00:00
neon 79b02e5b35 test(books): cover same-day reading completion
Protect equal manual start and finish dates through the client and status API.

Closes #1097
2026-08-31 02:15:34 +00:00
neon f555f080fe fix(server): merge imported read statuses safely
Preserve target-only statuses and update conflicts only when the source carries newer evidence. Treat timestamp-free source rows as insert-only so they cannot overwrite curated target state.

Fixes #1104
2026-08-31 02:07:33 +00:00
neon 07e8dcb90b test(server): cover narrator-specific rename paths
Verify matching audiobook editions resolve and move to separate destinations when only their narrator metadata differs.

Ref #1108
2026-08-31 01:53:49 +00:00
neon cc1b767264 fix: align Book Dock ready filter with filing readiness
Use the destination-complete predicate for the Ready chip and select-all operations so counts and filtered results remain consistent.

Fixes #1215
2026-08-30 17:21:35 +00:00
neon abc543044f fix(server): normalize Calibre indexes and restore image builds
Normalize integer-like Calibre OPF positions while preserving exact fractional labels. Make the static reply decoration explicit so Nest accepts production container builds.

Fixes #1212
2026-08-30 16:50:23 +00:00
neon cf84e37b2b fix(server): bind dc namespace during epub metadata writes
Preserve well-formed OPF output when publishers scope Dublin Core namespace declarations to individual metadata elements.

Fixes #1207
2026-08-30 16:28:27 +00:00
neon 5de4b9206b fix(server): support Fastify 5.12.1 proxy rules
Reject numeric trust-proxy values that Fastify no longer honors and retain explicit boolean or address-based configuration. Bridge Nest and cookie type augmentation differences so the pre-push typecheck succeeds under TypeScript 6.
2026-08-30 06:13:31 +00:00
neon 3196454944 fix(settings): persist complete display preferences
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
2026-08-30 05:59:19 +00:00
neon 3ea6063f31 fix(users): add safe superuser management
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
2026-08-30 05:44:37 +00:00
neon 67da9a2491 fix(server): reject ambiguous KOReader file hashes
Treat partial-hash matches across distinct books as unresolved so progress cannot be written to an unrelated file. Preserve same-book duplicates and user-scoped manual links as valid disambiguation paths.

Fixes #1135
2026-08-30 05:08:58 +00:00
neon 40c5abbbed fix(metadata): handle large Hardcover libraries
Bind book IDs as a PostgreSQL array so import previews do not exceed the wire-protocol parameter limit for large libraries.

Fixes #1196
2026-08-30 04:42:38 +00:00
neon 661b265251 fix(metadata): preserve calendar dates across timezones
Keep month-name metadata dates stable in positive UTC offsets so local-to-UTC conversion cannot change the stored calendar day.

Fixes #1194
2026-08-30 04:27:13 +00:00
neon 93c8136899 fix(series): repair legacy series index upgrades
Normalize legacy float values before migration 0076 so 2.7 databases can upgrade without constraint failures or precision loss. Serialize the compatibility conversion across concurrent startup processes.

Fixes #1189
2026-08-29 00:23:39 +00:00
neon 5a668b0015 feat(collections): add public collections with shared read access
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.
2026-08-28 16:13:28 +00:00
neon 1dd39bf0eb feat(books): add primary file size filter rule
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.
2026-08-28 15:32:28 +00:00
neon f39e0e8ab5 fix(reader): harden KOReader sync and credential refresh
Sanitize match-check batches before strict server validation and classify HTTP failures accurately. Rebind the active catalog client whenever authentication settings change so retries use current credentials.

Fixes #1150
2026-08-28 15:29:36 +00:00
neon 3b44fa5d7e build(deps): update server and client dependencies
Keep both workspaces current within their existing major versions and normalize virtual row keys for the updated type definitions.
2026-08-28 14:44:06 +00:00
neon a51122237d feat(library): rebuild the bulk rename tool
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.
2026-08-28 04:11:06 +00:00
neon ddb296cade fix(books): show read status on the book detail discovery shelf
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
2026-08-27 21:56:17 +00:00
neon ce5b000d7a fix(scanner): stop a self-write rescan wiping audiobook metadata
Saving metadata writes each audio file through a .bookorbit-write-* temp
file, which the watcher classified as content and turned into a folder
rescan of the book mid-write. The rescan read pre-write tags back over the
saved values, and an empty or failed audio read was persisted as nulls.

Closes #1164
2026-08-27 21:33:17 +00:00
neon b0b21b2b78 fix(kobo): version cover ids from the cover, not the metadata row
Any write to book_metadata bumped updated_at and minted a fresh
CoverImageId, so a device holding that cover locally fell back to the
placeholder offline. Stamp cover_updated_at at each cover write instead;
migration 0080 backfills it from updated_at so no id churns on upgrade.

Also trims the DECIMAL(10,2) scale Booklore hands back for a series
index, which sorted 2.50 after 2.9. Closes #943.
2026-08-27 17:36:32 +00:00
neon 7ae49c093c fix(types): trim trailing dots and spaces from resolved path segments
Token values are sanitized before modifiers run, so {authors:sort} rewrote
"J.K. Rowling" into a folder named "Rowling, J.K." that Windows and SMB reject,
serving a mangled 8.3 alias instead. Normalize each segment after assembly rather
than reusing sanitizePathSegment, whose reserved-name guard re-fires on a second
pass and would turn "NUL.txt_" into "NUL.txt__".

Closes #1160
2026-08-27 16:54:07 +00:00
neon c403ccb44c fix(books): read back user-supplied dates Postgres stored
Decode added dates and manual reading dates with an explicit parser instead
of new Date(), which reads a zero-padded year below 100 as a two-digit year:
years 0013 to 0031 became an Invalid Date and made every listing holding the
book answer 500, and years 0001 to 0099 shifted silently into 19xx or 20xx.
The column type is unchanged on the wire, so no migration is needed.

Closes #1143
2026-08-27 16:31:17 +00:00
neon 399129e447 fix(scanner): keep pattern folders when filing into File as Book libraries
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
2026-08-27 15:55:32 +00:00
neon c1794b677c feat(series): rebuild the list as a card and a list view sharing one volume track
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.
2026-08-27 04:11:10 +00:00
neon b41675c608 feat(authors): rebuild the page as an index and a gallery on one view toggle
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.
2026-08-27 04:04:18 +00:00
neon 95239ba701 feat(annotations): rebuild the hub and the highlights tab as one reading surface
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.
2026-08-26 22:34:11 +00:00
neon 8b9442be1f fix(book-detail): stop the reading log and highlights tabs flashing
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.
2026-08-26 19:53:36 +00:00
neon 87a3217c5e fix(metadata): stop Goodreads overwriting descriptions with snippets
Goodreads caps autocomplete blurbs near 180 characters and flags the cut,
but the mapper kept them, so a detail page it could not reach replaced a
full description with an ellipsised fragment. Drop a flagged snippet, retry
a transient 503 within a search budget, and raise a provider cooldown on a
bot challenge instead of degrading the whole batch.
2026-08-26 19:00:35 +00:00
neon 75ccb8dd1a feat(book-detail): rebuild the highlights tab as a single-screen ledger
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.
2026-08-26 17:04:48 +00:00
neon 4c42e0c73f fix(books): reshuffle random sort per visit instead of per day
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
2026-08-26 15:25:46 +00:00
neon 095113a594 feat(book-detail): rebuild the reading log as a single-screen ledger
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.
2026-08-26 07:28:41 +00:00
neon 0c02795a1c fix(metadata): merge embedded chapters across every file of an audiobook
Extraction reads one file, and each file of a multi-file audiobook embeds
its own chapters starting at zero, so later files had no chapters and the
first file's last chapter ran to the end of the book. Scanning now probes
every audio file in playback order and offsets each file's chapters by the
files before it, rebuilding books whose stored list stops short.

Closes #1146
2026-08-25 21:45:16 +00:00
neon a596133400 fix(kobo): derive reading sessions from the device reading-time counter
Current firmware never posts the LeaveContent analytics events the reading
log was fed from, so every Kobo statistic read zero. Derive sessions from the
SpentReadingMinutes counter the device already sends on each state push,
tracked per device in reading_session_sync_cursors so two readers of one book
cannot inflate each other. Analytics sessions still win and retire any
estimate they overlap.

Closes #1058
Closes #741
2026-08-25 20:40:00 +00:00
neon c383ad16e1 feat(naming): add a narrators token to file naming patterns
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
2026-08-25 19:03:37 +00:00
neon 88d814596d fix(metadata): write ComicInfo Volume as an integer and order elements per schema
<Volume> is xs:int, but it received the ComicVine volume name, which is usually a title. Strict
readers abort deserialization on the type error and discard every field, so metadata that is
present in the archive becomes unreadable. Write the element only when the name parses as an
integer, and emit elements in the schema's xs:sequence so the document validates.

Closes #1064
2026-08-25 18:01:16 +00:00
neon a25362244c feat(notifications): add per-category levels and collapse repeat notifications
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
2026-08-25 17:37:45 +00:00
Chris Mansell e2af7ba3b4 feat(stats): count KOReader sync reading toward streaks and statistics (#1140)
* feat(stats): count KOReader sync reading toward streaks and statistics

- Estimate a reading session from the interval between two advancing kosync pushes by the same device, so a plain KOReader install with no BookOrbit plugin still reaches the reading log, the streak and every daily statistic
- Bound each estimate with a one minute floor and a thirty minute ceiling, so a page turn is not a sitting and an overnight silence is not recorded as a long read
- Derive the estimate id from device, book file and interval start, so two pushes racing for the same file resolve to one row instead of counting the same reading twice
- Measure both the interval and the distance read against the same device's own previous row, so a second device syncing the book in between cannot lend it either one
- Skip estimating for a device that has swept in the last thirty days, since its plugin uploads real page timings for the same reading, and let the exclusion lapse so a removed or broken plugin does not retire the device from every statistic
- Retire estimates once a sweep measures the same reading, matched by an id prefix so the lookup stays bounded and indexed
- Rebuild a reader's daily stats when their timezone is submitted, so a corrected day boundary applies to history rather than only to the next couple of days
- Backfill daily stats once on upgrade for every reader with history, marked done only after a clean pass so a partial repair retries
- Keep a failed estimate or rebuild from taking down the progress push or the settings save that triggered it

Refs #261

* fix(stats): correct estimate logging, lock order, and settings latency

- Read the clock before handing the backfill pass to catch, so an outright failure reports its real elapsed time rather than durationMs=0
- Sort the hourly recompute's groups by user then library, so it and a concurrent per-user rebuild take the shared advisory locks in one order instead of deadlocking a reader who holds two libraries
- Detach the timezone rebuild from the settings write, which never returns its result and would otherwise hold the response open for a full-history walk
- Add durationMs to the koreader.sync_session end and fail logs
- Add durationMs to the sweep cleanup failure log
2026-08-25 09:56:06 -06:00