399 Commits

Author SHA1 Message Date
Oliver 41fd6f1881 feat(book): move books between libraries (#818)
* feat(books): add moving books between libraries

Allow moving books to another library without re-uploading, so a staging
library (e.g. book-dock imports) can feed curated genre libraries. Re-parent
the existing book row instead of delete+recreate so reading progress,
annotations, collections, and Kobo sync state survive the move. Relocate
files by rebasing library-relative paths onto the target folder with
rollback on failure and a copy+delete fallback for cross-filesystem moves,
then schedule the target library's rename pattern. Exposed as a bulk
endpoint plus UI actions in the selection bar and book detail page.

Closes #257

* ci: allow container image dispatch from any branch

Enable building a GHCR image from a feature branch via workflow_dispatch
to test changes in a container before they land on main. The main tag is
still only updated by the CI-gated workflow_run path.

Ref #257

* fix(books): update book row before files when moving between libraries

book_files_book_folder_consistency_fk references books(id, library_folder_id)
with ON UPDATE CASCADE, so updating a file's library_folder_id while the book
still points at the source folder violates the constraint and the move fails
with a 500. Update the books row first so the new folder id cascades to the
files, then rewrite the file paths. Verified against a real Postgres instance
in addition to the reordered unit test expectations.

Ref #257

* test(books): add book move e2e suite

Unit tests mock the database and cannot catch constraint-order bugs like
the book_files_book_folder_consistency_fk violation fixed in b1c430df.
Cover the move endpoint against a real Postgres and filesystem: successful
cross-library move with row re-parenting, file relocation, and preserved
user state, plus disk-collision skip, foreign-folder rejection, and
format-restriction skip. Reuses the metadata-write e2e harness.

Ref #257

* fix(books): emit transfer event so library views refresh after a move

Moving books via the API updated the database and files but never told
connected clients, so the source and target library views kept stale book
lists and counts until a manual reload. Emit the existing book:transferred
websocket event (grouped per source library) after successful moves; the
library and book detail views already subscribe to it and refresh their
lists, counts, and buckets.

Ref #257

* fix(books): refresh views deterministically after moving books

The initiating library view only removed rows locally and relied on the
book:transferred websocket round-trip for counts, so deployments where
websockets are unavailable kept stale headers and sidebar badges until a
reload. Refresh the view's list and buckets directly after a successful
move, refresh the sidebar library counts on transfer events like scans
and uploads already do, and surface an error toast when a move is
confirmed with an empty selection instead of silently doing nothing.
Promote the move action from the overflow menu to a dedicated,
count-gated button in the selection action bar.

Ref #257

* fix(books): show move action for demo-restricted accounts like delete

The move button was additionally gated on the demo-restriction flag, so
accounts carrying the explicit demo_restricted permission saw the delete
button but no way to move books. Gate it on library_delete_books alone,
mirroring the delete action next to it; demo instances withhold that
permission anyway.

Ref #257

* style(server): align union type formatting with upstream main

The criss-cross merges of two upstream states left seven files with the
older prettier union-type layout, which fails lint against the current
formatter output. Restore the files byte-identical to upstream main.

* i18n(client): translate the move-to-library UI

The move action, dialog, and detail menu entries carried hardcoded
English strings from before the vue-i18n migration. Move them to
translation keys following the existing catalog structure and add
entries to all locale catalogs so the parity validator passes; Crowdin
can refine the non-English translations later.

Ref #257

* security(deps): update brace-expansion and engine.io past new advisories

Trivy fails the container build on CVE-2026-13149 (brace-expansion DoS)
and CVE-2026-59725 (engine.io DoS). Both fixes are semver-compatible
patch releases, so refreshing the lockfile resolves them without
manifest changes.

* security(deps): patch fastify static and find-my-way CVEs

Trivy blocks image builds on CVE-2026-15074 (route guard bypass in
fastify/static 9.x) and CVE-2026-47219 (HTTP2 DDoS in find-my-way
9.6.0). Bump the direct fastify/static dependency to 10.1.2 and add a
find-my-way override because nestjs/platform-fastify pins it to an
exact vulnerable version. The static plugin usage (register with root
and prefix plus sendFile fallback) is unchanged in v10; the peer range
warning from NestJS is expected until it allows v10.

* i18n(client): add BookBulkMove audit action label to all catalogs

Upstream's audit filtering feature derives its action options from the
AuditAction enum and requires a label for every member. Add the label
for the move feature's BookBulkMove action to all nine locale
catalogs.

* ci: restore main-only guard for container image dispatch

Revert the fork-local change that allowed workflow_dispatch container
builds from any branch. The guard matches upstream again so the branch
only carries the move feature. Fork builds continue from the
fork-build branch which keeps the relaxed dispatch.

This reverts commit b13d276a4becbba7be80723ff6708df23132b3fb.

* style(client): restore upstream formatting in untouched files

Merge conflict resolution left four files with stale prettier
formatting that differs from upstream while carrying no functional
change. Restore them byte-identical to upstream/main so the move
feature diff stays clean.

* fix(book-move): harden error paths raised in PR review

Address review feedback on bookorbit#818:

- Guard the applyMove call so a database error fails only that book
  instead of aborting the batch, which discarded collected results and
  skipped transfer events for books that had already moved
- Replace the bare catch around the source access check with one that
  maps ForbiddenException to the access reason and logs unexpected
  errors with a distinct failure reason
- Report an incomplete rollback in the outcome reason so callers know
  a book may need manual verification
- Ignore backdrop and cancel clicks in the move dialog while a move is
  running
- Catch thrown move requests in handleBulkMove, surface an error
  toast, and keep the dialog open on transport errors so the user can
  retry
- Wire onBooksMoved in SmartScopeView so scope totals and jump buckets
  refresh deterministically after a move
- Extend the book-move e2e suite changedPaths with the library and
  scanner modules and the metadata-write harness it depends on

* i18n(client): fill move keys with English source for Crowdin

Crowdin owns the target catalogs, so hand-written translations get
overwritten or conflict on the next export. Replace the translated
move dialog, menu, tooltip, and audit label values in all fifteen
target catalogs with the English source strings, keeping the
locale-specific ICU plural categories the parity validator requires.
Real translations happen in Crowdin.

* fix(book-move): harden access, filesystem, and audit behavior from review

Address the maintainer review on bookorbit#818, server side:

- Require editor-level access on the target library via a new
  LibraryService.verifyUserAccessLevel backed by a repository lookup,
  mirroring the LibraryAccessGuard ranking; viewer access could
  previously write files into a library through this endpoint
- Skip books whose files are missing before touching the database
- Assert resolved target paths stay inside the target folder so a bad
  relPath cannot escape the library via mkdir and rename
- Bound empty-directory cleanup at the library folder root and walk
  nested directories upward so flat libraries keep their configured
  folder and Author/Series trees do not orphan
- Bump books.updatedAt in applyMove so Kobo sync sees moved books, and
  cancel the source library's pending unavailable-books notification
  like the scanner's transfer path does
- Suppress file-watcher events for all touched paths through
  SelfWriteRegistry while files move, closing the race against
  missing-marking and duplicate detection
- Replace rename with link+unlink (COPYFILE_EXCL on cross-device
  fallback) so concurrent moves resolving the same target fail with
  EEXIST instead of silently clobbering each other
- Record the actual moved/skipped/failed outcome in the audit entry
  instead of the requested count

* fix(book-move): use stable outcome codes and an accessible move dialog

Address the client-facing maintainer review items on bookorbit#818:

- Replace the free-form English outcome reasons with a stable code
  union in MoveBookOutcome; the prose stays in server log lines and
  the client maps codes to translated copy, so the UI no longer
  renders raw server strings
- Localize the single-book move toasts on the detail page through the
  new outcome keys (English source in all catalogs, translation via
  Crowdin)
- Rebuild MoveBooksDialog on reka-ui DialogRoot/Content/Title like
  ConfirmDialog, gaining role=dialog, focus trap and restore, Escape
  handling, and aria wiring; closing stays blocked while a move runs
- Filter libraries without folders out of the target picker since
  confirming one is a guaranteed 400

* test(book-move): assert database and disk state in no-op scenarios

The rejected and skipped e2e cases only checked the response before;
a partial move could have passed. Assert the 400 error message, the
unchanged libraryId and libraryFolderId, the source file remaining,
the destination staying absent, and that a colliding target file
keeps its original content.

* feat(book-move): stream bulk moves with per-book progress

The move endpoint processed an unbounded selection serially inside one
request, which cannot finish within proxy timeouts on target-scale
libraries and gave no progress. Convert it to the same SSE pattern as
bulk metadata refresh:

- The endpoint streams one event per book plus a final done summary
  and stops processing when the client disconnects
- Target validation runs before headers are written so user errors
  stay regular JSON error responses
- Audit is emitted manually with the actual outcome because the
  interceptor never sees a response body on streaming handlers
- The bulk composable consumes the stream, reports progress through
  the shared in-flight indicator, and removes moved books from the
  view as their events arrive; the detail page reads the single
  outcome from the stream
- The e2e suite parses the event stream and additionally asserts the
  done summary and content type

* fix(book-move): explain skipped books in multi-library selections

Collection and smart-scope selections can span libraries, so the
books' own library legitimately appears as a move target and picking
it skips them with already_in_target. Surface that in the dialog with
a hint that is only shown when the view has no single current library
to filter out. Exact target filtering would need a library id on every
book card, which collapsed series cards cannot provide unambiguously.

---------

Co-authored-by: ohoeltke <hoeltke@oho-dt.de>
2026-08-02 10:41:57 -06:00
Gideon VanRiette d86b32ed85 fix(metadata): stop concatenating series and issue number into comicvine title (#861)
* fix(metadata): stop concatenating series and issue number into comicvine title

ComicVine search and auto-fill set title to a synthesized "Series #N -
Name" string instead of the issue's own name, inconsistent with how
ComicInfo.xml's <Title> is used elsewhere in the app (issue title only,
series and issue number in their own fields). title now carries just
the issue name, falling back to "<series> #<issue>" only when ComicVine
has no name for that issue.

The concatenated format is genuinely useful for telling issues apart
in the search-results list, so a new optional displayTitle field
preserves that exact UX there, populated only by the ComicVine mapper.
It's display-only and never read by any metadata-apply path, so the
list keeps its current look while the metadata that actually gets
persisted is cleanly separated.

filterAndRank now scores candidates against displayTitle when present,
falling back to title otherwise, so shortening title doesn't drop named
issues when users search by series name. Since displayTitle is only
ever set by the ComicVine mapper, this reproduces today's exact
ranking behavior for ComicVine and leaves every other provider
untouched.

Closes #841

* fix(metadata): leave comic titles unset when comicvine has no issue name

Address review feedback on the comicvine title mapping.

Issue #841 asked for the issue name alone, or nothing when comicvine has none for that issue.
Falling back to "<series> #<issue>" kept writing a synthesized title for unnamed issues, which
is the common case in a comic run, so the ComicInfo round trip only held for named issues. Make
MetadataCandidate.title optional and leave it unset instead: the pipeline already skips absent
fields, and the client already omits diff rows whose candidate value is empty, so an unnamed
issue now leaves any existing title alone. displayTitle still labels those candidates in the UI.

Comic titles no longer parse as "<series> #<issue>", which is how ComicVineProvider decided
between its precise volume-plus-issue search and the far looser general issue search. Carry
seriesName and seriesIndex on MetadataSearchParams and prefer them when the title does not
parse, so a refresh or a re-search of an already-matched comic keeps the structured path. A
title that does parse still wins, since that is a user typing an explicit target.

Also apply the display title in the diff panel's within-provider result picker, which renders
only when a provider returned several candidates and so needs the issue number most; match the
skip patterns against the same string that gets scored rather than title alone; and drop the
redundant cover alt text on the result card, where the headline already names the result.

---------

Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
2026-08-01 22:11:42 -06:00
neon 02debbe543 feat(reader): increase font file size limit to 50MB for CJK fonts
Increase MAX_FONT_FILE_SIZE to 50MB to support large CJK and non-alphabetic fonts. Implement a lightweight binary font metadata parser to replace opentype.js buffer parsing, and update reader custom font composables to lazily manage blob URLs per font family.

Closes #793
2026-07-31 23:37:32 +00:00
neon a0bc01ea50 fix(client): remove Hungarian and Korean locales
Both catalogs held nothing but English fallback values, so selecting either language produced a
fully English interface that reads as broken rather than untranslated. Crowdin Translate covers only
24 languages and includes neither, so neither locale had any machine translation route, and no human
translations existed yet.

Drop them from the shared locale list, the Crowdin export configuration, the pull request allowlist,
the catalogs, and the tests, leaving fifteen target languages. Remove both from the Crowdin project
as well, after confirming each export was byte-identical to the English source so no translation work
was discarded.

Re-adding either is cheap once a translation route exists, whether that is a machine translation
engine that supports them or a volunteer translator.

Give the locale tests the same fallback semantics the application runs with. Production always loads
the English catalog and points `fallbackLocale` at it, so a key a target catalog does not carry
renders English. The test instances omitted both, which resolves a missing key to its own name, and
several assertions compared a translated label against the English one without checking that the
language actually translates that key. Both patterns pass only while every catalog is complete, and
would fail confusingly the first time a language lags behind a new key.

Delete the Crowdin service branch automatically once its pull request closes or merges. Crowdin
appends every export to the same branch and never rebases it, so a branch that outlives its pull
request keeps its original merge base and each later export diverges further from main until every
catalog conflicts. Deleting it forces Crowdin to recreate the branch from the current main.

Ref #576
2026-07-31 22:49:41 +00:00
neon 1215a45f93 feat(client): add nine locales with English fallback catalogs
Register Czech, Danish, Finnish, Hungarian, Korean, Russian, Simplified Chinese, Swedish, and
Ukrainian so the language picker, browser detection, and server preference validation accept them.
Catalogs hold English fallback values; Crowdin supplies translations through the normal export flow.

Give each catalog exactly the CLDR plural categories its locale defines rather than copying the
English branches. Czech, Russian, and Ukrainian gain `few` and `many` seeded from `other` because
the source has no matching text. Korean and Chinese drop `one`, which they have no rule for and
Crowdin cannot store. Those synthesized branches all start from the same English string, so machine
translation returns identical forms where the grammar differs and a human has to resolve them.

Ship Chinese as Simplified only under the bare `zh` locale so every zh-* browser resolves to it.
Traditional Chinese, also requested in #86, would arrive later as a separate zh-Hant catalog.

Use `sv-SE` for Swedish in the Crowdin config, because Crowdin has no bare `sv` language and would
have skipped the export. Tolerate a wrapped SUPPORTED_LOCALES array in validate-locales, which
prettier now produces since the list outgrew the print width, and extend the exhaustive ICU test to
reach Polish `other`, a category its counts never selected.

Add the repository activity embed to the README alongside the existing translation progress chart.

Repair the KOReader plugin source assertions that 40e366a4 left stale after reshaping the
dashboard streak block and the downloaded badge icon lookup, and watch `koreader-plugin/**` in
the server CI path filter so plugin-only changes run the server suite that reads those files.

Closes #86
Closes #363
Closes #576
2026-07-31 19:33:33 +00:00
Gideon VanRiette 4baad41ed1 fix(metadata): extract and write back comicvine id for comics (#842)
* fix(metadata): extract and write back comicvine id for comics

ComicVine references were never read from a comic's embedded
ComicInfo.xml on import, even though every other provider id
(Goodreads, Amazon, Hardcover, Google Books, OpenLibrary, Kobo)
already round-trips through the same <Web>/Notes fields. This
affected Book Dock import, direct upload, and library scanning
alike, since they share the same extractor. Write-back had the
same gap: a comicvineId resolved via search/auto-fill was never
persisted back into the file.

Closes #840

* fix(metadata): keep comicvine id off the contested Web slot and source-check notes

ComicInfo has a single Web element and buildComicInfoXml writes the first
provider id in COMIC_INFO_PROVIDER_ID_KEYS order into it. The other six
providers are also in COMIC_INFO_MANAGED_NOTES_KEYS, so losing that slot costs
them nothing; comicvineId had no second channel, so a write-back for a book
that also carried a goodreads/amazon/hardcover/google/openlibrary/kobo id
dropped the id from the file entirely and clobbered any ComicVine URL that was
already there. Add comicvineId to the managed notes keys, which also makes the
existing managed-note read precedence reachable from our own writes.

ComicTagger emits "[Issue ID N]" for whichever talker produced the tag, naming
the source separately as "using info from <source>". Matching the bare marker
attributed Metron and other non-ComicVine issue ids to ComicVine and wrote them
back out as fabricated comicvine.gamespot.com URLs. Require the Comic Vine
source in the same sentence.

Also anchor the Web URL scan to a path segment so a segment ending in 4000-,
such as "14000-777", is no longer read as an issue id.

---------

Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
2026-07-30 18:31:27 -06:00
neon 40e366a463 feat(koreader): add dashboard reading statistics and recency metadata
Add lastReadAt timestamp to KOReader catalog book items and introduce reading streak and goal statistics widgets to the KOReader plugin dashboard layout.
2026-07-31 00:25:09 +00:00
neon 50ba5f151b feat(books): allow sorting by custom metadata fields
Extend book sorting to support custom metadata field references (custom:<id>). Join custom metadata values in BookSortBuilder based on field data type, resolve active field types in BookService, and expose custom fields in BookSortBuilder UI.

Closes #829
2026-07-31 00:25:09 +00:00
neon 75a7cd58b2 fix(smart-scope): enable per-user kobo sync for shared smart scopes
Store per-user Kobo sync subscriptions for shared smart scopes in a new database table. This allows non-owner users to independently toggle Kobo sync on public smart scopes without modifying the owner's settings.

Closes #795
2026-07-28 21:28:12 +00:00
Chris Mansell 12ca966a79 i18n(client): add Spanish, French, Italian, and Polish locales (#811)
* i18n(client): add Spanish, French, Italian, and Polish locales

Add locale catalogs for Spanish, French, Italian, and Polish. Update locale types, Crowdin configuration, documentation, and test suites to support the expanded language options.

* i18n(client): sync Spanish, French, Italian, and Polish locale keys

Update Spanish, French, Italian, and Polish translation catalogs to include key additions for notification preferences, user management, and file naming settings.

* fix(client): correct expanded locale translations
2026-07-28 11:37:43 -06:00
neon 515f7b89e9 feat(client): redesign sidebar navigation, settings, and user management
Rework the app shell around a customizable sidebar: reorderable sections, sidebar zones, entity
popovers, and per-user preference persistence, backed by a dedicated browse counts endpoint so
entity totals stay cheap on large libraries.

Redesign the file naming settings with pattern fields, a token helper sheet, and a debounced live
preview. Rebuild the admin users table with server-side pagination, search, and filter DTOs, status
pills, confirmation dialogs, and magic link actions. Group notification preferences by category and
refine the account and privacy settings layouts.

Update theme tokens, icon references, and template event handler bindings across client features for
consistency, extend all locale catalogs, and add unit coverage for the new composables, components,
and server modules.
2026-07-28 16:12:36 +00:00
neon d0047bbf18 feat(library): add FB2 file metadata writing and library settings
Add server support for writing metadata, embedded covers, and FictionBook XML structures directly into FB2 format files. Introduce migration 0058 to persist FB2 file write preferences per library, and update library creator UI and locale catalogs.
2026-07-28 06:50:37 +00:00
neon f7716bdbdb feat(library): add MOBI file metadata writing and Kindle library settings
Add server support for writing metadata, covers, and EXTH records directly into MOBI/AZW format files. Introduce migration 0057 to persist Kindle file write preferences per library, and update the library creation UI and locale catalogs.
2026-07-28 05:48:15 +00:00
neon dbb10b5c2f feat(koreader): add bookmark sync and exchange support
Add two-way bookmark sync support between KOReader plugin and server. Include migration 0056 for bookmark sync columns, server endpoints and services for bookmark exchange, and KOReader plugin bookmark management with comprehensive unit and E2E tests.
2026-07-28 04:34:46 +00:00
neon e0ca660f2e feat(koreader): support configurable dashboard sections in KOReader catalog
Add server endpoints and KOReader plugin settings to render configurable dashboard section rows, such as Smart Scopes or custom scrollers, replacing the hardcoded Discover section. Update the plugin capability checks to fall back gracefully on older server versions.
2026-07-28 02:52:07 +00:00
neon edc2a2ccfc feat(reader): make KOReader plugin sync non-blocking and resumable
Lifecycle handlers previously blocked on network work, so closing or suspending a book stalled the
device on slow or offline links. Persist snapshots first and drain them through a lifecycle outbox
with connected-state gates and phase-aware retries, so the UI stays responsive and pending work
survives restarts.

Move persistence behind a state manager with atomic flushes, and chunk sweeps, bulk downloads, and
annotation exchange through a background queue with checkpoints and transfer policies, so large
libraries make progress incrementally instead of restarting from scratch after an interruption.

Server side, add bulk manifest and page stats endpoints with batched queries and catalog query
filtering so the plugin can reconcile many books per round trip rather than one request per book.
2026-07-28 02:06:50 +00:00
neon 20561c28df feat(audit): add detailed entry metadata and filtering to audit logs
Expand audit log entries to record detailed resource metadata (such as book titles on deletion), and add rich filtering by action, actor, and resource in the audit log viewer UI.

Closes #747
2026-07-27 05:42:38 +00:00
neon e850e929d9 feat(reader): add configurable spread gap for two-page comic view
Allow users to adjust the gap between pages when reading comics in two-page spread mode. Include backend preference validation, shared settings defaults, translation keys, and reader settings UI controls.

Closes #740
2026-07-27 04:28:22 +00:00
neon c0bd9ecf69 fix(smart-scope): handle invalid filters gracefully in count calculation
Validate smart scope filter rules before constructing query clauses during count computation. Catch validation errors for broken rules so invalid filters return null for bookCount rather than triggering an internal server error when fetching all smart scopes.

Closes #787
2026-07-26 21:08:51 +00:00
neon 58c5b422d3 feat(scanner): align bookdock duplicate detection with upload behavior
Replace metadata-based duplicate checks (ISBN, title/author) during BookDock finalization with destination path lookup. This allows books with shared or incorrect metadata to be imported whenever their resolved library target paths differ, matching the standard upload workflow.

Closes #736
2026-07-22 20:06:49 +00:00
neon 596284c389 feat(settings): expand accent color palette options
Add vivid and pastel accent color choices across client themes, user preferences, and shared types.
2026-07-22 16:47:06 +00:00
neon 544f4d5e53 fix(books): respect library cover aspect ratio for combined media books
Books containing both ebook and audiobook formats were incorrectly rendered with square covers in dashboard widgets, series views, and cards because aspect ratio logic checked for any audio format file instead of relying on the configured cover aspect ratio or primary media profile.

Closes #712
2026-07-22 04:12:13 +00:00
neon 06286d49b1 fix(metadata): persist hardcover and open library ids during book dock import
Pass external provider IDs (including Hardcover and Open Library IDs) from book dock metadata selection through ingest and finalization so they are saved to book metadata on import.

Closes #704
2026-07-16 05:45:36 +00:00
neon 095b347aff feat(books): add jump rail display preference
Let readers disable jump rails from grid display controls and skip bucket queries while the preference is off.
2026-07-15 22:32:59 -06:00
neon 504c9ce0e7 feat(books): expand and refine library jump navigation
Extend jump rails across alphabetical, temporal, and categorical sorts.

Keep large libraries responsive with bounded database buckets and exact offsets.

Refine active-state tracking, Unicode initials, localized labels, accessibility, adaptive sizing, and flat or collapsed-series navigation.
2026-07-15 22:32:59 -06:00
neon 5923ea95a5 feat(books): add adaptive temporal jump rail
Add bounded, user-scoped temporal buckets for supported date sorts and adapt resolution to populated dates. Keep rail navigation responsive with viewport sizing, sparse-gap compression, and virtual-grid synchronization.
2026-07-15 22:32:58 -06:00
neon f6394a9b0c security: remediate dependency and code scanning alerts
Upgrade vulnerable transitive dependencies and harden filesystem, URL, HTML, cryptographic, and input-processing boundaries identified by security scans.
2026-07-15 18:42:07 +00:00
neon 876e32654a feat(client): support system theme mode
Add system preference option for appearance theme with auto-switching based on OS color scheme settings. Update default theme preference to system and listen for OS dark mode changes dynamically.

Closes #695
2026-07-15 15:22:15 +00:00
neon ed5dbc9507 feat(books): add duplicate book detection and resolution tools
Introduce backend duplicate scanning services, persistence schemas, and frontend view components to allow users to scan their library for duplicate books by title/author similarity, group potential matches, and bulk delete redundant duplicates safely.
2026-07-15 05:20:16 +00:00
zanivann 6544d23002 feat(client): add Portuguese language localization (#688)
Expose Portuguese through shared locale preferences and keep the catalog synchronized with current English messages. Use keyed interpolation so series headings follow each language’s natural word order.

Closes #673

Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
2026-07-14 11:53:16 -06:00
Jadehawk ec84703cef feat(koreader): enhance settings and plugin behavior (#669)
* feat(koreader): add device file naming and improve plugin downloads (BO-658)

* fix(koreader): secure download paths and test collisions

* fix(koreader): preserve bulk collision identities

* fix(koreader): improve device file naming validation and path generation

* fix(koreader): remove duplicate settings lookup and audit file naming changes

* fix(koreader): resolve remaining CodeRabbit review issues

* fix(koreader): validate book detail query and finalize file naming release

validate book detail deviceId via DTO and ValidationPipe

localize new KOReader file naming settings (de, nl, sl)

bump KOReader plugin version to 1.3.0

* chore: rerun CI

* chore: normalized Closes #658 to not have embedded URL to issue. This is failing PR Conventions test

* test(koreader): align source and device removal assertions

* test(koreader): cover file naming settings flows

* test(koreader): cover file naming sync requests

* test(koreader): close remaining patch coverage gaps

* test(koreader): cover file naming server paths

* docs: add fixed2 modifier to file naming reference

* feat(koreader): support custom file naming patterns for KOReader sync

Add support for configurable KOReader file naming patterns, allowing users to define dynamic path templates for sync targets. Include UI draft settings, preview updates, and server path resolution.

Closes #658

---------

Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Co-authored-by: jadehawk <jadehawk@hotmail.com>
2026-07-14 11:26:27 -06:00
neon 214d887dd1 fix(authors): allow closing author enrichment progress notification
Ensure author enrichment progress notifications can be dismissed and resolve stuck status indicators that persist on screen after completion or matching failures.

Closes #678
2026-07-13 19:39:18 +00:00
neon 45e0bed6e3 fix(metadata): fallback to file name when book dock title is missing
Use the cleaned file stem as the search title when searching for book dock metadata if no title is present in embedded or selected metadata.

Closes #655
2026-07-13 05:40:50 +00:00
Chris Mansell a7c281955a feat(stats): expose privacy-aware reading insights (#662)
* build(deps): synchronize i18n lockfile

Record the locale dependencies so frozen installs can resolve the client workspace.

* feat(stats): expose privacy-aware reading insights

- Give administrators permission-gated access to bounded account activity.
- Let readers opt into auditable shared reading insights.

Ref #501
2026-07-12 23:11:35 -06:00
Dennis van der Stelt 31bda18c9a feat(i18n): add multi-language support with Dutch translation (#500)
* feat(i18n): add vue-i18n foundation with language switcher and Dutch locale

Introduce multi-language support for the client using vue-i18n and
@intlify/unplugin-vue-i18n. Locale is detected from a stored preference
or the browser language and persisted client-first, with optional server
sync riding the existing theme-sync mechanism (new locale category on the
generic user_preferences store, no migration needed).

Adds a language selector in Appearance > Behavior settings and seeds
en/nl message catalogs. Extraction of existing hardcoded strings follows
in subsequent commits.

* feat(i18n): add full en/nl message catalogs and test/build wiring

Populate the English and Dutch message catalogs (~4300 keys) covering the
whole UI, register a vitest setup that installs vue-i18n globally so
component tests resolve real messages, and allow inline HTML in messages
(strictMessage/escapeHtml off) for notes rendered via v-html / <i18n-t>.

* feat(i18n): localize settings and book UI strings

Replace hardcoded strings with t() calls across the settings and book
feature areas (including book detail, table, metadata edit, reading log,
highlights). Label collections captured at setup are wrapped in computed()
so they react to locale changes.

* feat(i18n): localize remaining feature UI strings

Replace hardcoded strings with t() across reader, email, library, tools,
migration, dashboard, auth, hardcover, author, series, book-dock,
book-metadata-fetch, annotations, admin, statistics, collection,
smart-scope, audit, whats-new, achievements, notifications, custom-icons,
scanner and metadata-score. Reactive label maps use computed().

* feat(i18n): localize shared components and view wrappers

Replace hardcoded strings with t() across shared components (app header,
sidebar, view header, pickers, selection bar) and top-level views.

* feat(i18n): localize settings tab bars and browser page titles

Make settings tab labels reactive via t() in the consuming components and
slim the *-tabs.ts helpers to ids/order/permission only. Route the browser
page titles in the router through the app i18n instance so document.title
follows the active locale. Adds settings.<area>.tabs.* and titles.* keys.

* feat(i18n): localize KOReader hash-linking and reader sidebar pin

Localize the strings from the KOReader manual hash-linking feature and the
reader sidebar pin/close controls that were merged from main during the
rebase, adding the corresponding en/nl keys.

* feat(i18n): localize upstream features merged during rebase onto main

Add en/nl catalog keys for the new upstream functionality pulled in by the
rebase (Kobo activity log, KOReader dismiss/remove-device and hash-link
modal, default library access, upload-size settings) and update a few
existing keys whose wording changed upstream (dynamic upload-size hints,
write-and-rename phrasing).

* feat(i18n): switch language picker to a dropdown

Replace the segmented language buttons with a native select, which scales
better as more locales are added.

* feat(i18n): add German and Slovenian translations

Register de and sl in the supported locales and ship full de.json and
sl.json catalogs (4616 keys each, structural parity with en.json).

* fix(client): harden locale switching and translation coverage

Keep locale catalogs structurally aligned, localize rebased UI changes, and use locale-aware formatting. Move language selection into the header, persist authenticated preferences, and prevent stale locale loads from winning.

---------

Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
2026-07-12 15:07:29 -06:00
neon f68a3ca98e feat(stats): add reading attempt history
Track repeat reads as distinct attempts while preserving existing status flows, device integrations, goals, sessions, and legacy data.

Closes #652
2026-07-12 17:41:35 +00:00
neon eac0917d22 fix(metadata): improve Libro.fm metadata integration
Persist Libro.fm identifiers across metadata refresh, editing, file round trips, and provider links. Enable explicitly selected audiobook providers during Auto-fill and retain AudNexus ASINs as Audible identifiers.
2026-07-12 07:06:42 +00:00
neon 5e03f92037 fix(annotations): open non-epub annotations in correct reader
Retrieve the book file format when querying the annotation hub, and dynamically determine the router parameters for navigation. This prevents hardcoding the epub format and correctly routes fb2 and other non-epub format annotations to their respective readers.

Closes #634
2026-07-12 06:19:42 +00:00
neon 37d3df4089 feat(metadata): add Libro.fm as a metadata source
Introduce a metadata provider for Libro.fm to fetch audiobook details, offering an alternative to Audible and iTunes. Add server-side mapping, fetch controller endpoints, validation DTOs, and client-side UI configurations for user metadata preferences.

Closes #636
2026-07-12 05:53:45 +00:00
neon 870abecb01 feat(reader): polish the PDF reading experience
Make long PDF reading sessions easier to navigate across desktop and mobile. Keep document loading recoverable when EmbedPDF initializes or tears down out of order, without enabling document-editing capabilities.
2026-07-11 15:29:34 +00:00
neon 56e0a554f3 feat(settings): allow users to disable achievements and gamification
Introduce user-specific settings to toggle achievements. Avoid evaluation
and awarding of achievements on the backend when disabled. Render a toggle
in the Account Settings interface on the frontend.

Closes #614
2026-07-10 04:24:26 +00:00
neon 910ce5a92e feat(smart-scope): add toggle to collapse series in smart scopes
Provide a series-collapse toggle in the Smart Scopes view to reduce clutter from large series. Use the existing series collapse preferences logic to allow users to toggle this setting per Smart Scope, maintaining consistency with Library and Collection views.

Closes #609
2026-07-09 22:20:54 +00:00
neon 0f37e197ad fix(books): clear reading progress and history when resetting book
Provide a new endpoint and user interface to reset the reading state of a book. Delete all related reading sessions, progress, audiobook progress, and KOReader progress. Clear lifecycle dates when setting the book status back to unread or want to read, preventing the book from persisting on the continue reading shelf.

Closes #606
2026-07-09 21:54:18 +00:00
neon 5541e1acdc fix(kobo): track library snapshots and sync progress per device
Migrate the snapshot and snapshot books tables to be per-device instead
of per-user. This resolves a sync race condition where only the first
registered Kobo device for a user receives the library delta, leaving the
others empty.

Closes #314
2026-07-09 20:57:02 +00:00
Chris Mansell 729b221427 feat(metadata): add full publication date support (#598)
- Add a persisted publication date while continuing to derive publication years.

- Thread full dates through metadata import, editing, filters, sorting, write-back, and sync payloads.

- Cover provider parsing, DTO validation, jump buckets, and date formatting with tests.

Closes #596
2026-07-09 08:17:53 -06:00
neon 7154a6c0d3 feat(library): allow discarding duplicate books from finalize dialog
Provide an action in the Book Dock finalize dialog to discard files that
are identified as duplicate candidates. This saves users from having to close
the finalize window, re-select the duplicates in the main queue, and manually
discard them.

Closes #591
2026-07-09 04:01:11 +00:00
neon 7e66f61dae feat(library): allow pausing book dock import task
Introduce a control mechanism to pause and resume the Book Dock import task.
Suspend resource-intensive background file ingestion and metadata finalization
to avoid system overhead or database locks during large-scale operations.

Closes #593
2026-07-09 02:22:09 +00:00
blinkidy ec4e5e2fc4 feat(storygraph): add StoryGraph sync integration (#568)
* feat(storygraph): add StoryGraph sync integration

Sync reading status and progress from BookOrbit to StoryGraph using
session-cookie authentication. Includes book matching with manual
rematch and edition selection, per-user sync toggles, a throttled
request queue, a storygraph_sync permission, and a StoryGraph
settings page.

Closes #274

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vi6e8ZJRAGMmRSmSj9sWXR

* fix(storygraph): address automated review findings

- clear stale cookie validation badge when inputs change
- reject saves with only one cookie value instead of silently dropping it
- handle linked-books load failures with a retryable error state so post-action
  reloads cannot misattribute failures to the action
- add a 30s request timeout so a hung StoryGraph response cannot stall a user's
  sync queue indefinitely
- reserve throttle slots synchronously so concurrent requests serialize
- reject empty cookie strings that would overwrite stored credentials
- report the retry response status when the rereading fallback fails
- add component specs for the connection card and linked books, plus regression
  tests for the throttle, settings, and retry-status fixes

* test(storygraph): fully restore setTimeout spies between queue tests

* feat(storygraph): add sync-scope option

Adds the book sync scope selector (all eligible books vs selected books
only) and the per-book StoryGraph sync control on the book details page,
mirroring the Hardcover integration's controls. All books with a syncable
status are now eligible — the previous connected-at cutoff that skipped
books finished before connecting is removed in favor of explicit scope
and per-book selection.

* fix(storygraph): stream live sync progress and surface failed books

The manual sync progress stream emitted the same mutated status object,
so distinctUntilChanged dropped every update after the first and the bar
never moved. Progress snapshots are now immutable and track processed
books (synced + skipped + failed), so the bar advances for every book.

Adds live synced/skipped/failed counters, a persistent last-run summary
from a terminal completed/cancelled event, and a failed-books list under
Manual sync backed by GET /storygraph/sync/failures. Details-page sync
state now refetches when the global sync scope changes.

(cherry picked from commit 075eeaa0c8678d4ebbba5dcce7aa15df42d64f51)

* feat(storygraph): show distinguishing identifiers in the editions list

Edition rows previously showed only format, pages, and language, making
identical-looking editions impossible to tell apart. The editions parser
now also scrapes ISBN, publisher, publication date, and cover image when
present, and each row shows the edition title, those identifiers, a cover
thumbnail, and the StoryGraph edition id linking to the edition page for
verification before switching.

(cherry picked from commit 09ec111613e9be149b556f64eb242cd3d82b296c)

* fix(storygraph): auto-refresh linked books after sync and add unmatched hint

The Linked books list only loaded on mount, so a book linked during a manual
sync run stayed on 'Not linked yet' until a manual page refresh. It now
silently reloads when a run completes (no spinner flash, keeps expanded rows),
guarded by a request id so overlapping reloads can't resolve out of order.
Books StoryGraph couldn't match automatically now show a short hint pointing
at 'Try auto-match' or pasting the StoryGraph URL.

(cherry picked from commit 1d0cfa1e06f3e211258620f7ad5e61373df7a310)

* feat(storygraph): compact edition link and wrap identifiers on mobile

The editions list showed the raw StoryGraph edition UUID as a long
monospace hyperlink, which was hard to read and cluttered the row. Replace
it with a small icon-only 'Open on StoryGraph' button (aria-labelled) next
to 'Use this', and let the publisher/date/ISBN line wrap instead of
truncating so the ISBN is no longer cut off on narrow/mobile screens.

(cherry picked from commit cdf4d64063534adeea9daafad11cd58a0cbc9a00)

* fix(storygraph): re-sync books re-added after being marked unread, clarify labels

A book synced while 'read' then marked 'unread' left its synced markers in
place, so re-marking it read compared against a stale lastSyncedStatus and was
silently skipped (shown as Linked but never re-pushed) until 'Try auto-match'.
On the unread transition the synced markers are now cleared while keeping the
match, so re-adding the book re-syncs.

Also present match methods with real casing (ISBN/Title/Cached/Manual) and
clarify that the manual-link field takes a StoryGraph URL or book ID (the code
after /books/), not an ISBN.

(cherry picked from commit 771e7e4fe793afbc30800717ae1c017ca307e5e1)

* refactor(storygraph): compare skippedBooks explicitly in sync-status dedup

The active-sync stream's equality check omitted skippedBooks, relying on
processedBooks (synced+skipped+failed) to capture skip-only changes. That is
correct but non-obvious; comparing every counter explicitly makes the intent
clear. No behavior change.

(cherry picked from commit f267088939f0d2493caadab8eb90f24ab4b28c0b)

* fix(storygraph): encode manually-entered book id in the lookup path

The manual-link id can be pasted directly, so a stray ?, #, or / would change
which /books/ URL is requested. Encode it before interpolating; a bad id now
404s cleanly instead of being stored.

* style(storygraph): fix prettier formatting and remove em dashes

The CI Lint (Client) job runs prettier --check, which flagged
StorygraphLinkedBooks.vue and StorygraphSyncProgress.vue. Reflow both
to prettier style and replace em dashes in UI strings and comments with
regular punctuation per project convention.

* fix(storygraph): harden sync access and batching

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
2026-07-08 17:15:17 -06:00
Oscar Nogueira Neto 4eed87a6fd feat(readwise): sync highlights to Readwise (#578)
* feat(types): add readwise_sync permission

* feat(types): add readwise settings types

* feat(db): add readwise_user_settings table

* feat(readwise): add constants

* feat(readwise): add throttle queue

* feat(readwise): add REST client

* feat(readwise): add repository

* feat(readwise): add settings service

* feat(readwise): add sync service with watermark

* refactor(readwise): drop unused logger in settings service

* feat(readwise): add debounced scheduler

* feat(readwise): add event listener

* feat(readwise): add controller and dtos

* feat(readwise): wire module into app

* test(readwise): add readwise_sync to authorization matrix

* feat(annotation): emit annotation.created on device sync ingest

* feat(client): add readwise api client

* feat(client): add useReadwiseSettings composable

* feat(client): add Readwise settings page

* feat(client): add Readwise settings route

* feat(client): add Readwise to settings nav

* fix(readwise): avoid typeof this in settings type annotation

Production build (nest build → tsc via tsconfig.build.json) rejects
`Parameters<typeof this.repo.upsertSettings>[1]` with TS2304. Reference
the repository class type directly so the prod image compiles.

* fix(client): add Readwise sync to admin permission list

The admin UserFormDrawer renders a hardcoded permission list, not an
iteration of PERMISSION_LABELS, so readwise_sync was ungrantable via UI.
Add it to the Devices & Access group and the Standard preset, mirroring
Hardcover sync.

* feat(readwise): send book cover to Readwise via OpenLibrary ISBN

Populate the Readwise highlight image_url with the OpenLibrary cover URL
built from the book's isbn13 (falling back to isbn10). Uses ?default=false
so a missing cover yields 404 (no image) rather than a blank placeholder.
Books without an ISBN send no image_url. BookOrbit's own cover endpoint is
auth-gated and stores no public source URL, so a public third-party cover
URL is the zero-infra way to give Readwise an image.

* test(readwise): add type parameters to vi.fn mocks

oxlint (vitest/require-mock-type-parameters) fails on untyped vi.fn()
in the settings page test; the pre-commit hook only runs eslint, so
this surfaced under full lint:check.

* docs(readme): add Readwise Sync to features list

* fix(readwise): surface settings fetch errors in the UI

The settings page destructured error from useReadwiseSettings but never
rendered it. A failed fetch on mount left the user with an empty default
form and no indication that loading failed. Render an error banner when
error is set, and cover it with component tests.

* test(readwise): cover settings page token test, save, and visibility toggle

Adds component tests for the Test-connection flow (empty/valid/invalid
token), Save (success clears the field, failure surfaces an error toast),
and the show/hide token toggle — the interactive handlers Codecov flagged
as uncovered in ReadwiseSettings.vue.

* test(readwise): match settings buttons by exact label in test helper

Use an exact text match instead of substring matching in the testButton
helper so it can never return the wrong button if labels overlap.

* fix(readwise): harden highlight sync

---------

Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Co-authored-by: Oscar Nogueira Neto <oscarnogueira@gmail.com>
2026-07-07 19:09:28 -06:00
Bernard Potocki d82a018d1e feat(kobo): sync smart scopes to kobo (#570)
Allow smart scopes to opt into Kobo sync while preserving collection sync behavior. Generate the current Drizzle migration and keep smart-scope matching aligned with the normal book query path.

Fixes #202

Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Co-authored-by: Jane Doe <janedoe@users.noreply.github.com>
2026-07-07 15:55:08 -06:00