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:
1 parent
66a0bb16e6
commit
de0bf8b156
1 file changed
-160
@@ -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`.
|
|
||||||
Reference in new issue
Block a user