Загрузить файлы в «static»

This commit is contained in:
2026-07-02 14:25:50 +00:00
parent f5f2f9e3f9
commit f594ccbab3

361
static/dashboard.html Normal file
View File

@@ -0,0 +1,361 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Robo Arena — Арена управления</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body class="page-app page-dashboard">
<nav class="topbar">
<div class="topbar-brand">
<span class="logo-icon">🤖</span>
<span>Robo Arena</span> / РСУ
</div>
<span class="topbar-pilot" id="topbar-pilot"></span>
<span class="topbar-user" id="topbar-email"></span>
<a class="btn btn-sm" href="/static/profile.html">Профиль</a>
<button class="btn btn-sm" id="logout-btn">Выйти</button>
</nav>
<div class="main">
<header class="arena-hero">
<div class="arena-hero-glow"></div>
<div class="arena-hero-content">
<div class="arena-hero-text">
<span class="hero-tag">Система управления · LIVE</span>
<h2>Арена боевых роботов</h2>
<p>Удалённое управление, мониторинг и телеметрия ваших боевых единиц</p>
</div>
<div class="arena-stats-row">
<div class="stat-card">
<div class="stat-icon">🤖</div>
<div class="stat-data">
<div class="num" id="stat-total">0</div>
<div class="lbl">Роботов</div>
</div>
</div>
<div class="stat-card stat-card--green">
<div class="stat-icon"></div>
<div class="stat-data">
<div class="num green" id="stat-active">0</div>
<div class="lbl">В бою</div>
</div>
</div>
<div class="stat-card stat-card--red">
<div class="stat-icon">🔧</div>
<div class="stat-data">
<div class="num red" id="stat-damaged">0</div>
<div class="lbl">Повреждено</div>
</div>
</div>
</div>
</div>
</header>
<div class="profile-banner" id="profile-banner" style="display:none">
<div class="profile-banner-icon">👤</div>
<div class="profile-banner-text">
<strong>Профиль пилота не заполнен</strong>
<p>Добавьте ФИО и контактные данные, чтобы участвовать в турнирах арены</p>
</div>
<a class="btn btn-sm btn-cyan" href="/static/profile.html">Заполнить профиль</a>
</div>
<section class="panel deploy-panel">
<div class="panel-header">
<div class="panel-header-icon"></div>
<div>
<h3 class="panel-title">Развернуть робота</h3>
<p class="panel-desc">Добавьте новую боевую единицу на арену</p>
</div>
</div>
<div class="deploy-form">
<div class="input-wrap input-wrap--wide">
<input type="text" id="new-name" placeholder="Thunder-X7, Iron Claw, Steel Fury…">
</div>
<button class="btn btn-primary btn-deploy" id="add-btn">
<span>Развернуть</span>
</button>
</div>
</section>
<div class="section-head">
<span class="section-title">
<span class="section-icon">🛡</span>
Мои роботы
</span>
<span class="section-count" id="obj-count"></span>
</div>
<div class="cards-grid" id="cards-grid"></div>
<p class="err-msg err-msg--block" id="error-text"></p>
</div>
<script>
const grid = document.getElementById("cards-grid");
const errEl = document.getElementById("error-text");
const objCount = document.getElementById("obj-count");
const STATUS_LABEL = { on: "В БОЮ", off: "СТОЯТ", error: "ПОВРЕЖДЁН" };
const ROBOT_EMOJIS = ["🤖", "🦾", "⚙️", "🔩", "🛡", "⚡"];
function esc(s) {
const d = document.createElement("div");
d.textContent = s;
return d.innerHTML;
}
function statusCls(s) {
if (s === "on") return "is-on";
if (s === "error") return "is-err";
return "is-off";
}
function fmtDate(s) {
return s ? s.replace("T", " ").slice(0, 16) : "";
}
function robotEmoji(id) {
return ROBOT_EMOJIS[id % ROBOT_EMOJIS.length];
}
function updateStats(objects) {
document.getElementById("stat-total").textContent = objects.length;
document.getElementById("stat-active").textContent = objects.filter(o => o.status === "on").length;
document.getElementById("stat-damaged").textContent = objects.filter(o => o.status === "error").length;
}
async function loadMe() {
const res = await fetch("/api/me", { credentials: "include" });
if (!res.ok) { window.location.href = "/static/login.html"; return; }
const u = await res.json();
document.getElementById("topbar-email").textContent = u.email;
if (u.full_name) {
document.getElementById("topbar-pilot").textContent = "⚔ " + u.full_name;
}
if (!u.profile_complete) {
document.getElementById("profile-banner").style.display = "flex";
}
}
async function loadObjects() {
errEl.textContent = "";
try {
const res = await fetch("/api/objects", { credentials: "include" });
if (!res.ok) throw new Error();
const data = await res.json();
renderCards(data.objects);
updateStats(data.objects);
} catch {
errEl.textContent = "Не удалось загрузить список роботов";
}
}
function renderCards(objects) {
grid.innerHTML = "";
objCount.textContent = objects.length ? `${objects.length} ед.` : "";
if (objects.length === 0) {
grid.innerHTML = `<div class="empty">
<div class="empty-icon">🤖</div>
<strong>Арена пуста</strong>
<span>Разверните первого боевого робота с помощью формы выше</span>
</div>`;
return;
}
objects.forEach(obj => {
const cls = statusCls(obj.status);
const card = document.createElement("div");
card.className = "obj-card";
card.dataset.id = obj.id;
const powerPct = Math.min(100, Math.max(0, Math.abs(obj.value)));
const label = STATUS_LABEL[obj.status] || obj.status.toUpperCase();
card.innerHTML = `
<div class="obj-card-top ${cls}"></div>
<div class="obj-card-body ${cls}">
<div class="obj-head">
<div class="robot-avatar">${robotEmoji(obj.id)}</div>
<div class="obj-info">
<span class="obj-name" title="${esc(obj.name)}">${esc(obj.name)}</span>
<span class="obj-meta">${fmtDate(obj.updated_at)}</span>
</div>
<span class="status-badge ${cls}">
<span class="status-dot ${cls}"></span>
${esc(label)}
</span>
</div>
<div class="power-section">
<div class="power-header">
<span>Мощность</span>
<span class="power-value">${obj.value}%</span>
</div>
<div class="power-bar-wrap">
<div class="power-bar" style="width:${powerPct}%"></div>
</div>
</div>
<div class="obj-controls">
<div class="controls-row controls-row--primary">
<button class="btn btn-sm btn-success" data-action="on">⚔ В бой</button>
<button class="btn btn-sm btn-ghost" data-action="off">■ Стоп</button>
</div>
<div class="controls-row controls-row--secondary">
<div class="input-wrap input-wrap--compact">
<input type="number" step="any" min="0" max="100" placeholder="0100" data-role="val-input">
<span class="input-suffix">%</span>
</div>
<button class="btn btn-sm btn-ghost" data-action="set">Задать</button>
<button class="btn btn-sm btn-ghost" data-action="history">📋 Журнал</button>
<button class="btn btn-sm btn-danger" data-action="delete">✕</button>
</div>
</div>
</div>
<div class="history-panel" data-role="history-panel" style="display:none">
<div class="history-panel-title">Журнал команд</div>
</div>
`;
card.querySelector('[data-action="on"]').addEventListener("click",
() => sendCmd(obj.id, "on", obj.value));
card.querySelector('[data-action="off"]').addEventListener("click",
() => sendCmd(obj.id, "off", obj.value));
card.querySelector('[data-action="set"]').addEventListener("click", () => {
const v = parseFloat(card.querySelector('[data-role="val-input"]').value);
if (isNaN(v)) { errEl.textContent = "Введите уровень мощности (0100)"; return; }
sendCmd(obj.id, obj.status, v);
});
card.querySelector('[data-action="history"]').addEventListener("click",
() => toggleHistory(obj.id, card));
card.querySelector('[data-action="delete"]').addEventListener("click",
() => deleteObj(obj.id));
const valInput = card.querySelector('[data-role="val-input"]');
valInput.addEventListener("input", function() {
let value = this.value;
if (value === '') return;
let num = parseFloat(value);
if (isNaN(num)) {
this.value = '';
return;
}
if (num < 0) this.value = 0;
if (num > 100) this.value = 100;
});
grid.appendChild(card);
});
}
document.getElementById("add-btn").addEventListener("click", async () => {
errEl.textContent = "";
const nameEl = document.getElementById("new-name");
const name = nameEl.value.trim();
if (!name) { errEl.textContent = "Введите имя робота"; return; }
try {
const res = await fetch("/api/objects", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ name })
});
if (!res.ok) {
const d = await res.json();
errEl.textContent = d.detail || "Не удалось развернуть робота";
return;
}
nameEl.value = "";
loadObjects();
} catch { errEl.textContent = "Не удалось связаться с сервером"; }
});
document.getElementById("new-name").addEventListener("keydown", e => {
if (e.key === "Enter") document.getElementById("add-btn").click();
});
async function sendCmd(id, status, value) {
errEl.textContent = "";
try {
const res = await fetch(`/api/objects/${id}/command`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ status, value })
});
if (!res.ok) {
const d = await res.json();
errEl.textContent = d.detail || "Не удалось отправить команду";
return;
}
loadObjects();
} catch { errEl.textContent = "Не удалось связаться с сервером"; }
}
async function deleteObj(id) {
errEl.textContent = "";
try {
const res = await fetch(`/api/objects/${id}`, {
method: "DELETE",
credentials: "include"
});
if (!res.ok) {
const d = await res.json();
errEl.textContent = d.detail || "Не удалось снять робота с арены";
return;
}
loadObjects();
} catch { errEl.textContent = "Не удалось связаться с сервером"; }
}
async function toggleHistory(id, card) {
const panel = card.querySelector('[data-role="history-panel"]');
if (panel.style.display === "block") { panel.style.display = "none"; return; }
panel.innerHTML = `<div class="history-panel-title">Журнал команд</div>
<div class="history-loading">Загружаем…</div>`;
panel.style.display = "block";
try {
const res = await fetch(`/api/objects/${id}/history`, { credentials: "include" });
const data = await res.json();
if (!data.history.length) {
panel.innerHTML = `<div class="history-panel-title">Журнал команд</div>
<div class="history-empty">Записей пока нет</div>`;
return;
}
panel.innerHTML = `<div class="history-panel-title">Журнал команд</div>` +
data.history.map(h => `
<div class="history-row">
<span class="history-src ${h.source === 'external' ? 'ext' : ''}">${h.source === 'external' ? 'арена' : 'пилот'}</span>
<span class="history-status">${esc(STATUS_LABEL[h.status] || h.status)}</span>
<span class="history-val">${h.value}%</span>
<span class="history-time">${fmtDate(h.created_at)}</span>
</div>
`).join("");
} catch {
panel.innerHTML = `<div class="history-panel-title">Журнал команд</div>
<div class="history-empty history-empty--err">Не удалось загрузить журнал</div>`;
}
}
document.getElementById("logout-btn").addEventListener("click", async () => {
await fetch("/api/logout", { method: "POST", credentials: "include" });
window.location.href = "/static/index.html";
});
loadMe();
loadObjects();
</script>
</body>
</html>