commit 7f6aaf786c96f9b0b8b01e021106824faf554644
parent eeebd3a4190826f2b2b46008cc7580fd2de57ee5
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 24 Sep 2026 19:38:10 -0400
plans: slice P record — the rack's layer ladder, the transcribe station, the audit
Both root causes, with the commits that caused them (57247cbf's slug z
bump; a handling branch that never matched how buckets are decided), the
audit table (five fixes, four recorded), the station/band divergence,
and the gates: common 1727, editor unit 69, full e2e 146/146 in 11.8 min,
builds clean, numbers diff empty back to back. Two [Unreleased] bullets.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 107 insertions(+), 0 deletions(-)
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -1,6 +1,8 @@
# Changelog
## [Unreleased]
+- **Channel rows no longer scroll over a group's controls on `/channels`.** Scrolled down and to the right, the pinned Slug column of every row painted over the pinned group header and its Sync / Download / Transcribe / Digest buttons, and took the clicks. The pinned Slug cell and the group header sat at the same stacking level, and the later rows won. The rack now has one named layer order, kept in one file: the Advanced panel, then the column header, then the group header, then the pinned checkbox and Slug cells. Nothing ties any more. The screenshot audit found four more problems, fixed as well. A group header's name and buttons now stay on screen however far the columns scroll across (they used to scroll off to the left). An Advanced panel opened near the bottom or the right edge scrolls itself into view instead of being cut off. The rule above a pinned group header moves with it instead of leaving a gap the rows showed through. On a phone, the column header no longer paints over the selection bar pinned to the bottom of the screen.
+- **A group's Transcribe works for YouTube channels, and it counts what it queues.** The station used to be disabled for every `youtube`-handling channel with the message "a youtube-handling channel never runs whisper". That was wrong. A YouTube video that came down with no captions is transcription work like any other, and the automatic runner already treats it that way. Transcribe now counts two kinds of video, after the usual members-only, deleted and private exclusions: downloaded videos with no transcript at all, and downloaded videos whose only transcript is YouTube's auto-captions. Pressing it queues those two kinds as the channel page does, as up to two jobs per channel on the transcription queue. Pressing it again while either job runs says *already running*. The wording names no method ("…has downloaded audio to transcribe", "…each takes minutes"). **This figure can now be higher than the Transcription band in the same rack on channels with many auto-caption-only videos.** The band counts videos with no transcript at all, while the station counts everything its button would queue. That is intended.
- **Every channel table and every job-in-flight line is now drawn one way.** The /channels rack, the dashboard's Channels table and the work tables on the operation pages and /cleanup are one table with a column set per page, over one channel row built on the server (which no longer ships a channel's config to the browser); the dashboard's "Needs work" seed is computed by the same code the widget endpoint serves. On the jobs side, /jobs rows, the "Active jobs" cards on channel/video/operation pages, the monitor widget's Active jobs strip and the operations board's "In flight" list are one job row in three sizes, with one rule for which buttons (Retry / Reorder / Drain / Cancel / Force-release) a job gets. **What you might notice:** a work table's report column reads "stale"/"missing" like the rack's instead of a date; the dashboard's Sync button is the rack's; a lane line on /jobs offers Force-release while its runner is running; widget job lines show who asked for the job; an in-flight download on the operations board links to its job page. Nothing a count says moved.
- **`site.json`, each channel's `config.json` and the per-video sidecars now have one schema each, and the two config files have generated key tables.** **`SITE.md`** and **`CHANNEL.md`** (new, repo root) list every key with its default and meaning, generated by `common/bin/file-schemas-docs.ts` and checked by a test. Nothing an operator has configured reads or saves differently: every live `site.json` and `config.json`, and a 1,763-file sample of sidecars, read and write back byte-for-byte as before. **Fixed:** a social-channel fetch no longer undoes Configure-form edits made while it was running (it used to write back the whole config it read when it started). Every change to a channel's config now re-reads the file at the moment it saves and changes only its own fields, so a sync stamping its time and a form save made at the same moment both land. Two writes to the same file from the editor no longer share one temporary file.
- **`settings.json` has one schema and one writer, and its key table is generated.** Every key, its default, its clamp and its documentation is now one zod schema (`common/lib/settingsSchema.ts`); `getSettings`/`writeSettings` both parse through it, and every settings form saves through one helper (`editor/app/settings/saveSettings.ts`) that merges only what the form changed. **`SETTINGS.md`** (new, repo root) lists every key with its default and what it does, and `settings.json.example` is now the full default object — both generated by `common/bin/settings-example.ts` and checked by a test, so neither can drift. Nothing an operator has configured reads differently. **Fixed:** adding or editing a storage location on `/storage` no longer erases the record of which location the saved-video store is on (`storage.savedVideosLocationId`).
diff --git a/plans/one-core-phase-3.md b/plans/one-core-phase-3.md
@@ -703,6 +703,111 @@ undated candidates`, twice — once per Next module graph). IO pressure at boot
`full avg10 7.26` (58–66 % at the last rollout, when a remux was saturating the platter), which
is the difference from the 25 minutes release 2 paid.
+### Slice P, as shipped — /channels rack polish (2026-09-24)
+
+Branch `one-core/phase-3-p` off `4130aca1`, nine commits (the eight below and this record),
+not merged — the parent merges; slice W merges after it (W folded nothing under
+`editor/app/channels/components`). The operator's ask: fix every table and z-index problem on
+/channels ("channel rows scroll OVER the group-based controls"). Also, the group Transcribe
+station must stop refusing youtube-handling channels with a whisper-specific sentence.
+
+| sha | what |
+|---|---|
+| `5ac3e8ca` | `rackLayout.ts` — one named layer ladder (popover z-40 > thead z-30 > group header md:z-20 > identity z-10, plus the deck), `RACK_IDENTITY`, `RACK_BRIDGE`; every class site reads it; unit test for the width invariant and the order; `data-testid="channels-rack"`; flat-path overflow comment; `channels-rack-layers.spec.ts` |
+| `7a0b3d75` | the transcribe station counts what its button queues: handling branch deleted, `transcribeStationIds` (one fold, both buckets, exclusions), the group action runs `transcribeAutoSubsBucketAction` + `transcribeMissingAction`, `KIND_FOR` lists both kinds, method-free strings; unit + e2e |
+| `564f767d` | `channels-rack-audit.spec.ts` — 12 screenshots behind `RACK_SHOTS` |
+| `99b06b40` | audit fix A: a group header's name and stations pin left (`sticky left-2`, capped at the measured `--rack-w`) |
+| `53bbd482` | audit fix B: an opened Advanced panel scrolls itself into view (`nearest`) |
+| `476c8470` | audit fix D: the section rule is the th's inset shadow, not the `<tr>`'s collapsed border |
+| `6ff63cbf` | audit fix C: the scroll region is `isolate` (its own stacking context); both rack specs wait for hydration |
+
+**Root cause 1 — rows over the group controls.** It was a z-index TIE, broken by DOM order.
+The group header (`ChannelGroupHeaderRow`, `md:sticky … md:z-20`) holds the five station
+buttons. The pinned Slug cell had been raised to `z-20` by **`20ee34db`** (2026-09-13) to
+close a sub-pixel seam against the checkbox cell. Scrolled down and right, every later row's
+slug band painted over the header, because it comes later in the DOM. The z bump was never
+needed. `left-8` (32 px) pins the Slug cell 4 px inside the `w-9` (36 px) checkbox cell. Two
+sibling cells at one z-index paint in DOM order, so the Slug cell already covers the overlap at
+z-10. The rack plan's order (`plans/editor-channels-rack.md:198-204`) was right. `278d4463`
+shipped it as 30/20/10, and `20ee34db` put the slug cell on the header's level. A second tie
+went unstated: the popover and the thead were both z-30, and the popover won on DOM order only.
+Both ties are gone. After the fix, grep finds no `z-<n>` literal in
+`editor/app/channels/components/` outside `rackLayout.ts`.
+
+**Root cause 2 — "a youtube-handling channel never runs whisper".** `stationWorkFor`
+refused `handling !== "transcribe"`. But buckets are decided by files, never by handling:
+`downloadedNoTranscript` is whisper work for every channel, and the runner drains it for
+every channel. The channel page already replaces auto-captions for any handling. So the
+station never counted what the runner would do. It now counts `|downloadedNoTranscript| +
+|downloadedAutoSubsOnly|` after the download exclusions. The two buckets are disjoint, and one
+batch cannot cover both, so the button queues two jobs:
+- `whisper-bucket-auto-subs` over the auto-caption ids;
+- `whisper-all` when there are captionless videos, or when the channel has no report.
+
+Both jobs run on `TRANSCRIPTION_QUEUE`. The dedupe spans both kinds. `queued` counts channels,
+and `jobIds` carries the whisper-all id when both jobs were queued. A combined job kind was
+rejected: it would need a replay spec and a /jobs label, and it would mirror nothing, since the
+channel page runs two jobs. A social account is still ineligible (`"social account"`), and
+that is now the only way `notEligible` can be reached.
+
+**The divergence, on purpose.** The rack's transcription band still counts
+`downloadedNoTranscript` alone (`channelSnapshot.ts:583-585`), and the stage title lists
+auto-captions as informational (`stageStatus.ts:344-352`). On a youtube channel with many
+auto-caption-only videos, the station's figure is now higher than the band's. That is the ask:
+the station counts exactly what its button queues. It is not a bug.
+
+**The audit.** Viewports 1440×900 and 390×844. Shots: grouped top; grouped and flat scrolled
+bottom-right; deck open scrolled to the bottom; deck parked over the column header (390×640 /
+1440×640); Advanced popover open. Baseline shots are in `$T/p-shots-before/` (pre-fix, plus
+the testid only). Fix C's before-shot is in `$T/p-shots-c-before/`. The final set is in
+`$T/p-shots-after/` (12 PNGs). `$T` = `/home/user/.claude/jobs/c0baff27/tmp`.
+
+| shot | finding | fix |
+|---|---|---|
+| desktop-grouped-scrolled, layers spec | the pinned slug band paints over the pinned group header and its stations (root cause 1) | `5ac3e8ca` |
+| desktop/mobile-grouped-scrolled | the group header's name and five stations scroll off to the left with the table; only "16 channels" stays in view | `99b06b40` |
+| desktop/mobile-popover-open, layers spec at 1280×720 | the Advanced panel on a row near the bottom (below md: near the right edge too) is clipped by the scroll region, with its selects out of reach | `53bbd482` |
+| desktop-grouped-scrolled (after `5ac3e8ca`) | a gap under the pinned group header: the `<tr>`'s collapsed `border-t-2` belongs to the table grid, so it stays behind when the th pins | `476c8470` |
+| mobile-deck-over-thead | the z-30 thead paints over the screen-pinned z-20 deck ("18 selected", tier select), because the region formed no stacking context | `6ff63cbf` |
+| mobile-* | below md the thead never pins. The region scrolls on both axes, so `sticky top-0` pins to the region and not to the document | recorded, not fixed — the documented trade-off (`ChannelsRack.tsx:152-155`, `channels/page.tsx:332-336`) |
+| mobile-deck-open | the deck at the end of the scroll sits in flow after the last row and covers nothing; while scrolling, it covers what passes under it, as a pinned bar does | recorded, no defect — no padding needed |
+| every shot | the Next dev-tools badge at the bottom left | recorded, dev-only |
+| desktop/mobile-*-scrolled | the Build/Tier columns show as a sliver under the pinned slug band | recorded, no defect — columns scroll under a pinned identity column |
+
+**The layers spec fails on the pre-fix code.** The run on `4130aca1` components plus the
+testid only: the header hit-test failed at step (1), with the point inside the slug band
+landing on the slug cell. The popover test failed because the last row's panel was clipped,
+which became finding B. After the fixes: 4/4.
+
+**Gates.**
+- tsc: clean after every commit.
+- common: **1727** (1723 − 1 rewritten + 5 new in `channelGroupSections.test.ts`).
+- editor unit (`tsx --test "app/**/*.test.ts"`): **69** (67 + 2 `rackLayout.test.ts`).
+- `test:scripts`: 156 + 1 skip.
+
+**e2e.** Every run was detached, from the worktree root:
+- baseline (audit + layers, pre-fix): 10 passed, 2 failed (the expected pair above), 1.0 min.
+- mid 1 (audit + layers + channel-groups): 20 passed, 1 failed (finding B), 1.3 min.
+- mid 2 (audit + layers): 13 passed, 2 failed. Both were clicks and measures before hydration. Fixed in the specs (`6ff63cbf`).
+- fix-C before-shot (isolate removed): 2/2, 0.3 min.
+- after (audit + layers): **15 passed, 0 failed**, 0.9 min.
+- full list (`$T/p-specs.txt`, 24 files, every named file present, none dropped): **146 passed, 0 failed, 0 flaky, exit 0, 11.8 min**.
+
+**Numbers.** `phase3-view-numbers.ts`, primary's `transcripts/` and `settings.json`,
+read-only. The first before/after pair, hours apart, differed: nuxanor-kick went from 4 to 3
+untranscribed, hasanabi dropped out, digest eligibility went from 76,519 to 76,521. That is
+the live editor transcribing, not code. None of P's files is in the tool's import graph. Run
+back to back, main (primary checkout `77f63356` = `4130aca1` + one plan file) and the branch
+gave **diff empty, 5,119 bytes each**.
+
+**Builds** (at `6ff63cbf`): editor `next build` exit 0, 82 s, route table lists `ƒ /api/view/[name]`. Export `next build` exit 0, 54 s.
+
+**Left.**
+- The mobile thead does not pin (a documented trade-off).
+- `20ee34db`'s `min-w-52` on the tier cell is unchanged.
+- An opened Advanced panel lengthens the region's scroll extent while it is open, because it
+ is absolute inside the scroll box. Closing it restores the extent.
+
## Next release — slice 3b and Phase 4 (inventory kept from 2026-09-23)
Slices 3a and 4b shipped in the release above (2026-09-24); the slice 3b bullets and the