Persist blob sizes (covers + chapter pages) and surface upload storage
usage to admins in two places:
- Admin System tab: total/covers/chapters totals, ratio of disk, average
image sizes, and top-5 largest mangas/chapters leaderboards, behind a
new GET /api/v1/admin/storage endpoint (separate from /admin/system so
the cheap DB aggregates aren't coupled to its 250ms CPU sample).
- Manga detail page: total chapter-content size and per-chapter size,
with an em-dash for uncrawled or not-yet-measured chapters.
Sizes are captured at write time (crawler put_stream return, uploaded
page/cover byte length) into nullable pages.size_bytes /
mangas.cover_size_bytes columns; NULL means "not yet measured", distinct
from a real 0. A one-shot, idempotent admin "Backfill sizes" action
(POST /api/v1/admin/storage/backfill) stats pre-existing blobs in
keyset-paginated, capped batches and reports more_remaining so a large
legacy library can be drained over multiple runs.
Aggregates and leaderboards treat NULL honestly (only fully-measured
entities are ranked); a dashboard banner flags unmeasured rows so partial
figures aren't read as complete. persist_pages now prunes stale page rows
on a shrinking re-crawl so totals and page_count stay consistent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surfaces the analysis worker's NSFW moderation at the manga level:
- repo::manga FILTER_WHERE gains cw_include (AND, page-content-warning
join to mangas) and cw_exclude (NONE) clauses; ListQuery + binds
renumbered ($6/$7, LIMIT/OFFSET $8/$9).
- repo::page_analysis::warnings_for_manga: deduped, alphabetical union
across all the manga's pages.
- domain::MangaDetail gains content_warnings; get_detail populates it.
- api::mangas list accepts cw_include/cw_exclude (reusing the shared
parse_warnings_csv validator).
Tests: detail union (deduped/sorted) + empty case; list include/exclude
filter; unknown-warning 422. Existing manga tests still green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User-owned named lists of mangas with an add-to-collection modal on
the manga page and dedicated /collections and /collections/:id pages.
- Schema (0010): `collections` (per-user case-insensitive name
uniqueness) + `collection_mangas` join with cascade FKs.
- Endpoints: full CRUD on `/v1/collections`, idempotent add/remove
for `/v1/collections/:id/mangas`, and `/v1/mangas/:id/my-collections`
for the modal's pre-checked state. Owner-mismatch surfaces as 404
(not 403) so the API doesn't disclose collection existence to
non-owners; the frontend funnels 401 to /login. Three-state PATCH
via a new shared `domain::patch::Patch<T>` lets clients distinguish
"leave alone", "clear", and "set" for description.
- Frontend: reusable `Modal` component (focus trap, opt-in
backdrop close, ESC) and `AddToCollectionModal` with optimistic
toggling that's race-safe under fast clicks. /collections page
renders cover-collage cards; /collections/:id is editable with
per-card remove. Top nav gets a Collections link.
155 backend tests (incl. 21 collection tests covering ownership,
idempotence, sample-cover enrichment, three-state PATCH, FK race);
88 frontend tests; svelte-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds first-class manga metadata across the stack:
- **Status** (ongoing / completed), **alternative titles**, normalized
**multi-author** support, **curated genres** (13 seeded), and
**free-form user tags** (case-insensitive, globally shared). Each is
modelled as its own table joined to mangas; `mangas.author` is
backfilled into `authors` + `manga_authors` and dropped.
- New endpoints: `PATCH /v1/mangas/:id` (three-state `description`),
`POST/DELETE /v1/mangas/:id/tags[/:tag_id]`, `GET /v1/genres`,
`GET /v1/tags?search=`.
- `GET /v1/mangas` now returns `MangaCard` (with authors + genres
batched in) and supports `?status=`, `?author_id=`, `?genre_id=`,
`?tag_id=` filters — AND across facets, with empty-array no-op
semantics for the unnest primitive.
- `GET /v1/mangas/:id` returns the enriched `MangaDetail` with tags.
- Frontend: reusable `Chip` component; manga detail page renders
authors as chips linking to `/authors/:id` (Phase 2), a status
badge, alt titles, genres, and tags with inline add/remove (only
the attacher sees remove); upload form supports multi-author /
multi-genre / alt titles / status; search page gets a collapsible
URL-synced filter panel with keyboard-navigable tag autocomplete.
- 126 backend tests (incl. AND-across-facets primitive, case-insens
author/tag de-dup, transactional create rollback, PATCH semantics
for missing / null / set on description); 72 frontend tests +
svelte-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Set up Mangalord with a Rust/axum backend, SvelteKit frontend, Postgres,
and Docker Compose deployment. Establishes the architecture and TDD
patterns the project will extend:
- Hexagonal-ish backend layering (domain / repo / storage / api) with
a pluggable Storage trait (LocalStorage today, S3 as a future impl).
- Initial migration: users, mangas, chapters, bookmarks.
- Vertical slice for mangas (list, search, create, get) with
#[sqlx::test] integration coverage and storage unit tests.
- SvelteKit frontend using Svelte 5 runes, typed API client, Vitest
unit tests and Playwright e2e with route mocking.
- CLAUDE.md documenting layering, TDD/git/SemVer workflow rules, and
extension points (tags, fulltext search, OCR, S3, auth).
- Project-scoped .claude/settings.json with permission allowlist for
the toolchain (git, cargo, npm/vite, docker, psql, gh, doc fetches).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>