# 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=