Add the sign-up page and the first-run checklist
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
This commit is contained in:
+13
-2
@@ -252,6 +252,11 @@ func handleLogout(db *sql.DB, publicURL string) http.HandlerFunc {
|
||||
type meResponse struct {
|
||||
User any `json:"user"`
|
||||
HasPassword bool `json:"has_password"`
|
||||
|
||||
// OnboardingDismissed is whether this person has put the first-run
|
||||
// checklist away. Per user rather than per browser: somebody who finishes
|
||||
// setting up on a laptop should not be nagged again on their phone.
|
||||
OnboardingDismissed bool `json:"onboarding_dismissed"`
|
||||
}
|
||||
|
||||
// handleMe says who the caller is. The web UI calls it on load to decide
|
||||
@@ -265,9 +270,15 @@ func handleMe(db *sql.DB) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
var hash sql.NullString
|
||||
var dismissed *int64
|
||||
db.QueryRowContext(r.Context(),
|
||||
"SELECT password_hash FROM users WHERE id = $1", caller.ID).Scan(&hash)
|
||||
respond(w, http.StatusOK, meResponse{User: user, HasPassword: hash.Valid})
|
||||
"SELECT password_hash, onboarding_dismissed_at FROM users WHERE id = $1",
|
||||
caller.ID).Scan(&hash, &dismissed)
|
||||
respond(w, http.StatusOK, meResponse{
|
||||
User: user,
|
||||
HasPassword: hash.Valid,
|
||||
OnboardingDismissed: dismissed != nil,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user