Repository navigation
POC G: the A + B + C combination for post-login landing - #677
Closed
MichaelDimmitt wants to merge 10 commits into
Closed
MichaelDimmitt wants to merge 10 commits into
MichaelDimmitt wants to merge 10 commits into
Conversation
Issue huddlz-hq#670 asks for several competing proofs of concept, each as its own draft PR, so one can be chosen and the rest declined. This is the shared brief those PRs are built from: the problem, what the code already does, one section per variant with its own task list, and the rubric they are all judged against. Records three findings that shape the variants: the return_to plumbing is already complete and safe but only the huddl page's RSVP button produces such a link; the agenda already loads both scopes every render to feed its chip counts; and the agenda has no query for huddlz outside a person's groups. Part of huddlz-hq#670
The POC variants are compared partly on whether people want to use them, so a variant that looks unfinished loses Desirable and Delightful for reasons unrelated to its idea. Ask each one to polish the surface it changes, and keep the scope discipline pointed at everything else. Part of huddlz-hq#670
A person who arrives on a huddl page from a link or a QR code and reaches for the header's "Sign in" loses the huddl they came for: the header's link is a bare /sign-in, so they land on the agenda, which by definition does not list a huddl they have not RSVP'd to yet. These scenarios describe the journey the person expects. Four of the five fail: signing in or signing up from the header does not come back. The fifth passes already and is meant to — it documents that AuthReturnTo.validate/1 refuses an off-site return destination before any of this work. - Cover sign-in from a huddl page and from a group page - Cover sign-up, where email confirmation intervenes - Cover a tagged arrival, so join attribution survives the sign-in hop Part of huddlz-hq#670
The return_to plumbing was already complete and already safe. What it was missing was reach: only the huddl page's RSVP call to action and the group page's join button produced a return destination. The topbar's "Sign in" and "Sign up" — the buttons that look like the way to sign in — were bare paths, so the two sign-in links on the same page disagreed about whether the person's intent was worth keeping. Layouts.app now takes a return_to a page can fill in, and the topbar's sign-in and sign-up carry it. Validation happens inside the component rather than at the call sites, so no page can hand the header an off-site destination and any future caller is covered without having to remember. The huddl page and the group page pass the destination they already computed for their own buttons, so the header and the in-page call to action cannot drift apart. On the group page that address still carries the join source tag, so attribution survives the trip through auth. - Add a validated return_to attr to Layouts.app - Point the topbar's sign-in and sign-up at it - Pass each page's existing return destination from huddl and group show - Record the measured click count and entry point audit in the POC spec Part of huddlz-hq#670
Six scenarios for variant B of the post-login landing POCs: the agenda opening on everything a person's groups have scheduled rather than only their own RSVPs, the RSVPs filter staying one click away, the filter counts staying right from either filter, the person in zero groups, and the preference that opts back out. These fail against the current default, which is the point: they are the outside-in specification the implementation follows. Part of huddlz-hq#670
The agenda was the signed-in home but opened on a person's own RSVPs, so the huddl someone arrived for was absent from it by definition: their groups had it scheduled, and they had not responded to it yet. Seeing it cost a click on the Groups chip, every visit. Groups is now the filter the agenda opens on, from a new per-person preference that defaults to it. - `parse_scope/2` takes the person's `agenda_landing` as the fallback. An explicit `?scope=` in the address still wins, so both filters stay linkable and bookmarkable. - `calendar_path/2` now omits whichever scope matches the person's preference rather than a hardcoded `:mine`. Without this the RSVPs chip would link to a bare `/agenda`, which resolves back to Groups, making the chip a no-op and the filter unreachable. - the chip counts needed no change: `handle_params/3` already computes both scopes on every render. - the empty states say what is true under the Groups filter. "huddlz you RSVP to show up here" was only half the story, so an empty Groups agenda now names the action that fills it: joining a group. One existing scenario asserted the old starting point and is reframed to start from an explicit `?scope=mine`; its behaviour is unchanged and still covered. Part of huddlz-hq#670
Groups is the better default, but it is not everyone's. `agenda_landing` is a per-person choice between everything their groups have scheduled and just their own RSVPs, shaped on `theme_preference`: an atom-typed attribute with a default, a narrow update action accepting one field, and a policy allowing a person to update only their own. It is a filter preference, not a destination. The agenda stays the one signed-in home, so the choice never changes which page someone lands on, only what that page opens showing. An enum of two atoms cannot become a bad redirect the way a stored path could. - Profile gains an Agenda panel: two cards that save on choice, the way the appearance menu does. Validation is the Ash enum's, so there is no HTML5 `required` and a bad value comes back as an error. - the post-auth redirect honours it by adding it as the last candidate in the existing `return_to` chain, through `AuthReturnTo.validate/1`. An explicit `return_to` still wins: someone who clicked RSVP on a huddl goes back to that huddl, not to their agenda. Signing out keeps the old user-less default. Part of huddlz-hq#670
The agenda only ever showed huddlz the person already has a relationship to, so somebody who signs in looking for something to go to has to leave for /discover. This adds a third scope beside RSVPs and Groups, showing upcoming huddlz near their home search location that they have not responded to, with no filters to configure. Nearby reuses Discover's query whole rather than adding a second search path: the same :search action, the same upcoming date filter and the same PostGIS distance filter, with no relationship argument so it reaches huddlz the person has nothing to do with. Results are put into the agenda's own entry shape, so the existing day grouping, status logic and list rendering draw them unchanged — a stranger's huddl reads as "No RSVP". - parse_scope/3 now takes the person's landing preference and the view mode: nearby is agenda-only, since a stranger's huddl is not a calendar fact and does not belong in a month cell. scope_param/3 drops the param from links leaving the agenda, and keeps omitting whichever scope the person already lands on. - handle_params/3 is split: it loads the two synchronous scopes and sets URL state, and put_view_state/1 derives everything the views draw from whichever scope is selected. handle_async/3 calls put_view_state/1 again, so the async scope redraws through exactly the same path as the synchronous ones. A stale-ref guard drops a late reply. - The Nearby chip carries no count. RSVPs and Groups are counted by comparing two in-memory lists; a paginated search cannot be counted the same way, and a number arriving a beat late reads as a bug. - Three states the other scopes do not have, each answered in place: no saved location points at /profile, a search in flight draws placeholder rows in the agenda's shape, and nothing in range offers Discover. - The window is the agenda's existing @agenda_days, so the scopes stay comparable; the search overfills it, so Nearby never paginates. Conflict resolution against the landing preference, for the combination: the two changes meet in parse_scope, scope_param and the agenda's empty states. parse_scope and scope_param each take both the preference and the view mode rather than one or the other, so an explicit ?scope= still wins over the preference and nearby still stays off the calendar grid. first_run? moves behind a helper: it now means "this person has nothing of their own and their groups have nothing on either", so the groups filter's first-run copy stays reachable and Nearby — which has its own three states — never draws a first-run panel on top of them. Part of huddlz-hq#670
The return destination and the agenda landing preference each answer "where do I go after sign in", and combined they can both be true at once. Nothing yet said which one wins, so the ordering was an accident of the list in `return_to/2` rather than a decision. Four scenarios over the combined journey: - arriving for one huddl beats the person's own preference, so a return destination stays worth more than a default; - arriving with nothing in mind opens the filter they chose; - arriving with nothing in mind and no preference lands on their groups and still reaches a huddl outside them, without leaving the page; - all three filters stay one click from wherever they land, so the combination has not made one of its own parts unreachable. Both halves of the precedence were checked by inverting them: ordering the preference ahead of the return destination fails the first scenario, and defaulting back to RSVPs fails the third. Part of huddlz-hq#670
Variant G's row, task list and Findings. The result worth reading is the `first_run?` collision: C narrowed first-run to the RSVPs filter while B wrote new first-run copy for the Groups filter, so cherry-picking both left B's copy reachable by no state. Git merged both hunks without a conflict marker — only a unit test found it. The verdict is coherent with one seam, and the seam is Nearby: the chip row presents three peers, but two are counted views of the person's own commitments and the third is an uncounted search. Part of huddlz-hq#670
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.
Summary
Variant G of #670: cherry-picks A (#672), B (#675) and C (#676) onto one branch to test
#670's claim that "1 + 2 + 3 may cover most cases without a new flow."
The combined agenda, and where the three pieces land on it:
One precedence ordering, applied in the two places that decide a landing:
A return destination is something the person did a moment ago; a preference is something
they said once; a default is something nobody said. Recency of intent wins.
Verdict: coherent, with one seam. All three ideas were already expressed in the
agenda's existing vocabulary — a destination, a default for it, and a third value for the
filter it opens on. None invents a page, a modal or a step, and the combination introduced
no new terminology. The seam is Nearby: the chip row shows three peers, but RSVPs and
Groups are counted from two in-memory lists while Nearby is an uncounted PostGIS search
with a loading state. It is the right seam to have, and you can feel it on the chip.
The conflicts that were real
calendar_live.exandapp.csswere the overlap. The CSS merged clean — zero selectoroverlap between B and C. Six conflict regions in
calendar_live.ex; five were plumbing,two functions growing an argument rather than choosing a side:
The sixth was a genuine behavioral collision, and git merged both halves of it without a
conflict marker. C narrowed
first_run?toown_entries == [] && scope == :mine, becauseunder C the agenda still opened on RSVPs. B independently wrote new first-run copy for the
Groups filter. Together, C's narrowing makes B's copy reachable by no state — the only
scope that can draw a first-run panel is the one whose copy B did not change:
Neither PR is wrong alone. The conflict exists only in the combination, and no merge marker
pointed at it — a unit test did. This is the class of problem G exists to find, and it is
invisible from any single variant's PR.
Also reconciled: a person cannot choose to land on Nearby, and
agenda_landingdeliberately has no
nearbyvalue. Nearby is a search, not a fact — no saved location,search in flight, nothing in range. Landing somebody there means the signed-in home can
open on a spinner or on "tell us where you are", a question instead of an answer. Nearby
stays one click from wherever the person lands.
Evidence
Screenshot
The combined agenda: all three filters in one row, opening on Groups (B) with Nearby (C) beside it, showing a huddl the person has not responded to. The seam described in Findings is visible here — RSVPs and Groups carry counts, Nearby does not.
Precedence is pinned by
test/features/combined_landing.feature, and both halves werechecked by inverting them rather than assumed.
Before — ordering the preference ahead of the return destination in
return_to/2:After — with
return_to> preference > default: passes.Before — defaulting
parse_scope/3back to:mine:After — with the preference consulted: passes.
Before — the
first_run?collision, which no scenario caught and no merge markerflagged:
After — first-run keyed on the selected filter as well as the person: passes, and
both B's and C's own tests pass with it.
A's, B's and C's inherited scenarios all pass alongside G's four. One inherited scenario
needed reframing, not deleting: C's "The RSVPs and Groups filters still behave as before"
opened a bare
/agendaand asserted only RSVPs were listed — a premise B's flipped defaultmakes false. Reframed to open
?scope=mineexplicitly, which is the same precedent B setfor
calendar_group_scope.featurein its own PR. Its behavior is unchanged and still covered.Merge Danger
Door: two-way for everything except B's migration, which is one-way in the usual
additive sense —
agenda_landingis a new column with a default, so rolling back the codeleaves a harmless unused column rather than broken data.
Blast Radius: the signed-in home.
Everyone who signs in lands somewhere this PR changed, so the default filter flip is felt by
every existing user on their next visit, not just newcomers.
return_tocontinues to gothrough
AuthReturnTo.validate/1, so the widened coverage of sign-in entry points addsreach without adding an open-redirect surface — A's PR covers that with its own unsafe
destination scenarios, inherited here. Nearby adds a PostGIS search to a page that
previously only read the person's own tables; it is async with a stale-ref guard, so a slow
or failing search degrades to an empty state rather than blocking the agenda.
What I would cut before shipping any of this: B's profile panel. It is the heaviest part
of the combination — an Ash attribute, an action, a policy, a migration and a UI panel — to
let people opt out of a default that, once C exists, is the right default for nearly
everybody. Ship B's flipped default plus A and C entire, and hold the preference until
somebody asks. That removes the precedence question from the product, though not from the
code, since
return_tostill has to beat the default.Part of #670
This is a POC for comparison, not intended to merge as-is. Verified by
scenario (failing -> passing) and a clean
--warnings-as-errorscompileonly;
mix precommitis a deliberate follow-up after review.Model: Claude Opus 5.5 (
claude-opus-5-5)🤖 Generated with Claude Code