Files
bookorbit/server
neon eba1640df4 fix(kobo): acknowledge device state pushes so hub progress reaches the reader
PUT /v1/library/{id}/state answered with the stored ReadingState instead of the
RequestResult/UpdateResults envelope the device expects. The device leaves an
unacknowledged push pending and treats its own copy as authoritative, so it
re-pushes every sync and opens the book at its local bookmark no matter what the
pull path delivered. That is why a first download honours the hub position while
an already-synced book only picks up the percentage.

Three smaller defects produced the same reversion on their own:

Hub progress kept the device's previous CurrentBookmark.Location when no KoboSpan
described the new position, shipping a bookmark whose position contradicted its own
percent. Drop the Location instead; the pull path re-adds one whenever it can
convert the cfi.

Both hub-side writers stamped last_modified_kobo, priority_timestamp and the
bookmark at wall-clock time while device timestamps are stored verbatim. A Kobo
clock running ahead therefore won every conflict. Stamp strictly past whatever is
stored, mirroring what upsertState already did for the envelope.

A conversion that gave up was invisible: getRawState swallowed throws and the
converter logged reasons at debug level, so an operator could not tell a healthy
bookmark from one that silently degraded to percent-only.

Closes #749
2026-08-16 17:45:20 +00:00
..

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:

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/config named configs (appConfig, dbConfig, authConfig, storageConfig, mailerConfig). Never read process.env directly in services.
  • DTOs use class-validator decorators. Global ValidationPipe rejects unknown fields.
  • Throw NestJS HttpException subclasses (NotFoundException, ForbiddenException, etc.) - never raw Error.
  • All user-owned data is scoped by userId. Service methods check ownership.