Files
WFL/templates/admin_db_index.html

826 lines
31 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">
<title>База</title>
<link rel="icon" href="/static/smith.ico" type="image/x-icon">
<style>
:root {
--bg: #0f1115;
--panel: #171a21;
--panel-2: #1d222b;
--border: #2b3240;
--text: #e8ecf3;
--muted: #9aa4b2;
--accent: #00ff88;
--accent-hover: #00d976;
--success-bg: rgba(46, 204, 113, 0.12);
--success-border: rgba(46, 204, 113, 0.35);
--danger-bg: rgba(255, 99, 99, 0.12);
--danger-border: rgba(255, 99, 99, 0.35);
--shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
--radius: 16px;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: Arial, sans-serif;
}
.page {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.panel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 20px;
}
.title {
font-size: 24px;
font-weight: 700;
margin-bottom: 18px;
}
.tabs,
.parser-actions,
.account-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.parser-actions,
.account-actions {
margin-top: 18px;
padding-top: 18px;
border-top: 1px solid var(--border);
}
.tab-link,
.action-btn,
.account-toggle-btn,
.submit-btn,
.cancel-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 42px;
padding: 0 16px;
border-radius: 999px;
background: var(--panel-2);
border: 1px solid var(--border);
color: var(--text);
text-decoration: none;
font-weight: 600;
cursor: pointer;
font-size: 14px;
transition: 0.2s ease;
}
.tab-link:hover,
.action-btn:hover,
.account-toggle-btn:hover,
.submit-btn:hover {
background: var(--accent);
border-color: var(--accent);
color: #04110b;
}
.cancel-btn:hover {
background: #293140;
border-color: #3b4658;
}
.action-form,
.account-create-form {
margin: 0;
}
.log-block {
margin-top: 20px;
border: 1px solid var(--border);
border-radius: 14px;
padding: 16px;
background: var(--panel-2);
}
.log-block.success {
background: var(--success-bg);
border-color: var(--success-border);
}
.log-block.error {
background: var(--danger-bg);
border-color: var(--danger-border);
}
.log-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 10px;
}
.log-status {
color: var(--muted);
margin-bottom: 10px;
}
.log-output {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-family: Consolas, Monaco, monospace;
font-size: 13px;
line-height: 1.5;
}
.action-form.is-loading .action-btn {
opacity: 0.75;
pointer-events: none;
}
.account-modal,
.update-modal {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(3, 8, 15, 0.84);
z-index: 9998;
padding: 20px;
}
.account-modal.active,
.update-modal.active {
display: flex;
}
.account-modal-card,
.update-modal-card {
width: min(100%, 460px);
background: rgba(12, 17, 26, 0.98);
border: 1px solid rgba(0, 255, 136, 0.28);
border-radius: 18px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
padding: 24px;
}
.update-modal-card {
width: min(100%, 760px);
max-height: 92vh;
overflow-y: auto;
}
.account-modal-title,
.update-modal-title {
font-size: 22px;
font-weight: 700;
margin-bottom: 8px;
}
.account-modal-subtitle,
.update-modal-subtitle {
color: var(--muted);
margin-bottom: 18px;
line-height: 1.5;
}
.form-grid {
display: grid;
gap: 14px;
}
.field-label {
display: block;
font-size: 13px;
font-weight: 700;
color: var(--muted);
margin-bottom: 6px;
}
.field-input,
.field-select,
.field-textarea {
width: 100%;
min-height: 44px;
border-radius: 12px;
border: 1px solid var(--border);
background: #0f141d;
color: var(--text);
padding: 0 14px;
font-size: 14px;
outline: none;
}
.field-textarea {
min-height: 96px;
padding: 12px 14px;
resize: vertical;
line-height: 1.45;
font-family: Arial, sans-serif;
}
.field-input:focus,
.field-select:focus,
.field-textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.12);
}
.update-form-grid {
display: grid;
grid-template-columns: 1fr 180px;
gap: 14px;
}
.update-items-editor {
display: grid;
gap: 14px;
}
.update-item-form {
display: grid;
grid-template-columns: 160px minmax(0, 1fr);
gap: 12px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 14px;
background: rgba(255, 255, 255, 0.025);
}
.update-item-text,
.update-item-image {
grid-column: 1 / -1;
}
.helper-text {
color: var(--muted);
font-size: 12px;
line-height: 1.45;
}
.format-guide {
margin-top: 8px;
padding: 10px 12px;
border: 1px solid rgba(0, 255, 136, 0.18);
border-radius: 12px;
background: rgba(0, 255, 136, 0.045);
color: var(--muted);
font-size: 12px;
line-height: 1.55;
}
.format-guide code {
color: #00ff88;
background: rgba(0, 255, 136, 0.10);
border: 1px solid rgba(0, 255, 136, 0.22);
border-radius: 7px;
padding: 1px 6px;
}
.format-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.format-btn {
min-height: 30px;
padding: 0 10px;
border: 1px solid rgba(0, 255, 136, 0.28);
border-radius: 999px;
color: #9cf3c8;
background: rgba(0, 255, 136, 0.055);
cursor: pointer;
font-size: 12px;
}
.format-btn:hover {
color: #00ff88;
background: rgba(0, 255, 136, 0.12);
}
.add-row-btn {
min-height: 38px;
padding: 0 14px;
border-radius: 999px;
border: 1px dashed rgba(0, 255, 136, 0.45);
background: rgba(0, 255, 136, 0.06);
color: var(--accent);
font-weight: 700;
cursor: pointer;
}
.add-row-btn:hover {
background: rgba(0, 255, 136, 0.12);
}
@media (max-width: 720px) {
.update-form-grid,
.update-item-form {
grid-template-columns: 1fr;
}
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 18px;
flex-wrap: wrap;
}
.matrix-loader {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(3, 8, 15, 0.94);
z-index: 9999;
overflow: hidden;
}
.matrix-loader.active {
display: flex;
}
.matrix-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.matrix-content {
position: relative;
z-index: 1;
width: min(90vw, 560px);
padding: 28px 24px;
border-radius: 18px;
border: 1px solid rgba(0, 255, 136, 0.35);
background: rgba(12, 17, 26, 0.88);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
text-align: center;
backdrop-filter: blur(6px);
}
.matrix-title {
font-size: 28px;
font-weight: 700;
color: #dcffef;
margin-bottom: 12px;
}
.matrix-status {
font-size: 16px;
color: #9cf3c8;
line-height: 1.6;
}
</style>
</head>
<body>
<div class="page">
<div class="panel">
<div class="title">Редактирование базы</div>
<div class="tabs">
<a class="tab-link" href="/admin/db/players">Игроки</a>
<a class="tab-link" href="/admin/db/referees">Судьи</a>
<a class="tab-link" href="/admin/db/teams">Команды</a>
<a class="tab-link" href="/admin/db/coaches">Тренеры</a>
<a class="tab-link" href="/admin/db/stadiums">Стадионы</a>
<a class="tab-link" href="/admin/matches">Назад к матчам</a>
</div>
<div class="parser-actions">
<form method="post" action="/admin/db/run-parser" class="action-form" data-parser-title="Игроки" data-parser-status="Сбор и сохранение базы игроков...">
<input type="hidden" name="parser_name" value="players">
<button type="submit" class="action-btn">🗄️ Заграбить игроков</button>
</form>
<form method="post" action="/admin/db/run-parser" class="action-form" data-parser-title="Расписание" data-parser-status="Загрузка расписания матчей с сайта...">
<input type="hidden" name="parser_name" value="schedule">
<button type="submit" class="action-btn">🗄️ Заграбить расписание</button>
</form>
<form method="post" action="/admin/db/run-parser" class="action-form" data-parser-title="Турнирка" data-parser-status="Обновление турнирной таблицы...">
<input type="hidden" name="parser_name" value="standings">
<button type="submit" class="action-btn">🗄️ Заграбить турнирку</button>
</form>
</div>
<div class="account-actions">
<button type="button" class="account-toggle-btn" id="openCreateAccountBtn"> Создать аккаунт</button>
<button type="button" class="account-toggle-btn" id="openAddUpdateBtn">📝 Добавить обновление</button>
</div>
{% if account_output %}
<div class="log-block {% if account_success %}success{% else %}error{% endif %}">
<div class="log-title">Создание аккаунта</div>
<div class="log-status">
Статус:
{% if account_success %}
успешно
{% else %}
ошибка
{% endif %}
</div>
<pre class="log-output">{{ account_output }}</pre>
</div>
{% endif %}
{% if parser_output %}
<div class="log-block {% if parser_success %}success{% else %}error{% endif %}">
<div class="log-title">Итог парсинга{% if parser_name %}: {{ parser_name }}{% endif %}</div>
<div class="log-status">
Статус:
{% if parser_success %}
успешно
{% else %}
ошибка
{% endif %}
</div>
<pre class="log-output">{{ parser_output }}</pre>
</div>
{% endif %}
{% if update_output %}
<div class="log-block {% if update_success %}success{% else %}error{% endif %}">
<div class="log-title">Добавление обновления</div>
<div class="log-status">
Статус:
{% if update_success %}
успешно
{% else %}
ошибка
{% endif %}
</div>
<pre class="log-output">{{ update_output }}</pre>
</div>
{% endif %}
</div>
</div>
<div id="accountModal" class="account-modal" aria-hidden="true">
<div class="account-modal-card">
<div class="account-modal-title">Создать аккаунт</div>
<div class="account-modal-subtitle">Заполните данные нового пользователя и выберите роль.</div>
<form method="post" action="/admin/db/create-account" class="account-create-form" id="createAccountForm">
<div class="form-grid">
<div>
<label class="field-label" for="accountUsername">Логин</label>
<input class="field-input" id="accountUsername" type="text" name="username" required>
</div>
<div>
<label class="field-label" for="accountPassword">Пароль</label>
<input class="field-input" id="accountPassword" type="password" name="password" required>
</div>
<div>
<label class="field-label" for="accountRole">Роль</label>
<select class="field-select" id="accountRole" name="role" required>
<option value="admin">admin</option>
<option value="operator">operator</option>
</select>
</div>
</div>
<div class="modal-actions">
<button type="button" class="cancel-btn" id="closeCreateAccountBtn">Отмена</button>
<button type="submit" class="submit-btn">Создать</button>
</div>
</form>
</div>
</div>
<div id="updateModal" class="update-modal" aria-hidden="true">
<div class="update-modal-card">
<div class="update-modal-title">Добавить обновление</div>
<div class="update-modal-subtitle">
Заполните версию, дату, действия для оператора и пункты изменений. При необходимости добавьте путь к картинке. После сохранения новая версия появится сверху на странице /info.
</div>
<form method="post" action="/admin/db/add-update" id="addUpdateForm">
<div class="form-grid">
<div class="update-form-grid">
<div>
<label class="field-label" for="updateVersion">Версия</label>
<input class="field-input" id="updateVersion" type="text" name="version" placeholder="1.4" required>
</div>
<div>
<label class="field-label" for="updateDate">Дата</label>
<input class="field-input" id="updateDate" type="date" name="date" required>
</div>
</div>
<div>
<label class="field-label" for="operatorActions">Что сделать оператору</label>
<textarea class="field-textarea" id="operatorActions" name="operator_actions" placeholder="Каждое действие с новой строки">Скачать новый проект vMix перед работой с матчем.
Полностью заменить все файлы проекта.</textarea>
<div class="helper-text">Каждая строка станет отдельным пунктом списка. Можно выделять клавиши и важные слова.</div>
<div class="format-guide">
Формат: <code>`F5`</code> — клавиша/команда, <code>**важно**</code> — выделенное слово.
</div>
<div class="format-toolbar" data-format-for="operatorActions">
<button type="button" class="format-btn" data-snippet="`F5`">⌨️ Клавиша</button>
<button type="button" class="format-btn" data-snippet="**важно**">✨ Выделить слово</button>
</div>
</div>
<div>
<label class="field-label">Пункты обновления</label>
<div class="update-items-editor" id="updateItemsEditor">
<div class="update-item-form">
<div>
<label class="field-label">Тип</label>
<select class="field-select" name="item_badge">
<option value="new">Новое</option>
<option value="fix">Исправлено</option>
<option value="improve">Улучшено</option>
<option value="important">Важно</option>
</select>
</div>
<div>
<label class="field-label">Заголовок</label>
<input class="field-input" type="text" name="item_title" placeholder="Например: Верхний счёт" required>
</div>
<div class="update-item-text">
<label class="field-label">Описание</label>
<textarea class="field-textarea" name="item_text" placeholder="Пример:
1. Нажмите `1`, чтобы уменьшить таймер на секунду.
2. Нажмите `2`, чтобы увеличить таймер на секунду.
Можно выделить **важный текст**." required></textarea>
<div class="format-guide">
Красивый список: <code>1. Первый пункт</code> или <code>- Первый пункт</code><br>
Клавиши/команды: <code>`F5`</code>, <code>`1`</code>. Выделение слов: <code>**важно**</code>.
</div>
<div class="format-toolbar">
<button type="button" class="format-btn" data-snippet="1. Первый пункт&#10;2. Второй пункт">🔢 Список</button>
<button type="button" class="format-btn" data-snippet="- Первый пункт&#10;- Второй пункт">• Маркеры</button>
<button type="button" class="format-btn" data-snippet="`F5`">⌨️ Клавиша</button>
<button type="button" class="format-btn" data-snippet="**важно**">✨ Выделить слово</button>
</div>
</div>
<div class="update-item-image">
<label class="field-label">Картинка / скриншот</label>
<input class="field-input" type="text" name="item_image" placeholder="Например: /static/docs/no photo.png или просто no photo.png">
<div class="helper-text">Картинку нужно положить в static/docs. Если указать только имя файла, система сама подставит /static/docs/.</div>
</div>
</div>
</div>
<button type="button" class="add-row-btn" id="addUpdateItemBtn"> Добавить ещё пункт</button>
</div>
</div>
<div class="modal-actions">
<button type="button" class="cancel-btn" id="closeAddUpdateBtn">Отмена</button>
<button type="submit" class="submit-btn">Сохранить обновление</button>
</div>
</form>
</div>
</div>
<div id="matrixLoader" class="matrix-loader" aria-hidden="true">
<canvas id="matrixCanvas" class="matrix-canvas"></canvas>
<div class="matrix-content">
<div class="matrix-title" id="matrixTitle">Запуск парсера</div>
<div class="matrix-status" id="matrixStatusText">Подготовка...</div>
</div>
</div>
<script>
const loader = document.getElementById("matrixLoader");
const titleEl = document.getElementById("matrixTitle");
const statusText = document.getElementById("matrixStatusText");
const canvas = document.getElementById("matrixCanvas");
const ctx = canvas ? canvas.getContext("2d") : null;
const parserForms = document.querySelectorAll(".action-form");
const openCreateAccountBtn = document.getElementById("openCreateAccountBtn");
const closeCreateAccountBtn = document.getElementById("closeCreateAccountBtn");
const accountModal = document.getElementById("accountModal");
const openAddUpdateBtn = document.getElementById("openAddUpdateBtn");
const closeAddUpdateBtn = document.getElementById("closeAddUpdateBtn");
const updateModal = document.getElementById("updateModal");
const updateDate = document.getElementById("updateDate");
const addUpdateItemBtn = document.getElementById("addUpdateItemBtn");
const updateItemsEditor = document.getElementById("updateItemsEditor");
let drops = [];
let fontSize = 18;
let columns = 0;
let matrixAnimationId = null;
let lastFrameTime = 0;
const frameInterval = 70;
const chars = "01アイウエオカキクケコサシスセソタチツテトABCDEFGHIJKLMNOPQRSTUVWXYZ";
function resizeCanvas() {
if (!canvas || !ctx) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
columns = Math.max(1, Math.floor(canvas.width / fontSize));
drops = Array.from({ length: columns }, () => Math.random() * canvas.height / fontSize);
}
function drawMatrix(timestamp = 0) {
if (!canvas || !ctx) return;
if (timestamp - lastFrameTime < frameInterval) {
matrixAnimationId = requestAnimationFrame(drawMatrix);
return;
}
lastFrameTime = timestamp;
ctx.fillStyle = "rgba(3, 8, 15, 0.08)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#00ff88";
ctx.font = `${fontSize}px monospace`;
for (let i = 0; i < drops.length; i++) {
const text = chars[Math.floor(Math.random() * chars.length)];
const x = i * fontSize;
const y = drops[i] * fontSize;
ctx.fillText(text, x, y);
if (y > canvas.height && Math.random() > 0.985) {
drops[i] = 0;
}
drops[i] += 0.65;
}
matrixAnimationId = requestAnimationFrame(drawMatrix);
}
function startLoader(title, message) {
if (!loader) return;
titleEl.textContent = title;
statusText.textContent = message;
loader.classList.add("active");
loader.setAttribute("aria-hidden", "false");
if (!matrixAnimationId) {
resizeCanvas();
lastFrameTime = 0;
drawMatrix();
}
}
function openAccountModal() {
if (!accountModal) return;
accountModal.classList.add("active");
accountModal.setAttribute("aria-hidden", "false");
}
function closeAccountModal() {
if (!accountModal) return;
accountModal.classList.remove("active");
accountModal.setAttribute("aria-hidden", "true");
}
function openUpdateModal() {
if (!updateModal) return;
if (updateDate && !updateDate.value) {
updateDate.value = new Date().toISOString().slice(0, 10);
}
updateModal.classList.add("active");
updateModal.setAttribute("aria-hidden", "false");
}
function closeUpdateModal() {
if (!updateModal) return;
updateModal.classList.remove("active");
updateModal.setAttribute("aria-hidden", "true");
}
function addUpdateItem() {
if (!updateItemsEditor) return;
const firstItem = updateItemsEditor.querySelector(".update-item-form");
if (!firstItem) return;
const clone = firstItem.cloneNode(true);
clone.querySelectorAll("input, textarea").forEach((field) => {
field.value = "";
});
clone.querySelectorAll("select").forEach((field) => {
field.selectedIndex = 0;
});
updateItemsEditor.appendChild(clone);
}
function insertSnippet(textarea, snippet) {
if (!textarea || !snippet) return;
const start = textarea.selectionStart ?? textarea.value.length;
const end = textarea.selectionEnd ?? textarea.value.length;
const before = textarea.value.slice(0, start);
const after = textarea.value.slice(end);
const spacerBefore = before && !before.endsWith("\n") ? "\n" : "";
const spacerAfter = after && !snippet.endsWith("\n") ? "\n" : "";
const insertion = spacerBefore + snippet + spacerAfter;
textarea.value = before + insertion + after;
const cursorPosition = before.length + insertion.length;
textarea.focus();
textarea.setSelectionRange(cursorPosition, cursorPosition);
}
document.addEventListener("click", (event) => {
const button = event.target.closest(".format-btn");
if (!button) return;
const snippet = button.dataset.snippet || "";
const explicitTargetId = button.closest(".format-toolbar")?.dataset.formatFor;
let textarea = explicitTargetId ? document.getElementById(explicitTargetId) : null;
if (!textarea) {
textarea = button.closest(".update-item-text")?.querySelector("textarea");
}
insertSnippet(textarea, snippet);
});
parserForms.forEach((form) => {
form.addEventListener("submit", () => {
const parserTitle = form.dataset.parserTitle || "Запуск парсера";
const parserStatus = form.dataset.parserStatus || "Обработка данных...";
document.querySelectorAll(".action-btn").forEach((btn) => {
btn.disabled = true;
});
form.classList.add("is-loading");
startLoader(`Парсинг: ${parserTitle}`, parserStatus);
});
});
if (openCreateAccountBtn) {
openCreateAccountBtn.addEventListener("click", openAccountModal);
}
if (closeCreateAccountBtn) {
closeCreateAccountBtn.addEventListener("click", closeAccountModal);
}
if (accountModal) {
accountModal.addEventListener("click", (event) => {
if (event.target === accountModal) {
closeAccountModal();
}
});
}
if (openAddUpdateBtn) {
openAddUpdateBtn.addEventListener("click", openUpdateModal);
}
if (closeAddUpdateBtn) {
closeAddUpdateBtn.addEventListener("click", closeUpdateModal);
}
if (updateModal) {
updateModal.addEventListener("click", (event) => {
if (event.target === updateModal) {
closeUpdateModal();
}
});
}
if (addUpdateItemBtn) {
addUpdateItemBtn.addEventListener("click", addUpdateItem);
}
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeAccountModal();
closeUpdateModal();
}
});
const queryParams = new URLSearchParams(window.location.search);
if (queryParams.get("open_update") === "1") {
openUpdateModal();
}
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
</script>
</body>
</html>