Audited on iPhone SE (320px), iPhone 13, Pixel 5 emulations:
- FOUND: login/search inputs at 13.3px (< 16px) -> iOS Safari
auto-zooms the page on focus. Fixed: 16px in style.css AND
login.html (separate page with own CSS).
- FOUND: no touch-action anywhere -> double-tap-zoom tap delay on
buttons. Fixed: touch-action: manipulation on interactive elements
in both stylesheets.
- FOUND: .pl-spin missing from prefers-reduced-motion. Added.
- VERIFIED CLEAN: no horizontal overflow at 320/390/393px, controls
row fits, chip+gauge in viewport, episode grid 66x31 ok,
Tap-to-play gesture flow works on BOTH hls.js and native paths
(simulated iOS autoplay block), toast fits, zero JS errors,
desktop playback regression OK, 18/18 API tests.
assets v=7; login.html is no-store so updates without a bump.
Root cause of 'eternal spinner': a browser without working hls.js
(no MSE, or hls.min.js blocked) threw 'HLS not supported' which the
old showStreamError hid UNDER the spinner overlay, and there was no
fallback — so the player sat on Loading… forever with no /stream
request ever leaving the browser (matched the prod log exactly).
- no-hls.js + canPlayType(mpegurl) -> v.src = proxied m3u8, native
<video> plays it (iOS < 17 path). Verified: readyState 4, playing,
no hls.js at all.
- no-hls.js + no native HLS -> VISIBLE 'Stream error: ...'
overlay with Retry (was: hidden under the spinner)
- every play() catch routes through the visible overlay now
Desktop hls.js regression: still plays. assets v=6.
- Loading/Buffering overlays now show a rotating ring, not just text
- thin gauge under the video: how much is buffered ahead (0-30s scale),
fills while loading, drains as it plays — you can see a stall building
- corner chip: live CDN KB/s from /api/cdn, green dot OK / amber degraded,
refreshed on every stall — explains WHY it's buffering
Verified in-browser (CDN-independent): chip reflects live state (0 KB/s
degraded), gauge fills from video.buffered, stall overlay + ring,
playing clears. 18/18 API tests. assets v=5.
Dribbling CDN segments (logged at 17.8s/59.7s) made the player stare
at 'Buffering…' for half a minute per attempt. Cap total load at 10s
so a retry kicks in sooner (timeoutRetry still 6x, backoff 1-20s);
fast segments (0.1-0.2s) are unaffected.
The name was only in the grid tooltip (hover-only, invisible on touch).
Add a persistent now-playing line between the player and controls:
'E1 — Ryomen Sukuna'. setNowEp() runs on every play() and refreshes
when the async Jikan name lands late. Bumped asset version to v=3.
index.html is no-store (always fresh) but app.js/style.css are cached
max-age=300 in the browser — and possibly in the HTTPS proxy — so users
kept seeing the pre-fix UI (episode names in buttons) long after the
removal shipped. A versioned URL from the always-fresh index.html
bypasses both caches. Bump ?v= on every app.js/style.css edit.
- toasts: aria-live region + action button (used for history delete Undo)
- history cards: keyboard-reachable title link; delete = remove + Undo
(re-POSTs the entry; no full list re-render)
- fail(): home/retry affordances instead of a dead-end error line
- per-view document.title + move focus to the new view on render
- player controls: episode jump (E#), auto-next toggle (persisted),
picture-in-picture (when supported)
- auto-next off: 'Next: En' toast with Play instead of the countdown
- no-results search + empty home: recent-search chips (shared helper)
- stats: 30d/90d/1y period chips (api already supported days=N)
- search input: autocapitalize/spellcheck off; reduced-motion disables
shimmer/toast animation; username in the menu on narrow screens
history filter chips, per-ep progress bars, stats in user menu
- sub/dub choice persists per anime (localStorage aniweb_lang_<slug>),
so search/reload no longer drops dub watchers back to sub
- 'hide fillers' toggle persists (aniweb_ff)
- continue-watching hero shows '~Xm left' when position+duration known
- detail page: prominent 'next episode' button when the resumed ep is
finished (auto-removed on play)
- ?history: all / watching / completed / on-hold filter chips (client-side)
- ep list: thin progress bar on partially-watched episodes
- stats page link moved into the user menu (always reachable)
- search results show a count line
- server/users.py replaces credentials.py: users.json (0600), PBKDF2,
one-time migration of the legacy single password to user admin
(history.json -> history/admin.json)
- accounts are CLI-managed only: python -m server.users add|rm|list
(no self-signup on an internet-exposed app); /signup route removed,
login page back to username + password
- per-user 30-day signed session cookie (payload carries the
username), per-user lockout, GET /api/me, POST /account/delete
- per-user watch history: server/data/history/<user>.json; ANIWEB_DATA
env override for the data dir
- header chip: logged-in user, logout, delete account
- tests/: throwaway-server suite (port 8399, temp data dir) — 9 HTTP
tests + 2 Playwright browser tests (login UI, real video playback
behind auth); runs via python tests/run.py
- jsdelivr CDN was a silent single point of failure (Hls undefined ->
unhandled rejection -> black screen with no feedback)
- play() now catches API errors; hls fatal + video media errors render
'Stream error: ...' under the player
- server now binds 0.0.0.0 (LAN access); was 127.0.0.1 only