- All 50 font-size declarations converted from px to rem, so a single root
value scales headings, labels and body together. The root is now 15px, a
deliberate ~7% increase over the previous 14px.
- Card width and grid column width are now --card-w / --grid-min variables,
driven by --card-scale independently of text. Defaults bumped too: shelf
cards 150 -> 160px, grid columns 148 -> 158px.
- Appearance panel (nav item, visible to everyone): Text size and Card size
as segmented controls, a live preview, and a reset. Settings are stored in
localStorage, per device rather than per account, because a TV across the
room wants bigger text than a laptop and the same person uses both.
- nav: 'settings' was missing from the View union after the home rewrite;
added back along with the Appearance entry.
Verified in the built bundle: root font 15px by default; "Larger" scales it to
19.2px; "Large" cards scale 160 to 192px; both persist across a reload and are
independent of each other.
The app looked like an admin panel. This is the product people actually see
when they open their library.
- Home: a landing screen with a Continue shelf (progress bars, straight from
the progress table) and per-type shelves of recent additions. One request
feeds it, so it does not pop in piece by piece.
- Browse: cover grid per media type with type chips, plus browse-all. Powered
by a new GET /items that lists across every visible library, filtered by
kind, through the same access chokepoint.
- Item page: cover, kind, title, authors, progress, a primary action that says
Continue when you have started, description, and the file list for
multi-file items. Cards now open the item page rather than jumping straight
into a reader.
- Search: results grouped by media type, cover-forward.
- Navigation moved from "libraries" to "media types" — you browse Ebooks, not
"the Ebooks folder". Libraries became an admin concern.
- Design system: dark by default, cover-forward, one accent, tighter type
scale, horizontal shelves, skeleton loaders.
- media: GET /home, GET /items, and authors on item detail.
Verified by screenshot in a live instance: home shelves, browse grid, and item
page all render correctly.
- jobs: a table-backed queue in the same SQLite file. No Redis, no second
process, included in the same backup. claim() is a single UPDATE...RETURNING
so a job cannot run twice even with several workers. Failures back off
exponentially; a missing handler fails permanently rather than retrying a
bug.
- metadata: Provider interface plus Open Library and Google Books (both
keyless). The enricher tries providers in registration order, falls back on
empty, and continues past a provider error.
- migration 0004: metadata_fetch records the attempt, not just the match, so a
rescan does not re-hammer a rate-limited API for an item that will never
match. Errors are always retried; ok/notfound are cached for 30 days.
- enrichment writes provider_ids, updates title/year, links authors through
people/item_people, and downloads the cover as an asset. The cover download
runs outside the transaction: a slow image host must not hold a write lock.
- endpoints: queue enrichment per library, and inspect an item's matches.
- web: Enrich button per library.
Verified live against Open Library: "The Hobbit" matched /works/OL27482W,
title and year 1937 written, a 301x500 JPEG cover downloaded and served.
The frontend the app needs to be usable at all.
- web/: Svelte 5 + Vite + TypeScript SPA. Login, app shell with per-type nav,
library list/create/scan/delete, item grid with covers, admin user
management, and an epub.js reader that syncs progress.
- Reader resumes by spine index — an integer — which is why the polymorphic
progress table needs no text column.
- Session state uses classic stores in a plain .ts module rather than runes in
a .svelte.ts module, avoiding special module resolution.
- Embedding: web/embed.go embeds web/dist. A committed .gitkeep keeps the
directive valid before the frontend is built, so `go build ./...` never
fails. Unknown non-API paths serve index.html for client routes; /api/* and
/healthz always return JSON and never fall back to HTML.
- Dockerfile builds the UI in a node stage and overlays dist into the Go stage.
- pnpm 12 needs allowBuilds in pnpm-workspace.yaml; esbuild approved, the two
unused transitive scripts explicitly denied so install exits 0.
svelte-check: 0 errors, 0 warnings. Verified served: SPA shell at /, route
fallback, hashed asset, and JSON 404 on /api/*.