Files
ani-web/PLAN.md
T
jozamudi 14bdb04ef6 Docs: sync with reality
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).
2026-09-01 21:25:13 +00:00

161 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `<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`.