diff --git a/app.py b/app.py
index c08da9e..fca0d87 100644
--- a/app.py
+++ b/app.py
@@ -27,6 +27,8 @@ from urllib.parse import quote, urlencode
import traceback
import contextlib
import os
+import html
+import re
from parsers.parser_game import run_parser_game
from parsers.parser_players import run_parser_players
from parsers.parser_schedule import run_parser_schedule
@@ -1924,6 +1926,230 @@ async def list_vmix_clients():
return {"items": clients}
+
+INFO_TEMPLATE_PATH = BASE_DIR / "templates" / "info.html"
+
+
+def _clean_update_text(value: str | None) -> str:
+ return (value or "").strip()
+
+
+def _escape_text(value: str | None) -> str:
+ return html.escape(_clean_update_text(value), quote=True)
+
+
+def _split_lines(value: str | None) -> list[str]:
+ return [line.strip() for line in (value or "").splitlines() if line.strip()]
+
+
+def _format_update_date(value: str | None) -> str:
+ value = _clean_update_text(value)
+ if re.fullmatch(r"\d{4}-\d{2}-\d{2}", value):
+ try:
+ return datetime.strptime(value, "%Y-%m-%d").strftime("%d.%m.%Y")
+ except ValueError:
+ return value
+ return value
+
+
+def _paragraphs_html(value: str | None) -> str:
+ lines = _split_lines(value)
+ if not lines:
+ return "
Описание не заполнено.
"
+
+ return "\n".join(f"
{_escape_text(line)}
" for line in lines)
+
+
+def _normalize_update_image_path(value: str | None) -> str:
+ path = _clean_update_text(value)
+ if not path:
+ return ""
+
+ # Удобный вариант для админки: можно ввести только имя файла,
+ # например "no photo.png" — оно превратится в /static/docs/no photo.png.
+ if path.startswith("static/"):
+ path = "/" + path
+ elif path.startswith("docs/"):
+ path = "/static/" + path
+ elif not path.startswith(("/static/", "http://", "https://")):
+ path = "/static/docs/" + path.lstrip("/")
+
+ return path
+
+
+def _image_html(value: str | None, alt: str | None = None) -> str:
+ path = _normalize_update_image_path(value)
+ if not path:
+ return ""
+
+ return f'\n '
+
+
+def _build_update_block(
+ version: str,
+ date: str,
+ operator_actions: str,
+ item_badges: list[str],
+ item_titles: list[str],
+ item_texts: list[str],
+ item_images: list[str] | None = None,
+) -> str:
+ badge_meta = {
+ "new": ("new", "Новое"),
+ "fix": ("fix", "Исправлено"),
+ "improve": ("improve", "Улучшено"),
+ "important": ("important", "Важно"),
+ }
+
+ version_html = _escape_text(version)
+ date_html = _escape_text(date)
+
+ actions = _split_lines(operator_actions)
+ if actions:
+ actions_html = "\n".join(f"
{_escape_text(action)}
" for action in actions)
+ else:
+ actions_html = "
Ознакомиться с обновлением перед работой с матчем.
"
+
+ item_images = item_images or []
+
+ update_items: list[str] = []
+ max_len = max(len(item_titles), len(item_texts), len(item_badges), len(item_images), 1)
+
+ for index in range(max_len):
+ title = _clean_update_text(item_titles[index] if index < len(item_titles) else "")
+ text = _clean_update_text(item_texts[index] if index < len(item_texts) else "")
+ badge = _clean_update_text(item_badges[index] if index < len(item_badges) else "new")
+ image = _clean_update_text(item_images[index] if index < len(item_images) else "")
+
+ if not title and not text:
+ continue
+
+ badge_class, badge_label = badge_meta.get(badge, badge_meta["new"])
+ item_title_html = _escape_text(title or "Без названия")
+ item_text_html = _paragraphs_html(text) + _image_html(image, title or "Скриншот обновления")
+
+ update_items.append(
+ f'''
+ {badge_label}
+
+
+
{item_title_html}
+
+
+ {item_text_html}
+
+
+
'''
+ )
+
+ if not update_items:
+ update_items.append(
+ '''
+
+
+
+'''.rstrip()
+
+
+def _update_latest_summary(html_text: str, version: str, date: str, item_titles: list[str]) -> str:
+ titles = [title.strip() for title in item_titles if title and title.strip()]
+ if not titles:
+ titles = ["Добавлено новое обновление системы."]
+
+ list_items = "\n".join(f"
{_escape_text(title)}
" for title in titles[:5])
+ summary_html = f'''
+
Последние изменения: версия {_escape_text(version)} от {_escape_text(date)}
+ Статус:
+ {% if update_success %}
+ успешно
+ {% else %}
+ ошибка
+ {% endif %}
+
+
{{ update_output }}
+
+ {% endif %}
@@ -376,6 +467,78 @@
+
+
+
+
Добавить обновление
+
+ Заполните версию, дату, действия для оператора и пункты изменений. При необходимости добавьте путь к картинке. После сохранения новая версия появится сверху на странице /info.
+
@@ -662,37 +720,30 @@
У нас тут есть сортировки по сегодня, по турам. Находим наш матч и нажимаем на кнопку Выбрать матч. Открывается рабочая страница матча, откуда уже можно управлять составами, событиями и готовить данные для трансляции.
-
Теперь нам нужно скачать проект vMix с нужными данными и запустить agent.exe. Проект скачивается второй кнопке на скриншоте выше (маленький значок vMix).
+
Теперь нам нужно скачать проект vMix с нужными данными и запустить agent.exe. Проект скачивается по второй кнопке на скриншоте выше (маленький значок vMix).
- Если на странице матча на сайте есть составы команд, то нам достаточно нажать первую кнопку (синий шар) на рабочей странице матча, и система сама загрузит составы, тренеров и судей.
+ Если на странице матча на сайте есть составы команд, то нам достаточно нажать на первую кнопку (синий шар) на рабочей странице матча, и система сама загрузит составы, тренеров и судей.
- Для добавление игроков в состав нужно нажать на кнопку "Редактировать" (значок карандаша) и заполнить данные по игрокам и тренерам.
+ Для добавления игроков в состав нужно нажать на кнопку "Редактировать" (значок карандаша) и заполнить данные по игрокам и тренерам.
-
Откроется форма, в которой мы выбираем игроков и заполняем их в основной состав или в запас. Там же у каждого игрока есть возможно перекинуть его в старт/запас и сделать капитаном. Чуть ниже выбираем тренеров и далее нажимаем кнопку "Сохранить изменения". Если автоматически в браузере не обновились составы в форме, то просто нажмите F5 или обновите страницу.
+
Откроется форма, в которой мы выбираем игроков и заполняем их в основной состав или в запас. Там же у каждого игрока есть возможность перекинуть его в старт/запас и сделать капитаном. Чуть ниже выбираем тренеров и далее нажимаем кнопку "Сохранить изменения". Если автоматически в браузере не обновились составы в форме, то просто нажмите F5 или обновите страницу.
Переходим на вкладку Судьи и опять нажимаем на кнопку "Редактировать" (значок карандаша). В поиске судьи вбиваем нашу фамилию или имя, выбираем судью, ниже выбираем роль ему и нажимаем кнопку "Добавить судью", добавляем таким же образом всех остальных, а уже далее нажимаем кнопку "Сохранить".
@@ -709,14 +760,14 @@
- Выбираем нашу расстановку, далее жмём кнопку "Применить расстановку". Открывается форма, где мы последовательно начиная с вратаря заполнять слева направо наши номера игроков. Нажимаем кнопку "Применить игроков" и далее нажимаем кнопку "Сохранить". В проекте vMix начнёться расставление координат в титре Расстановки.
+ Выбираем нашу расстановку, далее жмём кнопку "Применить расстановку". Открывается форма, где мы последовательно начиная с вратаря, заполнить слева направо номера игроков. Нажимаем кнопку "Применить игроков" и далее нажимаем кнопку "Сохранить". В проекте vMix начнётся расставление координат в титре Расстановки.
ВАЖНО
-
При повторном обновлении данных с сайта расстановка слетит в браузере, и если вам нужно внести изменения, например капитан поменялся или игрок заменился в основном составе, то расстановку нужно заного пробить.
-
Если видим, что на расстановке у кого нибудь пишется неверная фамилия или плашка вообще пустая или фотография неверно подтянулась, можно у своей команды нажать еще раз кнопку "Сохранить" и тогда данные должны обновиться, если не произойдет, нужно еще раз попробовать до момента пока не получиться (у меня получалось три раза так прожать).
+
При повторном обновлении данных с сайта расстановка слетит в браузере, и если вам нужно внести изменения, например капитан поменялся или игрок заменился в основном составе, то расстановку нужно заново пробить.
+
Если видим, что на расстановке у кого-нибудь пишется неверная фамилия или плашка вообще пустая или фотография неверно подтянулась, можно у своей команды нажать еще раз кнопку "Сохранить" и тогда данные должны обновиться, если не произойдёт, нужно еще раз попробовать до момента пока не получится (у меня получалось три раза так прожать).