feat(admin): Phase 4 — doctor, admin completeness, and bubbletea TUI

Adds the admin/diagnostics surface from SPEC §7.2:

- doctor [--account]: per-account IMAP + (RW) SMTP connectivity/auth checks via
  new mail.CheckIMAP/CheckSMTP (connect+auth only, no mail). Exit non-zero on any
  failure; secrets never printed.
- store.UpdateAccount: partial edit, re-encrypts password/secrets only when a
  non-empty value is supplied (blank keeps existing). RecentAuditFor(account).
- config set/get (validates audit_retention_days), audit list [--account][--limit],
  account edit (flag partial-update) / remove [--yes].
- internal/tui: bubbletea AccountForm with pure, fully-tested Fields (validation +
  store.Account assembly + edit prefill). init / bare `account add` / `account edit
  --name X` drop into the TUI; flag forms remain for scripting.

Built test-first; full suite green incl -race. Validated live against the mxlogin
(password) and Gmail (app-password) accounts. Live validation caught a real bug:
doctor authenticated with empty passwords because it iterated ListAccounts (which
strips secrets) — fixed to re-fetch via GetAccount, locked in by a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 20:09:43 +01:00
parent 193815dd25
commit a837b25d73
20 changed files with 1535 additions and 10 deletions
@@ -0,0 +1,75 @@
# emcli — Phase 4 Plan: Admin TUI + `doctor`
**Date:** 2026-06-22
**Depends on:** Phases 13 complete (read, send, Gmail-via-app-password).
**Scope (SPEC §7.2):** `doctor` connectivity/auth diagnostics; admin completeness
(`account edit/remove`, `config set/get`, `audit list`); and a **bubbletea TUI** for `init` and
interactive `account add/edit`. (OAuth consent in `init` is omitted — OAuth deferred in Phase 3.)
## Building blocks already present
- store: `GetSetting`/`SetSetting`, `RecentAudit(limit)`, `DeleteAccount`, `AddAccount`,
`GetAccount`, `ListAccounts`. mail: `Dial` (connect+login), `SendSMTP`.
- Need new: store `UpdateAccount` + account-filtered audit; mail `CheckIMAP`/`CheckSMTP`
(connect+auth only, no traffic); the TUI form.
## Tasks
### 1. `store`: `UpdateAccount` + account-filtered audit
- `UpdateAccount(a Account) error` — updates mutable fields by name; re-encrypts the password
**only if a non-empty one is supplied** (blank = keep existing); same for OAuth secrets.
- `RecentAuditFor(account string, limit int)` (account `""` = all) for `audit list --account`.
**Tests:** update changes fields + preserves password when blank; password re-encrypts when set;
audit filter returns only the named account.
### 2. `mail`: `CheckIMAP` / `CheckSMTP` (for doctor)
- `CheckIMAP(IMAPConfig) error``Dial` (logs in) then `Logout`. Surfaces connect/auth failure.
- `CheckSMTP(SMTPConfig) error` — dial (tls/starttls), `Auth` (SASL PLAIN), `Quit`. No mail sent.
**Tests:** both fail cleanly on an unroutable host (error, no panic). Live auth covered in task 8.
### 3. `cli`: `doctor`
`emcli doctor [--account <name>]` — human-readable. Verifies `EMCLI_KEY` + DB open (via
`openStore`), then per account prints IMAP and (RW + smtp set) SMTP as `ok`/`FAIL: <reason>`.
Exit non-zero if any check fails. Secrets never printed.
**Tests:** table rendering + pass/fail aggregation with injected check funcs (no network).
### 4. `cli`: `config set/get` + `audit list`
- `emcli config set <key> <value>` / `emcli config get <key>` — wraps settings; known key
`audit_retention_days` (validate integer ≥ 0 on set).
- `emcli audit list [--account <name>] [--limit N]` — table of recent entries (default 50).
**Tests:** set→get round-trip; retention validation; audit list renders rows.
### 5. `cli`: `account edit` / `account remove`
- `account edit --name <n> [--mode ...] [--imap-host ...] …` — flag-based partial update via
`UpdateAccount`; only provided flags change (others preserved). Bare `account edit --name <n>`
with no other flags drops into the TUI form (task 6).
- `account remove --name <n> [--yes]``DeleteAccount`; require `--yes` or interactive confirm.
**Tests:** edit changes only supplied fields; remove deletes; remove missing → error.
### 6. `tui`: bubbletea account form (testable model)
New `internal/tui` package (keeps bubbletea out of `cli`'s testable core, no store dependency).
- `AccountForm` — a bubbletea `Model` over `bubbles/textinput` fields: name, mode, imap
host/port/security, smtp host/port/security, username, password, whitelist-in/out toggles,
process-backlog. Validates; produces a `store.Account` (+ `PasswordSet bool`).
- Driven by `Update(tea.Msg)`; exposes `Account()`, `Done()`, `Cancelled()`, `Err()` so the
logic is unit-testable by feeding key messages — no terminal needed.
**Tests:** feed keystrokes → assert assembled Account; required-field validation blocks submit;
edit-mode prefill round-trips; blank password in edit ⇒ `PasswordSet == false`.
### 7. `cli`: wire `init` + interactive add/edit
- `emcli init` — if no accounts exist, run the TUI form, persist the first account, set
`audit_retention_days` default. Idempotent-ish: warns if an account already exists.
- `account add` with no flags → TUI form; `account edit --name X` with no other flags → TUI
prefilled. Flag forms remain for scripting.
- `runTUIAccount` glue persists the form's `store.Account` via Add/Update.
### 8. Build/vet/test (incl `-race`) + live `doctor` validation
`CGO_ENABLED=0 go build`, `go vet`, `go test ./...`/`-race`. Live: run `doctor` against the
mxlogin (password) and Gmail (app-password) accounts — assert IMAP+SMTP `ok`; assert a bad
password reports `FAIL` (auth), not a crash.
### 9. Status report + commit/push
`PHASE4-STATUS.md`; commit to `main`; push via tea token.
## Out of scope
- OAuth consent in `init` (Phase 3 deferred OAuth).
- Mouse/advanced TUI theming; a minimal, keyboard-driven lipgloss-styled form is enough.