The portal host followed the fullscreen element into it. When that element is the dialog's own
content (the library player's <video>, by its f key or its own button), the move is into the host's
own descendant, which the DOM refuses: HierarchyRequestError in a layout effect, and the crash
screen replaced the app. The host now stays where it is then — the content already shows.
The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
Deep-review round 5, over round 4's own fixes.
- Round 4 gave `ui/Modal` a persistent portal host so a fullscreen transition would stop tearing
the dialog down. The host was attached in a `useLayoutEffect` — one commit phase AFTER React
runs `autoFocus` in `commitMount`, and `focus()` on a disconnected node is a no-op. Every
autoFocus in every dialog was dead: Mod+K opened the command palette with the focus on the ✕,
so typing searched nothing and Enter closed it; the same for the download rename field and
every non-danger confirm. The host is attached in a `useInsertionEffect` now, a whole phase
earlier, and the whole re-parent — including the focus save/restore across the move, which
`appendChild` needs because it is remove-then-insert — lives in one `useFullscreenPortalHost`.
`ui/AnchoredPopover` had a second copy of that re-parent WITHOUT the focus restore, and a
comment claiming focus survived it; both now come from the hook.
- `ui/Toaster` classified a toast raised in the SAME commit as a host takeover among the ones it
inherits, because it read the store live at the takeover render. A dialog that notifies and
closes in one handler (UploadDialog) therefore put its toast up with no role, into a region
still `aria-live="off"` — announced by neither path. "Inherited" is now what the outgoing host
last had on screen, and a toast that is NOT inherited is announced even inside the unarmed
window.
- The onboarding wizard renders its own shell, and Escape ordering was all of the dialog contract
it had: no role, no label, no initial focus, no trap, no restore. A keyboard user tabbed out of
the first-login wizard into the app behind the scrim. The focus half of the contract is now one
shared `ui/dialogFocus` that both `ui/Modal` and the wizard use.
Every fix has a regression test, each mutation-checked (6 mutations, all red on the assertion
they are named after).
The platform's FloatingPortal targeted <body>, which is painted UNDERNEATH whatever
holds the fullscreen lock. That is why the player's in-bar menus were either
non-portaled (and clipped) or, for add-to-playlist / download, disabled outright in
fullscreen. PopoverSurface now portals into `document.fullscreenElement` while one
exists, via a new reactive `useFullscreenElement()` on the existing WebKit-aware
fullscreen lib.
Two floating-ui details this had to work around: a null `root` means "the container
does not exist yet" (no portal node is created at all), so the no-fullscreen case must
pass undefined; and the portal node is built once and ignores a later root change, so
the portal is keyed on the fullscreen state to remount on a transition.
Migrated the three hand-rolled hover engines onto the platform:
- Tooltip: kept as a passive caption via a new `passthrough` prop on PopoverSurface,
so it cannot swallow a click aimed at a control it overlaps during the close delay.
Gains aria-describedby wiring it never had.
- AboutCell: the 160 ms hide-timer hover bridge is now safePolygon; the close-on-page-
scroll is now hide-and-follow, so the card reappears when the row scrolls back.
- TagManager: same bridge removal; the scroll-fade moves to an inner scroller so the
mask rides the scrolling box rather than the glass panel.
Verified in-browser: tooltip flip + centering + pointer-transparency, About card flip-
above with the measured height, tag popover right-start anchoring, and the portal
round-trip body -> fullscreen element -> body on a live fullscreenchange.
The worst one was, again, the previous round's own fix.
- Round 7 stopped awaiting the HLS sweep so startup could serve immediately — which
also made it run CONCURRENTLY with playback, and `sweep_orphans` snapshotted the
live-session set BEFORE listing the root and rmtree'd outside the lock. Since
`start_session` reuses a directory path for the same (key, offset, tag), a stale
name can become a live session at any moment. Two guards now: nothing whose mtime
is newer than this process's start is ever deleted, and the live check + rmtree
happen together under the lock, one directory at a time.
- The sweep task is awaited after cancel, so shutdown can't leave it pending.
- `_rate_limiter` used a truthiness test on its timestamp, so a `now()` of exactly
0.0 read as "never ran" — unreachable in production, but `now` exists to be
injected and a test clock starting at 0 is the obvious choice.
- The RSS total-outage error now carries the first underlying error: "all N channels
failed" is also what ONE dead feed id looks like on a small instance, and a red
card the user can't act on is its own kind of dishonest.
- The WebKit fullscreen handling moved into `lib/fullscreen` and PlexPlayer uses it
too — it had the same unprefixed-only reads in three places, including an Escape
branch that would have closed the whole player instead of leaving fullscreen.
- The `--max` CSS comment claimed `inset: 0` did the sizing; Tailwind's `w-full` is
still on the element and wins. Comment now states the real mechanism.
Verified in real Chrome (the pane can't composite or do native fullscreen):
- F fills the 1920x889 viewport; the exit pill renders top-right on the letterbox
band and overlaps NO YouTube control — the collision this round suspected does not
reproduce in this embed (its own buttons sit bottom-left / bottom-right).
- Four real ArrowDown presses while maximised: persisted volume 100 -> 80, with the
level flash visible on screen.
- A real click on the exit pill un-maximises and returns focus to the card.
- lib/fullscreen driven from a real click: enter (innerHeight 889 -> 1024), the
change listener fires exactly once per transition, exit returns to 889.
Backend suite 150 -> 158; the new sweep tests are mutation-checked, and they read
their cutoff from the filesystem rather than a clock (the container VM and the
bind-mounted host disagree by enough to make a clock-based assertion flaky).