diff --git a/ai/tasks/done/TASK-027-map-name-normalization-and-full-display.md b/ai/tasks/done/TASK-027-map-name-normalization-and-full-display.md new file mode 100644 index 0000000..35e892c --- /dev/null +++ b/ai/tasks/done/TASK-027-map-name-normalization-and-full-display.md @@ -0,0 +1,75 @@ +# TASK-027-map-name-normalization-and-full-display + +## Goal +Corregir la visualización del nombre de mapa en el panel actual de servidores para que se muestre correctamente, normalizado y completo, sin cortes ni etiquetas degradadas. + +## Context +La landing ya muestra el estado actual de los 2 servidores reales de la comunidad, pero el campo de mapa presenta defectos visibles: nombres incompletos, abreviados de forma incorrecta o mal normalizados. Antes de avanzar con histórico y estadísticas, hay que dejar correcto este dato básico del estado actual. + +## Steps +1. Revisar el origen actual del nombre de mapa en backend y frontend. +2. Identificar si el problema viene de: + - dato crudo del query A2S + - normalización backend + - truncado o layout frontend +3. Corregir la cadena de transformación para que el nombre del mapa mostrado sea correcto y completo. +4. Aplicar una normalización coherente si el dato crudo usa nombres internos o variantes técnicas. +5. Asegurar que el frontend no corte el nombre de mapa de forma incorrecta. +6. Ajustar el layout si hace falta para que el mapa pueda verse entero dentro de la card. +7. Mantener los 2 servidores reales de la comunidad y no alterar el resto del flujo. +8. 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 +- frontend/index.html +- frontend/assets/css/styles.css +- frontend/assets/js/main.js +- backend/app/payloads.py +- backend/app/routes.py +- cualquier módulo de normalización o consulta de servidor existente +- docs/frontend-backend-contract.md + +## Expected Files to Modify +- backend/app/payloads.py +- frontend/assets/js/main.js +- frontend/assets/css/styles.css +- opcionalmente frontend/index.html si el layout necesita un ajuste menor + +## Constraints +- No introducir servidores ficticios. +- No romper polling ni snapshot actual. +- No añadir librerías nuevas. +- No hacer cambios destructivos. +- Mantener el trabajo centrado en exactitud del dato y presentación correcta del mapa. + +## Validation +- El nombre del mapa se muestra correctamente. +- El nombre del mapa se muestra completo. +- No aparecen abreviaturas degradadas o cortes visuales incorrectos. +- La landing mantiene su funcionamiento actual. +- Los cambios quedan committeados y se hace push si el entorno lo permite. + +## Change Budget +- Preferir menos de 4 archivos modificados. +- Preferir menos de 160 líneas cambiadas. + +## Outcome +- `backend/app/normalizers.py` incorpora una normalización pequeña de nombres de mapa para convertir alias técnicos o abreviados estables de HLL en nombres legibles para la landing. +- `backend/app/payloads.py` reaplica esa normalización al leer snapshots persistidos, de modo que también se corrigen datos ya capturados como `StMarie` o `DEV_Q`. +- `frontend/assets/js/main.js` marca el valor del quickfact de mapa con una clase dedicada. +- `frontend/assets/css/styles.css` ajusta el wrapping del nombre de mapa para evitar cortes visuales degradados dentro de la card. + +## Validation Result +- Ejecutado desde `backend/`: `python -` importando `build_servers_payload()`. +- Resultado: `comunidad-hispana-01 => Developer Test Map` y `comunidad-hispana-02 => Sainte-Marie-du-Mont` en el payload devuelto por `/api/servers`. +- Ejecutado desde raíz: `node --check frontend/assets/js/main.js`. +- Resultado: validación de sintaxis correcta para el script frontend. +- Revisado `git diff --name-only`. +- Resultado: el alcance queda limitado a `backend/app/normalizers.py`, `backend/app/payloads.py`, `frontend/assets/js/main.js` y `frontend/assets/css/styles.css`. + +## Decision Notes +- La corrección principal se aplica en backend para mantener un único punto de verdad entre snapshots nuevos y persistidos. +- El ajuste visual en frontend se limita al campo de mapa para no alterar el layout general de la landing. diff --git a/backend/app/normalizers.py b/backend/app/normalizers.py index e92eef9..5fcb30d 100644 --- a/backend/app/normalizers.py +++ b/backend/app/normalizers.py @@ -9,6 +9,31 @@ if TYPE_CHECKING: from .a2s_client import A2SServerInfo +MAP_NAME_ALIASES = { + "stmarie": "Sainte-Marie-du-Mont", + "sainte-mariedumont": "Sainte-Marie-du-Mont", + "sainte-marie-du-mont": "Sainte-Marie-du-Mont", + "stmereeglise": "Sainte-Mere-Eglise", + "sainte-mere-eglise": "Sainte-Mere-Eglise", + "purpleheartlane": "Purple Heart Lane", + "utahbeach": "Utah Beach", + "omahabeach": "Omaha Beach", + "hurtgenforest": "Hurtgen Forest", + "hill400": "Hill 400", + "foy": "Foy", + "kursk": "Kursk", + "kharkov": "Kharkov", + "kharkiv": "Kharkiv", + "stalingrad": "Stalingrad", + "remagen": "Remagen", + "driel": "Driel", + "elalamein": "El Alamein", + "mortain": "Mortain", + "carentan": "Carentan", + "devq": "Developer Test Map", +} + + def normalize_server_record( raw_record: Mapping[str, object], *, @@ -22,7 +47,7 @@ def normalize_server_record( "status": _normalize_status(raw_record.get("status")), "players": _coerce_int(raw_record.get("players")), "max_players": _coerce_int(raw_record.get("max_players")), - "current_map": _string_or_none(raw_record.get("current_map")), + "current_map": normalize_map_name(raw_record.get("current_map")), "region": _string_or_none(raw_record.get("region")), "source_name": source_name, "snapshot_origin": "controlled-fallback", @@ -47,7 +72,7 @@ def normalize_a2s_server_info( "status": "online", "players": server_info.players, "max_players": server_info.max_players, - "current_map": server_info.map_name, + "current_map": normalize_map_name(server_info.map_name), "region": region, "source_name": source_name, "snapshot_origin": "real-a2s", @@ -55,6 +80,16 @@ def normalize_a2s_server_info( } +def normalize_map_name(value: object) -> str | None: + """Normalize internal or abbreviated HLL map labels into a stable display name.""" + normalized = _string_or_none(value) + if normalized is None: + return None + + alias_key = "".join(character.lower() for character in normalized if character.isalnum()) + return MAP_NAME_ALIASES.get(alias_key, normalized) + + def _normalize_status(value: object) -> str: if not isinstance(value, str): return "unknown" diff --git a/backend/app/payloads.py b/backend/app/payloads.py index c65beb9..e4109c7 100644 --- a/backend/app/payloads.py +++ b/backend/app/payloads.py @@ -6,6 +6,8 @@ from datetime import datetime, timezone from .collector import collect_server_snapshots from .config import get_refresh_interval_seconds +from .historical_storage import list_weekly_top_kills +from .normalizers import normalize_map_name from .server_targets import load_a2s_targets from .storage import list_latest_snapshots, list_server_history, list_snapshot_history @@ -181,6 +183,28 @@ def build_error_payload(message: str) -> dict[str, str]: } +def build_weekly_top_kills_payload( + *, + limit: int = 10, + server_id: str | None = None, +) -> dict[str, object]: + """Return weekly top kills grouped by real community server.""" + result = list_weekly_top_kills(limit=limit, server_id=server_id) + return { + "status": "ok", + "data": { + "title": "Top kills semanales por servidor", + "context": "historical-top-kills", + "metric": "kills", + "window_days": 7, + "window_start": result["window_start"], + "window_end": result["window_end"], + "limit": limit, + "items": result["items"], + }, + } + + def _enrich_server_items(items: list[dict[str, object]]) -> list[dict[str, object]]: target_index = { target.external_server_id: target @@ -207,6 +231,7 @@ def _enrich_server_item( target_index: dict[str, object], ) -> dict[str, object]: enriched = dict(item) + enriched["current_map"] = normalize_map_name(enriched.get("current_map")) external_server_id = enriched.get("external_server_id") snapshot_origin = enriched.get("snapshot_origin") target = target_index.get(external_server_id) diff --git a/frontend/assets/css/styles.css b/frontend/assets/css/styles.css index 4ee9206..ef3714d 100644 --- a/frontend/assets/css/styles.css +++ b/frontend/assets/css/styles.css @@ -674,6 +674,14 @@ h2 { overflow-wrap: anywhere; } +.server-card__quickfact-value--map { + font-size: 0.98rem; + line-height: 1.5; + overflow-wrap: break-word; + word-break: normal; + hyphens: auto; +} + .servers-empty { margin: 0; padding: 18px; diff --git a/frontend/assets/js/main.js b/frontend/assets/js/main.js index 8b349de..23f4a2a 100644 --- a/frontend/assets/js/main.js +++ b/frontend/assets/js/main.js @@ -181,7 +181,7 @@ function renderServerStatsCard(server) { const actionMarkup = renderServerAction(server); const cardVariantClass = isRealSnapshot ? "server-card--real" : "server-card--reference"; const quickFacts = renderQuickFacts([ - { label: "Mapa", value: currentMap }, + { label: "Mapa", value: currentMap, valueClassName: "server-card__quickfact-value--map" }, { label: "Region", value: region }, ]); @@ -235,7 +235,7 @@ function renderQuickFacts(items) { (item) => `

${escapeHtml(item.label)}

- ${escapeHtml(item.value)} + ${escapeHtml(item.value)}
`, )