commit 39fae2a994dfd234210f136af4964ebb75c914c8
parent 3784ea0529d7ff58772e35be746812e2a7bbc691
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 11 Sep 2026 11:55:27 -0400
editor: the focus banner, on the lane console it is a statement about
S4's banner half of plans/channel-priority.md. `FocusBanner.tsx` — a stub since
S0 — says the one thing a focus creates and nothing else on the page can: "why
is only jeralyzer moving?" A console can show a runner running, a ladder full of
rungs and thousands pending and still not say that three quarters of those rungs
are held behind a group at the top.
`Focus: <name> (N channels) · <focus> pending in this lane · <rest> waiting
behind it · M channels held`, from `focusSummary` over the lane's own compiled
`pendingByLeaf`. The numbers are PER LANE by construction, so the same focus
reads differently on the four consoles — which is the point: a focus can be
holding one lane and exhausted on another, and the exhausted case says so
instead of showing a held count of zero.
It renders nothing when no focus resolves, including a focus that resolved to no
channels (an unknown siteId compiles no focus group and holds no one), so a
corpus with no priorities set sees no new pixel.
WHERE: OperationDetail, between HowPriorityWorks and RunnerOperationView — above
the page's one `<section data-lane>`, never inside it. That section's contract
forbids a nested <section> and reserves role="status"; the banner is neither,
and being outside keeps it clear of every `section[data-lane]`-scoped lookup in
the suite.
NO END-FOCUS BUTTON HERE, deliberately. `settings.channelPriority` has exactly
one writer (`saveChannelPriorityAction`, S3) and it is not on this branch; a
second writer for one button is the thing the model was built to avoid. The
banner links to /channels instead, and carries an `endFocus` slot for a page
that already holds that writer to drop its own control into. S5 wires S3's
control through that slot rather than teaching this component to post.
`HowPriorityWorks` gains the paragraph that says where the rules come from, and
it answers in both directions: generated from the channel priorities, or
hand-authored here because none are set.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
3 files changed, 134 insertions(+), 21 deletions(-)
diff --git a/editor/app/channels/components/FocusBanner.tsx b/editor/app/channels/components/FocusBanner.tsx
@@ -1,25 +1,31 @@
-// THE FOCUS BANNER — a STUB, filled in by slice S4.
+// THE FOCUS BANNER — what a focus is doing, said once, wherever dispatch is
+// being watched.
//
-// It exists at S0 for the same reason ChannelTierSelect.tsx does: S3 and S4
-// then each edit one file the other does not. It already honours the one rule
-// S4 is written around — RENDER NOTHING WHEN NOTHING IS FOCUSED — so it is
-// green on /channels before S3 exists and green on the lane consoles before
-// anything writes a focus.
+// 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.
//
-// What S4 makes of it: `Focus: <name> (N channels) · <units> pending in this
-// lane · M channels held`, plus an "End focus" button posting `endFocusAction()`.
-// The numbers come from `computeLeafPending`, which the status panel already
-// computes, so the banner is one sum over `counts` and no new read.
+// NOT A `role="status"`, and not a <section>. It sits immediately above
+// RunnerOperationView's `<section data-lane>`, whose structural contract
+// reserves `role="status"` for "Saved." and forbids a nested <section> — and
+// this is a persistent statement of state, not a live region announcing a
+// change. `data-focus-banner` is how a test finds it.
//
-// WHERE IT GOES on the lane consoles: OperationDetail.tsx, between
-// HowPriorityWorks and RunnerOperationView — i.e. OUTSIDE the `<section
-// data-lane>` that RunnerOperationView opens, whose contract comment reserves
-// `role="status"` and forbids a nested `<section>`.
+// 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.
//
-// It is NOT a new AutoRunnerIdleReason. A lane whose focus group holds the rest
-// is not idle, it is dispatching focus work; "M channels held" is a display
-// fact.
+// 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";
@@ -32,12 +38,65 @@ export type FocusBannerProps = {
// 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. S4 replaces the rest of this body.
+ // 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;
- return null;
+
+ const label = name ?? summary.siteId ?? "selected channels";
+
+ return (
+ <div
+ data-focus-banner={lane ?? "all"}
+ data-focus-holding={summary.holding ? "true" : "false"}
+ className="flex flex-wrap items-center gap-x-3 gap-y-2 rounded-md border border-brand/30 bg-brand-soft px-3 py-2 text-sm"
+ >
+ <span className="font-medium text-foreground">
+ Focus: {label} ({channelCount(summary.channelCount)})
+ </span>
+
+ {lane && (
+ <span className="tabular-nums text-muted-foreground">
+ · {summary.focusPending.toLocaleString()} pending in this lane ·{" "}
+ {summary.otherPending.toLocaleString()} waiting behind it
+ </span>
+ )}
+
+ <span className="text-muted-foreground">
+ {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</>
+ )}
+ </span>
+
+ <span className="ml-auto flex items-center gap-3">
+ {endFocus}
+ <Link
+ href="/channels"
+ className="underline underline-offset-2 hover:text-brand"
+ >
+ Channel priorities
+ </Link>
+ </span>
+ </div>
+ );
}
diff --git a/editor/app/operations/components/HowPriorityWorks.tsx b/editor/app/operations/components/HowPriorityWorks.tsx
@@ -12,8 +12,14 @@ import {
// It is reference material — true, worth having, and read once — so it was
// costing every subsequent visit the height of the answer to "what is it doing
// right now", which is the question people actually arrive with.
+//
+// `compiled` says whether the rules below are GENERATED from the channel
+// priorities rather than hand-authored here. It changes what the last paragraph
+// claims, because with a priority model set "read top to bottom" is still true
+// and "edit them here" is not: the compiler wins at dispatch, so a rule typed
+// into this tree could not change what the lane does.
-export function HowPriorityWorks() {
+export function HowPriorityWorks({ compiled = false }: { compiled?: boolean }) {
const [open, setOpen] = useState(false);
return (
<Collapsible open={open} onOpenChange={setOpen}>
@@ -35,6 +41,37 @@ export function HowPriorityWorks() {
claims. Rule order still wins — for a pure newest-first archive, use
one catch-all rule.
</p>
+ {/* THE TREE IS GENERATED, and this is where that is said in prose —
+ the read-only ladder says it as a state, this says it as a rule.
+ Drawn in both cases because "where do the rules come from" has an
+ answer either way, and the two answers are different. */}
+ <p>
+ {compiled ? (
+ <>
+ <strong className="text-foreground">
+ These rules are generated
+ </strong>{" "}
+ from the channel priorities — one tier per channel plus one
+ focus — set on{" "}
+ <Link href="/channels" className="underline">
+ the channels page
+ </Link>
+ . All four lanes are compiled from that one document, so a
+ focus group sits at the top of every one of them and the tree
+ below is read-only here.
+ </>
+ ) : (
+ <>
+ No channel priorities are set, so these rules are hand-authored
+ here. Setting a tier or a focus on{" "}
+ <Link href="/channels" className="underline">
+ the channels page
+ </Link>{" "}
+ generates all four lanes’ rules instead, and this tree
+ becomes read-only.
+ </>
+ )}
+ </p>
<p>
A video is claimed by exactly one rule (the first that matches), so
overlapping rules never double-process it. This is independent of the{" "}
diff --git a/editor/app/operations/components/OperationDetail.tsx b/editor/app/operations/components/OperationDetail.tsx
@@ -5,6 +5,7 @@ import type { AutoQueueKind } from "yt-dlp-transcript-common/jobs/autoQueueState
import type { AutoQueueStatusPayload } from "../status";
// Type-only: syncRow.ts is a server module. See OperationRail.
import type { SyncRowView } from "../syncRow";
+import FocusBanner from "../../channels/components/FocusBanner";
import { HowPriorityWorks } from "./HowPriorityWorks";
import { OperationRail } from "./OperationRail";
import { RunnerOperationView } from "./RunnerOperationView";
@@ -124,7 +125,23 @@ export function OperationDetail({
activeJobs={activeJobs}
/>
)}
- <HowPriorityWorks />
+ <HowPriorityWorks compiled={data[runnerKind].policyCompiled} />
+ {/* THE FOCUS BANNER, OUTSIDE THE LANE SECTION. RunnerOperationView
+ opens the page's one `<section data-lane>` and its contract
+ forbids a nested <section> and reserves role="status"; the banner
+ is neither, and it sits above rather than inside so a
+ `section[data-lane]`-scoped lookup in the suite never sees it.
+
+ Its numbers are THIS LANE's: `focusPending`/`otherPending` are
+ sums over the lane's own compiled `pendingByLeaf`, so the same
+ focus reads differently on the four consoles — which is the point,
+ since a focus can be holding one lane and exhausted on another.
+ It renders nothing at all when no focus resolves. */}
+ <FocusBanner
+ summary={data[runnerKind].focus}
+ name={data[runnerKind].focusName ?? undefined}
+ lane={runnerKind}
+ />
<RunnerOperationView
kind={runnerKind}
title={RUNNER_TITLE[runnerKind]}