Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d8471efdd
|
||
|
|
3eac1cea7d
|
@@ -4,6 +4,16 @@ Single-binary Go server (embeds the React SPA) that copies IMAP mailboxes
|
|||||||
between a source and a destination account. Non-destructive (copy only,
|
between a source and a destination account. Non-destructive (copy only,
|
||||||
never deletes), deduplicated by Message-ID, resumable/idempotent re-runs.
|
never deletes), deduplicated by Message-ID, resumable/idempotent re-runs.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
Task list — create migration jobs and track their status:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Task detail — run control, per-account credentials, live event log and progress:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Quick start (plain HTTP on :80)
|
## Quick start (plain HTTP on :80)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 221 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 326 KiB |
@@ -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<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`
|
||||||
|
|
||||||
|
```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.
|
||||||
Reference in New Issue
Block a user