Add the sign-up page and the first-run checklist #20

Merged
niklas merged 1 commits from onboarding-ui into main 2026-09-21 08:54:30 +00:00
Owner

Closes #7, and with it the last sub-issue of #1. The API could create accounts from invite links after #19; this is the part somebody can actually use.

The sign-up page

/signup is the one route that works without a session. It asks the server what it may offer before showing anything:

  • a good invite link names the team it leads to;
  • a bad one says so before somebody picks a password, not after;
  • an invite-only server with no link says that, instead of presenting a form it will refuse.

The login card only offers "create one" when sign-up is open — no point advertising a door nobody can walk through. Signing up signs you in and lands on the queue, because the alternative is a form saying "now go and log in" about the credential just chosen.

The checklist

Four things have to be true before an alert reaches a phone — a notification topic, somebody on the rota, an alert source, and an alert that has actually arrived. On a fresh install none of them are, and until now nothing said so.

It's computed from the data, not from stored progress. A topic is set or it isn't; an integration exists or it doesn't. So it can't claim a step is done when it isn't, and it comes back by itself if somebody deletes their integration a month later. The only stored state is the dismissal, which is per user, not per browser — finishing on a laptop shouldn't leave the phone nagging.

The test push

The topic step is the only one the checklist can finish by itself, and the only proof that counts is a phone buzzing. POST /api/me/notify/test publishes directly rather than through the outbox, which requires an incident this deliberately doesn't have.

Its failure is the useful part. A wrong topic, a token ntfy rejects, and an ntfy that's down all look identical from the phone — which is silence — so the error comes back to the browser instead.

Verified

make fmt lint test helm-lint green with -race, every UI import resolves, and the whole path driven against a live server with a real ntfy stand-in:

  1. owner mints an invite → URL with a 7-day expiry
  2. sign-up page reports it valid and names the team
  3. invitee signs up → signed in, not an admin, member of that team
  4. the checklist's four questions all answer correctly on a fresh install
  5. test push → 400 with no topic, 204 with one, and the ntfy log shows PAGED terdut-owner | terdut test
  6. dismissal persists

Not in here

No screenshots — I can't drive a browser here, so the layout is the one thing unverified. Worth a click-through before merging: make test-db, run the server, and open /signup?invite=… with a link you minted.

The in-app Alertmanager instructions that #7 also listed are already in the Team tab from #18 — the integration create flow shows the URL and a ready-to-paste receiver snippet. The checklist links there rather than growing a second copy.

https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7

Closes #7, and with it the last sub-issue of #1. The API could create accounts from invite links after #19; this is the part somebody can actually use. ### The sign-up page `/signup` is the one route that works without a session. It asks the server what it may offer **before showing anything**: - a good invite link names the team it leads to; - a bad one says so *before* somebody picks a password, not after; - an invite-only server with no link says that, instead of presenting a form it will refuse. The login card only offers "create one" when sign-up is open — no point advertising a door nobody can walk through. Signing up signs you in and lands on the queue, because the alternative is a form saying "now go and log in" about the credential just chosen. ### The checklist Four things have to be true before an alert reaches a phone — a notification topic, somebody on the rota, an alert source, and an alert that has actually arrived. On a fresh install none of them are, and until now nothing said so. **It's computed from the data, not from stored progress.** A topic is set or it isn't; an integration exists or it doesn't. So it can't claim a step is done when it isn't, and it comes back by itself if somebody deletes their integration a month later. The only stored state is the dismissal, which is **per user, not per browser** — finishing on a laptop shouldn't leave the phone nagging. ### The test push The topic step is the only one the checklist can finish by itself, and the only proof that counts is a phone buzzing. `POST /api/me/notify/test` publishes directly rather than through the outbox, which requires an incident this deliberately doesn't have. **Its failure is the useful part.** A wrong topic, a token ntfy rejects, and an ntfy that's down all look identical from the phone — which is silence — so the error comes back to the browser instead. ### Verified `make fmt lint test helm-lint` green with `-race`, every UI import resolves, and the whole path driven against a live server with a real ntfy stand-in: 1. owner mints an invite → URL with a 7-day expiry 2. sign-up page reports it valid and names the team 3. invitee signs up → signed in, not an admin, member of that team 4. the checklist's four questions all answer correctly on a fresh install 5. test push → `400` with no topic, `204` with one, and the ntfy log shows `PAGED terdut-owner | terdut test` 6. dismissal persists ### Not in here No screenshots — I can't drive a browser here, so the layout is the one thing unverified. Worth a click-through before merging: `make test-db`, run the server, and open `/signup?invite=…` with a link you minted. **The in-app Alertmanager instructions** that #7 also listed are already in the Team tab from #18 — the integration create flow shows the URL and a ready-to-paste receiver snippet. The checklist links there rather than growing a second copy. https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
niklas added 1 commit 2026-09-21 08:47:33 +00:00
Add the sign-up page and the first-run checklist
CI / chart (pull_request) Successful in 1s
CI / security (pull_request) Successful in 13s
CI / test (pull_request) Successful in 2m28s
b39aac36b7
Second half of #7. The API could create accounts from invite links since
the last change; this is the part somebody can actually use.

/signup is the one route that works without a session. It asks the server
what it may offer before showing anything: an invite link that is good
names the team it leads to, a link that is not says so before somebody
picks a password rather than after, and an invite-only server with no
link says that instead of presenting a form it will refuse. The login
card only offers "create one" when sign-up is open, so the door nobody
can walk through is not advertised.

Signing up signs you in and lands on the queue, because the alternative
is a form saying "now go and log in" about the credential just chosen.

The checklist is the other half. Four things have to be true before an
alert reaches a phone -- a notification topic, somebody on the rota, an
alert source, and an alert that has actually arrived -- and on a fresh
install none of them are. It sits above the queue until they are.

It is computed from the data rather than from stored progress: a topic is
set or it is not, an integration exists or it does not. That means it
cannot claim a step is done when it is not, and it comes back by itself
if somebody deletes their integration a month later. The only stored
state is the dismissal, which is per user and not per browser --
finishing on a laptop should not leave the phone nagging.

The topic step is the only one the checklist can finish itself, and the
only proof that counts is a phone buzzing, so there is a test push.
POST /api/me/notify/test publishes directly rather than through the
outbox, which requires an incident this deliberately does not have. Its
failure is the useful part: a wrong topic, a rejected token and an ntfy
that is down all look identical from the phone, which is silence, so the
error comes back to the browser instead.

Verified against a live server with a real ntfy stand-in, the whole path:
an owner mints an invite, the sign-up page reports it valid and names the
team, the invitee signs up and is signed in as a member of that team, the
checklist's four questions answer correctly on a fresh install, a test
push is refused with no topic and delivered with one -- "PAGED
terdut-owner | terdut test" -- and the dismissal survives a reload.

Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
niklas merged commit a92da7dcc0 into main 2026-09-21 08:54:30 +00:00
Sign in to join this conversation.