Add historical snapshot storage layer
This commit is contained in:
@@ -0,0 +1,68 @@
|
|||||||
|
# TASK-042-historical-snapshots-storage-and-model
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Diseñar e implementar una capa de almacenamiento de snapshots históricos precalculados para el proyecto, preparada para servir rápidamente resumen, rankings y partidas recientes sin recalcular agregados pesados en tiempo real.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
La página histórica ya funciona, pero parte de la información puede requerir agregaciones costosas si se calculan en el momento de cada carga o al cambiar de pestaña. El objetivo es introducir una capa de snapshots precalculados y persistidos que permita responder de forma rápida y estable. Esto debe incluir no solo el resumen del servidor, sino también los tops/rankings semanales.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Revisar la estructura histórica actual y los endpoints/API que hoy calculan resumen, rankings y partidas recientes.
|
||||||
|
2. Diseñar un modelo de snapshot persistido que soporte, como mínimo:
|
||||||
|
- resumen de servidor
|
||||||
|
- top kills semanales
|
||||||
|
- top muertes semanales
|
||||||
|
- top partidas con más de 100 kills por jugador
|
||||||
|
- top puntos de soporte semanales
|
||||||
|
- partidas recientes
|
||||||
|
3. Definir para cada snapshot metadatos claros, por ejemplo:
|
||||||
|
- server_key
|
||||||
|
- snapshot_type
|
||||||
|
- metric
|
||||||
|
- window
|
||||||
|
- payload_json
|
||||||
|
- generated_at
|
||||||
|
- source_range_start
|
||||||
|
- source_range_end
|
||||||
|
- is_stale
|
||||||
|
4. Implementar el almacenamiento local de snapshots de forma coherente con la persistencia histórica actual.
|
||||||
|
5. Documentar la estructura y propósito de esta nueva capa.
|
||||||
|
6. No migrar todavía toda la UI a snapshots en esta task.
|
||||||
|
7. Al completar la implementación:
|
||||||
|
- dejar el repositorio consistente
|
||||||
|
- hacer commit
|
||||||
|
- hacer push al remoto si el entorno lo permite
|
||||||
|
|
||||||
|
## Files to Read First
|
||||||
|
- AGENTS.md
|
||||||
|
- backend/README.md
|
||||||
|
- backend/app/historical_models.py
|
||||||
|
- backend/app/historical_storage.py
|
||||||
|
- backend/app/payloads.py
|
||||||
|
- backend/app/routes.py
|
||||||
|
- docs/historical-domain-model.md
|
||||||
|
|
||||||
|
## Expected Files to Modify
|
||||||
|
- backend/README.md
|
||||||
|
- backend/app/historical_models.py
|
||||||
|
- backend/app/historical_storage.py
|
||||||
|
- opcionalmente nuevos módulos, por ejemplo:
|
||||||
|
- backend/app/historical_snapshots.py
|
||||||
|
- backend/app/historical_snapshot_storage.py
|
||||||
|
- opcionalmente documentación técnica adicional
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
- No basar esta capa en A2S.
|
||||||
|
- No crear todavía nueva UI en esta task.
|
||||||
|
- No hacer cambios destructivos.
|
||||||
|
- Mantener el trabajo centrado en almacenamiento y modelo de snapshots.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
- Existe un modelo de snapshot persistido claro.
|
||||||
|
- El modelo cubre resumen, rankings y partidas recientes.
|
||||||
|
- La estructura está lista para ser rellenada periódicamente.
|
||||||
|
- Los cambios quedan committeados y se hace push al remoto si el entorno lo permite.
|
||||||
|
|
||||||
|
## Change Budget
|
||||||
|
- Preferir menos de 6 archivos modificados o creados.
|
||||||
|
- Preferir menos de 220 líneas cambiadas.
|
||||||
@@ -164,6 +164,8 @@ colector. Si todavia no hay snapshots guardados, responden `status: "ok"` con
|
|||||||
- `config.py` centraliza host, puerto y allowlist minima de origenes locales.
|
- `config.py` centraliza host, puerto y allowlist minima de origenes locales.
|
||||||
- `historical_ingestion.py` consulta la capa JSON publica de CRCON para bootstrap y refresh incremental.
|
- `historical_ingestion.py` consulta la capa JSON publica de CRCON para bootstrap y refresh incremental.
|
||||||
- `historical_models.py` fija las entidades historicas minimas del dominio.
|
- `historical_models.py` fija las entidades historicas minimas del dominio.
|
||||||
|
- `historical_snapshots.py` fija los tipos y selectores validos de snapshots historicos precalculados.
|
||||||
|
- `historical_snapshot_storage.py` persiste snapshots historicos precalculados listos para lectura rapida.
|
||||||
- `historical_runner.py` ejecuta refresh incremental periodico con reintentos basicos.
|
- `historical_runner.py` ejecuta refresh incremental periodico con reintentos basicos.
|
||||||
- `historical_storage.py` prepara la persistencia `historical_*` y las consultas agregadas iniciales.
|
- `historical_storage.py` prepara la persistencia `historical_*` y las consultas agregadas iniciales.
|
||||||
- `main.py` contiene el entrypoint HTTP y la creacion del servidor.
|
- `main.py` contiene el entrypoint HTTP y la creacion del servidor.
|
||||||
@@ -192,6 +194,7 @@ solo libreria estandar de Python. Esta base minima sigue el modelo logico de:
|
|||||||
- `historical_players`
|
- `historical_players`
|
||||||
- `historical_player_match_stats`
|
- `historical_player_match_stats`
|
||||||
- `historical_ingestion_runs`
|
- `historical_ingestion_runs`
|
||||||
|
- `historical_precomputed_snapshots`
|
||||||
|
|
||||||
Por defecto el archivo se crea en:
|
Por defecto el archivo se crea en:
|
||||||
|
|
||||||
@@ -209,6 +212,32 @@ La base logica sigue documentada en
|
|||||||
`docs/historical-domain-model.md` para el historico CRCON. Esta implementacion
|
`docs/historical-domain-model.md` para el historico CRCON. Esta implementacion
|
||||||
no introduce ORM, migraciones ni una decision de almacenamiento productivo.
|
no introduce ORM, migraciones ni una decision de almacenamiento productivo.
|
||||||
|
|
||||||
|
## Snapshots historicos precalculados
|
||||||
|
|
||||||
|
La capa historica incluye ahora una tabla adicional `historical_precomputed_snapshots`
|
||||||
|
en el mismo SQLite local para persistir payloads ya agregados y servirlos sin
|
||||||
|
recalculo pesado en cada request. Esta capa esta preparada para guardar:
|
||||||
|
|
||||||
|
- `server-summary`
|
||||||
|
- `weekly-leaderboard` con metricas `kills`, `deaths`, `support` y `matches_over_100_kills`
|
||||||
|
- `recent-matches`
|
||||||
|
|
||||||
|
Cada snapshot conserva metadatos operativos minimos:
|
||||||
|
|
||||||
|
- `server_key`
|
||||||
|
- `snapshot_type`
|
||||||
|
- `metric`
|
||||||
|
- `window`
|
||||||
|
- `payload_json`
|
||||||
|
- `generated_at`
|
||||||
|
- `source_range_start`
|
||||||
|
- `source_range_end`
|
||||||
|
- `is_stale`
|
||||||
|
|
||||||
|
La persistencia usa `upsert` por combinacion de servidor, tipo, metrica y
|
||||||
|
ventana para que la siguiente task pueda refrescar estos snapshots de forma
|
||||||
|
periodica sin duplicar filas.
|
||||||
|
|
||||||
## Bootstrap del colector
|
## Bootstrap del colector
|
||||||
|
|
||||||
El backend incluye un bootstrap minimo para el futuro flujo de snapshots:
|
El backend incluye un bootstrap minimo para el futuro flujo de snapshots:
|
||||||
|
|||||||
@@ -109,3 +109,18 @@ class HistoricalBackfillProgressSummary:
|
|||||||
last_run_started_at: datetime | None
|
last_run_started_at: datetime | None
|
||||||
last_run_completed_at: datetime | None
|
last_run_completed_at: datetime | None
|
||||||
last_error: str | None
|
last_error: str | None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class HistoricalSnapshotRecord:
|
||||||
|
"""Persisted precomputed historical snapshot ready for lightweight reads."""
|
||||||
|
|
||||||
|
server_key: str
|
||||||
|
snapshot_type: str
|
||||||
|
metric: str | None
|
||||||
|
window: str | None
|
||||||
|
payload_json: str
|
||||||
|
generated_at: datetime
|
||||||
|
source_range_start: datetime | None
|
||||||
|
source_range_end: datetime | None
|
||||||
|
is_stale: bool
|
||||||
|
|||||||
251
backend/app/historical_snapshot_storage.py
Normal file
251
backend/app/historical_snapshot_storage.py
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
"""SQLite persistence for precomputed historical snapshots."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .config import get_storage_path
|
||||||
|
from .historical_models import HistoricalSnapshotRecord
|
||||||
|
from .historical_snapshots import validate_snapshot_identity
|
||||||
|
from .historical_storage import initialize_historical_storage
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_historical_snapshot_storage(*, db_path: Path | None = None) -> Path:
|
||||||
|
"""Create the snapshot table used by precomputed historical payloads."""
|
||||||
|
resolved_path = initialize_historical_storage(db_path=db_path or get_storage_path())
|
||||||
|
|
||||||
|
with _connect(resolved_path) as connection:
|
||||||
|
connection.executescript(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS historical_precomputed_snapshots (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
server_key TEXT NOT NULL,
|
||||||
|
snapshot_type TEXT NOT NULL,
|
||||||
|
metric TEXT NOT NULL DEFAULT '',
|
||||||
|
window TEXT NOT NULL DEFAULT '',
|
||||||
|
payload_json TEXT NOT NULL,
|
||||||
|
generated_at TEXT NOT NULL,
|
||||||
|
source_range_start TEXT,
|
||||||
|
source_range_end TEXT,
|
||||||
|
is_stale INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(server_key, snapshot_type, metric, window)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_historical_precomputed_snapshots_lookup
|
||||||
|
ON historical_precomputed_snapshots(
|
||||||
|
server_key,
|
||||||
|
snapshot_type,
|
||||||
|
metric,
|
||||||
|
window,
|
||||||
|
generated_at DESC
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
return resolved_path
|
||||||
|
|
||||||
|
|
||||||
|
def persist_historical_snapshot(
|
||||||
|
*,
|
||||||
|
server_key: str,
|
||||||
|
snapshot_type: str,
|
||||||
|
payload: dict[str, object] | list[object],
|
||||||
|
metric: str | None = None,
|
||||||
|
window: str | None = None,
|
||||||
|
generated_at: datetime | None = None,
|
||||||
|
source_range_start: datetime | None = None,
|
||||||
|
source_range_end: datetime | None = None,
|
||||||
|
is_stale: bool = False,
|
||||||
|
db_path: Path | None = None,
|
||||||
|
) -> HistoricalSnapshotRecord:
|
||||||
|
"""Insert or replace one persisted historical snapshot."""
|
||||||
|
if not server_key.strip():
|
||||||
|
raise ValueError("server_key is required for historical snapshots.")
|
||||||
|
|
||||||
|
validate_snapshot_identity(snapshot_type=snapshot_type, metric=metric)
|
||||||
|
resolved_path = initialize_historical_snapshot_storage(db_path=db_path)
|
||||||
|
generated_at_value = generated_at or datetime.now(timezone.utc)
|
||||||
|
payload_json = json.dumps(payload, ensure_ascii=True, separators=(",", ":"))
|
||||||
|
normalized_metric = metric or ""
|
||||||
|
normalized_window = window or ""
|
||||||
|
|
||||||
|
with _connect(resolved_path) as connection:
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO historical_precomputed_snapshots (
|
||||||
|
server_key,
|
||||||
|
snapshot_type,
|
||||||
|
metric,
|
||||||
|
window,
|
||||||
|
payload_json,
|
||||||
|
generated_at,
|
||||||
|
source_range_start,
|
||||||
|
source_range_end,
|
||||||
|
is_stale
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(server_key, snapshot_type, metric, window)
|
||||||
|
DO UPDATE SET
|
||||||
|
payload_json = excluded.payload_json,
|
||||||
|
generated_at = excluded.generated_at,
|
||||||
|
source_range_start = excluded.source_range_start,
|
||||||
|
source_range_end = excluded.source_range_end,
|
||||||
|
is_stale = excluded.is_stale,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
server_key.strip(),
|
||||||
|
snapshot_type,
|
||||||
|
normalized_metric,
|
||||||
|
normalized_window,
|
||||||
|
payload_json,
|
||||||
|
_to_iso(generated_at_value),
|
||||||
|
_to_iso(source_range_start),
|
||||||
|
_to_iso(source_range_end),
|
||||||
|
1 if is_stale else 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return HistoricalSnapshotRecord(
|
||||||
|
server_key=server_key.strip(),
|
||||||
|
snapshot_type=snapshot_type,
|
||||||
|
metric=metric,
|
||||||
|
window=window,
|
||||||
|
payload_json=payload_json,
|
||||||
|
generated_at=_as_utc(generated_at_value),
|
||||||
|
source_range_start=_as_utc(source_range_start),
|
||||||
|
source_range_end=_as_utc(source_range_end),
|
||||||
|
is_stale=is_stale,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_historical_snapshot(
|
||||||
|
*,
|
||||||
|
server_key: str,
|
||||||
|
snapshot_type: str,
|
||||||
|
metric: str | None = None,
|
||||||
|
window: str | None = None,
|
||||||
|
db_path: Path | None = None,
|
||||||
|
) -> dict[str, object] | None:
|
||||||
|
"""Return one persisted snapshot and decoded payload, if present."""
|
||||||
|
validate_snapshot_identity(snapshot_type=snapshot_type, metric=metric)
|
||||||
|
resolved_path = initialize_historical_snapshot_storage(db_path=db_path)
|
||||||
|
|
||||||
|
with _connect(resolved_path) as connection:
|
||||||
|
row = connection.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
server_key,
|
||||||
|
snapshot_type,
|
||||||
|
metric,
|
||||||
|
window,
|
||||||
|
payload_json,
|
||||||
|
generated_at,
|
||||||
|
source_range_start,
|
||||||
|
source_range_end,
|
||||||
|
is_stale
|
||||||
|
FROM historical_precomputed_snapshots
|
||||||
|
WHERE server_key = ?
|
||||||
|
AND snapshot_type = ?
|
||||||
|
AND metric = ?
|
||||||
|
AND window = ?
|
||||||
|
""",
|
||||||
|
(server_key, snapshot_type, metric or "", window or ""),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
payload = json.loads(row["payload_json"])
|
||||||
|
return {
|
||||||
|
"server_key": row["server_key"],
|
||||||
|
"snapshot_type": row["snapshot_type"],
|
||||||
|
"metric": row["metric"] or None,
|
||||||
|
"window": row["window"] or None,
|
||||||
|
"generated_at": row["generated_at"],
|
||||||
|
"source_range_start": row["source_range_start"],
|
||||||
|
"source_range_end": row["source_range_end"],
|
||||||
|
"is_stale": bool(row["is_stale"]),
|
||||||
|
"payload": payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def list_historical_snapshots(
|
||||||
|
*,
|
||||||
|
server_key: str | None = None,
|
||||||
|
snapshot_type: str | None = None,
|
||||||
|
db_path: Path | None = None,
|
||||||
|
) -> list[dict[str, object]]:
|
||||||
|
"""List persisted snapshots for validation and operational inspection."""
|
||||||
|
resolved_path = initialize_historical_snapshot_storage(db_path=db_path)
|
||||||
|
where_parts: list[str] = []
|
||||||
|
params: list[object] = []
|
||||||
|
|
||||||
|
if server_key:
|
||||||
|
where_parts.append("server_key = ?")
|
||||||
|
params.append(server_key)
|
||||||
|
if snapshot_type:
|
||||||
|
validate_snapshot_identity(snapshot_type=snapshot_type)
|
||||||
|
where_parts.append("snapshot_type = ?")
|
||||||
|
params.append(snapshot_type)
|
||||||
|
|
||||||
|
where_sql = ""
|
||||||
|
if where_parts:
|
||||||
|
where_sql = "WHERE " + " AND ".join(where_parts)
|
||||||
|
|
||||||
|
with _connect(resolved_path) as connection:
|
||||||
|
rows = connection.execute(
|
||||||
|
f"""
|
||||||
|
SELECT
|
||||||
|
server_key,
|
||||||
|
snapshot_type,
|
||||||
|
metric,
|
||||||
|
window,
|
||||||
|
generated_at,
|
||||||
|
source_range_start,
|
||||||
|
source_range_end,
|
||||||
|
is_stale
|
||||||
|
FROM historical_precomputed_snapshots
|
||||||
|
{where_sql}
|
||||||
|
ORDER BY server_key ASC, snapshot_type ASC, generated_at DESC
|
||||||
|
""",
|
||||||
|
params,
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"server_key": row["server_key"],
|
||||||
|
"snapshot_type": row["snapshot_type"],
|
||||||
|
"metric": row["metric"] or None,
|
||||||
|
"window": row["window"] or None,
|
||||||
|
"generated_at": row["generated_at"],
|
||||||
|
"source_range_start": row["source_range_start"],
|
||||||
|
"source_range_end": row["source_range_end"],
|
||||||
|
"is_stale": bool(row["is_stale"]),
|
||||||
|
}
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _connect(db_path: Path) -> sqlite3.Connection:
|
||||||
|
connection = sqlite3.connect(db_path)
|
||||||
|
connection.row_factory = sqlite3.Row
|
||||||
|
return connection
|
||||||
|
|
||||||
|
|
||||||
|
def _to_iso(value: datetime | None) -> str | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return _as_utc(value).isoformat().replace("+00:00", "Z")
|
||||||
|
|
||||||
|
|
||||||
|
def _as_utc(value: datetime | None) -> datetime | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if value.tzinfo is None:
|
||||||
|
return value.replace(tzinfo=timezone.utc)
|
||||||
|
return value.astimezone(timezone.utc)
|
||||||
46
backend/app/historical_snapshots.py
Normal file
46
backend/app/historical_snapshots.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"""Definitions for persisted precomputed historical snapshots."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
SNAPSHOT_TYPE_SERVER_SUMMARY = "server-summary"
|
||||||
|
SNAPSHOT_TYPE_WEEKLY_LEADERBOARD = "weekly-leaderboard"
|
||||||
|
SNAPSHOT_TYPE_RECENT_MATCHES = "recent-matches"
|
||||||
|
|
||||||
|
SUPPORTED_SNAPSHOT_TYPES = frozenset(
|
||||||
|
{
|
||||||
|
SNAPSHOT_TYPE_SERVER_SUMMARY,
|
||||||
|
SNAPSHOT_TYPE_WEEKLY_LEADERBOARD,
|
||||||
|
SNAPSHOT_TYPE_RECENT_MATCHES,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SUPPORTED_LEADERBOARD_METRICS = frozenset(
|
||||||
|
{
|
||||||
|
"kills",
|
||||||
|
"deaths",
|
||||||
|
"support",
|
||||||
|
"matches_over_100_kills",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
DEFAULT_SNAPSHOT_WINDOW = "all-time"
|
||||||
|
DEFAULT_WEEKLY_SNAPSHOT_WINDOW = "7d"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_snapshot_identity(
|
||||||
|
*,
|
||||||
|
snapshot_type: str,
|
||||||
|
metric: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Validate the persisted snapshot selectors accepted by the storage layer."""
|
||||||
|
if snapshot_type not in SUPPORTED_SNAPSHOT_TYPES:
|
||||||
|
raise ValueError(f"Unsupported historical snapshot type: {snapshot_type}")
|
||||||
|
|
||||||
|
if snapshot_type == SNAPSHOT_TYPE_WEEKLY_LEADERBOARD:
|
||||||
|
if metric not in SUPPORTED_LEADERBOARD_METRICS:
|
||||||
|
raise ValueError(f"Unsupported historical snapshot metric: {metric}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if metric is not None:
|
||||||
|
raise ValueError(f"Metric is only supported for {SNAPSHOT_TYPE_WEEKLY_LEADERBOARD}.")
|
||||||
@@ -16,6 +16,7 @@ Esta capa cubre solo historico persistido en backend:
|
|||||||
- identidad reutilizable de jugadores
|
- identidad reutilizable de jugadores
|
||||||
- estadisticas de jugador por partida
|
- estadisticas de jugador por partida
|
||||||
- trazabilidad de ejecuciones de ingesta
|
- trazabilidad de ejecuciones de ingesta
|
||||||
|
- snapshots precalculados para lectura rapida
|
||||||
|
|
||||||
No sustituye ni modifica el flujo actual de snapshots live via A2S.
|
No sustituye ni modifica el flujo actual de snapshots live via A2S.
|
||||||
|
|
||||||
@@ -76,6 +77,19 @@ uniforme.
|
|||||||
- matches vistos
|
- matches vistos
|
||||||
- inserts y updates
|
- inserts y updates
|
||||||
|
|
||||||
|
### Precomputed Snapshot
|
||||||
|
|
||||||
|
- tabla: `historical_precomputed_snapshots`
|
||||||
|
- clave estable:
|
||||||
|
- `server_key`
|
||||||
|
- `snapshot_type`
|
||||||
|
- `metric`
|
||||||
|
- `window`
|
||||||
|
- razon:
|
||||||
|
- permite exponer resumen, rankings y partidas recientes sin recalcular
|
||||||
|
agregados pesados en cada request
|
||||||
|
- mantiene metadatos operativos sobre frescura y rango fuente
|
||||||
|
|
||||||
## Data Model
|
## Data Model
|
||||||
|
|
||||||
### `historical_servers`
|
### `historical_servers`
|
||||||
@@ -121,6 +135,20 @@ Metricas por jugador y partida con al menos:
|
|||||||
|
|
||||||
Trazabilidad operativa para bootstrap y refresh incremental.
|
Trazabilidad operativa para bootstrap y refresh incremental.
|
||||||
|
|
||||||
|
### `historical_precomputed_snapshots`
|
||||||
|
|
||||||
|
Payloads JSON precalculados listos para lectura rapida desde API/UI con:
|
||||||
|
|
||||||
|
- `server_key`
|
||||||
|
- `snapshot_type`
|
||||||
|
- `metric`
|
||||||
|
- `window`
|
||||||
|
- `payload_json`
|
||||||
|
- `generated_at`
|
||||||
|
- `source_range_start`
|
||||||
|
- `source_range_end`
|
||||||
|
- `is_stale`
|
||||||
|
|
||||||
## Idempotency Strategy
|
## Idempotency Strategy
|
||||||
|
|
||||||
- servidores sembrados de forma declarativa y actualizables por `slug`
|
- servidores sembrados de forma declarativa y actualizables por `slug`
|
||||||
@@ -130,12 +158,15 @@ Trazabilidad operativa para bootstrap y refresh incremental.
|
|||||||
`(historical_match_id, historical_player_id)`
|
`(historical_match_id, historical_player_id)`
|
||||||
- el refresco incremental usa una ventana de solape temporal para volver a leer
|
- el refresco incremental usa una ventana de solape temporal para volver a leer
|
||||||
partidas recientes y absorber cambios tardios sin rehacer todo el historico
|
partidas recientes y absorber cambios tardios sin rehacer todo el historico
|
||||||
|
- los snapshots precalculados usan `UPSERT` por identidad logica para refrescar
|
||||||
|
el payload sin crear duplicados
|
||||||
|
|
||||||
## Query Readiness
|
## Query Readiness
|
||||||
|
|
||||||
La estructura soporta ya consultas futuras como:
|
La estructura soporta ya consultas futuras como:
|
||||||
|
|
||||||
- top kills de la ultima semana por servidor
|
- top kills de la ultima semana por servidor
|
||||||
|
- top muertes, soporte y partidas de 100+ kills desde una capa cacheada
|
||||||
- partidas recientes por servidor
|
- partidas recientes por servidor
|
||||||
- mapas jugados y frecuencia
|
- mapas jugados y frecuencia
|
||||||
- agregados por jugador sobre ventanas temporales
|
- agregados por jugador sobre ventanas temporales
|
||||||
@@ -144,6 +175,8 @@ La estructura soporta ya consultas futuras como:
|
|||||||
|
|
||||||
- live state actual: `server_snapshots` via A2S
|
- live state actual: `server_snapshots` via A2S
|
||||||
- historico persistido: `historical_*` via CRCON scoreboard JSON
|
- historico persistido: `historical_*` via CRCON scoreboard JSON
|
||||||
|
- snapshots precalculados: `historical_precomputed_snapshots` sobre el mismo
|
||||||
|
historico persistido
|
||||||
|
|
||||||
Ambas lineas comparten el mismo SQLite local de desarrollo para reducir
|
Ambas lineas comparten el mismo SQLite local de desarrollo para reducir
|
||||||
complejidad operativa, pero mantienen tablas y contratos separados.
|
complejidad operativa, pero mantienen tablas y contratos separados.
|
||||||
|
|||||||
Reference in New Issue
Block a user