From 3eac1cea7d29249599dd793271c5296f64e6148f Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Wed, 8 Jul 2026 13:30:11 +0700 Subject: [PATCH] Add selective account run design spec Co-Authored-By: Claude Opus 4.8 (1M context) --- ...2026-07-08-selective-account-run-design.md | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-08-selective-account-run-design.md diff --git a/docs/superpowers/specs/2026-07-08-selective-account-run-design.md b/docs/superpowers/specs/2026-07-08-selective-account-run-design.md new file mode 100644 index 0000000..aac24c3 --- /dev/null +++ b/docs/superpowers/specs/2026-07-08-selective-account-run-design.md @@ -0,0 +1,111 @@ +# 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.