Commit Graph

7 Commits

Author SHA1 Message Date
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
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
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
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
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