feat(e24): the Home surface — one question, one answer, both sources

E24 S2. A new `home` module in layout C+ (the mockup the owner picked): the
time-budget chips on top, one large hero that answers them, then the comfort
shelf and a cross-source resume rail. Home is the landing — deliberately OVER
the remembered last module, since landing on whatever you happened to close the
app on is the habit this epic exists to replace; `LS.startOnHome = "0"` opts an
account back out (Settings toggle lands in S4).

No third card component: each unified card carries the native feed / Plex
payload, so the rails render through the existing VideoCard (`cardsSmall`) and
PlexPosterCard, and `aspect` decides the tile shape without branching on source.
Plex playback hands off to the Plex module via a new `jumpTo` type — the player
lives there with its lazy hls.js and its provider tree, and mounting a second one
on the landing page to save one hand-off would be the expensive way round.

Four defects found by driving it in a real browser against the live account, each
now pinned by a mutation-checked test:

- **The reroll gave the same card back.** A new seed only re-draws the SURPRISE
  tier; the resume tier deterministically returns the most recently progressed
  thing that fits, so anyone with something half-finished pressed a dead button.
  `/pick` now takes an `exclude` list of `<source>:<id>` keys and the client keeps
  a rolling window of 8. Excluding a SHOW also excludes the episode it resolves
  to — otherwise the same suggestion comes back wearing a hat.
- **"Nothing to suggest yet" flashed over a full library** whenever a reroll was
  in flight: the new query key has no data for a beat. An empty state standing in
  for a loading one is the dishonesty the house rules name outright. The hero now
  falls back to the loaded payload while a pick is pending, and only trusts a null
  once the pick has RESOLVED.
- **A budget change replaced the page — chips included — with a spinner**, so the
  control you just clicked vanished under your cursor. `keepPreviousData`, the
  same reason the feed uses it.
- **The hero printed twice**, once as itself and once as the first resume tile.

Two smaller UI calls made from the same session: the budget caption sits NEXT to
its chips rather than in SegmentedToggle's justify-between mode, which on an 80rem
page throws the answers to the far edge; and the feed takes the YouTube mark its
own toolbar already uses, since Home now owns the house icon.

Gate green: ruff, ruff format, 3x tsc, eslint, prettier, knip, vitest 780,
pytest 989. Verified end to end on localdev against the real account: budget
chips re-answer (matched on REMAINING time — a 51-minute video with 2:46 left is
a ten-minute answer), the reroll walks three distinct heroes, and a Plex show
tile opens its drill-down in the Plex module.
This commit is contained in:
2026-09-06 04:14:14 +02:00
parent 53b50231ff
commit f3c0b7be23
21 changed files with 964 additions and 14 deletions
+37
View File
@@ -0,0 +1,37 @@
{
"navLabel": "Home",
"title": "What should I watch?",
"error": "Could not load your suggestions.",
"budget": {
"ask": "How much time do you have?",
"short": "10 minutes",
"medium": "30 minutes",
"long": "An evening",
"any": "Doesn't matter"
},
"hero": {
"resume": "Where you left off",
"resumeLeft": "Where you left off · {{time}} to go",
"comfort": "From your shelf",
"surprise": "Surprise",
"play": "Play",
"continue": "Continue",
"reroll": "Something else",
"emptyAny": "Nothing to suggest yet. Subscribe to a channel, or pin something to your shelf.",
"emptyBudget": "Nothing in your library fits that much time."
},
"comfort": {
"title": "Comfort shelf",
"empty": "Nothing pinned yet. Pin a channel, a show or a playlist you always come back to."
},
"continue": {
"title": "Continue watching",
"empty": "Nothing half-finished right now."
},
"shelf": {
"unwatchedCount_one": "{{count}} unwatched",
"unwatchedCount_other": "{{count}} unwatched",
"itemCount_one": "{{count}} video",
"itemCount_other": "{{count}} videos"
}
}
+37
View File
@@ -0,0 +1,37 @@
{
"navLabel": "Kezdőlap",
"title": "Mit nézzek most?",
"error": "Nem sikerült betölteni az ajánlatokat.",
"budget": {
"ask": "Mennyi időd van?",
"short": "10 perc",
"medium": "30 perc",
"long": "Egy este",
"any": "Mindegy"
},
"hero": {
"resume": "Ott hagytad abba",
"resumeLeft": "Ott hagytad abba · {{time}} van hátra",
"comfort": "A polcodról",
"surprise": "Meglepetés",
"play": "Lejátszás",
"continue": "Folytatás",
"reroll": "Mást kérek",
"emptyAny": "Még nincs mit ajánlani. Iratkozz fel egy csatornára, vagy tűzz valamit a polcodra.",
"emptyBudget": "Ennyi időbe semmi sem fér bele a könyvtáradból."
},
"comfort": {
"title": "Komfort polc",
"empty": "Még nincs ide tűzve semmi. Tűzz ki egy csatornát, sorozatot vagy listát, amihez mindig visszatérsz."
},
"continue": {
"title": "Folytatás",
"empty": "Most nincs félbehagyott dolgod."
},
"shelf": {
"unwatchedCount_one": "{{count}} megnézetlen",
"unwatchedCount_other": "{{count}} megnézetlen",
"itemCount_one": "{{count}} videó",
"itemCount_other": "{{count}} videó"
}
}