// THE FOCUS BANNER — what a focus is doing, said once, wherever dispatch is // being watched. // // It answers the one question a focus creates and nothing else on the page can: // "why is only jeralyzer moving?" A lane console can show a runner running, a // ladder full of rungs and a pending count of thousands and still not say that // three quarters of those rungs are being held behind a group at the top. // // NOT A `role="status"`, and not a
. It sits immediately above // RunnerOperationView's `
`, whose structural contract // reserves `role="status"` for "Saved." and forbids a nested
— and // this is a persistent statement of state, not a live region announcing a // change. `data-focus-banner` is how a test finds it. // // NOT AN AutoRunnerIdleReason either. A lane whose focus group holds the rest is // not idle, it is dispatching focus work; "M channels held" is a DISPLAY fact, // computed from counts the status panel already had — one pass over // `pendingByLeaf` keyed by compiled leaf id, and no new read. // // NO WRITER OF ITS OWN. Ending a focus writes `settings.channelPriority`, and // that document has exactly one writer (`saveChannelPriorityAction`, S3). This // component therefore LINKS to /channels rather than posting an action of its // own — a second writer for one button is the thing the model was built to // avoid. `endFocus` is the slot a page that already holds that writer drops its // own control into; the link is what every other placement gets. import type { ReactNode } from "react"; import Link from "next/link"; import type { AutoQueueKind } from "yt-dlp-transcript-common/lib/autoQueueTypes"; import type { FocusSummary } from "yt-dlp-transcript-common/lib/channelPriority"; export type FocusBannerProps = { summary: FocusSummary; // Display name for the focus: a site's title for a site focus, or a short // channel list. Resolved by the caller — this file does no I/O and the model // stores a siteId, not a title. name?: string; // The lane this banner is drawn beside, when it is on a lane console. Null on // /channels, where the per-lane line is repeated for each ENABLED lane. lane?: AutoQueueKind | null; // An "End focus" control, supplied by a page that already owns the priority // writer. Omitted everywhere else, where the link below is the way out. endFocus?: ReactNode; }; function channelCount(n: number): string { return `${n} channel${n === 1 ? "" : "s"}`; } export default function FocusBanner({ summary, name, lane, endFocus, }: FocusBannerProps): React.ReactNode { // Nothing focused, nothing to say — including a focus that resolved to no // channels at all, which compiles no focus group and holds no one. if (!summary.active) return null; const label = name ?? summary.siteId ?? "selected channels"; return (
Focus: {label} ({channelCount(summary.channelCount)}) {lane && ( · {summary.focusPending.toLocaleString()} pending in this lane ·{" "} {summary.otherPending.toLocaleString()} waiting behind it )} {summary.holding ? ( <>· {channelCount(summary.heldChannels)} held ) : ( // The focus has nothing left here, so strict descent has already // fallen through to the groups below it. Worth saying: it is the // moment the operator is waiting for, and the banner is the only // thing that can see it. <>· nothing left to focus here — the rest of the lane is running )} {endFocus} Channel priorities
); }