889d2acf520899b35eef081a6076da8b6c64f19c
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3709b71a94 |
fix(e24): the review's low findings — dead constants, honest labels, a 500 that should be a 404
`add_pin`'s IntegrityError handler assumed the unique constraint was the only way in and re-raised otherwise, so a target row pruned between `resolve_ref` confirming it and the INSERT (a Plex sync dropping a removed item) tripped the FK and handed the user a 500. Semantically that is the same "no such thing" the pre-check already returns. Untested: the race needs two concurrent transactions. The five per-kind unique constraints had no test at all — the route's own dedupe returns before reaching the DB, and the migration drift check filters constraint diffs out as noise, so they could go missing with every lane green. Frontend, all from the same review: the shelf's unpin row is now `danger` like every other destructive row in the app; the hero's artwork button says what it will actually do and names the item instead of always announcing "Play"; the empty-budget message offers a reroll rather than `StateMessage`'s "Retry", which promised a re-run of something that had not failed; `setState` and the watch-later toggle catch their rejections instead of leaving an unhandled promise and a click that silently does nothing. `StateMessage` gained a `compact` rung, which retires the frontend's only two `!important` overrides — they existed to beat the primitive's own padding from the call site, and were the copyable idiom for the next surface that wanted a compact empty state. Dead vocabulary removed or made load-bearing: `DISCOVER_BUDGETS` was exported, re-exported and consumed by nothing while `Home.tsx` re-typed the same four bands (a `readonly DiscoverBudget[]` accepts any subset, so a fifth band would have typechecked green and rendered no chip); `PIN_KINDS`' value export had no consumer at all; `useComfortPin` no longer publishes `pinIdOf`/`toggle`, which nothing outside the module calls. Three comments corrected against the code they describe: a pointer to a `resolve.py` that was never written, the file header's claim that Home defines no card component of its own, and the `seen` window still quoting the 8 that was measured and abandoned for 32. The rest of the review's survivors are logged on ROADMAP.md under "Deferred — E24 epic-end deep-review (2026-09-08)" — the ones whose dissent was substantive, and the ones whose fix is wider than the finding. |
||
|
|
fdc1eabbe0 |
fix(e24): the review's seven frontend defects on the Home surface
Shift+W was dead whenever anything was open. The global handler called `setPage` directly, so the page change unmounted the player (it lives inside the module column) and that unmount's reconcile reclaimed the entry the navigation had just pushed — the player closed and the page snapped back. `closeThenNavigate` cannot serve here: a global key does not know which overlay is open, or whether one is. `navigateOverOverlays` is the same fix one step shorter — bury the live overlay entries first, so the unmount consumes the marks instead of issuing `history.back()`. `q.isError` had no `&& !data` guard, alone among 33 call sites and against the convention `ui/QueryState` documents: react-query flips status to error on a failed REFETCH while keeping the payload, and every mutation here invalidates the discover prefix, so one blip while marking a card watched blanked a fully populated landing page. The reroll's key rooted at "discover-pick", outside the prefix every mutation invalidates, so a rerolled hero outlived the page: play it, finish it, and it was still the answer with a zeroed progress bar. It now sits under the same root. A FAILED reroll also rewound the hero to the card the page loaded with, several presses back, because a new key per press means `keepPreviousData` (pending-only) cannot cover an error; the last resolved pick is held instead. The bookmark on a rail tile never updated — `VideoCard` reads `video.saved` from this payload and keeps no optimistic state — so the star stayed empty and the next click re-sent the same call. The shelf reorder wrote a captured snapshot back on failure rather than re-asking the server, which its own comment promised: two overlapping moves each reverted to their own snapshot and settled on an order neither side held, and since `before` is the whole payload a rollback also discarded any rail refresh that had landed meanwhile. It now invalidates the discover PREFIX on settle, which also fixes the sibling budgets keeping the pre-move order. The hero/rail de-duplication matched one key, so for a Plex series — the rail row is the show, the hero is the episode it resolves to — the same series printed twice; it now matches both keys, as `hero.pick`'s own exclusion already did. And when the hero empties the resume rail, the rail is hidden rather than announcing "nothing half-finished right now" under a hero captioned "where you left off". Ten regression tests, all mutation-checked. Three of them were rewritten after the first mutation round showed they could not go red: two asserted before the state under test had settled, one matched a title the Plex tile does not render. |
||
|
|
ccd5f3739a |
feat(e24): pin to the comfort shelf from every card and row
S3 wires the shelf's editor. The gesture is the E10 right-click menu, so the decision "this is comfort media" can be made where the thing is — the feed, a Plex grid, a channel row, a playlist — rather than only on the page that lists the shelf. One shared piece, `components/comfortPin.tsx`: the five pin-kind constructors, the shelf query, and ONE `ActionMenuItem` that switches Pin/Unpin honestly (and says "not known yet" with a disabled row until the shelf has answered, since a confident "Pin" over something already pinned is exactly the quiet lie the house rules forbid). Surfaces that already have a row menu compose the item in (`Playlists`, `PlaylistsRail`); the rest mount `ComfortPinMenu`. Two structural notes: - The feed's menu lives in `VirtualFeed`, not in `Feed` — that is the list, and it is also what gives the channel page the gesture for free, since it renders `<Feed channelScoped>` all the way down. - Cards take `menuProps` as a FUNCTION of the row, not the built props object. `triggerProps` is stable across renders while its return value is not, and a fresh object prop would blow past `VideoCard`'s memo for every card in the virtualized feed on every parent render. `DataTable` and `ChannelLayoutGrid` grow a generic `rowProps` so a channel row answers the same gesture in both of the manager's layouts; the table stays domain-free and never learns what a menu is. On Home the shelf tile's menu is the whole editor: four move rows (front / left / right / end, reusing the playlist menu's ordering idiom) plus the unpin, over menu rows rather than drag & drop — dragging inside a horizontal, scrolling Carousel fights the scroll, and menu rows are keyboard-reachable for free. The reorder is optimistic on the cached payload and re-asks the server on failure rather than showing an order that was never accepted. The exhausted shelf tile (a pinned channel with nothing unwatched left) is `aria-disabled`, not `disabled`: measured in Chromium, a disabled button still receives contextmenu and pointerdown, but it loses focus — and with it Shift+F10, the only keyboard route to the menu it most needs. Verified end to end in the browser against the live account: all five pin kinds, the reorder (persisted across a reload), the unpin, and the honest Unpin label on a resume tile that is already on the shelf. |