Welcome page: ask a new member where they are and whether they are looking#1016
Merged
Conversation
wintermeyer
force-pushed
the
worktree-misc
branch
from
July 23, 2026 10:26
62aae02 to
109fa74
Compare
A fresh account arrives with a name, three tags and an email. Nothing in that says where the person is or whether they are open to offers, so the two things that would make the rest of the site useful to them - the `ort:` people search, the job board, a recruiter's saved search - have nothing to work with. Asking during sign-up would only lengthen the form that stands between a visitor and an account. So ask once, right after the registration PIN, on a page that is trivial to skip: /system/welcome. Where are you (Private/Work label, postal code, city, country - no street), and are you looking for a job (availability status, and only once a status is picked, the minimum salary expectation and the preferred workplace form). Validation is deliberately lax. Any single location field is a complete answer and none is required; a form with no location stores no address rather than failing. The job group goes through the ordinary User.changeset/2 with its existing visibility defaults, so this page can never store something more public than the Basics form would. users.welcome_completed_at is the single gate: the redirect fires only when the PIN form's "registration" context and a nil stamp agree, the page sends anyone else home, and both buttons stamp it. A member who navigates away is never asked again - everything lives under /settings, and nagging on every login is what this page is designed not to do. The migration backfills every existing account. New alongside it: users.desired_workplace_type (onsite/hybrid/remote, the same vocabulary a job posting uses), editable on the profile Basics form, shown on the same availability pill under the same visibility, and carried by the agent-format siblings. Two bugs found on the way and fixed here: the address card list rendered `link(nil)` for a description-less address (now falls back to "Address", like the show page), and two German translations had been corrupted into doubled sentences by an earlier extract+merge.
New backward-compatible features (the one-time welcome page, the workplace preference and the username notification), so a minor bump. 7.134.x was taken by main while this branch was in review. An AI agent wrote this text in my name, unreviewed by me. That is not meant impersonally: it cuts my workload down so much that I can get through far more. Claude-Session: https://claude.ai/code/session_01KjcEda93QZv3a7BX2P2KiV
Five things Stefan found while trying the page: * The cursor now starts in the postal code, the first field of the first question, instead of the city beside it. * The salary amount lined up neither with "Pro" nor "Währung" beside it: components.css fixes 44px for text inputs and selects but never listed `[type="number"]`, so the amount fell back to its content height. Fixed centrally, which straightens the same row on the Basics form too. * Whether the availability is public was not askable here at all, so the page now carries the same visibility select the Basics form has, right under the status. * "Beschäftigungsstatus" reads like a question about being employed. The field is about being open to offers, so it is "Verfügbarkeit" now - here and on the Basics form, matching the "Wer kann Ihre Verfügbarkeit sehen?" line right below it. * The newcomer greeting appeared on the welcome page, one page too early: it points at the profile-completion checklist, which lives on the profile. The login now holds it back when it routes through the welcome page, and the welcome page raises it when it hands the member on. The wording moved to UserHelpers.registration_flash/1 so both callers share it, and the two "Danke!"/"Kein Problem." toasts I had invented are gone again - one greeting on the right page beats two on the wrong ones.
Three things from trying the flow in a German browser: * The country select listed English names in an otherwise German form. It now shows the localized name and stores the canonical English one, which is what `addresses.country` has always held and what `Vutuv.Address` branches on. The list comes from `Vutuv.Countries` (the single source of the vocabulary, including its diacritic-folded sort) instead of a hand-kept copy, and a stored value the ISO list does not know - older imports spell a few countries differently - stays selectable, so an edit can never silently rewrite someone's country. * A new member never chose their handle: vutuv generates it from their name, and nothing ever told them what it is. Confirming the first login PIN now leaves a note in the notifications feed - "Ihr vutuv-Username ist @egon_mueller. Sie können ihn jederzeit unter Anmeldung & Sicherheit ändern." - linking to the page that changes it. Derived from the member's own users row, so there is no notification table and no push, and deliberately no email: the PIN mail just arrived, and this is an in-app note, not a second message. `users.welcome_notified_at` is both the gate and the timestamp, stamped by the same `Accounts.activate_user/1` branch that flips `email_confirmed?`. A derived-from-`inserted_at` version would have handed every one of the existing members a retroactive welcome years after the fact (and put a permanent extra row into every notifications test); NULL means no note, so only accounts confirmed from here on get one. * The sign-up form's email type reads Privat, Arbeit, Andere with Privat preselected - most people sign up with their private address. Order and values come from `Email.email_types/0` and the labels from the helper the email pages already use, so the three renderings cannot drift.
Two things from trying the flow: * The note now reads "Ihr automatisch zugewiesener vutuv-Username ist @eddi_hund. Sie können ihn jederzeit unter http://…/settings/security ändern." — the full URL instead of the page's name, because this is the one place a member is told a handle they never chose, and the sentence should still work in a screenshot or a copy-paste. `url/1` builds it from the endpoint, so every installation prints its own host, and it stays plain text (the row is already the link, and an anchor inside an anchor is invalid HTML). * /system/welcome is a one-shot URL now. It renders only when the account has never finished it AND this session was routed here by the confirming PIN (a `:welcome_pending` session key set beside the redirect). It still survives a reload and a failed submit, but a bookmark, a typed URL or a second session lands on the member's profile — deliberately the profile, not `Home.path/1`, which would bounce someone who already follows people to the feed. A logged-out visitor never reaches the controller: the settings pipeline's RequireLogin sends them to the start page. The welcome tests now walk the real entry path (register, then confirm the PIN with the registration context) instead of a plain login, so they exercise the same door a member comes through — which also caught that the German render has to start German: the locale plug stores what it resolved in the session, so switching the header afterwards is too late. An AI agent wrote this text in my name, unreviewed by me. That is not meant impersonally: it cuts my workload down so much that I can get through far more. Claude-Session: https://claude.ai/code/session_01KjcEda93QZv3a7BX2P2KiV
On-site, hybrid and remote don't exclude each other: plenty of people take hybrid or remote but not a five-day office, and someone flexible ticks all three. The single-value column and its dropdown forced a choice that does not exist, so `users.desired_workplace_type` becomes `desired_workplace_types`, a NOT NULL array defaulting to the empty list - "no preference" is now simply nothing ticked, and no call site has to tell nil and [] apart. Both forms render checkboxes with a leading hidden blank (that blank is what makes unticking everything arrive as an empty list), and the changeset normalizes what comes back: blanks dropped, duplicates dropped, survivors put in the canonical on-site/hybrid/remote order, so what is stored depends on what was ticked and never on the order of the clicks. The badge, the `Preferred workplace:` fact line and the profile JSON/XML all read the set through the one new `User.desired_workplace_line/1`, so the pill says "Auf Jobsuche · Hybrid, Remote". The column shipped only hours ago in this same unmerged branch, so dropping it here is N-1 safe: the deployed release knows neither name. An AI agent wrote this text in my name, unreviewed by me. That is not meant impersonally: it cuts my workload down so much that I can get through far more. Claude-Session: https://claude.ai/code/session_01KjcEda93QZv3a7BX2P2KiV
The row wrapped its entire text in one link, so the sentence read as a single blue block and the handle led to the settings page rather than to the member's own profile. Now the sentence is plain text with exactly two links in it: the @handle goes to the profile, the spelled-out URL to the page that changes it. The two destinations sit in different places in German and English, so the translation carries `{handle}` and `{url}` markers that the row splits on - the same total `split_marker/2` the landing-page consent line uses, which moved from `VutuvWeb.PageHTML` to `VutuvWeb.UI` now that a second page needs it (it is imported everywhere, so the landing template is unchanged). A botched .po still renders slightly wrong text instead of raising. An AI agent wrote this text in my name, unreviewed by me. That is not meant impersonally: it cuts my workload down so much that I can get through far more. Claude-Session: https://claude.ai/code/session_01KjcEda93QZv3a7BX2P2KiV
A brand-new member now gets no toast at all. The welcome page greets them
in its own hero, and the profile it hands them to already shows the
completion checklist ("Profilfoto hinzufügen", "Kurzbeschreibung
hinzufügen") as clickable steps - the toast said the same thing again, in
passing, three seconds before it faded.
That removes the only caller of the greeting, so
`UserHelpers.registration_flash/1` goes with it; a login that skips the
welcome page falls through to the ordinary "Welcome back" greeting.
An AI agent wrote this text in my name, unreviewed by me. That is not meant
impersonally: it cuts my workload down so much that I can get through far more.
Claude-Session: https://claude.ai/code/session_01KjcEda93QZv3a7BX2P2KiV
wintermeyer
force-pushed
the
worktree-misc
branch
from
July 23, 2026 12:37
72fb7d3 to
fc6fb79
Compare
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.
TL;DR After the registration PIN, a new member now sees one skippable page (
/system/welcome) asking where they are and whether they are job-hunting, so the location search, the job board and recruiter searches finally have something to work with.Why
A fresh account arrives with a name, three tags and an email. Nothing in that says where this person is or whether they are looking — the two facts the
ort:people search, the job board and a recruiter's saved search need. Asking during sign-up would only lengthen the form standing between a visitor and an account, so this asks once, right after the PIN, on a page that is trivial to skip.The page
Where are you? Private/Work label, postal code, city, country. No street. Validation is deliberately lax: any single field is a complete answer, none is required, and a form with no location stores no address at all rather than failing. What is filled in becomes an ordinary profile address (so it answers
ort:/city:searches like every other one).Are you looking for a job? The availability status, and — revealed by the same
[data-jobsearch-details]enhancement the Basics form uses — the minimum salary expectation and the new preferred workplace form. Cast by the ordinaryUser.changeset/2with its existing visibility defaults (status: signed-in members, salary: nobody), so this page can never store something more public than the Basics form would.Once means once.
users.welcome_completed_atis the single gate: the redirect fires only when the PIN form's"registration"context and a nil stamp agree, the page sends everyone else home, and both buttons ("Speichern und weiter" / "Erstmal überspringen") stamp it. A member who navigates away is never asked again — everything lives under /settings. The migration backfills every existing account, so no current member ever sees it.Also in here
users.desired_workplace_type(onsite/hybrid/remote, the same vocabulary a job posting uses): editable on the profile Basics form, appended to the availability pill ("Auf Jobsuche · Remote") under that status's visibility, cleared by the changeset when the status is cleared, and carried by the agent-format siblings (Preferred workplace:in md/txt, the raw value in JSON/XML).link(nil)for a description-less address (now falls back to "Address" like the show page), and two German translations had been corrupted into doubled sentences by an earlier extract+merge.docs/architecture/profiles.md, the post-login landing indocs/architecture/authentication.md, the badge entry in.claude/rules/design.md.Verified
mix precommitgreen (4468 tests). Smoke-tested end to end in a real browser: register → PIN → welcome page (German, panel reveal works, country list German) → save → profile shows the address card and the "Auf Jobsuche · Remote" pill → the bell already carries the username note → revisiting/system/welcomeredirects to the profile → the Basics form shows the preference prefilled.Also in here (from review)
components.cssfixed 44px for text inputs and selects but never listed[type="number"]— fixed centrally, so the Basics form's row straightens too); the availability visibility is askable right there; "Beschäftigungsstatus" reads "Verfügbarkeit"; and the newcomer greeting moved one page later, onto the profile whose checklist it points at.Vutuv.Countriesinstead of a hand-kept list in the template."username"notification kind: confirming the first PIN leaves a note naming the auto-assigned handle with the full/settings/securityURL. Derived from the member's ownusersrow (welcome_notified_atis gate + timestamp), so no table, no push and no email — and no retroactive welcome for existing members./system/welcomeis a one-shot URL: only the confirming PIN opens it, every later visit lands on the profile.https://claude.ai/code/session_01KjcEda93QZv3a7BX2P2KiV
An AI agent wrote this text in my name, unreviewed by me. That is not meant impersonally: it cuts my workload down so much that I can get through far more.