README: storage is SQLite (aniweb.db), not JSON files; CDN serves a single quality (no ABR) — replaced the wrong 1080/720/360 claim; document the 16-lane parallel fetch, intro-skip, episode names, buffering UI, /api/cdn + background probe, the ?v= cache-busting rule, and the client self-report diagnostics. SPEC: same single-rendition correction (2 places). PLAN: marked complete (all items shipped; deferred list was stale).
7.2 KiB
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:
- Loading states — skeleton cards + player spinner
- Player failure + buffering overlay
- Toasts / confirmations
- Back/breadcrumb nav + logo fix
- 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:
mainis 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 usesBASE). - History:
GET /api/historyreturns 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.skelgrid inmain.showSearch: renderskel(8)beforeawait api().showAnime:skel()before the detail fetch; "loading episodes…" placeholder in#epsuntil the episode list resolves.- Player:
.pl-overlay(shared with item 2) shows "Loading…" right after the video element is attached; hidden onplaying, "Buffering…" onwaiting(guarded so it never clobbers the failure overlay).
style.css:@keyframes shimmer,.skel,.pl-overlay(absolute inset-0 over the video —.playeris alreadyposition: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
fatalerror →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. showStreamErrorstays 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.htmlshows 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 inlineonclickfrom the logo (keephreffallback).app.jsboot:logo.href = BASE(BASE ends in/→ home, clears query) — same proxy-safe mechanism as all other navigation.- Detail view:
← homecrumb link in.detail-head, before the title. Esccloses 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 withstr(slug_id) ==andstr(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 whendurknown) each with a×delete button (top-right; stopPropagation). Cards are clickable divs (valid HTML — a button inside<a>is not). - Delete →
DELETEfetch →toast("Removed from history")→ re-render. - Home: when the top-5 cap is reached, "Recently played" gets a
view all →link to?history.
- Boot:
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 intep_id, one string) →?all=1sees 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.histcards → click.del(force) → 1 card +.toastvisible.tests/test_browser.py::test_stream_failure_retry(admin):page.routeaborts**/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 diffreviewed before committing (no stray rewrites).
Execution order
- Backend:
app.pyhistory endpoints + API test app.js: items 1, 2, 3, 4, 7index.html+login.htmlstyle.css- Browser tests +
run.py - Full suite + screenshots
- Restart production, commit (includes this plan)
Outcome
Shipped. Full suite green (10 API + 4 browser tests).
Bugs found while implementing/testing (fixed):
?historyis a valueless param —p.get("history")is""(falsy); boot routing had to usep.has("history").- Delete button click bubbled to the card's
onclick→ navigated to?anime=<slug>; fixed withstopPropagation(). - After a fatal stream error,
v.play()rejects (no media) andshowPlayOverlayclobbered the failure overlay with "click to play"; guarded byplayerBroken. - Test route pattern
**/stream/**also matched the token endpoint/api/episode/{id}/stream— narrowed to**/stream/*. - Skeleton
.card.skelmatched the tests'.cardselector — tests now usea.card.