All notable changes to this project are documented here. Format loosely
follows Keep a Changelog.
[Unreleased]
Added
- Round results. A completed round's page (
/scores/rounds/[roundId]) now opens with results: each shooter's five station scores and total, ranked within the squad, with your own row highlighted and perfect stations in brass. The score sheet stays below. When a scorekeeper completes a round, their screen goes straight to these results with a "Score the next round" button. Opening a completed round from /account, a shooter profile or an event shows the same results. (A-073) /account now has a real home for anyone with a linked roster spot: your next event (with your trap if you're squadded), your five most recent completed official rounds, and your numbers (rounds shot and the average per round for each discipline, official rounds across all clubs). People without a linked spot see the same page as before. (A-022)- Scoring grid on a phone: the Sub and Total now appear the instant a station's last target is scored, instead of after the score syncs. At that moment the grid scrolls so the finished station (its cells and its Sub) sits right beside the shooter names, and the name column now sits flush at the left edge so the station isn't clipped. The first tap into the next station then moves the grid on to that station (Buttons mode included). Previously the view only moved on that first tap, and the Sub lagged behind. Applies to singles and doubles. (A-034)
- Draft Terms of Service (
/terms) and Privacy Policy (/privacy), clearly marked as drafts pending lawyer review. The footer links to both. Sign-up now has a required "I agree to the Terms and Privacy Policy" checkbox, enforced on the server as well. Contact email and mailing address are placeholders in src/lib/legal.ts until added. (A-002, A-003) - Session refresh now covers
/account and /dashboard as well as /scores, so an expired sign-in token is renewed before the page loads instead of looking signed out. Public pages are unaffected. Removed the unused root middleware.ts. (A-068) - Unfollow and remove-follower. On
/account, a follower can unfollow and a shooter can remove a follower, each with a confirm step; access ends immediately and the link disappears for the other person. (A-005) - Fix for a bug found while building this: the a3-3 viewer_links policies queried their own table, so any signed-in read of viewer_links failed with "infinite recursion detected in policy". New migration
20261030000000_viewer_links_policy_recursion_fix.sql (written, NOT applied) moves the checks into security-definer helpers. The RLS test run now covers revoke. - Account settings at
/account/settings: change your name, email and password. An email change sends a confirmation link and only takes effect once opened; passwords follow the same 8-character rule as sign-up. Linked from /account. Date of birth and account deletion are separate items. (A-004) - Password hint on sign-up and reset-password. While typing, the password field shows how many characters are left, then a Weak / Fair / Strong note once it reaches 8 characters. Submitting under 8 characters is blocked with a clear message. The rule lives in one shared validator (
src/lib/password-rules.ts) with unit tests; Supabase still enforces the real rule server-side. (A-001) - Signed-in navigation. Pages under
/scores, /account and /dashboard now share a shell-green bar with Scores, Account, Dashboard (only for people with a club role) and Sign out, with the current section underlined. On a phone the links sit on a second row. The bar scrolls away with the page (the live scoring grids keep their own sticky headers) and is hidden when printing. The separate Sign out buttons on the account and dashboard pages are gone, and the landing page's signed-in link now says "Go to your account" instead of sending non-club accounts to the dashboard. - Forgot password. The sign-in page has a "Forgot password?" link to
/forgot-password, which emails a reset link; the link signs you in via /auth/confirm and lands on /reset-password to choose a new password (8+ characters). The confirmation message is the same whether or not the address has an account. Needs the Supabase "Reset password" email template pointed at /auth/confirm (see PR notes). - Sign-in now returns you to where you were headed. Opening a
/scores page while signed out sends you to /login?next=<that page>, and signing in drops you back on it instead of always on /account. Return paths are checked so only same-site paths are honoured (/auth/confirm uses the same check). - A tested rollback for a3-6 (
supabase/rollback/20260909000005_rls_rewrite_rollback.sql). It restores the exact pre-a3-6 policies in one transaction; npm run test:rls now also proves rollback then re-apply works, and it was run on a real scratch project too. npm run test:rls - a Docker-free check that applies every migration to an in-process Postgres and runs the a3-6 RLS matrix; wired into CI. It caught two bugs in the still-unapplied a3-6 migration/test (a function-ordering error that would have failed db reset, and an invalid fixture UUID), both fixed.- Automated tests (Vitest) for the scoring core - round/station scoring, ATA classification, CSV import validation and the offline score outbox - plus a GitHub Actions CI workflow that runs lint, typecheck, tests and a production build on every pull request.
Changed
- Brand polish pass (in progress). The whole app is being brought onto the locked brand system - paper background, shell-green primary, brass and clay-orange accents, Barlow Condensed headings, Public Sans body, IBM Plex Mono tabular numerals in grids and tables, 8/12px radii, hairline borders instead of drop-shadowed boxes, light-only. Done so far: token system + fonts wiring; landing page rebuild; the public
/scores surface (event list, shooter averages, shooter profile with its trend/station/round-history views, readonly scoring grids); /login and /changelog; and the full scorekeeper dashboard - roster and event management, the live singles/doubles scoring grids and the button-mode sequential entry, round complete/void/delete controls, the sync-status indicator, and the historical CSV import wizard. Scoring marks are unchanged in shape - brand only adds colour (brass = hit, faint primary = miss), and the live scoring grids use the same soft brass wash for a scored hit as the read-only /scores grids rather than a solid fill. The printed ATA score sheet is deliberately left as a black-on-white facsimile; only the screen chrome around it is branded. - Identity foundation (slices a3-1 … a3-6). The move to one account per person with approval-gated club roles, laid in one migration sequence. a3-1 … a3-6 are all applied and live. a3-6 (RLS rewrite + public gate) went to production on 2026-10-15 after validation on a scratch Supabase project; signed-out visitors now get nothing from the API and only signed-in accounts can read the directory and official scores. The individual slices:
- Identity foundation, slice a3-1:
profiles (the scorekeeper-only person table, carrying club_id) is retired. accounts is now the single person record and club membership is sourced from club_roles. A signed-in account with no club role gets a "No club assigned" state on /dashboard instead of being bounced to login. The Grid/Buttons scoring preference moved from profiles to accounts; no scorekeeper action changes. Requires migration 20260909000000_person_table_merge.sql. - Identity foundation, slice a3-2 (backend only, no visible change): the club role catalog gains
club_deputy and renames scorekeeper to official_scorekeeper; each club now has exactly one primary (club_manager), enforced by a unique index, with a deputy cap of 3. Adds an accounts.is_platform_admin flag and helpers (is_club_manager_or_deputy(), is_platform_admin()) for later slices. Role management is still dashboard/DB-seeded — no UI yet. Requires migration 20260909000001_club_roles_v2.sql. - Identity foundation, slice a3-3 — minor protection. Sign-up now asks for a date of birth (required) and runs a server-side age check: under 13 can't create their own account (a parent must); 13–17 create a managed account (a parent can claim the managing link, and until then the club approves follow requests); 18+ is unchanged. Sign-up moved from a browser-only call to a server action in the process. Adds
accounts.date_of_birth / is_managed_override, shooters.date_of_birth, the is_managed() / age_years() / ata_age_class() functions, and a viewer_links.is_controller flag with its supervision RLS rules. Requires migration 20260909000002_minor_protection.sql. - Identity foundation, slice a3-4 (schema only, no visible change):
rounds gains the columns that later separate official from practice scoring and track official-round verification — is_practice, team_id, entered_by, verification_status (draft/pending_verification/locked/amended), verified_by_shooter_id, verified_at, self_attested. Existing completed rounds backfill to locked, everything else to draft. Nothing in the app reads or sets these yet. Requires migration 20260909000003_round_context_and_verification.sql. - Identity foundation, slice a3-5 — account-level averages. A shooter profile page (
/scores/shooters/…) for someone matched to their account now combines every club they shoot at into one set of career averages, trends, and station breakdowns, instead of showing each club's roster entry separately; the header notes "combined across N clubs" when it's more than one. Unmatched roster entries are unchanged. Practice rounds (once they exist) are excluded from these public averages. Adds the account_discipline_totals view. Requires migration 20260909000004_account_level_stats.sql. - Identity foundation, slice a3-6 — the RLS rewrite and public-site gate. Written and merged, but not applied to any database yet, so it has no effect on the live site. When it lands it will make every scores/roster/event read require a signed-in account (matching the
/scores redirect already in place), keep official results visible to any signed-in user while practice rounds stay private to the shooter, and scope official-round entry to club managers/deputies plus per-event assigned scorekeepers. Adds the event_scorekeepers assignment table and a dormant has_active_plan() hook for the future billing tiers. Ships with a per-rule spec (docs/a3-6-rls-spec.md) and a pgTAP matrix (supabase/tests/a3-6_rls_matrix.test.sql); promotion is gated on a branch-database validation run. Migration 20260909000005_rls_rewrite.sql. - Identity foundation —
/scores sign-in gate (the app half of a3-6, live now). The scores area (/scores — event results, round grids, shooter profiles) requires a signed-in account; a logged-out visitor is sent to the sign-in page. This lands ahead of a3-6's database-level enforcement so the redirect is in place before the RLS flip. The landing, pricing, sign-in, sign-up, and changelog pages stay open. src/app/scores/layout.tsx.
Fixed
- Live scoring: a fast second tap could land on the wrong cell instead of advancing, and an undo could silently revert. Both came from the same race - a synced write was dropped from the local outbox the instant the server acknowledged it, which left a brief window where the screen fell back to the not-yet-revalidated (stale) server value and "forgot" the tap. The outbox now keeps every local write (synced or not) as the authoritative value for its cell until a new tap or an explicit undo replaces it, so the display no longer depends on revalidation timing. Second live-field-test feedback item.
score-outbox.ts, use-score-outbox.ts. - Buttons mode showed two separate "Undo last" controls - one under the reference grid, one under the Dead/Loss buttons - each tracking its own last tap independently. Since scoring happens through Dead/Loss, the grid's copy never had anything to undo and sat there uselessly grayed out. The grid now hides its own undo control (and "press and hold to clear" hint) whenever it's shown as a reference alongside the button flow, leaving one working Undo on screen. Third live-field-test feedback item.
scoring-grid.tsx, doubles-scoring-grid.tsx, page.tsx.
Added
- Score sound cues. Every Dead plays a high ding, every Loss plays a low buzz, in the scoring grid and the Buttons-mode Dead/Loss flow alike (undo/clear stays silent) - so the scorekeeper can confirm a tap registered without looking down at the screen after every shot. Synthesized on the fly with the Web Audio API, no audio files shipped. A speaker toggle next to the sync status mutes it per-device; on by default. Fourth live-field-test feedback item.
score-sound.ts, use-score-sound.ts, sound-toggle.tsx. - Scoring grid auto-scrolls to a station the first time a tap lands in it, in both Singles/Doubles grids - so the scorekeeper doesn't have to swipe sideways to catch up before the next shot. Triggers on the tap, not on the previous station completing, so the view holds still through calling out that station's sub-score and only moves once they've actually started the next one; does nothing on a tap that's in an already-visited station, so correcting an earlier cell never jumps the view backwards. Offsets for the sticky shooter-name column(s) so the station's first cell lands next to the name instead of under it - round 1 of this feature scrolled flush to the grid's edge, which hid the first column or two underneath that overlay; round 2 measured the offset from the scroll container's own box instead of the sticky column's actual live edge, so it was still short by the container's left padding (both caught on the user's phone, 2026-10-02 - the fix now compares the two elements' rects directly rather than relying on either one's box model). Fifth and final live-field-test feedback item (2026-09-24). Known remaining issue: once aligned, the station's Sub column (its 6th column) falls off the visible edge on a phone - there isn't room to show all 5 targets and the sub-score at once - not fixed yet.
use-auto-scroll-station.ts, score-outbox.ts, use-score-outbox.ts.
[0.2.0] - 2026-08-31
Field-hardening pass on the live scoring loop, plus a print-sheet
rebuild. Everything below was bench-verified; the scoring changes still
want live reps at a shoot.
Added
- Fast corrections on the scoring grid. Press and hold a cell (~450ms) to clear it back to unscored - a tap still cycles dead/lost instantly, and dragging cancels the hold so the grid still scrolls. A new "Undo last" button on the grid clears the cell you most recently tapped. Together these cover every stuck-mark case a tap cycle couldn't: a stray tap on an unshot cell, an entry on the wrong shooter's row, or a miskey from several taps back. Works in Grid and Buttons mode, Singles and Doubles (per bird).
- Offline-tolerant score entry. Every tap now goes into a durable local queue (localStorage) and renders instantly, then drains to the server one write at a time whenever there's a connection - on the next tap, on reconnect, on tab re-focus, and every 15s while anything is pending. A dropped signal mid-round, a reload, or backgrounding the browser no longer loses scores; entries that were showing before the reload are still there after it. A status pill above the grid shows "All scores saved" / "N not synced yet" / "Syncing N…" / "Offline — N waiting to sync". The two Complete-round buttons are disabled while anything is unsynced, so "remaining as LOST" can't write misses over targets still sitting in the queue. Applies to all four entry surfaces (Singles/Handicap grid, Doubles grid, and both sequential Buttons modes). Station subtotals and running totals are still computed server-side, so they lag offline entries and catch up on sync - the cell marks themselves are immediate.
Changed
- Doubles scoring reworked to follow the official White Flyer TRAP DOUBLES score sheet. Each pair is now scored as two marks, one per target - a slash for dead, a circle for lost - stacked one over the other, instead of a single glyph collapsing the pair (X / circle / diagonal). A numbered post column (1-5) was added down the left of every Doubles grid and printout. Station grouping and the running Total are unchanged. Applies to all four surfaces: the live scoring grid, the public read-only grid, the printable filled sheet, and the printable blank sheet.
- Printable score sheets (filled and blank, Singles/Handicap and Doubles) rebuilt to match the ATA landscape "TRAP SHOOTING SCORE SHEET" forms: blue title with the discipline under it, red field labels with underline rules, a "Squad Leader Initials" line, post numbers down both sides, and station grouping shown by a heavy rule after every 5th target instead of a Sub column - paper sheets don't carry per-station subtotals. Cells and rows are larger for legible hand-scoring. Doubles keeps the two-marks-per-pair layout (first bird over second) on both the filled and blank sheets. All four sheets now share one
ScoreGrid component and a white "paper" preview shell so they're reviewable on screen, not just in the print dialog. - Doubles live grid: each pair cell is now two independent tap targets (top = first bird, bottom = second), each cycling unscored -> dead -> lost. Replaces the single button that cycled the four pair outcomes (dead pair / dead loss / loss dead / loss pair). Scoring a pair is now two taps.
- Doubles Buttons (sequential) entry now shows just Dead and Loss, tapped once per bird - two taps per pair - matching how a station is called on the line. Replaces the four pair-outcome buttons. The prompt shows "Bird 1 of 2" / "Bird 2 of 2" and Undo steps back one bird at a time.
- Singles/Handicap scoring grids (live and public read-only) now draw a slash for dead and a circle for lost instead of the letters D / L, matching the marks the printed filled sheet already used and the new Doubles grids.
[0.1.0] - 2026-08-28
Versioning scheme changed as of this release. 1.0.0 is now
reserved for the first version live after the ATA formally adopts/
partners with this app - everything before that milestone is a 0.x.y
release, restarting the counter (the previous scheme had reached
1.3.0; that number is retired, not superseded by this one).
Added
- Sequential button entry as an alternative to tapping the grid: a per-scorekeeper account preference (Grid/Buttons toggle on the round page), showing one shooter/target at a time with big Dead/Loss buttons (or the four real Doubles pair outcomes) and a single-level Undo. Auto-advances through the squad in post order per target, then to the next target, matching how a station is actually called. Undo deletes the row rather than setting hit: false, since "undo" means "not yet scored," not "asserting a miss." Needs the
20260828000000_scoring_input_mode.sql migration applied before the preference can be saved.
Changed
- Buttons mode shows the full scoring grid above the sequential entry buttons rather than replacing it - the grid stays visible for reference/overview (station subtotals, who's been scored) while entering with buttons.
- Singles/Handicap scoring grid now uses "Dead"/"Loss" (D/L) terminology instead of "Hit"/"Miss" (H/M), matching real trap scoring language and the call-out labels Doubles already used.
- Scoring grid tap targets enlarged (32px -> 44px, the standard mobile touch-target minimum) on both the Singles/Handicap and Doubles live grids, for easier accurate tapping in the field.
Fixed
- Scoring grid tap delay: the box now flips the instant you tap it (optimistic UI) instead of waiting on the full server round-trip (save -> revalidate -> re-render) before showing any feedback. First live-field-test feedback item.
[1.3.0] - 2026-08-26
Added
- Real landing page at
/ — hero, feature highlights, and a scorekeeper/public split, replacing the one-line placeholder. Reflects the app's current, fully-public reality (no accounts or paywall) rather than any future state.
Changed
- App name corrected to TrueGauge in page metadata and on the landing page (previously "Trapshooting Scoring").
[1.2.0] - 2026-08-01
Fixed
- Blank score sheet row height now matches the completed-round printout (both
h-14) - blank sheets were previously taller, so the two didn't line up when compared side by side.
Added
- ATA classification (six-class system, Singles/Doubles) built against the actual rulebook brackets, but shelved behind a
CLASSIFICATION_ ENABLED flag showing "Classification — coming soon" instead of a computed letter: a class computed from this app's own recorded average isn't the shooter's real ATA classification, which is based on their official average aggregated across every club via ATA's own system. Flip the flag once an actual ATA data-sharing partnership exists - nothing else needs to change. - Score trend chart on shooter profile pages: one line chart per discipline (when a shooter has 2+ rounds in it), score per round in shot order rather than calendar-date spacing, with a hover tooltip showing the exact score and date. The existing round-history table serves as its accessible tabular view.
- Consistency stat on shooter profile pages: population standard deviation of a shooter's all-time scores per discipline, shown as "±X.X targets" once they have 3+ rounds in it. Plain figure, no invented "very consistent / variable" bands - those thresholds would just be a guess.
- Station breakdown on shooter profile pages: a 5-bar chart per discipline showing all-time hit rate at each station. Live rounds derive it from score_targets' target_number; historical rounds use their stored per-station subtotals directly, since there's no target-level detail to derive from.
- Edit an event's name/date, and permanently delete an event (cascades to its squads/rounds/scores) or a single round, each behind an explicit inline confirmation step. Deleting a round is distinct from the existing void: void keeps the record but excludes it from averages, delete removes it entirely and can't be undone.
- Search box on the events list (scorekeeper dashboard), filtering by name or date as the list grows - only shown once there are more than 5 events.
- In-app changelog page at
/changelog, linked from the version footer on every page, rendering this file directly rather than duplicating it - so it can't go stale relative to what's actually shipped. - Search box on the shooter roster (scorekeeper dashboard), same pattern and 5-shooter threshold as the events search.
- Career rounds-shot headline on shooter profile pages ("29 rounds shot all-time"), combining all three disciplines - previously that total was only visible split three ways across the per-discipline cards.
Changed
- Station breakdown bars now also show the average targets hit per round at that station (e.g. "4.7/5"), not just the percentage.
- Reworked the score trend chart around a linear best-fit trend line as the primary signal, with a clear "Improving / Declining / Steady" label and total-change figure above it - the raw per-round line alone was too noisy to read as an actual trend, so it's now de-emphasized supporting detail rather than the headline.
- Starting round 2+ on a squad now defaults the discipline selector to the previous round's discipline instead of always resetting to Singles.
[1.1.0] - 2026-08-01
Added
- Print-friendly completed round sheet (scorekeeper dashboard only, available once a round is complete or voided): shooter names, event/squad info, full hit/miss or pair-call-out grid, station subtotals, and final totals.
- Blank paper score sheet for hand-scoring as a connectivity-outage fallback, printable either ahead of a round (from the squad card, using its discipline selector) or from an in-progress round (pre- filled with the assigned shooters).
- CSV export of a club's full round history (scorekeeper dashboard, scoped to the logged-in scorekeeper's club): one row per shooter per round, with event, date, trap, discipline, status, handicap yardage, per-station subtotals, and final score.
- Historical round import (scorekeeper dashboard): upload a CSV of prior scoring data (Date/Shooter/RoundType/Station1-5/TotalScore, plus optional Location/Notes/Time), map each raw shooter name in the file to a roster entry or confirm it as a new shooter, review a full preview with every invalid row flagged (bad discipline, out-of-range station counts, unparseable dates) before anything is written, and resolve any TotalScore/station-subtotal mismatches per row or in bulk. Nothing is imported until the preview is explicitly confirmed.
- Historical rounds (imported without target-by-target detail) are now a first-class case throughout the app: the round page, public round page, print view, and shooter profile round history all show a plain station-subtotal summary instead of a fabricated grid, and they count toward career averages using their real recorded subtotals.
shooter_discipline_totals now unions live and historical rounds to make this work.
Changed
- Shooter profile averages now display as a per-round total (e.g. "22.58 / 25"), matching ATA's own convention, instead of a percentage - raw broken/shot counts are still shown alongside.
[1.0.0] - 2026-07-31
Initial public release.
Added
- Scorekeeper accounts with login-gated dashboard access.
- Shooter roster management: add/edit, active/inactive toggle, handicap yardage.
- Event and squad setup, with the squad-builder UI doubling as the walk-up squad-formation flow.
- Live round scoring for Singles, Handicap, and Doubles:
- Singles/Handicap: 25-target entry grid with per-station (/5) subtotals and running totals.
- Doubles: dedicated 25-pair (50-target) grid using dead pair / dead loss / loss dead / loss pair call-outs, with per-station /10 subtotals.
- Unrestricted post-completion editing, with an edited-after-completion indicator.
- Discipline selector on round start, with per-shooter handicap yardage snapshotted at round start (so a later roster yardage edit never changes a past round's recorded yardage).
- Round completion split into plain "Complete round" (requires full scoring) and "Complete, remaining as LOST" (auto-fills unscored targets as misses).
- Void round / revert-from-voided action, with unrestricted status transitions.
- Public, no-login scores view: event list, squad/round drill-down, and read-only scoring grids mirroring the scorekeeper view.
- Per-shooter public profile pages: career averages tracked separately for Singles, Handicap, and Doubles (matching ATA's Average Card structure), each with raw broken/shot counts, personal best, perfect-round counts, and full round history.
- Version footer shown on every page.
Fixed
- Yardage number inputs snapping to their minimum value on interaction; switched to text inputs with digit filtering.
- The scoring upsert's conflict clause not matching the widened 4-column unique constraint added for Doubles support, which silently dropped every score write with no visible error.
- Doubles score grouping colliding barrel 1 and barrel 2 rows under the same target number, corrupting the on-screen grid despite correct underlying data.