From d558ac81ed1b8ac8e8eb9b3886cbefc2f37106b1 Mon Sep 17 00:00:00 2001 From: devRaGonSa Date: Thu, 21 May 2026 19:47:12 +0200 Subject: [PATCH] feat: refine current match live feed --- ...altime-killfeed-and-home-buttons-layout.md | 20 +++- ...SK-157-current-match-player-team-colors.md | 32 +++++- ...SK-158-current-match-realtime-transport.md | 28 ++++- ...nt-match-feed-rollback-and-weapon-icons.md | 21 +++- ...ASK-160-home-server-card-bottom-actions.md | 16 ++- backend/app/payloads.py | 7 +- backend/app/rcon_admin_log_storage.py | 19 +++- backend/app/routes.py | 4 +- backend/tests/test_rcon_admin_log_storage.py | 37 +++++++ frontend/assets/css/historico.css | 98 +++++++++-------- frontend/assets/css/styles.css | 23 +++- frontend/assets/js/main.js | 16 +-- frontend/assets/js/partida-actual.js | 100 ++++++++++++++---- frontend/index.html | 61 +---------- 14 files changed, 330 insertions(+), 152 deletions(-) rename ai/tasks/{pending => done}/TASK-156-current-match-realtime-killfeed-and-home-buttons-layout.md (87%) rename ai/tasks/{pending => done}/TASK-157-current-match-player-team-colors.md (77%) rename ai/tasks/{pending => done}/TASK-158-current-match-realtime-transport.md (68%) rename ai/tasks/{pending => done}/TASK-159-current-match-feed-rollback-and-weapon-icons.md (81%) rename ai/tasks/{pending => done}/TASK-160-home-server-card-bottom-actions.md (82%) diff --git a/ai/tasks/pending/TASK-156-current-match-realtime-killfeed-and-home-buttons-layout.md b/ai/tasks/done/TASK-156-current-match-realtime-killfeed-and-home-buttons-layout.md similarity index 87% rename from ai/tasks/pending/TASK-156-current-match-realtime-killfeed-and-home-buttons-layout.md rename to ai/tasks/done/TASK-156-current-match-realtime-killfeed-and-home-buttons-layout.md index 4923d38..7007878 100644 --- a/ai/tasks/pending/TASK-156-current-match-realtime-killfeed-and-home-buttons-layout.md +++ b/ai/tasks/done/TASK-156-current-match-realtime-killfeed-and-home-buttons-layout.md @@ -1,6 +1,6 @@ # TASK-156 - Current match realtime killfeed and home buttons layout -Status: pending +Status: done ## Goal @@ -178,7 +178,23 @@ The current-match kill feed behaves like a compact real-time combat overlay, and ## Outcome -Document the validation performed, notable decisions, and any follow-up task that should be created instead of expanding this task. +- Kept the existing `event_id`-based killfeed refresh path and changed only its + rendering surface: the capped 15-event panel now reads as a bounded combat + screen with three desktop columns, while smaller breakpoints remain bounded. +- Removed the home server-card public-scoreboard action from the hydrated markup + and left the remaining `Historico` / `Partida actual` actions aligned by the + existing status-column layout. +- Validated with: + - `node --check frontend/assets/js/partida-actual.js` + - `node --check frontend/assets/js/main.js` + - `git diff --check` + - `docker compose up -d --build frontend` + - live endpoint checks for `/api/current-match/kills` on Comunidad Hispana + `#01` and `#02` + - rendered Chrome headless screenshots of the home page, current-match `#01` + desktop view and current-match `#02` mobile empty-state view +- The in-app Browser automation runtime was not callable after tool discovery, + so rendered validation used local headless Chrome against the task URLs. ## Change Budget diff --git a/ai/tasks/pending/TASK-157-current-match-player-team-colors.md b/ai/tasks/done/TASK-157-current-match-player-team-colors.md similarity index 77% rename from ai/tasks/pending/TASK-157-current-match-player-team-colors.md rename to ai/tasks/done/TASK-157-current-match-player-team-colors.md index c81cbd1..6bf3b5b 100644 --- a/ai/tasks/pending/TASK-157-current-match-player-team-colors.md +++ b/ai/tasks/done/TASK-157-current-match-player-team-colors.md @@ -1,6 +1,6 @@ # TASK-157 - Current match player team colors -Status: pending +Status: done ## Goal @@ -160,7 +160,35 @@ The current-match player/stat section uses team-aware colors consistent with the ## Outcome -Document the validation performed, notable decisions, and any follow-up task that should be created instead of expanding this task. +- RCA found no backend contract gap: a live + `/api/current-match/players?server=comunidad-hispana-01` response observed + before the validation rebuild already exposed AdminLog-derived `team` values + such as `Allies` and `Axis`; unknown values remain handled by the existing + frontend neutral path. +- Reused the historical detail player row and team badge CSS modifiers from + `historico-scoreboard-detail.css` in the current-match player table instead + of adding a parallel style system. +- Included `team` in the visible player-table signature so a future team update + can rerender an already-visible row without changing its stats. +- Validated with: + - `node --check frontend/assets/js/partida-actual.js` + - `git diff --check` + - `docker compose up -d --build frontend` + - live `/api/current-match/players` checks on Comunidad Hispana `#01` and + `#02` + - rendered Chrome headless current-match validation for the available player + empty state after the backend restart + - served-script check confirming the historical row and badge modifiers are + present in `frontend/assets/js/partida-actual.js` +- The final rendered validation window had no current player rows after the + backend rebuild, so known-team colors were validated from the observed live + payload contract plus the reused historical class mapping rather than from a + live colored-row screenshot. +- Repository-level follow-up validation ran + `powershell -ExecutionPolicy Bypass -File scripts/run-integration-tests.ps1`. + It returned exit code `0`, but its output also printed a historical SQLite + `database disk image is malformed` traceback after the pass banner; that + historical storage issue is outside this frontend task scope. ## Change Budget diff --git a/ai/tasks/pending/TASK-158-current-match-realtime-transport.md b/ai/tasks/done/TASK-158-current-match-realtime-transport.md similarity index 68% rename from ai/tasks/pending/TASK-158-current-match-realtime-transport.md rename to ai/tasks/done/TASK-158-current-match-realtime-transport.md index 3631866..62f9336 100644 --- a/ai/tasks/pending/TASK-158-current-match-realtime-transport.md +++ b/ai/tasks/done/TASK-158-current-match-realtime-transport.md @@ -1,7 +1,7 @@ --- id: TASK-158 title: Current match realtime transport -status: pending +status: done type: backend team: Backend Senior supporting_teams: [Frontend Senior] @@ -160,7 +160,31 @@ The current-match kill feed updates in near-real-time, without depending on the ## Outcome -Document the RCA, transport decision, validation performed, load/freshness limitations, and any follow-up task that should be created instead of expanding this task. +RCA: + +- The current page used one `30s` interval in `frontend/assets/js/partida-actual.js` for current-match metadata, player stats and the kill feed together. +- The kill feed read path is already safe for higher-frequency reads because it queries normalized persisted `rcon_admin_log_events` rows through `/api/current-match/kills`; it does not query RCON from the browser or expose raw AdminLog lines. +- Current match metadata can still call live RCON through the existing payload path, and player stats aggregate the kill feed read model, so those surfaces remain on the slower refresh path. + +Transport decision: + +- Implemented incremental short polling for the kill feed instead of SSE. The backend is still the standard-library JSON route stack, while the existing persisted read model can cheaply support a cursor. +- `/api/current-match/kills` now accepts `since_event_id` plus the existing `server` and `limit` inputs. Trusted server validation remains in `routes.py`. +- The frontend polls kill-feed rows every `1500ms`, sends the latest event id cursor after the first load, deduplicates by `event_id`, caps the in-memory visible buffer and avoids overlapping kill-feed requests. Metadata and player stats keep the existing `30s` refresh cadence. + +Validation performed: + +- `python -m compileall backend/app` +- `node --check frontend/assets/js/partida-actual.js` +- `git diff --check` +- Ran a direct Python smoke check for `list_current_match_kill_feed(..., since_event_id=...)` with temporary AdminLog rows after the focused pytest command could not run. +- Attempted focused backend tests with `python -m pytest backend/tests/test_current_match_payload.py backend/tests/test_rcon_admin_log_storage.py`; this environment failed with `No module named pytest`. +- Reviewed `git diff --name-only`; task-owned code changes are limited to the allowed current-match backend/frontend files plus the focused storage test and this task transition. The worktree already contained frontend and prior task changes from `TASK-156`/`TASK-157`. + +Load and freshness limit: + +- Browser polling now reads persisted AdminLog rows rather than issuing per-browser RCON calls. +- New kill visibility is still bounded by how quickly `app.rcon_historical_worker` or manual AdminLog ingestion persists fresh AdminLog rows. The current documented worker example is a `120s` loop, so operations may need a follow-up cadence decision if that persisted source is not fresh enough for the desired live screen. ## Change Budget diff --git a/ai/tasks/pending/TASK-159-current-match-feed-rollback-and-weapon-icons.md b/ai/tasks/done/TASK-159-current-match-feed-rollback-and-weapon-icons.md similarity index 81% rename from ai/tasks/pending/TASK-159-current-match-feed-rollback-and-weapon-icons.md rename to ai/tasks/done/TASK-159-current-match-feed-rollback-and-weapon-icons.md index 9e0ba46..5b006a6 100644 --- a/ai/tasks/pending/TASK-159-current-match-feed-rollback-and-weapon-icons.md +++ b/ai/tasks/done/TASK-159-current-match-feed-rollback-and-weapon-icons.md @@ -1,7 +1,7 @@ --- id: TASK-159 title: Current match feed rollback and weapon icons -status: pending +status: done type: frontend team: Frontend Senior supporting_teams: [Disenador grafico, Experto en interfaz] @@ -162,7 +162,24 @@ The current-match feed returns to the previous preferred live-feed visual direct ## Outcome -Document the visual RCA, icon discovery/mapping decision, validation performed, and any follow-up task that should be created instead of expanding this task. +Visual RCA: + +- Git history before the compact feed change showed a vertical `historical-match-card` list with one readable event per row/card. The rejected version introduced a fixed multi-column killfeed surface that reads like a grid/table. +- The feed was refactored back to a vertical card-like list while keeping the current live buffer, deduplication and no-flicker signature guard. + +Weapon icon decision: + +- Reused local assets already present under `frontend/assets/img/weapons/`. +- `partida-actual.js` maps normalized AdminLog weapon names for `M1 GARAND`, `MP40`, `M1A1 THOMPSON`, `GEWEHR 43`, `MG42` and aliases to local PNGs. +- Unknown/unmapped weapons keep their label for tooltip/accessibility and render a compact local fallback marker instead of hotlinking assets. + +Validation performed: + +- `node --check frontend/assets/js/partida-actual.js` +- `git diff --check` +- `docker compose build frontend` +- Reviewed the history diff for the prior accepted feed direction and inspected local weapon asset filenames. +- Rendered Browser verification was attempted through the required Browser workflow but blocked because the Browser JavaScript execution tool was not exposed in this session after tool discovery. ## Change Budget diff --git a/ai/tasks/pending/TASK-160-home-server-card-bottom-actions.md b/ai/tasks/done/TASK-160-home-server-card-bottom-actions.md similarity index 82% rename from ai/tasks/pending/TASK-160-home-server-card-bottom-actions.md rename to ai/tasks/done/TASK-160-home-server-card-bottom-actions.md index aaf58f5..881d61f 100644 --- a/ai/tasks/pending/TASK-160-home-server-card-bottom-actions.md +++ b/ai/tasks/done/TASK-160-home-server-card-bottom-actions.md @@ -1,7 +1,7 @@ --- id: TASK-160 title: Home server card bottom actions -status: pending +status: done type: frontend team: Frontend Senior supporting_teams: [Experto en interfaz] @@ -143,7 +143,19 @@ The home server cards have a clean structural layout with only Historico and Par ## Outcome -Document the structural rendering cleanup, validation performed, and any follow-up task that should be created instead of expanding this task. +Structural cleanup: + +- Home server card actions now live in a dedicated bottom row beside the map quickfact rather than inside the status column. +- The rendered card action markup keeps only `Historico` and `Partida actual`; trusted URL mappings stay intact for those actions. +- Removed the inline server-card layout workaround from `frontend/index.html` and moved the bottom-row layout rules into `frontend/assets/css/styles.css`, with a stacked narrow layout. + +Validation performed: + +- `node --check frontend/assets/js/main.js` +- `git diff --check` +- `docker compose build frontend` +- Checked the existing local frontend and backend endpoints were serving on `127.0.0.1:8080` and `127.0.0.1:8000`. +- Rendered Browser verification was attempted through the required Browser workflow but blocked because the Browser JavaScript execution tool was not exposed in this session after tool discovery. ## Change Budget diff --git a/backend/app/payloads.py b/backend/app/payloads.py index adb7079..f926d1b 100644 --- a/backend/app/payloads.py +++ b/backend/app/payloads.py @@ -354,12 +354,17 @@ def build_current_match_kill_feed_payload( *, server_slug: str, limit: int = 30, + since_event_id: str | None = None, ) -> dict[str, object]: """Return normalized AdminLog kill rows for one trusted current-match page.""" origin = get_trusted_public_scoreboard_origin(server_slug) if origin is None: raise ValueError("Unsupported current match server.") - feed = list_current_match_kill_feed(server_key=origin.slug, limit=limit) + feed = list_current_match_kill_feed( + server_key=origin.slug, + limit=limit, + since_event_id=since_event_id, + ) return { "status": "ok", "data": { diff --git a/backend/app/rcon_admin_log_storage.py b/backend/app/rcon_admin_log_storage.py index c5c2e6a..a2fa049 100644 --- a/backend/app/rcon_admin_log_storage.py +++ b/backend/app/rcon_admin_log_storage.py @@ -337,11 +337,13 @@ def list_current_match_kill_feed( *, server_key: str, limit: int = 30, + since_event_id: str | None = None, db_path: Path | None = None, now: datetime | None = None, ) -> dict[str, object]: """Return safe recent kill rows for one AdminLog server window.""" resolved_path = initialize_rcon_admin_log_storage(db_path=db_path) + since_row_id = _parse_current_match_event_row_id(since_event_id) if use_postgres_rcon_storage(explicit_sqlite_path=db_path): from .postgres_rcon_storage import connect_postgres_compat @@ -377,10 +379,11 @@ def list_current_match_kill_feed( FROM rcon_admin_log_events WHERE (target_key = ? OR external_server_id = ?) AND event_type = 'kill' + AND (? IS NULL OR id > ?) ORDER BY server_time DESC, id DESC LIMIT ? """, - (server_key, server_key, limit), + (server_key, server_key, since_row_id, since_row_id, limit), ).fetchall() scope = "recent-admin-log-window" confidence = "partial" @@ -393,10 +396,11 @@ def list_current_match_kill_feed( WHERE (target_key = ? OR external_server_id = ?) AND event_type = 'kill' AND server_time >= ? + AND (? IS NULL OR id > ?) ORDER BY server_time DESC, id DESC LIMIT ? """, - (server_key, server_key, open_start_time, limit), + (server_key, server_key, open_start_time, since_row_id, since_row_id, limit), ).fetchall() scope = "open-admin-log-match-window" confidence = "admin-log-boundary" @@ -613,6 +617,17 @@ def _serialize_kill_feed_row(row: Mapping[str, object]) -> dict[str, object]: } +def _parse_current_match_event_row_id(value: object) -> int | None: + prefix, separator, row_id = str(value or "").rpartition(":") + if separator != ":" or not prefix.startswith("rcon-admin-log:"): + return None + try: + parsed = int(row_id) + except ValueError: + return None + return parsed if parsed > 0 else None + + def _safe_event_field(value: object) -> str | None: normalized = str(value or "").strip() return normalized or None diff --git a/backend/app/routes.py b/backend/app/routes.py index 6e6297d..87b9a21 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -76,7 +76,8 @@ def resolve_get_payload(path: str) -> tuple[HTTPStatus | None, dict[str, object] limit = _parse_limit(parsed.query) if limit is None: return HTTPStatus.BAD_REQUEST, build_error_payload("Invalid limit parameter") - server_slug = parse_qs(parsed.query).get("server", [None])[0] + params = parse_qs(parsed.query) + server_slug = params.get("server", [None])[0] if not server_slug: return HTTPStatus.BAD_REQUEST, build_error_payload("Server parameter is required") if get_trusted_public_scoreboard_origin(server_slug) is None: @@ -84,6 +85,7 @@ def resolve_get_payload(path: str) -> tuple[HTTPStatus | None, dict[str, object] return HTTPStatus.OK, build_current_match_kill_feed_payload( server_slug=server_slug, limit=limit, + since_event_id=params.get("since_event_id", [None])[0], ) if parsed.path == "/api/current-match/players": diff --git a/backend/tests/test_rcon_admin_log_storage.py b/backend/tests/test_rcon_admin_log_storage.py index 4b44fc9..406958f 100644 --- a/backend/tests/test_rcon_admin_log_storage.py +++ b/backend/tests/test_rcon_admin_log_storage.py @@ -368,3 +368,40 @@ def test_current_match_kill_feed_marks_fresh_recent_fallback_rows_partial(tmp_pa assert feed["stale_events_filtered"] == 0 assert [item["killer_name"] for item in feed["items"]] == ["Fresh Killer"] gc.collect() + + +def test_current_match_kill_feed_filters_rows_before_incremental_cursor(tmp_path): + db_path = tmp_path / "admin_log.sqlite3" + persist_rcon_admin_log_entries( + target=TARGET, + entries=[ + { + "timestamp": "2026-05-21T10:00:00Z", + "message": "[1:00 min (100)] MATCH START Mortain Warfare", + }, + { + "timestamp": "2026-05-21T10:01:00Z", + "message": ( + "[2:00 min (120)] KILL: First Killer(Allies/steam-first) -> " + "First Victim(Axis/steam-first-victim) with M1 GARAND" + ), + }, + { + "timestamp": "2026-05-21T10:02:00Z", + "message": ( + "[3:00 min (140)] KILL: Next Killer(Axis/steam-next) -> " + "Next Victim(Allies/steam-next-victim) with MP40" + ), + }, + ], + db_path=db_path, + ) + + feed = list_current_match_kill_feed( + server_key="test-rcon-target", + db_path=db_path, + since_event_id="rcon-admin-log:test-rcon-target:2", + ) + + assert [item["killer_name"] for item in feed["items"]] == ["Next Killer"] + gc.collect() diff --git a/frontend/assets/css/historico.css b/frontend/assets/css/historico.css index 8fe1a73..7a217b0 100644 --- a/frontend/assets/css/historico.css +++ b/frontend/assets/css/historico.css @@ -591,50 +591,41 @@ .current-match-killfeed { display: grid; - grid-auto-flow: column; - grid-template-rows: repeat(5, minmax(0, 1fr)); - grid-auto-columns: minmax(0, 1fr); - gap: 10px 12px; - min-height: 250px; - max-height: 320px; - padding: 16px; + gap: 10px; + max-height: 760px; + padding: 4px 0 0; overflow: hidden; - border: 1px solid rgba(210, 182, 118, 0.26); - border-radius: 8px; - background: - linear-gradient(90deg, rgba(210, 182, 118, 0.08) 1px, transparent 1px), - linear-gradient(180deg, rgba(183, 201, 125, 0.06) 1px, transparent 1px), - linear-gradient(180deg, rgba(12, 18, 11, 0.96), rgba(7, 10, 7, 0.98)); - background-size: 33.333% 100%, 100% 62px, auto; - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.04), - 0 18px 38px rgba(0, 0, 0, 0.22); } .current-match-killfeed__row { display: grid; - grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto; + grid-template-columns: minmax(0, 1fr) minmax(102px, 150px) minmax(0, 1fr) auto; align-items: center; - gap: 8px; - min-height: 38px; - padding: 7px 9px; + gap: 12px; + min-height: 56px; + padding: 12px 14px; overflow: hidden; - border: 1px solid rgba(159, 168, 141, 0.16); + border: 1px solid rgba(159, 168, 141, 0.18); + border-left: 3px solid rgba(183, 201, 125, 0.34); border-radius: 6px; color: var(--text); - background: rgba(24, 31, 21, 0.86); + background: + linear-gradient(135deg, rgba(183, 201, 125, 0.08), transparent 44%), + rgba(14, 19, 12, 0.84); animation: current-match-killfeed-enter 180ms ease-out; } .current-match-killfeed__row.is-teamkill { - border-color: rgba(210, 182, 118, 0.42); - background: rgba(73, 49, 27, 0.72); + border-left-color: rgba(210, 182, 118, 0.64); + background: + linear-gradient(135deg, rgba(210, 182, 118, 0.12), transparent 48%), + rgba(73, 49, 27, 0.42); } .current-match-killfeed__player { min-width: 0; overflow: hidden; - font-size: 0.84rem; + font-size: 0.94rem; line-height: 1.2; text-overflow: ellipsis; white-space: nowrap; @@ -649,20 +640,41 @@ } .current-match-killfeed__weapon { - display: inline-flex; + display: grid; + grid-template-rows: 34px; align-items: center; justify-content: center; - gap: 5px; - min-width: 34px; - min-height: 24px; - padding: 0 6px; + min-width: 102px; + min-height: 40px; + padding: 2px 8px; border: 1px solid rgba(183, 201, 125, 0.24); border-radius: 4px; color: var(--text); background: rgba(7, 10, 7, 0.8); - font-size: 0.68rem; +} + +.current-match-killfeed__weapon-icon { + display: block; + width: min(100%, 116px); + height: 32px; + object-fit: contain; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.72)); +} + +.current-match-killfeed__weapon-fallback { + display: grid; + width: 32px; + height: 32px; + place-items: center; + border: 1px solid rgba(210, 182, 118, 0.3); + border-radius: 4px; + color: var(--accent-warm); + font-size: 1rem; font-weight: 800; - line-height: 1; +} + +.current-match-killfeed__weapon-fallback[hidden] { + display: none; } .current-match-killfeed__weapon em { @@ -920,14 +932,11 @@ } .current-match-killfeed { - grid-template-rows: repeat(8, minmax(0, 1fr)); - min-height: 312px; - max-height: 380px; - background-size: 50% 100%, 100% 48px, auto; + max-height: none; } .current-match-killfeed__row { - grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + grid-template-columns: minmax(0, 1fr) minmax(96px, 128px) minmax(0, 1fr); } .current-match-killfeed__teamkill { @@ -937,12 +946,13 @@ } @media (max-width: 480px) { - .current-match-killfeed { - grid-auto-flow: row; - grid-template-rows: none; - min-height: 0; - max-height: none; - background-size: 100% 48px, 100% 48px, auto; + .current-match-killfeed__row { + grid-template-columns: 1fr; + gap: 8px; + } + + .current-match-killfeed__weapon { + justify-self: start; } } diff --git a/frontend/assets/css/styles.css b/frontend/assets/css/styles.css index 134ba11..a3699f2 100644 --- a/frontend/assets/css/styles.css +++ b/frontend/assets/css/styles.css @@ -617,7 +617,7 @@ h2 { } .server-card__top--stats { - align-items: stretch; + align-items: flex-start; gap: 18px; } @@ -669,9 +669,10 @@ h2 { .server-card__actions { margin: 0; - width: 100%; display: flex; - flex-direction: column; + align-items: flex-end; + align-self: end; + flex-wrap: wrap; gap: 8px; justify-content: flex-end; } @@ -680,7 +681,7 @@ h2 { display: inline-flex; align-items: center; justify-content: center; - width: 100%; + width: auto; max-width: 100%; min-height: 42px; min-width: 136px; @@ -742,6 +743,14 @@ h2 { gap: 12px; } +.server-card__bottom { + display: grid; + grid-template-columns: minmax(0, 280px) minmax(272px, 1fr); + align-items: end; + justify-content: space-between; + gap: 16px; +} + .server-card__quickfact { min-width: 0; padding: 12px 14px; @@ -1019,13 +1028,17 @@ h2 { } .server-card__actions { - justify-content: start; + justify-content: flex-start; } .server-card__quickfacts { grid-template-columns: 1fr; } + .server-card__bottom { + grid-template-columns: 1fr; + } + .clan-card__brand { grid-template-columns: 1fr; justify-items: start; diff --git a/frontend/assets/js/main.js b/frontend/assets/js/main.js index 9237d14..16fb624 100644 --- a/frontend/assets/js/main.js +++ b/frontend/assets/js/main.js @@ -297,10 +297,12 @@ function renderServerStatsCard(server) {
${escapeHtml(statusLabel)}

${escapeHtml(`${players} / ${maxPlayers}`)}

- ${actionMarkup}
- ${quickFacts} +
+ ${quickFacts} + ${actionMarkup} +
`; } @@ -342,16 +344,8 @@ function renderServerAction(server) { return `
- - Scoreboard publico - - Nuestro historico + Historico Partida actual diff --git a/frontend/assets/js/partida-actual.js b/frontend/assets/js/partida-actual.js index e00530f..cd942f4 100644 --- a/frontend/assets/js/partida-actual.js +++ b/frontend/assets/js/partida-actual.js @@ -1,4 +1,5 @@ const CURRENT_MATCH_POLL_INTERVAL_MS = 30 * 1000; +const CURRENT_MATCH_KILL_FEED_POLL_INTERVAL_MS = 1500; const CURRENT_MATCH_SERVERS = Object.freeze({ "comunidad-hispana-01": "Comunidad Hispana #01", "comunidad-hispana-02": "Comunidad Hispana #02", @@ -9,10 +10,14 @@ const CURRENT_MATCH_SCOREBOARDS = Object.freeze({ }); const CURRENT_MATCH_KILL_FEED_LIMIT = 15; const CURRENT_MATCH_WEAPONS = Object.freeze({ - "m1 garand": { label: "M1 Garand", glyph: "M1" }, - mp40: { label: "MP40", glyph: "MP" }, - "m1a1 thompson": { label: "M1A1 Thompson", glyph: "TH" }, - unknown: { label: "Arma desconocida", glyph: "?" }, + "m1 garand": { label: "M1 Garand", icon: "m1_garand.PNG" }, + mp40: { label: "MP40", icon: "mp40.PNG" }, + "m1a1 thompson": { label: "M1A1 Thompson", icon: "thompson.PNG" }, + thompson: { label: "M1A1 Thompson", icon: "thompson.PNG" }, + "gewehr 43": { label: "Gewehr 43", icon: "gewehr.PNG" }, + g43: { label: "Gewehr 43", icon: "gewehr.PNG" }, + mg42: { label: "MG42", icon: "mg42.PNG" }, + unknown: { label: "Arma desconocida", icon: "" }, }); document.addEventListener("DOMContentLoaded", () => { @@ -43,27 +48,43 @@ document.addEventListener("DOMContentLoaded", () => { nodes.history.href = `./historico.html?server=${encodeURIComponent(serverSlug)}`; const killFeedState = initializeKillFeed(nodes); const playerStatsState = initializePlayerStats(nodes); - let refreshInFlight = false; - const refresh = async () => { - if (refreshInFlight) { + let matchRefreshInFlight = false; + const refreshMatchPanels = async () => { + if (matchRefreshInFlight) { return; } - refreshInFlight = true; + matchRefreshInFlight = true; try { await Promise.allSettled([ loadCurrentMatch({ backendBaseUrl, serverSlug, nodes }), - loadKillFeed({ backendBaseUrl, serverSlug, nodes, killFeedState }), loadPlayerStats({ backendBaseUrl, serverSlug, nodes, playerStatsState }), ]); } finally { - refreshInFlight = false; + matchRefreshInFlight = false; } }; - void refresh(); + let killFeedRefreshInFlight = false; + const refreshKillFeed = async () => { + if (killFeedRefreshInFlight) { + return; + } + killFeedRefreshInFlight = true; + try { + await loadKillFeed({ backendBaseUrl, serverSlug, nodes, killFeedState }); + } finally { + killFeedRefreshInFlight = false; + } + }; + + void refreshMatchPanels(); + void refreshKillFeed(); window.setInterval(() => { - void refresh(); + void refreshMatchPanels(); }, CURRENT_MATCH_POLL_INTERVAL_MS); + window.setInterval(() => { + void refreshKillFeed(); + }, CURRENT_MATCH_KILL_FEED_POLL_INTERVAL_MS); }); async function loadCurrentMatch({ backendBaseUrl, serverSlug, nodes }) { @@ -80,8 +101,11 @@ async function loadCurrentMatch({ backendBaseUrl, serverSlug, nodes }) { async function loadKillFeed({ backendBaseUrl, serverSlug, nodes, killFeedState }) { try { + const cursor = killFeedState.latestEventId + ? `&since_event_id=${encodeURIComponent(killFeedState.latestEventId)}` + : ""; const payload = await fetchJson( - `${backendBaseUrl}/api/current-match/kills?server=${encodeURIComponent(serverSlug)}&limit=${CURRENT_MATCH_KILL_FEED_LIMIT}`, + `${backendBaseUrl}/api/current-match/kills?server=${encodeURIComponent(serverSlug)}&limit=${CURRENT_MATCH_KILL_FEED_LIMIT}${cursor}`, ); renderKillFeed(payload?.data || {}, nodes, killFeedState); } catch (error) { @@ -108,7 +132,7 @@ function renderCurrentMatch(data, nodes) { nodes.title.textContent = mapName; nodes.summary.textContent = serverName; nodes.note.textContent = data.found - ? "Lectura en vivo recibida. La pagina se actualiza cada 30 segundos." + ? "Lectura en vivo recibida. El feed de bajas se actualiza en tiempo casi real." : "Todavia no hay snapshot live disponible para este servidor."; nodes.scoreboard.href = scoreboardUrl || "./index.html"; nodes.scoreboard.hidden = !scoreboardUrl; @@ -150,6 +174,7 @@ function initializeKillFeed(nodes) { nodes.feedList = document.getElementById("current-match-feed-list"); return { byId: new Map(), + latestEventId: "", visibleSignature: "", }; } @@ -178,6 +203,7 @@ function renderKillFeed(data, nodes, state) { const incoming = Array.isArray(data.items) ? data.items : []; if (data.scope === "no-current-match-events") { state.byId.clear(); + state.latestEventId = ""; } incoming.forEach((event) => { if (event?.event_id) { @@ -187,6 +213,8 @@ function renderKillFeed(data, nodes, state) { const events = [...state.byId.values()] .sort(compareKillFeedEvents) .slice(0, CURRENT_MATCH_KILL_FEED_LIMIT); + state.byId = new Map(events.map((event) => [event.event_id, event])); + state.latestEventId = events[0]?.event_id || state.latestEventId; if (events.length === 0) { nodes.feedList.innerHTML = ""; state.visibleSignature = ""; @@ -227,7 +255,7 @@ function renderKillFeedRow(event) { title="${escapeHtml(weapon.label)}" aria-label="${escapeHtml(weapon.label)}" > - + ${renderKillFeedWeaponIcon(weapon)} ${escapeHtml(weapon.label)} @@ -242,10 +270,29 @@ function resolveKillFeedWeapon(value) { const key = normalizeLookupText(value); return CURRENT_MATCH_WEAPONS[key] || { label: String(value || CURRENT_MATCH_WEAPONS.unknown.label), - glyph: CURRENT_MATCH_WEAPONS.unknown.glyph, + icon: CURRENT_MATCH_WEAPONS.unknown.icon, }; } +function renderKillFeedWeaponIcon(weapon) { + if (!weapon.icon) { + return ''; + } + return ` + + + `; +} + function renderPlayerStats(data, nodes, state) { const items = Array.isArray(data.items) ? sortPlayerStats(data.items) : []; if (items.length === 0) { @@ -262,6 +309,7 @@ function renderPlayerStats(data, nodes, state) { .map((item) => [ item.player_name, + item.team, item.kills, item.deaths, item.teamkills, @@ -312,10 +360,15 @@ function renderPlayerStatsTable(items) { } function renderPlayerStatsRow(item) { + const team = getPlayerTeamDisplay(item.team); return ` - + ${escapeHtml(item.player_name || "Jugador no disponible")} - ${escapeHtml(item.team || "No disponible")} + + + ${escapeHtml(team.label)} + + ${escapeHtml(formatStatNumber(item.kills))} ${escapeHtml(formatStatNumber(item.deaths))} ${escapeHtml(formatStatNumber(item.teamkills))} @@ -325,6 +378,17 @@ function renderPlayerStatsRow(item) { `; } +function getPlayerTeamDisplay(value) { + const normalized = String(value || "").trim().toLowerCase(); + if (normalized === "allies" || normalized === "allied" || normalized === "aliados") { + return { key: "allies", label: "Aliados" }; + } + if (normalized === "axis" || normalized === "eje") { + return { key: "axis", label: "Eje" }; + } + return { key: "unknown", label: "No disponible" }; +} + function toStatNumber(value) { return Number.isFinite(Number(value)) ? Number(value) : 0; } diff --git a/frontend/index.html b/frontend/index.html index 356ebdb..2d35b52 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -9,65 +9,6 @@ /> Comunidad Hispana - HLL Vietnam - - \ No newline at end of file +