1044 Commits

Author SHA1 Message Date
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
v2.8.1
2026-08-29 00:23:39 +00:00
neon f8ce9f58f1 i18n(client): sync translations from Crowdin (#1185)
Keep sparse translated catalogs synchronized with reviewed Crowdin content.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
v2.8.0
2026-08-28 12:26:31 -06: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
dependabot[bot] 16e28c0f2f chore(deps): bump docker/setup-buildx-action from 4.2.0 to 4.3.0 in the github-actions group (#1174)
Bumps the github-actions group with 1 update: [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action).


Updates `docker/setup-buildx-action` from 4.2.0 to 4.3.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/bb05f3f5519dd87d3ba754cc423b652a5edd6d2c...37fe631027851001ddb9b187196cc803df7f5f0e)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-28 08:32:11 -06:00
neon 7eb33761c7 fix(reader): allow KOReader downloads from filesystem root
Preserve the filesystem root during destination normalization so valid downloads reach the transfer callback. Report local preflight failures accurately and cover root publishing and traversal rejection.

Closes #1177
2026-08-28 05:02:03 +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 21a7f897b9 feat(client): rebuild duplicate cluster review in entity manager
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.
2026-08-27 23:21:08 +00:00
neon 4bd90b865c feat(client): modernize the entity manager browse interface
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.
2026-08-27 22:45:31 +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 58c3ef8b95 feat(books): rebuild the Files tab around the file list
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.
2026-08-27 20:57:09 +00:00
neon 9652e985d7 i18n(client): sync translations from Crowdin (#1169)
Keep sparse translated catalogs synchronized with reviewed Crowdin content.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-27 13:05:53 -06:00
neon 90bb087180 i18n(client): fill a plural branch Crowdin omitted instead of dropping the message
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.
2026-08-27 18:54:17 +00:00
neon 07de9cf908 i18n(client): sync translations from Crowdin (#1168)
Keep sparse translated catalogs synchronized with reviewed Crowdin content.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-27 12:39:11 -06:00
neon 5ff6bdfecb ci(i18n): stop the crowdin sync failing on a single translator edit
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.
2026-08-27 18:29:42 +00:00
neon e81848f654 fix(client): report progress while an iOS web app buffers a download
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
2026-08-27 18:10:51 +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 84c3cfc118 fix(client): keep downloads from stranding an iOS home-screen web app
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
2026-08-27 16:11:00 +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 d587d8f2a8 feat(books): overhaul files tab with tree rail and file inspector
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.
2026-08-26 14:07:52 +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 3f9d21defa feat(book-detail): rebuild the edit metadata tab as a four-column ledger
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.
2026-08-26 06:42:49 +00:00
neon 2861aff338 feat(book-detail): rebuild the details tab as a single-screen layout
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.
2026-08-26 05:53:29 +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
Daniel Chong 602289a0f6 fix(kobo): forward x-kobo-userkey and decode the composite sync token in the catch-all proxy (#1138)
* fix(kobo): forward x-kobo-userkey and decode the composite sync token in the catch-all proxy

The catch-all proxy in kobo-device.controller.ts forwards device requests to
storeapi.kobo.com through KoboProxyService, which built its outgoing headers from
a static allow-list (FORWARD_HEADERS). The list covered accept, accept-language,
authorization, content-type, user-agent, and a curated set of x-kobo-* headers,
but two of the headers that matter for store-API authentication and cursor
tracking were missing: x-kobo-userkey, the device's per-account secret that
authorizes most Kobo store endpoints, and x-kobo-synctoken, the upstream cursor
the device carries on every store call.

Without x-kobo-userkey, every store interaction proxied through the catch-all
shipped without credentials, so Kobo either rejected the call or treated the
device as unauthenticated. The "Connecting to OverDrive" dialog that issue #738
describes is the borrow endpoint, /v1/library/borrow, hanging because Kobo
cannot reconcile a borrow into a user's library without the user key. The same
gap also left library books a user borrowed on libbyapp.com (which is just
OverDrive pushed to kobo.com) unreachable from the device, because every
proxy-bound sync the device makes lands at Kobo without the credentials it
needs to enumerate the user's entitlements.

The Kobo Plus / Kobo Store entitlement fix in 2.6.0 (closes #789) only patched
the one explicit library/sync call. That call already decodes the BookOrbit
composite sync token via kobo-sync-token.ts and overrides the outgoing header
with the Kobo cursor Kobo understands, so the store page works. Every other
store call still went through the catch-all proxy with the device's
PX.<base64> composite on it, which Kobo treats as a fresh cursor and answers
for. The borrow endpoint in particular was the most visible casualty, but
anything else proxied through @All('*') was affected the same way.

Switch the proxy to a deny-list (DROP_HEADERS) for the headers RFC 7230 names
hop-by-hop or otherwise scoped to a single connection (host, connection,
content-length, content-encoding, transfer-encoding, keep-alive,
proxy-authenticate, proxy-authorization, te, trailers, upgrade), and forward
everything else the device sends. x-kobo-userkey now reaches Kobo on every
store call, so the borrow endpoint can authorize the user's library and the
libby-arrived entitlements surface on the next /v1/library/sync.

Add a forward() path that decodes the device's composite sync token before
relaying, and forwards the Kobo cursor Kobo can use instead. A raw Kobo token
(no PX. prefix) is forwarded verbatim, the way devices that synced with Kobo
directly before BookOrbit was put in the path already do. A composite token
whose cursor is undefined has no Kobo-side value, so omit the header entirely
on the way out rather than leak our composite to Kobo.

The extraHeaders and omitHeaders escape hatches stay in place, so the explicit
library/sync call can still pin the upstream cursor (and the no-cursor path
still drops the header) without going through the new decode-on-forward path.
The librarySync controller in kobo-sync.controller.ts already uses those
exacts knobs for the store page, so this change does not touch it; this
commit is the catch-all side of the same fix.

Closes #738

* fix(kobo): honor RFC 7230 Trailer and Connection fields, and emit sanitized proxy logs

Three small corrections surfaced by code review on the catch-all proxy that
delivers Kobo store entitlements.

The deny-list shipped with the wrong name for one of the RFC 7230 hop-by-hop
fields. `trailers` (plural) does not match the standard `Trailer` header that
arrives in mixed case, so a Trailer field set by an upstream could leak through
on the way to the device. Replace it with `trailer`.

RFC 7230 says the fields listed in `Connection` are also hop-specific, and a
proxy must strip them before forwarding. The two header-copy paths only
checked DROP_HEADERS, so a `Connection: x-kobo-tracking` header from the device
or from upstream could let a nominated field slip through. Parse the
Connection header in both directions, normalize case-insensitively, and add
its fields to the ignore set alongside DROP_HEADERS. The new
readConnectionFieldNames helper does the splitting once and is reused on both
sides of the relay.

Add a structured start/end/fail log around the relay, replacing the previous
single-line warn that interpolated a request-derived targetUrl and a raw
err.message. The repo's logging conventions in AGENTS.md name an external
HTTP call as a non-trivial operation that warrants start/end/fail logs, and
require sanitizeLogValue() for every dynamic value that lands inside a quoted
log field. The fail line now carries durationMs, errorClass, sanitized error,
and sanitized targetUrl. The 502 response with the "Upstream Kobo API
unavailable" body is preserved.

Tests cover the new paths: Trailer is stripped, Connection-nominated fields
are stripped on the request side, and Connection-nominated fields are stripped
on the response side. All 10,486 server tests pass; lint and typecheck clean.

CodeRabbit also asked to instantiate KoboProxyService through
Test.createTestingModule. AGENTS.md says the same thing, but every sibling
test in the kobo module uses direct construction with no testing module, so
changing only this file would invert the local pattern. The convention is
already inconsistent across the module and is not enforced by any lint rule;
that is a separate cleanup better suited to its own PR.

* test(kobo): drop unused makeHeaders helper in proxy service tests

makeHeaders was a placeholder identity function that returned its argument
unchanged. Inline the headers object literals at the 9 call sites.

* fix(kobo): scrub BookOrbit credentials and the composite token in every relay

Move the sync-token substitution into request() so forwardTagDelete, which
relays with no options, stops handing Kobo a PX. token it cannot read. Apply it
before omitHeaders and extraHeaders so store sync keeps both escape hatches.

Strip BookOrbit's session cookies and the reverse proxy forwarding headers:
these routes sit on the BookOrbit origin, so a browser carrying a session
reached them with our auth cookies attached and relayed them to Kobo.

Restore the SSRF regression case that covers the allowed hostname in userinfo.

* test(maintenance): wait for the cover sweep instead of a fixed turn budget

The detached sweep does real filesystem I/O, so 50 setImmediate turns settled it
locally but not on a loaded runner. The drain loop then gave up silently and the
next call failed as "The cover sweep is not complete", pointing at the wrong
place. Poll the store to a deadline and fail loudly when it truly hangs.

---------

Co-authored-by: Daniel Chong <daniel@chong.nz>
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
2026-08-25 09:52:40 -06:00
neon d4718cbe68 i18n(client): sync translations from Crowdin (#1105)
Keep sparse translated catalogs synchronized with reviewed Crowdin content.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-25 09:09:53 -06:00
neon 7d3ff9997d feat(library): add a Missing Resources cleanup tool
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
2026-08-25 05:18:31 +00:00
neon 63d4dc3bb4 feat(reader): continue a comic series from the last page
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
2026-08-25 04:26:31 +00:00
neon 6702d5b729 feat(reader): jump to reading session checkpoints
Expose each session's originating file so the reader can restore a validated historical progress position without losing exact highlight navigation.

Closes #1045
2026-08-25 01:20:12 +00:00
neon e165812792 feat(client): confirm destructive Book Dock discards
Require explicit confirmation before row, sheet, or bulk discard deletes files from disk. Provide a separate deselect action for bulk selections.

Closes #1007
2026-08-25 00:53:04 +00:00
neon 8c854dada8 feat(metadata): merge fetched genres with existing values
Preserve curated genres while appending unique values returned by metadata
providers, and expose merge versus replace behavior in the web interface.

Closes #1125
2026-08-25 00:41:12 +00:00
neon f2310c5806 fix(metadata): restore Hardcover title and author matching
Use Hardcover's default search configuration because custom fields currently return a payload-level error. Surface future search errors in logs so failures remain diagnosable.

Fixes #1123
2026-08-25 00:06:20 +00:00
neon 9989ba23e5 fix(metadata): sync audiobook progress to Hardcover
Use audiobook progress and edition duration to send progress_seconds so Hardcover no longer leaves listened books at zero percent.

Fixes #1074
2026-08-24 23:57:43 +00:00