347 lines
13 KiB
Markdown
347 lines
13 KiB
Markdown
# Global Ranking Page Plan
|
|
|
|
## Objective
|
|
|
|
Define the V1 functional contract for a dedicated `Ranking` page that exposes public top lists across HLL Vietnam community servers without merging this flow into `Stats`.
|
|
|
|
## Why Ranking Is Separate From Stats
|
|
|
|
`Stats` and `Ranking` solve different user jobs:
|
|
|
|
- `Stats` is player-centric: search one player, open one profile, inspect personal performance.
|
|
- `Ranking` is leaderboard-centric: open a public table, switch filters, compare top players.
|
|
|
|
They can safely share:
|
|
|
|
- the same sober tactical visual system already used by `stats.html` and `historico.html`
|
|
- the same backend health/offline messaging pattern
|
|
- the same table/card rendering conventions for loading, empty and error states
|
|
|
|
They must remain separate in navigation and contracts:
|
|
|
|
- `Stats` keeps player search and personal summary endpoints.
|
|
- `Ranking` gets its own page and dedicated ranking endpoint.
|
|
- `Ranking` may link to `Stats` for player lookup, but it does not embed the player search workflow as a primary interaction.
|
|
|
|
## Current Foundation
|
|
|
|
- Weekly and monthly historical leaderboard reads already exist in `backend/app/rcon_historical_leaderboards.py`.
|
|
- Annual ranking reads already exist through snapshot storage in `backend/app/rcon_annual_rankings.py`.
|
|
- Historical ranking remains RCON-first.
|
|
- Public scoreboard remains fallback or enrichment only and must not become the primary read path while RCON coverage exists.
|
|
|
|
## V1 Scope
|
|
|
|
The first Ranking page supports:
|
|
|
|
- public top-player lists
|
|
- timeframe filter: `weekly`, `monthly`, `annual`
|
|
- server scope filter: `all`, `comunidad-hispana-01`, `comunidad-hispana-02`
|
|
- metric filter: V1 committed to `kills`
|
|
- limit filter: default `20`, with smaller values allowed
|
|
- filter changes without manual page reload where feasible
|
|
- clear differentiation between `ready`, `missing`, `empty`, `offline` and request-error states
|
|
|
|
## V1.1 Metric Expansion Contract
|
|
|
|
V1.1 expands `Ranking global` only within the existing RCON-first read paths and annual snapshot boundary.
|
|
|
|
Supported metrics:
|
|
|
|
- `kills`
|
|
- `deaths`
|
|
- `teamkills`
|
|
- `matches_considered`
|
|
- `kd_ratio`
|
|
- `kills_per_match`
|
|
|
|
Calculation rules:
|
|
|
|
- `kills`: `SUM(kills)`
|
|
- `deaths`: `SUM(deaths)`
|
|
- `teamkills`: `SUM(teamkills)`
|
|
- `matches_considered`: `COUNT(DISTINCT match_key)` across closed matches in the selected window
|
|
- `kd_ratio`: `SUM(kills) / SUM(deaths)`
|
|
- `kills_per_match`: `SUM(kills) / COUNT(DISTINCT match_key)`
|
|
|
|
Safe division rules:
|
|
|
|
- if `deaths = 0`, `kd_ratio` returns `kills` as a finite display-safe value rather than dividing by zero
|
|
- if `matches_considered = 0`, `kills_per_match` returns `0`
|
|
- backend should return rounded display-safe decimal values for ratio metrics
|
|
|
|
Ordering and tie-break rules:
|
|
|
|
- `kills`: order by `kills` desc, `matches_considered` desc, `player_name` asc
|
|
- `deaths`: order by `deaths` desc, `matches_considered` desc, `player_name` asc
|
|
- `teamkills`: order by `teamkills` desc, `matches_considered` desc, `player_name` asc
|
|
- `matches_considered`: order by `matches_considered` desc, `kills` desc, `player_name` asc
|
|
- `kd_ratio`: order by `kd_ratio` desc, `kills` desc, `matches_considered` desc, `player_name` asc
|
|
- `kills_per_match`: order by `kills_per_match` desc, `kills` desc, `matches_considered` desc, `player_name` asc
|
|
|
|
Timeframe support:
|
|
|
|
- `weekly`: supports all V1.1 metrics through the existing materialized RCON runtime read model
|
|
- `monthly`: supports all V1.1 metrics through the existing materialized RCON runtime read model
|
|
- `annual`: remains snapshot-safe first; `kills` is required support and extra annual metrics are allowed only when a precomputed annual snapshot already exists for that metric
|
|
|
|
Annual safety boundary:
|
|
|
|
- annual requests must remain snapshot-backed
|
|
- annual requests must not trigger full-year recomputation in the public request path
|
|
- if an annual metric does not have a safe snapshot-backed read path, backend must return controlled `400` instead of degrading to runtime aggregation
|
|
- until extra annual snapshots are explicitly implemented, annual support is effectively `kills`-only
|
|
|
|
## V1 Non-goals
|
|
|
|
- Elo/MMR
|
|
- authentication
|
|
- private or expanded player profiles
|
|
- advanced charts
|
|
- weapon or map breakdowns
|
|
- large schema changes
|
|
- annual recalculation on public request
|
|
- reintroducing Comunidad Hispana #03
|
|
|
|
## User Flow
|
|
|
|
1. User opens `Ranking`.
|
|
2. Page checks backend availability.
|
|
3. User chooses timeframe.
|
|
4. User chooses server scope.
|
|
5. User chooses metric.
|
|
6. User chooses limit if needed.
|
|
7. Frontend requests ranking data and updates the list in place.
|
|
8. User can switch filters again without manual reload.
|
|
9. If the user wants one-player analysis, the page offers a small link to `Stats`.
|
|
|
|
## Data Source Policy
|
|
|
|
### Weekly and monthly
|
|
|
|
- Source: materialized RCON/AdminLog leaderboard read model.
|
|
- Reader: existing weekly/monthly selection logic in `select_leaderboard_window(...)`.
|
|
- Ordering: metric desc, `matches_considered` desc, `player_name` asc.
|
|
- V1 metric commitment: `kills`.
|
|
- V1.1 metric support: `kills`, `deaths`, `teamkills`, `matches_considered`, `kd_ratio`, `kills_per_match`.
|
|
|
|
### Annual
|
|
|
|
- Source: persisted annual ranking snapshots.
|
|
- Reader: existing annual snapshot loader.
|
|
- No annual recomputation during public requests.
|
|
- Missing annual data must surface as a controlled `snapshot_status="missing"` state.
|
|
- V1 required annual metric: `kills`.
|
|
- Extra annual metrics are out of scope until their snapshot generation and read path are explicitly implemented.
|
|
|
|
## API Contract Direction
|
|
|
|
### Dedicated Ranking endpoint
|
|
|
|
```http
|
|
GET /api/ranking?timeframe=weekly|monthly|annual&server_id=<server-or-all>&metric=<metric>&limit=20&year=<year-when-annual>
|
|
```
|
|
|
|
Purpose:
|
|
|
|
- give Ranking its own public contract without changing Stats endpoints or mixing player-profile concerns into ranking reads
|
|
|
|
Query rules:
|
|
|
|
- `timeframe` is optional and defaults to `weekly`
|
|
- `server_id` is optional and defaults to `all`
|
|
- `metric` is optional and defaults to `kills`
|
|
- `limit` is optional and defaults to `20`
|
|
- `year` is required only when `timeframe=annual`
|
|
|
|
Validation rules:
|
|
|
|
- allowed `timeframe`: `weekly`, `monthly`, `annual`
|
|
- V1.1 allowed `metric` for weekly/monthly: `kills`, `deaths`, `teamkills`, `matches_considered`, `kd_ratio`, `kills_per_match`
|
|
- allowed `annual` metric by default: `kills`
|
|
- annual extra metrics require an explicit snapshot-backed implementation; otherwise return `400`
|
|
- allowed `limit`: `1..100`
|
|
- `year` must be a positive integer when annual is requested
|
|
- annual requests ignore weekly/monthly runtime window policy and read snapshots only
|
|
- unsupported metric, unsupported timeframe or unsupported annual metric must return a controlled request-validation error and must not silently downgrade to `kills`
|
|
|
|
## Response Contract
|
|
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"data": {
|
|
"page_kind": "global-ranking",
|
|
"timeframe": "monthly",
|
|
"server_id": "all",
|
|
"metric": "kills_per_match",
|
|
"limit": 20,
|
|
"requested_limit": 20,
|
|
"window_start": "2026-06-01T00:00:00Z",
|
|
"window_end": "2026-06-08T18:00:00Z",
|
|
"window_kind": "current-month",
|
|
"window_label": "Mes actual",
|
|
"source": {
|
|
"primary_source": "rcon",
|
|
"read_model": "rcon-materialized-admin-log-leaderboard",
|
|
"generated_at": "2026-06-08T18:00:00Z",
|
|
"freshness": "runtime"
|
|
},
|
|
"snapshot_status": "ready",
|
|
"items": [
|
|
{
|
|
"ranking_position": 1,
|
|
"player_id": "76561198000000000",
|
|
"player_name": "Rambo",
|
|
"metric_value": 30.07,
|
|
"matches_considered": 14,
|
|
"kills": 421,
|
|
"deaths": 280,
|
|
"teamkills": 3,
|
|
"kd_ratio": 1.5,
|
|
"kills_per_match": 30.07
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Field Rules
|
|
|
|
Common top-level fields:
|
|
|
|
- `timeframe`: `weekly`, `monthly`, `annual`
|
|
- `server_id`: `all` or one supported active server id
|
|
- `metric`: requested supported metric
|
|
- `limit`: effective served limit
|
|
- `requested_limit`: requested client limit
|
|
- `snapshot_status`: always present for frontend state handling
|
|
- `items`: always an array
|
|
|
|
Required item fields:
|
|
|
|
- `ranking_position`
|
|
- `player_id`
|
|
- `player_name`
|
|
- `metric_value`
|
|
- `matches_considered`
|
|
- `kills`
|
|
- `deaths`
|
|
- `teamkills`
|
|
- `kd_ratio`
|
|
- `kills_per_match`
|
|
|
|
Conditional item rules:
|
|
|
|
- `metric_value` may be integer or rounded decimal depending on the requested metric
|
|
- `kills_per_match` should be present when that metric is supported in the response contract
|
|
- `matches_considered` remains required because it is both a displayed field and a common tie-break input
|
|
|
|
Weekly/monthly-only metadata:
|
|
|
|
- `window_start`
|
|
- `window_end`
|
|
- `window_kind`
|
|
- `window_label`
|
|
- `selection_reason`
|
|
|
|
Annual-only metadata:
|
|
|
|
- `year`
|
|
- `generated_at`
|
|
- `snapshot_limit`
|
|
- `effective_limit`
|
|
- `item_count`
|
|
|
|
## Timeframe-specific expectations
|
|
|
|
### Weekly
|
|
|
|
- Use the same weekly fallback rule already defined by the leaderboard read model.
|
|
- If current-week sample is insufficient, backend may serve previous-week data.
|
|
- Frontend must not reinterpret the window; it should display returned window metadata.
|
|
|
|
### Monthly
|
|
|
|
- Use the same current-month vs previous-month rule already defined by the leaderboard read model.
|
|
- Frontend must display the returned monthly window metadata exactly as served.
|
|
|
|
### Annual
|
|
|
|
- Read only from annual snapshots.
|
|
- `snapshot_status="ready"` with `items=[]` is a valid empty-ready state.
|
|
- `snapshot_status="missing"` is not a backend crash and must be rendered distinctly.
|
|
- Annual must not pretend support for metrics that do not have precomputed snapshots.
|
|
- If client requests an unsupported annual metric, backend should return `400` rather than an empty-ready payload.
|
|
|
|
## UI State Contract
|
|
|
|
The Ranking page must support these states explicitly:
|
|
|
|
- `loading`: request in flight
|
|
- `backend offline`: `/health` unavailable or ranking request unreachable
|
|
- `no data`: successful response with `items=[]` in weekly/monthly
|
|
- `annual snapshot missing`: annual response with `snapshot_status="missing"`
|
|
- `unsupported metric`: client requested unsupported metric or backend returns 400 metric validation error
|
|
- `controlled error`: valid HTTP response with request validation error or unexpected backend failure
|
|
|
|
Expected rendering behavior:
|
|
|
|
- loading keeps previous table hidden or visually muted
|
|
- backend offline shows a clear retry-safe message
|
|
- no data keeps filters visible and explains that there are no rows for the active scope
|
|
- annual snapshot missing explains that the annual snapshot has not been generated yet
|
|
- unsupported metric does not silently downgrade to another metric
|
|
- unsupported annual metric explains that the metric is not available for annual snapshots yet
|
|
- controlled error does not break page navigation or filter controls
|
|
|
|
## Backend Behavior Requirements
|
|
|
|
- Preserve existing Stats endpoint compatibility.
|
|
- Do not expose public-scoreboard as the normal primary source for Ranking.
|
|
- Do not expand annual generation logic inside request handling.
|
|
- Keep server scope limited to active supported scopes: `all`, `comunidad-hispana-01`, `comunidad-hispana-02`.
|
|
- Do not surface Comunidad Hispana #03 in defaults, options or backend read scope.
|
|
- Do not add large new tables or a second ranking architecture for V1.1 metric expansion.
|
|
|
|
## Payload Adjustment Notes
|
|
|
|
- The existing `metric_value` field remains the primary sortable/display value for the active metric.
|
|
- Weekly and monthly responses should keep the existing totals payload and may add `kills_per_match` when the backend serves that metric.
|
|
- Annual responses should keep the same payload shape and only expand metrics when the snapshot schema already stores the required values safely.
|
|
- Client code should continue to rely on explicit `metric` and `snapshot_status` fields rather than inferring support from item contents alone.
|
|
|
|
## V1.1 Non-goals
|
|
|
|
- Elo/MMR
|
|
- public scoreboard as primary ranking source
|
|
- annual runtime full-year recomputation
|
|
- large new tables
|
|
- advanced charts
|
|
- authentication
|
|
- Comunidad Hispana #03
|
|
|
|
## Frontend Integration Guidance
|
|
|
|
Recommended page split:
|
|
|
|
- `stats.html`: personal lookup and personal ranking context
|
|
- `ranking.html`: public top lists
|
|
|
|
Recommended safe reuse:
|
|
|
|
- backend availability chip pattern from `stats.js`
|
|
- existing tactical panel and card styling from `styles.css`
|
|
- simple select/button driven filtering similar to `historico.html`
|
|
|
|
Recommended minimal cross-links:
|
|
|
|
- `Ranking` page links to `Stats` with copy like "Buscar jugador"
|
|
- `Stats` page may link back to `Ranking` with copy like "Ver ranking global"
|
|
|
|
## Follow-up Task Suggestions
|
|
|
|
- Implement `GET /api/ranking` by adapting existing weekly/monthly leaderboard reads and annual snapshot reads.
|
|
- Build `frontend/ranking.html` and `frontend/assets/js/ranking.js` against this contract.
|
|
- Add a small regression script for Ranking endpoint and frontend state validation.
|