feat(readwise): sync highlights to Readwise (#578)

* feat(types): add readwise_sync permission

* feat(types): add readwise settings types

* feat(db): add readwise_user_settings table

* feat(readwise): add constants

* feat(readwise): add throttle queue

* feat(readwise): add REST client

* feat(readwise): add repository

* feat(readwise): add settings service

* feat(readwise): add sync service with watermark

* refactor(readwise): drop unused logger in settings service

* feat(readwise): add debounced scheduler

* feat(readwise): add event listener

* feat(readwise): add controller and dtos

* feat(readwise): wire module into app

* test(readwise): add readwise_sync to authorization matrix

* feat(annotation): emit annotation.created on device sync ingest

* feat(client): add readwise api client

* feat(client): add useReadwiseSettings composable

* feat(client): add Readwise settings page

* feat(client): add Readwise settings route

* feat(client): add Readwise to settings nav

* fix(readwise): avoid typeof this in settings type annotation

Production build (nest build → tsc via tsconfig.build.json) rejects
`Parameters<typeof this.repo.upsertSettings>[1]` with TS2304. Reference
the repository class type directly so the prod image compiles.

* fix(client): add Readwise sync to admin permission list

The admin UserFormDrawer renders a hardcoded permission list, not an
iteration of PERMISSION_LABELS, so readwise_sync was ungrantable via UI.
Add it to the Devices & Access group and the Standard preset, mirroring
Hardcover sync.

* feat(readwise): send book cover to Readwise via OpenLibrary ISBN

Populate the Readwise highlight image_url with the OpenLibrary cover URL
built from the book's isbn13 (falling back to isbn10). Uses ?default=false
so a missing cover yields 404 (no image) rather than a blank placeholder.
Books without an ISBN send no image_url. BookOrbit's own cover endpoint is
auth-gated and stores no public source URL, so a public third-party cover
URL is the zero-infra way to give Readwise an image.

* test(readwise): add type parameters to vi.fn mocks

oxlint (vitest/require-mock-type-parameters) fails on untyped vi.fn()
in the settings page test; the pre-commit hook only runs eslint, so
this surfaced under full lint:check.

* docs(readme): add Readwise Sync to features list

* fix(readwise): surface settings fetch errors in the UI

The settings page destructured error from useReadwiseSettings but never
rendered it. A failed fetch on mount left the user with an empty default
form and no indication that loading failed. Render an error banner when
error is set, and cover it with component tests.

* test(readwise): cover settings page token test, save, and visibility toggle

Adds component tests for the Test-connection flow (empty/valid/invalid
token), Save (success clears the field, failure surfaces an error toast),
and the show/hide token toggle — the interactive handlers Codecov flagged
as uncovered in ReadwiseSettings.vue.

* test(readwise): match settings buttons by exact label in test helper

Use an exact text match instead of substring matching in the testButton
helper so it can never return the wrong button if labels overlap.

* fix(readwise): harden highlight sync

---------

Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
Co-authored-by: Oscar Nogueira Neto <oscarnogueira@gmail.com>
This commit is contained in:
Oscar Nogueira Neto
2026-07-07 21:09:28 -04:00
committed by GitHub
parent d82a018d1e
commit 4eed87a6fd
47 changed files with 30425 additions and 10 deletions
+2
View File
@@ -64,6 +64,7 @@ import { AppInfoModule } from './modules/app-info/app-info.module';
import { ReleaseNotesModule } from './modules/release-notes/release-notes.module';
import { AchievementModule } from './modules/achievement/achievement.module';
import { HardcoverModule } from './modules/hardcover/hardcover.module';
import { ReadwiseModule } from './modules/readwise/readwise.module';
import { CustomMetadataModule } from './modules/custom-metadata/custom-metadata.module';
import { CustomIconModule } from './modules/custom-icon/custom-icon.module';
@@ -139,6 +140,7 @@ import { CustomIconModule } from './modules/custom-icon/custom-icon.module';
ReleaseNotesModule,
AchievementModule,
HardcoverModule,
ReadwiseModule,
],
providers: [
{ provide: APP_INTERCEPTOR, useClass: AuditInterceptor },