Skip to content

Frontend

The web app lives at apps/web/ and is built with Nuxt 4, Vue 3, Nuxt UI v4, Pinia, and Tailwind CSS v4. It runs on port 3100 in development.

Pages

RouteFilePurpose
/pages/index.vueDashboard: stats cards, currently reading, recent additions, pipeline status
/inboxpages/inbox/index.vuePaginated inbox list with search, sort, upload button, processing status
/inbox/:idpages/inbox/[id].vueMetadata review: candidate picker, duplicate warnings, approve/delete/rescan
/librarypages/library/index.vueLibrary grid/list view with author/genre filters, full-text search, pagination
/library/:idpages/library/[id].vueBook detail: metadata, files, downloads, collection picker, edit/delete
/readingpages/reading/index.vueReading status views — shows books filtered by status
/reading/:statuspages/reading/[status].vueFiltered reading list: reading, finished, unread, paused
/statspages/stats.vueReading analytics: books finished, streaks, daily activity chart, genre distribution
/settingspages/settings.vueAuth setup/login, OPDS/KoSync config, server health, job queues, failed jobs

Authentication Flow

Uses a Backend-for-Frontend (BFF) pattern — the browser never sees the raw API key.

  1. User enters API key on /settings (or runs first-time setup)
  2. Nuxt BFF server route (/auth/login) validates the key and sets an httpOnly books-auth cookie
  3. All subsequent API requests go through Nuxt's server routes, which read the cookie and inject Authorization: Bearer headers
  4. Global middleware (auth.global.ts) checks session on every navigation, redirects unauthenticated users to /settings
  5. Client plugin (auth-hydrate.client.ts) verifies SSR/client state match after hydration

Layout

Single default.vue layout with:

  • Sidebar: Logo, search button, nav links (Home, Inbox with badge, Library, Stats), Reading section (Reading, Finished, Unread, Paused links with count badges), Settings link with failed jobs badge, color mode toggle
  • Global search: Debounced (200ms) command palette searching books and navigation links
  • Content area: NuxtPage with loading indicator

Composables

ComposablePurpose
useApi()Thin $fetch wrapper — auto 401 handling, SSR cookie forwarding
useDashboard()Shared keyboard shortcuts (G+H/I/L/S for navigation)
useInbox()Inbox API: list, get, upload, rescan, approve, delete, candidates, processing, count
useLibrary()Library API: list, get, update, delete, reorganize, facets, cover/download URLs
useReadingStatus()Reading status counts and filtered lists
useHardcover()Hardcover connection status, sync trigger, sync log
useSettings()Settings API: health, jobs, credentials, setup
useServerEvents()SSE event streaming for real-time updates

Components

ComponentPurpose
MetadataFieldPickerMulti-source metadata selector — field-by-field candidate selection with manual entry and validation
UploadBookModalDrag-and-drop file upload with progress tracking
EditBookModalBook metadata edit form with Zod validation
ConfirmDialogReusable confirmation dialog
ColorModeToggleDark/Light/System theme switcher
ApiErrorError display with retry button
RefetchMetadataModalModal for re-fetching and selecting metadata from external sources

Store

auth.ts (Pinia): Manages authenticated and checked state. Actions: check(), login(apiKey), logout(), setAuthenticated(value).

Keyboard Shortcuts

  • G then H — Go to Home
  • G then I — Go to Inbox
  • G then L — Go to Library
  • G then S — Go to Settings
  • Escape — Back to parent page (from detail views)

Styling

  • Theme: Nuxt UI v4 with blue primary, zinc neutral
  • Font: Public Sans (sans-serif)
  • Dark mode: Full support via useColorMode()
  • Icons: Lucide via Iconify

Form Validation

Zod schemas in utils/schemas.ts: ISBN-10/13 format, year range (1000-2100), page count, cover URL (HTTP/HTTPS).