Commit Graph

875 Commits

Author SHA1 Message Date
neon 25bc7a21d4 perf(dashboard): optimize random book lookups and scroller batching
Add index on books table for random dashboard scroller queries, batch dashboard scroller requests, instrument pg pool logging, and cover dashboard scroller batching in e2e tests.
2026-08-05 21:48:23 +00:00
neon 854b1a75d1 feat(reader): redesign comic settings as one panel in two containers
Replace the three-tab settings dropdown with a single scrollable column
rendered in a desktop popover and a mobile bottom sheet. Group spread
controls contextually under two-page layout, render backgrounds as color
swatches, move spread gap onto a labelled slider, and disable the header
reset while the book carries no overrides. Persist through the panel
update handler so reset no longer re-records defaults as overrides.
Restructure reader.cbz locale keys across all catalogs.
2026-08-04 00:02:08 +00:00
neon 563086328d feat(series): track expected series length and surface library completeness
Record how many books a series holds from Hardcover, ComicVine, RanobeDB,
Amazon and embedded ComicInfo metadata, storing it on the series row. Show
an ownership meter beside read progress on series detail, and extend gap
detection to report entries past the highest owned index instead of stopping
there. Allow the total to be set by hand in the book metadata editor.

Automated sources only ever raise the stored total so the result does not
depend on scan order; the manual field is the way to lower or clear it.

Closes #801
2026-08-03 00:54:26 +00:00
neon 04ac778645 feat(reader): redesign ebook settings as one panel in two containers
Replace the three-tab dropdown with a single scrollable column ordered by how often each
setting is used. The tabs gave no clue where anything lived, so finding hyphenation or the
font picker meant hunting; one column makes the whole set discoverable in a single scan.

Move continuous values onto sliders. Stepping page width across its range took thirty
clicks, and the labels named the variable rather than the outcome, so page width now reads
Narrow through Full while still storing pixels. Text size keeps discrete buttons because it
is nudged constantly and steps are what that control wants.

Render each theme as a miniature page in its own colors. Thirteen near-identical dots gave
no way to tell Sepia from Ember without selecting one and looking at the result.

Swap the desktop container from DropdownMenu to Popover, since menu semantics capture the
arrow keys the new sliders need, and add a bottom sheet below the sm breakpoint so the page
stays visible while it is being tuned and the controls sit within thumb reach. Both render
the same panel component; only the wrapper changes.

Wire up resetBookSettings, which already existed with no caller, and disable the control
when the book carries no overrides.

Trim the built-in fonts to Book default, Serif, Sans-serif and Monospace. The named serifs
duplicated what the generic keyword already resolves to, and Palatino fell back
unpredictably away from macOS. Saved selections keep rendering because the settings
sanitizer accepts any font string; they simply no longer show a selected chip.

Move the theme names and built-in font labels into the locale catalogs. Both were hardcoded
English and so stayed untranslated in every language.
2026-08-03 00:31:19 +00:00
neon 72ed93d3c0 fix(kobo): stop logging non-BookOrbit content as annotation sync failures
Kobo devices route every annotation request through us, including content BookOrbit does not
own, so a content id that resolves to no book is expected traffic rather than a sync failure.
Recording it surfaced a FAILED entry in the device sync history for each such request.

Keep returning 404: the device reconciles annotations by omission, so answering with an empty
set would make it delete its local annotations for that content.

Closes #831
2026-08-02 22:53:38 +00:00
neon 57e2616aec feat(koreader): add not-on-device tile, bulk folder picker, browse counts
Add a "Not on device" Browse tile listing recently added books not yet
downloaded here. On-device state is local to KOReader and has no
server-side filter, so the list is assembled by a bounded walk of the
newest books, capped by both a page budget and the server's ids limit.
It takes the slot of the Search tile, which duplicated the title bar
search button present on every non-detail page.

Add a folder picker to the bulk download settings, sharing one chooser
with the single-book dialog so both write the same download_dir.

Badge every Browse tile the server can count. Authors and series reuse
the cached totals the web sidebar already requests; libraries,
collections, smart scopes and in-progress get dedicated count queries
rather than the list methods behind the section endpoints, which
aggregate per-entity book counts a badge never reads. Badges are
right-aligned and abbreviated past 999 so the column stays fixed.

Replace the shared count path that paged and hydrated a book to read a
single integer, removing five queries per count on the dashboard, the
series read-through summary and the section endpoints.

Closes #869
2026-08-02 22:36:44 +00:00
neon 4be84710bd feat(fonts): add admin-managed server fonts for the web reader
Adds a server_fonts collection an administrator curates under Settings >
Admin, offered to every reader alongside their own uploads. Scoped CSS
family prefixes keep a user font and a server font of the same name
independent. Readers can hide individual server families from their own
picker via a new server-fonts user preference.

Validation, metadata parsing and storage are shared with per-user fonts
through an extracted upload pipeline rather than duplicated.
2026-08-02 21:07:50 +00:00
neon 1f6d9527aa feat(smart-scope): allow filtering by custom metadata fields
Address custom metadata fields as custom:<id> rules, mirroring the
existing custom sort fields, and resolve the value column from the
operator so buildWhere stays synchronous for every caller.

Closes #855
2026-08-02 18:13:32 +00:00
neon 49e0b12e97 fix(koreader): allow plugin API to reset read status to unread
The catalog read-status enum omitted unread, so a device write could only
be undone from the web dashboard. Add it to the settable enum and the
plugin status dialog, and mark Unread as current when a book has no
status row.

Closes #853
2026-08-02 17:38:45 +00:00
neon f4cfc3ca5d feat(books): rework cross-library moves with preview and progress
Replaces the implementation from #818 with a job-backed flow. Moves are
planned up front and surfaced as a preview the user confirms, then run
through a planner/executor split against a book_move_jobs table, so
progress is trackable and partial failures roll back.

The dialog-based flow from #818 is superseded: its module, service, and
repository are removed here in favour of the job-based equivalents. Its
e2e coverage is replaced by a book-move suite that exercises preview
classification, the collision policies, file relocation, preserved user
state, and the books-before-book_files update ordering that the
composite folder foreign key constrains.

Also adds dashboard shelf layout configuration.

Ref #257
2026-08-02 17:24:18 +00:00
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 9262704f0a docs: update PR template and contributing guide review requirements 2026-08-01 18:22:56 +00: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 d26f6999b4 i18n(client): sync translations from Crowdin (#848)
* 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.

Ref #576

* i18n(client): sync French translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Spanish translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Czech translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Danish translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Finnish translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Italian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Dutch translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Polish translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Russian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Slovenian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Swedish translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Ukrainian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Chinese Simplified translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Portuguese, Brazilian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Ukrainian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.
2026-07-31 17:07:27 -06: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
dependabot[bot] c9c5df7a87 chore(deps): bump the github-actions group with 2 updates (#843)
Bumps the github-actions group with 2 updates: [docker/login-action](https://github.com/docker/login-action) and [actions/stale](https://github.com/actions/stale).


Updates `docker/login-action` from 4.5.0 to 4.5.2
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/06fb636fac595d6fb4b28a5dfcb21a6f5091859c...371161bbe7024a29a25c5e19bfcbc0804fe9ad2c)

Updates `actions/stale` from 10.4.0 to 11.0.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/1e223db275d687790206a7acac4d1a11bd6fe629...4391f3da665fdf50b6810c1a66712fb9ba21aa93)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/stale
  dependency-version: 11.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  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-07-31 11:51:36 -06:00
dependabot[bot] 2144bdb101 chore(deps): bump the production-dependencies group across 1 directory with 6 updates (#844)
Bumps the production-dependencies group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@lucide/vue](https://github.com/lucide-icons/lucide/tree/HEAD/packages/vue) | `1.24.0` | `1.27.0` |
| [@tiptap/extension-link](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-link) | `3.29.0` | `3.29.1` |
| [@tiptap/pm](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/pm) | `3.29.0` | `3.29.1` |
| [@tiptap/starter-kit](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/starter-kit) | `3.29.0` | `3.29.1` |
| [@tiptap/vue-3](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/vue-3) | `3.29.0` | `3.29.1` |
| [mysql2](https://github.com/sidorares/node-mysql2) | `3.23.1` | `3.23.2` |



Updates `@lucide/vue` from 1.24.0 to 1.27.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.27.0/packages/vue)

Updates `@tiptap/extension-link` from 3.29.0 to 3.29.1
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-link/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.1/packages/extension-link)

Updates `@tiptap/pm` from 3.29.0 to 3.29.1
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/pm/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.1/packages/pm)

Updates `@tiptap/starter-kit` from 3.29.0 to 3.29.1
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/starter-kit/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.1/packages/starter-kit)

Updates `@tiptap/vue-3` from 3.29.0 to 3.29.1
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/vue-3/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.1/packages/vue-3)

Updates `mysql2` from 3.23.1 to 3.23.2
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.23.1...v3.23.2)

---
updated-dependencies:
- dependency-name: "@lucide/vue"
  dependency-version: 1.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@tiptap/extension-link"
  dependency-version: 3.29.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@tiptap/pm"
  dependency-version: 3.29.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@tiptap/starter-kit"
  dependency-version: 3.29.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@tiptap/vue-3"
  dependency-version: 3.29.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: mysql2
  dependency-version: 3.23.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-31 11:51:16 -06:00
neon 68f38411b3 feat(client): improve shell responsive gutters and cover hover transforms
Add responsive shell content gutters to tokens CSS and fix cover stack hover transform calculations for scaled square covers in SeriesDetailView.
2026-07-31 01:53:37 +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 5cea03799e feat(koreader): refine catalog detail layout and widget height calculations
Improve KOReader plugin catalog detail view layout by auto-adjusting widget text heights, fixing series line resolution, and polishing star rating, progress button, and navigation button styling.
2026-07-31 00:25:09 +00: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 d9057fed3b i18n(client): sync translations from Crowdin (#837)
* i18n(client): sync French translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync German translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Slovenian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.
2026-07-30 14:12:17 -06:00
neon f30b998568 fix(reader): support ffi/archiver in KOReader plugin updater
KOReader removed Device:unpackArchive in recent builds in favor of ffi/archiver. Update BookOrbitUpdater to extract archives via ffi/archiver when present, fallback to Device:unpackArchive on older builds, and report an error instead of crashing if neither is available.

Closes #827
2026-07-30 19:59:10 +00:00
neon 1400a07b6f fix(books): include books without status record in unread readStatus filter
Books without an explicit user_book_status record are implicitly treated as unread across the app. Update BookQueryBuilder's readStatus filter handling so that querying for unread books also matches books lacking a status row.

Closes #815
2026-07-30 19:54:44 +00:00
neon e7ccf6648c i18n(client): translate book filter, operator, and sort field labels
Replace hardcoded English filter, operator, and sort field labels with dynamic i18n lookup helpers that resolve against the active locale catalog.

Closes #807
2026-07-30 19:51:13 +00:00
neon 624dce34a8 feat(smart-scope): allow sharing smart scope after creation
Expose the "Visible to all users" toggle in the smart scope editor panel and allow updating the isPublic flag on existing smart scopes via the backend API.

Closes #805
2026-07-30 19:43:34 +00:00
Jadehawk 1f4420b454 feat(koreader): support customizable dashboard sections (#820)
* feat(koreader): add dashboard row configuration model

* feat(koreader): reuse catalog for dashboard source selection

* feat(koreader): configure dashboard rows on 1.4.0

* chore(koreader): preserve dashboard source line endings

* fix(koreader): keep BookOrbit tools menu loadable

* fix(koreader): keep dashboard requests out of rendering

* revert(koreader): restore stock 1.4.0 dashboard renderer

* revert(koreader): use stock single dashboard row

* fix(koreader): apply selected dashboard row source

* feat(koreader): add second dashboard row setting

* feat(koreader): render second dashboard row

* fix(koreader): fetch configured dashboard rows directly

* fix(koreader): honor configured dashboard row sources

* feat(koreader): support four configurable dashboard sections

* fix(koreader): use real catalog dashboard sources

* fix(koreader): select dashboard shelves from catalog

* fix(koreader): reuse catalog for dashboard source selection

* fix(koreader): close dashboard menu before catalog selection

* test(koreader): split dashboard configuration coverage

* chore(koreader): bump version to 1.5.0

* fix(koreader): address dashboard review feedback

* fix(koreader): address dashboard section review feedback

* test(koreader): cover dashboard behavior at runtime

* test(koreader): use Nest setup for dashboard DTO

* chore(ci): trigger pipeline

* fix(koreader): migrate untagged dashboard section settings

Plugin 1.4.x stored the configurable dashboard rows as a bare ordered
list with no schema marker, so gating the migration on schemaVersion == 1
never matched anything a device had actually written. Every upgrading
user who had ever picked a row was read positionally instead: the Stats
strip disappeared and Discover ended up in two slots, with a stored
SmartScope dropped entirely.

Treat an untagged stored list as the legacy shape and migrate it into the
slots those rows used to occupy. Cover it with the settings shapes 1.4.x
really wrote, rather than a marker nothing ever produced.

Also drop store() and primary(), which no caller uses now that slots are
written through storeAt(), along with the dead grow loop inside storeAt()
that normalize() already makes unreachable.

* fix(koreader): keep dashboard shelves in aspect, inside the page, and deduped

The four-slot renderer handed each shelf a height derived from the
leftover vertical budget while its cover cards kept the width computed
from the natural row, so any page with more than one shelf drew covers
out of aspect. It also had no answer for a page too small for the
configured slots: shelves clamped to a 48px floor and the layout simply
overflowed the available height.

Size shelves from their cards instead. A shelf that does not fit gets
more, narrower cards per row, so its height is always the height those
cards need. When even the narrowest row does not fit, drop slots the way
the pre-four-slot renderer did: the stats strip first, then shelves from
the bottom up.

Fetch each distinct shelf source once per dashboard load. Two slots
showing Discover previously rendered the same list twice, and two slots
on the same catalog filter each paid for their own request; a source that
fails is now also not retried by the next slot that wants it.

Drop what the four-slot rewrite left behind: the unused capabilities
require, the tall-layout helper and its constants, and the section
accessors and request helper that no longer have callers. Name the
per-shelf page size and the minimum cover width instead of repeating the
literals. Add setDashboardSections() so a whole configuration can be
applied in one refresh.

Add a layout spec that drives updateDashboardItems and asserts covers
stay in aspect, the drop order holds, and nothing is laid out past the
available height.

* fix(koreader): close the dashboard slot menu through its own API

Reset to Default wrote section_stale as a boolean where the renderer
reads a per-slot table, so the reset never actually marked anything
pending and stale books stayed on screen under the new headers until the
refresh landed. It also dereferenced current_context without a guard and
wrote the setting behind the catalog's back. Route it through the new
setDashboardSections() so the slots and the cache signature move
together in one refresh.

Close the invoking touch menu with closeMenu() rather than handing the
widget to UIManager:close(), which skips the close_callback that owns its
container.

Drop the section parameter from catalogDashboard() and the unused
catalogDashboardSection() helper: the plugin builds its shelves from the
ordinary catalog endpoints now and sends neither. The server keeps both
routes for devices still on 1.4.x, so its DTO test stays; rename it to
match the file it covers and drop the empty Nest module compile.

* revert(koreader): keep the plugin at 1.4.0

1.4.0 has not shipped yet: 1.3.0 is the last released plugin version, so
the dashboard slot work can land inside 1.4.0 rather than claiming a
version of its own. Devices in the field are on 1.3.0 and see a single
1.3.0 -> 1.4.0 step either way.

* fix(koreader): stop rotating upright read-status badges

buildReadStatusBadge turned every status icon a quarter turn, but only
two of the seven are corner folds. The dogear assets are drawn with their
right angle at the top left and the badge sits in the cover's top-right
corner, so those do need the turn. Bookmark, check and reload are upright
glyphs and were being drawn on their side: a want-to-read cover showed
its bookmark ribbon lying sideways like a chevron.

Rotate only the dogear icons.

---------

Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
2026-07-30 13:37:26 -06:00
neon c425b7949e security(deps): bump golang.org/x/text in kepubify to fix CVE-2026-56852
The bundled binaries embedded golang.org/x/text v0.37.0, where a norm.Iter can enter an
infinite loop on input containing invalid UTF-8 bytes. Trivy flags this as HIGH and fails
the PR image scan on every branch opened since the advisory was published.

Rebuild all six binaries from the pinned upstream commit with x/text v0.40.0. The Go
toolchain was already current at 1.26.5 and is unchanged, so a toolchain-only rebuild would
not have cleared the finding; the vulnerable code lives in a module dependency. x/sync moves
to v0.22.0 as a side effect of the same resolution.

Keep the reported version string at v4-dev so rendition hashes stay stable and existing
cached kepub conversions remain valid.
2026-07-30 17:59:59 +00:00
neon d6fad39edd i18n(client): sync translations from Crowdin (#808)
* i18n(client): sync Dutch translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Slovenian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Portuguese, Brazilian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Dutch translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Slovenian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Portuguese, Brazilian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync German translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync French translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Spanish translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync German translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Italian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Dutch translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Polish translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Slovenian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Portuguese, Brazilian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync French translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Spanish translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync German translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Italian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Dutch translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Polish translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Slovenian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.

* i18n(client): sync Portuguese, Brazilian translations from Crowdin

Keep translated catalogs synchronized with reviewed Crowdin content.
2026-07-30 11:45:23 -06:00
neon db11f4eab9 fix(kobo): sync manual book read status updates to kobo devices
Project manual book status changes onto Kobo reading states and mark device snapshot rows as unsynced. This ensures manual status updates made in the web UI reach registered Kobo devices on the next sync without overwriting device bookmarks.

Closes #812
2026-07-28 22:24:15 +00:00
neon 648a6cacf2 fix(client): translate dashboard widget names in customization modal
Use vue-i18n locale keys for dashboard widget and shelf labels in the dashboard customization sheet and view. This resolves hardcoded English strings when viewing dashboard settings in non-English languages.

Closes #796
2026-07-28 21:43: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
neon c2884bd664 chore(translations): add catalog verification script and update localization docs 2026-07-28 19:15:15 +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 e4e20f89e3 feat(koreader): add catalog detail info rows and series navigation
Format and render series, publisher, publication date, language, ISBN, page count, and file information rows in the KOReader catalog detail view. Support tappable series navigation and format-aware download button labels with unit test coverage.
2026-07-28 04:35:08 +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 3aff8f521b chore(i18n): automate Crowdin translation progress SVG chart and consolidate repo chart workflows
Add automated Crowdin translation progress SVG chart generator, test, and workflow. Consolidate star history and translation stats generation into unified repo-charts workflow, and update README links.
2026-07-27 18:25:59 +00:00
neon 5f47145a64 docs: refresh dashboard screenshot and add KOReader plugin preview 2026-07-27 17:32:12 +00:00
neon e8ca5fceb2 ci: add automated star history chart
Keep README growth data self-hosted and resilient when detailed GitHub stargazer history is unavailable.
2026-07-27 16:10:46 +00:00
neon 5290e63375 refactor(metadata): await metadata score calculations on metadata changes
Ensure metadata score calculations are awaited when book metadata or covers are saved or updated, and add concurrency queuing for on-demand score recalculations.
2026-07-27 06:13:54 +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