Compare commits

...
2 Commits
Author SHA1 Message Date
vasyansk 6d8471efdd add dick pics 2026-07-08 13:33:14 +07:00
vasyanskandClaude Opus 4.8 3eac1cea7d Add selective account run design spec
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 13:30:11 +07:00
4 changed files with 121 additions and 0 deletions
+10
View File
@@ -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:
![Migration tasks list](docs/SCR-20260708-mbxn.png)
Task detail — run control, per-account credentials, live event log and progress:
![Task detail](docs/SCR-20260708-mcal.png)
## 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.