Add Web local version using File System API#33
Conversation
Using the [File System API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API), the Web version of Citadel can read local directories & the metadata.db file. This was a very quick first approximation of getting data to load. There's a long tale of other things to do.
There was a problem hiding this comment.
Greptile Overview
Summary
This PR introduces web browser support to the Citadel library management application by implementing a File System API-based adapter that works alongside the existing desktop (Tauri) version. The changes enable users to access local Calibre libraries directly in web browsers without requiring server infrastructure.The implementation follows a dual-provider architecture where the application detects the platform (isDesktop()) and conditionally uses either LocalCalibreLibraryProvider (desktop) or WebCalibreLibraryProvider (web). The web version uses sql.js to read Calibre's metadata.db SQLite file and the File System API to access book covers and files locally in the browser.
Key architectural changes include:
- Provider Pattern Extension: The existing
LibraryProviderwas renamed toLocalCalibreLibraryProviderand a newWebCalibreLibraryProviderwas added, maintaining the sameLibraryinterface contract - Web Adapter Implementation: A complete web adapter layer with repositories, services, and entities that mirror the existing Rust-based libcalibre architecture
- Platform Detection: New utility functions to distinguish between desktop and web environments at runtime
- Dual Settings Management: Web version uses localStorage as a fallback to Tauri's settings system
- Entity Transformation: New TypeScript entities (Book, Author, LibraryBook, LibraryAuthor) with transformation methods to bridge sql.js results and the application's domain model
The web implementation supports basic functionality like listing books with titles, authors, and cover images. The File System API integration allows users to select their Calibre library directory, which is then accessed client-side without server communication. The architecture maintains consistency with the existing domain-driven design pattern while adapting to browser constraints.
Changed Files
| Filename | Score | Overview |
|---|---|---|
| src/lib/contexts/library/index.ts | 5/5 | Renamed LibraryProvider to LocalCalibreLibraryProvider and added WebCalibreLibraryProvider export |
| src/lib/services/library/_internal/adapters/web/db.ts | 4/5 | Added loadDb function for File System API access to Calibre's metadata.db using sql.js |
| src/lib/services/library/_internal/adapters/calibre.ts | 4/5 | Extended initCalibreClient to support 'web' connection type by importing web client generator |
| src/lib/services/library/_internal/adapters/web/repositories/Repository.ts | 5/5 | Defined TypeScript interfaces for repository pattern implementations in web adapter |
| src/stores/settings.ts | 4/5 | Replaced direct __TAURI__ check with isDesktop() function and added localStorage fallback |
| src/lib/services/library/_internal/adapters/web/entities/LibraryAuthor.ts | 4/5 | Added entity mapper for converting Author objects to LibraryAuthor format |
| src/lib/platform.ts | 4/5 | Introduced platform detection utility to distinguish desktop vs web environments |
| src/lib/isDefined.ts | 5/5 | Added type guard utilities for handling undefined/null values in TypeScript |
| package.json | 5/5 | Added sql.js dependency and TypeScript types for client-side SQLite operations |
| src/components/pages/firstTimeSetup.tsx | 4/5 | Added File System API integration for web directory selection alongside desktop functionality |
| src/lib/services/library/_internal/adapters/web/services/author.ts | 4/5 | Created author service for web adapter using dependency injection pattern |
| src/lib/services/library/_internal/adapters/web/entities/Author.ts | 4/5 | Introduced Author entity with interface and factory method for database row transformation |
| src/App.tsx | 3/5 | Added dual provider logic switching between desktop and web library providers |
| src/lib/contexts/library/Provider.tsx | 5/5 | Renamed LibraryProvider to LocalCalibreLibraryProvider for better specificity |
| src/lib/services/library/_internal/adapters/web/entities/LibraryBook.ts | 3/5 | Added LibraryBook entity adapter with incomplete fromRow implementation |
| src/lib/services/library/_internal/adapters/web/types.ts | 5/5 | Introduced branded type definitions for AuthorId and BookId using TypeScript patterns |
| src/lib/services/library/_internal/adapters/web/entities/Book.ts | 2/5 | Defined Book entity with critical field mapping mismatch between fromRow/toRow methods |
| src/lib/services/library/_internal/_types.ts | 5/5 | Added WebConnectionOptions interface for File System API-based web connections |
| src/lib/services/library/_internal/adapters/web/repositories/sqljs.ts | 1/5 | Implemented SQL.js repositories with critical SQL injection vulnerability in getAuthorsForBook |
| src/lib/services/library/_internal/adapters/web.ts | 3/5 | Implemented web-based Calibre client with File System API and sql.js integration |
| src/lib/services/library/_internal/adapters/web/services/catalog.ts | 4/5 | Created catalog service combining books and authors with performance considerations |
| src/lib/services/library/_internal/adapters/web/client.ts | 1/5 | Empty file missing critical genWebCalibreClient implementation causing runtime errors |
Confidence score: 2/5
- This PR contains serious implementation issues that will cause runtime failures and security vulnerabilities
- Score reflects critical missing implementations, SQL injection vulnerabilities, and data mapping errors that would break core functionality
- Pay close attention to src/lib/services/library/_internal/adapters/web/client.ts (empty file), web/repositories/sqljs.ts (SQL injection), and web/entities/Book.ts (field mapping errors)
Sequence Diagram
sequenceDiagram
participant User
participant App as "App.tsx"
participant FirstTimeSetup as "FirstTimeSetup"
participant WebProvider as "WebCalibreLibraryProvider"
participant WebClient as "genWebCalibreClient"
participant FileSystemAPI as "File System API"
participant SQLjs as "sql.js"
participant Database as "metadata.db"
User->>App: "Opens web application"
App->>App: "Check libraryPath from settings"
alt No library configured
App->>FirstTimeSetup: "Render first-time setup"
FirstTimeSetup->>User: "Show 'Choose Calibre library folder' button"
User->>FirstTimeSetup: "Clicks choose folder button"
FirstTimeSetup->>FileSystemAPI: "showDirectoryPicker()"
FileSystemAPI->>FirstTimeSetup: "Returns FileSystemDirectoryHandle"
FirstTimeSetup->>App: "onLibraryFSDirectoryHandlePicked(handle)"
App->>App: "Set libraryFSDirectoryHandle state"
end
App->>WebProvider: "Initialize with directoryHandle"
WebProvider->>WebClient: "initClient(webLibraryFromHandle(directoryHandle))"
WebClient->>SQLjs: "Initialize sql.js library"
SQLjs->>WebClient: "Return SQL instance"
WebClient->>FileSystemAPI: "directoryHandle.getFileHandle('metadata.db')"
FileSystemAPI->>Database: "Access metadata.db file"
Database->>WebClient: "Return file buffer"
WebClient->>SQLjs: "new Database(fileBuffer)"
SQLjs->>WebClient: "Return database instance"
WebProvider->>WebProvider: "dispatch init action with client"
WebProvider->>App: "Render children (RouterProvider)"
User->>App: "Navigate to books page"
App->>WebClient: "listBooks()"
WebClient->>SQLjs: "Execute SELECT * FROM 'books'"
SQLjs->>WebClient: "Return book data"
loop For each book with cover
WebClient->>FileSystemAPI: "Navigate to book directory"
WebClient->>FileSystemAPI: "getFileHandle('cover.jpg')"
FileSystemAPI->>WebClient: "Return cover image file"
WebClient->>WebClient: "Create object URL for cover"
end
WebClient->>App: "Return books with cover URLs"
App->>User: "Display books in UI"
22 files reviewed, 17 comments
| const fileBuffer = await ( | ||
| await ( | ||
| await libraryDirectoryHandle.getFileHandle("metadata.db", { | ||
| create: false, | ||
| }) | ||
| ).getFile() | ||
| ).arrayBuffer(); |
There was a problem hiding this comment.
style: The nested await chain could be flattened using intermediate variables for better readability and easier debugging
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/services/library/_internal/adapters/web/db.ts
Line: 8:14
Comment:
**style:** The nested await chain could be flattened using intermediate variables for better readability and easier debugging
How can I resolve this? If you propose a fix, please make it concise.| all: async (): Promise<Author[]> => { | ||
| return await authorRepository.all(); | ||
| }, |
There was a problem hiding this comment.
style: The await is unnecessary since you're already returning the Promise directly. Consider removing it for cleaner code.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/services/library/_internal/adapters/web/services/author.ts
Line: 6:8
Comment:
**style:** The `await` is unnecessary since you're already returning the Promise directly. Consider removing it for cleaner code.
How can I resolve this? If you propose a fix, please make it concise.| @@ -1,5 +1,6 @@ | |||
| import { LibraryProvider } from "./Provider"; | |||
| import { LocalCalibreLibraryProvider } from "./Provider"; | |||
| import {WebCalibreLibraryProvider} from "./WebProvider"; | |||
There was a problem hiding this comment.
style: Missing space after opening brace in import statement
| import {WebCalibreLibraryProvider} from "./WebProvider"; | |
| import { WebCalibreLibraryProvider } from "./WebProvider"; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/contexts/library/index.ts
Line: 2:2
Comment:
**style:** Missing space after opening brace in import statement
```suggestion
import { WebCalibreLibraryProvider } from "./WebProvider";
```
How can I resolve this? If you propose a fix, please make it concise.| fromRow: (row: any[]): LibraryBook => { | ||
| return { | ||
| id: row[0]?.toString() ?? "", | ||
| title: row[1]?.toString() ?? "", | ||
| author_list: [ | ||
| { | ||
| sortable_name: "", | ||
| name: "", | ||
| id: "", | ||
| }, | ||
| ], | ||
| uuid: null, | ||
| sortable_title: "", | ||
| author_sort_lookup: {}, | ||
| file_list: [], | ||
| cover_image: null, | ||
| identifier_list: [], | ||
| }; | ||
| }, |
There was a problem hiding this comment.
logic: The fromRow method has incomplete implementation - it ignores most row data and hardcodes empty author information. This will result in books without proper author data being displayed.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/services/library/_internal/adapters/web/entities/LibraryBook.ts
Line: 5:23
Comment:
**logic:** The `fromRow` method has incomplete implementation - it ignores most row data and hardcodes empty author information. This will result in books without proper author data being displayed.
How can I resolve this? If you propose a fix, please make it concise.| export interface Author { | ||
| id: AuthorId; | ||
| name: string; | ||
| sort: string; |
There was a problem hiding this comment.
style: Inconsistent indentation - uses spaces instead of tabs like the rest of the properties
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/services/library/_internal/adapters/web/entities/Author.ts
Line: 6:6
Comment:
**style:** Inconsistent indentation - uses spaces instead of tabs like the rest of the properties
How can I resolve this? If you propose a fix, please make it concise.| book.timestamp, | ||
| book.pubdate, | ||
| book.series_index, | ||
| book.author_sort, |
There was a problem hiding this comment.
logic: author_sort field is defined in interface but missing from toRow output, creating inconsistency
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/services/library/_internal/adapters/web/entities/Book.ts
Line: 43:43
Comment:
**logic:** author_sort field is defined in interface but missing from toRow output, creating inconsistency
How can I resolve this? If you propose a fix, please make it concise.| const res = db.exec( | ||
| `SELECT author FROM 'books_authors_link' WHERE book = ${bookId}`, | ||
| ); |
There was a problem hiding this comment.
logic: SQL injection vulnerability: bookId is directly interpolated into the query string. Use parameterized queries instead.
| const res = db.exec( | |
| `SELECT author FROM 'books_authors_link' WHERE book = ${bookId}`, | |
| ); | |
| const res = db.exec( | |
| "SELECT author FROM 'books_authors_link' WHERE book = ?", | |
| [bookId] | |
| ); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/services/library/_internal/adapters/web/repositories/sqljs.ts
Line: 17:19
Comment:
**logic:** SQL injection vulnerability: bookId is directly interpolated into the query string. Use parameterized queries instead.
```suggestion
const res = db.exec(
"SELECT author FROM 'books_authors_link' WHERE book = ?",
[bookId]
);
```
How can I resolve this? If you propose a fix, please make it concise.| const db = await loadDb(directoryHandle, sqlClient); | ||
| if (!db) throw new Error("No database"); |
There was a problem hiding this comment.
style: Inconsistent error handling: this method throws on null db while others return empty arrays
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/services/library/_internal/adapters/web/repositories/sqljs.ts
Line: 14:15
Comment:
**style:** Inconsistent error handling: this method throws on null db while others return empty arrays
How can I resolve this? If you propose a fix, please make it concise.| const rows = res[0].values; | ||
|
|
||
| const coverImagePromises = rows.map((row) => { | ||
| const path = (row[9] ?? "").toString(); |
There was a problem hiding this comment.
style: Magic number row[9] for accessing cover path column is brittle and unclear. Consider using named constants or a mapping function.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/services/library/_internal/adapters/web.ts
Line: 51:51
Comment:
**style:** Magic number `row[9]` for accessing cover path column is brittle and unclear. Consider using named constants or a mapping function.
How can I resolve this? If you propose a fix, please make it concise.| // File names can be invalid, which throws an error. | ||
| handle = await handle.getDirectoryHandle(parts[currDepth]); | ||
| } catch (e) { | ||
| console.log(e, handle, parts[currDepth], parts, currDepth); |
There was a problem hiding this comment.
style: Console.log in production code should be replaced with proper error handling or logging system.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/services/library/_internal/adapters/web.ts
Line: 129:129
Comment:
**style:** Console.log in production code should be replaced with proper error handling or logging system.
How can I resolve this? If you propose a fix, please make it concise.…st (#131) * feat(startup): foreground the app when the main window first shows The main window starts hidden and is revealed from the frontend once settings hydrate. show() made it visible but never activated the app, so Citadel could launch behind whatever window already had focus. Focus the window right after showing it, mirroring the settings-window pattern. Fixes CDL-16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(deps): patch Dependabot alerts (vite 6, tauri/tar/rand) Resolves 10 of 11 open Dependabot alerts: - vite 5 -> 6.4.3: server.fs.deny bypass (#50), optimized-deps .map path traversal (#27), launch-editor NTLMv2 disclosure (#51) - tar 0.4.44 -> 0.4.46 (#47/#24/#23) - tauri 2.10.3 -> 2.11.3 (#42) - rand -> 0.9.4 / 0.10.1 (#32/#28); cargo update dropped the vulnerable rand 0.7.3 / 0.8.5 from the phf build chain (#33) Bumped @vitejs/plugin-react-swc -> 4 and Storybook 8 -> 8.6.18 for vite 6 peer compatibility. Verified: web build, 184 vitest tests, storybook build, cargo check + cargo test all green. glib 0.18 (#22) remains: Linux-only, pinned by tauri's frozen gtk-rs 0.18 stack; no fix available upstream. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): upgrade build chain to latest (vite 8, vitest 4, storybook 10) Unblocks the latest toolchain now that Storybook 10 lifts the vite<=6 peer cap that held the previous commit at vite 6. - vite 6.4.3 -> 8.1.0 (now rolldown-based) - vitest 3.2.6 -> 4.1.9 - storybook 8.6 -> 10.4.6: dropped the consolidated packages (addon-essentials, addon-interactions, blocks, test) now folded into core; added @storybook/addon-docs; bumped storybook-dark-mode -> 5 and @chromatic-com/storybook -> 5 for SB10 peer support - migrated @storybook/preview-api imports -> storybook/preview-api subpath Verified: web build (vite 8), 184 vitest tests, storybook build, biome lint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): update all in-range deps to latest (bun update) Pulls every dependency to the newest version satisfying its existing semver range. Notable: @tanstack/react-router 1.29 -> 1.170 (regenerated routeTree.gen.ts to the new format), typescript 5.3 -> 5.9, all @tauri-apps plugins/api/cli to latest 2.x, all @radix-ui primitives, zustand/dompurify/ clsx/tailwind-merge/postcss/autoprefixer. React stays on 18 (normalized to ~18.2.0); major-version bumps left for follow-up. Verified: tsc, vite 8 build, 184 vitest tests, storybook build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): bump biome 2.5.1 and faker 10 (dev tooling) Low-risk dev-only major/minor bumps verified independently: - @biomejs/biome 2.4.16 -> 2.5.1 (lint + format clean) - @faker-js/faker 8.4.1 -> 10.5.0 (only stable namespaced APIs used in test factories; 184 vitest tests pass) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): typescript 6, unplugin-icons 23, postcss-load-config 6 Verifiable dev/build-tooling majors: - typescript 5.9 -> 6.0.3: migrated tsconfig off the now-deprecated `baseUrl` (paths resolve relative to the config dir under moduleResolution "bundler") - unplugin-icons 0.18 -> 23.0.1 (vite plugin loads; build clean) - postcss-load-config 5 -> 6.0.1 (autoprefixer still applies in build output) Verified: tsc, vite 8 build, 184 vitest tests, storybook build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): remove unused packages Drop four dependencies with zero references outside package.json (leftovers from the pre-Radix / Svelte era): - bits-ui (Svelte component lib) - @crabnebula/tauri-plugin-drag (JS side; the Rust plugin stays) - @melt-ui/pp (Svelte preprocessor) - @fontsource/fira-mono (unused font) Verified: vite 8 build + 184 vitest tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): upgrade to React 19 - react / react-dom 18.2 -> 19.2.7, @types/react(-dom) -> 19 - ref props now nullable per React 19 types: RefObject<T> -> RefObject<T | null> in use-library-keymap and BookGrid - replace deprecated MutableRefObject with RefObject (unified + mutable in 19) Entry point already uses createRoot. Verified: tsc, vite 8 build, 184 vitest tests, storybook build, biome lint. Runtime smoke-test to follow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): remove dead Tailwind stack Tailwind was never wired into the build: no @tailwind/@import directives, no PostCSS or Vite Tailwind plugin, and zero imports of tailwindcss/ tailwind-merge/tailwind-variants in src. The app styles entirely with CSS Modules + clsx, and tailwind.config.js was abandoned shadcn scaffolding whose HSL vars don't match the real --ctd-*/--pal- OKLCH tokens in styles.css. Removed tailwindcss, tailwind-merge, tailwind-variants, and tailwind.config.js. Kept clsx, postcss, autoprefixer (all in active use). Verified: vite 8 build (autoprefixer still applies), 184 vitest tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): type-check with TypeScript 7 native (tsgo) Migrate the build's type-check step from tsc to the native Go compiler: - add @typescript/native-preview (tsgo) as a dev dependency - build:web now runs `tsgo && vite build`; add a `typecheck` script - keep the `typescript` package (6.x) installed — Vite, Storybook docgen and the editor still consume its language-service API tsgo type-checks this repo cleanly (paths + project references + composite) in ~0.28s vs ~1.9s for tsc (~6.7x faster). Native binaries resolve per platform via optional deps, so CI on ubuntu-22.04 + macos-15 both use it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: reconcile Cargo.lock after merge (lock quick-xml) The text-merge of Cargo.lock dropped quick-xml (added by main's metadata SRU work); cargo check re-locked it. Workspace compiles + all tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ts): raise lib/target to ES2022 for tsgo The codebase uses ES2022 APIs (Array/String.prototype.at) while tsconfig declared lib ES2020. tsc tolerated this; the TS7 native compiler (tsgo) does not, so CI's build_app failed with TS2550 ("change lib to es2022 or later") on 4 files. Raise target + lib to ES2022 to match what the code actually uses. Verified with both tsc and tsgo; full build:web passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Using the File System API & sql.js, the Webapp version of Citadel can read a local, client-side library (including the SQLite metadata.db file) to list books & authors.
This branch is missing a lot of features compared to the Desktop version of the app:
On top of that, it also needs some cleanup:
The other piece I want to very strongly consider is if this semi-domain-driven-design pattern (which is adapted from the existing libcalibre design) is what I really want to do. I think so? I kinda came back to this, not sure if there is something better here.