Skip to content

feat(teams): server-enrich is_support_member on team responses#56

Merged
alejandro-runner merged 2 commits into
synvya-stagingfrom
feat/team-support-member-flag
May 6, 2026
Merged

feat(teams): server-enrich is_support_member on team responses#56
alejandro-runner merged 2 commits into
synvya-stagingfrom
feat/team-support-member-flag

Conversation

@alejandro-runner
Copy link
Copy Markdown
Member

Summary

Closes the gap in the support-users feature where the client could not reliably distinguish support agents from permanent team members in the UI.

The original Keycast spec (§8.3) had punted this label as "applied client-side via GET /api/admin/support-admins." That endpoint is full-admin gated, which leaves the actual viewers (restaurant owners and the support agents themselves) without a working data path. This PR makes Keycast the source of truth: every TeamUser returned to clients now carries an is_support_member: bool flag computed at request time from the Redis support_admins set.

Behavior

Endpoints affected (additive — no breaking changes):

Endpoint Returns
GET /api/teams Vec<TeamWithRelations> — each team_users row carries is_support_member.
GET /api/teams/:id TeamWithRelations — same enrichment.
POST /api/teams/:id/users TeamUser — single row, enriched.
  • One Redis SMEMBERS support_admins per request. Result is hashed once, then cross-referenced O(1) per row.
  • No database change. The flag is a runtime computation, not stored. A user toggled off the support set stops being flagged on the next request.
  • Safe downgrade. When Redis is unavailable (state error, missing client, command failure), the handler logs a warning and returns is_support_member: false for all rows. Capability downgrade, never a hard failure.
  • Backwards compatible. Existing clients that ignore unknown fields are unaffected. New clients reading the field get the enrichment without any other API change.

Commits

  1. feat(teams): server-enrich is_support_member on team responsesTeamUser gains the field with #[sqlx(default)] + #[serde(default)] so DB FromRow and JSON deserialization stay backwards-compatible. Adds fetch_support_admin_set and the pure helper mark_support_members. Wired into list_teams, get_team, and add_user. Unit tests cover: only-set-members flagged, idempotent, empty-set no-op, preexisting flag preserved (4 passing tests).
  2. docs(synvya): document is_support_member enrichment — adds §3.5 to the support-users spec describing the enrichment behavior, updates §8.3 to replace the original client-side punt with the new server-owned path, and marks the §9 checklist accordingly.

Companion PR

Client-side spec resolution: Synvya/client (forthcoming) closes the corresponding open question (Q4) in the client support-users.md spec.

Test plan

  • cargo fmt --all -- --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test -p keycast_api --lib mark_support_members (4/4 pass)
  • Manual on staging:
    • Promote a test pubkey via POST /api/admin/support-admins.
    • As a regular user (owner), call GET /api/teams. Confirm members in the support set return is_support_member: true; others return false.
    • Demote the test pubkey, refetch, confirm flag drops to false on next request.
    • Stop Redis (or simulate) and verify all rows return is_support_member: false plus a warning in logs (not a 5xx).

🤖 Generated with Claude Code

alejandro-runner and others added 2 commits May 5, 2026 20:45
Adds a server-side `is_support_member: bool` flag on each `TeamUser`
returned from the team endpoints, populated by cross-referencing each
member's pubkey against the Redis `support_admins` set. Restaurant
owners viewing their team during a support session can now distinguish
support agents from permanent members in the UI without holding any
admin role themselves.

Closes the gap left by the original support-users keycast spec §8.3,
which had punted this responsibility to the client. The client-side
fallback (GET /api/admin/support-admins) is full-admin gated and
therefore unusable for owners and support agents — making the
server-enriched path the only viable one.

Implementation
- `TeamUser` gains an `is_support_member` field with `#[sqlx(default)]`
  and `#[serde(default)]` so DB FromRow and JSON deserialization
  remain backwards-compatible (default `false`).
- `fetch_support_admin_set` reads the Redis set once per request and
  returns a HashSet; pure function `mark_support_members` flags
  matching rows. Both safe no-ops when Redis is unavailable.
- Wired into `list_teams`, `get_team`, and `add_user`. Single SMEMBERS
  per request; HashSet lookup is O(1) per row.
- Unit tests cover: only set members are flagged, idempotent, empty
  set is no-op, preexisting `true` is preserved.

Caller compatibility: existing clients that ignore unknown fields are
unaffected; clients that read the field get the enrichment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…spec

Updates docs/synvya/support-users.md to reflect the server-side
enrichment now landing alongside this spec change:

- New §3.5 documenting the enrichment behavior on GET /teams,
  GET /teams/:id, and POST /teams/:id/users — single SMEMBERS per
  request, HashSet lookup, no DB change, capability downgrade when
  Redis is unavailable.
- §8.3 updated: replaces the original "client-side, out of scope for
  Keycast" punt with the new server-owned data path. The visual
  treatment remains a client UX decision; the data is now Keycast's
  responsibility.
- §9 implementation checklist marked complete for the items landed
  in PR #54 plus this PR; the deferred HTTP-handler tests are noted
  separately.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alejandro-runner alejandro-runner merged commit 2d19384 into synvya-staging May 6, 2026
@alejandro-runner alejandro-runner deleted the feat/team-support-member-flag branch May 6, 2026 04:14
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