Files
steve a837b25d73 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>
2026-06-22 20:09:43 +01:00

87 lines
4.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.