Files
ani-web/PLAN.md
T
jozamudi 2ec0f8a034 ui: loading states, player failure overlay + retry, toasts, back nav, full history page with delete
Bundle 1-4+7 from PLAN.md: skeleton loaders, player Loading/Buffering/fatal overlay with Retry, action toasts, back-to-home crumb + logo fix, ?history full-history view with per-entry delete (GET /api/history?all=1, DELETE /api/history). 14/14 tests green.
2026-08-19 18:20:42 +00:00

7.0 KiB
Raw Blame History

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
  5. 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 <p class="error"> 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: <div id="toasts"></div>.
  • 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 <a> 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=<slug>; 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.