# Editor IA slice 8a+8b — sync is an operation; its settings and the workers' live on their pages ## Context **Verified read-only against `6f6c307` (clean) on 2026-08-29.** This is the Sync and Workers thirds of the IA doc's slice 8 (`plans/editor-operations-ia.md:168-171`); the `/jobs` + `/jobs/active` + `/jobs/queue` fold is a later plan of its own. The noun-model row for Sync (`:57`): an operation, channel-scoped and cadence-triggered; `OperationDescriptor` gains `scope` and `trigger`; it gets a board row and `/operations/sync` (today's `/scheduler`) and "keeps its own runner"; the keep-latest checks and the saved-video backup on the same heartbeat (`editor/app/scheduler/runTick.ts:25-27`) are Storage chores and labelled so. The nav end state (`:66-80`) has the Operations group down to one entry. **Decided (operator, 2026-08-29):** (A) `/scheduler` → `/operations/sync`; `scope`/`trigger` on the descriptor; a `sync` catalog entry; the board's SyncRow becomes a rail row; a 307 redirect; the nav's interim "Schedule" entry goes; the Storage chores are labelled. (B) **The whole `syncScheduler` block — headline and advanced fields — is the sync operation's settings form on its page** (slice 3's rule), declared as `settingsBlock: "syncScheduler"`, with `saveSchedulerSettingsAction` its one writer and `/settings` preserving the block the way it preserves `digest`/`diarization`. (C) `WorkersField.tsx` moves to `/workers` with its own action writing only `settings.workers`. (D) The lane NOTE strings become one. OUT: the `/jobs` fold; any change to tick logic, `SchedulerRun`, `syncSchedulerState` or a settings key; `Field.tsx`/`CardField`; moving `editor/app/scheduler/{runTick,heartbeat,status, auth,actions,intervalPresets}.ts`. **What the census found — every place the IA doc's wording cannot be implemented literally, and what to do instead:** 1. **"Keeps its own runner" cannot be `runner`.** `OperationDescriptor.runner` is typed `AutoQueueKind` (`common/lib/operations.ts:1382`; `autoQueueState.ts` = `"transcription" | "download"`), `operations.test.ts:1245-1262` pins it `undefined` for every other id, and `operations/[id]/page.tsx:138` feeds it straight into `RunnerOperationView`. The heartbeat is not an auto-queue runner. **Instead:** `trigger: "cadence"` is the switch — the page builds the scheduler console as a server slot when `op.trigger === "cadence"` (the `operationSettings`/`laneSettings`/`channelWork` slot pattern, `page.tsx:152-214`) and `OperationDetail` renders it where it would otherwise draw `NoConsoleView` (`OperationDetail.tsx:113-115`). `runner` stays `undefined` on sync; the test says why. 2. **Sync has no band, so "a real rail row" is composed, not folded.** `OperationRail` draws one `RailRow` per `OperationBand` (`OperationRail.tsx:74-81`) and a band's populations are videos (`band.ts`); the sync row's figures are channels (`syncRow.ts:8-22`). `buildOperationBands` (`buildBands.ts:159-193`) must not gain a sync band — "coverage unknown" over a hollow outline for a thing with no per-video coverage. **Instead:** `OperationRail` takes `sync?: SyncRowView` and renders a `SyncRailRow` `
Two Storage chores ride this heartbeat and are not sync: the keep-latest deletion check (its interval is in the settings below) and the saved-video backup (Saved videos).
`. Header comment notes the two polls on this page (5 s status here, 3 s auto-queue from the rail) and why they are not merged. **`git mv`** likewise `SchedulerSettingsForm.tsx`, `ChannelCadenceEditor.tsx`, `BulkCadenceBar.tsx`, `cadence.ts` into the same directory (imports become `../../../scheduler/actions` etc.). **`git rm editor/app/scheduler/page.tsx`.** `runTick.ts:1` gains a header line: this directory is the sync operation's runner — the heartbeat, the tick, the status payload; its page is `/operations/sync` (rendered by `operations/[id]`), and `/scheduler` redirects there. **Storage chores, labelled** (no logic change): `runTick.ts:22-27` imports get "STORAGE CHORES riding this heartbeat because it is the one timer the editor has. Neither is sync (IA doc, the Sync row): the keep-latest check consumes outputs, the backup copies them"; the comments at `:154-158` / `:181-183` open "Storage chore:"; `:172` reason → `` `storage chore (keep-latest check): ${result.error}` ``; `:196` slug → `"storage chore (saved-video backup)"`; `SchedulerTickResult` field comments (`:37-40`) say "Storage chore". `SavedVideosControls.tsx:86-87` → "the sync heartbeat runs this Storage chore on the cadence below". **Nav and redirect.** `lib/nav.ts:85-86` deleted, `CalendarClock` dropped from the import, the Operations entry's `keywords` (`:83`) gains `" sync schedule cadence cron"` (the palette, `CommandPalette.tsx:254`, still finds it). `next.config.ts:66-67` gains `{ source: "/scheduler", destination: "/operations/sync", permanent: false }` and the comment (`:48-62`) one sentence: "/scheduler was the sync operation's page before sync was catalogued; `/api/scheduler/*` is NOT redirected — the cron client and the console's own poll never moved." `HowPriorityWorks.tsx:41` → `href="/operations/sync"`. `scheduler/actions.ts:102,152` → `revalidatePath("/operations/sync")`. `api/scheduler/status/route.ts` comment → "the /operations/sync console". **Specs (commit 2):** `scheduler.spec.ts` — header `:6-12` and the `:86` comment → `/operations/sync`; all seven `goto`; test 1 (`:26-93`) adds `getByRole("heading", { name: "Sync", level: 1 })` visible and `page.locator('li[data-operation="sync"]')` contains "channels on a cadence". `navigation.spec.ts:108-114`: after the `/actionable` block, `goto("/scheduler")` → `toHaveURL(/\/operations\/sync$/)` → the "Sync" h1; comment gains "/scheduler was the sync operation's page before sync was catalogued". `auto-queue.spec.ts:1088-1093`: comment gains "and sync, the one channel-scoped row"; add `rail.filter({ hasText: "Sync" }).first()` visible and `rail.getByRole("link", { name: "Sync", exact: true })` has `href="/operations/sync"`. `:1108` and `:1166` pass unchanged. --- ## Commit 3 — `sync: its settings live on its page, and one action writes them` **`common/lib/operations.ts`:** `OperationSettingsBlock` (`:327`) gains `"syncScheduler"`; `SYNC_OPERATION.settingsBlock = "syncScheduler"`. **`operations.test.ts:1266-1280`** expected map gains `sync: "syncScheduler"`. **`operations/[id]/page.tsx` `settingsFormFor`** (`:71-110`) gains the arm — the exhaustive switch is exactly why the union grew: ```tsx case "syncScheduler": return (