Document current leaderboard API contract
This commit is contained in:
+213
-23
@@ -148,33 +148,222 @@ would require additional reporter identity/ordering information.
|
||||
|
||||
## Read API
|
||||
|
||||
`GET /api/leaderboard` accepts optional `game` (`t5` or `t6`), `map` (the opaque
|
||||
key returned in `maps`), `metric` (default `highestRound`), `q` (name/GUID search),
|
||||
`limit` (1–100, default 50), and `offset` (default 0). It returns ranked `entries`,
|
||||
available `maps`, the matching player `total`, pagination values, and `summary`.
|
||||
Each entry represents one GUID and combines its qualifying matches within the
|
||||
game/map filters. Entries have no game field; map options retain their game
|
||||
identity. Both `total` and `summary.players` count unique GUIDs; `total` applies
|
||||
the selected metric's qualification rules and player search, while the summary
|
||||
covers all completed qualifying participation in the game/map scope.
|
||||
This reference describes the current repository implementation. A deployed
|
||||
instance must run the corresponding code to provide the same behavior. The
|
||||
sources are the [HTTP handler](../src/routes/api/leaderboard/+server.ts),
|
||||
[response types](../src/lib/leaderboard.ts),
|
||||
[query implementation](../src/lib/server/fleet/leaderboard.ts), and
|
||||
[authentication guard](../src/lib/server/auth/guard.ts).
|
||||
|
||||
### Authentication and requests
|
||||
|
||||
`GET /api/leaderboard` is a session-authenticated JSON endpoint. A **viewer**,
|
||||
operator or administrator account can read it after any required temporary
|
||||
password change. There is no API-key or bearer-token authentication for this
|
||||
endpoint; telemetry and host-agent credentials do not grant access.
|
||||
|
||||
A tool can sign in with `POST /api/auth/login`, sending a JSON object with
|
||||
`username` and `password`, `Content-Type: application/json`, and an `Origin`
|
||||
header exactly matching the instance's origin (scheme, hostname and port, with
|
||||
no trailing slash). Preserve the returned `__Host-ec_session` cookie and send it
|
||||
on subsequent requests. Login returns `{ "user": ... }`; the session token is
|
||||
in `Set-Cookie`, not the JSON body. Sessions expire after 12 hours or 30 minutes
|
||||
of inactivity, and can be revoked earlier. Handle `401` by signing in again.
|
||||
See [Users and access](authentication.md) for setup and session rules.
|
||||
|
||||
For example, with an existing account's credentials in a private `login.json`
|
||||
file containing `{"username":"reader","password":"..."}`:
|
||||
|
||||
```sh
|
||||
EC_BASE_URL='https://ec.example.com'
|
||||
umask 077
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--cookie-jar ec-cookies.txt \
|
||||
--header "Origin: $EC_BASE_URL" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-binary @login.json \
|
||||
"$EC_BASE_URL/api/auth/login"
|
||||
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--cookie ec-cookies.txt \
|
||||
--get "$EC_BASE_URL/api/leaderboard" \
|
||||
--data-urlencode 'game=t6' \
|
||||
--data-urlencode 'metric=kills' \
|
||||
--data-urlencode 'limit=100' \
|
||||
--data-urlencode 'offset=0'
|
||||
```
|
||||
|
||||
HTTPS is required. Local HTTP development requires `EC_AUTH_ALLOW_HTTP=true`
|
||||
and a loopback hostname, and uses the separate `ec_session_dev` cookie. The
|
||||
read request does not require an `Origin` header. Successful responses use
|
||||
`Content-Type: application/json`, `Cache-Control: no-store` and `Vary: Cookie`.
|
||||
|
||||
### Query parameters
|
||||
|
||||
All parameters are optional. Names and enumerated values are case-sensitive.
|
||||
Use URL encoding for map keys and search text.
|
||||
|
||||
| Parameter | Default | Accepted values and behavior |
|
||||
| --- | --- | --- |
|
||||
| `game` | Both games | Exactly `t5` or `t6`. An empty value is invalid. |
|
||||
| `map` | All maps | Opaque `maps[].key`, at most 1024 characters. Empty means no map filter. An unknown key or a game/map mismatch returns an empty selection, not an error. |
|
||||
| `metric` | `highestRound` | One of the metric keys below. An empty or unknown value is invalid. |
|
||||
| `q` | No search | At most 100 characters before trimming. Matches a literal substring of the current name or GUID, ignoring ASCII letter case. Surrounding whitespace is trimmed; whitespace-only input means no search. `%` and `_` are literal characters, not wildcards. |
|
||||
| `limit` | `50` | Integer from 1 to 100. |
|
||||
| `offset` | `0` | Integer from 0 to 1,000,000; counts matching entries to skip, not ranks or pages. |
|
||||
|
||||
`limit` and `offset` accept decimal digits only. Empty strings, signs, spaces,
|
||||
fractions and exponent notation are rejected rather than clamped. String
|
||||
length limits use JavaScript string length (UTF-16 code units). Unrecognized
|
||||
query parameters are ignored; if a parameter is repeated, its first value wins.
|
||||
|
||||
Metric keys are `highestRound`, `kills`, `roundsSurvived`, `headshots`, `revives`,
|
||||
`downs`, `gamesPlayed`, `gamesCompleted`, `bestKills`, `bestScore`, `headshotRate`,
|
||||
`killsPerDown`, `averageKills`, `averageRounds`, and `completionRate`.
|
||||
Efficiency values in entries are unrounded ratios (for example, `0.5` means 50%)
|
||||
or `null` when their denominator is zero. UI percentages multiply by 100.
|
||||
Entries also include `records`, keyed by `highestRound`, `bestKills`, and
|
||||
`bestScore`, with the completed match ID, value, map/game/server, timing and
|
||||
observed participation behind each personal best.
|
||||
|
||||
### Success response
|
||||
|
||||
HTTP `200` returns the following top-level fields directly, with no `ok` or
|
||||
`data` wrapper:
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `entries` | Entry array | Requested page, ordered by the selected metric descending. |
|
||||
| `maps` | Map array | Available completed-match map options, filtered only by `game`, independent of `map`, `q`, metric qualification and pagination. |
|
||||
| `total` | number | Number of matching GUIDs after metric qualification and search, before pagination. |
|
||||
| `limit`, `offset` | number | Accepted pagination values. |
|
||||
| `metric` | string | Selected metric key, including the default when omitted. |
|
||||
| `summary` | object | Totals for all qualifying completed participation in the game/map scope, independent of `q`, metric qualification and pagination. |
|
||||
|
||||
Each entry combines one GUID's qualifying participation across the selected
|
||||
games/maps. It contains all of these fields regardless of the selected metric:
|
||||
|
||||
| Entry field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `guid` | string | Player identity. Preserve it as a string, including leading zeros. |
|
||||
| `name` | string | Latest reported name across both games, even if that report is outside the selected scope or from an unfinished match. |
|
||||
| `rank` | number | Rank within the scope after metric qualification, before search/pagination. Ties share rank and leave gaps: 1, 1, 3. |
|
||||
| `highestRound`, `kills`, `roundsSurvived`, `headshots`, `revives`, `downs`, `gamesPlayed`, `gamesCompleted`, `bestKills`, `bestScore` | number | The ten counters defined under [Recorded statistics](#recorded-statistics). |
|
||||
| `headshotRate`, `killsPerDown`, `averageKills`, `averageRounds`, `completionRate` | number or null | Ratios defined below; `null` if the denominator is zero. |
|
||||
| `lastSeen` | string | Latest player observation among the qualifying participation in this scope, not the latest global identity update. |
|
||||
| `records` | object | Personal-best evidence keyed by `highestRound`, `bestKills` and `bestScore`. |
|
||||
|
||||
Entries have no `game` field. Equal metric values are displayed in name order
|
||||
(ASCII case-insensitive, then exact name), then GUID order. `q` is a substring
|
||||
search, not an exact-GUID lookup; a tool looking up one GUID must check equality
|
||||
on returned `guid` values and account for pagination.
|
||||
|
||||
| Ratio | Calculation | Requirement to rank by this metric |
|
||||
| --- | --- | --- |
|
||||
| `headshotRate` | `headshots / kills` | `gamesPlayed >= 5` and `kills >= 100` |
|
||||
| `killsPerDown` | `kills / downs` | `gamesPlayed >= 5` and `downs >= 5` |
|
||||
| `averageKills` | `kills / gamesPlayed` | `gamesPlayed >= 5` |
|
||||
| `averageRounds` | `roundsSurvived / gamesPlayed` | `gamesPlayed >= 5` |
|
||||
| `completionRate` | `gamesCompleted / gamesPlayed` | `gamesPlayed >= 5` |
|
||||
|
||||
These are unrounded ratios, not formatted strings. For `headshotRate` and
|
||||
`completionRate`, `0.5` means 50%. Requirements use the selected game/map scope
|
||||
and apply only to the metric being ranked. Entries on a counter leaderboard
|
||||
still expose ratio values even when they would not qualify for a ratio ranking.
|
||||
`gamesPlayed` counts ended matches with qualifying participation, including
|
||||
matches the player left early; it does not require completion credit.
|
||||
|
||||
Each map option has string fields `key`, `game`, `map` and `name`. `map` is the
|
||||
engine map identifier; `name` is the display name. Pass `key` as the request's
|
||||
`map` parameter, preferably together with the option's `game`. Options come from
|
||||
completed matches reaching round 2, so an option can exist without any qualifying
|
||||
players. Removed servers' maps remain available.
|
||||
|
||||
| Summary field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `players` | number | Unique qualifying GUIDs, combined across games. |
|
||||
| `matches` | number | Distinct completed matches with qualifying player participation; not the sum of `gamesPlayed`. |
|
||||
| `kills`, `roundsSurvived` | number | Sums across qualifying players; rounds are player-rounds. |
|
||||
| `highestRound` | number | Highest qualifying player's recorded round. |
|
||||
| `firstTrackedAt` | string or null | Earliest match `startedAt` among those matches. |
|
||||
| `updatedAt` | string or null | Latest match report receipt time among those matches; not a response-generation time or a complete data-change token. |
|
||||
|
||||
For an empty scope, numeric summary fields are zero and both timestamps are
|
||||
`null`. A search with no results or an offset beyond `total` returns
|
||||
`entries: []` while preserving the scope's summary and available maps. An
|
||||
efficiency board can likewise have no entries while `summary.players > 0`.
|
||||
|
||||
Each `records` value has the following fields. Evidence uses the same game/map
|
||||
scope as the entry, and ties choose the earliest completion, then lowest match
|
||||
ID. The three records can refer to different matches.
|
||||
|
||||
| Evidence field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `matchId`, `value` | number | Completed match ID and that personal-best value. Link to `/matches/{matchId}` for its results page. |
|
||||
| `game`, `mapKey`, `mapName`, `server` | string | Recorded game, opaque map key, map display name and server name. |
|
||||
| `startedAt` | string | First recorded match observation; only the game start when `startObserved` is true. |
|
||||
| `endedAt` | string or null | Recorded completion time; can be unknown in older stored records. |
|
||||
| `startObserved` | boolean | Whether a `game_start` was received for the match. |
|
||||
| `presentAtStart` | boolean or null | Whether the player was first observed at the recorded start; `null` for unknown older participation. |
|
||||
| `firstObservedRound` | number or null | Round of the player's first observation, or unknown. This does not prove continuous survival. |
|
||||
|
||||
Timestamps reflect controller receipt/observation times, normally ISO 8601 UTC
|
||||
strings such as `2026-10-05T12:00:00.000Z`, not reporter gameplay-clock times.
|
||||
|
||||
### Errors, pagination and freshness
|
||||
|
||||
Invalid query values return HTTP `400` with
|
||||
`{ "ok": false, "kind": "bad-request", "error": "..." }`. Authentication errors
|
||||
have a different shape: HTTP `401` returns `{ "error": "...", "setupRequired":
|
||||
false }` (`true` before initial administrator setup); a required password change
|
||||
returns HTTP `403` with `{ "error": "...", "mustChangePassword": true }`.
|
||||
Host/enrollment bearer credentials return `403`. Disallowed HTTP transport
|
||||
returns `426` with an `error` string. Report-service failures can produce `5xx`;
|
||||
check status and content type before assuming the success schema or a uniform
|
||||
error body. Failed/rate-limited login requests use their own auth error responses.
|
||||
|
||||
For successive pages, retain the same filters and increase `offset` by `limit`
|
||||
until `offset + entries.length >= total` or the page is empty. There is no cursor,
|
||||
snapshot token, date-range filter or incremental `since` parameter. Each report
|
||||
reads a consistent database snapshot, but separate requests may see newly ended
|
||||
matches or name changes. A multi-page crawl can therefore miss or repeat players
|
||||
as their ordering changes; deduplicate by GUID and refresh as needed. The offset
|
||||
cap also limits how far a tool can page through a very large selection.
|
||||
|
||||
Reports run in a worker and use caches keyed by database revision, invalidated
|
||||
by writes to match, participation and player-identity tables. There is no fixed
|
||||
TTL wait before newly committed leaderboard data becomes eligible for a fresh
|
||||
report. Identical requests already in flight may share a response. Neither
|
||||
`summary.updatedAt` nor an entry's `lastSeen` should be used as a cache revision:
|
||||
an identity rename can change results without changing those timestamps.
|
||||
|
||||
### Related routes and current limits
|
||||
|
||||
`GET /api/matches` and `GET /api/servers/{name}/matches` return JSON match
|
||||
summaries as `{ "matches": [...], "total": number }`, under the same session
|
||||
authentication. Both accept only `limit` (default 25, range 1–10,000); there is
|
||||
no offset pagination. Results are newest first by first observation, then match
|
||||
ID, and include active/interrupted matches that reached round 2. Their
|
||||
[ServerMatch fields](../src/lib/match-history.ts) differ from leaderboard entries
|
||||
and do not include player counters or a roster. Unknown server names return an
|
||||
empty list. URL-encode server names in paths.
|
||||
|
||||
`/leaderboard/players/{guid}` and `/matches/{id}` are HTML pages backed by server
|
||||
loaders. There is currently no dedicated JSON endpoint at
|
||||
`/api/leaderboard/players/{guid}` or `/api/matches/{id}` for complete profiles,
|
||||
player match history or match rosters. A tool needing those records will need
|
||||
an API extension; the leaderboard endpoint supplies aggregate stats and
|
||||
personal-best evidence only.
|
||||
|
||||
## Query performance
|
||||
|
||||
Leaderboard reads materialize the selected completed participation once, reuse
|
||||
one player aggregate for ranking and summary totals, and reuse the ranked/search
|
||||
result for pagination and its matching count. Empty and out-of-range pages still
|
||||
return accurate totals. Record evidence is fetched only for players on the page.
|
||||
Exact player-rank reads share the ranking rules but skip unused fleet summaries
|
||||
and map lists. No response cache delays visibility of a newly completed game.
|
||||
On a cache miss, leaderboard reads aggregate the selected completed participation
|
||||
for rankings and summary totals. The ranked scope and map options are reused
|
||||
across search/pagination requests at the same database revision; filtering and
|
||||
pagination operate on that ranked list. Empty and out-of-range pages still
|
||||
return accurate totals. Record evidence is fetched only for requested entries.
|
||||
Exact player-rank reads use the same ranked scope, including its summaries and
|
||||
map list. Report work runs in a dedicated worker with revision-keyed response
|
||||
caching as described above.
|
||||
|
||||
The benchmark and measurements below describe the earlier SQL optimization,
|
||||
before the worker and revision caches were added. They are historical results,
|
||||
not measurements of the current HTTP implementation.
|
||||
|
||||
Run the deterministic, isolated benchmark with:
|
||||
|
||||
@@ -208,10 +397,11 @@ small-fixture parity cases and 22 timed small/large request cases matched the
|
||||
baseline's complete responses, including ratios and record evidence. Timings
|
||||
include SQL preparation and evidence lookup, exclude HTTP/browser rendering, and
|
||||
use a warm filesystem cache with alternating implementation order. These are
|
||||
local synthetic measurements, not production latency guarantees. Queries still
|
||||
run synchronously, so larger histories remain a capacity consideration.
|
||||
local synthetic measurements, not production latency guarantees. SQL queries
|
||||
run synchronously within the report worker; larger histories still affect
|
||||
report latency and worker capacity.
|
||||
|
||||
Validation for this change passed 1,019 unit tests, Svelte checking with no errors
|
||||
Validation for that optimization passed 1,019 unit tests, Svelte checking with no errors
|
||||
or warnings, and a production build. Isolated Firefox walkthroughs at 1440px and
|
||||
390px checked completed-only totals, record links, efficiency qualification,
|
||||
scoped ranks/history, preserved navigation, empty scopes and mobile expansion.
|
||||
|
||||
Reference in New Issue
Block a user