diff --git a/backend/app/rcon_admin_log_storage.py b/backend/app/rcon_admin_log_storage.py
index b32dca2..c479839 100644
--- a/backend/app/rcon_admin_log_storage.py
+++ b/backend/app/rcon_admin_log_storage.py
@@ -18,6 +18,14 @@ from .rcon_historical_storage import initialize_rcon_historical_storage
from .sqlite_utils import connect_sqlite_writer
CURRENT_MATCH_FALLBACK_FRESHNESS = timedelta(minutes=15)
+CURRENT_MATCH_PLAYER_EVENT_TYPES = (
+ "kill",
+ "team_switch",
+ "connected",
+ "disconnected",
+ "chat",
+ "message",
+)
def initialize_rcon_admin_log_storage(*, db_path: Path | None = None) -> Path:
@@ -462,74 +470,220 @@ def list_current_match_player_stats(
db_path: Path | None = None,
now: datetime | None = None,
) -> dict[str, object]:
- """Return partial current player stats derived from safe AdminLog kill rows."""
- feed = list_current_match_kill_feed(
- server_key=server_key,
- limit=100,
- db_path=db_path,
- now=now,
- )
- players: dict[str, dict[str, object]] = {}
- weapon_counts: dict[str, Counter[str]] = {}
- for item in feed["items"]:
- if not isinstance(item, Mapping):
- continue
- killer = _ensure_current_match_player(
- players,
- item.get("killer_name"),
- team=item.get("killer_team"),
- event_timestamp=item.get("event_timestamp"),
- )
- victim = _ensure_current_match_player(
- players,
- item.get("victim_name"),
- team=item.get("victim_team"),
- event_timestamp=item.get("event_timestamp"),
- )
- if killer is not None:
- weapon = _safe_event_field(item.get("weapon")) or "UNKNOWN"
- weapon_counts.setdefault(str(killer["player_name"]), Counter())[weapon] += 1
- if item.get("is_teamkill"):
- killer["teamkills"] = int(killer["teamkills"]) + 1
- else:
- killer["kills"] = int(killer["kills"]) + 1
- if victim is not None:
- victim["deaths"] = int(victim["deaths"]) + 1
- if item.get("is_teamkill"):
- victim["deaths_by_teamkill"] = int(victim["deaths_by_teamkill"]) + 1
+ """Return current-match participants and partial stats from the safe AdminLog window."""
+ resolved_path = initialize_rcon_admin_log_storage(db_path=db_path)
+ if use_postgres_rcon_storage(explicit_sqlite_path=db_path):
+ from .postgres_rcon_storage import connect_postgres_compat
- items = []
- for player in players.values():
- items.append(
- {
- **player,
- "favorite_weapon": _favorite_weapon_for_player(
- weapon_counts.get(str(player["player_name"]))
- ),
- "source": "rcon-admin-log-kill-events",
- "confidence": "event-derived-partial",
- }
+ connection_scope = connect_postgres_compat()
+ else:
+ connection_scope = closing(sqlite3.connect(resolved_path))
+
+ with connection_scope as connection:
+ if isinstance(connection, sqlite3.Connection):
+ connection.row_factory = sqlite3.Row
+ window = _resolve_current_match_window(
+ connection,
+ server_key=server_key,
+ now=now,
)
- items.sort(
- key=lambda player: (
- -int(player["kills"]),
- int(player["deaths"]),
- str(player["player_name"]).casefold(),
+ rows = _list_current_match_participant_rows(
+ connection,
+ server_key=server_key,
+ window=window,
)
- )
+
+ players: dict[str, dict[str, object]] = {}
+ for row in rows:
+ payload = _json_mapping(row["parsed_payload_json"])
+ event_timestamp = row["event_timestamp"]
+ event_type = str(row["event_type"] or "")
+ if event_type == "kill":
+ killer = _ensure_current_match_player(
+ players,
+ player_name=payload.get("killer_name"),
+ player_id=payload.get("killer_id"),
+ team=payload.get("killer_team"),
+ event_timestamp=event_timestamp,
+ is_connected=None,
+ source=event_type,
+ )
+ victim = _ensure_current_match_player(
+ players,
+ player_name=payload.get("victim_name"),
+ player_id=payload.get("victim_id"),
+ team=payload.get("victim_team"),
+ event_timestamp=event_timestamp,
+ is_connected=None,
+ source=event_type,
+ )
+ if killer is not None:
+ weapon = _safe_event_field(payload.get("weapon")) or "UNKNOWN"
+ _player_weapon_counts(killer)[weapon] += 1
+ if payload.get("killer_team") and payload.get("killer_team") == payload.get("victim_team"):
+ killer["teamkills"] = int(killer["teamkills"]) + 1
+ else:
+ killer["kills"] = int(killer["kills"]) + 1
+ if victim is not None:
+ victim["deaths"] = int(victim["deaths"]) + 1
+ if payload.get("killer_team") and payload.get("killer_team") == payload.get("victim_team"):
+ victim["deaths_by_teamkill"] = int(victim["deaths_by_teamkill"]) + 1
+ continue
+
+ if event_type == "team_switch":
+ _ensure_current_match_player(
+ players,
+ player_name=payload.get("player_name"),
+ player_id=payload.get("player_id"),
+ team=payload.get("to_team"),
+ event_timestamp=event_timestamp,
+ is_connected=None,
+ source=event_type,
+ )
+ continue
+
+ if event_type == "connected":
+ _ensure_current_match_player(
+ players,
+ player_name=payload.get("player_name"),
+ player_id=payload.get("player_id"),
+ team=None,
+ event_timestamp=event_timestamp,
+ is_connected=True,
+ source=event_type,
+ )
+ continue
+
+ if event_type == "disconnected":
+ _ensure_current_match_player(
+ players,
+ player_name=payload.get("player_name"),
+ player_id=payload.get("player_id"),
+ team=None,
+ event_timestamp=event_timestamp,
+ is_connected=False,
+ source=event_type,
+ )
+ continue
+
+ if event_type in {"chat", "message"}:
+ _ensure_current_match_player(
+ players,
+ player_name=payload.get("player_name"),
+ player_id=payload.get("player_id"),
+ team=payload.get("chat_team"),
+ event_timestamp=event_timestamp,
+ is_connected=None,
+ source=event_type,
+ )
+
+ items = [_serialize_current_match_player(player, window_confidence=window["confidence"]) for player in players.values()]
+ items.sort(key=_current_match_player_sort_key)
return {
- "scope": feed["scope"],
- "confidence": "event-derived-partial" if items else feed["confidence"],
- "source": "rcon-admin-log-kill-events",
+ "scope": window["scope"],
+ "confidence": window["confidence"] if items else window["confidence"],
+ "source": "rcon-admin-log-current-match-summary",
"updated_at": max(
(str(item["last_seen_at"]) for item in items if item.get("last_seen_at")),
default=None,
),
- "stale_events_filtered": feed["stale_events_filtered"],
+ "stale_events_filtered": int(window["stale_events_filtered"]),
"items": items,
}
+def _resolve_current_match_window(
+ connection: sqlite3.Connection | object,
+ *,
+ server_key: str,
+ now: datetime | None,
+) -> dict[str, object]:
+ boundary = connection.execute(
+ """
+ SELECT event_type, server_time
+ FROM rcon_admin_log_events
+ WHERE (target_key = ? OR external_server_id = ?)
+ AND event_type IN ('match_start', 'match_end')
+ AND server_time IS NOT NULL
+ ORDER BY server_time DESC, id DESC
+ LIMIT 1
+ """,
+ (server_key, server_key),
+ ).fetchone()
+ open_start_time = (
+ boundary["server_time"]
+ if boundary is not None and boundary["event_type"] == "match_start"
+ else None
+ )
+ if open_start_time is not None:
+ return {
+ "scope": "open-admin-log-match-window",
+ "confidence": "admin-log-boundary",
+ "open_start_time": int(open_start_time),
+ "stale_events_filtered": 0,
+ "freshness_anchor": None,
+ }
+
+ freshness_anchor = _as_utc_datetime(now) or datetime.now(timezone.utc)
+ return {
+ "scope": "recent-admin-log-window",
+ "confidence": "partial",
+ "open_start_time": None,
+ "stale_events_filtered": 0,
+ "freshness_anchor": freshness_anchor,
+ }
+
+
+def _list_current_match_participant_rows(
+ connection: sqlite3.Connection | object,
+ *,
+ server_key: str,
+ window: Mapping[str, object],
+) -> list[Mapping[str, object]]:
+ event_placeholders = ",".join("?" for _ in CURRENT_MATCH_PLAYER_EVENT_TYPES)
+ params: list[object] = [server_key, server_key, *CURRENT_MATCH_PLAYER_EVENT_TYPES]
+ if window.get("open_start_time") is not None:
+ params.append(window["open_start_time"])
+ rows = connection.execute(
+ f"""
+ SELECT id, target_key, external_server_id, event_timestamp, server_time, event_type,
+ parsed_payload_json
+ FROM rcon_admin_log_events
+ WHERE (target_key = ? OR external_server_id = ?)
+ AND event_type IN ({event_placeholders})
+ AND server_time >= ?
+ ORDER BY server_time ASC, id ASC
+ """,
+ params,
+ ).fetchall()
+ return rows
+
+ rows = connection.execute(
+ f"""
+ SELECT id, target_key, external_server_id, event_timestamp, server_time, event_type,
+ parsed_payload_json
+ FROM rcon_admin_log_events
+ WHERE (target_key = ? OR external_server_id = ?)
+ AND event_type IN ({event_placeholders})
+ ORDER BY server_time DESC, id DESC
+ LIMIT 500
+ """,
+ params,
+ ).fetchall()
+ freshness_anchor = window.get("freshness_anchor")
+ fresh_rows = [
+ row
+ for row in rows
+ if _row_is_current_match_fallback_fresh(row, freshness_anchor)
+ ]
+ window["stale_events_filtered"] = len(rows) - len(fresh_rows)
+ if not fresh_rows:
+ window["scope"] = "no-current-match-events"
+ window["confidence"] = "stale-filtered" if window["stale_events_filtered"] else "none"
+ return []
+ return list(reversed(fresh_rows))
+
+
def get_latest_rcon_player_profile_summaries(
*,
target_key: str,
@@ -664,37 +818,112 @@ def _safe_event_field(value: object) -> str | None:
def _ensure_current_match_player(
players: dict[str, dict[str, object]],
- player_name: object,
*,
+ player_name: object,
+ player_id: object,
team: object,
event_timestamp: object,
+ is_connected: bool | None,
+ source: str,
) -> dict[str, object] | None:
safe_name = _safe_event_field(player_name)
- if safe_name is None:
+ safe_id = _safe_event_field(player_id)
+ key = _current_match_player_key(safe_id, safe_name)
+ if key is None:
return None
player = players.setdefault(
- safe_name,
+ key,
{
+ "player_id": safe_id,
"player_name": safe_name,
"team": None,
"kills": 0,
"deaths": 0,
"teamkills": 0,
"deaths_by_teamkill": 0,
+ "is_connected": None,
"last_seen_at": None,
+ "_weapon_counts": Counter(),
+ "_sources": set(),
},
)
+ if safe_id:
+ player["player_id"] = safe_id
+ if safe_name:
+ current_name = _safe_event_field(player.get("player_name"))
+ if current_name is None or len(safe_name) >= len(current_name):
+ player["player_name"] = safe_name
safe_team = _safe_event_field(team)
if safe_team:
player["team"] = safe_team
+ if is_connected is not None:
+ player["is_connected"] = is_connected
safe_timestamp = _safe_event_field(event_timestamp)
if safe_timestamp and (
player["last_seen_at"] is None or safe_timestamp > str(player["last_seen_at"])
):
player["last_seen_at"] = safe_timestamp
+ player["_sources"].add(source)
return player
+def _current_match_player_key(
+ player_id: str | None,
+ player_name: str | None,
+) -> str | None:
+ if player_id:
+ return f"id:{player_id}"
+ if player_name:
+ return f"name:{player_name.casefold()}"
+ return None
+
+
+def _player_weapon_counts(player: Mapping[str, object]) -> Counter[str]:
+ weapon_counts = player.get("_weapon_counts")
+ if isinstance(weapon_counts, Counter):
+ return weapon_counts
+ return Counter()
+
+
+def _serialize_current_match_player(
+ player: Mapping[str, object],
+ *,
+ window_confidence: object,
+) -> dict[str, object]:
+ sources = sorted(str(value) for value in player.get("_sources", set()))
+ return {
+ "player_name": player.get("player_name"),
+ "player_id": player.get("player_id"),
+ "team": player.get("team"),
+ "kills": int(player.get("kills") or 0),
+ "deaths": int(player.get("deaths") or 0),
+ "teamkills": int(player.get("teamkills") or 0),
+ "deaths_by_teamkill": int(player.get("deaths_by_teamkill") or 0),
+ "favorite_weapon": _favorite_weapon_for_player(_player_weapon_counts(player)),
+ "last_seen_at": player.get("last_seen_at"),
+ "is_connected": player.get("is_connected"),
+ "connected": player.get("is_connected"),
+ "source": ",".join(sources) if sources else "unknown",
+ "confidence": str(window_confidence or "partial"),
+ }
+
+
+def _current_match_player_sort_key(player: Mapping[str, object]) -> tuple[int, int, int, str]:
+ connected = player.get("is_connected")
+ if connected is True:
+ connected_rank = 0
+ elif connected is None:
+ connected_rank = 1
+ else:
+ connected_rank = 2
+ return (
+ -int(player.get("kills") or 0),
+ int(player.get("deaths") or 0),
+ connected_rank,
+ str(player.get("player_name") or "").casefold(),
+ )
+
+
def _favorite_weapon_for_player(weapons: Counter[str] | None) -> str | None:
if not weapons:
return None
diff --git a/backend/tests/test_current_match_payload.py b/backend/tests/test_current_match_payload.py
index 6ad00c3..51b844d 100644
--- a/backend/tests/test_current_match_payload.py
+++ b/backend/tests/test_current_match_payload.py
@@ -245,24 +245,225 @@ def test_current_match_player_stats_aggregate_safe_admin_log_rows(tmp_path):
)
assert stats["scope"] == "open-admin-log-match-window"
- assert stats["confidence"] == "event-derived-partial"
- assert stats["source"] == "rcon-admin-log-kill-events"
+ assert stats["confidence"] == "admin-log-boundary"
+ assert stats["source"] == "rcon-admin-log-current-match-summary"
assert [item["player_name"] for item in stats["items"]] == ["Alpha", "Bravo", "Charlie"]
assert stats["items"][0] == {
"player_name": "Alpha",
+ "player_id": "steam-alpha",
"team": "Allies",
"kills": 1,
"deaths": 1,
"teamkills": 1,
"deaths_by_teamkill": 0,
+ "is_connected": None,
+ "connected": None,
"last_seen_at": "2026-05-21T10:03:00Z",
"favorite_weapon": "M1 GARAND",
- "source": "rcon-admin-log-kill-events",
- "confidence": "event-derived-partial",
+ "source": "kill",
+ "confidence": "admin-log-boundary",
}
assert "raw_message" not in stats["items"][0]
+def test_current_match_player_stats_include_connected_players_without_kills(tmp_path):
+ db_path = tmp_path / "admin-log.sqlite3"
+ persist_rcon_admin_log_entries(
+ target={
+ "target_key": "comunidad-hispana-01",
+ "external_server_id": "comunidad-hispana-01",
+ },
+ 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)] CONNECTED Quiet Player (steam-quiet)",
+ },
+ ],
+ db_path=db_path,
+ )
+
+ stats = list_current_match_player_stats(
+ server_key="comunidad-hispana-01",
+ db_path=db_path,
+ )
+
+ assert stats["scope"] == "open-admin-log-match-window"
+ assert stats["items"] == [
+ {
+ "player_name": "Quiet Player",
+ "player_id": "steam-quiet",
+ "team": None,
+ "kills": 0,
+ "deaths": 0,
+ "teamkills": 0,
+ "deaths_by_teamkill": 0,
+ "favorite_weapon": None,
+ "last_seen_at": "2026-05-21T10:01:00Z",
+ "is_connected": True,
+ "connected": True,
+ "source": "connected",
+ "confidence": "admin-log-boundary",
+ }
+ ]
+
+
+def test_current_match_player_stats_keep_disconnected_participants_visible(tmp_path):
+ db_path = tmp_path / "admin-log.sqlite3"
+ persist_rcon_admin_log_entries(
+ target={
+ "target_key": "comunidad-hispana-01",
+ "external_server_id": "comunidad-hispana-01",
+ },
+ 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)] CONNECTED Brief Player (steam-brief)",
+ },
+ {
+ "timestamp": "2026-05-21T10:05:00Z",
+ "message": "[6:00 min (180)] DISCONNECTED Brief Player (steam-brief)",
+ },
+ ],
+ db_path=db_path,
+ )
+
+ stats = list_current_match_player_stats(
+ server_key="comunidad-hispana-01",
+ db_path=db_path,
+ )
+
+ assert stats["items"] == [
+ {
+ "player_name": "Brief Player",
+ "player_id": "steam-brief",
+ "team": None,
+ "kills": 0,
+ "deaths": 0,
+ "teamkills": 0,
+ "deaths_by_teamkill": 0,
+ "favorite_weapon": None,
+ "last_seen_at": "2026-05-21T10:05:00Z",
+ "is_connected": False,
+ "connected": False,
+ "source": "connected,disconnected",
+ "confidence": "admin-log-boundary",
+ }
+ ]
+
+
+def test_current_match_player_stats_include_victim_only_players(tmp_path):
+ db_path = tmp_path / "admin-log.sqlite3"
+ persist_rcon_admin_log_entries(
+ target={
+ "target_key": "comunidad-hispana-01",
+ "external_server_id": "comunidad-hispana-01",
+ },
+ 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: Killer One(Axis/steam-killer) -> "
+ "Victim Only(Allies/steam-victim) with MP40"
+ ),
+ },
+ ],
+ db_path=db_path,
+ )
+
+ stats = list_current_match_player_stats(
+ server_key="comunidad-hispana-01",
+ db_path=db_path,
+ )
+ by_name = {item["player_name"]: item for item in stats["items"]}
+
+ assert by_name["Victim Only"]["kills"] == 0
+ assert by_name["Victim Only"]["deaths"] == 1
+ assert by_name["Victim Only"]["favorite_weapon"] is None
+
+
+def test_current_match_player_stats_exclude_players_before_open_match_start(tmp_path):
+ db_path = tmp_path / "admin-log.sqlite3"
+ persist_rcon_admin_log_entries(
+ target={
+ "target_key": "comunidad-hispana-01",
+ "external_server_id": "comunidad-hispana-01",
+ },
+ entries=[
+ {
+ "timestamp": "2026-05-21T09:55:00Z",
+ "message": "[0:30 min (90)] CONNECTED Old Match Player (steam-old)",
+ },
+ {
+ "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)] CONNECTED New Match Player (steam-new)",
+ },
+ ],
+ db_path=db_path,
+ )
+
+ stats = list_current_match_player_stats(
+ server_key="comunidad-hispana-01",
+ db_path=db_path,
+ )
+
+ assert [item["player_name"] for item in stats["items"]] == ["New Match Player"]
+
+
+def test_current_match_player_stats_sort_connected_before_disconnected_with_same_stats(tmp_path):
+ db_path = tmp_path / "admin-log.sqlite3"
+ persist_rcon_admin_log_entries(
+ target={
+ "target_key": "comunidad-hispana-01",
+ "external_server_id": "comunidad-hispana-01",
+ },
+ 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)] CONNECTED Connected Alpha (steam-connected)",
+ },
+ {
+ "timestamp": "2026-05-21T10:02:00Z",
+ "message": "[3:00 min (140)] CONNECTED Disconnected Bravo (steam-disconnected)",
+ },
+ {
+ "timestamp": "2026-05-21T10:03:00Z",
+ "message": "[4:00 min (160)] DISCONNECTED Disconnected Bravo (steam-disconnected)",
+ },
+ ],
+ db_path=db_path,
+ )
+
+ stats = list_current_match_player_stats(
+ server_key="comunidad-hispana-01",
+ db_path=db_path,
+ )
+
+ assert [item["player_name"] for item in stats["items"]] == [
+ "Connected Alpha",
+ "Disconnected Bravo",
+ ]
+
+
def test_current_match_player_stats_filter_stale_recent_events(tmp_path):
db_path = tmp_path / "admin-log.sqlite3"
persist_rcon_admin_log_entries(
diff --git a/frontend/assets/css/historico.css b/frontend/assets/css/historico.css
index bb12464..680ea74 100644
--- a/frontend/assets/css/historico.css
+++ b/frontend/assets/css/historico.css
@@ -838,6 +838,15 @@
line-height: 1.2;
}
+.current-match-killfeed__player-identity {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+ max-width: 100%;
+ flex-wrap: wrap;
+}
+
.current-match-killfeed__player-name {
min-width: 0;
overflow: hidden;
@@ -860,6 +869,16 @@
min-width: 0;
}
+.current-match-killfeed__player--killer .current-match-killfeed__player-identity,
+.current-match-killfeed__player--killer .current-match-killfeed__player-meta {
+ justify-content: flex-end;
+}
+
+.current-match-killfeed__player--victim .current-match-killfeed__player-identity,
+.current-match-killfeed__player--victim .current-match-killfeed__player-meta {
+ justify-content: flex-start;
+}
+
.current-match-killfeed__weapon {
display: grid;
grid-template-rows: 25px auto;
@@ -913,7 +932,6 @@
white-space: nowrap;
}
-.current-match-killfeed__team-badge,
.current-match-killfeed__teamkill {
display: inline-flex;
align-items: center;
@@ -925,35 +943,17 @@
font-weight: 800;
}
-.current-match-killfeed__team-badge {
- border: 1px solid rgba(159, 168, 141, 0.2);
- color: var(--text-soft);
- background: rgba(159, 168, 141, 0.08);
-}
-
-.current-match-killfeed__team-badge--allies {
- border-color: rgba(100, 139, 178, 0.34);
- color: rgba(192, 215, 238, 0.96);
- background: rgba(67, 101, 137, 0.28);
-}
-
-.current-match-killfeed__team-badge--axis {
- border-color: rgba(167, 109, 96, 0.36);
- color: rgba(231, 198, 190, 0.96);
- background: rgba(116, 68, 58, 0.3);
-}
-
-.current-match-killfeed__team-badge--unknown {
- border-color: rgba(159, 168, 141, 0.22);
- color: var(--muted);
- background: rgba(159, 168, 141, 0.1);
-}
-
.current-match-killfeed__teamkill {
border: 1px solid rgba(210, 182, 118, 0.36);
color: var(--accent-warm);
}
+.current-match-killfeed__team-badge {
+ min-width: auto;
+ padding-inline: 8px;
+ font-size: 0.62rem;
+}
+
.current-match-player-intro {
display: flex;
align-items: flex-end;
@@ -1261,6 +1261,10 @@
white-space: normal;
}
+ .current-match-killfeed__player-identity {
+ gap: 5px;
+ }
+
.current-match-killfeed__teamkill {
min-height: 16px;
font-size: 0.54rem;
@@ -1349,4 +1353,4 @@
The internal detail page can still show its own scoreboard button. */
#recent-matches-list .historical-match-card__link--scoreboard {
display: none;
-}
\ No newline at end of file
+}
diff --git a/frontend/assets/js/partida-actual.js b/frontend/assets/js/partida-actual.js
index 26afd80..e1c01c2 100644
--- a/frontend/assets/js/partida-actual.js
+++ b/frontend/assets/js/partida-actual.js
@@ -479,11 +479,13 @@ function renderKillFeedRow(event) {
data-event-id="${escapeHtml(event.event_id || "")}"
>
-
- ${escapeHtml(event.killer_name || "Jugador no disponible")}
-
-
+
+
+ ${escapeHtml(event.killer_name || "Jugador no disponible")}
+
${renderKillFeedTeamBadge(killerTeam)}
+
+
${teamkillBadge}
@@ -496,10 +498,12 @@ function renderKillFeedRow(event) {
${escapeHtml(weapon.label)}
-
- ${escapeHtml(event.victim_name || "Objetivo no disponible")}
+
+
+ ${escapeHtml(event.victim_name || "Objetivo no disponible")}
+
+ ${renderKillFeedTeamBadge(victimTeam)}
- ${renderKillFeedTeamBadge(victimTeam)}
`;
@@ -511,8 +515,11 @@ function getKillFeedTeamDisplay(value) {
}
function renderKillFeedTeamBadge(team) {
+ if (!team || team.key === "unknown") {
+ return "";
+ }
return `
-
+
${escapeHtml(team.label)}
`;