diff --git a/docs/superpowers/specs/2026-06-01-associate-connection-with-current-event-design.md b/docs/superpowers/specs/2026-06-01-associate-connection-with-current-event-design.md new file mode 100644 index 0000000..ed73624 --- /dev/null +++ b/docs/superpowers/specs/2026-06-01-associate-connection-with-current-event-design.md @@ -0,0 +1,147 @@ +# Associate a connection with your current event + +## Summary + +On a connection's profile page (`/connections/{did}`), when the logged-in +ATProto user is currently checked into an ongoing event, show a checkbox that +tags that person's `quest.atmo.connection` record in the user's PDS with the +current event. Checking associates the record with the current event; +unchecking clears the event tag. The update is in place (reusing the record's +rkey via `putRecord`), so the at-uri stays stable. + +## Goals + +- Let a user tag an existing connection with the event they're currently at, + directly from the connection's profile page. +- Persist the association to the user's PDS by updating the existing + `quest.atmo.connection` record in place. +- Make the toggle fully reversible: uncheck removes the event tag. + +## Non-goals + +- Local accounts (`local_*`) and local-only connections + (`local_connections` table) — they have no PDS record to update, so the + checkbox does not appear for them. +- Creating additional connection records per event (the lexicon supports this, + but this feature updates in place instead). +- Surfacing the checkbox when the user is not checked into an ongoing event. + +## Data model + +`quest.atmo.connection` records live in the viewer's PDS with shape +`{ with: did, connectedAt: datetime, event?: at-uri }`, keyed by a TID rkey. +The `event` field is optional. We update it in place. + +`checkin.Current(ctx, db, did)` returns the at-uri of the event the user is +currently checked into, if that event is ongoing. + +## Behavior + +The checkbox renders **only when both** hold: + +1. The viewer is an ATProto user (not a `local_*` account), and +2. `checkin.Current` returns an ongoing event, and +3. A `quest.atmo.connection` record exists in the viewer's PDS for this target. + +State: + +- **Checked** when the connection record's `event` equals the current event's + at-uri. +- **Unchecked** otherwise — including when the record is tagged with a + *different* event. Checking the box then overwrites that event with the + current one (accepted trade-off of in-place update). + +When multiple connection records exist for the same target (different events), +the **most recent** record is the one shown and updated, matching the +deduplication the list/profile pages already apply. + +## Components + +### 1. UI — `features/connections/pages/profile.templ` + +New `ProfileView` fields: + +- `CurrentEventURI string` — at-uri of the ongoing event, or empty. +- `CurrentEventName string` — display name for the label. +- `ConnRecordURI string` — at-uri (rkey-bearing) of the record to update. +- `AssociatedWithCurrent bool` — initial checked state. + +Render a checkbox (only when `CurrentEventURI != "" && ConnRecordURI != ""`), +near the existing notes/follow-up area, labeled e.g. `met at {CurrentEventName}`. +Carry `ConnRecordURI` and the target DID as `data-` attributes for the script. + +### 2. Handler — `features/connections/handlers.go` `View` + +After loading the viewer's connection entries (existing `connection.List` +call): + +- Call `checkin.Current(ctx, db, viewerDID)`; if ok, resolve the event name via + `event.Get`. +- Select the most recent connection record for this target, capturing its + `URI` → `ConnRecordURI` and its `EventURI`. +- Set `AssociatedWithCurrent = (record.EventURI == CurrentEventURI)`. +- Populate the new fields only when checked in AND a PDS record exists. + +### 3. Endpoint — `POST /connections/{did}/event` + +Wired in `features/connections/routes.go`. Steps: + +1. `h.Auth.RequireSession` to get the viewer DID + OAuth session (needs the + `repo:quest.atmo.connection` scope already used by the connect flow). +2. Decode body `{ "associate": bool }`. +3. Re-resolve the current ongoing event server-side with `checkin.Current` + (never trust the client for which event). If `associate` is true and there + is no current event → 400. +4. Find the target's most-recent connection record rkey via `connection.List`. + If none → 404. +5. Call `connection.SetEvent(ctx, sess, recordURI, eventURI)`, where + `eventURI == ""` (when `associate` is false) clears the tag. +6. Respond `{"status":"ok"}` (JSON), or appropriate 4xx/5xx. + +### 4. `connection.SetEvent` — `internal/connection` + +Mirrors the in-place `putRecord` pattern in `internal/event/put.go` `Update`: + +- Parse the rkey from the record's at-uri. +- Obtain the record's current `with` and `connectedAt` (carry them from the + list entry the handler already has, or re-fetch via `getRecord`) so no fields + are dropped. +- Build the record value with `event` set when non-empty, omitted when empty. +- `com.atproto.repo.putRecord` with `repo`, `collection`, `rkey`, `record`, + reusing the rkey so the at-uri is stable. `validate` omitted (consistent with + the rest of the codebase). + +### 5. Client JS + +A small script bound to the checkbox `change` event (new +`connection-event.js`, or appended to the existing `notes.js`): + +- POST `{associate: checkbox.checked}` to + `/connections/{encodeURI(did)}/event` with `credentials: "same-origin"`. +- Show transient `✓ saved` / `save failed` status (same pattern as notes). +- On failure, revert the checkbox to its prior state so the UI stays truthful + to the PDS. + +## Error handling + +| Situation | Result | +|-----------|--------| +| Not checked into an ongoing event | Checkbox not rendered; endpoint rejects `associate:true` with 400 | +| No PDS connection record for target | Checkbox not rendered; endpoint returns 404 | +| Record already tagged with a different event | Checkbox unchecked; checking overwrites with current event | +| Local viewer / local connection | Feature absent (out of scope) | +| `putRecord` fails | 5xx; client reverts checkbox and shows "save failed" | + +The server always re-derives the event from `checkin.Current`; the client only +sends the boolean. + +## Testing + +- **`connection.SetEvent`** — unit-test rkey parsing and that the `putRecord` + input includes/omits `event` correctly, following the style in the existing + `connection_test.go`. +- **Handler** — happy path (associate / disassociate), not-checked-in + rejection, and no-record-found, mirroring existing feature tests. +- **Manual verification** — load `/connections/{did}` while checked into an + ongoing event, toggle the box, and confirm the `event` field + appears/disappears on the PDS record via `com.atproto.repo.getRecord`.