Skip to content

Frontend

The web app lives at apps/web/ and is a Vue 3 SPA built with Vite 8, vue-router 5 (file-based routing via vue-router/unplugin/vite + data loaders via vue-router/experimental/pinia-colada), Nuxt UI v4 (the component library — framework-agnostic), and Tailwind CSS v4. It runs on port 3100 in development.

Pages

Pages live under src/pages/ (auto-routed by vue-router's file-based routing).

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, and uploader attribution
/inbox/:idpages/inbox/[id].vueMetadata review: candidate picker, duplicate warnings, approve/delete/rescan, uploader attribution
/librarypages/library/index.vueLibrary grid/list view with full-text search, pagination, and a consolidated filter panel for author, genre, language, series, and uploader
/library/:idpages/library/[id].vueBook detail: metadata, uploader attribution, files, downloads, collection picker, edit/delete
/readingpages/reading/index.vueThin redirect to /reading/reading (sidebar anchor for the Reading section)
/reading/:statuspages/reading/[status].vueFiltered reading list: reading, finished, unread, paused
/seriespages/series/index.vueSeries browsing page with search, cover grid, and book counts
/series/:namepages/series/[name].vueSeries detail: ordered book list with position numbers, covers, genres
/statspages/stats.vueReading analytics: books finished, streaks, daily activity chart, genre distribution
/settingspages/settings.vueAuth setup/login, OPDS/KoSync/Hardcover config (with metadata and sync toggles), server health, jobs browser, failed jobs, queue management, paths

Authentication Flow

The SPA authenticates directly with the Hono API via httpOnly cookies.

  1. User enters API key on /settings (or runs first-time setup)
  2. The SPA calls the Hono API's /api/auth/login endpoint, which validates the key and sets an httpOnly auth cookie
  3. All subsequent API requests include the cookie automatically — the Hono API reads it to authenticate
  4. A router.beforeEach guard (src/router/guards.ts) checks auth state on every navigation and redirects unauthenticated users to /settings

Layout

Single default.vue layout with:

  • Sidebar: Logo, search button, nav links (Home, Inbox with badge, Library, Series, 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: <RouterView>

Composables

Composables are split into general-purpose utilities (composables/) and data-fetching queries (composables/queries/). All queries use Pinia Colada (useQuery/defineQuery) with automatic caching and stale-time management.

General

ComposablePurpose
useApiClient()Typed Hono RPC client — cookie-based auth, automatic error wrapping via ApiError
useAuth()Auth state (isAuthenticated, checked) and methods (check, login, logout)
useDashboard()Shared keyboard shortcuts (G+H/I/L/S navigation, ? shortcuts modal)
useDebouncedSearch()Reactive search/debouncedSearch ref pair with configurable delay (default 300ms)
useUpload()XHR-based file upload with progress tracking and cancellation
useServerEvents()WebSocket event streaming for real-time updates (book pipeline, Hardcover sync, job status)

Query Composables (queries/)

ComposablePurpose
useDashboardQuery()Dashboard data (stats, currently reading, recent additions)
useInboxListQuery()Paginated inbox list with search and sort
useInboxProcessingQuery()Currently processing inbox items
useInboxDetailQuery()Single inbox item with metadata candidates
useLibraryListQuery()Paginated library list with search plus author/genre/language/series/uploader filters
useLibraryFacetsQuery()Library filter facets (authors, genres, languages, series, uploaders)
useBookDetailQuery()Single book detail
useBookProgressQuery()Reading progress for a single book
useReadingQuery()Filtered reading list by status (reading, finished, unread, paused)
useSeriesListQuery()Series list with search
useSeriesDetailQuery()Series detail with ordered book list
useStatsQuery()Reading analytics data
useSettingsStatusQuery()Combined settings status (health, queues, credentials, app settings)
useHardcoverStatusQuery()Hardcover connection status
useHardcoverSyncLogQuery()Hardcover sync log entries
useJobsQuery()Paginated job browser with queue/status filters
useInboxCountQuery()Inbox count for sidebar badge
useReadingCountsQuery()Reading status counts for sidebar badges
useFailedJobsCountQuery()Failed jobs count for settings badge

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
SettingsJobsBrowserPaginated job browser with queue/status filters, clickable rows opening detail slideover
SettingsJobDetailSlideover showing full job details: status, timestamps, payload, logs, errors, stack traces
SettingsQueueManagementQueue admin panel with pause/resume, clean failed jobs, and drain actions per queue

Auth

useAuth() composable (backed by a Pinia auth store in src/stores/auth.ts): Exposes isAuthenticated, isAdmin, userLabel, apiKeyId (computed) and checked refs. Methods: check(), login(apiKey), logout(), setAuthenticated(value). Auth state is checked via the Hono API /api/auth/session endpoint using the typed RPC client. Login sets an httpOnly cookie directly on the API. On both successful login and logout, clearFrontendQueryCache() cancels and removes all Pinia Colada cached queries (via useQueryCache() from @pinia/colada) to prevent stale data from one user being visible to another after switching accounts.

Realtime transport is owned by src/plugins/server-events.ts, which runs once at app bootstrap in main.ts and opens a single /api/events WebSocket per browser tab. The bus is exposed via app.provide(serverEventsKey, api); useServerEvents() injects it and is just a subscriber wrapper that unregisters listeners when the calling component scope is disposed.

Keyboard Shortcuts

  • ? — Toggle keyboard shortcuts modal
  • 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: inbox detail, library detail, series detail)

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
  • Cover images: Library and inbox covers use native <img loading="lazy"> because the cover sources are authenticated API endpoints and need the browser's cookie-authenticated request path.

Form Validation

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