# 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.
