diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index efd7759..0000000 --- a/PLAN.md +++ /dev/null @@ -1,160 +0,0 @@ -# Plan — UI/UX polish bundle (items 1–4 + 7) - -**STATUS: complete** — every item below shipped (see git log, c2461f8 and -friends). Kept as the planning record; the "deferred" list at the bottom -is stale (favicon, a11y pass, autoplay-next were all done afterwards). - -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=`; fixed with `stopPropagation()`. -- After a fatal stream error, `v.play()` rejects (no media) and - `showPlayOverlay` clobbered the failure overlay with "click to play"; - guarded by `playerBroken`. -- Test route pattern `**/stream/**` also matched the token endpoint - `/api/episode/{id}/stream` — narrowed to `**/stream/*`. -- Skeleton `.card.skel` matched the tests' `.card` selector — tests now - use `a.card`.