fix: address the feature review — accurate copy, reachable controls, real coverage

Round 1 on the feature diff found 15; the substantive ones:

- The delete control VANISHED on any invite whose address had registered, which
  is the ordinary sign-up shape (register writes the User and the pending invite
  together). The guard stays broad — that row is the account's approval gate, and
  orphaning it strands someone who can then neither sign in nor be approved — but
  the control is now a disabled button carrying the reason, on both lists, so an
  admin never sees a row silently missing a button its neighbours have.
- Deleting an entry is not always a revoke: is_allowed falls back to the env
  ALLOWED_EMAILS / ADMIN_EMAILS lists once no row exists, and an ADMIN_EMAILS
  address is force-promoted on its next Google sign-in. The route now reports
  still_allowed_by_env and the UI says so instead of claiming access was removed.
- "Deny keeps the row so they can't immediately re-ask" was simply false —
  upsert_pending_invite reopens a denied row. Corrected everywhere it was stated.
- The Configuration warning reads me.email_configured, which save() did not
  invalidate, so it kept warning about fields the admin had just filled in.
- The read-only variant was an aria-hidden span explained only by a Tooltip that
  the hints toggle can switch off; now a disabled button with title + aria-label.
- The delete error path threw away the 409's reason (the stale-list case) and
  left the stale row on screen.
- The throttle window was spent even when no notice was written, silencing the
  next hour on the path whose job is to stop silent drops.
- email_enabled() opened a second session and decrypted the SMTP password on
  every /api/me poll; it now takes the caller's session.
- welcomeMode.dom.test.ts never rendered the card, so reverting the history
  wiring to useState left it green. A render test now drives the real links and
  fails on that revert (verified).

Plus /api/me flag tests, the registration-shaped pending case, the env-allowlist
case, and a release note that matches what the code does.
This commit is contained in:
2026-09-01 23:31:49 +02:00
parent 44f626f732
commit 85043013f0
14 changed files with 298 additions and 55 deletions
+5 -5
View File
@@ -18,18 +18,18 @@ export const RELEASE_NOTES: ReleaseEntry[] = [
version: "0.78.6",
date: "2026-09-01",
summary:
"Admins can now delete a whitelist entry, and an instance that cannot send email finally says so — in the inbox when a message is dropped, and on the screens where it matters.",
"Admins can delete a whitelist entry, and an instance that cannot send email finally says so — in the inbox when a message is dropped, and on the screens where it matters.",
features: [
"Delete an entry from Users → Access requests: both a pending request and an address you whitelisted earlier. Denying a request keeps the row so the person cannot immediately re-ask; deleting forgets the address, and a later request from it starts over. An entry that already has an account behind it stays read-only — removing it would revoke that person's access without saying so, so delete the user instead.",
"When a message cannot go out, every admin gets it in their inbox, naming the message that was lost — an access request, a verification link, a password reset. Sending is deliberately fail-soft so a mail problem never breaks sign-up, but that used to mean it left no trace anywhere. Throttled to one notice an hour, so a dead relay cannot bury the inbox.",
"Delete an entry under Users → Access requests: an address you whitelisted earlier, or a request nobody has acted on. Deny records your decision and keeps the record; delete forgets the address, and a later request from it arrives as a new one. Entries that already have an account behind them show the control disabled — that row is the account's approval gate, so it is managed from Users & roles, where deleting the account removes the entry too. If the address is also allow-listed in the server's configuration, the confirmation says so, because then removing the entry does not actually take access away.",
"When a message cannot go out, every admin gets it in their inbox, naming the most recent message that was lost — an access request, a verification link, a password reset. Sending is deliberately fail-soft so a mail problem never breaks sign-up, but that used to mean it left no trace anywhere. Throttled to one notice an hour, so a dead relay cannot bury the inbox.",
"A standing warning where it can be acted on: on Configuration → Email / SMTP while the settings are incomplete, and above the access-request list, where it explains that nobody is getting a verification link and no request will reach you by email.",
],
fixes: [
"On the sign-in card, Create an account / Forgot password / Didn't get the verification email are proper history steps now, so Back — including the mouse's back button — returns to the previous view. It used to move the address bar and leave the form on screen unchanged.",
],
chores: [
"DELETE /api/admin/invites/{id}, refused with 409 while a User row exists for the address; the invite list carries has_account so the UI can withhold the affordance rather than offer a button that fails. Audited as invite_delete.",
"email._send() records an email_undelivered notification for every active admin on both the not-configured and the send-failed path, opening its own session and swallowing its own errors so the alert can never become a new way to break a send; /api/me carries email_configured for admins. The login card's mode moved to useHistorySubview, and the per-test rate-limiter reset now sweeps every loaded app.* module instead of a hand-kept list — this release added a limiter outside it.",
"DELETE /api/admin/invites/{id}, refused with 409 while a User row exists for the address, whatever its state — orphaning an account from its invite would leave someone who can neither sign in nor be approved. The list carries has_account so the UI disables rather than hides the control, and the response reports still_allowed_by_env, since is_allowed falls back to ALLOWED_EMAILS / ADMIN_EMAILS once no row exists. Audited as invite_delete.",
"email._send() records an email_undelivered notification for every active admin on both the not-configured and the send-failed path, opening its own session and swallowing its own errors so the alert can never become a new way to break a send; a failed or unnecessary write releases the throttle rather than costing the next hour. /api/me carries email_configured for admins, read through the request's own session. The login card's mode moved to useHistorySubview, covered by a render test that fails if it goes back to useState, and the per-test rate-limiter reset now sweeps every loaded app.* module instead of a hand-kept list.",
],
},
{