remove PLAN.md: one-shot planning record for a shipped bundle; its content is superseded by git log + docs/CHANGES-2026-09.md

This commit is contained in:
jozamudi committed 2026-09-20 23:37:20 +00:00
1 parent 66a0bb16e6
commit de0bf8b156
1 file changed
-160
-160
View File
@@ -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 `<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`.