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
|
## Read API
|
||||||
|
|
||||||
`GET /api/leaderboard` accepts optional `game` (`t5` or `t6`), `map` (the opaque
|
This reference describes the current repository implementation. A deployed
|
||||||
key returned in `maps`), `metric` (default `highestRound`), `q` (name/GUID search),
|
instance must run the corresponding code to provide the same behavior. The
|
||||||
`limit` (1–100, default 50), and `offset` (default 0). It returns ranked `entries`,
|
sources are the [HTTP handler](../src/routes/api/leaderboard/+server.ts),
|
||||||
available `maps`, the matching player `total`, pagination values, and `summary`.
|
[response types](../src/lib/leaderboard.ts),
|
||||||
Each entry represents one GUID and combines its qualifying matches within the
|
[query implementation](../src/lib/server/fleet/leaderboard.ts), and
|
||||||
game/map filters. Entries have no game field; map options retain their game
|
[authentication guard](../src/lib/server/auth/guard.ts).
|
||||||
identity. Both `total` and `summary.players` count unique GUIDs; `total` applies
|
|
||||||
the selected metric's qualification rules and player search, while the summary
|
### Authentication and requests
|
||||||
covers all completed qualifying participation in the game/map scope.
|
|
||||||
|
`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`,
|
Metric keys are `highestRound`, `kills`, `roundsSurvived`, `headshots`, `revives`,
|
||||||
`downs`, `gamesPlayed`, `gamesCompleted`, `bestKills`, `bestScore`, `headshotRate`,
|
`downs`, `gamesPlayed`, `gamesCompleted`, `bestKills`, `bestScore`, `headshotRate`,
|
||||||
`killsPerDown`, `averageKills`, `averageRounds`, and `completionRate`.
|
`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.
|
### Success response
|
||||||
Entries also include `records`, keyed by `highestRound`, `bestKills`, and
|
|
||||||
`bestScore`, with the completed match ID, value, map/game/server, timing and
|
HTTP `200` returns the following top-level fields directly, with no `ok` or
|
||||||
observed participation behind each personal best.
|
`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
|
## Query performance
|
||||||
|
|
||||||
Leaderboard reads materialize the selected completed participation once, reuse
|
On a cache miss, leaderboard reads aggregate the selected completed participation
|
||||||
one player aggregate for ranking and summary totals, and reuse the ranked/search
|
for rankings and summary totals. The ranked scope and map options are reused
|
||||||
result for pagination and its matching count. Empty and out-of-range pages still
|
across search/pagination requests at the same database revision; filtering and
|
||||||
return accurate totals. Record evidence is fetched only for players on the page.
|
pagination operate on that ranked list. Empty and out-of-range pages still
|
||||||
Exact player-rank reads share the ranking rules but skip unused fleet summaries
|
return accurate totals. Record evidence is fetched only for requested entries.
|
||||||
and map lists. No response cache delays visibility of a newly completed game.
|
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:
|
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
|
baseline's complete responses, including ratios and record evidence. Timings
|
||||||
include SQL preparation and evidence lookup, exclude HTTP/browser rendering, and
|
include SQL preparation and evidence lookup, exclude HTTP/browser rendering, and
|
||||||
use a warm filesystem cache with alternating implementation order. These are
|
use a warm filesystem cache with alternating implementation order. These are
|
||||||
local synthetic measurements, not production latency guarantees. Queries still
|
local synthetic measurements, not production latency guarantees. SQL queries
|
||||||
run synchronously, so larger histories remain a capacity consideration.
|
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
|
or warnings, and a production build. Isolated Firefox walkthroughs at 1440px and
|
||||||
390px checked completed-only totals, record links, efficiency qualification,
|
390px checked completed-only totals, record links, efficiency qualification,
|
||||||
scoped ranks/history, preserved navigation, empty scopes and mobile expansion.
|
scoped ranks/history, preserved navigation, empty scopes and mobile expansion.
|
||||||
|
|||||||
Reference in New Issue
Block a user