Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.4 KiB
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:
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
ListAccountsByTaskresult down to the requested IDs beforegateOK, 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<number>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 isrunning. - "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
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.Runwith a subset filters correctly; empty filter runs all; unknown IDs dropped; empty-after-filter →ErrNotTested. - Backend handler:
handleRunparses 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_startedevents), and the unselected accounts' counters/status are untouched.