Ballina Athletics Club

Logic Reference โ€” 2026 Season

Business rules reference for the BAC cross-country handicap system. Verified against the live codebase.

๐Ÿ•

Handicap System

What is a handicap?

Your handicap is your personal start time advantage. A longer handicap means you start earlier. The system targets everyone finishing at roughly the same time โ€” so a slow runner starts well ahead of a fast runner.

Target finish times

DistanceTarget
1K26:00
3K38:00
6K44:00

Adjustment rules

How you ran vs targetHandicap change
Finished early (faster than target)+seconds early, rounded UP to nearest 10 seconds
0โ€“60 seconds lateNo change
61โ€“120 seconds lateโˆ’10 seconds
More than 120 seconds lateโˆ’20 seconds
๐Ÿ’ก
Early finish example: finished 12 seconds faster than target โ†’ adjustment = ceil(12/10) ร— 10 = +20 seconds. The rounding always goes up, never down.

DNF โ€” no adjustment

A runner who did not finish keeps their stored handicap unchanged. The values 0:00 for start and 99:00 for finish are placeholders used in DNF storage โ€” they are not real times and must not be used in calculations.

Wrong Handicap formula

When a runner starts at the wrong time, the system cannot use the normal formula (which assumes start = correct handicap). Instead it computes an effective finish time:

effective_finish = storedHandicap + (clockFinish โˆ’ actualStart)

This effective finish is then compared to the target using the standard adjustment rules. Both wrong-hcp-early and wrong-hcp-late runners have their handicap adjusted via this formula.

New runners

A new runner's opening handicap is set from the DOS master list at season start. If no DOS value exists, the coordinator sets it manually before Week 1.

Season reset

At the start of each season, 2 minutes are subtracted from every runner's handicap. This is applied once, after the final week of the previous season is processed.


๐Ÿ†

Points System

Race points by position

PositionPoints
1st place20
2nd place15
3rd place10
4th place8
5th and beyond5

Special entry types

TypeEligible for placing?PointsHandicap adjusted?
Normal runnerYesPer positionYes
Wrong hcp โ€” started lateYesPer positionYes (DOS-parity formula)
Wrong hcp โ€” started earlyNo5Yes (DOS-parity formula)
Early runnerYesPer positionYes
Early runner + volunteerYesHigher of position points or 8Yes
Time TrialNo5Yes
DNFNo8 (if also volunteering)No
Volunteer (did not run)No8No
๐Ÿ’ก
Early runner + volunteer dual-role rule: max(position_points, 8). A runner who finishes 3rd (10 pts) and volunteers gets 10 pts. A runner who finishes 5th (5 pts) and volunteers gets 8 pts.

Placing groups

Runners compete within their distance ร— gender ร— age group. Age is calculated as the age you turn in the current calendar year, not your age on race day. Placings are by clock finish time โ€” earliest finisher wins.

Cross-distance

A runner can run a different distance each week. Their placing is only within runners of the same distance that week.


๐Ÿ“…

Season Structure

The season runs for 24 weeks on Sundays from late January to late July.

Monthly blocks

The season is divided into 6 blocks of 4 weeks each. Monthly blocks are used for monthly prize computation.

Monthly prizes

RuleDetail
Prize scopeOne prize winner per age group ร— gender ร— distance per block
Winner determinationMost points accumulated in the block within their category
Tie-breakMost weeks run in the block
One-win limitOnce a runner wins a monthly prize, they are excluded from winning again in future months that season

Season standings

Accumulated points across all weeks. Updated live after each week is saved. Grouped by distance ร— gender ร— age group.


๐Ÿ‘ฅ

Age Groups

How determined

Age = the age the runner turns in the current calendar year (not their age on race day). This is computed from their date of birth stored at registration.

2026 age brackets (by distance)

Groups are set per distance ร— gender. The exact brackets are configured in the Google Sheet's config tab. Typical groups: 8 & under, 9โ€“11, 12โ€“14, 15โ€“46, 47โ€“59, 60+.

Adjustment period

At the start of the season, age groups are re-evaluated based on the new calendar year. Runners who move into a new age group at year-change will compete in the new group from Week 1.

Locking

Age groups are locked per season once Week 1 is saved. Mid-season corrections require an admin recalculate.

Splits

If an age group has fewer than 3 runners across all weeks, it may be merged with an adjacent group for prize purposes. The coordinator decides.


๐Ÿ“

Distances

Three race distances: 1K, 3K, 6K.

Distances alternate on a rotating weekly schedule (1K โ†’ 3K โ†’ 6K โ†’ repeat). A runner may run any distance each week regardless of their "preferred" distance. Distance for a given week is per-entry, not per-runner.

Storage format

Always stored as 1K, 3K, 6K (with the K suffix). Never as bare integers (1, 3, 6). This applies in both Google Sheets and Supabase.

No preferred distance at registration

Runners do not nominate a preferred distance at registration time for racing purposes. Volunteer points are assigned to their preferred distance (set in their registration record).


๐Ÿท๏ธ

Registration and Bib Numbers

Registration

Runners register via Google Form. Registration data flows into the Runners tab of the main app. New runners appear as PENDING until a bib is assigned.

Bib pool

Bibs are assigned from a pre-allocated pool. Numbers are permanent within a season โ€” a bib assigned to a runner stays with them for all 24 weeks. Do not reassign a bib that has been used in any week's results.

Duplicate detection

The system flags if the same bib appears more than once in a week's results. Fix before saving.

Permanent bibs

Once a bib number has results recorded against it, it should not be changed, even if the runner's name is corrected. Name corrections go through the name-aliases mechanism.

Name aliases

Long hyphenated names may truncate in source data. Unresolved name mismatches are added to data/name-aliases.csv โ€” not hand-edited in the results rows.


๐Ÿ—„๏ธ

Data Storage

Dual storage architecture (2026)

The system uses Google Sheets as the primary write destination and Supabase as the read-first data source.

LayerRole
Google SheetsPrimary write: results, runners, volunteers, config
Supabase (lrfhvesylfwbabzxbqhr)Read-first: results, handicaps, result-weeks

When results are saved, the system writes to Google Sheets first, then mirrors to Supabase via supabase-mirror.js.

Handicaps in Supabase

There is no separate handicaps table in Supabase. Handicaps are derived from the latest results.handicap per (name, distance) across all weeks.

Supabase results table columns

ColumnTypeMeaning
startedTEXTActual start time used on race day. MM.SS format.
handicapTEXTFor backfill: correctHcp (pre-race). For live mirror: newHandicap (post-race). MM.SS format.
notesTEXTFlag: 'DNF', 'WrongHcp', or 'TimeTrial'. No spaces, no dots.
categoryTEXTAge group + gender, e.g. "15-46 Female"
distanceTEXT'1K', '3K', or '6K' (with K suffix)
โš ๏ธ
Unique constraint: UNIQUE (season, week, bib, distance). NULL bibs are excluded (NULL โ‰  NULL in SQL).

Time format conventions

Two conventions exist in the system:

FormatWhere usedExample
Period format (MM.SS)Supabase, DOS legacy data, internal storage25.29 = 25 min 29 sec
Colon format (MM:SS)App display, Google Sheets25:29 = 25 min 29 sec

The dotToColon() function in supabase-reads.js converts period format to colon format on read. Never write colon format into a Supabase column that originated from DOS data.

Per-week tabs (Google Sheets)

After each week is saved, the system creates backup tabs: Week N Results and Week N Handicaps. These are the audit trail. Do not delete them.

Test mode

Add ?test=1 to the main app URL to switch to the test Google Sheet. All writes in test mode go to the test sheet only. A TEST MODE banner appears.

Corrections

Individual result corrections go through the Admin panel. After any correction, run Admin โ†’ Recalculate Standings.


๐Ÿ“ฅ

Data Import Methods

MethodWhen to use
Manual entrySmall corrections โ€” type bib and time directly
Single-file CSVOne file with bib, distance, finish time. Most common.
Two-file CSVSeparate bibs file and times file โ€” scribe and timer worked independently
Stopwatch importImport the Week N Stopwatch tab data directly
OCR ScannerPhoto of handwritten race sheet โ€” AI extracts the values
๐Ÿ“‹
OCR note: On the paper sheet, Y = correct handicap (normal), N = wrong handicap. The OCR scanner translates this automatically.

๐Ÿ”’

Security

Main app PIN

Required to access the main app, OCR scanner, and admin panel. Shared among coordinators. Change at the start of each season.

Admin password

Separate from the PIN. Required for Admin panel actions (corrections, recalculate, register, assign bibs). More restricted distribution.

API

All API routes that serve data (/api/data?type=...) are read-only. Write operations go through PIN-protected Vercel functions. The Supabase service key is server-side only โ€” never exposed to client JavaScript.

Vercel function limit

The project runs on the Vercel Hobby plan (12-function limit). Do not add new files under api/ โ€” extend existing handlers instead.


๐Ÿงฎ

Key Formulas

Standard handicap adjustment

diff = clockFinish โˆ’ target
if diff < 0 (finished early):
    adjustment = ceil(|diff| / 10) ร— 10   โ† rounds UP to nearest 10s
elif diff โ‰ค 60:  adjustment = 0
elif diff โ‰ค 120: adjustment = โˆ’10
else:            adjustment = โˆ’20
newHandicap = storedHandicap + adjustment

Wrong Hcp adjustment (DOS-parity formula)

effectiveFinish = storedHandicap + (clockFinish โˆ’ actualStart)
diff = effectiveFinish โˆ’ target
adjustment = standard_formula(diff)    โ† same rounding rules apply
newHandicap = storedHandicap + adjustment

Early runner + volunteer points

points = max(positionPoints, 8)

Age calculation

age = currentYear โˆ’ birthYear

Uses the year the runner turns that age, not their age on race day.

Placing

Within distance ร— gender ร— age group, ranked by clock finish time ascending.

Season standings

Sum of all weekly points per runner per distance.