# Selective Account Run — Design **Date:** 2026-07-08 **Status:** Approved (pending spec review) ## Problem When a migration run fails for some accounts (e.g. transient `use of closed network connection`), the user wants to re-run **only the affected accounts**, not the whole task. An earlier idea — per-error "retry" buttons inside the errors modal — was rejected: on a `closed network connection` the copy aborts the whole folder (`copy.go`, the `net.ErrClosed` branch), so the undelivered tail of the folder never appears in the error list. A per-UID retry would replay only the few visible errors and leave the invisible tail uncopied, creating a false sense of completion. The correct primitive is a **re-run of the selected accounts**. Copy is idempotent — `IsMigrated`/`MarkMigrated` dedup by message key — so a re-run skips everything already copied and delivers only the missing tail, covering the network-drop case fully. ## Solution Add checkboxes to the accounts table. The main "Run migration" button runs the selected accounts; with no selection it runs all accounts (current behavior, preserved). ## Backend ### `orchestrator.Run` Change signature to: ```go func (o *Orchestrator) Run(ctx context.Context, taskID int64, trigger string, accountIDs []int64) (int64, error) ``` - `accountIDs == nil || len == 0` → all accounts (unchanged behavior). - Otherwise, filter the `ListAccountsByTask` result down to the requested IDs **before** `gateOK`, so only the selected accounts are gate-checked and run. - IDs not belonging to the task are silently dropped by the filter (defense in depth; the handler also validates). - If the filter yields zero accounts, return `ErrNotTested` (nothing runnable) — reuses the existing 409 path; the handler maps empty selection separately (see below). Call sites updated: - `scheduler.go` → `Run(ctx, id, "scheduled", nil)` (always all accounts). - `run.go` (`handleRun`) → passes the parsed IDs. `runAll`, `runAccount`, counters, `FinishRun`, and task-status reconciliation are unchanged: they already operate over whatever `accs` slice they are handed. ### `handleRun` - Parse an **optional** JSON body `{"account_ids": [1,2,3]}`. Empty/absent body → `nil` → all accounts. - Validate parsed IDs against the task's accounts; if a non-empty selection resolves to zero valid IDs → `400 Bad Request`. - Existing error mapping preserved: `ErrNotTested` → 409 "accounts must pass connection tests first"; `ErrAlreadyRunning` → 409. ## Frontend ### Accounts table (`TaskDetail.tsx`) - New leading column with a per-row checkbox; a "select all" checkbox in the header (selects only selectable rows). - State: `selected: Set` of account IDs. - A row checkbox is disabled when the account has not passed **both** tests (`test_src_status`/`test_dst_status` !== 'ok') or the task is `running`. - "select all" reflects indeterminate/checked state over the selectable rows. ### Run button - With a non-empty selection: label "Run selected (N)"; calls `runTask(id, [...selected])`. - With no selection: label "Run migration"; calls `runTask(id)` (all). - Enable gate computed over the **effective set** (selected, or all when none selected): every account in that set must be tested OK, and the task must not be `running`. - Side benefit: one untested account no longer blocks running the ready ones — select only the ready accounts and run them. ### `api.ts` ```ts export const runTask = (id: number, accountIds?: number[]) => api(`/api/tasks/${id}/run`, accountIds?.length ? jsonBody({ account_ids: accountIds }) : { method: 'POST' }) ``` ## Out of scope (YAGNI) - The errors modal (`AccountErrorsModal`) is untouched; no retry buttons there. - No new "partial" task status. After a partial run the task status reflects the last run's outcome over the accounts that ran — same as today. - No selection persistence across reloads. ## Testing - **Backend unit:** `orchestrator.Run` with a subset filters correctly; empty filter runs all; unknown IDs dropped; empty-after-filter → `ErrNotTested`. - **Backend handler:** `handleRun` parses body, validates IDs, maps empty selection → 400, absent body → all. - **Frontend:** manual verification in the running app — select a subset, run, confirm only those accounts start (WS `account_started` events), and the unselected accounts' counters/status are untouched.