Release Notes

Changelog

Everything we've shipped on RoundsUp, in order.

RoundsUp Changelog

Versioning follows a Stern Pinball-style model. Everything before V1.0 is pre-release. V1.0 is feature complete. V2.0 is a major content drop.


V1.1 - May 24, 2026

Built for league night. What started as "functional but feels like multiple admin pages glued together" is now one coherent cockpit at roundsup.app/<your-league>/admin/meets/<id>. The single screen TDs run league nights from.

One screen, everything in reach. Top bar shows your league name and a meet switcher. Status pills sit where they belong. Live scores stream in a table that flashes new entries red as they arrive. A right rail keeps recent edits, quick actions, audience-view links, and (for VN League) Send-to-Shane within glance distance. Group cards show progress, players, and per-player Mark Out at a glance.

Add players mid-setup, not just to a roster. Quick Add Player on the right rail opens a drawer that adds a player to a specific group in tonight's meet — not just to your org roster. Tab into "Existing Player" to pick from your roster (filtered to people not already in this meet), or "New Player" to create someone fresh. The group picker shows capacity upfront — G2 (3/4) joinable, G4 (4/4 full) disabled, G3 (3/4 ✗ scoring active) disabled when scoring has begun. No 409 surprises after you click Save.

Fix a mistake, undo it. Every score edit now writes to an audit log. The Recent Edits panel in the right rail shows the last 50 edits with a one-click ↶ Undo on each. Click the wrong DQ button? Edit a score wrong? One click reverts the change, points recalculate, and the reversal itself shows up in the feed (so you can un-undo if needed). Edits stay undoable until the meet is marked complete.

Edit a score the right way. The score edit modal now lets you reassign a row to a different player (if the original player attribution was wrong). The dropdown shows only valid swap candidates — players who don't already have a score on that machine. The Save button stays disabled until something actually changed. ESC closes, edits surface in the Recent Edits feed instantly.

Scoring fix for A-vs-Everyone meets. Two pre-existing bugs in the points calculator surfaced during V1.1's wellness pass and shipped fixed: DQ players were getting 0 points instead of sharing the remaining slots (a 4-way DQ now correctly distributes the full point pool); and disqualified A-division players were still triggering bonuses for everyone who "beat" them with a positive score. Both bugs only manifested in A-vs-Everyone meets with at least one DQ. The fix was applied to past meets — Meet #8's scores recomputed cleanly with full audit trail.

Meet list polish. When you don't have an active meet, the "Create New Meet" call to action takes the page. When you do, it shrinks to a small button so the active meet is the main thing on screen. Meet-list rows lose their "Complete" button — completing a meet now lives in the cockpit only, the source of truth for status changes.

Edit your roster, then jump back. The standalone Players and Machines pages each got an "Open in cockpit ↗" button. One click takes you back to wherever you're running tonight's meet.

Smaller fixes. The RoundsUp wordmark now scales down on mobile so it doesn't overflow on small phones. The auth screens (login, signup, password reset) also got mobile wordmark sizing. The Setup pill on an active or completed meet is now disabled with a tooltip explaining it'll come back in V1.2.

What's coming in V1.2. Three TD asks land in V1.2: a setup-mode editing route so you can adjust groups, players, or machines after creating a meet without deleting and recreating; a clean split between "Step Out" (player paused, scores preserved) and "Disqualify Player" (admin DQ, scores zeroed); and a group-level bulk machine reassign for when scores were entered against the wrong machine.

Behind the scenes. The cockpit's polling layer is now in a useMeetData hook, paving the way for a future Supabase realtime swap-in. Score edits are captured by a single shared backend helper so PUT and Undo always behave identically. The old /papa-view route is deleted (Standings + Summary + Public cover the same surface). The score calculator has its first proper test suite to catch regressions.


V1.0 - May 19, 2026

RoundsUp goes public. A scoring tool for one league becomes a platform any league can be found on. Plus a new name: we're RoundsUp now, not On Tilt.

Find any league at /leagues. The new public directory lists every league running on RoundsUp. Each card shows the league name, location, recurring schedule, and a link to that league's homepage. No login required. Built for the moment someone mentions you in a Discord and a stranger asks "wait, where's that?"

Every league gets a homepage. Visit roundsup.app/<your-league> and you'll see a page built around the league: name, location, tagline, recurring schedule, recent meets, current machine lineup. Updates as meets happen. Share the URL anywhere — Twitter, Discord, iMessage — and the preview card renders with a custom image featuring your league's name (not a generic site preview). This is the page to send people when they ask about your league.

Built to be fast. Every public page is cached close to every visitor. Loads in under a second from anywhere. When 100 curious readers click your league's URL at once, they all see it instantly. We built this assuming it'd someday matter.

Your league's data is yours. Every API endpoint that returns league data verifies the caller is authorized for that specific league. No path exists for one league's admin to see another league's meets, players, machines, or scores — even by passing the right query parameters. Hardened during V1.0, ahead of any second league signing up.

Legacy continuity. The old ontiltleague.com still works exactly as before; vn-league members see zero change.

What's next. V1.1 begins now, focused on the tournament-director experience: distributed scorekeeping, real-time activity on the admin dashboard, edit/undo on scores, and IFPA player rankings. Watch this page or follow the status page.


V0.98 - May 18, 2026

Rebrand to RoundsUp. User-visible platform branding renamed from "On Tilt" to RoundsUp, separating the SaaS product from the OnTilt Pinball venue brand. Text and metadata only — no functional changes.

  • Wordmark, page titles, metadata, and platform copy renamed across all public, auth, admin, score-entry, and TV pages
  • Slogan "Score it. Send it. Done." kept; appears next to the new RoundsUp wordmark
  • TV broadcast and standings print headers now render the org name dynamically (e.g., "VN League") instead of platform brand — these surfaces are about the league, not the platform
  • Metadata title set to RoundsUp · Find pinball leagues
  • Slug-input preview in /create-org switched from ontiltleague.com/{slug}/admin to path-relative /{slug}/admin
  • Domain transition: roundsup.app is the new canonical domain. ontiltleague.com continues to work and now renders RoundsUp branding — DNS/Vercel work tracked separately
  • package.json and package-lock.json name fields updated to roundsup-app
  • Internal identifiers unchanged: CSS variables (--ontilt-*), CSS classes (tilt-wordmark, ot-red, ot-black, etc.), file/directory names, code symbols, and DB schema (org_id, org_members, etc.) all stay
  • RESERVED_SLUGS additions from V1.0 work (roundsup, ontilt, on-tilt) protect both old and new brand names from being claimed as org slugs
  • Historical CHANGELOG entries for V0.85–V0.97 preserved as-is

V0.97 - May 14, 2026

Real auth, members-only meets, multi-tenant cleanup. Replaces the shared admin password with per-user Supabase Auth, adds opt-in members-only meets that require sign-in to enter scores, and closes the multi-tenant scoping gaps left open after V0.96.

Auth:

  • Supabase Auth replaces the custom ADMIN_PASSWORD cookie. Legacy cookie still works during the hybrid period; V0.98 will deprecate it.
  • Signup with email verification via Resend SMTP, password reset flow, profile page at /profile for display name and IFPA edits
  • Onboarding flow: signup → verify email → /create-org → become super_admin of the new org via the atomic create_org_with_admin RPC
  • Logout clears both auths (legacy cookie + Supabase session) and lands on /login
  • Display name now syncs from profile to every org_members row on update so admin views stay current
  • OrgRole type fixed to match the DB CHECK constraint (super_admin | org_admin | scorekeeper | viewer). Pre-V0.97 the code compared against 'admin', which doesn't exist as a role — any non-super_admin would have been silently locked out of admin routes. Caught and fixed before a second admin was ever created.

Players / IFPA:

  • papa_id column dropped; replaced with optional ifpa_id integer per player per org
  • All PAPA-facing strings removed from schema, code, and UI. CSV export header now ifpa_id
  • "PAPA View" admin link/heading renamed to "Score Mirror". URL path /papa-view/ kept for stability; folder rename deferred to V0.98

Meets / members-only:

  • New per-meet members_only toggle on the create-meet form. When ON, only signed-in org members can enter scores; the score URL renders a MEMBERS ONLY wall with a Sign In button for anonymous visitors. Enforcement is at both the API (hard gate) and client UI layers.
  • Per-group scorekeeper token system removed entirely. Old /{org}/score/{token} URLs return 404.
  • Offline submission queue is now auth-aware. Queued scores that hit a 401 stay in localStorage with a needs_auth flag and surface a "N scores need sign-in to submit" banner with a Sign In CTA. Previously they were silently dropped on the first failed retry.

Multi-tenant correctness (V0.96 follow-through):

V0.96 moved all pages under /{org}/... but several enforcement layers were missing. With only one org (vn-league) in production, none of these gaps surfaced — each would have failed the moment a second org was created. This segment closed them:

  • All API routes filter by org_id on reads (19 routes covering meets, scores, group-players, extra-balls, score-table, send-email, papa-view, archive, heartbeat). Cross-org meet access by direct UUID now returns 404 instead of leaking data.
  • app/[org]/meet/[id]/score/page.tsx brought in line with its three siblings (public/standings/summary) — verifies the meet belongs to the URL's org slug
  • All revalidatePath() calls now target /{slug}/... paths (~72 calls across 13 routes). Pre-V0.97 these were no-ops against the actual cached page paths
  • Hardcoded /admin/... and /meet/... Link targets prefixed with /{org}/... throughout admin UI. Pre-V0.97 they worked only via a next.config.ts redirect hardcoded to vn-league
  • MeetScoreClient receives org as an explicit prop and threads it through every API call and internal Link. Side effect: this is what finally fixed the members-only gate from firing reliably — the broken <Link href="/meet/[id]/standings"> was generating an _rsc=h1599 404 in the console, and pre-Commit 19 the fetches couldn't return the members_only flag at all because they weren't org-scoped.

Schema:

  • Migration 010: drops players.papa_id and groups.scorekeeper_token; adds players.ifpa_id, meets.members_only, org_members.display_name
  • Migration 011: create_org_with_admin SECURITY DEFINER RPC for atomic org + super_admin creation
  • Row-level security enabled on all 10 tenant tables. Service-role API routes bypass RLS as expected; anonymous score writes are gated by the can_anon_write_score() SECURITY DEFINER function which checks the meet's members_only flag.

Bug fixes (incident log):

  • Meet creation broken in production for ~1 hour after migration 010 deployed (May 14 morning). Migration 010 dropped papa_id and scorekeeper_token columns but the meet-creation API still attempted to insert them. The first admin who tried to create a meet got a 500. Hotfixed same morning in commit c956a37.
  • Send-email route broken silently for the same period. /api/admin/meets/[id]/send-email selected scorekeeper_token in its Postgres query. After migration 010 dropped that column, the query errored. Anyone trying to email meet results would have gotten a failure. Probably nobody used it during the window. Fixed in this segment (Commit 13 of the segment, after the original feature work landed).
  • Heartbeat endpoint quietly read a non-existent column for months. /api/meets/[id]/heartbeat selected meets.updated_at, which has never existed on the meets table — only on scores. Postgres silently returned the row without the field, leaving meet.updated_at as undefined. The last_updated calculation accidentally returned correct values via null-coalescing on the latest score timestamp, so nothing user-visible broke. Cleaned up in Commit 18 of this segment: select is now accurate, calculation simplified to use the score timestamp directly.
  • retryQueue 401 data-loss bug. Prior implementation treated any 4xx response (including 401 members_only) as a permanent client error and silently dropped the entry from localStorage. A user whose session expired mid-night with queued offline scores would have lost them. Fixed to preserve needs_auth entries and surface a sign-in banner (see Meets section).

Known issues (deferred to V0.98):

  • Admin "list all" GET endpoints still return cross-org data. /api/admin/meets, /api/admin/players, /api/admin/machines, /api/admin/attendance were not in this segment's scope. Severity low today (single org; admin-facing only) but must-fix before any second org signs up.
  • extra_balls table has no org_id column. Migration 009 didn't add it. The three extra-ball API routes currently defend via the parent meet's org_id — functionally sufficient, but adding the column directly would provide defense-in-depth.
  • ADMIN_PASSWORD deprecation. Hybrid auth period continues. V0.98 drops the legacy code path once real Supabase Auth signup is verified end-to-end on production.
  • /papa-view/ folder rename. UI strings already say "Score Mirror"; URL path follows in V0.98.

Breaking changes:

  • Old scorekeeper token URLs (/{org}/score/{token}) return 404
  • players.papa_id column dropped; external integrations depending on it must migrate to players.ifpa_id
  • Email CSV export header changed from papa_id to ifpa_id
  • Cross-org meet access by direct UUID now returns 404 (was previously allowing access — no real-world impact since only one org existed)

V0.96 - May 13, 2026

Multi-tenant foundation. The app now supports multiple leagues sharing one database, each scoped to their own org.

  • Path-based org routing: all pages now live under /{org}/... (e.g. /vn-league/leaguenight, /vn-league/admin)
  • Legacy redirects: /leaguenight, /tv, /score, /admin permanently redirect to /vn-league/... — Yodeck TV URLs remain functional
  • Org validation layout: invalid slug format and unknown orgs return 404; inactive orgs show a holding page
  • OrgProvider context: org slug available to all client components via useOrg() hook
  • org_id column added to 8 tables (meets, groups, players, machines, scores, attendance, group_players, group_machines) with NOT NULL constraint; existing rows backfilled to vn-league org
  • All INSERT/UPSERT operations now set org_id — admin routes accept orgSlug in body and look up the org
  • All server-side reads now filter by org_id — leaguenight, TV, meet pages, and admin dashboard are fully scoped

V0.95 - May 9, 2026

Post-launch polish day. Five fixes shipped within 24 hours of going live.

  • Flexible roster editing on Assign Groups screen. Admin can add or remove players at any step without navigating back. Removed players are evicted from group slots automatically.
  • shortName() helper rewritten to parse the single name column. Last initials now show on /leaguenight, standings, /tv, PAPA view, score entry, and confirmation overlay.
  • Score popover on standings replaced with bottom-sheet modal. Full comma-formatted scores no longer clipped on mobile.
  • EB (Extra Ball) indicator now shows a red dot next to player names across all games in a meet, persistent visual reminder for the scorekeeper.
  • EB persistence migrated to per-game scores.extra_ball boolean column (migration 008). State now survives navigation, reload, and reconnect.
  • TV flash made longer and more aggressive (10s, 3 pulses, hard border outline) for better visibility on venue TVs.

V0.90 - May 7, 2026

Live launch. Real users, real scores, zero downtime.

  • 30+ players attended at AYCE Gogi Van Nuys
  • First score: Moriah, 770,130 on High Speed
  • Beta framing: "Hey guys, we're beta testing this new software I built. Let's double-do our scores. Give me bugs, give me feedback."
  • Result: Everything logged in, no downtime, no failures, all groups completed, winner screen displayed correctly

View early development history

V0.85 - May 7, 2026 (pre-launch)

Pre-launch hardening pass.

  • DQ scoring fix: DQ = 0 points everywhere (was incorrectly receiving last-place points)
  • A vs Everyone bonus on DQ'd A player corrected
  • PAPA mirror view added for Shane's score entry workflow
  • 4th-player row clipping in PAPA view fixed
  • Double-submit guard on score submission
  • Negative score validation
  • Connection timer
  • Error state clearing
  • Admin route guards
  • Custom domain ontiltleague.com configured (apex A record to 76.76.21.21 in Cloudflare, www CNAME via Vercel)
  • Apex domain redirects to /leaguenight
  • Admin login redirect bug fixed (hard navigation after cookie set)
  • Cache invalidation for player-dependent pages on PATCH
  • Machine pick confirmation modal + edit machine before scoring

V0.80 - May 6, 2026

Bulletproof score submission released.

  • Full-screen confirmation overlay with raw scores prominent
  • localStorage drafts auto-save every 500ms
  • Offline queue with auto-retry on connection recovery
  • 3-tier connection recovery (silent reconnect → yellow banner → red TAP TO REFRESH)
  • Raw scores shown in submission overlay for verification
  • Connection recovery hook integrated

V0.70 - May 5, 2026

A vs Everyone tournament rule and TV broadcast hardened.

  • A vs Everyone rule shipped (+1 bonus to non-A players who beat the A player on raw score)
  • DQ/OUT points calculation corrected
  • Group completion gate added
  • Scroll-to-top fix on game change
  • Extended TV flash duration
  • Admin meet copy raw score only
  • Status transition fixes (open → active → complete → submitted)
  • Latest 20 scores by default with expand button on admin score table
  • Past meets archive view added
  • Select All on attendance picker

V0.60 - May 4, 2026

Brand and design pass.

  • Renamed from TILT to On Tilt
  • Last Game Spotlight added on /tv
  • Card layout overlap fix
  • Spotlight replaced with slim ticker for better screen real estate
  • TV layout supports up to 14 groups (5×3 grid)

V0.50 - May 3, 2026

TV broadcast mode for Yodeck.

  • /tv view added with all groups, scores, current machine
  • TV display mode for Yodeck kiosk (no chrome, full screen)
  • Smart 60s heartbeat for connection health
  • 10-minute hard reload safety net (Yodeck-specific)
  • Dual-layer refresh strategy for WebSocket recovery

V0.30 - May 3, 2026

Admin and machine management.

  • Admin dashboard with live score table
  • Machine active/inactive toggle
  • Machine delete
  • Status management UI

V0.10 - May 5, 2026 (initial commit retroactively dated to project start)

Initial scaffolding.

  • Next.js 15 App Router setup
  • Supabase integration (Postgres + auth)
  • Score entry skeleton
  • Group and meet data model
  • Vercel deployment