Repository navigation
Features
Jens Gross edited this page Oct 1, 2026
·
16 revisions
- League format: Traditional league with all teams competing across matchdays
- Swiss format: Swiss-system tournament with automatic pairing (see Swiss Pairing)
- Each season has a pool of eligible cars and tracks
- Season drivers are assigned to teams per season via
SeasonDriver - A team can leave a season once it holds no place in any of the season's phases and nothing in the season references it: no match, lineup, season driver, playoff place or team replacement. Other teams do not block it. When the last sub-team leaves, its parent leaves too, unless the parent is still referenced
- A season is divided into matchdays (race days)
- Each matchday contains one or more races
- Matchday lineups define which drivers from each team compete
- Each race is linked to a track and optionally to cars
- Race results record per driver: finishing position, qualifying position, fastest lap flag
- Scoring is automatic based on the scoring table:
- Race points: 20, 17, 14, 12, 10, 8, 7, 6, 5, 4, 3, 2 (positions 1-12)
- Qualifying bonus: 3, 2, 1 (top 3)
- Fastest lap bonus: 2 points
- Race attachments: upload files or add links (replays, screenshots, etc.)
- Teams can have a parent-child hierarchy (e.g., "CTC Alpha" and "CTC Bravo" under "CTC")
- Sub-teams allow organizing larger communities into competitive units
- Each team competes as a unit in matchdays and playoffs
- Brand Colors: Primary, secondary, and accent color per team (Color Picker + Hex input)
-
Logo Upload: Team logo stored via
FileStorageService - SeasonTeam: Per-season team data (rating, optional color/logo overrides)
- Sub-teams inherit colors and logo from their parent team via
getEffective*()fallback
- Generate 1080x1920px team card PNGs (Instagram Story format) from the admin UI
- Design: Gradient Fade — vertical gradient with team colors, rating circle, logo, points, and W-L-D record
-
Tool page at
/admin/tools/team-cards: season selector, card gallery with thumbnails, single/batch generation, PNG download, ZIP download - Rendering: Thymeleaf HTML template → Playwright headless Chromium screenshot
-
Sandboxed renderer: all graphics, custom templates included, render without JavaScript and without network access. Images and fonts must be embedded as
data:URIs, and team names are HTML-escaped -
Data sources: Rating from
SeasonTeam, points and record fromStandingsService, colors/logo viagetEffective*()fallback chain (SeasonTeam → Team → Parent Team) - Requires Chromium installed via Playwright (see Development Setup)
- Define which season drivers represent their team on a specific matchday
- Supports roster rotation across matchdays
- Identified by PSN ID (PlayStation Network) and an optional nickname
- Assigned to teams per season via
SeasonDriver - Driver profiles track participation across seasons
- Calculated from aggregated race results per team
- Updated automatically after each race result import
- Deleting a race, clearing its results, removing a scored guest from a lineup or merging drivers re-sums the match from its remaining legs. A match without any scored leg counts as unplayed again, not as a 0:0 draw
- A phase without match scoring still counts its games, wins, draws and losses but awards no match points. Without race scoring the phase accepts no race results; the results form says so
- Sum every phase of every season: regular, placement and playoff
- A playoff matchup counts once it has a winner, as one game however many legs it had. The winner gets the playoff phase's win points and the loser its loss points. Points for and against are the summed race points of all legs
- A bye is a win with 0:0 race points. A walkover credits the regular walkover score once and gives the forfeiting team no match points
- A running or reopened matchup does not count
- Cross-season rankings based on cumulative driver performance
- Managed by
DriverRankingService
- When teams are tied on points in Swiss format seasons, the Buchholz score (sum of opponents' points) is used as a tiebreaker
- Only opponents from the same phase count, and in a group view only opponents from that group's matchdays. A placement or playoff match never changes a regular phase's Buchholz
- Support for 4-team or 8-team brackets
- Best-of-legs format: each matchup consists of multiple legs (races)
- Bracket structure:
- 8 teams: Round of 8 -> Quarterfinals -> Semifinals -> Final
- 4 teams: Semifinals -> Final
- Bracket visualization in the admin UI
- Playoff results can be imported via the data import flow
- Winner rules: auto-determine needs results for every scheduled leg. A manual winner who does not lead on points after every leg (early completion, tie, adjudication) needs a reason, which the matchup shows. Race points stay unchanged
- Decided matchups are locked: results, lineups, legs and imports of a matchup with a winner are rejected until it is reopened
- Reopening needs a reason and is blocked while a later matchup has results or a winner. It removes the advancement into the next round and the revoked team's lineups there, and keeps schedules and results. Every decision and reopening is kept in the matchup's decision history
- Seeding freezes once any matchup has results or a winner, and the seeding page then becomes read-only. Before that, an empty slot is cleared on save, and an all-empty save removes the seed numbers
- Bye: a matchup with a single team and no legs can advance that team on a bye. It is rejected while an earlier matchup still feeds the open slot
- Walkover: a matchup with both teams and no results can be decided for the opponent of the team that forfeits. Reopening clears the bye or walkover
- Automatic round generation via
SwissPairingService - Pairs teams with similar point totals
- Rematch avoidance: teams that already faced each other are not paired again
- Each pairing gets the phase's configured number of legs, every second leg with home and away reversed. A bye stays a single race
- The next round needs an outcome for every pairing of the current one: a bye, a walkover, results or a score. A walkover pairing shows
w/oand takes no quick score - Generates balanced matchups for fair competition
- Master data: Full GT7 car and track catalog (importable via GT7 Sync)
- Season pools: Each season defines which cars and tracks are eligible
- Image upload: Car and track images stored locally
- Cars linked to race results, tracks linked to races
Detailed guide: Data Audit.
- Read-only check at
/admin/data-auditfor stale aggregates, duplicate or reversed pairings, phase and group inconsistencies, succession problems, playoff decisions and advancement, and ambiguous public URLs - Every finding carries its evidence and a proposed correction, marked as reconstructible, needing a decision, or not inferable
- Attach files (screenshots, replays) or links (YouTube, Twitch) to any race
- Two attachment types:
FILE(uploaded) andLINK(URL reference) - Managed via
FileStorageService
Available since v1.10. Detailed guide: Backup & Restore.
- Full database backup/restore at
/admin/backup— captures all 27 entity tables and theuploads/tree into a single ZIP -
Export: streaming ZIP download (
ctc-backup-<ISO-instant>.zip) withmanifest.json(schema version + per-table row counts) +data/<entity>.json+uploads/ - Import: preview screen with row-count diff and schema-version check; replace-all transaction — database is only touched after explicit confirmation
- Schema-version gate: accepts schema versions 1, 2 and 3 and rejects any other version with HTTP 400 before any DB write
-
Auto-backup-before-import: every import is preceded by an automatic ZIP at
<import-backups-dir>/<timestamp>/auto-backup-before-import.zip, retained for 24 hours along with the previousuploads-old/tree - Import lock: while an import runs, every admin page shows a yellow read-only banner; concurrent writes are rejected with HTTP 503
-
Audit log: every import is recorded in the
data_import_audittable - Operator runbook for manual recovery:
docs/operations/import-runbook.md