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:
@@ -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 1–4 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.
|
||||
@@ -0,0 +1,75 @@
|
||||
# emcli — Phase 4 Plan: Admin TUI + `doctor`
|
||||
|
||||
**Date:** 2026-06-22
|
||||
**Depends on:** Phases 1–3 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.
|
||||
Reference in New Issue
Block a user