The auth screens say what happens next, in the buyer's words - #337
Merged
Conversation
Apolloccrypt
added a commit
that referenced
this pull request
Sep 2, 2026
Review of PR #337 found sixteen defects. This is that list, and only that list. Layout. /auth/request-reset and /auth/reset-confirm used <main class="page-main">, a class no stylesheet in frontend/ defines, so at 390px the heading and the blue button ran flush to both edges. Both now use container-sm plus section-lg/section, the shape /auth/backup already had. Measured at 390px: 24px each side, matching backup (24) and login (21). Truth. Four claims did not survive a check against admin/server.js: - the reset confirmation token is set with { EX: 3600 }, not 15 minutes; - "arrives within a minute" was a delivery promise nothing in the code makes, and the mail is sent fire-and-forget; - the sign-in 429 is per IP (5/900s) AND per email address (10/900s), so another device can cause it and "from this device" sent the reader after the wrong cause; - POST /user/account/totp/reset calls delete-totp before the setup mail goes out and then drops every session, so the old app stops working immediately and you are signed out. The HTML was right, the confirm dialog was wrong, and neither mentioned the sign-out. The AppImage signing status is not evidenced anywhere in the tree, so the verify box goes back to the scope main published: of the three installers only the .exe carries a signature. Words. /account said "TOTP" in the deactivate block and in the passkey field while calling it the authenticator app everywhere else; it now says authenticator app throughout, and the two kickers say what the block is for instead of which standard implements it. /signup/verified made the reader rank seven authenticator apps: it names two, keeps the two password managers, and links the help page that compares them. Its step badge said 2 on a page with no step 1. /download answered its own warning with four installers, three unsigned, and an instruction to click past SmartScreen. The first screen now says what to do in plain words, the evidence follows, and the installers sit behind "Still want the outdated desktop version? (for administrators)". Attribution. None of the eight screens named the party behind the product. The legal-strip is stamped by apply-nav.py and stays untouched, so the company, the town and the contact address are in the page body instead. tests/ui-truthfulness.test.mjs re-pins the two sentences that changed and adds eight assertions, one per finding, so none of them can come back. Green locally: seo-contract, links, ui-truthfulness, frontend-loading-contract, frontend-module-scripts, navigation-shell (24 checks), check-csp-inline.sh, check-cache-bust.sh (330 links), eslint (exit 0). All eight pages re-measured at 390x844: scrollWidth === clientWidth on every one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XJk2nCLCLmi3F71qkCUn7N
The five auth screens and /signup/verified each open with one sentence
saying what the screen is for and what happens after the button, per
docs/brand/messaging.md section 8: an auth screen does not sell, it
reports. Passkey is explained where it is clicked, not in a glossary.
Layout, measured at 390px with Playwright:
- request-reset and reset-confirm used <main class="page-main">, a class
no stylesheet defines, so the heading and the primary button sat flush
against both screen edges (0px). They now use container-sm, the same
container as backup: 24px a side, and the button starts at 24px.
- .lede has no top margin and h1 no bottom margin, so on setup, backup
and request-reset the heading touched the lead paragraph (0px, against
8px on login and 16px on verified). Those three ledes take the
existing .mt-3 utility, which leaves .lede alone for the eleven other
pages that use it. Now 16px.
Text that did not match the code:
- reset-confirm claimed confirmation links last 15 minutes. The token is
written with { EX: 3600 } (admin/server.js:1890, admin variant :3173),
so it says 60 minutes now.
- reset-confirm promised the mail "arrives within a minute". Sending is
fire-and-forget and nothing measures delivery, so the promise is gone.
The same unsourced promise on verified ("up to 60 seconds") goes with
it: the rule has to hold on every screen or it is not a rule.
- reset-confirm now says why the reset takes two mails. Clicking a link
in a mail and then being told a second mail follows reads as phishing
unless the page explains that the split is what stops someone who only
had the mailbox open for a moment.
- the 429 on login blamed "this device". The limit is per IP (5/900s)
and per email address (10/900s), admin/server.js:974-979, and the
email counter is account-wide. It now names both counters.
- verified asked the reader to rank seven authenticator apps and called
a SHA-256 app "stronger" without saying what that means. It now names
two apps, says a password manager also works, and links the help page
that does the comparing. The orphan step badge "2" on a page with no
step 1 is a phrase instead of a number.
/js/passkey.js goes back to ?v=3: this branch no longer changes it.
Every fix in the previous commit gets an assertion that fails if the sentence or the class is quietly dropped again: - the opening sentence of each of the six screens, by name; - no price, tier name or founder block on an auth screen, and no claim of a legal effect or a verified identity; - <main> on request-reset and reset-confirm must carry a class that some stylesheet actually defines, which is what went wrong the first time; - the 60 minute TTL, the absence of a delivery promise on both screens that used to make one, and the reason the reset takes two mails; - the 429 text naming both counters instead of the device; - the lead paragraph under the h1 on setup, backup and request-reset carrying a top margin; - no app ranking and no orphan step badge on verified; - the company and the country named on all six screens. Scope: /account and /download are deliberately not asserted here. The account screen is being rewritten in the dashboard PR and /download is not an auth screen, so both are back at their main version on this branch and their assertions came out with them.
Apolloccrypt
force-pushed
the
feat/fe-auth
branch
from
September 2, 2026 17:40
cb9e3a3 to
b0b6721
Compare
POST /api/user/auth/request-totp-reset sends a confirmation mail, not a
setup link. The success message claimed the opposite.
- The confirmation token is { EX: 3600 } (admin/server.js:1890). The
14 day token is only written after the confirmation is opened, in the
reset-confirm handler (admin/server.js:1935), so at the moment this
message appears no setup link exists yet. It now names both mails and
puts each lifetime on the right one: 60 minutes for the confirmation,
14 days for the setup link that follows it. /auth/reset-confirm was
already saying this correctly, so the two screens now agree.
- The lede said "we send a link that lets you link a new authenticator
app", which folds the two mails into one. Same correction.
- The success message could not be seen when it fired. #success sat
inside #reset-form, and the handler hides that form on success, so the
confirmation was hidden along with its parent and the screen went
blank after submitting. It now sits after the form. This was already
the case on main; it surfaced while rendering the new text. None of
the other auth screens nest a result inside an element they hide.
- 429 had no branch of its own and fell through to "Try again", while
the server answers retry_after 86400 off a limit of 5 per address per
24 hours and 10 per connection per hour (admin/server.js:1875). It now
says the wait can run to 24 hours, offers the support address, and
leaves the button disabled, because retrying does not help.
Six sabotages, each reverted, all red: 60 to 99 minutes, the setup-link
wording back, the 14 days detached from the second mail, the 429 branch
removed, the 24 hours softened, and #success moved back inside the form.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The five auth screens plus /signup/verified, rewritten so each one says what it is for and what happens after the button. Rebased on main, and two files that did not belong here are back at their main version.
Scope was cut back
/account and /js/account.inline1.js are out of this PR. #332 (feat/dashboard-ingelogd) rewrites both: plan name Community, the paid band per product, the Cancel gate on product plans. Two PRs editing the same two files is a merge conflict waiting to happen and the dashboard PR owns the account screen, so this branch leaves them at main. The account assertions came out of
tests/ui-truthfulness.test.mjswith them, including the one that would have forcedsessions and your authenticator app link are removedon a file this branch no longer touches.frontend/js/account.inline2.jsandfrontend/js/passkey.jsare back at main for the same reason: their changes were account-screen copy./js/passkey.jstherefore goes back to?v=3on login and setup./download is out of this PR, back at the main version. It is not an auth screen, and it was rejected twice on its own merits: internally contradictory (a banner telling you not to install it above a full product page),
ML-KEM-768 and AES-256-GCMin the first screenful,37 bits of nickname entropy, and a signing claim about the AppImage that nothing in the repo evidences. None of that is fixed by editing it inside an auth PR.Backlog item, not carried here: /download needs its own PR. One answer per page (use the web app), the desktop build and its 21 protections behind a disclosure, the installer signing status stated only as far as the repo can prove it, and a real button in the first screenful instead of a domain name buried in a warning paragraph.
What changed
frontend/apply-nav.pywas run after the rebase. It produced no diff: the nav and legal strip on these pages already match main.Layout, measured at 390px with Playwright
/auth/request-resetand/auth/reset-confirmused<main class="page-main">, and no stylesheet infrontend/defines that class. The heading and the primary button sat flush against both screen edges. Both now usecontainer-sm, the container/auth/backupalready uses..ledecarries no top margin andh1no bottom margin, so on setup, backup and request-reset the heading touched the lead paragraph. Those three take the existing.mt-3utility (16px), which leaves.ledealone for the eleven other pages that use it.The
YES, RESET MY AUTHENTICATORbutton on reset-confirm ran to the left edge; it now starts at 24px. No page scrolls horizontally at 390px.Text that did not match the code
The reset confirmation link lasts 60 minutes, not 15. The token is written with
{ EX: 3600 }atadmin/server.js:1890(// 60 minutes), and the admin-initiated variant at:3173uses the same TTL. There is no 15 minute TTL anywhere. The number was already wrong on main, but the old draft rewrote that very sentence and attached a safety justification to it, which made a wrong number sound deliberate.No delivery promise on either screen that made one.
Request a new one and it arrives within a minuteis gone from reset-confirm:sendResetConfirmEmailis fire-and-forget inside a try/catch (admin/server.js:1692) and nothing measures delivery. The same unsourced promise on/signup/verified(Delivery can take up to 60 seconds) goes with it. The earlier draft applied this rule to one screen and not the other, which is not a rule.reset-confirm now says why the reset takes two mails. You have just clicked a link in an email and the page tells you a second email follows. Without a reason that reads as phishing. The page now says the split is the point: the second link is only sent after this confirmation and has to be opened as well, so someone who briefly had your mailbox open cannot finish the reset.
The 429 on sign-in no longer blames the device. The limit is double,
admin/server.js:974-979: per IP (5 per 900s) and per email address (10 per 900s), andipCount > 5 || emailCount > 10returns the 429. The email counter is account-wide, so another device can cause it, and a shared connection can cause it without this account being involved. The text now names both counters instead of pointing the reader at the wrong cause.The first reset mail is a confirmation, not the setup link.
auth-request-reset.jstold youa setup link is on its way ... The link works for 14 days. The handler does something else: it writes a confirmation token with{ EX: 3600 }(admin/server.js:1890) and mails that. The 14 day setup token is only written after the confirmation is opened, in the reset-confirm handler (admin/server.js:1935), so at the moment that message appears no setup link exists yet. The success message now names both mails and puts each lifetime on the right one: 60 minutes for the confirmation, 14 days for the setup link that follows./auth/reset-confirmwas already saying this correctly, so the two screens now agree. The lede had the same fold and got the same correction.The success message could not be seen when it fired.
#successsat inside#reset-form, and the handler hides that form on success, so the confirmation was hidden along with its parent and the screen went blank after submitting. It now sits after the form. This was already the case on main and surfaced while rendering the new text; none of the other auth screens nest a result inside an element they hide.429 on request-reset got its own branch. It fell through to
Try again, while the server answersretry_after: 86400off a limit of 5 per address per 24 hours and 10 per connection per hour (admin/server.js:1875). The text now says the wait can run to 24 hours, offers the support address, and the button stays disabled, because retrying does not help./signup/verified stopped asking a lawyer to rank authenticator apps.
A SHA-256 app is stronger: Raivo (iOS, open source), Aegis (Android, open source), 1Password, Bitwarden, Authy, Ente Auth or 2FASappeared twice. It now names Google Authenticator and Microsoft Authenticator, says a password manager you already use also works, and links/help/authenticator-appsfor the comparison. The orphan step badge2on a page with no step 1 is now a phrase.Tests
tests/ui-truthfulness.test.mjspins every one of the above. Twelve sabotages in the working tree, each reverted, all twelve red:mt-3page-main#successback inside the formThe
page-mainassertion is structural rather than textual:<main>on those two pages must carry a class that some stylesheet actually defines, which is the defect that produced the 0px margin in the first place.Green locally: links, seo-contract, ui-truthfulness, frontend-loading-contract, navigation-shell (25 checks), csp-inline, cache-bust (335 links, one consistent
?v=), eslint, andtests/static-sanity.shPASS with all 10 sections green.frontend/index.htmlandfrontend/js/nav-auth.jsare not in the diff.frontend/apply-nav.pyis not in the diff either: running it changed nothing.