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>
This commit is contained in:
39
backend/migrations/0011_history.sql
Normal file
39
backend/migrations/0011_history.sql
Normal file
@@ -0,0 +1,39 @@
|
||||
-- Per-user reading progress and uploader attribution.
|
||||
--
|
||||
-- Reading progress is the simplest shape that supports "jump to last
|
||||
-- read chapter" — one row per (user, manga). The reader writes
|
||||
-- through on chapter open and on page advance (debounced); the
|
||||
-- history view shows them sorted by most-recently-touched.
|
||||
--
|
||||
-- Uploader attribution adds nullable `uploaded_by` columns to the two
|
||||
-- upload sinks. Historical rows have NULL because the original
|
||||
-- handlers didn't track this; new uploads stamp the current user.
|
||||
|
||||
CREATE TABLE read_progress (
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
manga_id uuid NOT NULL REFERENCES mangas(id) ON DELETE CASCADE,
|
||||
-- Chapter is nullable so a deleted chapter doesn't blow away
|
||||
-- the user's progress row entirely — they just see "(chapter
|
||||
-- removed)" in the history UI.
|
||||
chapter_id uuid REFERENCES chapters(id) ON DELETE SET NULL,
|
||||
page integer NOT NULL DEFAULT 1 CHECK (page >= 1),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (user_id, manga_id)
|
||||
);
|
||||
|
||||
-- Most queries on this table want "most recent first" per user; the
|
||||
-- composite index makes both filter and sort index-only.
|
||||
CREATE INDEX read_progress_user_idx
|
||||
ON read_progress (user_id, updated_at DESC);
|
||||
|
||||
ALTER TABLE mangas
|
||||
ADD COLUMN uploaded_by uuid REFERENCES users(id) ON DELETE SET NULL;
|
||||
CREATE INDEX mangas_uploaded_by_idx
|
||||
ON mangas (uploaded_by, created_at DESC)
|
||||
WHERE uploaded_by IS NOT NULL;
|
||||
|
||||
ALTER TABLE chapters
|
||||
ADD COLUMN uploaded_by uuid REFERENCES users(id) ON DELETE SET NULL;
|
||||
CREATE INDEX chapters_uploaded_by_idx
|
||||
ON chapters (uploaded_by, created_at DESC)
|
||||
WHERE uploaded_by IS NOT NULL;
|
||||
Reference in New Issue
Block a user