Skip to content

bug: fix panel bugs #507, #515, #607, #608, #609, #610, #617, #618, #619 - #576

Merged
lukepolo merged 17 commits into
mainfrom
bug/panel-bugs
Sep 18, 2026
Merged

lukepolo merged 17 commits into
mainfrom
bug/panel-bugs

Conversation

@lukepolo

Copy link
Copy Markdown
Contributor

This fixes the open [BUG] issues from 5stack-panel #507 onward that need web changes. It pairs with 5stackgg/api#408.

It also adds a vitest test setup, since web had no automated tests. Every fix below began with a failing test that reproduced the bug.

Deploy notes

  • Release the api first on every channel. The tournament page now selects tournaments.substitutes_enabled (feature: import a Valve match from a pasted share code #619). If web ships before the api migration and hasura metadata apply, Hasura rejects the whole subscription and tournament pages go blank.
  • Run yarn codegen against the migrated stack so the zeus types include substitutes_enabled. It works at runtime without codegen; only the editor types lag.
  • i18n: new keys are translated into all 16 locales, and the stale validation_extras.min_max_teams_min_value key is removed (16/16 locales match en.json).

Test infrastructure (vitest)

  • Stack: vitest 3 and @nuxt/test-utils 3.23 in the nuxt environment, with happy-dom and @vue/test-utils. Every spec runs inside the real Nuxt app: auto-imports, i18n, pinia, the router and NuxtLink.
  • Isolation from dev and prod:
    • nuxt.config.ts is unchanged.
    • Test build output goes to .nuxt/vitest, so it never overwrites the dev server's .nuxt.
    • The lockfile only gains entries; no existing package changes version.
  • Conventions:
    • yarn test runs the suite, yarn test:watch watches.
    • Specs live in tests/, mirroring the source path.
    • Mount components with mountSuspended.
    • Pure logic specs can opt into // @vitest-environment node.
  • Choices:
    • Everything runs in the nuxt environment. Splitting into projects boots two Nuxt instances in one process and randomly loses auto-imports.
    • A resolutions pin stops yarn from nesting test-utils 4 and a second copy of Vue.
    • tests/setup.ts waits out vaul-vue's and the preloader's real 200–300ms timers before the environment removes document; otherwise the run fails on an unhandled document is not defined.
    • testTimeout is 30s because the first test in a file pays for the Nuxt boot.
  • CI: .github/workflows/tests.yml runs on pull requests and on pushes to main/beta.

#608 Teams participating in an Event are not clickable

Problem. Team cards on the event page look clickable (hover styles) but go nowhere. The same happens with:

  • team links in event standings (award winners and the medal table)
  • team names in match table rows, where it's worse: the click handler still stopped the row's own navigation, so clicking a team name did nothing at all
  • team logos in the match page header

Root cause. <component :is="team.id ? 'NuxtLink' : 'div'"> passes NuxtLink as a string, which Vue resolves at runtime from the component registry. Nuxt only rewrites literal <NuxtLink> tags into imports, and this app registers no global components. So Vue rendered an unknown <nuxtlink> element instead of an <a>.

Fix. import { NuxtLink } from "#components" and bind the component itself, the pattern TournamentDetail.vue already uses. That covers 9 sites:

  • EventTeamsPanel (1)
  • EventStandings (2)
  • MatchTableRow (4)
  • the match page (2)

Tests.

  • EventTeamsPanel, EventStandings and MatchTableRow (compact and full) render <a href="/teams/<id>"> for real teams and no link for ad-hoc teams.
  • Clicking a match-row team name opens the team, not the match.
  • A guard spec fails if any component, page or layout passes NuxtLink by name again.

#607 Events page displays past events big, upcoming events small

Problem.

  • /events showed past events as large full-width heroes and upcoming events as small tiles.
  • Live events and the soonest upcoming ones could also be missing from page 1.

Root cause.

  • The layout was hard-coded: Upcoming used the small EventSquare row and Finished used EventHero in a 2-column grid.
  • Separately, the page fetched one list ordered by starts_at desc with limit/offset and only then split it into Live/Upcoming/Past. Page 1 held the furthest-future events, while live, soonest-upcoming and recent past events spilled onto later pages, and every page repeated all three sections.

Fix.

  • Upcoming events are large hero cards, soonest first. Past events are a compact scroll row that loads more as you scroll, like the tournaments page.
  • Each section has its own subscription, filtered by the new eventPhaseWhere, which matches the client-side eventPhase. All three share one $now.
  • Live and Upcoming show 4 at a time with "Show more".
  • Hero banner images lazy-load, and banner videos only load and play while on screen.
  • EventSquare's phase chip is coloured by phase and shows start–end dates.
  • The page-size selector and ?page are gone.

Tests.

  • The page spec, backed by a fake Hasura client, covers:
    • hero vs compact layout
    • upcoming order
    • the soonest upcoming and live events shown when there are more events than one page
    • loading more past events
  • The page spec also covers the Live/Upcoming limits with "Show more".
  • The EventSquare spec covers the chip colour and date range.
  • The EventHero spec covers lazy images and videos that only play on screen.
  • A node spec checks eventPhaseWhere ≡ eventPhase, including edge cases.

#609 Manual awards can't be granted to the user granting them

Problem. In Grant Award (on /awards, /awards/:id, seasons and events), searching for yourself never finds you.

Root cause. AwardComposer used PlayerSearch without :self="true", so PlayerSearch:

  • always added the logged-in user to exclude (becoming a Typesense steam_id:!= filter)
  • skipped pinning "you" at the top
  • left you out of the online list

The api already allows self-grants.

Fix. Pass :self="true", as the other pickers that allow selecting yourself do.

Tests. AwardComposer.spec.ts:

  • the granting user is offered under the online filter
  • the search request doesn't exclude them

#610 Manually granted awards don't show up for teams

Problem. A manual team grant shows in the award log and on every roster player, but not on the team page or in the /teams list.

Root cause.

  • A manual team grant is a row with team_id and no tournament_team_id.
  • The team page filtered team awards by tournament_team.team_id.
  • /teams grouped by tournament_team?.team_id and never selected team_id.
  • TeamsTable assumed every award had a tournament, so it showed "#null" and "Tournament", and clicking went nowhere.

Fix.

  • Team awards are filtered and grouped by team_id. Calculated tournament rows set it too.
  • The "tournament winners" filter only counts source = 'tournament'.
  • TeamsTable names manual grants by the award, labels them "Granted" with the grant date, and links them to /awards/:id, including manual grants made inside a tournament.
  • AwardCase and AwardModal also title and date a manual grant by the award rather than its tournament.

Tests.

  • tests/pages/teams/teamAwards.spec.ts runs the pages' real subscriptions over stored rows.
  • TeamsTable.spec.ts covers the label, the click target and the tooltip.
  • AwardCase.spec.ts and AwardModal.spec.ts cover manual grants made inside a tournament.

#515 Android PWA does not show install

Problem. On Android phones there was no way to install the app. On Android, the Settings → Notifications card said "Already installed?" even when the app wasn't installed.

Root cause.

  1. The install button never rendered on phones for most players. Only roles above match organizer get the left sidebar with its install button. Everyone else gets TopNav, which has hidden InstallPWA at ≤ 768px since chore: refactor menus #157, with nothing in its place.
  2. Install was only offered in two cases: on iOS, or when Chrome's beforeinstallprompt had been captured.
    • The vite-pwa client plugin calls preventDefault() on that event, which also suppresses Chrome's own install banner.
    • Browsers that never fire it got nothing: Firefox, in-app browsers, and Chrome after a dismiss.
    • The steps drawer only had iOS instructions.

The manifest and service worker were verified installable on 5stack.gg.

Fix.

  • At phone width, TopNav adds an "Install App" row to the profile menu. The desktop header button is unchanged.
  • usePwaInstall has a manual-install state (ios, android or in_app) for when there's no install dialog and the app isn't running standalone.
  • install() opens Chrome's dialog when it's available, and otherwise shows platform-specific steps.
  • The installed state is shared, so accepting the dialog anywhere hides every install entry.
  • The steps drawer is extracted into InstallPWADrawer, with Android and in-app-browser steps added. The iOS steps are unchanged.
  • Notification preferences now shows Install plus the steps, instead of "Already installed?".

Tests. Vitest specs cover usePwaInstall, InstallPWA, ProfileMenu, TopNav at 412px, and the notification-preferences page.

  • Guards that pass before and after: an installed app shows nothing, iOS is unchanged, desktop is unchanged.

On-device QA needed.

  • Android Chrome, both the dialog path and the steps path
  • Firefox / Samsung Internet wording
  • an in-app browser
  • iPhone Safari at phone width
  • header layout at 360px

#507 / #617 / #618 Tournament stage form

Problem. The stage form enforced the old rules the api has now relaxed:

  • after a round robin, SingleElimination only offered 4 or more teams
  • round robins required at least 4 teams per group
  • round robins were capped at 32 teams

Fix. The new utilities/tournamentStageTeams.ts mirrors the api's validate_tournament_stage rules, and TournamentStageForm uses it for both options and validation:

  • Round robin needs at least 3 teams per group, with the cap raised to 256.
  • SE/DE as stage 1 needs at least 4 teams per group.
  • After a round robin or no-elimination Swiss:
    • the next stage holds 2 up to that stage's max
    • DE stays at 4 or more
  • After a Valve Swiss, the next stage holds at most half the Swiss.
  • After an SE/DE stage, the next stage's min is at least that stage's last-round match count.
  • A grouped stage's halved min must split evenly into its groups.
  • A ranked stage can't shrink below the stage after it.

Validation messages show the actual counts.

Tests. tests/utilities/tournamentStageTeams.spec.ts

#619 Tournament substitutes setting

  • "Allow Substitutes" switch on tournament creation and in match settings. It defaults to on and is hidden for Duel, which the api always holds at 0 substitutes.
  • The match-settings panels show the real substitute count (max − min players per lineup), so a Duel tournament match no longer shows "Substitutes 2".
  • The fix and its tests live in the api (test/tournament-substitutes.spec.ts).

lukepolo and others added 9 commits September 16, 2026 14:25
Mirrors the api: an odd field's bye is a free win, so a 15-team Valve Swiss
sends 8 teams on.
The form offered up to 256 teams in a single round robin group. Every pairing
is a bracket row rebuilt on each stage edit, so 256 teams meant 32,640 rows.
Each group now takes up to 32 teams, the old limit, and more groups still
reach 256.
Mirrors the api guard: once the bracket is seeded, turning substitutes off
strands a team at its minimum. The switch can still turn them back on.
The event page rendered its member tournaments as a bordered div with a
status badge and the name, in both the Tournaments tab and the Overview
section, because the event subscription only selected id/name/status/start
for the nested tournament. It now spreads simpleTournamentFields and uses
TournamentCard, stacked like /tournaments, /watch and /play.

A mixed-status list has no section header to hoist the status variant onto,
so tournamentStatusVariant() derives it per card from the same groupings the
/tournaments status filter uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The readout is built for a fifteen-minute match check-in, where H:MM:SS
always reads as a clock. Reused for a tournament check-in window that can
open weeks out, it degenerated into "1390:29:38". Past a day it now shows
days and hours, ticks once a minute instead of once a second, and follows
the hour rather than the minute for the rollover. Under a day it is the
exact clock as before, and the ticker goes back to 1s on the crossing.

The heading had the same problem: "Check-in opens at 04:00 PM" for a window
two months out reads as today. It now carries the date when the window is
not today, and the year when it is not this year. required_window keeps the
clock -- "between X and Y on the day" is describing the window's shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The outlined echo behind the hero name was held to one line, so a title that
wrapped left it running out across the banner. It already takes the title's
own width from left-5/right-[-5px], so dropping the nowrap breaks it at the
same points and echoes each line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The region section only ever offered online regions, so a stage could not be
pointed at a LAN region. A LAN switch -- shown to match_organizer and above,
and only when a LAN region exists -- swaps the region list to the LAN ones
and turns region veto off, since there is nothing to veto on a single hall.

The switch is inferred back from the stage's saved regions, so reopening a
LAN stage shows it on with its region named in the select rather than an
empty placeholder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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