Skip to content

feat(web,landing): lead with mapping, and say where accounts come from - #491

Merged
efiten merged 6 commits into
efiten:masterfrom
khagele:feat/490-account-discovery
Aug 26, 2026
Merged

efiten merged 6 commits into
efiten:masterfrom
khagele:feat/490-account-discovery

Conversation

@khagele

@khagele khagele commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

A visitor asked in Discord where to make an account for the map and could not find one. There is no answer to find: the login card offers login and nothing else, and the map cannot register anyone anyway, since /api/auth/register rejects a body without a companion_pubkey (server/internal/httpapi/auth.go). Only the RX webapp has one.

That gap turned out to be a positioning one. The RX webapp is the product: you pair a companion to it and map the mesh, and the map is where every mapper's receptions meet. Both sites led with the map instead, which sends a first-time visitor to a login screen for data they have no part in yet.

The map

  • #rx-cta, a Start mapping link in the bar next to Log in, opening rx.mesh-hunter.eu.
  • The login card gains a footer line: no account yet, register in the RX webapp, it links the companion your phone is paired to, an admin verifies you as a member afterwards. Inside the <form>, so the focus trap includes it.
  • The walkthrough explains the button, is anchored to it, and its panel now says where mapping happens.
  • The guest notice goes back to "Log in to see more" (see below).

The landing

  • Nav lists the RX webapp before the map, on all four pages.
  • Hero leads with Start mapping and names the companion pairing; "See the map" is secondary.
  • The RX webapp section comes before the map section, and a new "Get an account" section carries the four steps through to admin member verification, out of the small print at the foot of the page.
  • Mapper and hunter stay both words in the copy: mapping is what the radio does for you, hunting is going after one node. Role names are untouched.

Two measurements, because #bar wraps by content width

  • Naming the RX webapp in the guest notice took it from 703px to 800px, which pushed the SF counts onto a row of their own at 1440px. The notice is back to its old wording; the button beside it carries that message now.
  • #rx-cta costs no bar row at 1024/1280/1440. At 375px its long label does, so it shortens to "Map it" below the 640px breakpoint: that row holds ~80px, and "Start mapping" is 109px.

Checks

  • npx vitest run in web/ (413), app/ (716) and landing/ (35); npx playwright test in web/ (159); eslint clean.
  • New tests were red first, and the reworded guest-notice assertion was mutation-checked by putting the long copy back.
  • Verified in the browser at 375/1024/1280/1440 in both themes: bar height unchanged with the button present, the login card footer renders, the fragment link to Get an account clears the sticky header.

Closes #490

@khagele
khagele force-pushed the feat/490-account-discovery branch from b5a9b00 to 783d58c Compare August 25, 2026 17:12
@khagele

khagele commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto master for #497. The only conflict was changelog.json, where #497 added an entry of the same date; both are kept, both copies byte-identical.

One thing that changed with the rebase and is worth stating rather than leaving to be noticed: #497 filled the toolbar up, so #rx-cta now costs a bar row where it cost none before. Measured after the rebase:

viewport without the button with it
1440 83 px 119 px
1280 119 px 156 px

Short label and icon-only were both measured too: at 1440 the bar wraps either way, so the row is the price of having the control at all, not of its wording.

Kept, deliberately. The map is downstream of the RX webapp, and the one visitor this whole PR exists for is the one who has never mapped — 36 px of map height is what that costs on desktop. The mobile breakpoint still shortens the label to "Map it", where the bar has the room.

web 423, landing 35, playwright 166, eslint clean.

khagele and others added 6 commits August 26, 2026 06:51
The login card offered logging in and nothing else, so a visitor who had
never made an account had nowhere to go. The map cannot register anyone
either: /api/auth/register rejects a body without a companion_pubkey, so
only the RX webapp can. Reported in Discord by someone who went looking
for a sign-up page and found none.

Three places now say it. The login card carries a footer line linking
rx.mesh-hunter.eu; the guest notice names it instead of ending at "Log in
to see more"; and the landing gets a "Get an account" section with the
four steps through to admin member verification, moved out of the small
print at the foot of the page.

The notice wording is length-bound: #bar is flex-wrap and the notice
shares its row with the SF counts, so a longer line costs the map a bar
row. Measured at 375/1024/1280/1440: bar height unchanged.

Closes efiten#490
The RX webapp is the product: you pair a companion to it and map the
mesh, and this map is where every mapper's receptions meet. Both sites
led with the map instead, which sends a first-time visitor to a login
screen for data they have no part in yet.

The landing now opens on "Start mapping", puts the RX webapp section
before the map, and says the pairing in the hero; the nav on all four
pages lists the RX webapp first. The map gains #rx-cta, a Start mapping
link in the bar next to Log in, and the walkthrough explains it and says
where mapping happens.

Mapper and hunter stay both words, as the landing now states: mapping is
what you do with the radio, hunting is going after one node, and it is
one account either way. The role names are untouched.

Two measurements. #rx-cta costs no bar row at 1024/1280/1440, and at
375px only with its long label, so it shortens to "Map it" below the
640px breakpoint (measured: the row holds ~80px, "Start mapping" is
109px). The guest notice goes back to "Log in to see more" for the same
reason -- naming the RX webapp there cost a row, and the button beside
it now carries that message.

Closes efiten#490
That line was our own vocabulary note, written into the page by mistake.
The reader does not need the distinction spelled out; the steps above it
already say what happens.
CI caught the account callout 900px from the button it describes. #bar is
flex-wrap, so Start mapping and Log in share a row at some widths and sit
on separate rows at others, and a callout anchored to both is placed
against the union of the two: with the buttons split across rows, the
union spans the whole bar and the box lands in the empty middle. The
widths that reproduce it depend on font metrics, so a Mac never showed
it.

Each button now has its own box. The sweep that would have caught this
goes with it, and it forces the row break with a margin instead of
hoping a viewport width produces one -- mutation-checked by restoring
the shared anchor, which fails only the forced-split cases.
The sweep added with the callout fix tried to force Start mapping and
Log in onto separate rows with a 9999px margin, so the union placement
could be proved wrong on any machine. CI failed on that case: at 1440px
the forced layout puts the account box somewhere the assertion does not
allow, and neither placement is the one a reader would ever see. A
narrower bar does not work either -- the tour drops to its inline panel
before the split happens, so there is no box left to measure.

The width sweep stays and the assertion is unchanged. What actually
rules the shape out is the one-target-per-button test in
onboarding.test.js, and both comments now say so instead of claiming the
sweep would have caught it.
parity.test.js asserts that the dates in changelog.json do not increase
down the file. Master gained several notes dated 2026-08-25 while this PR
was open, and the rebase left this branch's own 08-25 entry sitting below
an 08-24 one.

Order only, in both copies. Same entries, same ids, same text, so nobody's
seen-state moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@khagele
khagele force-pushed the feat/490-account-discovery branch from 783d58c to 26547cb Compare August 26, 2026 04:54

@efiten efiten left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the code; the product call is the maintainer's. Everything I checked holds up, and the one thing I would ask for is a re-run rather than a change.

The green here is stale, and it matters for this PR in particular

CI last ran at 04:56; #528 merged at 09:39 and added a Sender id toggle to #bar (76px). This PR adds #rx-cta (106px) plus a seventh callout. That is the same density that broke three onboarding.spec.js tests on the runner yesterday while passing on Windows, so the two changes land on exactly the shared resource.

I merged current master into this branch locally and ran it. It holds:

bar height   1024: 161   1280: 125   1440: 86
widths       rx-cta 106   f-idclass-wrap 77   f-types 882
onboarding   17 passed
web          479 unit + 182 playwright passed
landing      35 passed

Local Windows metrics are the narrow ones though, and the runner's are what caught it last time, so please merge master in and let CI say so rather than taking my machine's word for it.

(landing.test.js failed for me at first on a missing fast-xml-parser -- my checkout, not this branch. Clean after npm ci.)

Checked, and each one holds

  • "Inside the <form>, so the focus trap includes it." Accurate: login.js:53 is trapFocus($('login-form')), so the trap really is scoped to the form, and login.test.js slicing between id="login-form" and </form> pins precisely the thing the claim rests on rather than merely the link's presence.
  • The guest notice. The diff adds a longer wording and then takes it back out, with the measurement recorded (972px against a row that holds ~800px). Net effect on the notice is nothing, which is the right outcome; worth saying out loud that the PR body's "goes back to" is the whole story and not a leftover.
  • The responsive label. .rx-cta-long / .rx-cta-short swap on display, which drops the hidden one out of the accessibility tree too, so a screen reader gets one label rather than "Start mappingMap it". No aria-hidden needed.
  • One callout per control. Splitting wb-co-mapping out rather than anchoring one box to the union of rx-cta and auth-btn is right, and the comment gives the reason plus the width it was found at.
  • Both link targets resolve: rx.mesh-hunter.eu and mesh-hunter.eu return 200, rel="noopener" present on every new target="_blank".

Not a blocker, one thing left unsaid

The login card now tells a visitor to register in the RX webapp and that "an admin verifies you as a member afterwards". Nothing tells them how long that takes or how they would know it happened. Not this PR's job to build, but it is the next dead end on the same path, and worth an issue rather than being discovered by the next person who asks in Discord.

@efiten

efiten commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Filed the open end I mentioned as #530, so it is a decision rather than a discovery. Not a blocker for this PR.

efiten added a commit that referenced this pull request Aug 26, 2026
…through (#531)

Closes #530

## What was happening

Two surfaces promise it -- `guestNotice('hunter')` and, since #491, the
login card -- and the promise ended at a role change plus an audit row
(`admin.go:133`). Nothing else happened.

Worse, the role is read once at load (`login.js:49`, `map.js`
`applyRole`). The person this is about is a hunter waiting to be
verified, and what they do is leave the map open. Before this, that page
kept saying "Hunter view" and drawing coarse hex however long ago an
admin had acted, so the honest reading was that nothing had happened.

## Why the map has to tell them, not mail

Mail was configured all along (`brevoSmtpHost`, `brevoSmtpPort`,
`mailFrom` and the Brevo user are all set in the production config).
Reach is the problem. From `users` on the live DB:

| | |
|---|---|
| users | 16 |
| with an email on file | 5 |
| hunters waiting for member | 4 |
| ...of those, with an address | **1** |

`register` rejects a missing `companion_pubkey` but never checks `Email`
before `CreateUser` (`auth.go:233`), so an address is optional and most
people do not give one. Mail reaches one of the four people waiting
today. It is the addition; the map is the mechanism.

## The map

`web/rolechange.js` holds the comparison and stores the last-seen role
in `localStorage`, the shape `whatsnew.js` already uses for its
last-seen entry. No schema, no address.

Two moments, one comparison: `applyRole` covers first load and login,
and a `focus` listener re-reads `/api/auth/me` for the page that is
already open. It re-applies only when the role actually moved --
`applyRole` re-runs the range and layer gates, and doing that on every
tab switch is cost for nothing.

- **Silent on a first visit.** With nothing stored, a promotion cannot
be told from an arrival, and congratulating a member of six months on
becoming one reads as a bug. The first load records; it does not
announce.
- **Silent on the way down.** A demotion is the admin's to explain, and
the standing notice already changes to say what they can see.
- The notice is `role="status" aria-live="polite"` and dismissible, and
it is a separate element from `#guest-notice`: that one describes a
standing state, this one is news.

## The mail

`SendMemberVerified` joins the two methods on `Mailer`, sent
synchronously like the invite mail beside it in the same file.

The condition is **crossing** the member line, not merely rising. My
first version was "rises and is at least member", and a test caught that
it mails a member promoted to admin -- someone who has seen everything
for months.

## Verified

- `web/` 483 unit + 184 playwright; `server/` `go vet` + `go test
./...`; `app/` 782 (untouched, checked anyway); eslint clean.
- Mutation-checked, three mutations, each killed: mail on any rise
rather than on crossing (2 Go subtests), mail without an address (1
subtest), and announcing on a first visit (1 unit + 3 e2e).
- The e2e drives the case that matters: role flips server-side while the
page sits open, nothing is touched, and the notice appears while the
standing notice goes.

## Note on the history

`59b5da4` landed four of fourteen files: a `git add` with one bad
pathspec stages nothing at all, and I pushed before noticing. `0d07ce4`
completes it. Squash-merged the two are one commit; flagging it rather
than force-pushing over it.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

@efiten efiten left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. The three calls in here are the maintainer's, and all three are accepted (2026-08-26): the RX webapp leading over the map, "mapper" alongside "hunter" in the copy, and the account path spelled out.

Re-verified against current master, which has moved a long way since this branch's CI ran at 10:11 (#477, #500 and three releases). Merged master in locally and ran the lot:

web       510 unit, 193 playwright
landing    35

Onboarding included, so the extra bar control still costs no callout.

One thing said plainly rather than hidden: the first of my four playwright runs reported 1 failed / 192 passed, and I lost the output before identifying which test it was. The three runs after it were clean at 193. So it looks like a one-off flake rather than a fault in this branch, but CI runs with --fail-on-flaky-tests and is the arbiter, not my laptop. If it turns up there, it is that, not something new.

The code review from my earlier pass stands unchanged: the focus-trap claim holds (login.js:53 traps on #login-form, and the test slices exactly that), the guest-notice wording is added and then taken back out with the 972px measurement recorded, the responsive label swaps on display so a screen reader gets one label, and both link targets return 200 with rel="noopener".

The open end this leaves, that nothing tells a hunter when the verification actually comes through, is closed separately in #530 and shipped.

@efiten
efiten merged commit f9f326c into efiten:master Aug 26, 2026
6 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 26, 2026
efiten pushed a commit that referenced this pull request Aug 26, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>app: 1.20.0</summary>

##
[1.20.0](app-v1.19.1...app-v1.20.0)
(2026-08-26)


### Features

* **app:** search the target sheet by name or id prefix
([#477](#477))
([c69c57c](c69c57c))
* **web,landing:** lead with mapping, and say where accounts come from
([#491](#491))
([f9f326c](f9f326c))
* **web,server:** ignore a sender from the map
([#500](#500))
([1b62809](1b62809))
</details>

<details><summary>server: 1.8.0</summary>

##
[1.8.0](server-v1.7.0...server-v1.8.0)
(2026-08-26)


### Features

* **web,server:** ignore a sender from the map
([#500](#500))
([1b62809](1b62809))
</details>

<details><summary>web: 1.16.0</summary>

##
[1.16.0](web-v1.15.1...web-v1.16.0)
(2026-08-26)


### Features

* **app:** search the target sheet by name or id prefix
([#477](#477))
([c69c57c](c69c57c))
* **web,landing:** lead with mapping, and say where accounts come from
([#491](#491))
([f9f326c](f9f326c))
* **web,server:** ignore a sender from the map
([#500](#500))
([1b62809](1b62809))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@khagele
khagele deleted the feat/490-account-discovery branch September 12, 2026 16:42
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.

web,landing: no path from the map to creating an account

2 participants