This is the complete, human step-by-step: set up a season, register clubs & teams, add players, play matches, submit & confirm scores, and watch the standings and DUPR move — as every role, on the safe sandbox. Every rule the rulebook is supposed to enforce is demonstrated here, both the ways it lets you through (happy path) and the ways it stops you (unhappy path).
You are testing on uat.silkvo.com — a full clone of the real HKPL league, but caged so nothing escapes. The production league is hkpl.silkvo.com. Same code, same rules, same screens — the only differences are what reaches the outside world.
| What | Sandbox — uat.silkvo.com | Production — hkpl.silkvo.com |
|---|---|---|
| Real email | Suppressed (caged) — invitations & notices are generated and logged, never sent. You confirm invites in-app, no inbox needed. | Real email is sent to real inboxes. |
| SMS / WhatsApp | Suppressed (caged). | Real messages sent. |
| DUPR writes (submit a match rating) | Suppressed → the match is marked submitted (sandbox); nothing is written to DUPR's servers. | Written to the real DUPR Partner API — the players' official ratings move. |
| DUPR reads (look up a rating) | Real reads against the DUPR sandbox for consented test players. | Real reads for real players. |
| One-click role login (/qa.html) | Enabled — sign in as any role without an account. | Disabled — everyone uses real magic-link / SMS sign-in. |
| Data | Dummy: "UAT Player P001…P057", teams "UAT A-1", 3 divisions. Wipeable. | The real 1,833 members / 191 teams. |
The single most important testing tool. You never need an email account or password to test a role. A person has one real role in the league — to see the app as someone else, you don't change their role, you step into their shoes.
Open /qa.html → tap any of the 11 role cards → you are instantly signed in as a ready-made person in that role, on a real team with real fixtures.
Tap "Switch persona" (top bar) any time to jump to another role in the same browser. No sign-out dance.
On /qa.html use the "sign in as any user" search — type a name / email, and you become that exact person (their team, their pending items, their view). This is the "impersonate" tool.
Great for reproducing "it doesn't work for player X" — become player X and see exactly what they see.
Direct login links (bookmark them): Platform Owner · League Admin · Registrar · Finance · Division Rep · Captain · Referee · Player · Editor · Cup Admin · Spectator
Do this as the League Admin in /admin.html. The order matters — each step feeds the next.


Admin → Seasons → + New season. A season is the container for everything. Mark one season "current".Admin → Divisions. For each division set its DUPR band (min / max rating), roster limits, and gender rules. These values ARE the rules the engine enforces — e.g. Division B = 3.000–3.699. (Details in §3.)/register.html (with a payment receipt) and the Registrar approves it; or the admin adds one directly in Admin → Clubs. Assign a Club Manager so someone can run that club's page + teams./club.html → "Register a new team", with a receipt), or the admin adds one. The team is placed in a division — that fixes which rulebook applies to its roster./app.html. Every add is checked against the division's rulebook (band, roster cap, one-team-per-season). An ineligible player is refused with the reason (§3).Admin → ⚡ Generate Schedule builds the round-robin fixtures. Capacity = match-days × slots × courts — increase these if it says "N matches couldn't be placed".The rulebook isn't a PDF nobody reads — it's a set of rules the system enforces the moment someone tries to break them. Rules are defined per division and each carries a severity: block = refuse, warn = allow but flag, info = note only.
These are the rules the cloned HKPL rulebook runs on every player add right now — verified firing on the sandbox. Each division can tune the numbers and set a rule to block / warn / off.
| Rule | Where it applies | Severity |
|---|---|---|
| One team per season — a player can't be on two teams in the same season | All divisions | block |
| Player registration deadline — no new players after the cut-off date | All divisions | block |
| Roster cap — no more than N players on a team | All divisions (max 20) | block |
| Already on this team — no duplicate roster rows | All divisions | block |
| Age check — date of birth on file, minimum age | All divisions | block / warn |
| Gender minimum on the roster | All divisions (≥2 male) | block |
| DUPR account integrity — flags fake / duplicate IDs; higher rating applies for multi-account | All divisions | block / info |
| Score format — game sequence LD/MD/MIX, points to 21 | On score submit | block |
| DUPR rating band — rating must sit inside the division's min–max | Supported — set a division band + switch on | optional |
Cannot add this player — already has this player this season, one team per season only.

There are several ways a player can land on a roster, and all of them ask the rulebook first: the captain's Add-Player wizard, approving a player's join-request, accepting a team invitation, and borrowing a player from a sibling team. If the engine ever errors, the action is refused (fail-closed) rather than let through.
By design, an admin can override. A League Admin / Platform Owner adding a player directly, or a Registrar approving a held item, can knowingly accept an exception — because sometimes the human needs to. Those overrides are audit-logged.
This is the heartbeat of the league. Follow it once end-to-end and you understand 80% of the product. Each step names who does it and where.
/schedule.html and on each captain's /app.html./app.html → the match → Plan Lineup. The rulebook checks each slot (gender per game, no same player twice, play-up limits)./app.html. Once both sign, the result is final./referee.html.


For each role: tap the green button to become that person, then run the happy path, try the unhappy path (what it should refuse), and verify where the change appears.
Runs the whole platform across all leagues. Portal: /super.html + everything the League Admin can do.
/super.html — see every tenant (league)./super.html · audit trail in Admin → Audit Log.Runs HKPL day-to-day. Portal: /admin.html (38 tabs, each with a one-line description under its title).
/admin.html reflects the Registrar queue + disputes; each tab title shows a plain description.
The human gate for registrations. Portal: /registrar.html — a queue of pending clubs, teams, players.

Sees the money side — registration fees + receipts. Portal: /registrar.html (finance view).
/register.html.Looks after one division — approvals + reschedules scoped to their division. Portal: /registrar.html (division view).
/schedule.html.Runs knockout / round-robin tournaments (separate from the league). Portal: tournaments surface.

Runs one club's public page + registers its teams. Portal: /club.html (with a Club Manager dashboard).
www.x.com) or a logo-only sponsor → a plain error ("Website must be a full link…"), not raw code. (It also auto-fixes a bare link.)
Runs one team. Portal: /app.html. The busiest role — do this one thoroughly.

A rostered member. Portal: /app.html (player view).

Officiates matches + live scoring. Portal: /referee.html → venue picker → live scoresheet.

Owns the public content — announcements, the Organization page, galleries, mission cards, FAQ. Portal: /admin.html (editorial tabs).

The public view — no login needed for most of it.
The answer to "I did X — where should it appear?" Do the action, then check every "shows on" cell.
| Action | Who | Shows on these pages | How to verify |
|---|---|---|---|
| Submit & both-confirm a score | Captains | Results · Standings · both team pages · both captains' /app.html · History · DUPR queue | Standings points/rank move; result card appears with all 5 games; "awaiting" → "final". |
| Add a player | Captain | Team roster (/app.html) · the player's dashboard (invite) · admin team view | Row shows "pending invite" → "active" after they accept. |
| Approve a registration | Registrar | Registrar queue (removed) · Clubs/Teams lists (added) · Admin dashboard count | Item leaves queue; the club/team/player is now live. |
| Raise a dispute | Captain | Admin dashboard alert + sidebar badge · Disputes queue | Badge count increments; item in the dispute panel. |
| Edit the club page | Club Manager | /club.html (that club) · Clubs list card | Banner/logo/colours/sponsors update on save. |
| Reschedule a match | Captain → Div Rep | /schedule.html · both /app.html match cards | Date/court updates once the rep responds. |
| Broadcast a notice | Admin | In-app notification bell · (live: email/SMS/push) | Bell badge increments for recipients. |
| Log a social game + submit to DUPR | Admin | /social-games.html · DUPR queue | Row shows "submitted (sandbox)"; on live it writes DUPR. |
Who sees it: the League Admin, on the /admin.html health card. It's not a status light that says "probably fine" — the "Full run" button actually calls the DUPR Partner API and reports each function pass/fail.
Last run: 10 work · 1 reachable · 3 not-granted (by design) · 0 broken.
"Works" = live green. "Not granted / 403 by design" = functions our partner tier intentionally doesn't expose (e.g. calculated-stats) — expected, not a failure. "Broken" = anything red needs attention. Today: none.

Who sees it: the League Admin, on the /admin.html health card → "Run integrity check". It cross-checks the whole database for contradictions that shouldn't exist.
12 read-only checks: teams with no captain, orphaned rosters, teams whose division was deleted, clubs with a dangling manager, duplicate DUPR ids, a match won by a team that didn't play in it, a team playing itself, completed matches with no games or no winner, stranded pending approvals, and more. Last run: 0 errors, 0 warnings — clean. Run it after you've been testing to confirm nothing you did left the data inconsistent.

A league app is only trustworthy if it refuses the wrong thing. Try each — the system should stop you with a clear reason.
| Try this | Expected result |
|---|---|
| Add a player already on another team this season | Refused — one team per season only. |
| Add a player after the registration deadline | Refused — deadline passed (or held for Registrar). |
| Set a division DUPR band, then add an out-of-band player | Refused — rating outside the division band. |
| Add a 21st player to a full roster | Refused — roster is full. |
| Accept a team invite after the roster/band no longer fits you | Account created + logged in, but not rostered — registrar can override. |
| Submit a tied match (no decisive winner) | Refused — DUPR needs a winner. |
| Register a club/team with no payment receipt | Refused — receipt required. |
| Reject a registration with a reason | Rejected — leaves the queue (fixed). |
| Approve several registrations in a row | All work — no refresh needed (fixed). |
| Save a club page with a broken website URL | Plain error + auto-adds https:// to a bare link. |
| Visit the player directory signed out | "Sign in to browse" — members-only. |
Everything on the sandbox is disposable. The dummy data is clearly labelled so it's never mistaken for real:
UAT Player P001 … P057, emails end @uat-roster.hkpl.test.UAT A-1, UAT B-2… in three test divisions.uat-<role>@uat.hkpl.test.Anything you create while testing (extra players, social games, edits) can be wiped by re-seeding the sandbox, which restores the clean known state in one step. Ask the engineer to run the re-seed; it does not touch the real HKPL league (they are fully separate databases/hosts).
When you're happy from this walkthrough, going from sandbox to the real HKPL league is a short, deliberate checklist — same code, flip the outside-world switches on.
So you're not starting cold — here's what has been proven on the sandbox (and how to re-run it yourself via the sections above).