PLATFORM UPDATES

CHANGELOG

Every update, fix, and improvement to TFCL PUG, newest first.

LATEST
v0.196 2026-08-19 12:00:00

Released: League admin panel — run your own league like RGL/UGC/ETF2L

  • The Seasonal Leagues system was redesigned into a "league-in-a-box" platform. Every league now has a dedicated admin panel at /l//admin for owners and operators, with an Overview setup checklist, Settings (owner-only league rename — your /l/ links never break), Branding (banner, logo, custom CSS), Header & Footer (switch to fully league-owned page chrome with your own navigation and footer), Custom Pages (create public rules/announcements/contact pages), Staff management, Seasons, and an owner-only Danger Zone to archive or restore the league. The public league page was rewritten as a clean visitor-facing page, the creation wizard gained one-click rules templates (competitive or casual) and now drops new owners straight into their admin panel with a welcome walkthrough. Also fixed a bug that made league custom pages (/l//) unreachable.
v0.192 2026-08-16 20:38:43

Opt-in league-owned chrome

  • League pages keep the shared TFCL layout by default. Operators can opt into league-owned header and footer chrome while retaining league branding, custom pages, links, and CSS controls.
v0.191 2026-08-16 20:16:33

League-owned page chrome

  • League owners and operators can replace the shared TFCL header and footer with sanitized custom HTML while required TFCL Play attribution remains visible.
v0.190 2026-08-16 19:36:14

League header and footer customization

  • Adds league-owned header title, logo, navigation, footer copy, links, layout, and colors while preserving the required TFCL Play attribution.
v0.194 2026-08-16 12:00:00

Released: Sixes lobby visual refresh

  • Improved the Sixes lobby presentation and class selection visuals. The homepage Sixes queue now uses the shorter "Queue up. Play Sixes." headline without the previous eyebrow label, and the /pug/sixes class signup cards now use verified high-contrast TF2 class artwork for Scout, Soldier, Demoman, and Medic. Map vote thumbnails remain available across the RGL Season 20 pool. Also clarified the Rules-page ban policy so bans from other leagues are not automatically mirrored, while TFCL Prime bans continue through the separate verified integration.
v0.185 2026-08-06 17:06:51

League rules and season descriptions

  • Allows operators to publish league rules and edit descriptions for leagues and seasons.
v0.184 2026-08-06 15:52:30

League page branding

  • Allows league operators to customize the public league page with a header image and custom CSS.
v0.183 2026-08-06 12:42:14

Seasonal weekly maps

  • Allows league operators to assign a map to each regular-season week.
v0.182 2026-08-06 12:20:02

Relative seasonal roster lock

  • Allows roster changes during the regular season and locks them a configurable number of weeks before playoffs, defaulting to two weeks.
v0.181 2026-08-06 11:52:09

Persist league division names

  • Stores reusable division names at the league level so future seasons can reuse and edit the same divisions.
v0.180 2026-08-06 11:33:45

Separate round generation and match schedule timing

  • Allows operators to generate rounds on one recurring day while assigning matches to a separate default day and time.
v0.179 2026-08-06 11:10:47

Season match communications and private server information

  • Adds public match discussion threads and leader-only server connection details with a TFCL reservation link.
v0.192 2026-08-06 11:01:07

Configurable seasonal match and roster timing

  • Operators can set the first match due time, recurring weekly match day and time, and the roster lock deadline when creating a season.
v0.191 2026-08-06 10:50:11

Canonical seasonal league URLs

  • Seasonal league pages now use /l/ and /l//; the original league 1 uses the canonical slug /l/ultiduo.
v0.176 2026-08-06 10:39:51

Self-service seasonal TF2 leagues

  • Adds reusable leagues, seasons, up to five divisions, weekly Swiss-style team pairings with one to four matches per team, and configurable eight or sixteen team single-elimination playoffs.
v0.175 2026-08-06 10:13:46

Tournament Swiss brackets

  • Tournament creators can choose Swiss competition with 1-7 rounds. Pairings are generated round by round from standings, prefer equal-score opponents, avoid rematches, award deterministic byes for odd fields, and finalize standings after the last round.
v0.187 2026-08-01 05:16:03

Fixed: orphaned Vultr servers with no database record now get cleaned up automatically

  • A site-wide safety-net sweep now periodically cross-checks the actual list of running Vultr servers against every place TFCL Play tracks them, and destroys any real server that somehow has no record anywhere (e.g. if a creation call succeeded but the follow-up database write failed). Also broadened the existing expired-server cleanup so it runs from high-traffic pages instead of only the page that created the reservation.
v0.183 2026-07-31 07:09:07

SDR everywhere: PUG lobbies + tournament servers now use fake-IP

  • Extends the Steam Datagram Relay (SDR) fake-IP DDoS protection rolled out in v0.182 to the entire server network. Every player-facing connect string (self-service /servers page, PUG lobby connect box, tournament server connect info) now prefers the SDR fake-IP address once Steam has allocated one, falling back to the real server IP for the short window (roughly 1-2 minutes after boot) before allocation completes. SDR is an official, Valve-documented feature built specifically for third-party dedicated server operators - see partner.steamgames.com/doc/features/multiplayer/steamdatagramrelay - so this is fully compliant with Valve's Terms of Service.
v0.175 2026-07-25 00:20:39

Fixed "Generate Bracket" creating hundreds of fake placeholder teams instead of a real bracket

  • Generating a bracket sized it off the cup's registration cap (how many teams were ALLOWED to sign up) instead of how many teams actually registered - so a cup allowing up to 128 teams but with only a handful of real signups got padded out with over a hundred fake placeholder teams, making the bracket look broken or empty. Fixed to size the bracket off the real team count. The one cup that had already hit this (Ultiduo Cup #1) has been reset to a clean pre-bracket state so it can be regenerated correctly.
v0.174 2026-07-25 00:04:33

Fixed cup bracket generation error (D1_ERROR: no such table cup_teams__legacy_teams_cap)

  • Generating a bracket for any cup (including Ultiduo cups) was failing with a database error. Root cause: a schema-rebuild migration from several versions ago missed repairing one table's links after a table rename, leaving it pointing at names that no longer existed. Fixed by rebuilding that table with the correct links. No cup or bracket data was lost - the affected table had never successfully stored a row because of this bug.
v0.161 2026-07-20 05:42:43

Server Pool: renamed + plan type support + manual admin provisioning/destroy

  • The persistent server pool (previously "Ultiduo Pool") now supports both ultiduo and sixes/highlander server plans.\n- Admin panel routes renamed from /ultiduo-pool to /pool with new plan_type parameter.\n- Admins can now manually provision a pool server of any plan type, and destroy a pool server at any time (even if busy).\n- Under the hood: the table is renamed from ultiduo_server_pool to server_pool and gains a plan_type column.
v0.159 2026-07-19 02:54:24

Ultiduo servers: instant availability + 24h auto-update rotation

  • Ultiduo lobbies now provision their server the MOMENT the lobby is created, not after gathering/ready-check/voice-check finish - no more waiting on a cold Vultr boot right when you expect to connect.\n- One Ultiduo server now runs persistently 24/7 as an always-ready baseline. If it's already in use when a new lobby is created, a second on-demand server boots immediately so there's always at least one instantly available.\n- The persistent baseline server is automatically reinstalled (fresh Docker image pull) once every 24 hours, but only while idle - a live match is never interrupted.
v0.158.16 2026-07-18 09:00:00

Hotfix: Roster-size banner showed "?" placeholders, and departed teammates still counted against the cup's roster cap

  • After v0.158.15 fixed the "Network error" crash, captains hitting the `team_too_large_for_cup` / `team_too_small_for_cup` 409 saw the inline banner render with garbage placeholders: *"this cup's ceiling is ?, your roster has ? ... Remove 0 players to register."* Separately, captains with a roster that correctly fit the cup's format were still rejected as "too large" — reported live on Ultiduo Cup #1 (cup #8) for a 2-player team against a 2-player cap.
  • *Two independent bugs.**
  • 1. **"?" placeholders.** The register page's `call()` helper returned only `{ __error, __status }` when the server responded with non-2xx, silently dropping the `rescue` object (ceiling, roster size, surplus player ids) and the human-readable `message`. `showRosterAlert()` rendered with `rescue` fully `undefined`. **Fix:** `call()` now preserves the full server response body on error, spreading it before adding the `__error`/`__status` markers.
  • 2. **Departed members inflating roster count.** The query that auto-fills a cup roster from a captain's `/teams` row never excluded team members who had since left the team — every historical member of the parent team was still counted toward the cup's player cap. A team with 2 active players but 1 former member showed up as a 3-player roster. **Fix:** all three roster auto-fill queries in `cups-db.ts` now filter `WHERE team_id = ? AND left_at IS NULL`, matching the pattern in `teams-db.ts`.
v0.158.15 2026-07-18 08:00:00

Hotfix: "Network error" on register was actually a client-side crash hiding the real roster-size banner

  • Right after v0.158.14 shipped the correct `team_too_large_for_cup` / `team_too_small_for_cup` 409, captains hitting that 409 saw the register form report a generic "Network error" instead of the intended inline roster-size banner. Console showed `esc is not defined`.
  • *Root cause.** The register page's inline `