Adds a reader section covering the vendored tree, the $foliate alias and the traps around it, and refreshes the stale stack line, oklch claim and rough edges. Records why CSP is not enabled in TODO.md.
122 lines
5.8 KiB
Markdown
122 lines
5.8 KiB
Markdown
# TODO
|
||
|
||
Known issues and deferred work. Agent-facing notes belong in the `AGENTS.md` files;
|
||
this is for things that are broken or missing and not yet scheduled.
|
||
|
||
## Backend
|
||
|
||
### Identifier extraction drops most ISBNs
|
||
|
||
`backend/src/chitai/services/metadata_extractor.py` — `EpubExtractor._extract_identifiers`
|
||
|
||
The EPUB path validates `DC:identifier` values verbatim:
|
||
|
||
```python
|
||
for id in epub.get_metadata("DC", "identifier"):
|
||
if is_valid_isbn(id[0]):
|
||
...
|
||
```
|
||
|
||
`is_valid_isbn` branches on `len(isbn)` being exactly 10 or 13, so anything carrying
|
||
formatting fails. Two consequences:
|
||
|
||
- **Hyphenated ISBNs are silently dropped.** `978-0-486-28211-4` is 17 characters, so it
|
||
never reaches the checksum. Most EPUBs write ISBNs hyphenated, so the majority are lost.
|
||
The PDF path already does this correctly — `_extract_isbns` calls
|
||
`match.replace("-", "")` before validating.
|
||
- **`urn:isbn:` prefixes are dropped** for the same reason. This is a common EPUB form.
|
||
|
||
Fix: normalise before validating — strip a leading `urn:isbn:`, then remove everything
|
||
that isn't `0-9` or `X`. Reuse the PDF path's approach rather than duplicating it.
|
||
|
||
Note this only runs at upload, so fixing it changes nothing for books already imported.
|
||
A backfill would need to re-read the files on disk.
|
||
|
||
### Non-ISBN identifiers are discarded
|
||
|
||
Same function. Anything that isn't a valid ISBN is thrown away, including values EPUBs
|
||
routinely carry: `urn:uuid:…`, `calibre:…`, Google Books volume IDs and ASINs.
|
||
|
||
The `Identifier` model is already generic (`name` + `value`, unique per book), so storing
|
||
them needs no schema change — only the extractor decides what survives. The frontend
|
||
already renders ISBN, ASIN and DOI as links and shows unknown types as plain values, so
|
||
anything stored will display sensibly.
|
||
|
||
Worth adding at the same time:
|
||
|
||
- A DOI regex (`10.\d{4,9}/\S+`) alongside the ISBN scan in `PdfExtractor._extract_isbns`
|
||
— academic PDFs carry one and it is the most useful identifier they have.
|
||
- Deriving ISBN-10 from ISBN-13 when only the latter is present. It is a pure checksum
|
||
conversion and doubles the chance of an external lookup matching.
|
||
|
||
## Frontend
|
||
|
||
### No CSP, so scripted EPUBs run against the app origin
|
||
|
||
EPUB files may contain JavaScript. foliate-js renders each section in an iframe from a
|
||
**same-origin** `blob:` URL and cannot sandbox it — `allow-scripts` is required, and
|
||
blob URLs inherit the embedder's origin — so script inside a book can reach `/api/*`
|
||
with the session cookie attached. foliate's own README says not to use it without a
|
||
Content Security Policy blocking scripts.
|
||
|
||
The obvious policy is `kit.csp` in `svelte.config.js` with `script-src: ['self']`, and
|
||
deliberately no `default-src` (it would also cover `style-src`/`img-src`/`font-src` and
|
||
kill both the book's own blob: assets and the inline `<style id="chitai-theme">` that
|
||
`hooks.server.ts` injects via `transformPageChunk`).
|
||
|
||
**What blocks it:** `mode-watcher` renders its own inline `setInitialMode` script, which
|
||
sets the dark class before first paint. SvelteKit only nonces the bootstrap script it
|
||
injects itself, so that one is blocked and every page load flashes the light theme.
|
||
Fixing it means pinning a SHA-256 of a third-party inline script whose contents change
|
||
with the package version and the props passed — it would break silently on upgrade, and
|
||
the symptom would be a theme flash rather than an error.
|
||
|
||
Worth revisiting if `mode-watcher` gains a nonce prop, or if the theme class moves to a
|
||
cookie so the server can set it without an inline script.
|
||
|
||
### Remove the epub.js locations-cache purge
|
||
|
||
`frontend/src/lib/reader/legacy-cache.ts` — `purgeLegacyLocationCache`
|
||
|
||
The epub.js reader cached generated locations in `localStorage` under
|
||
`${bookId}-locations`, a few hundred KB of JSON per long book. foliate computes
|
||
progress from section byte sizes at open time, so nothing writes those keys any
|
||
more, but existing browsers still hold them — and a reader near the 5–10 MB
|
||
origin quota would make the new reader-settings write throw `QuotaExceededError`.
|
||
|
||
The reader clears them once per browser, behind a `chitai:locations-purged` flag.
|
||
Delete the module, its call in `epub-reader.svelte` and the flag once deployments
|
||
have had a release or two to run it — after roughly 2026-12.
|
||
|
||
### Cover dimensions are unknown until load
|
||
|
||
`book-cover.svelte` renders covers at a fixed height with natural width so nothing is
|
||
cropped or distorted. Because the intrinsic size is not known ahead of time, the text
|
||
beside the cover settles once when the image loads. `min-width` bounds the movement but
|
||
does not remove it.
|
||
|
||
Removing it properly means storing cover dimensions at ingest and emitting them as
|
||
`width`/`height` attributes so the browser reserves exact space. That is a model change
|
||
plus a migration.
|
||
|
||
### No series navigation
|
||
|
||
`Book` has `series` and `series_position`, and the detail page shows both, but there is no
|
||
way to reach the other volumes. The books list endpoint filters by author, publisher, tag,
|
||
shelf and progress — a `series` filter would need adding in
|
||
`backend/src/chitai/services/filters/book.py` and wiring through
|
||
`services/dependencies.py`, following the existing `AuthorFilter` pattern.
|
||
|
||
### Book pages are sparse for most books
|
||
|
||
EPUB metadata is thin: most imports arrive with a title, an author and nothing else. Two
|
||
independent directions, neither started:
|
||
|
||
- **Use what exists.** "More by this author" (the list endpoint already accepts
|
||
`authors=`), and exposing `Book.created_at` and `BookProgress.updated_at` — both are in
|
||
the database, neither is on `BookRead` / `BookProgressRead`.
|
||
- **Fetch from outside.** Open Library or Google Books lookup by ISBN for descriptions and
|
||
covers. This is what actually fixes the sparseness, but it needs outbound requests, rate
|
||
limiting, a manual-vs-automatic decision, and a rule for not clobbering hand-edited
|
||
metadata.
|