Problem (account.templ + handlers)
- The one-time app-password secret renders at the top of the page above
the IMAP guide — confusing placement; it belongs under App Passwords.
- Password list rows show the raw Stalwart credential ID on the right
(a meaningless stray string to users).
- No way to delete an app password from the UI (XRPC
removeAppPassword
exists server-side).
Decisions (locked 2026-09-11)
- Native
confirm() for the delete confirmation (works well on mobile).
- Keep no-JS fallbacks (depends on d656998 HTMX foundation).
Proposal
- Render the new secret once, under the App Passwords heading, with an OK
button that client-side dismisses it (no server state: the banner only
exists in the issue-response HTML; refresh already never reshows it).
- Row layout: description left; right side shows expiry (
ExpiresAt, or
“never expires”) + Delete button → native confirm() → new
POST …/app-passwords/{id}/delete handler calling appwd.Revoke
(fragment for htmx, redirect fallback).
- Drop the raw credential ID from the row (or label it if kept for
support debugging — default to dropping).
Files
internal/appview/ui/account.templ, handler.go (new delete
handler), ui_test.go
Acceptance
- Secret appears once under App Passwords, OK dismisses it.
- Rows show description + expiry + working Delete with confirm.
3 Comments
Inline Mailbox + Account Page UX Implementation Plan
Goal: Stay-on-page inline mailbox creation (domain screen) plus secret-placement, row-cleanup, and confirm-delete (account screen), using the
d656998HTMX foundation with no-JS fallbacks intact. (Combined plan for4f7a69b+6663bc8; same text posted on both.)Architecture: HX success paths return
200fragments (full list re-render + OOB notice/secret) instead of redirects; validation errors keep the existing422+formErrorpath. NewPOST …/app-passwords/{pid}/deletehandler callsappwd.Revoke, returns empty200for HX row-removal,303otherwise. All dismiss/confirm is inlineonclick/onsubmitso swapped-in fragments work with zero new JS listeners.Tech Stack: Go 1.26.5,
a-h/templ v0.3.1020, HTMX 4.0.0 vendored (hx.go:isHXRequest/writeHXError/writeHXFragment,fragments.templ:formError),httptest+seedStore/newTestServerpatterns.File map: - Modify:
internal/appview/ui/domain.templ,account.templ,handler.go(routes + 3 handlers),hx.go(1 helper) - Create: fragments in existinginternal/appview/ui/fragments.templ(mailboxList,mailboxRow,passwordList,passwordRow,appPasswordSecret) - Test: extendinternal/appview/ui/hx_handler_test.go, createinternal/appview/ui/fragments_test.goTask 1: Domain fragments (list + rows, Manage link)
Files: - Modify:
internal/appview/ui/fragments.templ,internal/appview/ui/domain.templ:42-55- Test:internal/appview/ui/fragments_test.goRun:
go test ./internal/appview/ui/ -run TestMailboxList -vExpected: FAILundefined: mailboxList.Link text is “Manage” (renamed from “Set up” per review; navigates to the account page for app passwords / IMAP setup).
fragments.templneedsimport "git.kilimanjaro.io/sovrn/internal/store"header.Also update the existing inline row link text
domain.templ:51from “Set up” to “Manage” (or replace the inline list with@mailboxList— prefer replacing so there is exactly one row template). Always render an (empty, hidden)#mailbox-listin the empty case so the HXouterHTMLtarget always exists; the success OOB removes#mailbox-empty(Task 2).Run:
templ generate ./internal/appview/ui/ && go test ./internal/appview/ui/ -run TestMailboxList -vExpected: PASS.Task 2: createAccount HX success stays on page
Files: - Modify:
internal/appview/ui/handler.go:145-171(createAccount),internal/appview/ui/domain.templ:56-63(form) - Test: extendinternal/appview/ui/hx_handler_test.goRun:
go test ./internal/appview/ui/ -run 'TestCreateAccount' -vExpected: PASS already (fallback + 422 paths exist fromd656998); these lock the fallback while the success branch is added below.Main swap replaces
#mailbox-list(full re-render handles empty→1 and N→N+1 uniformly); the notice and empty-state removal ride along as OOB swaps. Add"bytes"tohandler.goimports. Success path needs Stalwart so live verification is via smoke; unit tests cover fallback + validation + fragment rendering.Run:
templ generate ./internal/appview/ui/ && go test ./internal/appview/ui/ -run 'TestCreateAccount' -vExpected: PASS.Task 3: Account secret under App Passwords + OK dismiss
Files: - Modify:
internal/appview/ui/fragments.templ,internal/appview/ui/account.templ:3-10,43-65- Test:internal/appview/ui/fragments_test.goRun:
go test ./internal/appview/ui/ -run TestAppPasswordSecret -vExpected: FAILundefined: appPasswordSecret.Inline
onclick(not anapp.jslistener): swapped-in HX fragments work with no event rebinding. No server state — the banner exists only in the issue-response HTML;accountDetailnever setsNewSecret, so refresh never reshows it (unchanged semantics, new placement).if d.NewSecretblock (:5-10), render under the App Passwords heading instead:No-JS issue flow re-renders the full page with the secret in the new spot (same
AccountPageData.NewSecretfield, zeroview.gochanges).Run:
templ generate ./internal/appview/ui/ && go test ./internal/appview/ui/ -run TestAppPasswordSecret -vExpected: PASS.Task 4: Password rows (expiry + delete) + passwordExpiry helper
Files: - Modify:
internal/appview/ui/fragments.templ,internal/appview/ui/account.templ:48-56,internal/appview/ui/hx.go- Test:internal/appview/ui/fragments_test.goRun:
go test ./internal/appview/ui/ -run 'TestPasswordExpiry|TestPasswordRow' -vExpected: FAILundefined: passwordExpiry,undefined: passwordRow.(
hx.goneedsimport "git.kilimanjaro.io/sovrn/internal/appwd".)Raw Stalwart credential ID leaves the visible row (dropped per issue default; it remains in the delete URL +
liid for targeting). Nativeconfirm()viaonsubmitcovers both HX and no-JS.fragments.templneedsimport "git.kilimanjaro.io/sovrn/internal/appwd".[ ] Step 4: Replace account.templ:48-56 inline list with
@passwordList(d.Account.DomainID, d.Account.ID, d.Passwords)(keep thelen == 0empty-state paragraph).[ ] Step 5: Regenerate and verify
Run:
templ generate ./internal/appview/ui/ && go test ./internal/appview/ui/ -run 'TestPasswordExpiry|TestPasswordRow' -vExpected: PASS.Task 5: issueAppPassword HX success (secret + list, no navigation)
Files: - Modify:
internal/appview/ui/handler.go:192-229(issueAppPassword),internal/appview/ui/account.templ:57-64(form) - Test:internal/appview/ui/hx_handler_test.go[ ] Step 2: Run to verify it passes already (locks current notFound behavior for both paths).
[ ] Step 3: Add HX success branch after the second appwd.List (before the full-page render)
Add
passwordListOOBtofragments.templ— identical rows topasswordListbut the root carries the OOB attr:Main swap target is
#app-password-result(secret); the fresh list rides along as OOB. Form change:No-JS keeps the full-page render (Task 3 placement). Success path needs Stalwart; unit tests cover fragments + 404s, smoke covers live issue.
Run:
templ generate ./internal/appview/ui/ && go test ./internal/appview/ui/ -vExpected: PASS.Task 6: deleteAppPassword handler + route
Files: - Modify:
internal/appview/ui/handler.go:38-49(route), newdeleteAppPasswordfunc - Test:internal/appview/ui/hx_handler_test.goRun:
go test ./internal/appview/ui/ -run TestDeleteAppPassword -vExpected: FAIL (404 from mux — no route;*http.ServeMuxreturns 404 with empty body, status matches but the handler is missing; the test locks the shape, implementation adds the explicitnotFound+ Revoke logic). More precisely: it may PASS with mux-404 — either way proceed; the route-registration assertion below is the real gate.Run:
go test ./internal/appview/ui/ -run TestDeleteAppPassword -vExpected: PASS.Task 7: Verify + report
Run:
Expected:
gofmtempty, vet clean, all tests PASS.f4092c9per-session checklist).Task 7 (final) verification + combined fix report for 4f7a69b (inline-mailbox) + 6663bc8 (account-page).
What landed - Inline mailbox creation stays on the domain page: HX fragment + OOB notice, rows carry Manage links. - App-password secret renders under the App Passwords heading with OK dismiss. - Password rows show description + expiry + Delete with native confirm. - Issue flow is inline (secret + OOB list update). - New delete route returns empty-200 for HX row removal. - No-JS fallbacks preserved everywhere (non-HX POST/redirect paths untouched).
Verification evidence (run in order, 2026-09-11) -
templ generate ./internal/appview/ui/— clean, updates=0, EXIT 0. -gofmt -l internal/appview/ui/— prints NOTHING, EXIT 0 (one pre-existing alignment drift in fragments_test.go fixed and committed as style(ui): gofmt fragments_test alignment). -go vet ./...— clean, EXIT 0. -SOVRN_INTEGRATION=0 go test ./... -count=1— ALL PASS, EXIT 0: 16 packages ok, 0 failures (5 with no test files: api/sovrn, cmd/gensecrets, cmd/sovrn-lexgen, cmd/sovrnd, internal/appwd, internal/xrpc). - No-CDN sweeprg -n 'src="https?://|href="https?://.*htmx' internal/appview/ui/— prints nothing (EXIT 1 = no matches). The only “cdn” string in the tree is inside hx_handler_test.go test literals (the vendored-only assertion itself).Session commits (
jj log, newest first, excl. empty @) - pkkzvryx style(ui): gofmt fragments_test alignment - vstxwpwy feat(ui): delete app password with confirm - wklnqqpm fix(ui): first app-password issue swaps list in place - kvpnytzp feat(ui): issue app password inline with secret and list - qtlmvzpo test(ui): cover password expiry date branch and row attrs - mqzkkvwt feat(ui): password rows with expiry and delete - pqyktmop test(ui): pin account secret placement - ynyyyyzz feat(ui): app-password secret under heading with OK dismiss - yknpkuqm fix(ui): HX mailbox errors preserve list via OOB, keep input on failure - ywsyqoqx feat(ui): inline mailbox creation stays on domain page - lumpspsu test(ui): assert mailbox row id and escaping - skosyupt Extract mailbox list/row fragments with Manage linkDeviations from the posted plan (honest) - (a) HX mailbox 422s now preserve the list via OOB
formErrorOOB+writeMailboxError(the plan had the error replacing the list), and form reset is guarded byevent.detail.successful. - (b) Empty states always render hidden#mailbox-list/#password-listshells +#mailbox-empty/#password-empty, so first-create / first-issue OOB swaps land on empty accounts.Known minors (follow-ups, NOT blockers) - A stale green success notice can linger next to a later red error. - Deleting the LAST password leaves an empty list until reload (no empty-state restore on the delete path). - HX delete failure replaces the row with the error (refresh restores). - Stalwart-backed success paths (create/issue/delete) need live smoke since unit Stalwart is nil.
NOT closing — awaiting review (per f4092c9 per-session checklist).
Implementation complete — ready for review (NOT closing)
Combined
4f7a69b+6663bc8work is done across 14 commits (skosyupt..rxoxkxlt), all ininternal/appview/ui/.What landed
Domain screen (
4f7a69b): -mailboxList/mailboxRowfragments; row link renamed “Set up” → “Manage”. -createAccountHX success returns 200 full-list re-render + OOB success notice (“the account owner logs in with their ATProto account to set up their own client”) + OOB empty-state removal. Stays on page; form resets only on success (event.detail.successfulguard). - HX 422s preserve the list via OOBformErrorOOB+writeMailboxError(error rides OOB into#form-error, list is the main swap). No-JS keeps the 303 redirect.Account screen (
6663bc8): - One-time secret moved from page top to under the App Passwords heading (#app-password-resultslot), with inline-onclickOK dismiss. No server state; refresh never reshows. - Rows: description + expiry (passwordExpiry: “never expires” for zero time, else “expires YYYY-MM-DD”) + Delete with nativeconfirm(). Raw Stalwart credential ID dropped from visible text. -issueAppPasswordHX returns secret + OOB list refresh; empty states always render hidden#password-listshell +#password-emptyso first-issue OOB lands. - NewPOST …/app-passwords/{pid}/delete→appwd.Revoke: empty 200 removes the row in place (HX), 303 fallback (no-JS). - HX password errors (issue/list/delete failures) retarget into page-level#form-errorviaHX-Retarget/HX-Reswap(writeHXSlotError), leaving the secret/list slots untouched. Verified the vendored htmx 4.0.0 honorse.hx.retarget/e.hx.reswap. - Success paths clear stale#form-errorvia OOB.Verification evidence (fresh, this session)
gofmt -l internal/appview/ui/→ empty, exit 0go vet ./...→ clean, exit 0SOVRN_INTEGRATION=0 go test ./... -count=1→ all 17 test packages ok, exit 0 (UI package: 36 tests pass, 0 fail)src="https?:/// htmx href) → no matches; vendored-onlyDeviations from the posted plan (honest)
(a) HX mailbox 422s preserve the list via OOB instead of replacing it, and form reset is success-guarded (both lessons applied forward to the password forms). (b) Empty states always render hidden list shells + empty-state ids so first-create/first-issue OOB swaps land. © Password-issue/delete HX errors use
HX-Retargetheader retargeting instead of the planned OOB-list main swap (the planned shape would have duplicated#password-listinto the wrong slot — caught in final review, fixed inrxoxkxlt).Known minors (follow-ups, NOT blockers)
#form-error(row untouched); refresh restores either way.NOT closing — awaiting review (per
f4092c9per-session checklist).