A public, read-only Swiss Golf handicap dashboard for one owner. The site loads the owner's official WHS record automatically and presents the current handicap, low handicap, counting scores, a differential chart, and the latest 20 rounds.
Important
This is an independent, unofficial project. It is not affiliated with, endorsed by, or supported by Swiss Golf or GolfBox. It uses undocumented upstream behavior that may change or become unavailable without notice.
See the deployed project at golf-handicap.markwalder.net.
The current scoring record is also ranked from its best to worst Score Differential. The ranked chart identifies counting results, the newest and oldest rounds, and the divider between the eighth and ninth values.
The bottom section estimates how a future Stableford result could change the player's handicap using rating data from the most recent round on each course/tee combination in the public scoring history.
The browser never receives or asks for Swiss Golf credentials. A Cloudflare Pages Function reads them from encrypted secret bindings, fetches the owner's record, and exposes only the normalized handicap data intended for the public dashboard.
Browser ──GET /api/whs-record──> Cloudflare Pages Function
│
├── Workers KV snapshot
│
└── Swiss Golf / GolfBox service
(credentials remain server-side)
- Static assets live in
public/.app.jscoordinates configuration and data loading, whiledashboard.jsassembles small UI component instances frompublic/components/. - Components own their DOM roots and update them with DOM APIs. SVG and upstream-derived tooltip text are never assembled as markup strings.
functions/api/whs-record.jsis the read-only public API.- The Function keeps the last successful record in Workers KV.
- A record is refreshed from Swiss Golf after 15 minutes.
- If Swiss Golf fails, the last snapshot is returned as stale data and another upstream attempt is delayed for 15 minutes.
- Successful API responses may be cached publicly for 60 seconds.
- The GolfBox token, Swiss Golf username, password, club identifier, and union identifier are never returned to the browser.
This project uses an undocumented Swiss Golf service that can change without notice. It does not submit scores or modify a player profile.
The default local server uses sanitized sample data and needs no credentials:
npm run dev:mockThen open http://127.0.0.1:4174/.
Run all dependency-free JavaScript and Python tests with:
npm test
python -m unittest discover -s tests -vEach deployment represents one Swiss Golf account and requires its own Cloudflare secrets and Workers KV namespace.
See the deployment and operations guide for the complete fork-and-deploy procedure, Cloudflare Pages and custom-domain DNS configuration, verification, credential rotation, rollback, and troubleshooting.
Everything returned by GET /api/whs-record is intentionally public. Do not
deploy the application for an account unless the player accepts publication of
the following normalized Swiss Golf data:
- Player name, home club, handicap, low handicap, soft cap, and hard cap.
- Current
scoresand olderhistoricalScores. - Round dates, tournaments, courses, tees, hole counts, forms of play, par, Course Rating, Slope Rating, handicapping scores, Score Differentials, PCC, course handicap, resulting handicap, counting status, and adjustments.
grossScore,netScore, and the completeholeScoresarray when Swiss Golf provides those values.
The same public record is persisted in Workers KV as the last successful
snapshot. The response and KV snapshot exclude the Swiss Golf username and
password, GolfBox token, clubId, and unionId.
The fork owner is responsible for obtaining the player's consent, protecting the account credentials, and complying with applicable service terms and data protection requirements. The MIT license applies to this repository's code; it does not grant permission to use upstream services, data, names, or trademarks.
scoreDifferential is always a finite number or null:
- For current
scores, Swiss Golf supplies a fixed-point value scaled by 10,000. A finite upstream zero is retained as the legitimate value0; missing, empty, malformed, and non-finite values becomenull. - For
historicalScores,scoreDifferentialis alwaysnull. These are legacy handicap-history rows for which MySwissGolf does not present a Score Differential, even though the upstream response can contain a placeholderDifferential: 0. A non-zero value in this array is also treated as unverified rather than claimed to be a computed differential.
totalAdjustment is a finite number or null. Numeric upstream values and
numeric strings are normalized without scaling; missing, empty, malformed, and
non-finite values become null. TotalAdjustment is an undocumented GolfBox
field, and this project does not assign a more specific meaning or unit to it.
The sanitized record examined for this decision contained only the string
"0", which is exposed as numeric 0.
The historical rule is based on a sanitized July 2026 comparison. All 187
available historical rows, spanning July 2013 through September 2020, contained
Differential: 0 across widely varying scores. MySwissGolf displayed the same
rows as legacy history with Exact HCP and HCP-relevance columns, omitted Score
Differential entirely, and retained duplicate rows returned by the service.
Current score rows used the separate WHS presentation and supplied computed
fixed-point differentials. No player identifiers, credentials, tokens, or
screenshots are retained in the repository.
This changes the earlier public response: historical scoreDifferential: 0
becomes null, and string adjustments such as "0" become numbers. Cached
snapshots are migrated to this contract when read.
Exports, charts, rankings, tooltips, and AI analysis must exclude a
scoreDifferential whose value is null and describe it as unavailable when a
label is needed. They must not coerce it to zero or infer scratch-level play.
A numeric totalAdjustment may be copied as an opaque upstream value, but it
must not be given a more specific interpretation unless future evidence
establishes the field's semantics.
The handicap history chart uses only Swiss Golf's official per-round
newHandicapIndex values across current and legacy rows. It does not reconstruct
missing values. Rounds without an official update are omitted from the chart;
historical Score Differentials remain labeled unavailable.
Predictions run locally in the browser and assume a PCC of 0.0. The course
selector includes a course/tee/hole-count combination only when its most recent
round has valid par, Course Rating, and Slope Rating values. Its latest eligible
combination is selected by default.
For 18-hole Stableford rounds, the projected Score Differential is:
(113 / Slope Rating)
× (par + Course Handicap - (Stableford points - 36) - Course Rating - PCC)
For 9-hole rounds, the same calculation uses 18 points as the baseline and half the PCC. The result for the nine holes played is combined with an estimated expected 9-hole differential:
0.52 × current handicap + 1.2
That estimate was derived from 11 recent current-method 9-hole rows in the
examined Swiss Golf record. A linear fit produced
0.5165 × handicap + 1.2469; the simpler formula above had a maximum residual
below 0.05 against the official published differentials in those rows. It also
reproduces the public WHS examples for handicaps of 14.0 and 15.0.
The new handicap calculation uses the applicable lowest-score table, removes
the oldest result once 20 are present, applies an exceptional-score reduction
when triggered, and then applies the supplied soft and hard cap values. Future
PCC and Handicap Committee adjustments are inherently not predictable, so the
UI labels every result as an estimate and Swiss Golf remains authoritative.
Player insights are calculated beyond the 13 displayed rows, beginning at zero
Stableford points and continuing until the first improving result is found.
The player-specific course comparison is the difference between the unrounded
Course Handicap and the portable Handicap Index. Its standard-rated benchmark
uses a Slope Rating of 113 and a Course Rating equal to par.
The handicap record shown by this application is intentionally public. The KV snapshot persists that public record so ordinary page visits do not repeatedly log in to Swiss Golf. Cloudflare processes the Function request and stored snapshot as the hosting provider.
- Credentials are encrypted Cloudflare bindings and are never sent by browser code.
- Credentials and GolfBox tokens are not written to application logs or KV.
- The endpoint accepts
GETonly and is not a general-purpose proxy. - Upstream failures expose a generic error, not upstream response details.
- The page includes CSP, HSTS, frame, MIME-sniffing, referrer, and permissions protections.
- No analytics, external fonts, CDN scripts, or third-party browser resources are included.
swissgolf_fetch.py remains as a diagnostic and reverse-engineering reference.
It implements the same unofficial read-only login and WHS archive flow outside
Cloudflare. It is not used by the deployed website.
The recommended form prompts for the password without displaying it:
python .\swissgolf_fetch.py 7000-123456To save the normalized record locally:
python .\swissgolf_fetch.py 7000-123456 --output .\whs-record.jsonThe default transport mirrors the Swiss Golf website's CORS proxy. A native
client can test the direct GolfBox route using --transport direct.
The project is available under the MIT License.