Skip to content

@libris/api-hono

0.29.0

Minor Changes

  • 2376f08: Pull reading status from Hardcover into Libris on every Hardcover sync.

    The hardcover-sync worker is now bidirectional for status. In addition to pushing Libris-derived status to Hardcover (existing behavior), it also fetches each connected user's me.user_books from Hardcover and stores the status as reading_aggregate.external_status. This means a fresh Libris install with a large existing Hardcover library will instantly reflect the user's existing reading state across the library view — no need to re-mark anything by hand.

    Schema: reading_aggregate gains two columns: external_status (the Hardcover-pulled status) and external_status_synced_at. Hardcover's "Did Not Finish" (status_id 5) folds into Libris paused.

    Effective status precedence is now:

    manual_status                                        -- user override
    ?? local-computed (only if any local reading_progress for this book)
    ?? external_status                                   -- Hardcover-pulled fallback
    ?? "unread"

    Local progress (KOReader / kosync) still wins over Hardcover-pulled status the moment any reading data lands in Libris. Manual overrides still beat both. external_status is read-only from the push side, so pulled values cannot loop back outward.

Patch Changes

  • bc775c9: Push manual_status overrides to Hardcover.

    The Hardcover sync push side now respects reading_aggregate.manual_status — if a user marks a book "finished" (or "reading", "paused") manually in Libris, that status is pushed to Hardcover on the next sync, even when no KOReader/kosync data exists for the book. Previously the push side only computed status from local progress and ignored manual overrides.

    Effective status pushed to Hardcover is manual_status ?? computed. The existing "never push unread" guard still applies so we don't clobber Hardcover with no-data states.

    The change-detection CTE in hardcover-sync.ts was extracted into lib/hardcover/sync-candidates.ts so it can be exercised directly by integration tests; 7 new tests cover the manual-only path (book selected with no progress; sync log diff vs. effective status; manual overrides progress-derived status in change detection; per-api-key scoping).

0.28.0

Minor Changes

  • 19f7b77: Manual reading status override on the book detail page.

    API: reading_aggregate gains five sticky-override columns (manual_status, manual_started_at, manual_finished_at, manual_paused_at, manual_set_at). When manual_status is set it wins over the computed status everywhere — GET /api/library/{id} (now also returns progress), GET /api/library/sync, GET /api/reading-status/counts, and GET /api/reading-status/{status} — until the user clears it. Underlying reading_progress history and the auto started_at / finished_at derivation from KoSync are untouched, so the override is reversible.

    Two new endpoints:

    • PATCH /api/library/{id}/reading-status — body { status, startedAt?, finishedAt?, pausedAt? }. Per-status field policy: unread clears all dates, reading keeps started_at, finished keeps started_at + finished_at, paused keeps started_at + paused_at. Rejects future dates and finishedAt < startedAt.
    • DELETE /api/library/{id}/reading-status — clears the override; subsequent reads fall back to the computed status from KoSync.

    The ProgressAggregate schema gains pausedAt (manual-only) and manuallySet: boolean so clients can render a Clear-override affordance.

    Web: New "Edit reading status" action on the book detail page action menu opens a modal with a status select and date pickers that appear conditionally per status. Pre-fills finished_on to today and started_at from the existing aggregate. A "Clear override" button appears when an override is active. The Obsidian plugin remains read-only and consumes the effective (override-aware) status automatically via /api/library/sync.

Patch Changes

  • e05296d: Migrate the monorepo to Vite+ as the unified toolchain.

    • Drop Turborepo. The root vite.config.ts and per-package vite.config.ts files now hold all fmt / lint / pack / test config. Workspace orchestration runs through vp run -r <task> (with -F filters replacing --filter).
    • Drop standalone oxlint, oxfmt, and oxlint-tsgolint dev deps — vp lint and vp fmt ship the same binaries. Per-package .oxlintrc.json files are removed (rules now live in the root vite.config.ts lint block).
    • services/api-hono builds via vp pack (still tsdown under the hood) instead of invoking tsdown directly. tsdown.config.ts content is inlined into the pack block of services/api-hono/vite.config.ts.
    • apps/obsidian-plugin keeps esbuild for the production build (Obsidian-specific output) but uses vp for test/lint/fmt/check.
    • apps/web branches its plugin set on process.env.VITEST inside vite.config.ts so component tests render UBadge etc. as plain elements (matches the pre-migration test environment); the main path carries the full plugin set for dev/build. (Test-only config previously lived in a separate vitest.config.ts — removed in the follow-up cleanup.)
    • All vitest / vitest/config imports rewritten to vite-plus/test. Catalog vite and vitest entries are aliased to @voidzero-dev/vite-plus-core and @voidzero-dev/vite-plus-test so transitive peer-dep resolutions still deduplicate correctly.
    • Lefthook hooks call vp lint --fix / vp fmt / vp run check -r instead of pnpm exec.
    • CI: switched to the official voidzero-dev/setup-vp@v1 action (handles Node + pnpm + cache in one step, reading .node-version). Removed .turbo cache steps, the manual corepack blocks, and the now-defunct generate-types step. pnpm run X invocations rewritten to vp run -r X / vp run -F <pkg> X; pnpm exec invocations rewritten to vp exec. --affected flags are dropped (no direct Vite+ equivalent yet); check / test now run across all workspace packages every time. Docs-deploy change-detection regex updated from turbo.json to vite.config.ts.
    • Commit hooks: lefthook's pre-commit now calls vp staged (Vite+'s native staged-file runner). The per-extension command map moved from lefthook.yml glob commands to a staged block in the root vite.config.ts. Lefthook still owns the other hooks (prepare-commit-msg, pre-push, post-merge, post-checkout) for the beads integration.
    • AGENTS.md / CLAUDE.md: added the canonical <!--VITE PLUS START--> ... <!--VITE PLUS END--> block at the top so generic agents pick up the toolchain context without reading the project-specific section below.
    • VitePress bumped to 2.0.0-alpha.17. Adds native rolldown-vite support and switches the bundler to oxc-minify — eliminates the "plugin assigns to bundle variable" warnings and the transformWithEsbuild is deprecated warning we saw on every docs build, and roughly halves docs build time (14s → 7s). Required adding oxc-minify (peer dep of vitepress 2 when running on rolldown-vite) to @libris/docs devDeps.
    • Type-aware lint enabled globally — both typeAware and typeCheck set to true in lint.options (matching the vp create/vp migrate default). Per-package --type-aware flags removed from scripts since it's now the default. Surfaced 32 no-floating-promises violations in apps/web:
      • onSettled callbacks: rewrote each as () => queryCache.invalidateQueries(...) (single) or () => Promise.all([...]) (multiple). Returning the promise is more idiomatic than void-ing it — pinia-colada (and any other consumer) can await invalidation if it cares.
      • apps/web/src/main.ts: bootstrap() is the app entry point. Switched from void bootstrap() to top-level await bootstrap() (supported by our target: esnext build).
      • apps/obsidian-plugin/src/main.ts: workspace.revealLeaf(leaf) properly awaited inside the already-async activateSyncStatusView.
    • MetadataFieldPicker.types.ts extracted from MetadataFieldPicker.vue. tsgolint can lint the <script> block of a .vue file but can't resolve types declared inside an SFC when they're imported from a .ts file — types meant to cross the SFC boundary must live in a regular .ts module. The test now imports Candidate directly instead of relying on the InstanceType<typeof Component>["$props"] extraction trick.
    • Per-package check scripts collapsed to a single vp check invocation. Previously each package ran vp fmt --check . && vp lint && tsc --noEmit as three serial subprocesses; with typeCheck: true enabled, vp check is one cohesive run that does fmt + lint + tsgolint type-check. apps/web still chains && vue-tsc --build because tsgolint can't see inside .vue SFCs (vue-tsc covers templates and Vue component types). Workspace check now executes 8 tasks instead of 13.
    • IDE integration (vp create adds these by default; vp migrate doesn't): .vscode/extensions.json now recommends VoidZero.vite-plus-extension-pack, and .vscode/settings.json sets npm.scriptRunner: vp and the oxc formatter as the default + format-on-save + fix-on-save action.
    • CI gains a node_modules/.vite/task-cache actions/cache step (per check / test / build / docs job), and the cacheable steps now use vp run --cache -r <task>. Verified locally: 11/13 cache hits and ~74s saved on a warm second run of vp run --cache -r check. Tasks that mutate their own inputs (e.g. @libris/docs#check, @libris/api-hono#build:spec) are correctly identified as non-cacheable.
    • fmt.sortPackageJson: true added to root vite.config.ts. One-time reformat applied to every package.json in the workspace.
    • lefthook.yml now carries an explanatory header noting why we don't run vp config (we already use lefthook for the broader hook lanes — pre-push, post-merge, post-checkout, prepare-commit-msg — which integrate beads).

    Validation: vp install, vp run -r check, vp run -r test, vp run -r build all green locally.

  • e05296d: Finish the Vite+ migration: drop legacy script/task patterns and wire up the workspace task graph properly.

    apps/web — collapsed vitest.config.ts into the test block of vite.config.ts (per Vite+ guidance: "We do not recommend using vitest.config.ts with Vite+"). Plugin set is now branched on process.env.VITEST so shallowMount keeps seeing raw .u-badge HTML instead of stubbed <u-badge-stub> components. Replaced the npm-run-all2 build (run-p type-check "build-only {@}") with a Vite Task graph: type-check (vue-tsc), build (vp build, dependsOn type-check), check (vp check, dependsOn type-check). Cross-package dependsOn: ['@libris/api-hono#generate:version'] declared on type-check so standalone vp run -F @libris/web build triggers the upstream codegen — workspace dep ordering only kicks in for -r. Excluded dist/, node_modules/.nuxt-ui/, and the auto-generated .d.ts files from the input fingerprint to stop self-poisoning the cache. Dropped npm-run-all2 from the catalog.

    services/api-hono — replaced the pre* + pnpm run hook chains with vite.config.ts tasks and dependsOn. Five scripts (predev, prebuild, prebuild:spec, precheck, pretest) all forwarded to pnpm run generate:version, and build did pnpm run build:spec && vp pack && cp -r migrations dist/migrations. pnpm run inside a script bypasses the Vite Task graph (separate process, no cache, no inlining). Now: generate:version, build:spec, build, check, test, dev, db:reset, db:studio, reset:bullmq are all tasks with proper dependsOn and cache settings. Inline GOMEMLIMIT=1GiB GOGC=50 kept inside the check task's command (cleaner than untrackedEnv for a fixed knob — untrackedEnv only passes through values the caller has set). Input filters added to exclude self-written outputs (src/generated/, openapi.json, dist/).

    docs — added input filters to the vitepress tasks: .vitepress/dist/, .vitepress/cache/, .vitepress/.temp/, node_modules/.vite-temp/. With these excluded, the docs build is now 100% cache-hit on warm runs (~9s saved per invocation).

    Root package.json + vite.config.ts — dropped ~15 thin alias scripts (dev:server, dev:web, dev:docs, build:docs, test:api, db:reset, db:studio, reset:bullmq, docs:generate:db, …) that were pure vp run -F wrappers. Promoted the three CI-style aggregators (check, test, build) and the two shell wrappers (bruno:import, test:e2e:docker) into root tasks in vite.config.ts. Per-package work is now invoked directly via vp run -F @libris/<pkg> <task>. Updated README.md, docs/contributing.md, docs/database.md, docs/testing.md, docs/ci-cd.md, .github/pull_request_template.md, and AGENTS.md (CLAUDE.md is a symlink) to reflect the new invocation surface.

    Dockerfile — migrated to install vite-plus globally and use vp run -F @libris/<pkg> build for both stages. The previous pnpm run build / pnpm run generate:version invocations stopped working when those moved from package.json scripts to vite.config.ts tasks. Build-web stage now copies services/api-hono/vite.config.ts + scripts/ so the cross-package dependsOn resolves.

    .forgejo/workflows/publish-images.yml — version job migrated from raw corepack/pnpm install --frozen-lockfile/pnpm changeset version to setup-vp@v1 + vp install --frozen-lockfile + vp exec changeset version, matching the rest of CI.

    Validation: vp run check, vp run -F @libris/web test, vp run -F @libris/api-hono test, vp run -F @libris/obsidian-plugin test, vp run -F @libris/api-hono build, vp run -F @libris/web build, vp run -F @libris/docs build all green. Recursive vp run check warm cache: 5/7 hits, 61s saved. Docker image build couldn't be validated locally (Docker not present in WSL) — flag for human verification on next image publish.

0.27.0

Minor Changes

  • c58d4f3: Add GET /api/library/sync — a paginated bulk endpoint optimised for full-vault mirror clients (Obsidian plugin, future CLIs). Each BookSyncRecord bundles the BookSummary fields plus the per-book progress aggregate (MAX(percentage) across devices + derived reading status), so a sync run is one paginated drain instead of N×3 requests against the catalog. Optional ?since=<ISO> filters to records whose metadata or progress changed after that timestamp; the response's serverTime is the cursor for the next incremental run. Auth is the same bearer-token gate as the rest of /api/*. New schemas: ProgressAggregate, BookSyncRecord, BookSyncResponse.

  • c58d4f3: Plugin bases pass + lifecycle dates from server.

    API: GET /api/library/sync now returns progress.startedAt and progress.finishedAt per book — read from the new reading_aggregate table introduced in the previous changeset. Both fields are nullable ISO 8601 strings.

    Plugin: pre-release, breaking changes are accepted.

    • Per-book frontmatter gains started_at and finished_at (allowlisted, written from the server response).
    • Series and author pages migrated from markdown bullet lists to Series/<name>.base and Authors/<name>.base — cards view with covers, sorted by series_index ASC / year ASC.
    • Reading.md replaced with Reading.base — four cover-grid views: Currently reading, Recently finished (last 30 days), Paused, Read this year. Uses the server-tracked finished_at for objective filtering.
    • Library.base, My Reading.base, and Reading.base regenerate on every sync. My Reading.base drops its "Read this year" view (now in Reading.base) and the my_read_date property entry.
    • Empty my_rating: placeholder seeded into newly-created book notes for discoverability. Plugin never overwrites it.
    • Legacy markdown relation/dashboard pages (Reading.md, Series/*.md, Authors/*.md) are moved to the system trash on each sync, identified by their libris_kind frontmatter.
    • New bases-format.escapeBaseString helper for safely interpolating series/author names into Bases filter expressions.
    • Ribbon tooltips drop the "Libris:" prefix; commands "Open current book in Libris" / "Open current book's series in Libris" renamed to "Open current book in browser" / "Open current series in browser" per Obsidian directory naming guidelines.

0.26.0

Minor Changes

  • 7119824: Track per-(user, book) reading lifecycle timestamps server-side.

    • New reading_aggregate table keyed by (api_key_id, book_id) with nullable started_at and finished_at.
    • KOReader sync (PUT /syncs/progress) now upserts the aggregate after each progress write: started_at is set on the first non-zero progress; finished_at on the first reading at or above FINISHED_THRESHOLD (0.95). Neither field is overwritten once set — COALESCE preserves the prior value on conflict.
    • One-shot backfill-reading-aggregate maintenance job (enqueued once on startup) derives both timestamps from reading_progress_history for every existing (api_key_id, book_id) pair, so already-finished books populate immediately.
    • Aggregate writes are fire-and-forget so they never block the kosync response.

    The new fields are not yet surfaced through any read API — that's a follow-up bundled with the Obsidian plugin update that consumes them.

Patch Changes

  • 9193223: Upgrade dependencies across the monorepo. Notable major bumps: TypeScript 5 → 6.0.3, @hono/node-server 1 → 2, @loglayer/transport-pretty-terminal 5 → 6, @unhead/vue 2 → 3, chokidar 4 → 5, lefthook 1 → 2, @electric-sql/pglite 0.3 → 0.4.

    Two breakages fixed:

    • @loglayer/transport-pretty-terminal v6 makes database required and no longer bundles better-sqlite3. The dev logger now dynamically imports better-sqlite3 alongside the transport and passes an in-memory DB; both stay out of the production bundle.
    • lefthook v2 refuses lefthook install when core.hooksPath is set locally. The root prepare script now uses lefthook install --force so pnpm install works on any clone.

    Also bumps the @opentelemetry/api-logs pnpm override from 0.214.00.215.0 to match the upgraded @opentelemetry/sdk-logs / sdk-node and avoid a duplicated logger singleton.

    Sets NO_COLOR=1 on every bd hooks run command in lefthook.yml. bd 1.0.2 emits OSC 11 and DA terminal-detection queries on stdout and exits before reading the responses, so with lefthook v2's verbose post-checkout output the unread responses leak into the interactive shell (visible as stray ^[]11;rgb:...^G^[[?62;22;52c garbage at the prompt after git switch). NO_COLOR=1 makes bd skip the detection entirely; the env blocks can be removed once a fixed bd release ships.

    Bumps the CI mcr.microsoft.com/playwright container image from v1.58.2-jammyv1.59.1-jammy in both the PR smoke-test and main full-suite E2E jobs so the bundled Playwright binary matches the upgraded @playwright/test catalog pin (1.59.1).

0.25.0

Minor Changes

  • b8f7d26: BREAKING: drop DATABASE_URL and REDIS_URL env vars. The app now assembles both connection URLs from split vars, giving docker-compose.dev.yml and .env a single source of truth.

    • Postgres: POSTGRES_HOST, POSTGRES_PORT (default 5432), POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB
    • Redis: REDIS_HOST, REDIS_PORT (default 6379), REDIS_USER (optional), REDIS_PASSWORD (optional), REDIS_TLS=1 for rediss://

    The assembly lives in services/api-hono/src/lib/resolve-database-url.ts and resolve-redis-url.ts. docker-compose.dev.yml reads the same split vars via ${VAR:-default} interpolation, so editing .env is reflected on both sides.

    Migration: replace any DATABASE_URL=postgresql://user:pass@host:5432/db in your .env, deployment config, or CI secrets with the five POSTGRES_* vars; same for REDIS_URLREDIS_*. See .env.example for the dev template and docs/deployment.md for prod.

Patch Changes

  • b8f7d26: CI: cap tsgolint memory and raise vitest hook timeout

    • services/api-hono check script now sets GOMEMLIMIT=1GiB GOGC=50 when invoking oxlint --type-aware. Oxlint's type-aware pass shells out to the tsgolint Go binary, which was getting SIGKILL'd by the OOM killer on the Forgejo docker runner. The soft memory limit makes Go GC more aggressively so the process stays under the container's memory ceiling.
    • services/api-hono/vitest.config.ts adds hookTimeout: 60_000. The CI runner is ~3x slower than local; PGlite spin-up + migrations in beforeAll / beforeEach was exceeding the 10s default, causing cascading test failures (later tests saw stale state from the interrupted hook).
  • 4c007d2: Fix publish-images build: generate services/api-hono/src/generated/version.ts in the build-web Dockerfile stage.

    Since the switch to vue-tsc --build (which actually follows workspace imports), apps/web's type-check chases @libris/api-hono/clientservices/api-hono/src/routes/index.ts../generated/version.js. That file is produced by the generate:version script at api-hono build time and is gitignored, so the isolated build-web stage had nothing to resolve to (TS2307).

    Fix: the build-web stage now also copies services/api-hono/scripts/ and runs pnpm --filter @libris/api-hono run generate:version before the web build, producing the version module next to the sources the stage already has. Stays independent of build-api so the two stages can still build in parallel.

  • 9da0f5d: fix: return cover of the first volume in a series

  • 6aa46fe: Scan existing inbox files on startup

    The inbox watcher now processes files present at startup instead of only watching for new additions. Files dropped into the inbox while the server is offline are picked up on the next boot; the book-detected worker dedupes by SHA-256 checksum so already-ingested files are cheap no-ops.

  • b8f7d26: Declare services/api-hono/src/generated/** as a turbo output of both build:spec and build.

    apps/web transitively imports services/api-hono/src/routes/index.ts via @libris/api-hono/client + /types, which imports ../generated/version.js (written by the generate:version hook that runs before each of prebuild, prebuild:spec, precheck, pretest). On a CI runner, turbo restores cached tasks by replacing only the declared outputs — so when build:spec or build was a cache hit, the sibling version.ts was left missing and downstream vue-tsc --build in apps/web failed with TS2307 (seen in CI runs 529 → check and 530 → build).

    Adding src/generated/** to both task outputs captures the generated version module alongside each task's primary artifacts, so downstream packages resolve it whether the upstream api-hono task ran fresh or was a cache hit.

0.24.1

Patch Changes

  • 79dc86f: Avoid noisy 404 cover requests for inbox items with no cover source

    • Inbox list page now only requests cover extraction for items with EPUB files or an HTTP coverUrl; non-EPUB items without a cover show a placeholder immediately
    • Inbox list cover cells gracefully fall back to the placeholder icon when image loading fails (e.g. corrupt EPUB with no extractable cover)
    • Backend cover endpoint downgrades "no cover source" log from warn to debug since it's an expected condition

0.24.0

Minor Changes

  • 29f1d25: Rename LIBRARY_PATH to LIBRIS_LIBRARY_PATH and INBOX_PATH to LIBRIS_INBOX_PATH to avoid collisions with system env vars (Nix C linker). Enable devenv native dotenv integration, add dotenv-cli for non-devenv users, fix devenv postgres database name, and remove dead env vars.

    Breaking: Rename in your .env / docker-compose / CI config:

    • LIBRARY_PATH -> LIBRIS_LIBRARY_PATH
    • INBOX_PATH -> LIBRIS_INBOX_PATH

0.23.1

Patch Changes

  • d9a6eef: Catalog hygiene — consolidate direct version pins into the shared pnpm-workspace.yaml catalog and drop stale entries.

    Moved to catalog (were direct pins without special reason):

    • apps/web: @nuxt/ui, nuxt, tailwindcss, vue
    • docs: vitepress, vitepress-sidebar

    Kept as direct pin (intentional exact-pin):

    • tests/e2e: @playwright/test (1.58.2, no caret) — E2E determinism.

    Removed from catalog + dependents:

    • consola — no longer imported by any source file since the logging stack migrated from consola to LogLayer. The remaining vi.mock("consola", ...) stub in services/api-hono/src/lib/metadata/clients/metadata-clients.test.ts was dead code and has been deleted (closes libris-detd).
    • @types/bcryptjs — deprecated upstream; types are now bundled with bcryptjs itself.

    No functional changes — resolved versions are unchanged after the lockfile refresh (verified via pnpm list in each workspace).

  • e3e9196: Update drizzle-orm and drizzle-kit from 1.0.0-beta.18-7eb39f0 to 1.0.0-beta.21.

    Stays on the beta channel — the stable latest dist-tag (0.45.x) would be a downgrade, since the codebase already uses v1-only APIs (defineRelations() in src/db/relations.ts, imports from drizzle-orm/zod, and the new per-folder migration structure).

    Relevant changes between beta.18 and beta.21:

    • beta.20 (2026-03-27) — security: escaping fix for sql.identifier() and sql.as() (CWE-89, SQL injection). Libris does not use either function so we were not directly exposed, but defense-in-depth is cheap.
    • beta.19 (2026-03-23): SQL commenter support (.comment({ ... }))
      • drizzle-kit bug fixes around migration schema handling, index operator classes, and RDS Data API schema pulling.
    • beta.21 (2026-04-14): PostgreSQL enum migrations are no longer treated as commutative, and enum values added in different leaves now merge properly during migration generation.

0.23.0

Minor Changes

  • 346a032: Rebuild the stats page with Apache ECharts and expand it to seven charts.

    The reading-stats page previously shipped two hand-rolled DIV-based charts with inline-style height percentages and CSS flex tricks. They worked but had no axes, no real tooltips, no responsive redraw, and were fragile to extend. Replaced with Apache ECharts (Apache-2.0) via nuxt-echarts + vue-echarts, and the page now tells a richer reading story:

    1. Yearly pages-read heatmap (GitHub-contribution style, calendar year + year picker) — replaces the old 30-day bar.
    2. Genre distribution as a donut — replaces the stacked CSS bar.
    3. Books finished per month (current year, 12 bars).
    4. Reading velocity — 7-day moving average of pages per day over the last 90 days.
    5. Top 10 authors by organized-book count (horizontal bar).
    6. Days-to-finish distribution (fixed 6 buckets).
    7. Library growth — cumulative book count by month since the first book was added.

    Backend: /api/stats gains pagesHeatmap, finishedPerMonth, readingVelocity, topAuthors, daysToFinishBuckets, and libraryGrowth fields plus an optional ?year=YYYY query param for the heatmap. The now-unused dailyActivity field is removed. All new aggregations are scoped per API key.

    Frontend: new <StatChart> wrapper component (handles theme, autoresize, empty state, card shell) and useChartTheme composable (resolves an ECharts theme from Nuxt UI CSS custom properties — follows light/dark palette swaps automatically, no hardcoded hex values in chart components).

    Licensing note: explicitly chose Apache ECharts over ApexCharts, which moved to a dual-license freemium model (free only for orgs < $2M USD revenue; paid for commercial or OEM/redistribution). libris is OSS and self-hostable, so downstream forks and users at larger orgs benefit from the permissive license.

0.22.4

Patch Changes

  • 90fac7d: Admin diagnostics now list every registered queue. /api/jobs/status, /api/jobs/failed, and /api/settings/status previously read only the ingestion pipeline queues, hiding failures and activity from the hardcover-sync, progress-history-cleanup, and db-maintenance queues. They now use the full queue registry so operational visibility matches reality. The home dashboard's ingestion pipeline indicator is unchanged.
  • 78cf951: The scheduled cleanup-orphaned-files maintenance job now uses keyset pagination over book_files.id instead of OFFSET. The previous loop deleted rows from the same table it was paginating through, shifting later rows and silently skipping eligible orphans on each pass. With keyset pagination, a single nightly run processes every row whose storage_path no longer exists on disk. The handler has also been extracted from bootstrap.ts into its own worker module with regression test coverage.
  • 3c8b15b: Remove unsupported in-memory path overrides from the settings API. PATCH /api/settings no longer accepts libraryPath or inboxPath — these always come from the LIBRARY_PATH and INBOX_PATH environment variables. The previous override was half-broken: it was visible to API read handlers but the inbox watcher and book-organize worker still read the env vars directly, leaving the system in a split-brain state when overrides were applied. The settings UI was already display-only, so user-facing behavior is unchanged. PATCH /api/settings now only accepts the hardcoverMetadataEnabled and hardcoverSyncEnabled flags; the response body shrinks to { updated: string[] }.

0.22.3

Patch Changes

  • f36f496: Show uploader attribution in library and inbox views, add library language and uploader filtering, and replace the crowded library header filters with a consolidated filter panel.

0.22.2

Patch Changes

  • cde31d0: Avoid ambiguous external metadata lookups for corrupt or metadata-empty EPUBs by moving them into manual review, and show a clearer review message when no metadata sources are available.
  • c74393b: Add a BullMQ reset utility and use it in local and E2E reset flows so queue diagnostics reflect a truly fresh dev state.

0.22.1

Patch Changes

  • 664e492: Fix OpenTelemetry log export by aligning @opentelemetry/api-logs to a single version. The loglayer transport and SDK were using different versions, creating separate global registries so the SDK's LoggerProvider never reached the transport.

0.22.0

Minor Changes

  • f8fbb02: Add OpenTelemetry SDK initialization. Set OTEL_EXPORTER_OTLP_ENDPOINT to enable log and trace export via OTLP. Uses standard OTel environment variables -- no custom configuration needed. When unset, the SDK does not initialize and adds zero overhead.

0.21.1

Patch Changes

  • 0652683: Fix production crash by excluding @loglayer/transport-pretty-terminal from bundle. The dev-only transport transitively imports better-sqlite3 (native module) which doesn't exist in the Docker runtime. Switched to dynamic import so it's only loaded in development.

0.21.0

Minor Changes

  • c6ff439: Replace consola with LogLayer for structured logging

    • Dev: @loglayer/transport-pretty-terminal with moonlight theme
    • Prod: @loglayer/transport-pino for JSON output
    • OTel: opt-in via OTEL_ENABLED=1 with @loglayer/transport-opentelemetry and @loglayer/plugin-opentelemetry for trace_id/span_id correlation
    • New env vars: LOG_LEVEL (trace|debug|info|warn|error|fatal), OTEL_ENABLED (0|1)
    • Per-request child loggers with requestId and IP context
    • All 22 source files migrated from consola.withTag() to getLogger()

0.20.1

Patch Changes

  • 1df5cc2: Fix EPUB cover extraction for SVG-wrapped covers (Standard Ebooks)
  • 6172de2: Fix Hardcover sync rate limit retry budget leaking across books
  • 00ae7d1: Fix non-deterministic upload ownership for duplicate checksums
  • f5f9867: Add $onUpdate for updatedAt columns, index on hardcoverBookId, and unique constraint on series+seriesIndex
  • 83ddfa5: Wrap inbox rescan in transaction for atomic candidate delete and status reset
  • b958a58: Restrict settings status diagnostics to admin users only

0.20.0

Minor Changes

  • 28c4cc6: Migrate real-time events from SSE to WebSocket

    Replace Server-Sent Events (streamSSE + useEventSource) with WebSocket (@hono/node-ws + useWebSocket) for all real-time event streaming between backend and frontend.

    Backend:

    • Add @hono/node-ws adapter with createNodeWebSocket in app.ts
    • Replace SSE endpoint with WebSocket handler in events.ts using upgradeWebSocket
    • Add POST /__test/emit-event endpoint for deterministic E2E event testing

    Frontend:

    • Replace useEventSource with VueUse useWebSocket in useServerEvents composable
    • Add exponential backoff reconnect (1s-30s) and heartbeat keep-alive
    • Add runtimeConfig.public.wsBaseUrl for dev/prod WebSocket URL configuration

    E2E:

    • Add livePage fixture that allows WebSocket connections through
    • Add 4 new tests verifying WebSocket-driven UI reactivity (connection, inbox refresh, badge update, settings events)

0.19.0

Minor Changes

  • ff4d612: Add multi-user authentication with API key identity

    • API keys serve as user identity; first key is admin, subsequent keys are regular users
    • Admin-only: key creation/deletion, global settings, job management
    • Per-user: service credentials (OPDS, KoSync, Hardcover), reading progress, stats
    • Book ownership: creator can edit, others read-only, admin can edit all
    • OPDS and KoSync auth resolves per-user credentials by username
    • Hardcover sync worker processes all users independently
    • Frontend: admin-only UI sections, user label in sidebar, conditional edit controls
  • ff4d612: Add upload_registry table to track book ownership through the upload-to-detection pipeline

Patch Changes

  • ff4d612: Export clearAuthCaches for use by future privilege-editing routes
  • ff4d612: Add unit test coverage for multi-user auth access control
  • ff4d612: Clean up dead code: remove unused safeCompare, deduplicate DUMMY_HASH, remove redundant deviceFilter, fix route-policy shadowing
  • ff4d612: Skip caching when apiKeyId is missing to prevent cross-user cache sharing
  • ff4d612: Add missing requireBookOwnership check on candidates route
  • ff4d612: Fix Hardcover sync to retry books when edition pages unavailable and normalize Date usage
  • ff4d612: Default isAdmin to false in auth middleware to prevent undefined at runtime
  • ff4d612: Swap KoSync bcrypt comparison order to try MD5 first (common case)
  • ff4d612: Prevent deletion of the last admin API key
  • ff4d612: Fix service_credentials schema to use uniqueIndex instead of index
  • ff4d612: Add multi-user integration and E2E test suite with dual-user fixtures
  • ff4d612: Add test coverage for upload registry flow: checksum computation unit tests, upload route registry insert integration tests, and book-detected worker ownership attribution tests
  • ff4d612: Add composite unique constraint on (checksum, api_key_id) to upload_registry table, replacing the non-unique index. Use onConflictDoNothing on insert to prevent duplicate rows from concurrent uploads.

0.18.2

Patch Changes

  • dd3f003: Scope CSP header to API routes only so Nuxt SPA inline scripts are not blocked

0.18.1

Patch Changes

  • ac06294: Fix auth middleware blocking SPA static file routes when served by Hono

0.18.0

Minor Changes

  • 0633ad1: Serve SPA from Hono backend in production, consolidating two Docker images into one. Removes CORS, config.json runtime injection, nginx, and separate web Dockerfile. Adds static file serving with SPA fallback to Hono. CI/CD publishes a single unified image with composite version tag.

0.17.1

Patch Changes

  • b44c3d3: Normalize OpenAPI tag casing for reading-status routes from 'Reading Status' to 'reading-status' for consistency with all other tags
  • c6e752f: Add OpenAPI documentation to all OPDS catalog routes so they appear in the Scalar API docs at /_docs/scalar

0.17.0

Minor Changes

  • 234cad0: Add job detail view, all-jobs browser, job logs via BullMQ job.log(), and queue management actions (pause/resume/drain/clean)
  • b68c9bb: Consolidate Redis connections from ~20 to ~9 by sharing ioredis instances across BullMQ queues, replacing unstorage, and deduplicating queue instances

0.16.1

Patch Changes

  • d34297c: Remove overly strict Redis TLS requirement in production env validation

0.16.0

Minor Changes

  • 5d8f42b: Add OpenAPI documentation to inbox cover, inbox upload, library cover, and library download routes

Patch Changes

  • 241b97e: Fix fallback DATABASE_URL in drizzle.config.ts to match devenv database name
  • 22ba82e: Remove duplicate API test step from CI workflow
  • bacaa4e: Remove unbounded .returning() from progress history cleanup DELETE
  • 5a63107: Update tsdown build target from node22 to node24 to match runtime
  • 953c40b: Escape ILIKE wildcard characters in series search query to prevent % or _ from matching all series
  • 96ba0c0: Fix parseRedisUrl to pass tls: true when the URL scheme is rediss://, preventing silent TLS downgrade in production
  • 7c2e560: Replace sql.raw() with parameterized interval in reading-status queries
  • 8ca620d: Remove redundant double type assertion in hardcover-sync worker
  • 0c400ea: Fix fallbackExtract resolving promise twice in epub.ts metadata extractor
  • d3fb616: Fix infinite retry loop on Hardcover API rate limits by adding a maximum retry count of 5 to all rate-limit retry logic in hardcover-sync and matching workers
  • 040d7c5: Add onConflictDoUpdate to book-parse-file insert for retry safety

0.15.3

Patch Changes

  • 2c20a70: Fix Docker builds and release pipeline: remove stale root tsconfig.json COPY from both Dockerfiles, restructure publish workflow so images are built before the version bump is committed to main.

0.15.2

Patch Changes

  • 519bca5: Fix Docker build: remove stale COPY tsconfig.json that referenced a deleted root-level file. The service-level tsconfig is already included via COPY services/api-hono/.

0.15.1

Patch Changes

  • 66c4b08: Security hardening for open-source release:

    • Fix SQL injection via string interpolation in test DB setup (use parameterized query)
    • Enforce CORS_ORIGIN != '*' in production at startup (was warn-only, now throws)
    • Enforce REDIS_URL must use rediss:// (TLS) in production
    • Add security headers to nginx: X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy
    • Add .env.example template for contributors
    • Unignore .env.example and .env.test.example in .gitignore
  • 125b165: Quality: replace raw SQL with Drizzle (inArray/ne/desc), Hono built-in bodyLimit, response compression, stats caching, graceful shutdown drain, typed normalized jsonb column

  • b417375: P3 quality and security improvements:

    • Add X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset headers to all rate-limited responses
    • Fix OPDS Basic auth timing side-channel: always run bcrypt.compare() regardless of username match
    • Add keyboard shortcuts help modal (press ? to open) listing all navigation shortcuts
    • Replace z.catch() with .optional().default() in API query schemas — enables static OpenAPI spec generation
    • Generate openapi.json at build time; docs now import it statically instead of relying on runtime fetch
  • 75ea45d: Security: protect OPDS downloads with auth, fix rate limit IP spoofing, cache bcrypt validation, fix book delete path bug, fix README broken link

0.15.0

Minor Changes

  • e4184da: Show series name and position on library book cards and list view, and add a "Series order" sort option

0.14.1

Patch Changes

  • 2a67fec: DB hardening: Drizzle relations, index audit, materialize book_id on reading_progress

    • Add Drizzle relations for books → bookFiles and books → bookMetadataCandidates (j7u)
    • Add missing indexes: book_files.checksum, books.isbn_13, books(status, created_at) composite, reading_progress_history(document, created_at) composite, hardcover_sync_log.last_synced_at (acf)
    • Remove redundant indexes superseded by unique constraints: reading_progress_doc_device_idx, hardcover_sync_log_book_id_idx, books_status_idx, reading_progress_history_doc_idx (acf)
    • Materialize book_id on reading_progress and reading_progress_history, eliminating OR-join bottleneck across reading-status, stats, and hardcover-sync queries (pnx)
    • Populate book_id at write time in KoSync PUT endpoint via content hash lookup
    • Backfill migration for existing reading progress records
  • 8916fd2: Complete E2E test suite optimization and data-testid audit

    data-testid coverage (tasks 6xjv, 9yh5, z0v9, 81kn):

    • Added testids to library/index.vue: search, view toggle, book cards, filters, pagination
    • Added testids to inbox/index.vue: upload btn, search, sort, pagination
    • Added testids to home page: section containers, stat value cards (de-duplicated), book cards
    • Added testids to inbox/[id].vue action buttons: rescan, delete, approve
    • Added testids to library/[id].vue: cover image, download button
    • Fixed 4 duplicate stat-value testids in stats.vue → specific names
    • Added activity-chart testid to stats.vue chart container
    • Added testids to ConfirmDialog.vue and ApiError.vue

    Fragile selector replacement (task 6ugb):

    • Replaced locator("section").filter({hasText}) with section testids in home/stats tests
    • Replaced CSS class selectors (.flex.items-end) with activity-chart testid
    • Replaced img[alt='...'] with book-cover-img testid in library tests
    • Replaced getByRole("button", { name: ... }) with testid selectors for action buttons

    Test count reduction (tasks bxqe, jts5, z0oi):

    • book-progress.spec.ts: 8 tests → 3 (consolidated visibility checks into one test)
    • reading-status.spec.ts: 4 identical tab tests → 1 parametrized loop
    • Removed 3 redundant navigation tests (home ×2, reading-status ×1)
    • Total: 99 → 91 E2E tests

    OPDS tests moved (task rxoq):

    • Created services/api-hono/src/routes/opds.test.ts with 7 Vitest integration tests
    • OPDS Playwright tests skipped (no browser needed for HTTP API tests)

    CI sharding (task upf4):

    • Full suite on main now runs as 2 parallel shards (each with own DB/Redis)
    • PR smoke tests remain single job
    • Estimated ~50% CI wall-time reduction on main push
  • b674b64: Reset migrations to single init snapshot, resolving Drizzle v0→v1 migration tracking incompatibility.

  • 19397c8: Fix metadata upsert crash on retry and switch fileSize to number mode

    • Add ON CONFLICT DO UPDATE to metadata candidate insert in book-fetch-metadata worker (retries no longer crash on unique constraint violation)
    • Switch bookFiles.fileSize from bigint mode:'bigint' to mode:'number' (avoids BigInt serialization overhead; ebooks are well under 2^53 bytes)

0.14.0

Minor Changes

  • 6fb5245: Add series support: group books by series with browsing, metadata extraction, and OPDS feeds
    • Add series and series_index columns to books table with partial index and search vector
    • Extract series from EPUB calibre:series meta tags and Hardcover API (Typesense + GraphQL)
    • Embed calibre:series/calibre:series_index in EPUB files on export
    • Write series data from Hardcover matching to books table
    • Add GET /api/series and GET /api/series/:name API endpoints
    • Add series filter to GET /api/library and series facet to GET /api/library/facets
    • Add OPDS series navigation and acquisition feeds
    • Add series listing page, series detail page, and sidebar navigation
    • Add series/seriesIndex to MetadataFieldPicker, EditBookModal, and book detail page
    • Add series filter dropdown to library page

Patch Changes

  • bd1a814: Fix series UX and dev infrastructure
    • Combine series name and position into a single grouped field in MetadataFieldPicker
    • Add Nitro devProxy so API requests go through same origin in dev (fixes SSE/cookie issues)
    • Use relative URLs for EventSource (SSE) connection
    • Consolidate migrations into single drizzle-kit compatible migration with snapshot

0.13.1

Patch Changes

  • ed90947: Fix Hardcover sync to use edition page count instead of local page_count

    The sync worker now fetches the page count from the matched Hardcover edition instead of using the local page_count (which may come from EPUB metadata for a different edition). Also adds a backfill step that updates local page_count from Hardcover edition data for all already-matched books.

0.13.0

Minor Changes

  • 7b2b14d: Drop support for non-EPUB formats (PDF, MOBI, AZW3, CBZ, CBR)
  • 7b2b14d: Remove Google Books metadata source, making Hardcover the sole external metadata provider

Patch Changes

  • 7b2b14d: Auto-populate page_count from Hardcover edition during ISBN matching

0.12.2

Patch Changes

  • 8c57241: Remove unsupported progress field from Hardcover API calls, only send progress_pages

0.12.1

Patch Changes

  • 2470ae4: Fix orphaned files cleanup deleting all book_files records

    The cleanup job used relative storagePath without prepending LIBRARY_PATH, causing every lstat check to fail. Added rebuild-book-files startup job to recover.

0.12.0

Minor Changes

  • 08c02b2: Fix KoSync progress matching and improve Hardcover sync
    • Use KoReader-compatible partial MD5 for content hashing (fixes progress not showing on dashboard)
    • Store original content hash before EPUB metadata embedding for dual-hash matching
    • Add startup backfill job to recompute existing content hashes
    • Prevent Hardcover sync from overwriting existing statuses with "Want to Read"
    • Send percentage and started_at to Hardcover API (not just progress_pages)
    • Send reading progress for all statuses, not only "currently reading"

0.11.4

Patch Changes

  • 4116e04: retrigger deploy

0.11.3

Patch Changes

  • db1eae3: Fix OPDS pagination returning stale cached data for all pages

    unstorage's normalizeKey strips everything after ? in keys, so routes:/opds/books?page=1 and routes:/opds/books?page=2 resolved to the same cache key. Encode query params as a path segment instead.

0.11.2

Patch Changes

  • 44be8f1: Fix OPDS feed links using http:// instead of https:// behind reverse proxies

    OPDS feeds generated http:// links for sub-pages because getBaseUrl read the internal Hono URL (behind TLS-terminating proxy) instead of the external URL. KOReader correctly drops Authorization headers when following links that downgrade from HTTPS to HTTP, causing auth failures on all sub-page navigations (books, search, genres, etc).

    Now respects the X-Forwarded-Proto header set by Traefik/nginx to generate correct https:// links.

0.11.1

Patch Changes

  • b762556: Fix Hardcover sync failures caused by upstream GraphQL API type renames (Hasura snake_case → PascalCase)

0.11.0

Minor Changes

  • d308b84: Support OPDS auth via ?key= query parameter for clients like KOReader that don't send HTTP Basic auth headers

  • 29b5dc5: Simplify OPDS and KoSync authentication to username/password only

    OPDS and KoSync now authenticate exclusively via service credentials (username + password) configured through the Settings UI. Removed API key fallback for OPDS, ?key= query parameter support, and KOSYNC_USERNAME/KOSYNC_PASSWORD_HASH environment variables.

    Breaking changes:

    • OPDS no longer accepts API keys (Bearer, Basic with API key, or ?key= query param). Set OPDS credentials in Settings.
    • KoSync no longer falls back to KOSYNC_USERNAME/KOSYNC_PASSWORD_HASH env vars. Set KoSync credentials in Settings.

0.10.0

Minor Changes

  • c30505a: Fix KoSync authentication for KOReader: store bcrypt(md5(password)) since KOReader sends MD5-hashed passwords, and return userkey in auth responses for KOReader compatibility

Patch Changes

  • 24a7b3a: Fix Bruno import: use lightweight OpenAPI-only server that doesn't require DB/Redis

0.9.4

Patch Changes

  • 1ff0518: Add temporary debug logging to KoSync and OPDS auth paths to diagnose KOReader authentication failures

0.9.3

Patch Changes

  • 73aa619: Fix OPDS auth for HTTP clients (KOReader): return WWW-Authenticate: Basic header on 401 responses so clients know to send credentials

0.9.2

Patch Changes

  • 00e7cdf: Fix KoSync login from KOReader: add GET handler for /kosync/users/auth that reads credentials from x-auth-user/x-auth-key headers

0.9.1

Patch Changes

  • 7a7fefe: fix(api-hono): add filename heuristic fallback for EPUB cover extraction and embedding

    EPUB cover extraction now falls back to scanning ZIP entries for files named cover.{jpg,jpeg,png,webp,gif} when the OPF metadata doesn't declare a cover via standard EPUB2/EPUB3 patterns. This fixes cover extraction for many real-world EPUBs that have embedded covers but don't reference them in the OPF.

    The metadata embedding step also uses the same heuristic — when replacing a cover during organize, it now finds and replaces the existing cover.jpeg instead of adding a duplicate cover-embedded.jpg alongside it.

0.9.0

Minor Changes

  • e8ef69a: fix(api-hono,web): inbox cover endpoint falls back to coverUrl proxy, frontend uses coverUrl directly

    The inbox cover endpoint previously only extracted covers from EPUB files and returned 404 for everything else. Now it tries EPUB extraction first, then falls back to proxying the book's coverUrl from external metadata sources (Google Books, Hardcover). Non-EPUB books with a coverUrl now get covers too.

    The frontend inbox list now uses coverUrl directly from the API response when available (instant, no extraction round-trip), falling back to the server-side endpoint for EPUB extraction or coverUrl proxy.

    Added diagnostic logging throughout the cover extraction pipeline to aid debugging.

0.8.1

Patch Changes

  • 45bc6a4: Fix cover image caching and EPUB metadata extraction
    • Stop EPUB extractor from putting internal file paths (e.g. images/cover.jpg) into coverUrl. This field is reserved for external HTTP URLs from metadata sources. The organize worker already extracts EPUB covers directly via extractEpubCoverImage().
    • Add ETag headers to library and OPDS cover endpoints for proper cache revalidation when covers change (e.g. after force re-download). Supports If-None-Match for 304 responses.
    • Change library and inbox cover Cache-Control from public to private since these endpoints require authentication.

0.8.0

Minor Changes

  • 3e8e53e: Support updating cover images on organized books
    • Add forceRedownloadCover flag to organize worker so it re-downloads covers when the URL changes
    • PATCH /api/library/:id now accepts coverUrl and enqueues an organize job to download the new cover
    • apply-metadata endpoint passes forceRedownloadCover when coverUrl is in the update set
    • EditBookModal now includes a Cover Image URL field

0.7.0

Minor Changes

  • d525462: feat: aggregate settings endpoint + Pinia Colada migration

    Backend: Add GET /api/settings/status aggregate endpoint that returns health checks, job queue status, failed jobs, app settings, and all credential statuses in a single Promise.all() call. Reduces settings page API calls from 9 to 1.

    Frontend: Replace Nuxt's useAsyncData with Pinia Colada's useQuery/useQueryCache across all pages. Add @pinia/nuxt and @pinia/colada-nuxt modules. Create reusable query composables in composables/queries/. Add skeleton loaders to the settings Connections tab.

    Migrated pages: settings, dashboard, library list, library detail, inbox list, inbox detail, reading status, stats, and layout sidebar.

0.6.0

Minor Changes

  • 34aa0ea: Add independent toggles for Hardcover metadata fetching and reading progress sync

    Users can now control Hardcover features independently from the Settings > Connections page:

    • "Use as metadata source" — enable/disable Hardcover as a book metadata provider during ingestion
    • "Sync reading progress" — enable/disable pushing reading status and progress to Hardcover

    Both default to enabled when a Hardcover token is configured (backward compatible). Settings are persisted in a new app_settings database table and survive server restarts.

Patch Changes

  • 678ec8c: Fix OpenAPI spec generation failing on z.catch() schemas

    Add explicit .openapi({ type }) metadata to all Zod schemas using .catch() so @hono/zod-openapi can serialize them to the OpenAPI spec. Previously, the /_docs/openapi.json endpoint returned an "Unknown zod object type" error.

0.5.3

Patch Changes

  • 72f3a5b: Align Hardcover and Google Books metadata search strategy: both now validate ISBN check digits and fall back to title+author when ISBN is invalid
  • 6c51b40: Fix Google Books metadata test assertions to match ISBN-only search behavior

0.5.2

Patch Changes

  • c19d555: Fix Google Books search returning no results when ISBN is available

    Two bugs in query construction: (1) ISBN was combined with intitle: and inauthor: qualifiers, making queries too restrictive since Google's metadata often doesn't match exactly. Now uses ISBN alone when available. (2) Field qualifiers were joined with + which gets double-encoded by ofetch's URL encoding — switched to space separator.

0.5.1

Patch Changes

  • 7f7da5f: Fix Docker build: use existing node user (UID 1000) instead of creating app user

    node:24-slim already has a node user at UID 1000, causing useradd to fail with "UID is not unique".

0.5.0

Minor Changes

  • 95b0120: Complete Nitro-to-Hono cutover with SPA pivot.

    API (@libris/api-hono):

    • Export typed hc client via @libris/api-hono/client
    • Add cookie-based auth endpoints (login/logout/session) for SPA frontend
    • Auth middleware supports httpOnly cookie fallback alongside Bearer tokens
    • CORS supports credentials for cross-origin cookie auth
    • Add COOKIE_DOMAIN env var for subdomain cookie scoping
    • Fix z.unknown() in approve endpoint — now validates value types
    • Add missing DB indexes (books.status, reading_progress.device, hardcover_sync_log.last_status)
    • Add FK constraint on books.possibleDuplicateOf
    • Configure DB connection pool (max 20, idle timeout, max lifetime) and graceful shutdown
    • Await job scheduler registration on startup (no more silent failures)
    • Batch orphaned file cleanup to avoid OOM on large datasets
    • Delete DB record before file cleanup in book delete (prevents orphaned files)
    • Sanitize filesystem paths from error messages
    • Add error handling to fire-and-forget event publishing
    • Standardize reading-status pagination response shape
    • Read OpenAPI version from package.json
    • Fix serviceCredentials.updatedAt to NOT NULL

    Web (@libris/web):

    • Switch to SPA mode (ssr: false), drop Pinia
    • Delete entire BFF server layer (proxy, session, CSRF)
    • Use hc typed client directly in all pages via useApiClient()
    • Replace useAuthStore with useAuth() composable (useState-based)
    • Add useUpload() composable for XHR file upload with progress
    • All pages use useAsyncData + hc — fully typed API calls
    • Dockerfile serves static files via nginx (no Node.js runtime)
    • Add search debounce (300ms) to library and inbox
    • Add lazy loading to cover images in grid views
    • Fix SSE race condition on component unmount
    • Extract shared pagination constant

    Removed: services/api/ (old Nitro backend), Dockerfile.api, BFF server, Pinia

Patch Changes

  • dd9a3b6: Fix audit regressions from Hono migration

    • Add removeOnComplete with TTL and removeOnFail to all BullMQ pipeline queues
    • Configure lockDuration, stalledInterval, and maxStalledCount on all workers
    • Add retryStrategy and reconnectOnError to event bus Redis connections
    • Add foreign key constraint on books.possibleDuplicateOf (self-reference with ON DELETE SET NULL)
    • Exclude searchVector from all book queries and response schemas (never sent to clients)
  • 6dea8ff: fix(e2e): resolve auth template reactivity, CORS, and approve error handling

    • Destructure useAuth() so isAuthenticated is a top-level ref that Vue auto-unwraps in templates (was a nested ComputedRef in a plain object, always truthy)
    • Add CORS_ORIGIN to CI and test-e2e.sh so the SPA (port 3100) can make credentialed requests to the API (port 3000)
    • Check res.ok in inbox approve/delete handlers — hono hc client doesn't throw on 4xx/5xx
    • Fix E2E assertions for cover/download URLs that now include the full API base URL in SPA mode
    • Add dist/ to api-hono .gitignore so oxfmt skips bundled output

0.3.0

Minor Changes

  • 131dd0c: Fix Docker image: correct ESM entrypoint (.mjs), migrations path, and skip lifecycle scripts during build.