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,
|
||||
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)
|
||||
|
||||
```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