Full-featured web UI

Rewrites the page around a persistent player (speed, seek, resume,
MediaSession, keyboard shortcuts), artwork, filter tabs, episode search,
pagination and live progress, with modals and toasts replacing prompt()
and a status line.

Backend gains the metadata that makes that possible: feed and episode
artwork, durations, season/episode numbers and playback position, plus
filters, search, totals, mark-all-read, download-latest and OPML over
HTTP. Schema changes arrive through a real migration, since CREATE TABLE
IF NOT EXISTS does nothing to an installed database.

Fixes filtering, which returned 500 whenever no search term was given:
the search clause was dropped while its parameter was still bound.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPyeapneuXrCdojsaiXGbe
This commit is contained in:
2026-09-10 01:42:55 +00:00
parent 93b4815d84
commit 5666166769
6 changed files with 1143 additions and 310 deletions

View File

@@ -56,6 +56,47 @@ and until now nothing set them.
---
## 2026-09-10 — Phase 3: full-featured UI
Rewrite of `web/index.html` (~714 lines) plus the backend it needed.
**New stored metadata**, all of which the real feed carries on all 131 episodes and none of which
was being captured: feed and per-episode artwork (`itunes:image`), duration (`itunes:duration`,
parsed from either raw seconds or a `1:34:09` clock), season/episode numbers, and a playback
`position` so episodes resume.
**Schema migration.** `Db::open` now runs `migrate()`: `PRAGMA table_info` then `ALTER TABLE ADD
COLUMN` for anything missing, since `CREATE TABLE IF NOT EXISTS` does nothing to an existing table.
Tested on a *copy* of the live database first. Accidentally SIGPIPE'd it partway through (a `head -4`
on the output) which usefully proved it is idempotent and self-heals: the next run added the
remaining columns, 131 entries and 6 downloads intact.
**New endpoints:** entry filters (all/unread/downloaded/flagged), case-insensitive search over title
and notes, pagination totals, `POST .../position`, `read-all`, `download-latest`, and OPML
import/export over HTTP.
**UI:** persistent player bar surviving navigation (play/pause, ±15s/30s, 0.8-2.5x speed, volume,
scrubber, all remembered in localStorage), resume playback via `sendBeacon` every 10s, MediaSession
for lock-screen controls, keyboard shortcuts (space, arrows, `/`, Esc), artwork everywhere with
generated-initials fallback, filter tabs, episode search, load-more pagination, live progress bars
off SSE, real modals, toasts, light/dark toggle, and a mobile layout with a collapsing sidebar.
**Bug found and fixed in review: filtering was broken while searching worked.** `count_entries`
dropped the search clause when no term was given but still bound `?2`; rusqlite rejects a parameter
the statement never mentions -- *"Wrong number of parameters passed to query. Got 2, needed 1"*. So
`?q=...` worked and a plain Unread/Downloaded/Flagged filter 500'd. The clause is now always present
with `?2 = ''` short-circuiting it. Regression test runs all four filters with and without a search
term and asserts the page and the count agree.
**A non-bug worth recording:** artwork looked broken on the live DB (0/131) while a fresh DB filled
all 131. The parser was fine -- the live DB simply had not been rescanned yet, since backfill happens
through `record_entry`'s update path. After a forced scan: 131/131 image, duration and season.
Verified live: all four filters return sane totals (131/129/10/0), search works (`session zero` -> 2,
`trunk` -> 10), `/media/1` with a Range header returns 206, index serves 34 KB. `cargo test` 32/32.
---
## 2026-09-10 — Fixed: the UI's Download button downloaded the wrong episodes
Reported from the running instance: clicking Download on *Music from a Darkened Room | Session Zero*