Skip to content

feature: events pages (LAN mini-seasons)#484

Open
Flegma wants to merge 15 commits into
mainfrom
feature/events
Open

feature: events pages (LAN mini-seasons)#484
Flegma wants to merge 15 commits into
mainfrom
feature/events

Conversation

@Flegma

@Flegma Flegma commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Web side of the Events feature (LAN mini-seasons). Design: docs/plans/2026-07-03-events-feature-design.md; implementation plan: docs/plans/2026-07-04-events-feature-implementation-plan.md.

Depends on the api PR (5stackgg/api#337). Merge the api PR first: the regenerated GraphQL types and every query here assume the events schema exists in Hasura.

What's included

  • generated/zeus regenerated with the events types.
  • graphql/simpleEventFields.ts, composables/useEventContext.ts, utilities/eventDisplay.ts.
  • pages/events/index.vue (list), create.vue, manage.vue, shared components/events/EventCard.vue, components/events/EventForm.vue.
  • pages/events/[eventId]/index.vue (detail) with tabs: Leaderboard, Standings, Tournaments, Teams, plus an organizer-only Settings tab (edit name/description/dates and move the event between Setup/Live/Finished).
  • components/events/EventLeaderboard.vue (get_event_leaderboard; categories rating/adr/kdr/kills/wins), EventStandings.vue (member-tournament trophies + medal table), EventMembershipPanel.vue (organizer-only attach/detach tournaments/teams/players + import-players-from-rosters).
  • Nav entries (Events + admin Manage Events), breadcrumb branch, full en.json i18n keys.
  • Old pages/events.vue "coming soon" stub removed.

Post-review fixes, round 1 (2026-07-04)

  • f61cb96 create/manage gates aligned with the backend's create_events_role (new eventCreateRole store getter); Manage Events list broadened for privileged roles.
  • 685f54d tournaments-tab null guard; medal table keeps ad-hoc teams; Min. Rounds blank coerced to 0; removed a wasted fallback query at mount.
  • d8dba9e subscription error() handlers + malformed-uuid guard (no stuck skeleton); breadcrumb no longer shows a raw uuid or literal create/manage segment.

Post-review fixes, round 2 (2026-07-07)

A second full-PR review (8 finder angles, per-candidate adversarial verify, then an adversarial regression pass on the fixes, which itself caught and fixed one Minor regression):

  • 2918f87 + 487782c standings trophy shapes. Placement trophies are stored as one canonical team row plus one row per roster player, and MVP rows also carry the winning team's id. The standings component treated every row independently: placement slots stacked roster-size + 1 duplicate badges, the medal table credited one tournament win as roster-size + 1 golds, and the MVP slot showed the winning team's name/link instead of the player. Rows now collapse to one entry per team per placement, medal counts dedupe per (tournament, team, placement), MVP is player-scoped, and manually awarded player trophies keep their player link.
  • 0a8b956 organizer Settings tab (fixes unreachable edit mode). EventForm shipped with a complete edit mode that nothing mounted, so no UI could move an event out of Setup; newly created events stayed hidden from the public forever. The detail page now mounts the form in an organizer-gated Settings tab (mirroring the tournament detail page), and populateFromEvent correctly sets/clears dates.
  • a0b8c79 membership panel. The tournament attach search was scoped to tournaments the viewer personally organizes, locking out co-organizers and privileged roles (the server allows attaching any tournament); the roster-import mutation used on_conflict, which Hasura does not expose without an update permission, so it failed validation for every non-admin organizer.
  • 1f13265 isRoleAbove fail-closed. An unknown role gate (settings still loading) mapped to indexOf -1 and granted every logged-in user; the Create Event/Tournament buttons flashed for users the server would reject.
  • 8c495cc shared EventCard + eventDisplay helpers. The card markup and three display helpers were copy-pasted across the list page, manage page, and detail header; extracted to one component + one utility module. Also drops dead code (unused isOrganizer context field, unused imports).

Verification

Every task was built and reviewed against an ephemeral TimescaleDB + Hasura v2.49.2 stack loaded with the api events schema and dev fixtures (round-1 details in the commit history). The round-2 fixes were verified by a 4-agent adversarial regression pass: every trophy-row shape traced through both the placement cards and the medal table (7 scenarios), the settings-tab tab-state wiring (useRouteTab whitelist), the update mutation against the Hasura update permission columns, the attach-search/import mutations against the insert permissions, an audit of every isRoleAbove call site, and the EventCard extraction (i18n keys, no dangling references, simpleEventFields covers the card's fields). One regression was found and fixed (487782c). A live typecheck could not be re-run because the checked-in codegen targets the ephemeral schema (see below).

MUST DO before merge (needs the real production Hasura)

  • Re-run yarn codegen against production Hasura and recommit generated/. The committed regen was generated against the ephemeral verification Hasura, which had schema drift: it drops unrelated, unused columns (draft_games.scheduled_at, game_server_nodes.cs2_launch_options), adds players.faceit_synced_at, flips nullability on player_faceit_rank_history.elo/skill_level, and sets the zeus HOST constant to http://localhost:58080 (dead code, unused by the Apollo client). None affect the events code, and all are restored by the prod re-codegen. Run this AFTER the api PR merges, then merge this PR.
  • nuxi typecheck after the re-codegen (expect zero new errors over baseline).

Notes / follow-ups (not blockers)

  • pages/tournaments/manage.vue shows all tournaments to tournament_organizer/admin roles via its is_organizer filter (pre-existing); events/manage.vue handles the equivalent explicitly. Worth a ticket to make tournaments consistent.
  • The Manage Events nav entry stays gated on tournament_organizer/admin like Manage Tournaments (reviewed and kept: the whole Administration nav section is already role-gated above user, and those roles genuinely manage all events; the create gate inside the pages uses create_events_role).
  • No settings-page UI for public.create_events_role yet (DB-only; defaults to any logged-in user until set). Adding a control next to create_tournaments_role is a small follow-up.
  • EventMembershipPanel.vue/EventStandings.vue use raw gql / loosely-typed $apollo.query to sidestep a zeus ExtractVariables inference gap (documented workaround).
  • Accepted review observations: EventForm's date+time picker is a fork of the tournament form's (a shared DateTimePicker is a cross-domain refactor); the leaderboard fetch runs the rows query and the aggregate count together on every page change (LAN-sized cost); concurrent roster imports can race the PK and surface an error toast (retry self-heals).

Flegma added 15 commits July 4, 2026 08:56
Regenerate generated/zeus against the verification Hasura, which now
includes the events schema (events, event_organizers, event_players,
event_teams, event_tournaments, e_event_status, get_event_leaderboard,
is_event_organizer) on top of the full production metadata.

Add graphql/simpleEventFields.ts (Selector("events") shared selection,
mirrors simpleTournamentFields.ts) and composables/useEventContext.ts
(module singleton ref, clones useTournamentContext.ts exactly).

Remove the pages/events.vue coming-soon stub since it owned the
/events route and would have swallowed the child routes later tasks
add; nothing currently links to it.
Adds pages/events/index.vue, a leaner analog of pages/tournaments/index.vue:
TacticalPageHeader with a role-gated Create Event CTA, a typedGql subscription
over events (ordered by starts_at desc_nulls_last, paginated via limit/offset)
using simpleEventFields, card list linking to the events-eventId detail route,
and Pagination + usePerPage("events"). No status/date/search filters are added
since Hasura already scopes guests to non-Setup events and events have far
fewer filterable dimensions than tournaments.
The backend authorizes event inserts on public.create_events_role, but
the web create/manage buttons gated on tournamentCreateRole
(public.create_tournaments_role), so the button could show when the
server would reject the insert, or hide from users the server would
allow. Add an eventCreateRole getter that reads create_events_role
(defaulting to the lowest role to match the backend's absent-setting
branch) and switch canCreateEvent / canManageEvents to it.

Also broaden the Manage Events list: is_event_organizer treats
tournament_organizer/administrator as an organizer of every event, so
those roles can manage all events, but the list was hard-scoped to
events the user personally organizes and hid the rest. Pass an empty
where-filter for those roles (their select permission already returns
every event) and keep the personal scope for regular organizers.
- Tournaments tab: filter out member tournaments whose nested tournament
  resolves to null (a viewer without read access to it), so the template
  no longer dereferences entry.tournament.id and the tab count is right.
- Standings medal table: group team placements by the ad-hoc
  tournament_team when there is no linked global team, instead of
  dropping the row, so the table matches the placement cards above it.
  MVP trophies stay player-scoped.
- Leaderboard: clearing the Min. Rounds input left an empty string that
  is not a valid Int and errored the query into a blank board; coerce
  blank/NaN to 0 (no minimum).
- Detail page: the leaderboard-participants fallback query no longer
  fires at mount for events that have directly-attached players (the
  skip guard read an undefined event as "no players" and wasted a
  round-trip whose result was discarded).
- Add an error() handler to the events list, manage, and detail
  subscriptions so a hard subscription error clears the loading flag
  instead of leaving the skeleton up forever.
- Detail page: guard against a malformed (non-uuid) route param before
  subscribing, rendering the not-found state rather than erroring the
  subscription.
- BreadCrumbs: skip the events crumb until the event context matches the
  route segment, matching the tournaments branch, so a raw event uuid
  (while loading or on a hidden/not-found event) or a literal
  create/manage segment is no longer shown.
Placement trophies are awarded as one canonical team row plus one row
per roster player (all sharing tournament_team_id), and MVP rows also
carry the winning team's tournament_team_id. The standings component
treated every row independently, so:
- each Champion/Runner-Up/Third slot stacked roster-size + 1 duplicate
  badges instead of one entry per team;
- the medal table credited a single tournament win as roster-size + 1
  gold medals for the team;
- the MVP slot and medal rows displayed the winning team's name and
  linked to the team page instead of the MVP player.

Collapse placement rows to one entry per team (preferring the canonical
row, falling back to one roster row for ad-hoc teams without a linked
global team), dedupe medal counts per (tournament, team, placement),
treat placement 0 as player-scoped in the name/link helpers, and keep
manually awarded player-scoped placement trophies as player rows. Ad-hoc
team rows render unlinked instead of linking a roster player's page.

Also trim the standings query to the fields the component actually
renders (drops an 18-field player block and other unused columns).
EventForm shipped with a full edit mode (event prop, update mutation,
status select, dirty tracking) but nothing ever mounted it with an
event, so the entire edit path was unreachable; in particular no UI
could move an event out of Setup, leaving newly created events
permanently hidden from the public unless the status was changed via
the API directly. Mount the form in an organizer-gated Settings tab on
the detail page, mirroring how the tournament detail page mounts
TournamentForm for organizers.

Also fix populateFromEvent to set (or clear) starts_at/ends_at in the
form values: the date/time watchers early-return when either part is
undefined, so a dateless event kept a stale starts_at and discarding a
date edit did not restore the server value.
The tournament attach search was hard-scoped to tournaments the viewer
personally organizes, but the event_tournaments insert permission lets
an event organizer attach any tournament; the filter locked out
co-organizers and privileged roles managing an event whose tournaments
belong to the owner. Drop the ownership filter so the search matches
what the server allows.

The roster import insert used on_conflict, but Hasura only exposes that
argument to roles with an update permission on the table and
event_players defines none, so the mutation failed GraphQL validation
for every non-admin organizer. Remove it; duplicates were already
pre-filtered against the attached players before the insert.

Also drop an unused vue computed import.
isRoleAbove mapped an unknown role argument to indexOf -1, which every
real role index clears, so gates driven by the settings-store role
getters (which return false while settings are still loading) granted
access to every logged-in user during the loading window; the Create
Event and Create Tournament buttons flashed for users the server would
reject. Deny when the requested role is not in roleOrder.
The event card markup and the formatEventDate/statusBadgeVariant/
statusLabelKey helpers were copy-pasted across pages/events/index.vue,
manage.vue, and the detail page header. Extract the card into
components/events/EventCard.vue and the helpers into
utilities/eventDisplay.ts, and use them from all three pages. The
manage list now also shows the organizing chip and organizer, matching
the browse list (relevant since privileged roles see events organized
by others there).

Also drop dead code: the never-read isOrganizer field on
useEventContext, and unused computed/Trophy imports on the detail page.
The standings rework unlinked every non-MVP placement row without a
linked global team, which also stripped the player-page link from
manually awarded player-scoped trophies. Those are distinguishable from
ad-hoc roster rows (roster rows always carry tournament_team_id), so
link the player when the row has one and no tournament_team.
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