443 lines
34 KiB
Markdown
443 lines
34 KiB
Markdown
# Leaderboard
|
||
|
||
Open **Leaderboard** in the main navigation to compare players across the fleet,
|
||
with one entry per GUID combining Black Ops I (T5) and Black Ops II (T6). Game, map,
|
||
and starting squad-size filters limit which matches contribute to each player's statistics and
|
||
rank. Choose a ranking, search a name or GUID, and share the page URL with its
|
||
filters intact. Results refresh while the page is visible. The page defaults to
|
||
**All maps** for a fleet-wide comparison; choose a map to narrow the results. A URL
|
||
with no `map` parameter or with `map=` selects all maps. **Players** selects Solo, Duo, Trio or
|
||
Four players using the recorded starting roster; unknown starting counts appear
|
||
only under All squad sizes. The JSON API also defaults to all maps.
|
||
|
||
## Qualifying matches
|
||
|
||
Matches only appear in server/global history once they reach round 2.
|
||
Leaderboards, summaries, map options, profile statistics and player history include only
|
||
completed matches with a received `game_end`. Active, interrupted, and
|
||
**End report missing** matches do not contribute. A player must also be observed
|
||
at round 2 or later; players who left before a completed game's end retain their
|
||
recorded stats.
|
||
This also excludes existing round 1 records. Early telemetry is retained so
|
||
qualifying games keep their start time, roster and cumulative counters.
|
||
|
||
## Player profiles and match results
|
||
|
||
### Profiles and filters
|
||
|
||
Players have a profile as soon as they are first reported, even before they have
|
||
qualifying match history. These profiles show their identity, an explanation of
|
||
when statistics become available, and dashes for personal bests with no recorded
|
||
match. The page fills in on refresh after a qualifying game ends. Unknown player
|
||
GUIDs still return 404.
|
||
|
||
Click a player's name to open their shareable profile at
|
||
`/leaderboard/players/[guid]`. Profiles combine Black Ops I (T5) and Black Ops II
|
||
(T6) records for the same GUID. Existing `/leaderboard/players/[game]/[guid]`
|
||
links redirect to the combined profile. Profiles show all ten lifetime statistics,
|
||
headshots as a percentage of kills, kills per down, average kills and observed
|
||
rounds per game, and completion rate. Each map has its own complete set of
|
||
records; each personal best shows its rank among players on that map in that game,
|
||
across all squad sizes. Equal bests share a rank, with the next rank skipping tied
|
||
positions (for example, 1, 1, 3). Map records and match history identify their game. Match history shows
|
||
individual counters, server, date and completion credit in pages of 25 matches
|
||
across both games. Map names link back to that map's leaderboard in its game.
|
||
Profiles update every 30 seconds while visible; the freshness indicator offers Retry if an update fails. Ratios with a zero denominator
|
||
display a dash. Lifetime statistics and per-map records always cover all recorded
|
||
maps across both games.
|
||
|
||
Opening a player from the leaderboard preserves the originating game, map, squad size,
|
||
metric, search and page in the **Leaderboard** return link. The profile's
|
||
**Filters** controls initialize from that game/map/squad/metric and show
|
||
the player's rank within that competition, independent of the original search.
|
||
These controls also filter match history; lifetime totals stay unchanged.
|
||
History pagination retains the selection and return link. Map links preserve
|
||
the selected ranking metric. A player without qualifying matches in a selection
|
||
can still view their lifetime profile and choose another scope.
|
||
|
||
On phones, per-map records and match history use compact cards with the map,
|
||
game, record values and match identity up front. Record values link directly to
|
||
their completed match, where the full evidence is available. Desktop profiles
|
||
use compact tables; both views use the same history filters
|
||
and pagination. History dates say **First observed**, since a missing start
|
||
report can make the first recorded observation later than the actual game start.
|
||
|
||
### Record evidence and match results
|
||
|
||
Highest-round, best-game-kill and highest-score records link to the completed
|
||
match that earned them on the leaderboard and in lifetime/per-map profile stats.
|
||
Each source names its match and map. Open the match to see completion time,
|
||
server, whether its start was recorded, and player participation evidence.
|
||
Presence is not proof of continuous survival. Equal personal bests link to the
|
||
earliest recorded completion, then the lowest match ID when times tie.
|
||
|
||
Completed matches link to their own results page at `/matches/[id]` from global
|
||
Match History, server history, and player profiles. The page shows the recorded
|
||
map and server, final round, start/end times, duration when the start was observed,
|
||
player counts, and the full saved roster. Players missing from the final report
|
||
retain their earlier counters; players with no stats display dashes. Counters are
|
||
the highest reported values within the match, not necessarily its final scoreboard.
|
||
|
||
Each match shows one score column. If any player has a recorded total (including
|
||
zero), **Total score** shows each player's highest recorded cumulative points
|
||
earned in that match. Players absent from the final report retain their latest
|
||
recorded total. A dash in this column means no total was recorded for that player;
|
||
their points balance is never substituted. Historical matches where all totals
|
||
are missing retain **Highest reported score**, the highest recorded points
|
||
balance, not total points earned or necessarily the final score. This choice is
|
||
based on recorded data, not the match date. It does not change leaderboard or
|
||
profile score rankings, which still use balances.
|
||
|
||
Active, interrupted, and **End report missing** matches remain listed
|
||
without results links. Matches can filter each status and show known gaps in the
|
||
reporter's sequence; a delayed report can reduce that missing-report count.
|
||
Match pages remain available after the original server is removed.
|
||
|
||
### Player identity
|
||
|
||
The leaderboard uses the existing telemetry events; no reporter change is
|
||
required. The leaderboard and profiles identify a player by **GUID**, displaying
|
||
the latest reported name across both games, regardless of game or map filters.
|
||
Different names for the same GUID do not split their leaderboard entry or
|
||
profile; identical names with different GUIDs remain separate. Stored player
|
||
records remain keyed by **game + GUID** for telemetry ingestion; combining their
|
||
identity requires no rekeying. The record-source upgrade automatically adds
|
||
nullable participation-start fields; existing rows keep an unknown start instead
|
||
of inferring one. GUIDs stay strings.
|
||
|
||
## Recorded statistics
|
||
|
||
Telemetry stat reports (`round_change`, `heartbeat`, and `game_end`) may include
|
||
`score_total`, a nonnegative integer read from the game's cumulative match score.
|
||
`score` remains the spendable balance. Updated T5/T6 reporters capture game end
|
||
at the first end signal, before stock intermission replaces the balance with the
|
||
total. `game_start` remains identity-only. Deploy the receiver before the updated
|
||
reporters so it retains the new field.
|
||
|
||
The receiver accepts older reports without `score_total`. Latest telemetry
|
||
preserves the field when supplied; match history retains the highest observed
|
||
total independently of the balance and exposes it as `scoreTotal` on each player
|
||
in the match-detail API. Missing totals and pre-upgrade history are unknown
|
||
(`null`), not zero, and are never inferred from balances. Totals are per match;
|
||
repeated snapshots are not added together. Existing score rankings continue to
|
||
use the highest reported balance.
|
||
|
||
| Statistic | Meaning |
|
||
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| Highest round | Highest round reported with that player present, including players who joined mid-game. |
|
||
| Total kills, headshots, revives, downs | Sum of each player's highest reported counter in each game. Round snapshots are cumulative, so they are never added together within a game. |
|
||
| Total rounds | Round progression during observed participation, added across games. A player observed at game start and then round 8 receives seven rounds. A player first seen at round 8 gets a highest-round record of 8, but zero total rounds; reaching round 9 then adds one. |
|
||
| Games played | Completed games in which a stats report at round 2 or later contains the player, excluding players who left in the same round they joined. Staying through game end counts even without a round change. Legacy participation with an unknown joining round still counts. Start-only and round 1 participation do not count. |
|
||
| Games completed | Games whose `game_end` roster contains the player. Disconnected players retain earlier stats without receiving completion credit. |
|
||
| Best game kills | Highest kill count recorded in any one game. |
|
||
| Highest reported score | Highest observed score balance. Spending points can lower the score, so this is not cumulative points earned. |
|
||
|
||
Same-round departures retain their recorded stats, personal bests, and match
|
||
history; only the games-played count excludes them. This also changes the game
|
||
count used for efficiency qualification and per-game averages.
|
||
|
||
Every ranking sorts highest first, including downs (a count, not an efficiency
|
||
rating). Equal values share a rank; name and GUID provide a stable display order.
|
||
Searching preserves the player's rank for the selected game/map filters, or
|
||
across both games when unfiltered. Summary cards describe the same selection,
|
||
regardless of the search text. Player counts include each qualifying GUID once,
|
||
including when they have records in both games. Summary rounds are
|
||
player-rounds: four players progressing from game start to round 8 contribute 28.
|
||
Highest-round records always use the round reached in a completed match,
|
||
including the final round and players who joined mid-game.
|
||
|
||
### Efficiency rankings
|
||
|
||
Efficiency rankings are also available for **Headshots / kills**, **Kills per
|
||
down**, **Average kills**, **Average rounds**, and **Completion rate**. Each
|
||
requires at least five completed games in the selected game/map scope. Headshot
|
||
rate also requires 100 kills; kills per down requires five downs. The board shows
|
||
the numerator, denominator and game count beside each ratio. Ratios are computed
|
||
from aggregate counters, not an average of individual match percentages.
|
||
Completion rate is the share of those ended games whose final report contained
|
||
the player; it is not a win rate. Games without a received `game_end`, including
|
||
**End report missing** games, do not enter any denominator. Zero-denominator ratios
|
||
remain unknown and are not ranked.
|
||
|
||
Qualification is applied before ranking, search and pagination. Summary cards
|
||
still describe all players in the game/map selection, including those below the
|
||
threshold. Profile metrics show their values and whether the player qualifies
|
||
for the combined lifetime board; a scoped board can have a smaller sample.
|
||
|
||
### Participation limits
|
||
|
||
Round totals require the player to appear in consecutive observations. If a
|
||
report omits them, their return starts a new participation interval and the gap
|
||
is not credited. The payload cannot tell whether a present player was alive,
|
||
spectating, or absent between reports. Mid-game snapshots still contribute their
|
||
reported kills and other counters, even when earlier participation is unknown.
|
||
|
||
## Persistence and match boundaries
|
||
|
||
Match and player records live in SQLite alongside the rest of Electric Cherry.
|
||
They persist across app restarts and server deletion, and are not subject to
|
||
campaign history retention. The latest scoreboard and leaderboard update in the
|
||
same transaction. Each match keeps player counters even after someone leaves.
|
||
|
||
A `game_start` opens a match; `game_end` completes it. Reporters can send an optional
|
||
`match` key and per-game `seq` on every event; see the
|
||
[reporter protocol](beta-test-plans.md#event-fields) for field limits and retry rules.
|
||
A key identifies one game on one server, including when the start report was
|
||
missed. An unseen key opens a new game and labels the previous active keyed game
|
||
**End report missing**. This records missing evidence; it does not establish
|
||
whether the game finished or was interrupted. A late `game_end` for a previously
|
||
seen key can still complete its own match; reports for known older keys do not
|
||
replace the newer game's live state.
|
||
|
||
Keys are opaque, and `seq` orders reports only within one game. Electric Cherry
|
||
therefore orders games by their first processed report. If game `new` is already
|
||
reporting when the first report for game `old` arrives, the unseen `old` key
|
||
supersedes `new` and marks it **End report missing**. Later `new` heartbeats update
|
||
its history but cannot restore it as the live game. If `old` arrived as a
|
||
heartbeat, it can remain active even after `new` ends. Reporters should preserve
|
||
delivery order across games, keeping queued retries ahead of newer games' reports.
|
||
Reliably ordering unseen keys would require additional reporter metadata, such
|
||
as a persistent, increasing per-server game counter; the current protocol has no
|
||
such field.
|
||
|
||
With both fields present, each sequence is counted once, including across app
|
||
restarts. Lower sequences fill missing-report gaps without rolling back newer
|
||
rounds, counters, or rosters. A delayed `game_start` may fill the recorded starting
|
||
roster without resetting progression. The history API exposes the highest
|
||
sequence minus the count of unique received sequences as `missingReports`, or
|
||
`null` when sequencing is unavailable. A missing report count does not grant
|
||
credit for unobserved rounds or complete a game without `game_end`.
|
||
|
||
Without a match key, the legacy heuristics remain: a changed map, a lower round,
|
||
or a round report after a completed game can establish a new match when the
|
||
start was missed. Superseded active matches remain interrupted. Repeated starts
|
||
before any stats belong to the same match. Identical consecutive payloads
|
||
(ignoring receipt time and roster order) do not add records. Repeated snapshots
|
||
in a match cannot increase counters or rounds twice.
|
||
|
||
The first upgrade imports the latest stored full scoreboard from each server
|
||
once. That provides the available counters and highest round, but not discarded
|
||
earlier games or unobserved round progression. Old round-only snapshots have no
|
||
player identity and cannot be imported. Tracking continues from future reports.
|
||
|
||
T6 selections sharing an engine map, such as Town Survival and TranZit, remain
|
||
separate using the server's registered map variant. Unknown variants remain
|
||
explicitly identified instead of being labeled as a known selection. A match
|
||
retains the game/map identity recorded at ingestion even if the server is later
|
||
reconfigured or removed.
|
||
|
||
Legacy reports without match identity cannot reliably distinguish a late report
|
||
from an older game from a new game. Likewise, identical starts before any stats
|
||
are indistinguishable from retries. Send those reports once in order. Within a
|
||
match, counter decreases use the previous high-water mark; if a reconnect resets
|
||
a player's counters, subsequent activity may be undercounted until those counters
|
||
pass their earlier maxima. Match keys and sequence numbers do not identify
|
||
player counter resets.
|
||
|
||
## Read API
|
||
|
||
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 public JSON endpoint; `HEAD` is public too. A tool
|
||
does not need an account, session cookie, API key or authorization header to
|
||
read it. Reads also work before administrator setup and with an expired session
|
||
or a session awaiting a password change. This exception applies only to reads
|
||
of this exact API route. Leaderboard/profile HTML pages, match-history APIs and
|
||
management routes still require a session; see [Users and access](authentication.md).
|
||
|
||
For example:
|
||
|
||
```sh
|
||
EC_BASE_URL='https://ec.example.com'
|
||
curl --fail-with-body --silent --show-error \
|
||
--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. The read request does not require an `Origin` header.
|
||
This endpoint does not add cross-origin browser access (CORS) headers; the
|
||
example above works for command-line or server-side clients. 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. |
|
||
| `players` | All squad sizes | Exactly `1`, `2`, `3`, or `4`, matching the recorded starting player count. Unknown counts contribute only when this filter is absent. |
|
||
| `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`.
|
||
|
||
### 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": "..." }`. Public reads do not
|
||
return `401` for missing/expired sessions or `403` for a required password change.
|
||
Sending host/enrollment bearer credentials still returns `403` because those
|
||
credentials are restricted to the host-agent API; omit them. 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.
|
||
|
||
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, "page": number, "limit": number, "maps": [...] }`.
|
||
These routes still require session authentication. Both accept `limit` (default
|
||
25, range 1–10,000), `page`, `game`, `map`, `q` (server name), `days` (0, 7, 30, or
|
||
90), `status`, `sort` (`started`, `round`, `duration`, or `players`), and `direction`
|
||
(`asc` or `desc`). Results default to newest first by first observation, then
|
||
match ID, and include unfinished matches that reached round 2. Status values are
|
||
`active`, `completed`, `interrupted`, and `end-report-missing`; omit the filter for
|
||
all statuses. `reporterMatchKey` is the supplied match key or `null`;
|
||
`missingReports` is the known sequence-gap count or `null`. 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
|
||
|
||
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.
|
||
|
||
See the [historical SQL benchmark](../research/leaderboard-query-benchmark.md)
|
||
for the earlier optimization measurements and methodology.
|