Files
imap-copier/docs/superpowers/specs/2026-07-08-selective-account-run-design.md
T
2026-07-08 13:30:11 +07:00

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 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.goRun(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 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

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.