// The monitor widget's optional "Needs work" strip, as a pure fold. // // The same actionable census the operation pages and the dashboard draw, // reduced to the two buckets a widget acts on — videos to download and videos // to transcribe — for each channel that has any, sorted by the larger backlog // first. The counting itself stays in the editor // (`lib/actionable/loadActionable.ts`, whose four count helpers know the // availability exclusions); what moved here is the shape and the two decisions // about it: which channels appear, and in what order. // // THE PATH KEEPS ITS NAME. /actionable the page is gone; /api/widget/actionable // is a wire contract a pinned widget in someone's browser is polling right now, // and renaming it would break that for no gain — which is why slice 2 serves it // as a REWRITE onto /api/view/widgetActionable rather than a redirect. // // ZERO IMPORTS, deliberately. export type WidgetActionableChannel = { slug: string; undownloaded: number; untranscribed: number; // What the digest lane could act on today — the digest band's `reachable`, // per channel. Reported but STILL NOT used to decide whether a channel // "needs work", and the reasoning is unchanged: during the backfill this is // ~99.87% of the corpus, so // counting it would put every channel in the list forever and drown the two // buckets a human can actually act on today. The registry's classification // removes untranscribed and cues-stale videos from the number, which makes it // smaller and more honest — nowhere near small enough to filter on. digestReachable: number; }; export type WidgetActionablePayload = { channels: WidgetActionableChannel[]; }; export function buildWidgetActionablePayload( rows: readonly WidgetActionableChannel[], ): WidgetActionablePayload { const channels = rows .filter((c) => c.undownloaded > 0 || c.untranscribed > 0) .sort( (a, b) => b.undownloaded + b.untranscribed - (a.undownloaded + a.untranscribed), ); return { channels }; }