Titiranglers FedEx Cup
Administration

Admin

Logging In

Admin pages are protected by Cloudflare Access. There are two ways in:

1. IP bypass — When you're on the registered home Spark NZ IP, no login is required. Admin pages just open.
2. One-time PIN — From any other network, Cloudflare emails you a 6-digit code. Enter it and you're in for the session.

Allowed emails are configured in the Cloudflare Zero Trust dashboard. Once Cloudflare lets you in, the app then checks your email is also in the local admin_users table — both layers must allow you.

If you get "Access Denied" after the PIN, your email is in CF but not in admin_users. Existing admin must add you (see below).

Adding & Removing Admins

Adding a new admin is a two-step process:

Step 1 — Cloudflare Access: Add their email to the CF Zero Trust policy at one.dash.cloudflare.com → Access → Applications → fedex.philipson.online → Edit policy → add email.
Step 2 — In-app: Go to Admin → Admins → enter their email → click Add.

Both layers must list the email. Skipping Step 1 means they can't even get past the login. Skipping Step 2 means they pass login but get "Access Denied" inside.

To remove an admin: remove them from the CF policy and remove from the in-app list. The bootstrap admin ([email protected]) is protected and can't be removed via UI.

DotGolf Connection (JWT)

Auto-entry pulls scores from golf.co.nz using a logged-in session. The login token (JWT) is valid for ~24 hours, then needs refreshing.

Status check: Admin → DotGolf shows when the token was last issued and how long until expiry.

Re-login when expired:

Go to Admin → DotGolf → enter golf.co.nz username + password → click Login. The system replicates the web login flow, captures the new JWT and session cookie, and stores them. Auto-entry works again immediately — no need to restart anything.

Symptoms of an expired token: Fetch returns no results, or the preview is empty even when you know the players played.

Weekly Workflow

1. (If needed) Find the CompetitionID

Go to golf.co.nz → Saturday Scramble results for that week → grab the number from the URL: competition-results?CompetitionID=1274147
Optional — if you skip this and just enter a date, the system tries the profile API for every player (Pass 2 only).

2. Auto Entry

Admin → Auto Entry → paste the CompetitionID or just set the event date → click Fetch Results. A green progress overlay shows player-by-player progress as the system fetches scores.

3. Review the preview

Each row shows where the data came from:
via scramble — pulled from the Saturday Scramble comp (richest data, full hole-by-hole + stableford pts)
via profile — pulled from the player's profile API (no per-hole stableford because no SI returned)
Players who played but couldn't be found appear in the "Not in Scramble / no profile match" section for manual entry.

4. Add missing players manually

For each missing player, type their Gross and Course HC from their DotGolf profile. Stableford auto-calculates as 36 + par + course_hc − adj_gross.
If they DNF'd (didn't finish the round) — type 999 in the gross box. They'll be flagged DNF, get 0 points, and still count toward field size for everyone else's points.

5. Check event settings

Set Format (Stableford / Nett / Gross) and Points Multiplier (1× regular, 2× majors). These determine ranking direction and points awarded.

6. Import

Click Import Results. The progress overlay reappears as the system re-fetches and writes to the database. Event is created, results inserted with hole-by-hole data where available, points calculated, and weekly insights commentary auto-generated.

7. Review & Verify

You'll land on the event admin page. Review results, edit commentary if you want, then Save & Verify. Verified events show on the public site.

How Auto-Entry Works (Three Passes)

Auto-entry tries three sources, in order, and combines what it finds:

Pass 1 — Saturday Scramble comp API

If you provided a CompetitionID, the system pulls the official Scramble results. This gives the richest data: stableford, gross, nett, handicap, hole-by-hole scores and stroke index per hole. All insights features work. Source tag: scramble.

Pass 2 — Profile API (per missing player)

For any FedEx player NOT in the Scramble (or all players if no CompetitionID was given), the system queries each player's DotGolf profile for a same-date round at Titirangi. Returns gross, nett, stableford total, and per-hole gross — but no stroke index, so per-hole stableford points are unknown. Some insights features (e.g. "owned both nines") can't run on Pass 2 entries. Source tag: profile.
If the profile shows the round was incomplete (no AdjustedGross), the player is flagged DNF. Source tag: profile_dnf.

Pass 3 — Manual entry

Any player still missing appears in the manual-entry block. You type Gross + Course HC. Type 999 to flag DNF. Source tags: manual or manual_dnf.

Why three passes? Some players don't enter the Scramble but still play with the group. Some weeks no Scramble runs at all but the Saturday round still happens. Some players DNF. The three-pass system handles all of these without dropping anyone.

DNF Handling — "Did Not Finish"

A DNF player still counts toward field size for the points calculation, but receives 0 points themselves and no rank.

Why count them? If 14 players show up and one DNFs, points are still awarded for a 14-player field — top points = ceil(14/2) × 100 = 700. Otherwise people who DNF'd would shrink the field and reduce everyone else's points unfairly.

How to flag DNF:

Auto-detected when the profile API returns null AdjustedGross (round was abandoned mid-play).
Manually: in the Pass 3 manual entry box, type 999 as the gross score.

Picking up on a hole or two ≠ DNF. That's normal stableford play. The round-level total is still valid; only individual hole scores show null.

Event Admin

Each event's admin page (Admin → Latest Event) has:

Commentary box — Auto-generated insights from the data. Edit to add your own colour.
Save & Verify — Publishes the event with commentary.
WhatsApp Post — Pre-formatted results text for sharing (available after verify).
Event Settings — Change format (Stableford/Nett/Gross) and points multiplier.
Save & Recalculate — Updates settings and recalculates all points for the event.
Regenerate Insights — Re-runs the auto-commentary generator.
Delete Event — Removes the event and all results (cannot be undone).
Inline Scorecard — Shown below the results table for events with hole-by-hole data.

Points Formula

max_points = CEILING(N ÷ 2) × 100 × multiplier
points = max_points − 100 × (RANK.AVG − 1)
if points ≤ 0 → no points awarded

N = total field size, including DNF players. Only the top ~half of the field earns points. Tied players share the average of their positions (so tied 3rd → both get rank 3.5). Bigger turnouts award more points. Double-points events multiply everything by 2.

Ranking direction: Stableford ranks highest-first. Nett and Gross rank lowest-first. DNFs are excluded from the ranking pool but count toward N.

Bulk Actions

Recalculate All Points — Re-runs the points formula for every event in the active season. Use after fixing formats or multipliers across multiple events.

Regenerate All Insights — Re-runs the auto-commentary generator for every event. Use after importing new data or fixing player names.

Generate Season Recap — Creates the animated recap timeline commentary. Run after all events are imported:

docker compose exec app python3 generate_recap.py 2026

Tip: If the AI commentary on an old event uses the wrong format (e.g. says "wins on stableford" when it was a Nett event), Recalculate All Points refreshes the format column, then Regenerate All Insights gives the right narrative.

Troubleshooting

Fetch returns nothing or only some players

Most likely cause: DotGolf JWT expired. Go to Admin → DotGolf and re-login. Try Fetch again.

Progress overlay stuck on "Connecting…"

Cloudflare cache on the JS file. Hard-refresh with Ctrl+Shift+R. If it persists, the API call is genuinely slow (golf.co.nz being sluggish) — give it 30-60 seconds.

Insights say "0 front, 0 back" or weird zero counts

Event was Pass-2 only (no Scramble). Per-hole stableford features need stroke index, which the profile API doesn't return. The narrative skips those features automatically — if you see them, it means the data was partially populated. Regenerate insights.

Insights commentary uses Stableford for a Nett event

The event's format column is NULL or wrong. Run Save & Recalculate on the event admin page (which updates format), then Regenerate Insights.

"Access Denied" on admin pages

Your email passed Cloudflare but isn't in the in-app admin_users table. Existing admin needs to add you at /admin/users.

Page changes not appearing

Cloudflare cache. Hard refresh (Ctrl+Shift+R). If it persists, purge in Cloudflare dashboard → Caching → Purge Everything.

Player missing from the Scramble who definitely played

They probably didn't enter the Scramble comp. They'll appear in the "Not in Scramble" section with a profile link — click it to verify their score, then enter manually. If their profile is also empty, they need to post their card on golf.co.nz first.

Backups

Always back up before major changes. Save container files to host first, then tar + pg_dump:

cd /opt/fedex

# Save container files to host
docker compose cp app:/app/main.py        app/main.py
docker compose cp app:/app/dotgolf.py     app/dotgolf.py
docker compose cp app:/app/insights.py    app/insights.py
docker compose cp app:/app/templates/     app/templates/
docker compose cp app:/app/static/        app/static/
docker compose cp app:/app/data/          app/data/

# Database backup
TS=$(date +%Y%m%d-%H%M)
docker compose exec -T postgres pg_dump -U fedex fedex > /opt/fedex-db-$TS.sql

# Full app backup
tar czf /opt/fedex-backup-$TS.tar.gz app/ docker-compose.yml Caddyfile

echo "Done: /opt/fedex-backup-$TS.tar.gz + /opt/fedex-db-$TS.sql"

Restoring:

cd /opt/fedex
tar xzf /opt/fedex-backup-YYYYMMDD-HHMM.tar.gz
docker compose build --no-cache app
docker compose up -d
docker compose exec -T postgres psql -U fedex -d fedex < /opt/fedex-db-YYYYMMDD-HHMM.sql