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.
The visible names made the grid uneven (long titles wrapped to 2-3
lines). Buttons now show only the E-number; the full name lives in
the title attribute (hover tooltip: 'E1 — Ryomen Sukuna'). Dropped
the now-unused .ep-name CSS.
anidb's episodes API has no names (id/number/filler only), but the
Jikan call we already make for intro timings carries them (title +
title_japanese). _parse_intros now keeps one entry per episode with
title and optional start/length (entries are never null anymore).
Client: attachEpNames() reuses the introCache promise, stamps
epsList[i].name, re-renders; buttons show the name (3-line clamp) +
full tooltip. Jikan down => no names, no error (same graceful
degradation as the skip-intro button).
Verified headless: Jujutsu Kaisen S1 24/24 buttons named
('E1 | Ryomen Sukuna' ...); S2 (Jikan 504 at the time) rendered
gracefully without names. 17/17 API tests.
- upstream: _intros(mal_id) -> per-episode {start,length} (seconds),
30d TTL cache; Jikan is down or show uncovered -> [] (no button)
- app: GET /api/intros/{mal_id}, never errors into the player
- player: 'skip intro' pill appears while currentTime is inside the
intro window (+8s grace); click seeks past; reappears if rewound.
Ep-1 intros are often missing in MAL data, so falls back to the
show's first known timing (usually identical across eps)
- test: _parse_intros unit test (Jikan itself is 504ing right now,
live path verified by hand: endpoint returns [] cleanly)
- 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
- labels now sit on the week CONTAINING the 1st of the month (old
logic labelled the first column starting in that month, so a month
beginning mid-week was labelled a whole column late — hovering the
cells under 'Aug' showed Jul dates); skip labels that would crowd
the previous one
- label + grid rows are inline-flex so flexbox shrink can't drift
them apart at narrow widths
- cell scale back to the orange accent (color-mix of --acc)
- GitHub's green scale (#0e4429 -> #39d353) instead of the orange
accent-mix
- Mon/Wed/Fri row labels down the left (right-aligned, muted),
month-label row offset to line up with the grid
- legend right-aligned under the grid
Label slots were 14px wide + 3px gap (17px pitch) vs grid columns
11px + 3px gap (14px pitch), so labels drifted up to ~150px right of
their column by the last month. Label slots are now exactly cell
width with min-width: 0 so the text overflows instead of widening
the slot.
- drop the history widget: the status board already groups the same
shows by state; full list stays at ?history
- 6 stat cards + top-3 -> one muted line (watch time, eps, active
days, streak, on hold) with 'see all ->' to ?stats
- section headers now small uppercase muted labels so the hero is
the only loud element (page h2s unaffected)
- heatmap sits in a rounded card panel
- heatmap now fetches /api/stats?days=365 (endpoint already caps at
365; no server change) so the grid spans the past year
- week columns with month labels (Aug, Sep, ...), first column
labelled by its first in-window day, final column padded
- grid wrapped in a horizontal-scroll container (53 weeks ~ 750px)
- replace 'Recently played' with one card per anime grouped by state:
Continue watching (<=6, progress + ~Xm left), On hold (<=4, dimmed,
days ago), Recently finished (<=6, check)
- hero button says 'Next ep' when the last ep is >=95% watched
- empty home shows recent searches as clickable chips (race-guarded:
only appended while the empty state is still on screen)
- helpers: byShow (latest row per anime), homeCard, showSec, link
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: /api/stats top_anime rows enriched — poster, total eps, cur_ep,
last_seen, status (finished/ongoing/hold heuristic), pace (ep/day),
sub/dub watch-time split, rewatches (backward jump >60s); new on_hold
+ best_streak top-level keys; optional ?slug= filter (json_extract)
- frontend: 'Most watched' rows = poster + meta + status chip + completion
bar (ongoing -> hold -> finished order); stat cards swap in on-hold and
best streak, errors demoted to a one-liner; anime detail page gets a
progress line (E12/24 · 4h 30m · 2.1 ep/day · status); history cards
dim + label 'on hold' after 7 days; savePos now includes total eps
- css: .chip, .stat-anime rows, .card.hold, .detail-prog; drop dead .stat-row
- test: test_stats asserts the new fields + slug filter
- server: 'play' event on successful stream fetch, 'error' on 404 stream
failures; new POST /api/events for client-reported playback errors
(only type=error accepted, fields allowlisted + length-capped)
- server: GET /api/stats?days=N — watch time from position-event pos
deltas (<=90s cap: bigger jumps are seeks), episodes started/completed
(>=95% of dur), days active, weekday histogram, top anime/searches,
sub/dub split, error count
- frontend: ?stats page (cards, weekday bars, most-watched, top
searches) + 'stats' link on home; playerFail now reports the error
- test: test_stats with synthetic viewing data (deltas, isolation, 422)
- showHome(): most recent entry renders as a hero card (poster,
episode/lang/position, progress bar, Resume); the rest become
mini poster cards with progress + relative timestamp
- history entries now persist dur (video.duration) and poster;
/api/anime returns poster (parsed from the details page)
- old entries without dur render without a bar (backward compatible)
- browser test asserts the seeded entry, not a section header that
only existed via a cross-test side effect
- 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
- no ?ep= param: resume last watched ep of the season from history,
else start at ep 1 (verified in headless Chromium, no click needed)
- if the browser blocks autoplay (no user gesture yet), show a
'click to play' overlay instead of a silent black screen