Non-credential secrets rendered as input[type="password"], so every
settings tab click unmounted a populated password form during a
pushState navigation, matching Chrome's heuristic for a submitted login
form and popping the "Update password?" bubble.
Render them as masked text inputs instead, and guard the boundary with a
static scan that restricts input[type="password"] to BookOrbit login
forms.
Kobo sync replaces the store API for the device but only ever answered for
BookOrbit's own books, so anything owned through Kobo fell through the gaps.
Library sync never queried Kobo, so Kobo Plus and purchased titles had no route
to the device at all. Add an opt-in per-user store sync that pulls the device's
upstream entitlements with the device's own Kobo credentials, which still reach
us because initialization leaves device_auth and device_refresh pointing at
Kobo, and appends them to BookOrbit's own. Both cursors ride in one composite
sync token, so no server-side state is needed and tokens minted before this
change keep working. The store is only consulted once BookOrbit has drained its
own pages, and paging stops if Kobo's cursor fails to advance.
Everything on the store path stays inside one guarantee: reading the setting, or
a store that is slow, down, unauthorized or answering with a body we cannot use,
costs the store page and never the BookOrbit half of the sync.
Initialization also rewrites the image templates to point at BookOrbit, so the
device asked us for store covers too. Those were forwarded to storeapi.kobo.com,
which does not serve images, and 404d. Redirect unresolved cover ids to Kobo's
image CDN instead. This is not gated on store sync, since browsing the store
shows those covers whether or not the setting is on.
Deliberately left alone: annotation pulls still 404 for content that is not a
BookOrbit book. The device reconciles annotations by omission, so answering with
an empty set would wipe its local highlights.
Closes#789
A shelf rendered a single horizontal row, which wastes space on wide screens and shows very
few books on narrow ones. Each shelf now carries a row count of 1 to 3, set per shelf in the
dashboard settings drawer.
Books fill bands left to right rather than filling each column top to bottom. The cheaper
grid-auto-flow approach would put the second most recent book directly under the first, which
reads wrong on ordered shelves like Continue Reading and Up Next in Series.
Below the sm breakpoint the row count is capped at two, since only two covers fit across and a
third row costs a screen height to gain two books. A shelf fetches limit * rows books, clamped
to the limit the server already accepts, so no server behaviour changes. DASHBOARD_SCROLLER_MAX_LIMIT
moves into the shared types package so both sides read one value instead of drifting apart.
The row count is part of the shelf key because a shelf snapshots its fetch limit during setup;
without it, switching to three rows would re-flow the books fetched for one.
Also widen the settings drawer, which was pinned at 384px because SheetContent's own sm:max-w-sm
outranked the plain max-w override in tailwind-merge, put Shelves ahead of Widgets, and move the
customize control out of the floating layer into the greeting row where it is not missed.
Closes#928
Selecting inside a collapsed library was broken in every layout: a grid series card navigated
into the series instead of selecting, and a list or table series row selected only the
representative book rather than the series it stands for.
Suppress collapse for as long as select mode is active, so the list reloads uncollapsed and
every row is an individual, selectable book. The saved collapse preference is untouched and
comes back on exit. The toolbar toggle stays focusable while locked and explains itself
through its tooltip, since entering select mode visibly re-lays out the view.
Closes#963
The skeleton kept animate-pulse after the image landed, and opacity-0 could not hide it because a
CSS animation overrides the specified value. Every rendered cover ran an infinite opacity animation
behind an opaque image for the life of the page. Idle library view in Firefox 153: 109% CPU before,
0.0% after.
Closes#626
Book Dock was a single permission that the standard preset handed to every user, so an ordinary
account could pause processing, trigger a rescan, and finalize straight into the library. Keep
book_dock_access for personal upload and review, and move global control behind a new
manage_book_dock permission in the administration group.
Finalization now also requires library_upload, since it writes into a library. Per-file reads and
mutations are scoped to the uploader unless the caller can manage the dock, which closes the gap
that let any Book Dock user read, edit, or discard another user's entry by id. The summary
websocket now announces a change instead of broadcasting global counts, so each client refetches
its own scoped totals.
Closes#961
The admin preset selected every permission group, including the restrictions group whose only
member is demo_restricted. That permission is enforced with ForbidPermission, so holding it
removes capability instead of granting it: users created from the preset silently lost
notifications, account settings, bulk edits, bulk downloads and font management.
Move the groups into a testable module and mark the restrictions group as inverted so presets
skip it, while leaving the checkbox available for deliberate selection.
Fixes#953
Wire the existing allow_registration setting to an admin toggle in the
Users tab and a public /register page, so the documented self-registration
flow is reachable from the UI instead of the API alone. Expose the flag on
the public setup-status endpoint the router guard already calls, so the
sign-in page can offer sign-up without an extra request.
Harden the newly public path: return a conflict instead of a 500 when
concurrent signups race the unique index, sanitize the logged username,
and reject control, zero-width, and text-direction characters in the
username and display name so a new account cannot impersonate another in
the user list or audit log.
Closes#952
The membership row used a fixed grid whose non-name columns take about 326px, so the
minmax(0,1fr) name column shrank to zero in any container narrower than that. On phones,
and on tablets where the series field only gets half the form width, the input disappeared
behind the index and total boxes.
Lay the row out with flex wrapping instead: the name keeps a minimum width and the index,
total and row actions move to a second line when they no longer fit. Wide layouts render
exactly as before, and the reflow follows the container rather than a viewport breakpoint,
which is what the tablet case needs.
Closes#949
The lockout was checked before the password was verified, so a correct password on a locked
account returned the same generic invalid-credentials error as a wrong one. Users had no way to
tell the two apart, and the natural response was to keep retrying a password that was already
right.
Completing a password reset did not clear the lock either, so the one recovery path the UI
advertises appeared to fail for up to fifteen minutes after a successful reset.
Verify the password first and report the lockout, with the remaining wait, only when the supplied
password was correct. Someone guessing wrong passwords still gets the generic failure, so account
existence is not leaked. Clear the lock whenever a password is reset or changed, and give admins
an explicit unlock action for the case where waiting is not acceptable.
* feat(metadata): add hardcover edition selection and refresh pinning
Mirrors the StoryGraph edition picker for Hardcover, but backed by real
GraphQL edition data instead of scraping. An edition can now be chosen
either from the Hardcover settings page's new "Linked books" section or
from the metadata downloader/refresh flow, and once chosen it pins
future automatic metadata refreshes to that exact edition instead of
silently re-deriving one by ISBN every time.
book_metadata.hardcover_edition_id is shared across all users, while
hardcover_book_state.hardcover_edition_id is scoped per (user, book).
Cross-writing between them is one-directional and non-destructive per
flow: the settings page always updates the acting user's own sync
state and only back-fills the shared field if it is empty; the
metadata downloader always updates the shared field and only updates
the acting user's own sync state if they already have one. The
propagation uses the existing achievement event bus rather than a
direct module import, since HardcoverModule already depends on
BookModule.
* fix(metadata): address hardcover edition selection review findings
Responds to CodeRabbit review on PR #936:
- Surface Hardcover edition-lookup failures as errors instead of an
empty list, so a provider outage cannot be mistaken for "no
editions". Updates the client to throw and the UI to show a
retryable error state.
- Validate that a manually-picked edition id actually belongs to the
book's matched Hardcover book before persisting it, rejecting
mismatches with a 400 instead of trusting an unvalidated client
input.
- Require an exact safe-integer string before parsing a propagated
hardcoverEditionId, so a value like "200abc" cannot be silently
truncated into a valid-looking edition id.
- Add durationMs to the edition-propagation failure log per the
project's logging conventions.
- Honor a pinned edition in the ISBN search() path too, not only in
lookupById(), so a refresh that falls back to search still returns
the previously chosen edition when it's present.
* test(metadata): strengthen hardcover edition-pin fallback assertions
The fallback tests for search() and lookupById() only used a
single-edition fixture, so they would have passed even if the
edition-pin filtering never ran. Use two-edition fixtures and assert
on which edition is actually selected instead.
* i18n(hardcover): use i18n keys for Linked books strings
HardcoverLinkedBooks.vue hardcoded its UI text instead of using
useI18n like its sibling components, breaking the ability to
translate the Hardcover settings page and non-English catalogs.
* fix(hardcover): harden edition selection and bound the linked-book list
Validate a picked edition against the book it belongs to rather than against a
capped, unordered list. Editions past the cap stay selectable, and two calls
can no longer disagree about which window they saw.
- Order the edition query by id and fetch one row past the cap, so truncation
is detectable instead of silent. Verified against the live Hardcover API:
both "The Name of the Wind" and "Pride and Prejudice" exceed the 50 cap.
- Report truncation for the linked-book list and the edition list, and say so
in the UI rather than presenting a partial list as complete.
- Bound and order the linked-book query. It backed a settings list with the
unbounded sync query; sync callers keep their previous unordered behaviour.
- Throw for "not connected" and "not matched" instead of returning an empty
list, which the UI could not tell apart from a genuinely empty catalogue.
- Drop release_year from the edition query. Hardcover never populates it, so
it was dead weight in the payload.
- Add a tuned --success token. The status pill hardcoded text-green-600, which
is untuned in dark mode; the light value is set for 4.5:1 on card and page.
- Expose row expansion with aria-expanded and aria-controls, announce load
failures through a live region, and keep the edition list open after a
switch so the refreshed marker lands where the user is looking.
---------
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
A tab that slept past the 15 minute access-token lifetime woke up holding a token every consumer
then handed to the server independently. socket.io reconnected all seven namespaces with it, each
gateway rejected the handshake and closed the socket, and because a server-initiated close reports
`io server disconnect` the client never reconnects from it, so live updates stayed dead until the
page was reloaded. The dashboard widgets fetch once on mount with no retry, so whatever failed in
that window sat on "Failed to load" for the life of the page.
Make a valid token the thing consumers ask for rather than something they hope is still good.
Decode `exp` on the client and renew inside a 30 second skew before the token is used, so websocket
handshakes, uploads and `api()` requests no longer discover the expiry one 401 at a time. Route
every authenticated namespace through a shared factory that resolves the token per handshake and,
on an auth rejection, refreshes and reconnects with bounded exponential backoff.
Let the server say which rejections are worth retrying. Gateways now answer an authentication
failure with an `unauthorized` event before closing, and stay silent on a permission denial, which
a fresh token would not fix. Classifying that needed the gateways to stop throwing bare `Error` for
both cases, so the handshake paths now throw `UnauthorizedException` or `ForbiddenException` as the
project's conventions already require, and jsonwebtoken's own `TokenExpiredError` counts as an
authentication failure since it is the single most common way a slept-through handshake fails.
Collapse the twelve identical widget composables onto one loader that reloads a failed widget once
a rejected request is followed by a successful refresh.
Closes#942
Extract dominant color from book covers to apply ambient background tinting in BookDetailView, add cover tinting toggle to display settings, and test cover tint extraction.
validate-styles.mjs compared path.relative() output against POSIX-style
ALLOWED keys. On Windows path.relative() yields backslashes, so the lookup
never matched: the allowlisted views/NotFoundView.vue was reported as a
violation and its allowlist entry was simultaneously reported as unused.
That made verify:fast fail on a clean checkout on Windows, which in turn
made the husky pre-push hook fail.
Fixes#824
Co-authored-by: andysom25 <7364828+andysom25@users.noreply.github.com>
* fix(kobo): stop offering plugin updates to devices that crash applying them
Plugin 1.3.x calls Device:unpackArchive, which KOReader removed in
koreader/koreader@751b4978 (July 2026). The call is unguarded and the whole apply path runs
without a pcall inside a ConfirmBox callback, so the missing method raises on UIManager's event
loop and takes KOReader down. Plugin 1.4.0 fixed the extraction, but the broken updater is the
code that would have to install that fix, so it can never self-deliver: every device still on
1.3.x crashes on every update attempt, forever.
Gate the offer server side instead. GET /koreader/plugin/version now resolves the calling user
and withholds pluginVersion when any of their devices last reported a plugin older than 1.4.0.
Those clients already treat "unknown" as "the server could not tell me" and show a message
rather than the update prompt, so the crashing path becomes unreachable without touching the
plugin on the device.
Two limits are inherent to a server-only mitigation and are not bugs. The endpoint carries no
device id, only the user, so one stale device parks self-update for all of that user's devices
until it is updated by hand or removed. A device that has never completed a sweep has no
recorded version, so it is not gated. Neither is closable until the plugin reports its own
version on the version check.
Surface requiresManualUpdate per device in the KOReader settings so the affected devices say
what to do, since the 1.3.x dialog string is baked into the installed plugin and cannot be
changed from here.
Closes#905
* fix(kobo): keep blank plugin versions inside the self-update gate
A device reporting an empty pluginVersion produced a falsy blocker, so the truthiness check
opened the gate and handed out the real version. The DTO allows an empty string, so this was
reachable. Also move the settings spec fixtures above the manual-update cutoff, since a
sub-1.4.0 version with requiresManualUpdate false describes a state that cannot occur.
Disable horizontal page swipe gestures in scrolled flow and use screen coordinates for touch movement checks to prevent touch scrolling from triggering accidental page jumps.
Closes#910
Wrap LibrariesView, SmartScopesView, and CollectionsView in a single root div element so Vue Router transition caching functions properly when navigating between index views and library views.
Closes#899
Include the required /api/v1/koreader path and keep the new source string in English so Crowdin can provide localized translations.
Co-authored-by: Matthew Martori <matthew.martori@gmail.com>
Include completion timestamp in enrichment status payloads and track card dismissal in useMetadataCardDismissal to prevent finished notifications from reappearing across page navigations.
Closes#895
Expose loaded and error state properties in useLibraries, and render explicit loading and retry alert states on DashboardView to avoid showing the empty-library welcome state on fetch failure.
Closes#866
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.
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.
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
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.
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.
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
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
* 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>
* 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>
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
* 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.
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
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#86Closes#363Closes#576
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
* 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.