Commit Graph

950 Commits

Author SHA1 Message Date
neon d7556011df build(deps): upgrade Node.js to 26 and pnpm to 11
Update package manager configuration to pnpm 11.22.0 and Node.js requirement to Node 26 across Dockerfile, GitHub Actions workflows, root package.json, and workspace configurations.

Upgrade dependencies, update GitHub Action versions, and remove deprecated scorecard workflow.
2026-08-16 00:41:56 +00:00
neon e2aeaf95ea fix(scanner): resolve case-insensitive path identity to prevent duplicate books
When library items are scanned or moved on case-insensitive filesystems, paths with non-standard author or folder capitalization point to the same physical disk entries. Treating previous path spellings as distinct caused scanner, upload, dock, and rename services to fail path-reuse lookups and create duplicate book records.

Introduce path identity helpers to check whether paths resolve to the same underlying filesystem entry, treating matching entries as path updates rather than creating duplicate books.

Closes #1012
2026-08-16 00:29:32 +00:00
neon f49541cb20 style(client): refine app shell actions
Move GitHub actions into the sidebar footer and tighten header grouping.
2026-08-15 23:55:28 +00:00
neon c9ebf0ab7f i18n(client): sync translations from Crowdin (#1010)
Keep sparse translated catalogs synchronized with reviewed Crowdin content.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-15 15:18:47 -06:00
neon 0816afb2b3 security(kobo): rebuild kepubify with go 1.26.6
Use the patched Go standard library to address CVE-2026-39821 and CVE-2026-46600 in the bundled binaries. Keep the reported v4-dev version unchanged so existing rendition hashes remain stable.
2026-08-14 17:32:34 +00:00
neon 6ab4329223 fix(metadata): preserve authors during metadata refresh
Filter provider contributors by author role and treat empty values as absent for non-destructive merge strategies while preserving explicit overwrite clears.

Fixes #1001
2026-08-14 17:09:03 +00:00
neon 5d55b8aa0d fix(stats): evaluate achievements for updated KOReader sessions
KOReader page-stat uploads can grow an existing derived session as more page events arrive, but
only newly inserted sessions reached achievement evaluation. A session that crossed a threshold by
being updated showed progress past that threshold while the badge stayed locked.

Carry the updated sessions out of the repository instead of a bare count and emit for them too.
The oversized-upload branch emitted a backfill event that nothing listened for, so subscribe
achievement evaluation to it as well; those runs are serialized per user and a burst collapses into
one trailing re-run, since each backfill re-evaluates the whole catalogue. Awards from that branch
stay silent: an upload that large is a device catching up, and notifying per badge would burst a
toast for every historical unlock.

Fixes #991
2026-08-14 06:09:03 +00:00
neon 0480242a98 feat(koreader): retire devices without deleting their synced data
Removing a KOReader device was the only way to take a decommissioned reader off the list, and it
permanently deleted that device's page-by-page reading events, per-device progress, sweep record,
naming pattern and sole-source unmatched books. Derived reading sessions survived, but the raw
events did not, and the plugin keeps its upload watermark on the device, so nothing ever
re-uploaded them.

Add retirement as a marker table rather than a device registry. Device identity is implicit,
spread across five device-keyed tables and surfaced by a DISTINCT ON over device progress, so
there is no row to carry a flag and nothing to backfill. Retiring records only (user, device);
every byte the device synced stays where it is.

Clear the marker whenever a retired device syncs again, on single-book progress, bulk sweeps,
page-stat uploads and sweep completion, so reading never accrues against a device the user cannot
see. Keep deleting synced data as a separate, explicit action on the retired row, and clear the
marker it would otherwise leave behind.

Move the confirm copy to new message keys instead of reusing the ones whose meaning changed, so
the sparse target catalogs cannot ship a stale translation of a destructive dialog.

Closes #996
2026-08-14 05:34:06 +00:00
neon 53be54752d fix(library): run scans on the stored auto-scan schedule
The auto-scan cron expression was validated, persisted and shown in the UI, but nothing ever
consumed it, so a library configured with a schedule never scanned and no scan job was ever
recorded with the schedule trigger.

Register a cron job per configured library on boot and keep it in step as libraries are created,
updated and deleted. An occurrence that lands while a scan is already running is skipped rather
than retried, and a failing occurrence leaves the job registered so later ones still fire.

Schedules run in the server timezone, matching the cleanup, audit and statistics jobs, so a
deployment that sets TZ gets the wall-clock time its users expect.

Move cron validation into a single shared grammar used by both the API and the schedule editor.
The two sides previously disagreed in both directions: the UI accepted step-zero and reversed
ranges that the API rejected, and rejected day names the API accepted. The grammar is a strict
subset of what the scheduler and the human-readable preview can each parse, so an expression the
editor marks valid can always be scheduled.

Closes #985
2026-08-14 04:50:10 +00:00
dependabot[bot] 34e09fa655 chore(deps): bump the production-dependencies group with 41 updates (#1009)
* chore(deps): bump the production-dependencies group with 41 updates

Bumps the production-dependencies group with 41 updates:

| Package | From | To |
| --- | --- | --- |
| [@embedpdf/engines](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/engines) | `2.14.4` | `2.15.0` |
| [@embedpdf/models](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/models) | `2.14.4` | `2.15.0` |
| [@embedpdf/pdfium](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/pdfium) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-annotation](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-annotation) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-bookmark](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-bookmark) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-document-manager](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-document-manager) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-history](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-history) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-interaction-manager](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-interaction-manager) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-pan](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-pan) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-render](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-render) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-rotate](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-rotate) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-search](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-search) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-selection](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-selection) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-spread](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-spread) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-thumbnail](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-thumbnail) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-tiling](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-tiling) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-viewport](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-viewport) | `2.14.4` | `2.15.0` |
| [@embedpdf/plugin-zoom](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-zoom) | `2.14.4` | `2.15.0` |
| [@formatjs/icu-messageformat-parser](https://github.com/formatjs/formatjs/tree/HEAD/packages/icu-messageformat-parser) | `3.5.15` | `3.5.16` |
| [@lucide/vue](https://github.com/lucide-icons/lucide/tree/HEAD/packages/vue) | `1.27.0` | `1.31.0` |
| [@tanstack/vue-virtual](https://github.com/TanStack/virtual/tree/HEAD/packages/vue-virtual) | `3.13.34` | `3.13.35` |
| [@tiptap/extension-link](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-link) | `3.29.1` | `3.29.2` |
| [@tiptap/pm](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/pm) | `3.29.1` | `3.29.2` |
| [@tiptap/starter-kit](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/starter-kit) | `3.29.1` | `3.29.2` |
| [@tiptap/vue-3](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/vue-3) | `3.29.1` | `3.29.2` |
| [@vueuse/core](https://github.com/vueuse/vueuse/tree/HEAD/packages/core) | `14.3.0` | `14.4.0` |
| [intl-messageformat](https://github.com/formatjs/formatjs) | `11.2.12` | `11.2.13` |
| [reka-ui](https://github.com/unovue/reka-ui) | `2.10.1` | `2.10.3` |
| [vue](https://github.com/vuejs/core) | `3.5.40` | `3.5.41` |
| [@fastify/compress](https://github.com/fastify/fastify-compress) | `9.1.0` | `9.2.0` |
| [@fastify/static](https://github.com/fastify/fastify-static) | `10.1.2` | `10.1.3` |
| [@nestjs/common](https://github.com/nestjs/nest/tree/HEAD/packages/common) | `11.1.28` | `11.1.29` |
| [@nestjs/core](https://github.com/nestjs/nest/tree/HEAD/packages/core) | `11.1.28` | `11.1.29` |
| [@nestjs/platform-fastify](https://github.com/nestjs/nest/tree/HEAD/packages/platform-fastify) | `11.1.28` | `11.1.29` |
| [@nestjs/platform-socket.io](https://github.com/nestjs/nest/tree/HEAD/packages/platform-socket.io) | `11.1.28` | `11.1.29` |
| [@nestjs/websockets](https://github.com/nestjs/nest/tree/HEAD/packages/websockets) | `11.1.28` | `11.1.29` |
| [fastify](https://github.com/fastify/fastify) | `5.10.0` | `5.11.3` |
| [jose](https://github.com/panva/jose) | `6.2.4` | `6.2.8` |
| [mysql2](https://github.com/sidorares/node-mysql2) | `3.23.2` | `3.23.3` |
| [nodemailer](https://github.com/nodemailer/nodemailer) | `9.0.3` | `9.0.5` |
| [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) | `8.22.0` | `8.23.0` |


Updates `@embedpdf/engines` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/engines/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/engines)

Updates `@embedpdf/models` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/models/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/models)

Updates `@embedpdf/pdfium` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/pdfium/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/pdfium)

Updates `@embedpdf/plugin-annotation` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-annotation/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-annotation)

Updates `@embedpdf/plugin-bookmark` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-bookmark/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-bookmark)

Updates `@embedpdf/plugin-document-manager` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-document-manager/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-document-manager)

Updates `@embedpdf/plugin-history` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-history/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-history)

Updates `@embedpdf/plugin-interaction-manager` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-interaction-manager/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-interaction-manager)

Updates `@embedpdf/plugin-pan` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-pan/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-pan)

Updates `@embedpdf/plugin-render` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-render/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-render)

Updates `@embedpdf/plugin-rotate` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-rotate/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-rotate)

Updates `@embedpdf/plugin-search` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-search/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-search)

Updates `@embedpdf/plugin-selection` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-selection/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-selection)

Updates `@embedpdf/plugin-spread` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-spread/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-spread)

Updates `@embedpdf/plugin-thumbnail` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-thumbnail/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-thumbnail)

Updates `@embedpdf/plugin-tiling` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-tiling/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-tiling)

Updates `@embedpdf/plugin-viewport` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-viewport/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-viewport)

Updates `@embedpdf/plugin-zoom` from 2.14.4 to 2.15.0
- [Release notes](https://github.com/embedpdf/embed-pdf-viewer/releases)
- [Changelog](https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-zoom/CHANGELOG.md)
- [Commits](https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-zoom)

Updates `@formatjs/icu-messageformat-parser` from 3.5.15 to 3.5.16
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Changelog](https://github.com/formatjs/formatjs/blob/main/packages/icu-messageformat-parser/CHANGELOG.md)
- [Commits](https://github.com/formatjs/formatjs/commits/@formatjs/icu-messageformat-parser@3.5.16/packages/icu-messageformat-parser)

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

Updates `@tanstack/vue-virtual` from 3.13.34 to 3.13.35
- [Release notes](https://github.com/TanStack/virtual/releases)
- [Changelog](https://github.com/TanStack/virtual/blob/main/packages/vue-virtual/CHANGELOG.md)
- [Commits](https://github.com/TanStack/virtual/commits/@tanstack/vue-virtual@3.13.35/packages/vue-virtual)

Updates `@tiptap/extension-link` from 3.29.1 to 3.29.2
- [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.2/packages/extension-link)

Updates `@tiptap/pm` from 3.29.1 to 3.29.2
- [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.2/packages/pm)

Updates `@tiptap/starter-kit` from 3.29.1 to 3.29.2
- [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.2/packages/starter-kit)

Updates `@tiptap/vue-3` from 3.29.1 to 3.29.2
- [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.2/packages/vue-3)

Updates `@vueuse/core` from 14.3.0 to 14.4.0
- [Release notes](https://github.com/vueuse/vueuse/releases)
- [Commits](https://github.com/vueuse/vueuse/commits/v14.4.0/packages/core)

Updates `intl-messageformat` from 11.2.12 to 11.2.13
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/intl-messageformat@11.2.12...intl-messageformat@11.2.13)

Updates `reka-ui` from 2.10.1 to 2.10.3
- [Release notes](https://github.com/unovue/reka-ui/releases)
- [Commits](https://github.com/unovue/reka-ui/compare/v2.10.1...v2.10.3)

Updates `vue` from 3.5.40 to 3.5.41
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.40...v3.5.41)

Updates `@fastify/compress` from 9.1.0 to 9.2.0
- [Release notes](https://github.com/fastify/fastify-compress/releases)
- [Commits](https://github.com/fastify/fastify-compress/compare/v9.1.0...v9.2.0)

Updates `@fastify/static` from 10.1.2 to 10.1.3
- [Release notes](https://github.com/fastify/fastify-static/releases)
- [Commits](https://github.com/fastify/fastify-static/commits)

Updates `@nestjs/common` from 11.1.28 to 11.1.29
- [Release notes](https://github.com/nestjs/nest/releases)
- [Commits](https://github.com/nestjs/nest/commits/v11.1.29/packages/common)

Updates `@nestjs/core` from 11.1.28 to 11.1.29
- [Release notes](https://github.com/nestjs/nest/releases)
- [Commits](https://github.com/nestjs/nest/commits/v11.1.29/packages/core)

Updates `@nestjs/platform-fastify` from 11.1.28 to 11.1.29
- [Release notes](https://github.com/nestjs/nest/releases)
- [Commits](https://github.com/nestjs/nest/commits/v11.1.29/packages/platform-fastify)

Updates `@nestjs/platform-socket.io` from 11.1.28 to 11.1.29
- [Release notes](https://github.com/nestjs/nest/releases)
- [Commits](https://github.com/nestjs/nest/commits/v11.1.29/packages/platform-socket.io)

Updates `@nestjs/websockets` from 11.1.28 to 11.1.29
- [Release notes](https://github.com/nestjs/nest/releases)
- [Commits](https://github.com/nestjs/nest/commits/v11.1.29/packages/websockets)

Updates `fastify` from 5.10.0 to 5.11.3
- [Release notes](https://github.com/fastify/fastify/releases)
- [Commits](https://github.com/fastify/fastify/compare/v5.10.0...v5.11.3)

Updates `jose` from 6.2.4 to 6.2.8
- [Release notes](https://github.com/panva/jose/releases)
- [Changelog](https://github.com/panva/jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/jose/compare/v6.2.4...v6.2.8)

Updates `mysql2` from 3.23.2 to 3.23.3
- [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.2...v3.23.3)

Updates `nodemailer` from 9.0.3 to 9.0.5
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v9.0.3...v9.0.5)

Updates `pg` from 8.22.0 to 8.23.0
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.23.0/packages/pg)

---
updated-dependencies:
- dependency-name: "@embedpdf/engines"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/models"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/pdfium"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-annotation"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-bookmark"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-document-manager"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-history"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-interaction-manager"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-pan"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-render"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-rotate"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-search"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-selection"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-spread"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-thumbnail"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-tiling"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-viewport"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@embedpdf/plugin-zoom"
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@formatjs/icu-messageformat-parser"
  dependency-version: 3.5.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@lucide/vue"
  dependency-version: 1.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@tanstack/vue-virtual"
  dependency-version: 3.13.35
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@tiptap/extension-link"
  dependency-version: 3.29.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@tiptap/pm"
  dependency-version: 3.29.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@tiptap/starter-kit"
  dependency-version: 3.29.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@tiptap/vue-3"
  dependency-version: 3.29.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@vueuse/core"
  dependency-version: 14.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: intl-messageformat
  dependency-version: 11.2.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: reka-ui
  dependency-version: 2.10.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: vue
  dependency-version: 3.5.41
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@fastify/compress"
  dependency-version: 9.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: "@fastify/static"
  dependency-version: 10.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@nestjs/common"
  dependency-version: 11.1.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@nestjs/core"
  dependency-version: 11.1.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@nestjs/platform-fastify"
  dependency-version: 11.1.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@nestjs/platform-socket.io"
  dependency-version: 11.1.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: "@nestjs/websockets"
  dependency-version: 11.1.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: fastify
  dependency-version: 5.11.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: jose
  dependency-version: 6.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: mysql2
  dependency-version: 3.23.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: nodemailer
  dependency-version: 9.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: pg
  dependency-version: 8.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(deps): complete the embedpdf 2.15.0 bump

Dependabot left @embedpdf/core and @embedpdf/plugin-scroll at 2.14.4 while
moving the other 18 packages to 2.15.0. core@2.14.4 pulls models@2.14.4, so
two copies of @embedpdf/models ended up in the tree; PdfTask carries a private
_promise, making them structurally incompatible and failing vue-tsc.

pnpm.patchedDependencies keys on an exact version, so bumping engines to
2.15.0 orphaned the patch key and silently dropped the wasmError rejection
patch, which embedpdf-worker.spec.ts exists to guard.

Bump the two stragglers, re-key the utils packageExtension, and rename the
three patches to 2.15.0. All three patch bodies apply unchanged.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
2026-08-13 22:23:36 -06:00
neon 26b7c602f8 security(settings): gate Book Dock settings behind manage_book_dock
Require manage_book_dock for the settings UI and the scoped API so regular Book Dock users cannot
view or modify administrative configuration.

Reject every Book Dock key on the generic PATCH /app-settings/:key route as well, including
book_dock_paused. That route accepted the same keys with any string value, so an operator with
manage_app_settings could store an out-of-range threshold or an unknown metadata mode and bypass
the validation the scoped endpoints apply.

Closes #1003
2026-08-14 03:15:30 +00:00
dependabot[bot] 8cbc8bd8ab chore(deps): bump dorny/paths-filter in the github-actions group (#1008)
Bumps the github-actions group with 1 update: [dorny/paths-filter](https://github.com/dorny/paths-filter).


Updates `dorny/paths-filter` from 4.0.2 to 4.0.3
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dorny/paths-filter/compare/7b450fff21473bca461d4b92ce414b9d0420d706...ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d)

---
updated-dependencies:
- dependency-name: dorny/paths-filter
  dependency-version: 4.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-13 21:14:26 -06:00
neon 2b8e1d6a79 fix(kobo): stop endless sync retries and cut round trips
Report success when Kobo rejects a delete for a tag it no longer has, so the device stops retrying
one it can never complete. Skip the full-library reconcile while a device is still draining pages,
and page fifty books at a time so a large re-announce costs a few round trips instead of hundreds.

Ref #1005
2026-08-14 02:26:46 +00:00
neon fddb41fdb9 fix(settings): expose koreader credential rotation
Expose the existing credential update path so users can rotate the single KOReader login without deleting synced data. Clarify the current settings location and device reconnection requirement.

Closes #990
2026-08-13 19:23:38 +00:00
neon becfb6a72e fix(settings): display complete endpoint path in koreader sync url
The KOReader settings page displayed the origin-only base URL without the /api/v1/koreader path suffix, causing manual URL copy and built-in progress sync setup to fail due to incomplete endpoint paths.

Updates the UI to display the full KOReader sync URL with the API endpoint path included, consolidates redundant copy fields, and updates plugin URL normalization to handle full stock sync URLs seamlessly.

Closes #981
2026-08-13 19:08:53 +00:00
neon 0104898371 i18n(client): sync translations from Crowdin (#1000)
Keep sparse translated catalogs synchronized with reviewed Crowdin content.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-13 12:55:09 -06:00
neon 8fbdb9a9d1 i18n(client): sync translations from Crowdin (#994)
Keep sparse translated catalogs synchronized with reviewed Crowdin content.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-12 11:03:13 -06:00
neon 9eff914aef fix(client): stop requiring plural categories no count can select
The locale validator demanded every CLDR plural category, so each new ICU plural key took the daily
Crowdin sync red until a `many` branch was hand-added for Spanish and French. That branch can never
render: `many` selects at a million and over in Spanish, French, Italian and Portuguese, and only
for fractional counts in Czech and Slovak, while every catalog plural counts whole items well below
a million and intl-messageformat falls back to `other` regardless. Require only the categories an
integer count 0..1000 can select, which is the same set every integer below a million selects, and
keep `other` always so the categories everyday counts reach stay mandatory.

Drop a target message that still fails validation from the sparse catalog instead of failing the
whole export, so one broken string can no longer block the other 23 languages for days. Report the
rejections in the run log and the pull request body, and keep the retention guard fatal when a
rejected message would replace a translation that is already in Git.
2026-08-12 16:54:51 +00:00
neon 0087164c2e feat(scanner): rebuild the Book Dock page as a uniform-row triage list
Replace the nine-column table with one list of uniform rows that reflows through a container
query, so a single component serves both desktop and phone. Surface the match score, applied
state and write path that the old row buried in tertiary text.

Back it with a needsReview filter and attention sort over existing indexed columns, threaded
through every bulk selection path so select-all cannot target the wrong rows. Keep confidence
advisory: it gates automatic finalization only.

BookDockSummary gains three additive counts; ../bookorbit-ios mirrors these types by hand.
2026-08-12 00:40:05 +00:00
neon f199e46d1c feat(client): make the sidebar Browse zone collapsible
Browse was the only labelled zone that could not be collapsed, leaving no way to reclaim
those rows for someone who never browses by author or series. Reuse the section header,
persistence and rail behaviour the entity sections already have rather than introducing a
second mechanism, and extract the shared collapse transition so both call sites stay in
step.

Browse has no entity list to bound, so make the persisted section cap optional and keep it
off that section entirely. Zones now carry their own section id, so adding another
collapsible zone needs no change in the template.
2026-08-11 20:58:26 +00:00
neon 867295cb63 fix(metadata): stop iTunes returning audiobook editions for ebooks
The search params used one flag for two questions: which edition a provider should return, and
whether audiobook-only providers may run. Every caller that wanted the second forced the flag true,
so iTunes asked for entity=audiobook whenever Audible, AudNexus or Libro.fm was in the provider set.
Split the two, and derive the media type from the book's primary file so a record holding both an
ebook and an audiobook no longer searches as an audiobook in one drawer and an ebook in the other.

Closes #982
2026-08-11 20:32:01 +00:00
neon 7afc128bcb feat(cover): let users pick the default provider for cover search
Cover search always opened on DuckDuckGo, so anyone who prefers another source had to switch it by
hand for every book. Store the choice per user and apply it when the drawer opens. Cache the value
client-side so the drawer never waits on a request to become usable, and save it to the account
rather than the device so it reaches the iOS client too. AudiobookCovers is not offered as a
default because it only returns results for audiobooks.

Closes #983
2026-08-11 20:02:49 +00:00
neon 37c3f3acd2 feat(collections): keep books in the order they were added
Collections opened in title order, so a curated shelf came back rearranged. Add a collectionOrder
sort field, make it the default for collection views, and offer it from a sort control that grid
and list previously did not have.

Back the ordering with a new collection_books.position sequence rather than added_at: added_at
defaults to now(), the transaction timestamp, so every book added in one request shared it and the
order fell back to books.id. Kobo collection paging had the same tie and now orders by position.

Also stop useBookWindow blanking its slots and total on every query change. Clearing them collapsed
the virtual list to zero height and back, which read as a flash on any sort, search, or filter
change.

Closes #978
2026-08-11 17:31:55 +00:00
Moritz ed0964a50c feat(library): add {library} placeholder to file naming patterns (#976)
Resolve {library} to the name of the library a book belongs to, so a
single KOReader device pattern can keep each library in its own subtree
on the device instead of merging every library into one tree.

The token is populated in every server-side pattern surface: KOReader
manifest and book detail device paths, upload, automatic and bulk
rename, book move (target library), download filenames, and BookDock
finalize.
2026-08-11 09:57:58 -06:00
neon 7dcc509a54 feat(client): add nine interface languages and a searchable language picker
Traditional Chinese cannot reuse the `%two_letters_code%` filename because it would collide with
Simplified under `zh.json`, so `crowdin.yml` maps `zh-TW` to a `zh-Hant` catalog and locale
matching resolves by script subtag before falling back to the base language. Every other new
locale keeps its bare two-letter ID.

Twenty-five entries outgrew the old menus. The mobile submenu inherited `overflow-hidden` with no
max-height, so the last nine languages were unreachable on a phone, and the list was ordered by
ISO code while displaying endonyms, which reads as unsorted. Both header menus now render one
LanguagePicker that searches the native name, the English name and the locale code, groups the
browser's own languages as suggestions, and orders the rest with a collator on the string it
actually shows. Mobile opens a sheet instead of a nested flyout, since a flyout is the wrong
shape for touch regardless of height. Settings gains the Appearance language section its copy
has anticipated since the keys were added.

Tighten the Crowdin progress chart, which the longer language list had stretched to 810px tall.
2026-08-11 07:43:38 +00:00
neon 953c9e334b i18n(client): sync translations from Crowdin (#972)
Keep sparse translated catalogs synchronized with reviewed Crowdin content.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-11 01:42:22 -06:00
neon a5385633b0 fix(settings): stop Chrome offering to save provider secrets
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.
2026-08-11 05:43:59 +00:00
neon 4c46a4fc2c fix(settings): unify responsive settings navigation
Keep active settings sections and tabs visible on small screens while centralizing repeated navigation, card, and row styling.
2026-08-11 05:19:43 +00:00
neon 208d04d218 fix(opds): advertise search inline for clients that skip OpenSearch
Moon+ Reader and CrossPoint never fetch the OpenSearch description document, so they
detected no search capability and hid the search affordance entirely. Both look only for a
rel="search" link whose href already carries {searchTerms}, and they match the link type by
equality rather than by substring.

Emit that templated link alongside the existing OpenSearch pointer, keeping the pointer
first so KOReader's has_opensearch guard still wins and its behaviour is unchanged. The
type stays bare application/atom+xml and the href stays relative like every other href in
the feed, so nothing has to reconstruct an external base URL behind a reverse proxy.
Calibre-Web ships the same two-link pairing.

Add the pair to the libraries, collections, smart-scopes, authors and series feeds too.
Those carried only a self link, so search disappeared once a client descended into a
sub-folder even when it did support OpenSearch.

Closes #860
2026-08-11 00:58:47 +00:00
neon 2d09c37806 feat(kobo): deliver Kobo store titles and fix store cover 404s
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
2026-08-10 23:45:56 +00:00
Jadehawk 80223b5e67 fix(koreader): accept optional KOSync document metadata (#966)
* fix(koreader): accept optional KOSync document metadata

* fix(koreader): reject null progress metadata
2026-08-10 17:42:59 -06:00
neon 2b215141a9 feat(metadata): limit fetched genres per book
Keep provider metadata from creating overly granular genre catalogs while preserving unlimited behavior by default.

Closes #710
2026-08-10 21:35:09 +00:00
neon e56d253dc1 fix(library): exclude missing books from library counts
Align library list totals with statistics by counting only present records while preserving empty libraries and user content filters.

Fixes #639
2026-08-10 20:04:07 +00:00
neon 9e1eb6119a feat(client): add configurable multi-row dashboard shelves
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
2026-08-10 19:44:32 +00:00
neon c113c63bde fix(series): make select mode work when series are collapsed
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
2026-08-10 18:36:27 +00:00
neon bf8bb52fc7 perf(cover): stop the loading shimmer once the cover has loaded (#958)
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
2026-08-10 11:43:27 -06:00
neon 7299664f64 feat(reader): allow smaller EPUB text sizes
Permit readers to reduce reflowable book text to 6px while keeping validation and controls aligned across shared types, server, and web.

Closes #784
2026-08-09 22:04:54 +00:00
neon ca3c637271 fix(auth): split Book Dock access from Book Dock administration
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
2026-08-09 21:27:33 +00:00
neon 68fae9a121 fix(reader): route KOReader progress by format so paged books resume
KOReader reports a paged document's position as a page number and a reflowable one's as an
xpointer, but applySharedProgress treated every incoming position as an xpointer and
upsertReadingProgress hardcoded pageNumber to null on every write. A PDF synced from a device
therefore stored a correct percentage with no page, and the web reader reopened it at page 1.
The same write also destroyed a page the web reader had saved earlier, including during an
unattended bulk sweep, so this lost positions that were never synced from a device at all.

Carry the book file's format through hash resolution and pick the column from it. CBZ/CBR were
affected identically at the data level but masked in the UI, because the comic reader estimates
a page from the percentage when none is stored.

Closes #954
2026-08-09 17:29:19 +00:00
neon 4607ed8235 fix(users): stop the admin preset granting demo restricted
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
2026-08-09 16:53:33 +00:00
neon e7039e93a0 feat(auth): add self-registration toggle and sign-up page
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
2026-08-09 16:33:00 +00:00
neon 1ec0094187 fix(settings): show white accent swatch correctly
Keep the white preview distinct from its accessible light-theme primary and outline it on light surfaces.

Fixes #948
2026-08-09 16:02:13 +00:00
dependabot[bot] 9a80b2b1a4 chore(deps): bump the github-actions group with 2 updates (#921)
Bumps the github-actions group with 2 updates: [docker/login-action](https://github.com/docker/login-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action).


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

Updates `github/codeql-action/upload-sarif` from 4.37.3 to 4.37.6
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81...5595ccaf912efad79be6eef63a5619ff05969be3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.37.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-09 00:26:59 -06:00
neon 02058e82fd fix(series): stop the series name input collapsing on narrow screens
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
2026-08-09 05:52:01 +00:00
neon 2e8f882f4d fix(annotations): resolve html entities when indexing chapter text
Chapters are parsed in XML mode, which resolves only the five predefined XML entities and numeric
character references. EPUB2 XHTML may use any named HTML entity through its DTD, so `&nbsp;` and its
kin were indexed as literal text while kepubify wrote the decoded character. The two text indexes
then never matched, which disabled structural resolution for every annotation in the chapter and
failed each one with kepub_text_mismatch, even though the Kobo position was exact. Resolve the
references on the raw markup before parsing, where an escaped `&amp;nbsp;` is still distinguishable
from a real reference, and read each one back through an attribute value so an unknown reference is
never partially matched against a legacy entity.

Rebuild canonical positions from the stored kobo_span position on position retry as well. Retry only
ran the cfi backfill, which derives cfi from an xpointer and therefore had no candidate when the
xpointer failed too, leaving an affected annotation pending forever with no way to recover from the
UI. Route the rebuild through a registry that the Kobo module registers into, since it already
imports the annotation module and the dependency cannot run the other way.

Fixes #944
2026-08-09 05:32:41 +00:00
neon 23fa46fca7 docs: correct postgresql version references to 18
docker-compose.dev.yml and e2e-runner.yml have run pgvector/pgvector:pg18
since v2.2.0, but the development and testing guides still described the
container as PostgreSQL 16.
2026-08-09 05:29:54 +00:00
neon 6769bf4b0f fix(auth): make account lockout recoverable and visible
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.
2026-08-09 05:12:57 +00:00
Dukko 9a349311cf feat(metadata): add hardcover edition selection and refresh pinning (#936)
* 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>
2026-08-08 22:35:53 -06:00
neon 54cb7dbda0 fix(auth): recover live sessions when an access token expires
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
2026-08-09 03:24:24 +00:00
neon d8105d4820 fix(kobo): keep metadata file writes off the re-download path
Embedding metadata rewrites the book file, so its hash moves even though the book itself is
unchanged. Kobo reconcile reads a moved hash as new content and re-delivers the book as a
NewEntitlement, which makes the device rebuild its local entry and orphan every annotation
attached to the old one.

Carry the post-write hash into the device snapshots that recorded the pre-write hash, so
reconcile stays on the metadata-only path. The device keeps the bytes it already has and never
receives the rewritten file; a content change from any other source still triggers a download.

Ref #925
2026-08-09 03:24:24 +00:00