Skip to content

Add Web local version using File System API#33

Draft
phildenhoff wants to merge 9 commits into
mainfrom
web
Draft

Add Web local version using File System API#33
phildenhoff wants to merge 9 commits into
mainfrom
web

Conversation

@phildenhoff

@phildenhoff phildenhoff commented May 21, 2024

Copy link
Copy Markdown
Member

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:

  • List books titles & authors
  • Display book cover art
  • Add books
  • List book files / file formats
  • Open files in default app (how, without downloading the file again?)
  • Edit existing book metadata

On top of that, it also needs some cleanup:

  • Extract the logic for reading & writing Calibre data to a typescript package
  • Provide a better way, in src/App.tsx, to select which library client kind to use
  • Better UX when loading the app on the web
    • Each web app load requires the user to re-select their library, because the File System API doesn't provide any way (AFAIK) to save handles across page loads.
    • The minimum here is saving some info to localstorage that a library exists at some file path, and, if that is set, showing a "Welcome Back" screen instead of a "First-time setup" screen.

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.

@phildenhoff

phildenhoff commented Oct 4, 2025

Copy link
Copy Markdown
Member Author

@greptileai

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 LibraryProvider was renamed to LocalCalibreLibraryProvider and a new WebCalibreLibraryProvider was added, maintaining the same Library interface 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"
Loading

22 files reviewed, 17 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +8 to +14
const fileBuffer = await (
await (
await libraryDirectoryHandle.getFileHandle("metadata.db", {
create: false,
})
).getFile()
).arrayBuffer();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +8
all: async (): Promise<Author[]> => {
return await authorRepository.all();
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Missing space after opening brace in import statement

Suggested change
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.

Comment on lines +5 to +23
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: [],
};
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +17 to +19
const res = db.exec(
`SELECT author FROM 'books_authors_link' WHERE book = ${bookId}`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: SQL injection vulnerability: bookId is directly interpolated into the query string. Use parameterized queries instead.

Suggested change
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.

Comment on lines +14 to +15
const db = await loadDb(directoryHandle, sqlClient);
if (!db) throw new Error("No database");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

phildenhoff added a commit that referenced this pull request Jun 24, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant