feat: Implement tournament schedule tab and fix E2E tests (#27)
Release / release (push) Failing after 9s
Build CI Images / build-ci-base (push) Failing after 18s

## Summary

This PR implements the tournament schedule tab functionality and fixes all remaining E2E test failures.

### Changes Included

1. **Tournament Schedule Feature**
   - Added tournament schedule page at `/admin/tournaments/[id]/schedule`
   - Implemented "Generate Schedule" button functionality
   - Added schedule generation logic for round-robin tournaments

2. **E2E Test Fixes**
   - Fixed database connection issues in production builds
   - Improved test reliability with better error handling and debugging
   - Updated test infrastructure to use environment variables instead of hardcoded values

3. **CI/CD Updates**
   - Added E2E test job to PR workflow
   - Configured tests to run against development database
   - Moved database password to Gitea secrets

4. **Code Quality**
   - Removed hardcoded passwords from codebase
   - Improved Prisma client configuration
   - Enhanced authentication and navigation components

### Test Results
All 16 E2E test scenarios are now passing:
- Authentication tests: 
- Registration tests: 
- Tournament schedule tests: 
- Player schedule tests: 
- Admin navigation tests: 

### Database Configuration
- Tests run against `euchre_camp_dev` database
- Production builds use environment variables for database configuration
- Database password stored in Gitea secrets as `DB_PASSWORD`

### CI Pipeline
The PR workflow now includes:
1. Unit tests
2. E2E tests (using production build)
3. Version bump analysis

E2E tests must pass before PR can be merged.

Reviewed-on: #27
Co-authored-by: David Gwilliam <dhgwilliam@gmail.com>
Co-committed-by: David Gwilliam <dhgwilliam@gmail.com>
This commit was merged in pull request #27.
This commit is contained in:
2026-04-27 01:59:16 +00:00
committed by david
parent 75358bf20d
commit bb6be245b7
97 changed files with 7950 additions and 1341 deletions
+88 -59
View File
@@ -7,32 +7,35 @@ datasource db {
}
model Player {
id Int @id @default(autoincrement())
name String
rating Int @default(0)
currentElo Int @default(1000)
gamesPlayed Int @default(0)
wins Int @default(0)
losses Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
normalizedName String @unique
eloSnapshots EloSnapshot[]
eventParticipants EventParticipant[]
matchesAsP1 Match[] @relation("MatchPlayer1")
matchesAsP2 Match[] @relation("MatchPlayer2")
matchesAsP3 Match[] @relation("MatchPlayer3")
matchesAsP4 Match[] @relation("MatchPlayer4")
partnershipGames PartnershipGame[] @relation("PartnershipPlayer1")
partnershipGames2 PartnershipGame[] @relation("PartnershipPlayer2")
partnershipStats PartnershipStat[] @relation("StatPlayer1")
partnershipStats2 PartnershipStat[] @relation("StatPlayer2")
teamsAsPlayer1 Team[] @relation("TeamPlayer1")
teamsAsPlayer2 Team[] @relation("TeamPlayer2")
user User?
eloRating EloRating?
glicko2Rating Glicko2Rating?
openSkillRating OpenSkillRating?
id Int @id @default(autoincrement())
name String
rating Int @default(0)
currentElo Int @default(1000)
gamesPlayed Int @default(0)
wins Int @default(0)
losses Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
normalizedName String @unique
eloSnapshots EloSnapshot[]
eventParticipants EventParticipant[]
matchesAsP1 Match[] @relation("MatchPlayer1")
matchesAsP2 Match[] @relation("MatchPlayer2")
matchesAsP3 Match[] @relation("MatchPlayer3")
matchesAsP4 Match[] @relation("MatchPlayer4")
partnershipGames PartnershipGame[] @relation("PartnershipPlayer1")
partnershipGames2 PartnershipGame[] @relation("PartnershipPlayer2")
partnershipStats PartnershipStat[] @relation("StatPlayer1")
partnershipStats2 PartnershipStat[] @relation("StatPlayer2")
activities Activity[]
user User?
eloRating EloRating?
glicko2Rating Glicko2Rating?
openSkillRating OpenSkillRating?
bracketMatchupsAsP1P1 BracketMatchup[] @relation("BracketMatchupPlayer1P1")
bracketMatchupsAsP1P2 BracketMatchup[] @relation("BracketMatchupPlayer1P2")
bracketMatchupsAsP2P1 BracketMatchup[] @relation("BracketMatchupPlayer2P1")
bracketMatchupsAsP2P2 BracketMatchup[] @relation("BracketMatchupPlayer2P2")
@@map("players")
}
@@ -51,6 +54,7 @@ model User {
ownedTournaments Event[] @relation("TournamentOwner")
createdMatches Match[] @relation("MatchCreator")
sessions Session[]
activities Activity[]
player Player? @relation(fields: [playerId], references: [id])
@@map("users")
@@ -63,6 +67,7 @@ model Event {
description String?
eventDate DateTime?
eventType String @default("tournament")
tournamentType String @default("individual")
format String @default("round_robin")
status String @default("planned")
maxParticipants Int?
@@ -75,8 +80,16 @@ model Event {
participants EventParticipant[]
owner User? @relation("TournamentOwner", fields: [ownerId], references: [id])
matches Match[]
teams Team[]
rounds TournamentRound[]
activities Activity[]
// Team configuration fields
teamDurability String @default("permanent") // permanent, variable, per_round
partnerRotation String @default("none") // none, minimize_repeat, maximize_even, elo_based
allowByes Boolean @default(true)
teamConfiguration Json? // Additional configuration options
maxRosterChanges Int? // Maximum roster changes per player
requireAdminVerify Boolean @default(false) // For match score verification
@@map("events")
}
@@ -85,7 +98,6 @@ model EventParticipant {
id Int @id @default(autoincrement())
eventId Int
playerId Int
teamId Int?
seed Int?
status String @default("registered")
registrationDate DateTime?
@@ -93,30 +105,11 @@ model EventParticipant {
updatedAt DateTime @updatedAt
event Event @relation(fields: [eventId], references: [id])
player Player @relation(fields: [playerId], references: [id])
team Team? @relation(fields: [teamId], references: [id])
@@unique([eventId, playerId])
@@map("event_participants")
}
model Team {
id Int @id @default(autoincrement())
eventId Int
teamName String?
player1Id Int
player2Id Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
bracketMatchups1 BracketMatchup[] @relation("BracketTeam1")
bracketMatchups2 BracketMatchup[] @relation("BracketTeam2")
eventParticipants EventParticipant[]
event Event @relation(fields: [eventId], references: [id])
player1 Player @relation("TeamPlayer1", fields: [player1Id], references: [id])
player2 Player @relation("TeamPlayer2", fields: [player2Id], references: [id])
@@map("teams")
}
model TournamentRound {
id Int @id @default(autoincrement())
eventId Int
@@ -137,8 +130,10 @@ model BracketMatchup {
id Int @id @default(autoincrement())
roundId Int
eventId Int
team1Id Int?
team2Id Int?
player1P1Id Int?
player1P2Id Int?
player2P1Id Int?
player2P2Id Int?
matchId Int?
tableNumber Int?
bracketPosition Int?
@@ -150,8 +145,10 @@ model BracketMatchup {
event Event @relation(fields: [eventId], references: [id])
match Match? @relation(fields: [matchId], references: [id], onDelete: Cascade)
round TournamentRound @relation(fields: [roundId], references: [id])
team1 Team? @relation("BracketTeam1", fields: [team1Id], references: [id])
team2 Team? @relation("BracketTeam2", fields: [team2Id], references: [id])
player1P1 Player? @relation("BracketMatchupPlayer1P1", fields: [player1P1Id], references: [id])
player1P2 Player? @relation("BracketMatchupPlayer1P2", fields: [player1P2Id], references: [id])
player2P1 Player? @relation("BracketMatchupPlayer2P1", fields: [player2P1Id], references: [id])
player2P2 Player? @relation("BracketMatchupPlayer2P2", fields: [player2P2Id], references: [id])
@@map("bracket_matchups")
}
@@ -160,10 +157,10 @@ model Match {
id Int @id @default(autoincrement())
eventId Int?
playedAt DateTime?
team1P1Id Int
team1P2Id Int
team2P1Id Int
team2P2Id Int
player1P1Id Int?
player1P2Id Int?
player2P1Id Int?
player2P2Id Int?
team1Score Int
team2Score Int
status String @default("completed")
@@ -173,12 +170,13 @@ model Match {
isCasual Boolean @default(false)
bracketMatchups BracketMatchup[]
eloSnapshots EloSnapshot[]
activities Activity[]
createdBy User? @relation("MatchCreator", fields: [createdById], references: [id])
event Event? @relation(fields: [eventId], references: [id], onDelete: Cascade)
team1P1 Player @relation("MatchPlayer1", fields: [team1P1Id], references: [id])
team1P2 Player @relation("MatchPlayer2", fields: [team1P2Id], references: [id])
team2P1 Player @relation("MatchPlayer3", fields: [team2P1Id], references: [id])
team2P2 Player @relation("MatchPlayer4", fields: [team2P2Id], references: [id])
player1P1 Player? @relation("MatchPlayer1", fields: [player1P1Id], references: [id])
player1P2 Player? @relation("MatchPlayer2", fields: [player1P2Id], references: [id])
player2P1 Player? @relation("MatchPlayer3", fields: [player2P1Id], references: [id])
player2P2 Player? @relation("MatchPlayer4", fields: [player2P2Id], references: [id])
partnershipGames PartnershipGame[]
@@map("matches")
@@ -331,3 +329,34 @@ model OpenSkillRating {
@@map("open_skill_ratings")
}
model Activity {
id Int @id @default(autoincrement())
type String // "player_registration", "tournament_created", "match_completed", "partnership_recorded"
description String
userId String?
playerId Int?
eventId Int?
matchId Int?
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id])
player Player? @relation(fields: [playerId], references: [id])
event Event? @relation(fields: [eventId], references: [id])
match Match? @relation(fields: [matchId], references: [id])
@@map("activities")
}
model ClubSettings {
id Int @id @default(autoincrement())
clubName String @default("Euchre Club")
defaultEloRating Int @default(1200)
partnershipEnabled Boolean @default(true)
notificationsEnabled Boolean @default(true)
matchVerification Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("club_settings")
}