Files
Uch-Practic/static/dashboard.html

362 lines
14 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 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>