8000 POC G: the A + B + C combination for post-login landing by MichaelDimmitt · Pull Request #677 · huddlz-hq/huddlz · GitHub
[go: up one dir, main page]

Skip to content

POC G: the A + B + C combination for post-login landing - #677

Closed
MichaelDimmitt wants to merge 10 commits into
huddlz-hq:mainfrom
MichaelDimmitt:poc/670-combination
Closed

MichaelDimmitt wants to merge 10 commits into
huddlz-hq:mainfrom
MichaelDimmitt:poc/670-combination

Conversation

@MichaelDimmitt
@MichaelDimmitt MichaelDimmitt commented Oct 9, 2026 •
Copy link
Copy Markdown
Contributor

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:

 sign in
-  -> /agenda                        # always, intent dropped
+  -> return destination, if any     # A: header sign-in now carries one
+  -> else the filter you chose      # B: agenda_landing preference
+  -> else your groups               # B: flipped default

 /agenda
   [ RSVPs ]  [ Groups ]             # two views of your own commitments
+  [ Nearby ]                        # C: a search, uncounted

One precedence ordering, applied in the two places that decide a landing:

return_to (explicit)  >  stored preference  >  default
  auth_controller.ex return_to/2    picks the page
  calendar_live.ex   parse_scope/3  picks the filter

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.ex and app.css were the overlap. The CSS merged clean — zero selector
overlap between B and C. Six conflict regions in calendar_live.ex; five were plumbing,
two functions growing an argument rather than choosing a side:

-parse_scope(param, preference)      # B
-parse_scope(param, view_mode)       # C
+parse_scope(param, preference, view_mode)

-scope_param(scope, nav.landing)     # B: omit the scope you land on
-scope_param(scope, view)            # C: nearby is agenda-only
+scope_param(scope, landing, view)

The sixth was a genuine behavioral collision, and git merged both halves of it without a
conflict marker.
C narrowed first_run? to own_entries == [] && scope == :mine, because
under 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:

-first_run? = own_entries == [] && scope == :mine     # kills B's Groups copy
+first_run? = :mine   with no own entries
+          or :groups with neither own nor group entries
+          never :nearby (it has its own three states)

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_landing
deliberately has no nearby value. 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

Variant G

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 were
checked by inverting them rather than assumed.

  • Before — ordering the preference ahead of the return destination in return_to/2:

    Scenario: Arriving for one huddl beats my own agenda preference
      [passed] And I sign in as "arrival+combined@example.com" with password "Password123!"
      [failed] Then I should be on the huddl page for "Combined Dawn Walk"
    

    After — with return_to > preference > default: passes.

  • Before — defaulting parse_scope/3 back to :mine:

    Scenario: Arriving with nothing in mind lands on my groups and still reaches further
      [failed] Then the user lands on everything their groups have on
    

    After — with the preference consulted: passes.

  • Before — the first_run? collision, which no scenario caught and no merge marker
    flagged:

    test "a newcomer's groups still show what they have on"
      Could not find any elements with selector "#calendar-first-run"
    

    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 /agenda and asserted only RSVPs were listed — a premise B's flipped default
makes false. Reframed to open ?scope=mine explicitly, which is the same precedent B set
for calendar_group_scope.feature in 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_landing is a new column with a default, so rolling back the code
leaves 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_to continues to go
through AuthReturnTo.validate/1, so the widened coverage of sign-in entry points adds
reach 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_to still 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-errors compile
only; mix precommit is a deliberate follow-up after review.

Model: Claude Opus 5.5 (claude-opus-5-5)

🤖 Generated with Claude Code

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
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.

1 participant

0