*** visual.py	2025-01-22 00:00:00.000000000 +0000
--- visual.py	2025-10-07 00:00:00.000000000 +0000
***************
*** 1,10 ****
  import os
  import json
  import socket
  import platform
  import numpy as np
  import pandas as pd
  import streamlit as st
  import sys
  from streamlit_autorefresh import st_autorefresh
- 
  st.set_page_config(
      page_title="Баскетбол",
      page_icon="🏀",
      layout="wide",
--- 1,10 ----
  import os
  import json
  import socket
  import platform
  import numpy as np
  import pandas as pd
  import streamlit as st
  import sys
  from streamlit_autorefresh import st_autorefresh
+ 
  st.set_page_config(
      page_title="Баскетбол",
      page_icon="🏀",
      layout="wide",
***************
*** 164,169 ****
--- 164,216 ----
  def ensure_state(key: str, default=None):
      # Инициализирует ключ один раз и возвращает значение
      return st.session_state.setdefault(key, default)
  
+ 
+ # ======== UNIVERSAL SAFE RENDER WRAPPER ========
+ def _is_empty_like(x) -> bool:
+     if x is None:
+         return True
+     # DataFrame
+     if isinstance(x, pd.DataFrame):
+         return x.empty
+     # Pandas Styler
+     try:
+         # импортируем лениво, чтобы не ломаться, если нет pandas.io.formats.style в рантайме
+         from pandas.io.formats.style import Styler  # type: ignore
+         if isinstance(x, Styler):
+             # у Styler нет __len__, но есть .data
+             return getattr(x, "data", pd.DataFrame()).empty
+     except Exception:
+         pass
+     # пустые коллекции
+     if isinstance(x, (list, tuple, dict, set)):
+         return len(x) == 0
+     return False
+ 
+ def safe_show(func, *args, **kwargs):
+     """
+     Безопасно вызывает функции отображения Streamlit (dataframe, table, metric, image, markdown, ...).
+     - Ничего не рендерит, если основной аргумент данных пуст/None.
+     - Нормализует height (убирает None, <0; приводит к int).
+     - Возвращает результат вызова func (нужно для dataframe-selection).
+     - Перехватывает исключения и показывает предупреждение.
+     """
+     # Если среди позиционных/именованных аргументов есть пустые/None-данные — не показываем
+     for a in args:
+         if _is_empty_like(a):
+             return None
+     for k, v in kwargs.items():
+         # пропускаем не-данные параметры (типа width/unsafe_allow_html)
+         if k.lower() in ("height", "width", "use_container_width", "unsafe_allow_html", "on_select", "selection_mode", "column_config", "hide_index", "border", "delta_color", "key"):
+             continue
+         if _is_empty_like(v):
+             return None
+ 
+     # height -> валидный int, иначе уберём
+     if "height" in kwargs:
+         h = kwargs.get("height")
+         if h is None:
+             kwargs.pop("height")
+         else:
+             try:
+                 h = int(h)
+                 if h < 0:
+                     kwargs.pop("height")
+                 else:
+                     kwargs["height"] = h
+             except Exception:
+                 kwargs.pop("height")
+ 
+     try:
+         return func(*args, **kwargs)
+     except Exception as e:
+         st.warning(f"⚠️ Ошибка при отображении: {e}")
+         return None
+ # ======== /SAFE WRAPPER ========
+ 
***************
*** 221,231 ****
      col1, col4, col2, col5, col3 = st.columns([1, 5, 3, 5, 1])
  
      t1 = (result.get("team1") or {})
      t2 = (result.get("team2") or {})
-     if t1.get("logo"):
-         col1.image(t1["logo"], width=100)
      team1_name = t1.get("name") or ""
      team2_name = t2.get("name") or ""
-     if team1_name or team2_name:
-         col2.markdown(
-             f"<h2 style='text-align: center'>{team1_name} — {team2_name}</h2>",
-             unsafe_allow_html=True,
-         )
-     if t2.get("logo"):
-         col3.image(t2["logo"], width=100)
+     safe_show(col1.image, t1.get("logo"), width=100)
+     if team1_name or team2_name:
+         safe_show(
+             col2.markdown,
+             f"<h2 style='text-align: center'>{team1_name} — {team2_name}</h2>",
+             unsafe_allow_html=True,
+         )
+     safe_show(col3.image, t2.get("logo"), width=100)
  
      col4_1, col4_2, col4_3 = col4.columns((1, 1, 1))
      col5_1, col5_2, col5_3 = col5.columns((1, 1, 1))
  
***************
*** 237,253 ****
      if isinstance(cached_team_stats, list) and len(cached_team_stats) > 0:
          v1 = cached_team_stats[0].get("val1")
          v2 = cached_team_stats[0].get("val2")
          if v1 is not None and v2 is not None:
              val1, val2 = int(v1), int(v2)
              delta_color_1 = "off" if val1 == val2 else "normal"
-             col4_1.metric("Points", v1, val1 - val2, delta_color_1)
-             col5_3.metric("Points", v2, val2 - val1, delta_color_1)
+             safe_show(col4_1.metric, "Points", v1, val1 - val2, delta_color_1)
+             safe_show(col5_3.metric, "Points", v2, val2 - val1, delta_color_1)
  
-     col4_3.metric("TimeOuts", len(timeout1))
-     col5_1.metric("TimeOuts", len(timeout2))
+     safe_show(col4_3.metric, "TimeOuts", len(timeout1))
+     safe_show(col5_1.metric, "TimeOuts", len(timeout2))
  
      if isinstance(cached_live_status, list) and cached_live_status:
          foulsA = (cached_live_status[0] or {}).get("foulsA")
          foulsB = (cached_live_status[0] or {}).get("foulsB")
          if foulsA is not None:
-             col4_2.metric("Fouls", foulsA)
+             safe_show(col4_2.metric, "Fouls", foulsA)
          if foulsB is not None:
-             col5_2.metric("Fouls", foulsB)
+             safe_show(col5_2.metric, "Fouls", foulsB)
  
***************
*** 270,280 ****
      for q1, q2, col1_i, col2_i in zip(score_by_quarter_1, score_by_quarter_2, col_1_col, col_2_col):
          count_q += 1
          name_q = f"OT{count_q-4}" if count_q > 4 else f"Q{count_q}"
          try:
              delta_color = "off" if int(q1) == int(q2) else "normal"
-             col1_i.metric(name_q, q1, int(q1) - int(q2), delta_color, border=True)
-             col2_i.metric(name_q, q2, int(q2) - int(q1), delta_color, border=True)
+             safe_show(col1_i.metric, name_q, q1, int(q1) - int(q2), delta_color, border=True)
+             safe_show(col2_i.metric, name_q, q2, int(q2) - int(q1), delta_color, border=True)
          except (ValueError, TypeError):
              # если кривые данные в JSON, просто пропустим
              pass
  
***************
*** 403,424 ****
      team1_styled = (
          team1_data.style.apply(highlight_grey, axis=1)
          .apply(highlight_foul, subset="foul")
          .apply(highlight_max, subset="pts")
      )
      team2_styled = (
          team2_data.style.apply(highlight_grey, axis=1)
          .apply(highlight_foul, subset="foul")
          .apply(highlight_max, subset="pts")
      )
  
      # Вывод данных
      col_player1, col_player2 = tab_temp_1.columns((5, 5))
-     event1 = col_player1.dataframe(
-         team1_styled,
-         column_config=config,
-         hide_index=True,
-         height=460,
-         on_select="rerun",
-         selection_mode=[
-             "single-row",
-         ],
-     )
-     event2 = col_player2.dataframe(
-         team2_styled,
-         column_config=config,
-         hide_index=True,
-         height=460,
-         on_select="rerun",
-         selection_mode=[
-             "single-row",
-         ],
-     )
+     event1 = safe_show(
+         col_player1.dataframe,
+         team1_styled,
+         column_config=config,
+         hide_index=True,
+         height=460,
+         on_select="rerun",
+         selection_mode=["single-row"],
+     )
+     event2 = safe_show(
+         col_player2.dataframe,
+         team2_styled,
+         column_config=config,
+         hide_index=True,
+         height=460,
+         on_select="rerun",
+         selection_mode=["single-row"],
+     )
  
      if event1 and getattr(event1, "selection", None) and event1.selection.get("rows"):
          selected_index1 = event1.selection["rows"][0]
          st.session_state["player1"] = (
              selected_index1  # Сохранение состояния в session_state
***************
*** 433,441 ****
          if player_data_1["num"]:
              z, a, b, c, d, e = col_player1.columns((1, 6, 1, 1, 1, 1))
-             z.metric("Номер", player_data_1["num"], border=False)
-             a.metric("Игрок", player_data_1["NameGFX"], border=False)
-             b.metric("Амплуа", player_data_1["roleShort"], border=False)
-             c.metric("Возраст", player_data_1["age"], border=False)
-             d.metric("Рост", player_data_1["height"].split()[0], border=False)
-             e.metric("Вес", player_data_1["weight"].split()[0], border=False)
- 
-             col_player1.dataframe(
-                 selected_player_1,
-                 column_config=config_season,
-                 hide_index=True,
-             )
+             safe_show(z.metric, "Номер", player_data_1["num"], border=False)
+             safe_show(a.metric, "Игрок", player_data_1["NameGFX"], border=False)
+             safe_show(b.metric, "Амплуа", player_data_1["roleShort"], border=False)
+             safe_show(c.metric, "Возраст", player_data_1["age"], border=False)
+             safe_show(d.metric, "Рост", player_data_1["height"].split()[0], border=False)
+             safe_show(e.metric, "Вес", player_data_1["weight"].split()[0], border=False)
+ 
+             safe_show(
+                 col_player1.dataframe,
+                 selected_player_1,
+                 column_config=config_season,
+                 hide_index=True,
+             )
***************
*** 446,454 ****
          if player_data_2["num"]:
              z, a, b, c, d, e = col_player2.columns((1, 6, 1, 1, 1, 1))
-             z.metric("Номер", player_data_2["num"], border=False)
-             a.metric("Игрок", player_data_2["NameGFX"], border=False)
-             b.metric("Амплуа", player_data_2["roleShort"], border=False)
-             c.metric("Возраст", player_data_2["age"], border=False)
-             d.metric("Рост", player_data_2["height"].split()[0], border=False)
-             e.metric("Вес", player_data_2["weight"].split()[0], border=False)
- 
-             col_player2.dataframe(
-                 selected_player_2,
-                 column_config=config_season,
-                 hide_index=True,
-             )
+             safe_show(z.metric, "Номер", player_data_2["num"], border=False)
+             safe_show(a.metric, "Игрок", player_data_2["NameGFX"], border=False)
+             safe_show(b.metric, "Амплуа", player_data_2["roleShort"], border=False)
+             safe_show(c.metric, "Возраст", player_data_2["age"], border=False)
+             safe_show(d.metric, "Рост", player_data_2["height"].split()[0], border=False)
+             safe_show(e.metric, "Вес", player_data_2["weight"].split()[0], border=False)
+ 
+             safe_show(
+                 col_player2.dataframe,
+                 selected_player_2,
+                 column_config=config_season,
+                 hide_index=True,
+             )
***************
*** 459,468 ****
  if isinstance(cached_team_stats, list) and len(cached_team_stats) >= 34:
      cached_team_stats_new = [
          cached_team_stats[0],
          *cached_team_stats[25:29],
          cached_team_stats[7],
          cached_team_stats[33],
          *cached_team_stats[9:11],
          *cached_team_stats[15:17],
      ]
-     tab_temp_2.table(cached_team_stats_new)
+     safe_show(tab_temp_2.table, cached_team_stats_new)
  
***************
*** 470,484 ****
- if isinstance(cached_referee, (list, pd.DataFrame)):
-     tab_temp_3.dataframe(cached_referee, height=600, column_config={"flag": st.column_config.ImageColumn("flag")})
- 
- 
  column_config_ref = {
      "flag": st.column_config.ImageColumn(
          "flag",
      ),
  }
  
  if cached_referee:
-     tab_temp_3.dataframe(cached_referee, height=600, column_config=column_config_ref)
+     safe_show(tab_temp_3.dataframe, cached_referee, height=600, column_config=column_config_ref)
  
***************
*** 503,511 ****
      styled = df_st.style.apply(highlight_teams, axis=1)
-     tab_temp_4.dataframe(
-         styled,
-         column_config={"logo": st.column_config.ImageColumn("logo")},
-         hide_index=True,
-         height=610,
-     )
+     safe_show(
+         tab_temp_4.dataframe,
+         styled,
+         column_config={"logo": st.column_config.ImageColumn("logo")},
+         hide_index=True,
+         height=610,
+     )
  
***************
*** 552,558 ****
          ]
          col.write(q)
-         col.dataframe(df_col)
+         safe_show(col.dataframe, df_col)
  
      # Овертаймы
      for index, col in enumerate(columns_quarters):
          q = columns_quarters_name_ot[index]
          df_col = [
--- 564,570 ----
***************
*** 572,578 ****
          ]
          col.write(q)
-         col.dataframe(df_col)
+         safe_show(col.dataframe, df_col)
  
***************
*** 582,586 ****
  if isinstance(cached_play_by_play, list) and isinstance(cached_game_online, dict):
      plays = (cached_game_online.get("result") or {}).get("plays") or []
      if plays:
-         tab_temp_6.table(cached_play_by_play)
+         safe_show(tab_temp_6.table, cached_play_by_play)
  
***************
*** 703,724 ****
          height1 = 38 * max(count_game_1, 10)
          height2 = 38 * max(count_game_2, 10)
-         col1_schedule.dataframe(
-             team1_data,
-             hide_index=True,
-             height=int(min(height1, 1200)),
-             column_config=column_config,
-         )
-         col2_schedule.dataframe(
-             team2_data,
-             hide_index=True,
-             height=int(min(height2, 1200)),
-             column_config=column_config,
-         )
+         safe_show(
+             col1_schedule.dataframe,
+             team1_data,
+             hide_index=True,
+             height=int(min(height1, 1200)),
+             column_config=column_config,
+         )
+         safe_show(
+             col2_schedule.dataframe,
+             team2_data,
+             hide_index=True,
+             height=int(min(height2, 1200)),
+             column_config=column_config,
+         )
  
***************
*** 836,845 ****
              filtered_data_pbp = temp_data_pbp[mask1]
              count_pbp = len(filtered_data_pbp)
  
              column_pbp = ["num", "info", "who", "period", "time"]
              column_config_pbp = {
                  "info": st.column_config.TextColumn(width="medium"),
                  "who": st.column_config.TextColumn(width="large"),
              }
-             col2_pbp.dataframe(
-                 filtered_data_pbp[column_pbp],
-                 column_config=column_config_pbp,
-                 hide_index=True,
-                 height=(38 * count_pbp if count_pbp > 10 else None),
-             )
+             safe_show(
+                 col2_pbp.dataframe,
+                 filtered_data_pbp[column_pbp],
+                 column_config=column_config_pbp,
+                 hide_index=True,
+                 height=(38 * count_pbp if count_pbp > 10 else None),
+             )
      else:
          st.info("Данных play-by-play нет.")
