Skip to content

The signed-in shell: support a phone can reach, a homepage that stops selling, and one apology - #396

Merged
Apolloccrypt merged 2 commits into
mainfrom
koper/ingelogde-schil
Sep 3, 2026
Merged

The signed-in shell: support a phone can reach, a homepage that stops selling, and one apology#396
Apolloccrypt merged 2 commits into
mainfrom
koper/ingelogde-schil

Conversation

@Apolloccrypt

@Apolloccrypt Apolloccrypt commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Four findings from a buyer review of the signed-in journey (screenshots in the review set), all on surfaces that only exist after someone has paid.

1. Support was unreachable on a signed-in phone

The signed-in navigation is Documents / Send / Sign / Verify / Settings, and js/nav-auth.js deleted the strip under the mobile drawer once the session check came back, on the reasoning that the account menu carries Help from then on. On 390px that leaves nothing:

  • nav.css hides the .nav-help text link below 700px
  • the drawer is pinned by tests/navigation-shell to exactly the five workspace links
  • the menu behind the email address is where you go to sign out

A customer with a stuck signature had to type the url.

The strip stays now and carries Help alone. Sign in is dropped from it, because that is not an action you still need. The text link is back in the bar from 700px up, in the same place it sits signed out, with the 44px target nav.css already gives it.

tests/navigation-shell.test.mjs counts this in taps, not in "a link exists": the drawer is open (tap one), anything inside the closed account menu is explicitly excluded because it would cost a third, and the test then clicks the link and follows it to /help. A 44px target on a dead link is worse than no target.

The four public nav pins and the signed-out phone bar are untouched.

2. The signed-in homepage was still the sales page

It swapped the hero and left everything under it: why half of it is free, the two products, the price table, the founder letter. Three actions and then eight screens of pitch, on a phone, for someone who had already bought.

js/home-auth.js now stamps data-session="in" on <html>; index.html hides every section after the hero off that one attribute, in CSS. No inline script (CSP script-src 'self'), and nothing walks a list of section ids, so a section added tomorrow is covered the day it lands.

The hero keeps its three actions and the founder line (ui-truthfulness pins the wording). Signed out the page is what it was, and the suite checks that too: same section count, no attribute on <html>.

3. The signing error the customer could not act on

enrolEphemeralSigningKeyWithTotp translated three TOTP failures into their own words and rethrew everything else untouched. What reached the screen was the wire's own message: _postJSON builds one out of the relay's error field or 'http_' + status, and a dropped connection arrives as a TypeError with whatever the browser calls it. http_502 is not an instruction.

frontend/js/error-message.js owns one sentence for the unplanned case, with a next step and an address that reaches a human, and the technical detail goes to console.error instead of the screen. sign-flow.js and co-sign.js render it rather than falling through to their passkey guess. js/parashare.page.js printed 'Error: ' + e.message in three places and now prints the same sentence from the same file (it is a classic script, so it reads the namespace off the global; the file is the same UMD shape as js/parasign-pdf-ops.js, which is what lets node require it).

tests/error-message.test.mjs runs a real thrown TypeError through the translator. Four sabotages verified red before commit:

sabotage result
userFacingMessage returns error.message 3 failures
sentence loses the address and the next step 1 failure
throw e back in the signer 1 failure
parashare prints e.message again 1 failure

4. Two prices for one account

/dashboard answered "what does it cost?" with "Paid plans start at 15 euro a month, which is ParaSend Pro"; /help answered the same question with "ParaSign Pro at 49 euro a month". One customer, two of our own pages, two numbers, and the cheaper one on the surface that exists for signing.

One answer now names both products with both prices. relay/test/pricing-page.test.js pins it the way the pricing cards are pinned: each amount is read back out of relay/lib/billing-catalog.js (excl. btw of the monthly variant checkout actually charges), the plan name is pinned to relay/lib/tiers.js, and every amount in the sentence has to stand on /pricing with a real boundary, so a bare 15 cannot be satisfied by a 150 elsewhere on the page. Sabotages verified red: dropping the signing half, quoting an amount not on /pricing, and moving the catalog price.

Cache-busts

nav-auth.js v6 -> v7 and home-auth.js v1 -> v2, across every stamped page and apply-nav.py, because the files changed and nginx serves them immutable.

Gates run locally

first-screen, ui-truthfulness, site-claims, seo-contract, links, navigation-shell (51 checks), frontend-loading-contract, frontend-module-scripts, pricing-fold, cache-bust, csp-inline, static-sanity, check-test-declarations, eslint, app-contrast, app-theme, apply-nav-idempotent (apply-nav.py updates 0 files), user-dashboard-documents, code-manifest, apply_seo_head --check (0 pages), sign-full (33/33), relay pricing-page (48 checks), the full browser set (41/41) and the root non-browser set.

One pre-existing environment failure, unrelated to this branch: tests/heartbeat-lib.test.mjs cannot resolve @noble/post-quantum in this checkout's shared node_modules. CI installs it with npm ci.


Follow-up after review: the hero art was being cut in half

The first commit shrank the signed-in hero to 472px at 1440 with padding-bottom:48px. .hp-doc inside .hp-art did not shrink with it: that element is absolutely positioned and draws about 120px below the 380px box .hp-art reserves for it. Signed out that overhang lands on the section underneath and is never seen. Signed in the hero is the last thing in <main>, main carries overflow:hidden, and the document, the receipt and the signature were sliced through where main ends.

Making the hero taller does not fix it, and that is worth stating: with the padding rule removed entirely and the art left in, the overhang is still 74px past the clip. The height was never the cause.

So the art retires with the pitch it illustrates. It is a document that gets signed, sealed and then burns on a 16-second loop, and it is there to explain the product to someone who does not have it yet. A phone has hidden it since it shipped, for its own reason; signed in, every width does now. With it gone the second grid column is empty, so the hero is one column, keeping the 1120px wrap that lines it up with the nav and the footer.

The check measures against the edge that actually cuts: every box the art draws, at 1440, against main's own rectangle, which is the clip. One exclusion, named and argued in the test: .hp-art-glow is a blurred radial gradient with inset:-40% -20%, built to bleed, with no edge that can read as cut, and it has hung 13px over the top of main since the art shipped. A signed-out control runs the same measurement and requires the art to be present and whole, so the signed-in pass cannot be a pass over an empty list.

sabotage result
the reviewed state (art visible, hero at 48px bottom padding) red, hp-doc 531..1030 outside main 56..956
art visible with no padding change at all red, same element

navigation-shell is 53 checks. Rebased onto current main (#394).

Known merge overlap

…hat stops selling, and one apology

Four things a buyer review found on the ingelogde reis, all of them on the
surfaces you only see after you have paid.

Support. The signed-in navigation is Documents, Send, Sign, Verify, Settings,
and js/nav-auth.js DELETED the strip under the mobile drawer once you signed
in, on the reasoning that the account menu carries Help from then on. On a
390px screen that left no Help at all: nav.css sheds the .nav-help text link
below 700px, the drawer is pinned to the five workspace links, and the menu
behind the email address is where you go to sign out, not where you go when a
signature is stuck. A paying customer had to type the url. The strip stays now
and carries Help alone (Sign in is not an action you still need), and the text
link is back in the bar from 700px up. tests/navigation-shell counts it in
TAPS: menu button, then Help, and it follows the second tap to /help so a
44px target on a dead link cannot pass. The four public nav pins are untouched.

The signed-in homepage. It swapped the hero and left the entire sales page
under it: why half of it is free, the two products, the price table, the
founder letter. Three actions and then eight screens of pitch, for someone who
had already bought. js/home-auth.js now stamps data-session="in" on <html> and
index.html hides every section after the hero off that one attribute, in CSS,
so a section added tomorrow is covered the day it lands and no inline script
is involved. Signed out the page is byte-for-byte what it was, and the suite
checks that too.

Error messages. enrolEphemeralSigningKeyWithTotp translated three TOTP
failures and rethrew everything else, so what reached the customer was the
wire's own words: "http_502" from _postJSON, or whatever a browser calls a
lost connection on a TypeError. Neither names a problem, let alone a next
step. frontend/js/error-message.js owns one sentence for the unplanned case,
with what to do and an address that reaches a human, and the technical detail
goes to the console instead of the screen. js/parashare.page.js printed
"Error: " + e.message in three places and now prints the same sentence from
the same file. tests/error-message.test.mjs runs a real TypeError through the
translator; four sabotages were verified red.

The price question. /dashboard answered "what does it cost?" with "Paid plans
start at 15 euro a month, which is ParaSend Pro" while /help answered it with
"ParaSign Pro 49 euro". One customer, two of our pages, two numbers, and the
cheaper one on the surface that exists for signing. The answer names both
products with both prices, and relay/test/pricing-page.test.js pins both to
relay/lib/billing-catalog.js the way the pricing cards are pinned, plus the
plan name to relay/lib/tiers.js. No amount in it that is not on /pricing.

nav-auth.js and home-auth.js changed, so their cache-busts move with them.

Gates: first-screen, ui-truthfulness, site-claims, seo-contract, links,
navigation-shell (51 checks), frontend-loading-contract, pricing-fold,
cache-bust, csp-inline, static-sanity, check-test-declarations, eslint,
app-contrast, app-theme, apply-nav-idempotent, user-dashboard-documents,
apply_seo_head --check, sign-full (33/33), pricing-page (48 checks).
…cut in half

Review of the first commit found the one thing it broke. Signed in at 1440 the
hero shrank from 654px to 472px, and .hp-doc inside .hp-art did not shrink with
it: that element is absolutely positioned and draws about 120px BELOW the 380px
box .hp-art reserves for it. Signed out the overhang lands on the section
underneath and nobody ever sees it. Signed in the hero is the last thing in
<main>, main carries overflow:hidden, and the document, the receipt and the
signature were sliced through where main ends.

Making the hero taller does not fix that, and this is worth being precise
about: with the padding rule removed entirely and the art left in, the overhang
is still 74px past the clip. The height was never the problem.

So the art goes where the rest of the pitch went. It is a document that gets
signed, sealed and then burns, on a 16-second loop, and it is there to explain
the product to someone who does not have it yet. A phone has hidden it since it
shipped, for its own reason; signed in, every width does now. With it gone the
second grid column is empty, so the hero is one column, keeping the 1120px wrap
that lines it up with the nav and the footer.

The check measures against the edge that actually cuts. Every box the art
draws, at 1440, against main's own rectangle, which IS the clip. One exclusion,
named and argued in the test: .hp-art-glow is a blurred radial gradient with
inset:-40% -20%, built to bleed, with no edge that can read as cut, and it has
hung 13px over the top of main since the art shipped. The signed-out control
runs the same measurement and requires the art to be there and whole, so the
signed-in pass cannot be a pass over an empty list.

Sabotage, both red: the merged state (art visible, hero at 48px bottom padding)
reports hp-doc cut, and so does the art with no padding change at all.

navigation-shell is 53 checks. first-screen, ui-truthfulness, site-claims,
seo-contract, links, pricing-fold, frontend-loading-contract, cache-bust,
csp-inline, static-sanity, check-test-declarations and apply_seo_head --check
all green.
@Apolloccrypt
Apolloccrypt merged commit 482306c into main Sep 3, 2026
14 checks passed
Apolloccrypt pushed a commit that referenced this pull request Sep 3, 2026
Follow-up to the security review, on top of #396.

The failure sites this branch added now go through failureText(), the
helper #396 put at the top of this file, so /parashare has one voice for
what we did not plan for instead of three. The account-key failure reports
through it and keeps its own banner sentence, because a key that will not
load is a case we DID plan for and "sign in again" is a better next step
than "try again in a minute". Same for the relay-sector sentence; what used
to be a second wording invented at the call site is now the shared one.

parashare.page.js goes to ?v=5. #396 shipped a v4 of that file and the
commit below this one shipped a different v4. Both sides agree on the
number, so check-cache-bust cannot see it: one immutable url, two contents,
and whichever a browser cached first is the one it keeps. The order of the
two script tags is pinned in the suite as well, because error-message.js is
a plain script and failureText reads window.paramantErrors at call time: put
parashare.page.js first and the page quietly falls back to its own copy of
the sentence. Verified by sabotage.

The vm suite now loads the real frontend/js/error-message.js into the
context, so what it measures is the shipped sentence and not the fallback.
Apolloccrypt pushed a commit that referenced this pull request Sep 3, 2026
Follow-up to the security review, on top of #396.

The failure sites this branch added now go through failureText(), the
helper #396 put at the top of this file, so /parashare has one voice for
what we did not plan for instead of three. The account-key failure reports
through it and keeps its own banner sentence, because a key that will not
load is a case we DID plan for and "sign in again" is a better next step
than "try again in a minute". Same for the relay-sector sentence; what used
to be a second wording invented at the call site is now the shared one.

parashare.page.js goes to ?v=5. #396 shipped a v4 of that file and the
commit below this one shipped a different v4. Both sides agree on the
number, so check-cache-bust cannot see it: one immutable url, two contents,
and whichever a browser cached first is the one it keeps. The order of the
two script tags is pinned in the suite as well, because error-message.js is
a plain script and failureText reads window.paramantErrors at call time: put
parashare.page.js first and the page quietly falls back to its own copy of
the sentence. Verified by sabotage.

The vm suite now loads the real frontend/js/error-message.js into the
context, so what it measures is the shipped sentence and not the fallback.
@Apolloccrypt
Apolloccrypt deleted the koper/ingelogde-schil branch September 5, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant