Files
WFL/templates/matches.html
Юрий Черненко 7baed89b6a 1. исправил agent.py
2. поменял явки и пароли для создания операторов
3. убрал из maches.html оператора, и добавил галочку завершенные матчи
2026-04-21 11:44:58 +03:00

562 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Выбор матча</title>
<style>
:root {
--bg: #0f1115;
--panel: #171a21;
--panel-2: #1d222b;
--border: #2b3240;
--text: #e8ecf3;
--muted: #9aa4b2;
--accent: #4f8cff;
--accent-hover: #3e78e6;
--success-bg: rgba(46, 160, 67, 0.16);
--success-border: rgba(46, 160, 67, 0.4);
--live-bg: rgba(245, 158, 11, 0.16);
--live-border: rgba(245, 158, 11, 0.4);
--scheduled-bg: transparent;
--input-bg: #11151c;
--shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
--radius: 14px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 28px;
font-family: Arial, sans-serif;
background: var(--bg);
color: var(--text);
}
.page {
max-width: 1400px;
margin: 0 auto;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
}
h1 {
margin: 0;
font-size: 28px;
font-weight: 700;
letter-spacing: 0.2px;
}
.subtitle {
color: var(--muted);
font-size: 14px;
margin-top: 6px;
}
.panel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.filters {
display: flex;
flex-wrap: wrap;
gap: 18px;
align-items: end;
padding: 18px;
margin-bottom: 18px;
}
.filter-group label {
display: block;
font-size: 13px;
color: var(--muted);
margin-bottom: 8px;
}
.check-line {
display: flex;
align-items: center;
gap: 8px;
min-height: 40px;
}
input[type="checkbox"] {
transform: scale(1.15);
accent-color: var(--accent);
}
select,
input[type="text"] {
min-height: 40px;
padding: 0 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--input-bg);
color: var(--text);
outline: none;
}
select:focus,
input[type="text"]:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(79, 140, 255, 0.15);
}
a {
color: #8ab4ff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
thead th {
position: sticky;
top: 0;
background: var(--panel-2);
color: var(--muted);
font-size: 13px;
font-weight: 600;
text-align: left;
padding: 14px 12px;
border-bottom: 1px solid var(--border);
}
thead th:first-child {
border-top-left-radius: var(--radius);
}
thead th:last-child {
border-top-right-radius: var(--radius);
}
tbody td {
padding: 14px 12px;
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
tbody tr:last-child td {
border-bottom: none;
}
tbody tr:hover {
background: rgba(255, 255, 255, 0.03);
}
tbody tr.finished {
background: var(--success-bg);
}
tbody tr.finished:hover {
background: rgba(46, 160, 67, 0.22);
}
tbody tr.live {
background: var(--live-bg);
}
tbody tr.live:hover {
background: rgba(245, 158, 11, 0.22);
}
.match-cell {
font-weight: 600;
}
.muted {
color: var(--muted);
font-size: 13px;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.4px;
border: 1px solid var(--border);
}
.status-finished {
background: var(--success-bg);
border-color: var(--success-border);
color: #8ee29b;
}
.status-live {
background: var(--live-bg);
border-color: var(--live-border);
color: #ffd27a;
}
.status-scheduled {
background: rgba(255,255,255,0.04);
color: #c8d1dc;
}
.select-form {
display: flex;
gap: 8px;
align-items: center;
}
.operator-input {
width: 170px;
}
.btn {
min-height: 40px;
padding: 0 14px;
border: none;
border-radius: 10px;
background: var(--accent);
color: white;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease, transform 0.05s ease;
}
.btn:hover {
background: var(--accent-hover);
}
.btn:active {
transform: translateY(1px);
}
.empty {
padding: 28px;
color: var(--muted);
}
.id-chip {
display: inline-block;
padding: 4px 8px;
border-radius: 999px;
background: rgba(255,255,255,0.05);
color: var(--muted);
font-size: 12px;
}
.page-actions {
display: flex;
align-items: center;
gap: 10px;
}
.logout-form {
margin: 0;
}
.btn-secondary {
background: rgba(255,255,255,0.08);
color: var(--text);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: rgba(255,255,255,0.12);
}
.session-banner {
display: none;
margin-bottom: 16px;
padding: 12px 14px;
border-radius: 10px;
border: 1px solid rgba(255, 193, 7, 0.35);
background: rgba(255, 193, 7, 0.08);
color: var(--text);
}
</style>
</head>
<body>
<div class="page">
<div class="page-header">
<div>
<h1>Выбор матча</h1>
<div class="subtitle">Рабочая панель оператора трансляции</div>
</div>
<div class="page-actions">
<form method="post" action="/logout" class="logout-form">
<button type="submit" class="btn btn-secondary">Выйти</button>
</form>
</div>
</div>
<div id="sessionStatusBanner" class="session-banner"></div>
<form method="get" action="/admin/matches" class="filters panel" id="filters-form">
<div class="filter-group">
<label>Фильтр по дате</label>
<div class="check-line">
<input
type="checkbox"
name="today_only"
value="true"
{% if today_only %}checked{% endif %}
onchange="document.getElementById('filters-form').submit();"
>
<span>Только сегодня</span>
</div>
</div>
<div class="filter-group">
<label for="tour">Тур</label>
<select
name="tour"
id="tour"
onchange="document.getElementById('filters-form').submit();"
>
<option value="">Все туры</option>
{% for t in tours %}
<option value="{{ t }}" {% if selected_tour == t %}selected{% endif %}>{{ t }}</option>
{% endfor %}
</select>
</div>
<div class="filter-group">
<label>Статус матчей</label>
<div class="check-line">
<input type="hidden" name="hide_finished" value="false">
<input
type="checkbox"
name="hide_finished"
value="true"
{% if hide_finished %}checked{% endif %}
onchange="document.getElementById('filters-form').submit();"
>
<span>Завершенные матчи</span>
</div>
</div>
<div class="filter-group">
<label>&nbsp;</label>
<div class="check-line">
<a href="/admin/matches">Сбросить фильтры</a>
</div>
</div>
</form>
<div class="panel table-wrap">
{% if matches %}
<table>
<thead>
<tr>
<th>ID</th>
<th>Дата</th>
<th>Матч</th>
<th>Тур</th>
<th>Сезон</th>
<th>Статус</th>
<th>Действие</th>
</tr>
</thead>
<tbody>
{% for row in matches %}
{% set status = (row[3] or 'scheduled') %}
<tr class="{{ status }}">
<td><span class="id-chip">{{ row[0] }}</span></td>
<td>
<div>{{ row[2] or "" }}</div>
</td>
<td class="match-cell">
{{ row[6] }} — {{ row[7] }}
</td>
<td>{{ row[4] or "" }}</td>
<td>{{ row[5] or "" }}</td>
<td>
<span class="status-badge status-{{ status }}">
{{ status }}
</span>
</td>
<td>
<form method="get" action="/admin/matches/{{ row[0] }}/select" class="select-form js-select-form">
<button type="submit" class="btn">Выбрать матч</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty">Матчи не найдены.</div>
{% endif %}
</div>
</div>
<script>
(function () {
async function handleSelectSubmit(event) {
const form = event.target;
if (!form.classList.contains("js-select-form")) return;
event.preventDefault();
const submitButton = form.querySelector('button[type="submit"]');
const operatorInput = form.querySelector('input[name="operator_name"]');
const operatorName = operatorInput ? operatorInput.value : "";
if (submitButton) {
submitButton.disabled = true;
submitButton.textContent = "Подготовка...";
}
try {
const url = new URL(form.action, window.location.origin);
url.searchParams.set("operator_name", operatorName);
const response = await fetch(url.toString(), {
method: "GET",
headers: {
"X-Requested-With": "XMLHttpRequest",
"Accept": "application/json"
},
credentials: "same-origin"
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || data.vmix_error || "Не удалось выбрать матч");
}
if (data.download_url) {
const downloadResponse = await fetch(data.download_url, {
method: "GET",
credentials: "same-origin"
});
if (!downloadResponse.ok) {
let errText = "Не удалось скачать vMix проект";
try {
const errJson = await downloadResponse.json();
errText = errJson.error || errText;
} catch (_) {}
throw new Error(errText);
}
const blob = await downloadResponse.blob();
let filename = "project.vmix";
const disposition = downloadResponse.headers.get("Content-Disposition");
if (disposition) {
let match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
if (match && match[1]) {
filename = decodeURIComponent(match[1]);
} else {
match = disposition.match(/filename="([^"]+)"/i);
if (match && match[1]) {
filename = match[1];
}
}
}
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(blobUrl);
}
window.location.href = data.session_url;
} catch (error) {
alert(error.message || "Ошибка при подготовке матча");
if (submitButton) {
submitButton.disabled = false;
submitButton.textContent = "Выбрать матч";
}
}
}
document.addEventListener("submit", handleSelectSubmit);
})();
(function () {
const LOGIN_URL = "/login";
const IDLE_LIMIT_MS = 2 * 60 * 60 * 1000;
const WARNING_BEFORE_MS = 60 * 1000;
const ACTIVITY_EVENTS = ["mousemove", "mousedown", "keydown", "scroll", "touchstart", "click"];
const banner = document.getElementById("sessionStatusBanner");
let lastActivityAt = Date.now();
let warned = false;
let redirected = false;
function showBanner(message) {
if (!banner) return;
banner.textContent = message;
banner.style.display = "block";
}
function markActivity() {
lastActivityAt = Date.now();
warned = false;
if (banner) banner.style.display = "none";
}
function redirectToLogin(reason) {
if (redirected) return;
redirected = true;
const url = new URL(LOGIN_URL, window.location.origin);
if (reason) url.searchParams.set("reason", reason);
window.location.replace(url.toString());
}
ACTIVITY_EVENTS.forEach((eventName) => {
window.addEventListener(eventName, markActivity, { passive: true });
});
document.addEventListener("submit", markActivity, true);
setInterval(() => {
const idleFor = Date.now() - lastActivityAt;
const leftMs = IDLE_LIMIT_MS - idleFor;
if (!warned && leftMs <= WARNING_BEFORE_MS && leftMs > 0) {
warned = true;
const leftMinutes = Math.ceil(leftMs / 60000);
showBanner(`До выхода из системы из-за неактивности осталось около ${leftMinutes} мин.`);
}
if (idleFor >= IDLE_LIMIT_MS) {
showBanner("Сессия завершена из-за 2 часов неактивности.");
redirectToLogin("idle");
}
}, 15000);
})();
</script>
</body>
</html>