alumni_lookup

27.5 — Hub Additions

Status: ✅ Built August 12, 2026 Surface: Signup (alum-facing) — app/controllers/signup/signups_controller.rb, app/views/signup/signups/show.html.erb Held surfaces touched: None Migration: None Depends on: 27.3 (ChampionSignupSession, device_label, forget_device!)


1. Scope, and What Is Already Done

Phase 27 §5 lists three items: “past opportunity responses, sign out, device list.”

Sign out already shipped in 27.2. The header renders first name plus “Sign out” for a signed-in visitor, and signing out lands on the landing page with a confirmation rather than a blank form. 27.3 then made it destroy the device row as well as the cookie. Nothing left to build; it is listed here only so the wrap does not go looking for it.

So 27.5 is two things: past opportunity responses and the device list — and the device list was cut on August 12 after being built, on the grounds that sign out already covers the only case that matters. See §4.

Both live on the hub, which is verified-only — authorized_for_profile?, unchanged since 19.7 and untouched by this sub-phase.


2. Open Question 4, Resolved

Phase 27 §6.4: “Does a recognized-but-not-verified alum see their own past opportunity responses? Decision B says no — that is PII. Confirm the hub degrades gracefully rather than 404ing.”

Resolved August 12, 2026: no, and the degradation already exists and is already correct.

A recognized visitor cannot reach the hub at all. show redirects anyone failing authorized_for_profile? to signup_sign_in_path(link_request: true) — a working sign-in page with the email-a-link form, not a 404 and not an error. That is the graceful degradation the question asked for, and it predates the question.

The answer is therefore structural rather than a new guard: past responses are rendered inside a view only verified visitors can load, so there is no recognized-tier branch to write and no PII decision left open. Adding one would be the mistake — a recognized-tier partial on this page would be dead code guarding a route recognized visitors cannot reach.

This closes the last open question in phase-27 §6.


3. Past Opportunity Responses

Decision — collapsed rows, expandable to the full receipt

Interview, August 12, 2026. Title and date always visible; a disclosure expands to the same answer list the thank-you page already renders as “What you sent us.”

Things you've said yes to

▸ Admissions Fair — Nashville      Aug 2
▾ Send-Off Party Host              Jul 14
    Can host          Yes
    Approx. guests    25
    Notes             Backyard, need a tent
▸ Speak to Students                May 3

Full detail is safe here because the hub is verified-only, and it is the only thing that answers the question an alum actually has — what did I tell them? Rejected: title and date alone (nothing to expand, so the question goes unanswered); a link back to the opportunity form (which renders blank, and answers a different question).

Reuse, not duplication

The thank-you page’s receipt block (thank_you.html.erb:63-95) becomes a shared partial that both pages render. This is CLAUDE.md’s “the shared layer is the first answer, not the last resort” applied to a partial: two consumers exist on the day it moves, so it moves on that day rather than being copied with a note to extract it later.

It renders @response.answered_fields, which already handles checkbox arrays (21.7) and blank-answer skipping — no per-page logic to fork.

Placement: app/views/signup/opportunities/_response_receipt.html.erb. Both consumers are on the signup surface, so this is not app/views/shared/ — that layer is for cross-surface sharing, and a file there must be able to name a consumer outside its own surface.

Query

@signup.opportunity_responses.order(created_at: :desc) — the association already exists via OpportunityResponse belongs_to :champion_signup. Includes :opportunity so the titles do not N+1.

Both link methods are includedsignup_verified and signup_email_matched alike. An email-matched response is one the person filled in with their own address; withholding it would make the hub silently incomplete, and the row is already theirs in Signup Admin.

Section hides entirely when empty. The hub’s existing convention (@opportunities is rendered only when there are any), and an empty state here would be a shrug.

Placement: main column, after “Ways to show up.” The pairing is deliberate — what you can say yes to, then what you have said yes to.


4. Device List — CUT

Cut August 12, 2026, after the sub-phase was built. Rationale, from the user: “We don’t need to worry about the devices section. They can just sign out if they need to.”

The list was built as specced — sidebar card, per-row revoke, “This device” marker, scoped lookup, empty state — and then removed: _devices.html.erb, signups#revoke_device, the DELETE /sign-up/:id/devices/:device_id route, and the two hub ivars.

What stays, and why it is not dead code: ChampionSignupSession, device_label, and Signup::Identity#device_session all remain. The table backs the cookie itself, the label still names the device in device_remembered / device_revoked events, and device_session still resolves while verified so a reload cannot stack duplicate device rows on sign-in — the first of the two reasons Identity#resolve! documents. Only the second reason, this list, is gone.

Sign out already does the job. It destroys the device row, not just the cookie (27.3), so the one action an alum actually needs — stop remembering me here — was covered before this list existed. What the list added was managing a device you are not currently using, which presumes both that an alum knows they left one remembered and that they would think to look. Neither is likely enough to earn a permanent card on the hub.

Worth noting for a future phase: this leaves no way to revoke a lost or stolen device remotely. The exposure is bounded by what recognized grants — a first name and a masked email, no hub, no edits — which is exactly the asymmetry Decision B bought. If the recognized tier is ever widened, this cut should be revisited with it.

5. Events

No new types. device_revoked exists (27.3). Nothing is recorded for viewing the hub beyond the profile_viewed / return-visit stamps that already fire — a section appearing on a page is not an action.


6. Test Coverage

The device-list assertions were removed with the feature (§4).


7. What Was Built

Piece Where
Responses list signup/signups/_past_responses<details> rows, no JavaScript
Shared receipt signup/opportunities/_response_receipt — two consumers on the day it moved
Association ChampionSignup has_many :opportunity_responses, dependent: :nullify
Tests test/controllers/signup/hub_additions_test.rb (7)
Device list + revocation Built, then cut — see §4

8. Findings

8.1 The thank-you page was 21.7’s fifth consumer, and it was missed

OpportunityResponse’s own comment says the checkbox-answer helpers exist because “four unrelated consumers need identical semantics: validation, the staff detail view, the CSV export, and the notification email.”

The public thank-you page is a fifth, and it went through neither. It rendered <%= value %>, so a two-box answer printed FridaySaturday with no separator; and it skipped blanks with value.to_s.strip.blank?, which is never true for an array — the same [].to_s == "[]" gap the model’s own comment describes — so a checkbox with nothing ticked printed an empty row.

Found while extracting the block into a partial. Fixed there rather than copied, which would have doubled the defect into the hub. The lesson is the one the comment already tried to teach and did not reach far enough to prevent: a list of consumers written into a comment does not stay accurate, and 21.7’s own view-layer change was the thing that falsified it. Routed to /debug and .github/copilot-instructions.md.

8.2 ChampionSignup had no inverse for opportunity_responses

belongs_to :champion_signup has existed since 21.2 with nothing on the other side, so a real destroy would have raised on the foreign key. Added as dependent: :nullify: a response is a lead in its own right, with staff follow-up and its own contact details on the row, and it began life able to stand alone. Severing the link returns it to that state rather than deleting work.

9. Open Questions

None. §2 closes phase-27 §6.4, which was the last one outstanding in the phase.