// The registry of placeable widget sections. // // One entry per strip the monitor widget can render. This is the single list // that decides what exists, what it's called, which config flag turns it on, // and which display options belong to it — replacing the nine hardcoded // conditionals MonitorWidget used to carry and driving both configuration // surfaces (the /widget/builder floorplan board and the in-widget gear). // // Deliberately React-free so the pure layout math in ./placement.ts (and its // tests) can import it without pulling in a renderer. The card miniatures live // with the board that draws them, in ../builder/components/SectionMini.tsx. import type { WidgetConfig } from "./config"; export type SectionId = | "controls" | "lastSync" | "scheduler" | "disk" | "cleanable" | "cleanChannels" | "workers" | "jobs" | "actionable" // APPENDED, never inserted: the order of this union feeds DEFAULT_ORDER, and // DEFAULT_ORDER is what a link with no `l=` renders. Inserting here would // silently re-arrange every already-copied widget link. | "backfill"; // The visibility/display flags a section reads, i.e. the whole config minus the // placement it is being placed into. Taking this rather than WidgetConfig lets // parseWidgetConfig ask "which sections are on?" while it is still building the // config that will hold the answer. export type SectionFlags = Omit; // Only the boolean flags are togglable as a section option, so a typo like // `key: "pollSeconds"` is a compile error rather than a checkbox that writes a // number field. // `-?` strips optionality first, so the optional `channel` narrows to `never` // rather than leaking `undefined` into the union. export type BooleanConfigKey = { [K in keyof WidgetConfig]-?: WidgetConfig[K] extends boolean ? K : never; }[keyof WidgetConfig]; export type SectionOption = { key: BooleanConfigKey; label: string; // Optional second line, for an option whose effect reaches beyond its section. hint?: string; }; export type SectionDef = { id: SectionId; // Short, stable code used in the `l=` layout param. Changing one breaks // already-copied links, so treat these as part of the URL contract. code: string; label: string; // One line of card copy: what the strip is for. hint: string; enabled(config: SectionFlags): boolean; setEnabled(on: boolean): Partial; options: SectionOption[]; }; // Declared in the widget's original render order, so DEFAULT_ORDER below is the // pre-layout behaviour and a link with no `l=` renders exactly as it always did. export const SECTIONS: SectionDef[] = [ { id: "controls", code: "ctl", label: "Controls & sync", hint: "Pause, drain, retry and the Sync button.", // WidgetControls renders when either capability is on, so the card is on // when either is — and turning the card off has to clear both. enabled: (c) => c.controls || c.sync, setEnabled: (on) => (on ? { controls: true } : { controls: false, sync: false }), options: [ { key: "controls", label: "Interactive buttons", hint: "Also adds per-channel actions to the Needs work and Needs cleaning lists.", }, { key: "sync", label: "Sync button" }, { key: "syncConfirm", label: "Confirm before Sync all" }, ], }, { id: "lastSync", code: "lsync", label: "Last sync", hint: "How long ago the corpus was last swept.", enabled: (c) => c.lastSync, setEnabled: (on) => ({ lastSync: on }), options: [], }, { id: "scheduler", code: "sched", label: "Scheduler", hint: "Auto-sync on/off, next due, last run.", enabled: (c) => c.scheduler, setEnabled: (on) => ({ scheduler: on }), options: [], }, { id: "disk", code: "disk", label: "Disk space", hint: "Free space against the floor that blocks downloads.", enabled: (c) => c.disk, setEnabled: (on) => ({ disk: on }), options: [], }, { id: "cleanable", code: "cln", label: "Cleanable total", hint: "One number: audio you could reclaim right now.", enabled: (c) => c.cleanable, setEnabled: (on) => ({ cleanable: on }), options: [], }, { id: "cleanChannels", code: "clnl", label: "Needs cleaning", hint: "The channels holding that reclaimable audio.", enabled: (c) => c.cleanChannels, setEnabled: (on) => ({ cleanChannels: on }), options: [], }, { id: "workers", code: "wk", label: "Workers", hint: "A dot per worker: busy, idle, draining, degraded.", enabled: (c) => c.workers, setEnabled: (on) => ({ workers: on }), options: [{ key: "workerLabels", label: "Show worker names" }], }, { id: "jobs", code: "jobs", label: "Active jobs", hint: "Running and queued work, with progress bars.", enabled: (c) => c.jobs, setEnabled: (on) => ({ jobs: on }), options: [ { key: "compact", label: "Compact (hide per-task detail)" }, { key: "jobBar", label: "Show batch progress bar" }, { key: "headingProgress", label: "Show progress in job heading" }, { key: "eta", label: "Show time estimates (ETA)" }, ], }, { id: "actionable", code: "act", label: "Needs work", hint: "Channels with videos left to download or transcribe.", enabled: (c) => c.actionable, setEnabled: (on) => ({ actionable: on }), options: [], }, // APPENDED at the end — see the SectionId union. The `code` is part of the URL // contract too, so it must never be reused or renamed. { id: "backfill", code: "bf", label: "Backfill", hint: "Derived data the existing corpus is missing, and how much of it needs its media back.", enabled: (c) => c.backfill, setEnabled: (on) => ({ backfill: on }), options: [], }, ]; // The order a widget renders in when no layout is set. Unchanged from the // original hardcoded JSX, which is what keeps every already-copied link stable. export const DEFAULT_ORDER: SectionId[] = SECTIONS.map((s) => s.id); export const SECTION_BY_ID: Record = Object.fromEntries( SECTIONS.map((s) => [s.id, s]), ) as Record; const BY_CODE: Record = Object.fromEntries( SECTIONS.map((s) => [s.code, s]), ); // Unknown codes resolve to undefined so a hand-edited or future-dated link is // normalized rather than throwing. export function sectionByCode(code: string): SectionDef | undefined { return BY_CODE[code]; } export function enabledSections(config: SectionFlags): SectionId[] { return DEFAULT_ORDER.filter((id) => SECTION_BY_ID[id].enabled(config)); }