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
+86
View File
@@ -0,0 +1,86 @@
# emcli — Phase 4 Status Report
**Date:** 2026-06-22
**Branch:** `main`
**Phase 4 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`.
## TL;DR
**Phase 4 is complete and validated live.** Built test-first; the binary builds as a single static
CGO-free executable, `go vet` is clean, and the full suite passes including `-race`. `doctor` and
the admin commands were exercised against the two real accounts (mxlogin password auth + Gmail
app-password). A real bug — `doctor` running checks with stripped (empty) credentials — was caught
by live validation, reproduced with a regression test, and fixed.
## What was built
| Area | Change | Status |
|---|---|---|
| `store` | `UpdateAccount` (partial edit; re-encrypts password/secrets only when non-empty, blank keeps existing); `RecentAuditFor(account, limit)`. | ✅ |
| `mail` | `CheckIMAP` (connect+login+logout) and `CheckSMTP` (connect+SASL-PLAIN auth+quit) — no mail transferred. | ✅ |
| `cli` | `doctor [--account]` (per-account IMAP/SMTP `ok`/`FAIL`, exit non-zero on any failure, no secrets); `config set`/`get` (validates `audit_retention_days`); `audit list [--account] [--limit]`; `account edit` (flag partial-update) / `account remove [--yes]`. | ✅ |
| `tui` (new pkg) | `AccountForm` bubbletea model over `bubbles/textinput`, with pure, fully-tested `Fields` (validation + `store.Account` assembly + edit prefill). | ✅ |
| `cli` wiring | `init` (create/open DB, seed `audit_retention_days=90`, add first account via TUI); bare `account add` → TUI; `account edit --name X` (only `--name`) → TUI prefilled. | ✅ |
### Commands added
```
emcli doctor [--account <name>]
emcli config set <key> <value> # e.g. audit_retention_days
emcli config get <key>
emcli audit list [--account <name>] [--limit N]
emcli account edit --name <n> [--mode|--imap-host|--smtp-host|…] # flag partial-update
emcli account edit --name <n> # interactive (TUI)
emcli account remove --name <n> --yes
emcli account add # interactive (TUI)
emcli init # interactive (TUI)
```
## Live validation
Against the two real accounts (`mxlogin` password auth, `gmail` app-password) in an isolated DB:
- **`doctor`** — `mxlogin` and `gmail` both report `IMAP ok` / `SMTP ok`; a deliberately
bad-password account reports `IMAP FAIL: Invalid credentials` (clean error, **no crash**); SMTP
shown `n/a` for RO. Exit `1` when any check fails, `0` when `--account` targets a passing one.
- **`config`** — `set`/`get` round-trip; `audit_retention_days=-5` rejected (exit 2).
- **`audit list`** — rendered a real `list`/`allowed` row.
- **`account edit`** (flag) — set a subject-regex on `mxlogin`; a follow-up `doctor` still passed,
proving `UpdateAccount` **preserved the encrypted password** through the edit.
- **`account remove --yes`** — deleted an account; gone from `account list`.
- **TUI** (`init`/interactive) requires a real terminal; without a TTY it fails cleanly
(`could not open a new TTY`), no panic. Drive it interactively to use.
## Bug found and fixed during live validation
`doctor` initially authenticated with **empty passwords** for every account — it iterated
`ListAccounts()`, which deliberately strips secrets, and passed those credential-less structs to the
live checks. Caught immediately against real servers ("Empty username or password"). Fixed by
re-fetching each account with `GetAccount` (which decrypts) before checking; locked in with
`TestDoctorUsesDecryptedCredentials`.
## Verification
```
CGO_ENABLED=0 go build ./... → OK, single static binary
go vet ./... → clean
go test ./... → all packages pass (incl. new internal/tui)
go test -race ./... → all packages pass
```
New tests: `store` update/audit-filter; `mail` check-fails-cleanly; `cli` doctor (all-ok, failure,
RO-skip, account-filter, decrypted-creds regression), config/audit/edit/remove via `Run()`; `tui`
Fields validation/assembly/prefill and form submit/cancel.
## Known limitations / deferred
- TUI is a minimal keyboard-driven form (bool fields entered as `y/n`, enums as text); no mouse or
theming. Sufficient for admin use.
- OAuth consent in `init` omitted (OAuth deferred in Phase 3).
- Carry-over Minor items from Phase 1 (audit-row completeness, some CLI polish) remain open.
## Project status
Phases 14 complete: read path, send path, Gmail (app password), and admin/TUI/doctor. The core
`emcli` surface from the SPEC is implemented and validated live, with OAuth2 (§10) the one
deliberately-deferred item.