* feat(reader): add PDF highlights and annotations Persist and render PDF highlights through the same annotation system as EPUB. The annotation_positions table already supported a 'pdf' format, so this wires the web create path, response, and reader UI to it. Backend: - create-annotation DTO accepts a pdf position (page + segment rects) as an alternative to a cfi; exactly one is required - repository createPdf() persists a format='pdf' position (geometry in pos0, pageno in extras); selectWithCfi exposes the pdf geometry and status - response DTO parses the pdf position and derives positionStatus Frontend (pdf-v4 reader): - usePdfAnnotations REST store and usePdfHighlights orchestration render persisted highlights via the EmbedPDF annotation plugin (autoCommit off, DB stays source of truth) - selection popup (colour/style/note/copy/delete), note dialog, and a Highlights sidebar tab with search/colour/notes filters, navigate, delete - NoteDialog relocated to reader/shared for reuse by both readers Shared types gain AnnotationRect/AnnotationPdfPosition and an optional pdf field on AnnotationItem. * fix(reader): preserve input on failed PDF annotation save; convert mixed PDF+CFI for KOReader - usePdfHighlights: createFromSelection/restyleExisting now return success; applyHighlight and saveNote only clear the selection, dismiss the popup, and close the note dialog on success. On failure the mark, popup, and typed note are preserved so the user can retry without losing input. - koreader annotation exchange: the PDF push-down guard now skips only PDF-only annotations (no EPUB anchor). A mixed PDF+CFI annotation still converts its CFI to xpointer instead of being dropped. - Tests for both paths. * fix(reader): label PDF selection buttons and dedupe multi-page highlight retries - PdfSelectionPopup: add aria-labels to the copy, highlight, note, and delete icon-only buttons so screen readers can identify them. - usePdfHighlights: createFromSelection keeps only the pages that failed, so a retry after a partial multi-page failure never duplicates already-saved pages. - Test for the multi-page partial-failure retry path. * fix(reader): harden PDF annotation store, validation, and create logging - usePdfAnnotations: catch rejected api() calls (session expiry / network) so load() sets loadError and mutations return null/false instead of rejecting. - create-annotation DTO: require positive rect width/height so degenerate PDF geometry is rejected; tests for zero/negative dimensions. - annotation.service: add [annotation.create] start/end/fail lifecycle logs with IDs, durationMs, and sanitized error for the multi-step create. * test(reader): cover PDF annotations in the hub and add missing docstrings Adds a real-DB e2e proving a PDF highlight persists as a pdf position and surfaces in the library-wide Annotations hub with the exact fields the page deep link needs (jumpFileFormat=pdf, cfi=null, pageno). Adds a hub-service unit test for the pdf row mapping. Documents the PDF render helpers, the annotation deep-link builder, and the createPdf repository method. * fix(annotations): make PDF highlights reliable across reloads Reconcile persisted highlights after EmbedPDF finishes loading its annotation state. Keep file-scoped pagination bounded and prevent PDF-only positions from starving device sync. Ref #1195 * fix(annotations): keep pdf annotations stable during reload Use authenticated native pagination fetches and discard responses that overlap successful mutations. Align shared dialog styling and annotation tests with project conventions. Ref #1195 --------- Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
BookOrbit - Server
NestJS 11 backend running on Fastify.
Running
From the repo root: pnpm dev (starts both server and client).
Server only: pnpm start:dev (from this directory).
The server loads environment variables from .env in this directory. See .env.example for all available options.
API docs
OpenAPI docs are disabled by default. Enable them with SWAGGER_ENABLED=true:
SWAGGER_ENABLED=true pnpm start:dev
Then open:
- Swagger UI: http://localhost:3000/api/docs
- OpenAPI JSON: http://localhost:3000/api/docs-json
Dev watch uses a Nest config without the Swagger metadata generator so the
server does not restart immediately after startup. Full build output still uses
the standard Nest config and generates src/metadata.ts for enriched docs.
Module structure
Each feature is a self-contained NestJS module under src/modules/:
src/modules/
├── auth/ Authentication (JWT, OIDC, password reset)
├── book/ Book CRUD, cover images, file serving
├── library/ Library management
├── scanner/ File system scanning, metadata extraction
├── collection/ User book collections
├── smart-scope/ Saved search/filter views
├── user/ User management
├── role/ Roles and permissions (RBAC)
├── metadata/ Book metadata providers
├── kobo/ Kobo device sync
├── annotation/ Highlights and annotations
├── bookmark/ Reading position bookmarks
├── reader-preferences/ Per-user reader settings
├── cbz/ CBZ archive handling
├── path/ File path utilities
├── seed/ Database seeding
└── app-settings/ Global application settings
Database
Schema files live in src/db/schema/, one per domain. Migrations are generated by Drizzle Kit.
pnpm db:generate add_some_column # generate migration from schema diff
pnpm db:migrate # apply pending migrations
pnpm db:studio # open Drizzle Studio (DB browser)
Testing
pnpm test # unit tests
pnpm test:watch # unit tests in watch mode
pnpm e2e:run # app-smoke end-to-end test from server package
pnpm test:cov # unit tests with coverage
For suite-based e2e runs, run from the repository root:
pnpm run e2e:list
pnpm run e2e:all
pnpm run e2e:run -- scanner-scenarios
Quality
pnpm type-check # TypeScript checks (no emit)
pnpm lint:check # Non-mutating lint checks
pnpm lint:fix # Auto-fix lint issues
pnpm format:check # Prettier check
pnpm format # Prettier write
Key conventions
- Constructor injection with
@Inject(DB)for the Drizzle instance. - Config via
@nestjs/confignamed configs (appConfig,dbConfig,authConfig,storageConfig,mailerConfig). Never readprocess.envdirectly in services. - DTOs use
class-validatordecorators. GlobalValidationPiperejects unknown fields. - Throw NestJS
HttpExceptionsubclasses (NotFoundException,ForbiddenException, etc.) - never rawError. - All user-owned data is scoped by
userId. Service methods check ownership.