// The monitor widget's cleanable-data indicator AND its "Needs cleaning" // channel list, as a pure fold. // // The per-channel rows come from one snapshot read (the editor's // `cleanup/lib/loadCleanup.ts`, which already honours the per-channel // `excludeFromCleanup` flag and drops empty rows), so serving both the total // and the list costs no extra I/O and no second poll. `bytes` is their sum — // and the sum lives HERE, beside the type it sums into, rather than in the // route that used to hold both. // // ZERO IMPORTS, deliberately: the input row is re-declared structurally below // instead of imported, because it is an editor type and a view may not reach // into the app. The editor asserts the two stay assignable at compile time // (`editor/app/api/view/views.test.ts`). // One channel's reclaimable-audio row: the structural twin of // `CleanableChannelRow` in the editor's cleanup loader, and the wire type the // widget's strip renders. export type CleanableChannel = { slug: string; // Videos in the transcribed-with-audio bucket (the sweep's targets). count: number; // Reclaim estimate for the primary "clean audio" sweep, net of protection. bytes: number; }; export type CleanablePayload = { bytes: number; channels: CleanableChannel[]; }; // The rows arrive already filtered and sorted by reclaim, descending — that // ordering is the loader's, and this fold preserves it rather than re-deciding // it, so the widget's list and the sidebar badge can never disagree about which // channel is the biggest. export function buildCleanablePayload( rows: readonly CleanableChannel[], ): CleanablePayload { const channels = [...rows]; const bytes = channels.reduce((sum, c) => sum + c.bytes, 0); return { bytes, channels }; }