Commit Graph

38 Commits

Author SHA1 Message Date
MechaCat02
679abae736 feat(chapter): preserve source-site order in chapter list (0.52.0)
Some checks failed
deploy / test-backend (push) Failing after 11m48s
deploy / test-frontend (push) Successful in 9m45s
deploy / build-and-push (push) Has been skipped
deploy / deploy (push) Has been skipped
The user-facing chapter list ordered by (number ASC, created_at ASC),
which broke the source site's order in two ways: non-numeric entries
("notice. : Officials") parsed to number=0 and clustered at the top,
even though the site placed them mid-list, and variants sharing a
number ("Ch.14 : PH" / "Ch.14 : Official") were torn apart by the
created_at tiebreak.

Capture each chapter's position in the source DOM as `source_index`
(0 = first = newest on this site) on every crawler sync, including the
UPDATE branch so a new chapter prepended on the source shifts every
existing row down by one on the next tick. The list query reverses
this with `ORDER BY source_index DESC NULLS LAST, number ASC,
created_at ASC` so the oldest chapter appears first, variants stay
adjacent in the order the site shows them, and non-numeric entries
land where the site placed them. User-uploaded chapters and pre-
migration rows keep their NULL source_index and fall through to the
prior number/created_at tiebreak via NULLS LAST.

The reader's client-side `[...chapters].sort((a,b) => a.number - b.number)`
is dropped; prev/next now walks the server-ordered array positionally
so it traverses variants and non-numeric entries in display order.

Existing data populates on the next cron tick or via admin force-resync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 07:25:09 +02:00
MechaCat02
b812c6d16c fix(reader): drop "Chapter N:" prefix from chapter title display (0.51.2)
The chapter list on the manga detail page, the reader's chapter-select
dropdown, the continuous-mode chapter bar, the browser tab title, and
the profile upload-history entries all prepended "Chapter {number}:"
in front of the crawled site title. Source titles already include
"Ch.N" themselves and the manga page renders chapters inside an <ol>,
so the prefix duplicated information the user could already see.

A small chapterLabel(c) helper in $lib/api/chapters returns the site
title as-is, falling back to "Chapter {number}" only when the
crawler captured an empty title (link/option stays non-empty). The
five render sites now call it. The previous-/next-chapter nav
buttons still read "Previous chapter (Ch. N)" / "Next chapter (Ch. N)"
since those are wayfinding labels, not title display.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 07:22:17 +02:00
MechaCat02
8818c890c5 feat(reader): chapter select dropdown for direct chapter jumps (0.51.0)
Adds a chapter `<select>` to the reader's top nav listing every chapter
of the current manga, defaulting to the open chapter; picking another
entry navigates straight to it without going back to the manga detail
page. Options use the "Ch. N — Title" form to match the existing
chapter tile and prev/next buttons in the reader bar.

Covered by a new Playwright spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 07:09:30 +02:00
MechaCat02
c134bdbbde feat: cover retry backfill + admin force-resync for manga & chapter (0.50.0)
Adds a per-tick cover-backfill pass to the crawler daemon so mangas whose
cover download failed on first attempt get retried — the metadata pass's
early-stop optimisation otherwise prevents the walk from revisiting them.

Adds admin-only POST /admin/mangas/:id/resync and POST /admin/chapters/:id/resync
that refetch metadata + cover (or chapter content with force_refetch) from the
crawler source synchronously and return the refreshed row. Surfaced in the
UI as "Force resync" buttons on the manga detail and reader pages,
admin-only via session.user.is_admin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 22:00:09 +02:00
MechaCat02
5c22dfdb41 feat: paginate list views, fix stale page titles, tidy admin filter bar
Bundle of small UI/UX fixes plus a build hygiene tweak.

* List pagination — Home (`/`) and `/authors/[id]` silently capped at
  the backend default of 50 with no UI to advance. New reusable
  `Pager.svelte` (Prev/Next + numbered with ellipsis), URL-synced
  `?page=N`, and filter/search/sort reset to page 1 so users aren't
  stranded on an out-of-range page. Count label now shows a range
  ("Showing 51–100 of 237").

* Stale page title — Pages without a `<svelte:head><title>` left the
  document title at whatever the last manga / author / collection page
  set it to. Move static-route titles into a route-id → title map in
  the root layout and invert every dynamic title to brand-first
  (`Mangalord | {X}`) for consistency.

* Admin filter bar — `/admin/mangas` search input had `flex: 1` and
  ballooned across the row, shoving the sync-state select + Search
  button to the far right. Cap at 24rem, vertical-align the row, and
  promote the previously aria-only "Sync state" label to visible text.

* Build hygiene — `backend/target` had grown to 68 GiB. Cleaned and
  added `[profile.dev] debug = "line-tables-only"` (and `[profile.test]`
  too) to cut future dev builds by ~50–70% while keeping line numbers
  in backtraces.

Also: configure vitest to resolve Svelte's browser entry so
`@testing-library/svelte` can mount components in jsdom — needed for
the new `Pager.svelte.test.ts`.

Bump 0.48.0 -> 0.49.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 21:18:53 +02:00
MechaCat02
e50fc093c3 feat: add PRIVATE_MODE site-wide auth gate (0.48.0)
When `PRIVATE_MODE=true`, every API path except a small allowlist
(`/health`, `/auth/{config,login,logout,register}`) requires a valid
session cookie or bearer token — anonymous reads are rejected with
401. Self-registration is force-disabled in private mode regardless
of `ALLOW_SELF_REGISTER`, so a locked-down instance flips with a
single switch (admins still mint accounts via `POST /admin/users`).

The backend gate is a tower middleware that reuses the existing
`CurrentUser` extractor, so the cookie + bearer paths cannot drift
from per-handler auth. `/auth/config` now exposes the flag plus the
effective `self_register_enabled` value so the frontend can render
the navbar correctly on the first paint.

On the frontend, a new universal root `+layout.ts` fetches the
config and redirects anonymous visitors to `/login?next=<path>`
before page-specific loads fire. The redirect is UX only — the
backend middleware is the source of truth, so crafted requests
still 401.

Defaults stay public (`PRIVATE_MODE=false`); existing deployments
need no env change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 20:11:22 +02:00
MechaCat02
030b27754b feat(api): admin-initiated user creation via POST /admin/users (0.43.0)
Some checks failed
deploy / test-backend (push) Failing after 8s
deploy / test-frontend (push) Failing after 38s
deploy / build-and-push (push) Has been skipped
deploy / deploy (push) Has been skipped
Pairs with the ALLOW_SELF_REGISTER toggle from 0.42.0: admins can mint
accounts regardless of the toggle state, so a closed-membership
deployment still has a working enrollment path. The endpoint accepts
{ username, password, is_admin? } so admins can mint co-admins in one
call (avoiding a separate promote + extra audit row for the common
"invite a co-admin" flow).

Implementation:
- POST /api/v1/admin/users guarded by RequireAdmin
- Reuses validate_username / validate_password from api::auth (made
  pub(crate)) so the admin path can never produce an account self-
  register would reject and vice versa
- repo::user::admin_create_user wraps INSERT + admin_audit insert in
  a single tx — same "audit reflects what committed" semantics as the
  existing admin_safe_* fns
- Audit row: action="create_user", payload={username, is_admin}

Frontend:
- createAdminUser() in lib/api/admin.ts
- /admin/users grows a collapsible "Create user" form above the table
  (username, password, "Make admin" checkbox). Errors surface inline;
  the list reloads on success.

Backend tests: 7 new, including the headline
`create_user_works_even_when_self_register_disabled` that pins the
admin-create path is NOT gated by the public toggle.
2026-05-31 14:00:31 +02:00
MechaCat02
2f47faa11c feat(auth): ALLOW_SELF_REGISTER toggle + public /auth/config endpoint (0.42.0)
Lets operators run a closed-membership deployment by setting
ALLOW_SELF_REGISTER=false (default true, so existing deploys are
unaffected). When off, POST /auth/register returns 403 forbidden. The
rate-limit token is consumed BEFORE the disabled check so the timing
doesn't distinguish enabled-but-rejected from disabled — closes the
toggle-state probe channel.

New public GET /auth/config returns { self_register_enabled: bool }
so the frontend can render its register affordances correctly
without conflating "disabled" with "rate-limited" (which a probe
attempt would).

Frontend: a lightweight reactive `authConfig` store loads the flag
once on root-layout mount (and again on /register direct navigation,
which bypasses the layout's onMount). Header hides the Register link
when the toggle is off; /register renders a "self-registration is
disabled — ask an administrator" notice instead of the form.

Admin-create endpoint that pairs with this toggle is intentionally
not in this PR — it lands as the next branch (feat/admin-user-create).
The toggle alone is independently useful for deployments that want
to lock down enrollment without yet wiring an admin UI.
2026-05-31 13:56:18 +02:00
MechaCat02
f6728dc71a fix(admin): security-audit findings — paginate chapters, lock down unchecked helper (0.41.2)
Addresses the security-audit findings on top of the admin feature stack:

M1: /admin/mangas/:id/chapters now paginates (default limit 200, max 500).
A long-runner with thousands of chapters would otherwise produce a multi-MB
response with that many scalar subqueries per row — admin-only but a real
stall risk on one expand-click. Adds explicit pagination tests for the cap
and offset; frontend renders a "Showing first N of M" hint when the cap
clips the result.

L1: repo::user::set_is_admin renamed to set_is_admin_unchecked with a
doc-comment pointing at admin_safe_set_is_admin for production use. The
short name was a footgun — a future contributor reaching for it would
silently bypass self-protection, the last-admin invariant, and the audit
log. Used only by integration-test setup; production code goes through
the admin_safe_* paths.

CSRF posture: build_session_cookie carries a comment that the
SameSite=Lax default is the project's CSRF defense for state-changing
mutations and breaks the instant anyone adds a side-effecting GET under
/admin/*. Spells out what to do then (Strict + explicit token check).

Test counts: 43 backend admin tests + 12 vitest admin tests all green;
svelte-check 0/0 across 446 files.
2026-05-30 22:23:55 +02:00
MechaCat02
b434c9b68d feat(frontend): /admin dashboard with users/mangas/system views (0.41.0)
Adds the SvelteKit /admin route tree backed by the admin endpoints
landed in PR 1-4. Pages: Overview (alerts + summary cards), Users
(list / promote-demote / delete), Mangas (list with sync state +
expandable per-chapter state), System (live disk/mem/cpu bars,
refreshing every 5s).

Security model: the backend's RequireAdmin extractor is the actual
boundary. /admin/+layout.ts calls getSystemStats() at load and
translates the response — 401 → redirect to /login, 403 → throw
SvelteKit error(403) which renders the framework error page. The
header's "Admin" link is hidden unless `session.user?.is_admin`,
but that's UX only.

Carries `is_admin: boolean` through to the frontend User TS type so
the header check works and so admin tables can show role per row.

Vitest covers lib/api/admin.ts (10 tests: list/delete/PATCH for
users, sync-state filter for mangas, nested chapter route, system
disk-nullable case). Playwright is intentionally deferred until the
routes stabilise — admin UI is operator-only and changes shape often
in v0.
2026-05-30 21:49:39 +02:00
MechaCat02
8d34132883 bugfix: security & correctness bundle (0.34.1)
Five fixes bundled into one release:

- preserve user-attached tags across crawler upserts
  (repo::crawler::sync_tags now scopes to added_by IS NULL; orphaned
  attachments from deleted users are reaped as crawler-owned)
- gate manga PATCH and cover endpoints on uploaded_by (require_can_edit
  in api::mangas; non-NULL uploaded_by must match the caller)
- equalise login response time across user-existence branches
  (run argon2 against a OnceLock-cached dummy hash on the no-user
  branch so timing doesn't leak username existence)
- crawler download defences (SSRF allowlist of host literals
  including IPv4-mapped IPv6 ranges, 32 MiB streamed size cap,
  reject non-whitelisted image types, three-way chapter-probe
  classifier replaces the binary #avatar_menu check)
- tighten validation and clean up dead unload path
  (attach_tag + create_token enforce 64-char caps; LocalStorage
  rejects NUL bytes explicitly; reader flushFinalProgress drops
  the always-405 sendBeacon path)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:24:51 +02:00
MechaCat02
fa0a7da311 feat: edit existing manga metadata (0.31.0)
Adds PUT /mangas/:id/cover (multipart) and DELETE /mangas/:id/cover so
covers can be replaced or cleared after creation, and wires a dedicated
/manga/[id]/edit SvelteKit route that combines the existing PATCH with
the new cover endpoints. Cover PUT cleans up the old blob when the
extension changes, swallowing StorageError::NotFound so a manually-gone
file doesn't surface as a 404 to the client. Edit link on the manga
detail page is gated on session.user, matching the auth posture of the
underlying handlers.

Also pins the local-dev port story via loadEnv() in vite.config.ts so
VITE_PORT / BACKEND_URL from a (gitignored) .env keep the dev URL
stable across runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:26:23 +02:00
MechaCat02
51346227dd feat: route reader by chapter id, allow duplicate-numbered chapters (0.24.0)
Real-world sources publish multiple chapters at the same number:
different scanlators ("Ch.52 from bloomingdale" + "Ch.52 from mina"),
translator notices and farewells, alt-translations. The (manga_id,
number) UNIQUE constraint from 0001 silently collapsed all of those
into a single row via the upsert path in repo::crawler. Migration 0013
drops the constraint; sync_manga_chapters now plain-INSERTs each
SourceChapterRef so every parsed chapter survives as its own row.

Identity moves from the (manga_id, number) tuple to the chapter UUID:

- `GET /api/v1/mangas/:manga_id/chapters/:chapter_id` (replaces :number)
- `GET /api/v1/mangas/:manga_id/chapters/:chapter_id/pages`
- `repo::chapter::find_by_id_in_manga` (replaces find_by_manga_and_number)
- Frontend reader route renamed to `/manga/[id]/chapter/[chapter_id]`
- Chapter links throughout (manga page list, continue-reading CTA,
  reader prev/next, history rows, bookmark cards) use chapter.id
- API clients getChapter/getChapterPages take a chapter id string

read_progress + bookmarks already FK chapter_id; they only enrich with
chapter_number for display, which is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:37:07 +02:00
MechaCat02
89b8785a40 bugfix: reader-nav is fully fixed; no settle-on-scroll (0.21.3) 2026-05-17 20:57:05 +02:00
MechaCat02
64ccc0ba84 bugfix: measure bar heights with ResizeObserver instead of magic numbers (0.21.2) 2026-05-17 20:47:32 +02:00
MechaCat02
215325ad2f bugfix: reader nav sticks under the app header instead of behind it (0.21.1)
$(top offset was 44px (header's 60px minus var(--space-4)), placing the bar inside the layout header. Now sticks at var(--app-header-h).)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:42:38 +02:00
MechaCat02
7aa6e7e6d9 feat: chapter chevrons, sticky app frame, and focus mode (0.21.0)
Reader gets chapter-aware chevrons + a persistent app frame +
distraction-free focus mode.

- Single-mode chevrons (and ArrowLeft/Right + j/k) advance pages
  within the chapter and fall through to the adjacent chapter at the
  boundaries. Last page of last chapter / first page of first
  chapter disables the chevron and silent-no-ops on the keypress.
- Continuous-mode gets a fixed bottom bar with prev/next chapter
  buttons; arrows + j/k jump chapters directly.
- `?page=N` and `?page=last` URL query lets the prev-chapter jump
  land on the previous chapter's last page.
- Layout header is fixed at the top; reader nav is sticky just
  below it; both stay visible while scrolling so reading settings
  are always reachable.
- New "Focus" toggle in the reader nav hides the layout header,
  reader nav, and bottom chapter bar with smooth 220ms slide
  animations. Exit via Esc or a small floating Minimize2 button at
  the top-right (low resting opacity, full on hover). Reset on
  reader unmount so it doesn't leak to other pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:39:03 +02:00
MechaCat02
c95c1805df feat: upload flow revamp (0.20.0)
- `/upload` is now manga-only with optional N initial chapters
  staged inline.
- Additional chapters from a new `/manga/[id]/upload-chapter` route,
  reached via an "Upload chapter" button on the manga page.
- New `ChapterPagesEditor` component: thumbnails next to each row,
  click-to-preview-modal, drag-drop + reorder.
- Pages renamed to `page-NNN.<ext>` before multipart submission;
  original filenames shown as dimmed reference text during upload
  and dropped on submit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:59:22 +02:00
MechaCat02
19c1276490 feat: read & upload history (0.19.0)
Per-user reading progress and uploader attribution.

Schema (migration 0011): `read_progress` table (one row per (user,
manga); chapter_id nullable on chapter delete) and nullable
`uploaded_by` columns on mangas + chapters with partial indexes
scoped to non-null rows.

Endpoints (all `/me/*`, auth-scoped):
- PUT `/v1/me/read-progress` upserts. FK violations + cross-manga
  chapter ids both surface as 4xx (404 / 422) so the API can't be
  used to write logically invalid rows.
- GET `/v1/me/read-progress` paged newest-first list.
- GET `/v1/me/read-progress/:manga_id` enriched with chapter_number
  for the manga page's Continue CTA.
- DELETE `/v1/me/read-progress/:manga_id` idempotent.
- GET `/v1/me/uploads` interleaved manga + chapter uploads as a
  tagged union; limit-only pagination.

Existing manga + chapter upload handlers stamp `uploaded_by`.

Frontend:
- Reader emits progress on mount + page change (debounce) and via
  IntersectionObserver in continuous mode. High-water mark is seeded
  from the persisted server value so re-opening a chapter doesn't
  regress to page 1. Tab close survives via `sendBeacon` (fallback
  `keepalive` fetch); SPA navigation flushes via regular fetch.
- Manga detail page shows "Continue reading Chapter N — page M"
  above the chapters list, working even for mangas with >50
  chapters.
- New `/profile/history` tab with reading history (clear-per-row,
  inline error on failure) and uploads (mangas + chapters mixed
  chronologically with type-aware rendering).

171 backend tests (incl. 16 history tests covering ownership, FK
race, cross-link guard, chapter SET NULL behaviour) and 97 frontend
tests + svelte-check clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:19:52 +02:00
MechaCat02
7560d59616 feat: /profile dashboard with tabbed preferences, account, bookmarks, collections (0.18.0)
Tabbed user dashboard at `/profile` that absorbs `/settings` and
surfaces bookmarks + collections in one place.

- New `/profile` shell with tabs: Overview (counts), Preferences
  (theme + reader prefs, ported from /settings; works for guests
  via localStorage), Account (password change; auth-gated),
  Bookmarks, Collections. Guest tab list is filtered to what they
  can actually use.
- `/settings` is a 308 redirect to `/profile/preferences` so old
  bookmarks land cleanly. The "Settings" link in the top nav is
  replaced by a Profile link between Upload and Bookmarks; Bookmarks
  + Collections stay as shortcuts per the user spec.
- Extracts `lib/components/BookmarkList.svelte` and
  `lib/components/CollectionsGrid.svelte` so the top-level
  /bookmarks + /collections routes and the new profile tabs render
  the same UI without duplication. Both layers use a three-state
  load (authenticated / guest / error) to handle network hiccups
  inline.
- Deep links preserved via `?next=` on every sign-in CTA.

88 frontend unit tests + svelte-check clean; 12 of 12 e2e tests in
profile.spec.ts and reader-mode.spec.ts pass (8 other e2e failures
predate this branch and stay flagged for cleanup).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:59:29 +02:00
MechaCat02
274cc819ca feat: manga collections (0.17.0)
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>
2026-05-17 17:43:06 +02:00
MechaCat02
5e92a2c450 feat: author pages with /authors/:id route (0.16.0)
- `GET /v1/authors/:id` returns `AuthorWithCount` (id, name, manga_count).
- `GET /v1/authors/:id/mangas` paged works by that author.
- `GET /v1/authors?search=` autocomplete (already used by Phase 1 forms;
  now formally exposed).
- New `/authors/:id` page on the frontend; author chips on the manga
  detail page (added in Phase 1) now link to a real page.
- Extracts `lib/components/MangaCard.svelte` — already used by the home
  page, ready for the collection page in Phase 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 14:39:11 +02:00
MechaCat02
59d380b6d7 feat: manga metadata with status, authors, genres, tags, and search filters (0.15.0)
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>
2026-05-17 14:32:03 +02:00
MechaCat02
60cc7712fa feat: continuous reader mode with persisted preference
Add a vertical-scroll continuous mode to the reader alongside the
existing single-page mode. A segmented toggle in the reader top bar
switches between them; in continuous mode a gap selector
(None/Small/Medium/Large → 0/12/32/64px) controls the spacing
between stacked pages. Settings page mirrors the same controls.

Backend: new user_preferences table (one row per user, lazily
inserted, ON DELETE CASCADE) and GET/PATCH /api/v1/auth/me/preferences
gated by the existing CurrentUser extractor. Allowed values are
enforced both by API validation and table-level CHECK constraints.
Eight integration tests cover defaults, persistence, partial
updates, validation errors, auth, per-user isolation, and cascade.

Frontend: a new preferences store mirrors the theme-store pattern
with a localStorage shadow so anonymous browsers get a consistent
experience and logged-in users don't flash defaults while the
server response is in flight. Server values that the frontend
doesn't recognize (forward-compat) are ignored rather than poisoning
the UI; non-401 PATCH errors revert the optimistic local update;
logout clears the shadow so user A's settings don't follow user B
on a shared browser.

In continuous mode native scrolling handles Space/PageDown/arrows;
Home/End remain wired and call scrollIntoView() so jumping to chapter
bounds stays one keystroke. Single-page mode (chevrons, arrow-key
pagination, next-page preload) is unchanged.

Versions bumped 0.13.0 → 0.14.0 in lockstep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 13:15:03 +02:00
MechaCat02
567d56bfa1 feat: design system with light/dark themes and icon-first UI
Adds a real design system to replace the per-route ad-hoc styling:

- docs/design-system.md is the contract. Semantic CSS custom-property
  tokens (color/type/spacing/radii/shadows/z-index) with verified WCAG
  AA/AAA contrast ratios for both themes.
- frontend/src/lib/styles/tokens.css defines :root tokens + a
  [data-theme="dark"] override + base element resets, a .form-field
  helper, and a global prefers-reduced-motion rule.
- frontend/src/lib/theme.svelte.ts is a Svelte 5 runes store backing
  the theme state machine (system | light | dark). localStorage key
  'mangalord-theme'; matchMedia subscription that re-resolves on OS
  theme change while in 'system' mode; init() / destroy() lifecycle
  wired from +layout.svelte.
- frontend/src/app.html runs a synchronous inline script before
  %sveltekit.head% to set [data-theme] before first paint. No FOUC.
- /settings gains a System / Light / Dark radiogroup (real fieldset +
  legend + radios with lucide icons).
- Every route's <style> block is rewritten to consume tokens — home,
  auth, upload (drop-zone + page list), bookmarks, manga overview,
  reader.
- @lucide/svelte icons replace ad-hoc text controls per the spec:
  Search (icon-only primary), LogOut (icon-only muted), Upload /
  Bookmarks / Settings nav inline icons, ChevronLeft/Right for the
  reader, ArrowUp/Down/Trash2 for the upload page list. The bookmark
  toggle keeps its '☆ Bookmark' / '★ Bookmarked' text verbatim.
- Home search controls split into two rows: input + Search CTA on
  row 1, Sort (and future filters) on row 2.

Accessibility: every icon-only button carries aria-label, every
decorative SVG aria-hidden; existing image alt text preserved;
focus-visible rings reach every interactive element including the
visually-hidden theme radios; color is never the sole conveyor.

Version bump 0.12.0 → 0.13.0 across backend/Cargo.toml and
frontend/package.json (feat: → minor per CLAUDE.md).

Bars: svelte-check 0/0, vitest 51/51, playwright 18/18, cargo test
88/88, clippy -D warnings clean. Two rounds of independent review;
verdict ship-ready.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:52:58 +02:00
MechaCat02
f0e57b0615 feat: show manga covers everywhere a manga is referenced
Every place that surfaced a manga title used to show *just* the
title — the home page, the reader's back-to-manga link, and the
chapter upload form's manga selector. Adding the cover image
alongside makes the app feel like an actual manga library.

- Home (`/`): manga list switched from a one-line `<a>` per item to
  a responsive grid of cards (`auto-fill, minmax(140px, 1fr)`),
  each card showing the cover (with 📖 placeholder when no cover is
  set), the title (line-clamped to 2 rows), and the author.
- Reader (`/manga/[id]/chapter/[n]`): the back-to-manga link in the
  reader header now shows a 28×42 thumbnail of the manga's cover next
  to the title. Reuses the placeholder pattern for cover-less mangas.
- Upload (`/upload`): the chapter form's manga `<select>` still uses
  a native dropdown (covers don't fit in `<option>`), but a preview
  pane below the select now shows the currently-selected manga's
  cover + title + author so the user can visually confirm which
  manga they're attaching the chapter to.

No backend changes — `cover_image_path` was already in the Manga
JSON; only the frontend needed to read it.

Lockstep version bump to 0.12.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 10:52:57 +02:00
MechaCat02
dee7f1d160 bugfix: /bookmarks renders manga title and cover
The bookmarks list was rendering "Manga bookmark <date>" with no
indication of which manga the bookmark referred to. The data is
already in the DB — the list query just wasn't pulling it.

Backend:
- BookmarkSummary gains manga_title (String) and
  manga_cover_image_path (Option<String>). Populated by an INNER JOIN
  on `mangas` in `repo::bookmark::list_for_user`. The JOIN is INNER
  because `bookmarks.manga_id` has ON DELETE CASCADE, so a bookmark
  cannot outlive its manga. Chapter LEFT JOIN unchanged.
- The existing list_me_enriches_chapter_bookmarks_with_chapter_number
  test now also asserts manga_title is populated for both chapter-
  and manga-level bookmarks, and that manga_cover_image_path is null
  when no cover was uploaded.

Frontend:
- Bookmark type carries optional manga_title and
  manga_cover_image_path (optional because POST /bookmarks returns
  the bare Bookmark, not the enriched summary).
- /bookmarks page redesigned as a grid: cover thumbnail (64×96 with
  a placeholder when no cover) on the left, then the manga title (as
  the primary link), then either "Chapter N — page M" linked to the
  reader, "(chapter removed)" for orphan chapter bookmarks, or
  "Whole manga" for manga-level bookmarks. Bookmark date moves to a
  subdued footer.
- E2E fixtures track the enriched shape returned by the list endpoint
  (vs. the bare Bookmark returned by POST). The toggle test now
  asserts the manga title appears on the bookmarks card after the
  bookmark is created.

Also: tighten .gitignore. `/data` only catches the compose volume
root; the dev backend writes to `/backend/data` (default STORAGE_DIR
is `./data/storage` relative to backend cwd), so local uploads were
showing as untracked. Adding `/backend/data` keeps test uploads out
of the index.

Lockstep version bump to 0.11.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 10:46:01 +02:00
MechaCat02
c7cb689984 feat: settings page exercises the password-change endpoint
The 0.10.0 backend endpoint had no UI caller — the audit flagged it
as either-ship-a-form-or-remove-the-endpoint dead code. Shipping the
form, plus the bearer-token-keeps-working regression test the audit
asked for to pin the docstring contract.

Backend:
- New test change_password_via_bearer_leaves_bearer_working asserts
  that PATCH /me/password called with Authorization: Bearer wipes
  cookie sessions but leaves the bearer (api_token) intact and usable
  — matches the docstring claim that bot tokens are opt-in to revoke.

Frontend:
- lib/api/auth.ts: new changePassword(input) wrapping PATCH
  /v1/auth/me/password. Vitest covers happy 204, 401 unauthenticated
  (wrong current), 400 invalid_input (weak new) — same envelope
  parsing shape used elsewhere.
- routes/settings/+page.svelte: minimal form with current /
  new / confirm fields, derived passwordsMatch + canSubmit guards
  (submit stays disabled until current is filled, new is ≥8 chars,
  new == confirm). Shows the API's message inline on failure.
  Documents the "other devices signed out, bot tokens stay" UX in a
  short hint.
- routes/+layout.svelte: new "Settings" link in the session-aware
  nav (between username and Logout) for authed users only.
- e2e/settings.spec.ts (5 cases): nav link reaches the form,
  successful change shows confirmation + clears the form, 401
  surfaces inline, password mismatch keeps submit disabled, anonymous
  user gets a sign-in prompt instead of the form.

Lockstep version bump to 0.11.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 00:16:22 +02:00
MechaCat02
49f6d4d213 bugfix: third-pass audit follow-ups (F1-F4 + dockerignore)
- F1: backend/Dockerfile now copies Cargo.lock alongside Cargo.toml
  and builds with --locked, so the production image runs against the
  exact crate versions CI tested. Without this, cargo silently
  resolved fresh on each image build and "we tested it" stopped being
  true for the binary you ship.
- F2: POST /api/v1/mangas/{id}/chapters rejects chapter `number < 1`
  with 422 validation_failed. Mirrors the bookmark page>=1 rule from
  0.9.4 — chapter numbers are 1-indexed everywhere (URLs, upload
  form, reader) and 0/negative numbers had no legitimate use. Three
  cases (0, -1, -100) in api_uploads.rs.
- F3: bookmarks/+page.ts no longer re-throws non-401 ApiErrors as
  SvelteKit's generic 500 page. Surfaces the error message inline via
  a new `data.error` field; the page renders an alert when present.
  Same UX shape as the home page's existing error handling.
- F4: dropped Space from the reader keyboard binding. On portrait
  phones and narrow desktop windows the page image overflows the
  viewport and the user expects Space to scroll — preventDefaulting
  it skipped past unread content. ArrowRight + j remain.
- New backend/.dockerignore and frontend/.dockerignore so the local
  target/ and node_modules/ don't get shipped into the build context
  on every `docker compose build`.

Lockstep version bump to 0.10.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 00:13:14 +02:00
MechaCat02
a8d6da167c bugfix: second-pass audit follow-ups (N1-N4)
Four small follow-ups from the second-pass audit:

- N1: `manga_upload_rolls_back_when_cover_storage_fails` covers the
  manga-side of the transactional rollback path. The chapter case had
  a `FailingStorage` regression test already; this completes the
  symmetric pair. With fail-on-put-index=0, the cover put fails on
  the first call, the transaction aborts, and `SELECT count(*) FROM
  mangas WHERE title = 'Berserk'` is 0.

- N2: The SvelteKit proxy now catches network-layer failures from the
  upstream `fetch` (DNS / connection refused / TLS handshake) and
  returns a 502 with the standard error envelope
  (`code: 'upstream_unavailable'`) instead of letting SvelteKit's
  generic 500 HTML page through. `client.ts` can `.json()` the result
  cleanly so callers see a real ApiError with a meaningful code. The
  underlying cause is logged via `console.error` for the operator.
  Test in hooks.server.test.ts asserts the 502, the JSON envelope, and
  that `resolve` is not called (the proxy short-circuits).

- N3: `GET /api/v1/files/*key` now sets
  `X-Content-Type-Options: nosniff`. The upload-time magic-byte sniff
  is authoritative for what we declare as Content-Type; `nosniff`
  makes the contract explicit so older user-agents can't try to
  re-detect HTML/JS in a polyglot file that survived the sniff. Test
  in api_uploads.rs asserts the header.

- N4: The /bookmarks page used `{#if b.page}` to gate the "— page N"
  display, which falsy-elided a legitimate `page == 0`. Backend now
  rejects `page < 1` for new bookmarks (already shipped in 0.9.4),
  but any pre-0.9.4 row with page=0 still rendered without its
  number. Strengthened to `{#if b.page != null && b.page > 0}`.

Lockstep version bump to 0.10.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:55:53 +02:00
MechaCat02
563524d51e bugfix: bookmark chapter links use chapter number, not UUID
The reader route is keyed on chapter number (URL `/manga/{id}/chapter/{n}`,
loaded via `Number(params.n)`), but the bookmarks list was building
hrefs from `chapter_id` (a UUID). Following any chapter bookmark
produced a NaN load on the reader page.

Fix at the API layer so every consumer of /me/bookmarks gets the
information without a follow-up round-trip per bookmark.

- domain::BookmarkSummary: new type, `Bookmark` plus
  `chapter_number: Option<i32>`. Populated by a LEFT JOIN on chapters
  so manga-level bookmarks come back with `chapter_number = null` and
  chapter-level ones get the value. `Bookmark` itself stays minimal
  for POST / DELETE responses.
- repo::bookmark::list_for_user returns Vec<BookmarkSummary>.
- api::bookmarks::list_me returns PagedResponse<BookmarkSummary>.
- Frontend `Bookmark` type carries an optional `chapter_number`.
- /bookmarks page builds `/manga/{manga_id}/chapter/{chapter_number}`
  for chapter bookmarks, falling back to the manga overview if the
  chapter has been deleted out from under the bookmark (chapter_id is
  ON DELETE SET NULL, so this is a real edge case).

New test asserts both branches of the JOIN: a chapter-level bookmark
comes back with the right chapter_number and page, a manga-level one
has a null chapter_number.

Lockstep version bump to 0.9.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:20:45 +02:00
MechaCat02
b259d1f571 feat: drag-drop upload page for manga and chapters
Frontend-only branch consuming the multipart endpoints from feat/uploads.

- /upload page with two sections:
  - "Create manga": title (required), author, description, optional
    cover. Submit posts the FormData to POST /api/v1/mangas via the
    existing createManga client.
  - "Upload chapter": manga selector (preloaded via listMangas
    sort=title, limit=200), chapter number, optional title, and a
    drag-drop zone for page images. Pages render in an ordered list
    with up/down/remove controls so the user can fix order without
    re-uploading. The same hidden file input is used by both the
    "browse" link and Playwright's setInputFiles, so the e2e test
    exercises the real submission code path even though it doesn't
    simulate the drag mechanics.

- Client-side preflight in lib/upload-validation.ts (extracted so
  Vitest can target it directly): rejects files over 20 MiB with a
  sized message and rejects MIME types outside the
  jpeg/png/webp/gif/avif whitelist. Files with an empty file.type fall
  through to the backend's magic-byte sniff, which stays the
  authoritative check. The submit button is disabled while any pending
  page has a client-side error, so an oversized file never reaches the
  network.

- API errors are surfaced via the envelope: 401 redirects to /login,
  everything else is rendered as the form's role=alert message. The
  backend's 415/413/422/409 message strings carry enough context that
  the user can act on them without us repeating the field name
  client-side (matches what we already surface for /auth errors).

- /upload requires auth: anonymous users see a "Sign in to upload"
  prompt linking to /login instead of empty forms.

Vitest coverage (10 cases):
- validateImageFile null on small images and on each of the five
  whitelisted MIMEs.
- Oversized files → sized "too large" message that names the file.
- Non-image MIME → "unsupported image type X" naming the type.
- Empty file.type → passes (deferred to backend sniff).
- formatBytes handles B / KiB / MiB.

Playwright coverage (e2e/upload.spec.ts, 4 cases):
- Anonymous user sees the sign-in prompt.
- A "page.png" whose bytes are a PDF (client validator passes because
  it trusts the declared MIME for preflight) reaches the mocked
  backend, which 415s, and the form renders the backend's message.
- Happy path: create a manga, then upload a 2-page chapter, with both
  successes asserted from the mocked 201 responses.
- A 21 MiB file is added to the pages list with a "too large" error,
  the submit button stays disabled, and zero POSTs leave the browser.

Lockstep version bump to 0.9.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:54:00 +02:00
MechaCat02
1883356d7d feat: pg_trgm search, sort options, populated total count
Backend:
- Migration 0005_search.sql enables pg_trgm and adds GIN indexes
  (gin_trgm_ops) on mangas.title and on mangas.author (partial, WHERE
  author IS NOT NULL).
- repo::manga::list keeps the existing substring (ILIKE) clause and
  adds the `%` operator on title + author so the search tolerates typos
  ('narto' → 'Naruto'). Both branches share the trgm index. A second
  count(*) query (same WHERE clause, indexed) yields the total without
  scanning twice in any meaningful sense.
- New ListSort enum (Recent / Title) interpolated into ORDER BY from a
  hard-coded match — never from request input, so the format!() is not
  a SQL-injection seam. Default stays Recent (created_at DESC).
- api::mangas accepts `?sort=recent|title` (snake_case) via serde and
  returns `page.total` as a number instead of null.
- api::pagination::PagedResponse gains a `with_total` constructor.

Backend coverage in tests/api_mangas.rs (4 new cases plus the existing
list_is_empty_initially updated to assert total: 0):
- list_returns_total_count_independent_of_pagination — limit=2 with 3
  rows returns 2 items and total=3.
- search_via_trigram_tolerates_typos — `?search=narto` finds Naruto.
- list_sort_title_orders_alphabetically — three out-of-order inserts
  come back A→Z.
- search_reflects_filtered_total — search narrows total to 1.

Frontend:
- lib/api/mangas.ts gains a `MangaSort` type and threads `sort` through
  listMangas's query-string builder.
- Home page renders a "Sort" select (Recent / Title A→Z) that re-runs
  the list query, and shows "Showing N of M" when total is present.

Lockstep version bump to 0.8.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:46:16 +02:00
MechaCat02
e92c581c7b feat: bookmarks (CRUD + per-user listing + frontend toggle)
Backend:
- Migration 0004_bookmarks_unique.sql adds a partial unique index on
  (user_id, manga_id) WHERE chapter_id IS NULL. The 0001 UNIQUE
  constraint over (user_id, manga_id, chapter_id) doesn't block dupes
  when chapter_id is NULL under Postgres's default NULLS DISTINCT, so a
  user could otherwise bookmark the same manga twice at the manga
  level. Chapter-level dupes are still caught by the 0001 constraint.
- repo::bookmark with create / list_for_user / find_owner / delete.
  create catches the 23505 unique violation and surfaces it as
  AppError::Conflict so handlers return a clean 409.
- POST /api/v1/bookmarks { manga_id, chapter_id?, page? } — CurrentUser
  required. Pre-validates the manga exists (404 if not) and, when
  chapter_id is supplied, that the chapter belongs to that manga (also
  404), so FK violations can't bubble up as 500s.
- DELETE /api/v1/bookmarks/{id} — owner-only. 404 if unknown, 403 if it
  exists for another user, 204 on success. Idempotent: deleting an
  already-deleted bookmark is 404, not 500.
- GET /api/v1/me/bookmarks — paged envelope, sorted by created_at DESC,
  scoped to the current user so the URL itself can't be used to peek at
  someone else's bookmarks.

Integration coverage in tests/api_bookmarks.rs (9 cases): create+list
returns only own; duplicate manga-level bookmark → 409; unknown manga
→ 404; unauthenticated POST → 401; user A cannot delete user B's
bookmark (403); unknown delete → 404; double-delete → 404, not 500;
/me/bookmarks requires auth; paged envelope shape on empty list.

Frontend:
- lib/api/bookmarks.ts with createBookmark / deleteBookmark /
  listMyBookmarks. listMyBookmarksOrEmpty wraps the 401 case so pages
  can render anonymously without try/catch boilerplate.
- /manga/[id] overview: pre-loads the user's bookmark list in its load
  function and renders either:
  - "★ Bookmarked" / "☆ Bookmark" toggle with aria-pressed when authed;
    click POSTs or DELETEs and mutates a local working copy of the
    bookmark list (optimistic UI without re-fetching);
  - or a "Sign in to bookmark" link for anonymous users.
- /bookmarks page lists the current user's bookmarks (chapter-level
  bookmarks link into the reader, manga-level back to the overview).
  Anonymous users see a sign-in prompt instead of a 401 page.

E2E in e2e/bookmarks.spec.ts (3 cases): authed toggle round-trip
(bookmark, see in /bookmarks list, unbookmark); anonymous user gets the
sign-in CTA on the overview; anonymous /bookmarks shows the sign-in
prompt. Existing reader.spec.ts updated for the new
bookmark-signin/toggle test IDs.

Lockstep version bump to 0.7.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:40:27 +02:00
MechaCat02
9af070608b feat: streaming files endpoint + reader pages + chapter pages metadata
Backend:
- Migration 0003_pages.sql adds a `pages` table (id, chapter_id,
  page_number, storage_key, content_type) with a unique (chapter_id,
  page_number). New table because chapter pages can have different MIME
  types per page; reconstructing keys from a single template would
  break the moment a chapter mixes png and jpg pages.
- `domain::Page` + `repo::page` (create + list_for_chapter).
- The chapter upload handler now inserts one page row per part as it
  writes the bytes to storage.
- GET /api/v1/mangas/{id}/chapters/{n}/pages returns `{pages: [...]}`
  with the storage_key clients need to construct image URLs. 404 if
  the manga or chapter doesn't exist; reads are public.

Storage trait grows `get_stream(&str) -> StreamingFile` returning a
`Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>` + size. The
local backend implements via `tokio::fs::File` + `tokio_util::io::
ReaderStream` with a 64 KiB chunk size. GET /api/v1/files/*key now
streams via `axum::body::Body::from_stream` instead of buffering — the
test asserts a 200 KiB file emits >1 frame end-to-end through the
router.

Frontend:
- lib/api/client.ts gains `fileUrl(key)` so components don't
  reconstruct the `/api/v1/files/...` path manually.
- lib/api/chapters.ts gains `ChapterPage` type + `getChapterPages` (the
  type is named ChapterPage to avoid colliding with `Page` from
  client.ts, which is the pagination envelope).
- /manga/[id]/+page.svelte: overview with cover, title, author,
  description, chapter list, and a disabled bookmark control (real
  bookmarking lands in feat/bookmarks). Responsive at 640 px.
- /manga/[id]/chapter/[n]/+page.svelte: paginated reader. Current page
  loads eagerly; next page is preloaded in a hidden img so navigation
  feels instant. Keyboard handler maps ArrowRight/j/Space → next,
  ArrowLeft/k → prev, Home/End → first/last; skips when the user is
  typing in an input. Focus ring on the prev/next buttons.
- SSR is disabled on both routes via `export const ssr = false` so the
  client-only fetch flow doesn't need to be replicated server-side; the
  routes are interactive features, not SEO surfaces.
- E2E (e2e/reader.spec.ts): overview shows the title/cover/chapter
  list; reader pages through three pages via ArrowRight, j, k, and
  ArrowLeft, and the preload img holds the page-2 src on initial load.

Lockstep version bump to 0.6.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:32:08 +02:00
MechaCat02
383cfbed3b feat: argon2id passwords, session cookies, bot bearer tokens
Adds the full auth flow. Reads stay public; writes (currently only POST
/api/v1/mangas) require a CurrentUser. Both browsers and bot scripts hit
the same endpoints — they just present credentials differently.

Migration 0002_auth.sql introduces users.password_hash, a sessions
table, and an api_tokens table. Sessions and api_tokens store only
sha256(raw_token) — the raw value lives in the cookie or the
Authorization header.

New endpoints under /api/v1/auth/:
- POST /register — argon2id hash, creates a session, sets cookie.
- POST /login — verifies, rotates to a fresh session (old ones expire
  naturally so other devices stay signed in).
- POST /logout — deletes the server-side session row + clears the
  cookie via Max-Age=0.
- GET  /me — current user via the new CurrentUser extractor.
- POST /tokens — issue a bot bearer token; raw value returned exactly
  once at creation.
- DELETE /tokens/{id} — owner-only: 404 if unknown, 403 if it exists
  but belongs to another user, 204 on success.

The CurrentUser axum extractor resolves cookie first, then
Authorization: Bearer; failure → AppError::Unauthenticated (401). New
AppError variants Unauthenticated/Forbidden/Conflict carry the matching
envelope codes; the top-level match in `code()` stays exhaustive.

Backend integration coverage in tests/api_auth.rs: register sets a
HttpOnly SameSite=Lax cookie and never leaks password_hash; duplicate
username → 409; weak password → 400; login rotates the cookie; wrong
password / unknown user → 401; /me with vs without cookie; logout
invalidates the cookie; bot-token roundtrip via Bearer; user A cannot
delete user B's token (403); unknown delete → 404.

Frontend:
- lib/api/auth.ts — typed wrappers; me() returns null on 401.
- lib/session.svelte.ts — per-tab user state with a seq counter to
  guard against an in-flight /me clobbering a fresh setUser.
- lib/api/client.ts — request<T> returns undefined for 204.
- routes/login + routes/register — forms with action="javascript:void(0)"
  so the no-JS path is a no-op (avoids the hydration-race where a
  pre-attach click would submit via the browser default).
- routes/+layout.svelte — session-aware nav: spinner → user + Logout,
  or Login / Register.
- e2e/auth-flow.spec.ts — login flips the layout, logout flips back;
  bad credentials surface the API error message.

Config grows AuthConfig (cookie_secure, cookie_domain, session_ttl_days)
and CORS_ALLOWED_ORIGINS. CORS middleware is mounted in app::build and
stays a no-op (same-origin) until origins are listed.

Lockstep version bump to 0.3.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:04:25 +02:00
MechaCat02
ce9a01793f feat: nest API under /api/v1, structured error envelope, paged lists
Move every handler from /api/* to /api/v1/*. /api/* is now reserved for
future versioning.

Standardise the error response shape across the API as
{"error": {"code": "snake_case", "message": "..."}}. AppError gains a
`code()` whose top-level variants are matched exhaustively without a
wildcard — new variants are a compile error until coded. 500-class
responses always emit the fixed "internal error" string and log the
real cause via tracing only.

Lock in the list pagination envelope as {"items": [...], "page": {
"limit", "offset", "total"}} and apply it to GET /api/v1/mangas. `total`
serialises as null until feat/list-search-polish lands an indexed count.

The frontend client parses the envelope into ApiError.code with an
http_error fallback for non-JSON bodies. listMangas now returns the
paged shape; the root route consumes .items. New client.test.ts covers
envelope parsing and the fallback paths.

Lockstep version bump to 0.2.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 21:41:20 +02:00
MechaCat02
6c1d04aaf4 chore: initial project scaffold
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>
2026-05-16 21:05:16 +02:00