Search for an anime to get started.
diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..81a2473 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,156 @@ +# Plan — UI/UX polish bundle (items 1–4 + 7) + +The agreed bundle from the polish list: + +1. **Loading states** — skeleton cards + player spinner +2. **Player failure + buffering overlay** +3. **Toasts / confirmations** +4. **Back/breadcrumb nav + logo fix** +7. **Full history page + delete entries** + +Out of scope (deferred): favicon, search-box clear/count, synopsis clamp, a11y pass, +resume toast, sub fallback, autoplay-next, light theme. + +## Current state (grounding) + +- SPA with three views: `?q=` search, `?anime=&season=&ep=&lang=` detail+player + (player is inline on the detail page), no-param = home. +- No loading feedback anywhere: `main` is blank during fetches; the player is a + black box until the first frame. +- Stream errors render as a `
` **below** the video — easy to + miss, no retry. +- No in-app way back to home (browser back only). **The logo links to + hardcoded `/` — breaks behind the subpath reverse proxy** (everything else + uses `BASE`). +- History: `GET /api/history` returns only the top 5; no delete endpoint; + no UI to remove an entry (only nuke the whole account). + +## Item 1 — loading states + +- `app.js`: + - `skel(n)` helper → shimmering `.card.skel` grid in `main`. + - `showSearch`: render `skel(8)` before `await api()`. + - `showAnime`: `skel()` before the detail fetch; "loading episodes…" + placeholder in `#eps` until the episode list resolves. + - Player: `.pl-overlay` (shared with item 2) shows "Loading…" right after + the video element is attached; hidden on `playing`, "Buffering…" on + `waiting` (guarded so it never clobbers the failure overlay). +- `style.css`: `@keyframes shimmer`, `.skel`, `.pl-overlay` + (absolute inset-0 over the video — `.player` is already `position:relative`). + +**Accept:** no blank `main` during a search; skeleton visible; player shows +Loading → (Buffering) → hidden once the first frame plays. + +## Item 2 — player failure + retry + +- `app.js`: + - `playerFail(msg)` → overlay on the video: message + **Retry** button. + - hls.js `fatal` error → `stopHls()` + `playerFail(details + HTTP code)`. + - `video.onerror` → `playerFail("media error")`. + - Retry re-runs `play(ep_id, ep_no)` — fresh stream fetch; the server-side + cold-cache retry already recovers rotated playlist URLs. + - `showStreamError` stays only for pre-play fetch errors (e.g. 404 "no + eng stream"). +- `showPlayOverlay` (autoplay-blocked prompt) hides the loading overlay first. + +**Accept:** headless: abort `**/stream/**` → detail page auto-plays → overlay +with Retry appears; unroute → click Retry → `video.readyState >= 4`. + +## Item 3 — toasts / confirmations + +- `index.html`: `
`. +- `app.js`: `toast(msg)` → `.toast`, auto-removed after 2.6 s. + - Used for history-entry removal (stays on the page). + - Logout / delete account navigate away, so they use one-shot banners + instead: `login?signedout=1` / `login?deleted=1`; `login.html` shows a + fixed `.banner` ("Signed out." / "Account deleted.") when present. +- `style.css`: `.toast` (fixed bottom-center, fade-in), login `.banner`. + +**Accept:** entry removal → toast appears then disappears; logout → banner on +the login page; account delete → banner. + +## Item 4 — back nav + logo fix + +- `index.html`: drop the inline `onclick` from the logo (keep `href` fallback). +- `app.js` boot: `logo.href = BASE` (BASE ends in `/` → home, clears query) — + same proxy-safe mechanism as all other navigation. +- Detail view: `← home` crumb link in `.detail-head`, before the title. +- `Esc` closes the hamburger menu. + +**Accept:** from `?anime=…`, logo click → home hero visible; crumb works. + +## Item 7 — full history page + delete entries + +- `server/app.py`: + - `GET /api/history?all=1` → full list (no top-5 cap), same no-store header. + - `DELETE /api/history?slug_id=…&ep_id=…` → removes all entries with + `str(slug_id) ==` and `str(ep_id) ==` (loose match on purpose: legacy rows + store the same ep as int *and* string — one click clears both ghost rows); + returns `{"removed": n}`. +- `app.js`: + - Boot: `?history` → `showHistory()`: skeleton → grid of cards (poster, + title, `E# · dub/sub · time-ago`, progress bar when `dur` known) each with + a `×` delete button (top-right; stopPropagation). Cards are clickable + divs (valid HTML — a button inside `` is not). + - Delete → `DELETE` fetch → `toast("Removed from history")` → re-render. + - Home: when the top-5 cap is reached, "Recently played" gets a + `view all →` link to `?history`. +- `style.css`: `.del` (absolute top-right circle, danger on hover), + `.sec-head` (flex row for section title + link), `.card.hist`. + +**Accept:** API: `?all=1` returns everything, DELETE removes only the matching +entry and is idempotent. Browser: `?history` lists entries; `×` removes one +card and shows a toast. + +## New tests + +- `tests/test_api.py::test_history_all_and_delete` (alice): POST 2 entries + (one int `ep_id`, one string) → `?all=1` sees both → plain GET still caps at + 5 → DELETE the int row via string query param (loose match) → `removed ≥ 1`, + row gone, other row untouched → second DELETE → `removed == 0`. +- `tests/test_browser.py::test_history_page_and_delete` (new seeded user + **erin**, self-contained entries via urllib): `?history` → 2 `.card.hist` + cards → click `.del` (force) → 1 card + `.toast` visible. +- `tests/test_browser.py::test_stream_failure_retry` (admin): `page.route` + aborts `**/stream/**` → search + open detail (auto-play fails) → wait for + `.pl-overlay button` (Retry) → `page.unroute` → click Retry → video plays + (`readyState >= 4`, 120 s timeout for cold upstream). +- `tests/run.py`: seed erin; register both new browser tests. + +All waits use explicit `polling=` (headless rAF stall) and `force=True` +clicks, per the established Chromium 151 quirk. + +## Verification + +- `.venv/bin/python tests/run.py` → all green (11 existing + 3 new). +- Ad-hoc Playwright screenshots: home, history page, failure overlay, mobile + 390 px — eyeballed. +- `git diff` reviewed before committing (no stray rewrites). + +## Execution order + +1. Backend: `app.py` history endpoints + API test +2. `app.js`: items 1, 2, 3, 4, 7 +3. `index.html` + `login.html` +4. `style.css` +5. Browser tests + `run.py` +6. Full suite + screenshots +7. Restart production, commit (includes this plan) + +## Outcome + +Shipped. Full suite green (10 API + 4 browser tests). + +Bugs found while implementing/testing (fixed): + +- `?history` is a valueless param — `p.get("history")` is `""` (falsy); + boot routing had to use `p.has("history")`. +- Delete button click bubbled to the card's `onclick` → navigated to + `?anime=Search for an anime to get started.