feature: events pages (LAN mini-seasons)#484
Open
Flegma wants to merge 15 commits into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/zeusregenerated with the events types.graphql/simpleEventFields.ts,composables/useEventContext.ts,utilities/eventDisplay.ts.pages/events/index.vue(list),create.vue,manage.vue, sharedcomponents/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).pages/events.vue"coming soon" stub removed.Post-review fixes, round 1 (2026-07-04)
f61cb96create/manage gates aligned with the backend'screate_events_role(neweventCreateRolestore getter); Manage Events list broadened for privileged roles.685f54dtournaments-tab null guard; medal table keeps ad-hoc teams; Min. Rounds blank coerced to 0; removed a wasted fallback query at mount.d8dba9esubscriptionerror()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+487782cstandings 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.0a8b956organizer 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), andpopulateFromEventcorrectly sets/clears dates.a0b8c79membership 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 usedon_conflict, which Hasura does not expose without an update permission, so it failed validation for every non-admin organizer.1f13265isRoleAbove 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.8c495ccshared 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 (unusedisOrganizercontext 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 (
useRouteTabwhitelist), the update mutation against the Hasura update permission columns, the attach-search/import mutations against the insert permissions, an audit of everyisRoleAbovecall site, and the EventCard extraction (i18n keys, no dangling references,simpleEventFieldscovers 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)
yarn codegenagainst production Hasura and recommitgenerated/. 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), addsplayers.faceit_synced_at, flips nullability onplayer_faceit_rank_history.elo/skill_level, and sets the zeusHOSTconstant tohttp://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 typecheckafter the re-codegen (expect zero new errors over baseline).Notes / follow-ups (not blockers)
pages/tournaments/manage.vueshows all tournaments to tournament_organizer/admin roles via itsis_organizerfilter (pre-existing);events/manage.vuehandles the equivalent explicitly. Worth a ticket to make tournaments consistent.user, and those roles genuinely manage all events; the create gate inside the pages usescreate_events_role).public.create_events_roleyet (DB-only; defaults to any logged-in user until set). Adding a control next tocreate_tournaments_roleis a small follow-up.EventMembershipPanel.vue/EventStandings.vueuse raw gql / loosely-typed$apollo.queryto sidestep a zeusExtractVariablesinference gap (documented workaround).