Загрузить файлы в «static»
This commit is contained in:
361
static/dashboard.html
Normal file
361
static/dashboard.html
Normal 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="0–100" 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 = "Введите уровень мощности (0–100)"; 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>
|
||||||
Reference in New Issue
Block a user