в процессе, нет аккордиона, нет графика
This commit is contained in:
@@ -209,11 +209,13 @@ h1 {
|
||||
.accordion-body {
|
||||
max-height: 520px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
transition: max-height 0.3s ease, padding 0.3s ease;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.accordion-body.collapsed {
|
||||
max-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.progress-with-signal {
|
||||
@@ -269,4 +271,15 @@ h1 {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
#signalChart {
|
||||
width: 100%;
|
||||
background: #050805;
|
||||
border: 1px solid #00ff88;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#history {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -59,10 +59,14 @@
|
||||
<div id="history"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card wide">
|
||||
<h3>ГРАФИК СИГНАЛА (последние 1000)</h3>
|
||||
<canvas id="signalChart" height="120"></canvas>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/app.js"></script>
|
||||
<script type="module" src="/js/app.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
19
cmd/server/web/js/accordion.js
Normal file
19
cmd/server/web/js/accordion.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// accordion.js
|
||||
import { DOM } from "./ui.js";
|
||||
|
||||
export function initAccordion() {
|
||||
if (!DOM.historyHeader || !DOM.historyBody) return;
|
||||
|
||||
let open = false;
|
||||
|
||||
DOM.historyBody.classList.add("collapsed");
|
||||
|
||||
DOM.historyHeader.addEventListener("click", () => {
|
||||
open = !open;
|
||||
|
||||
DOM.historyHeader.textContent =
|
||||
(open ? "▼" : "▶") + " ПРИНЯТЫЕ ДАННЫЕ";
|
||||
|
||||
DOM.historyBody.classList.toggle("collapsed", !open);
|
||||
});
|
||||
}
|
||||
@@ -1,148 +1,90 @@
|
||||
// ===== SMOOTH STATE =====
|
||||
let lastValue = 0;
|
||||
import { fetchHealth, fetchHistory } from "./data.js";
|
||||
import { state, smooth, addSignal } from "./state.js";
|
||||
import {
|
||||
initDOM,
|
||||
setStatus,
|
||||
setMeta,
|
||||
setBars,
|
||||
setSignal,
|
||||
DOM
|
||||
} from "./ui.js";
|
||||
|
||||
// ===== ACCORDION =====
|
||||
const historyHeader = document.getElementById("history-header");
|
||||
const historyBody = document.getElementById("history-body");
|
||||
import { initAccordion } from "./accordion.js";
|
||||
import { drawChart } from "./chart.js";
|
||||
|
||||
let accordionOpen = false;
|
||||
let lastChart = 0;
|
||||
|
||||
if (historyBody && historyHeader) {
|
||||
historyBody.classList.add("collapsed");
|
||||
// ================= INIT =================
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
initDOM();
|
||||
initAccordion();
|
||||
});
|
||||
|
||||
historyHeader.addEventListener("click", () => {
|
||||
accordionOpen = !accordionOpen;
|
||||
historyHeader.textContent =
|
||||
(accordionOpen ? "▼" : "▶") + " ПРИНЯТЫЕ ДАННЫЕ";
|
||||
|
||||
historyBody.classList.toggle("collapsed", !accordionOpen);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== PROGRESS =====
|
||||
function setBar(id, value) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
|
||||
value = Math.max(0, Math.min(100, value));
|
||||
el.style.width = value + "%";
|
||||
}
|
||||
|
||||
// ===== SIGNAL =====
|
||||
function setSignal(level) {
|
||||
const bars = document.querySelectorAll(".sig-bar");
|
||||
if (!bars.length) return;
|
||||
|
||||
bars.forEach(b => b.classList.remove("active-1", "active-2", "active-3", "dim"));
|
||||
|
||||
bars.forEach(b => b.classList.add("dim"));
|
||||
|
||||
if (level >= 1) {
|
||||
bars[0].classList.add("active-1");
|
||||
bars[0].classList.remove("dim");
|
||||
}
|
||||
if (level >= 2) {
|
||||
bars[1].classList.add("active-2");
|
||||
bars[1].classList.remove("dim");
|
||||
}
|
||||
if (level >= 3) {
|
||||
bars[2].classList.add("active-3");
|
||||
bars[2].classList.remove("dim");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== SMOOTH =====
|
||||
function smoothBar(target) {
|
||||
const alpha = 0.2;
|
||||
lastValue = lastValue + alpha * (target - lastValue);
|
||||
return lastValue;
|
||||
}
|
||||
|
||||
// ===== MEDIAN =====
|
||||
function median(values) {
|
||||
if (!values || values.length === 0) return 0;
|
||||
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const mid = Math.floor(sorted.length / 2);
|
||||
|
||||
return sorted.length % 2
|
||||
? sorted[mid]
|
||||
: (sorted[mid - 1] + sorted[mid]) / 2;
|
||||
}
|
||||
|
||||
// ===== UPDATE LOOP =====
|
||||
// ================= UPDATE =================
|
||||
async function update() {
|
||||
try {
|
||||
const [r1, r2] = await Promise.all([
|
||||
fetch("/api/health"),
|
||||
fetch("/api/history")
|
||||
const [h, hist] = await Promise.all([
|
||||
fetchHealth(),
|
||||
fetchHistory()
|
||||
]);
|
||||
|
||||
const h = await r1.json();
|
||||
const hist = await r2.json();
|
||||
|
||||
// ===== STATUS =====
|
||||
const statusEl = document.getElementById("status");
|
||||
if (statusEl) statusEl.textContent = h.status || "неизвестно";
|
||||
setStatus(h.status || "неизвестно");
|
||||
|
||||
const filled = h.stats?.filled ?? 0;
|
||||
const arr = Array.isArray(hist.bytes) ? hist.bytes : [];
|
||||
|
||||
const raw = arr[arr.length - 1] ?? 0;
|
||||
|
||||
// ===== DECODE =====
|
||||
const value = raw & 0x3F;
|
||||
const level = (raw >> 6) & 0x3;
|
||||
const percent = (value / 63) * 100;
|
||||
|
||||
// ===== STATE =====
|
||||
addSignal(value);
|
||||
|
||||
// ===== UI =====
|
||||
setBars(smooth(percent));
|
||||
setSignal(level);
|
||||
|
||||
// 🔥 ВОТ ЭТО ТЫ ПОТЕРЯЛ
|
||||
if (DOM.lastByte) {
|
||||
DOM.lastByte.textContent = value;
|
||||
}
|
||||
|
||||
// ===== META =====
|
||||
const uptime = Math.round(h.uptime_sec || 0);
|
||||
|
||||
const filled = h.stats?.filled ?? 0;
|
||||
const bps = Math.round(h.stats?.bits_per_sec ?? 0);
|
||||
const Bps = Math.round(h.stats?.bytes_per_sec ?? 0);
|
||||
|
||||
const meta = document.getElementById("meta");
|
||||
if (meta) {
|
||||
meta.textContent =
|
||||
`работа: ${uptime}s | буфер: ${filled} | ${bps} bit/s (${Bps} B/s)`;
|
||||
}
|
||||
|
||||
// ===== LATEST =====
|
||||
const latest = Number(h.latest || 0);
|
||||
|
||||
const latestEl = document.getElementById("latest");
|
||||
if (latestEl) latestEl.textContent = latest;
|
||||
|
||||
// optional bits view
|
||||
if (typeof renderBits === "function") {
|
||||
renderBits(latest);
|
||||
}
|
||||
setMeta(
|
||||
`работа: ${uptime}s | буфер: ${filled} | ${bps} bit/s (${Bps} B/s)`
|
||||
);
|
||||
|
||||
// ===== HISTORY =====
|
||||
const arr = Array.isArray(hist.bytes) ? hist.bytes : [];
|
||||
|
||||
const historyEl = document.getElementById("history");
|
||||
if (historyEl) {
|
||||
historyEl.innerHTML = arr
|
||||
if (DOM.history) {
|
||||
DOM.history.innerHTML = arr
|
||||
.map((b, i) =>
|
||||
`[${i}] ${(Number(b) & 0xFF).toString(2).padStart(8, "0")} = ${Number(b) & 0xFF}`
|
||||
`[${i}] ${(b & 0xFF).toString(2).padStart(8, "0")} = ${b & 0xFF}`
|
||||
)
|
||||
.join("<br>");
|
||||
}
|
||||
|
||||
// ===== SIGNAL PARSING =====
|
||||
const raw = arr.length ? (arr[arr.length - 1] & 0xFF) : 0;
|
||||
// ===== CHART =====
|
||||
const now = performance.now();
|
||||
|
||||
const barValue = raw & 0x3F; // 0–63
|
||||
const signalLevel = (raw >> 6) & 0x3; // 0–3
|
||||
|
||||
const percent = (barValue / 63) * 100;
|
||||
const smooth = smoothBar(percent);
|
||||
|
||||
setBar("bar1", smooth);
|
||||
setSignal(signalLevel);
|
||||
|
||||
const lastByte = document.getElementById("last-byte");
|
||||
if (lastByte) lastByte.textContent = barValue;
|
||||
if (now - lastChart > 100) {
|
||||
drawChart(state.signalHistory);
|
||||
lastChart = now;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("UPDATE ERROR:", e);
|
||||
|
||||
const status = document.getElementById("status");
|
||||
if (status) status.textContent = "СЕРВЕР НЕДОСТУПЕН";
|
||||
setStatus("СЕРВЕР НЕДОСТУПЕН");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== START =====
|
||||
// ================= LOOP =================
|
||||
setInterval(update, 500);
|
||||
update();
|
||||
33
cmd/server/web/js/chart.js
Normal file
33
cmd/server/web/js/chart.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// chart.js
|
||||
let ctx = null;
|
||||
|
||||
export function drawChart(canvas, history) {
|
||||
if (!canvas || !history?.length) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const stepX = w / 1000;
|
||||
const scaleY = h / 63;
|
||||
|
||||
ctx.strokeStyle = "#00ff88";
|
||||
ctx.beginPath();
|
||||
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
const x = i * stepX;
|
||||
const y = h - history[i].value * scaleY; // 👈 FIX
|
||||
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
}
|
||||
10
cmd/server/web/js/data.js
Normal file
10
cmd/server/web/js/data.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// data.js (API слой)
|
||||
export async function fetchHealth() {
|
||||
const r = await fetch("/api/health");
|
||||
return r.json();
|
||||
}
|
||||
|
||||
export async function fetchHistory() {
|
||||
const r = await fetch("/api/history");
|
||||
return r.json();
|
||||
}
|
||||
21
cmd/server/web/js/state.js
Normal file
21
cmd/server/web/js/state.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// state.js (вся логика состояния)
|
||||
export const state = {
|
||||
maxPoints: 1000,
|
||||
signalHistory: [],
|
||||
smoothValue: 0,
|
||||
};
|
||||
|
||||
// сглаживание
|
||||
export function smooth(target) {
|
||||
state.smoothValue += 0.2 * (target - state.smoothValue);
|
||||
return state.smoothValue;
|
||||
}
|
||||
|
||||
// добавление в историю
|
||||
export function addSignal(value) {
|
||||
state.signalHistory.push(value);
|
||||
|
||||
if (state.signalHistory.length > state.maxPoints) {
|
||||
state.signalHistory.shift();
|
||||
}
|
||||
}
|
||||
56
cmd/server/web/js/ui.js
Normal file
56
cmd/server/web/js/ui.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// ui.js (DOM слой)
|
||||
export let DOM = {};
|
||||
|
||||
export function initDOM() {
|
||||
DOM.status = document.getElementById("status");
|
||||
DOM.meta = document.getElementById("meta");
|
||||
DOM.latest = document.getElementById("latest");
|
||||
DOM.lastByte = document.getElementById("last-byte");
|
||||
DOM.history = document.getElementById("history");
|
||||
DOM.canvas = document.getElementById("signalChart");
|
||||
|
||||
DOM.bars = document.querySelectorAll(".sig-bar");
|
||||
}
|
||||
|
||||
export function setStatus(v) {
|
||||
if (DOM.status) DOM.status.textContent = v;
|
||||
}
|
||||
|
||||
export function setMeta(v) {
|
||||
if (DOM.meta) DOM.meta.textContent = v;
|
||||
}
|
||||
|
||||
export function setBars(p) {
|
||||
const el = document.getElementById("bar1");
|
||||
if (!el) return;
|
||||
el.style.width = Math.max(0, Math.min(100, p)) + "%";
|
||||
}
|
||||
|
||||
export function setSignal(level) {
|
||||
const bars = DOM.bars;
|
||||
if (!bars || bars.length < 3) return;
|
||||
|
||||
// reset
|
||||
bars.forEach(b => {
|
||||
b.classList.remove("active-1", "active-2", "active-3");
|
||||
b.classList.add("dim");
|
||||
});
|
||||
|
||||
// 1 bar
|
||||
if (level >= 1) {
|
||||
bars[0].classList.add("active-1");
|
||||
bars[0].classList.remove("dim");
|
||||
}
|
||||
|
||||
// 2 bar
|
||||
if (level >= 2) {
|
||||
bars[1].classList.add("active-2");
|
||||
bars[1].classList.remove("dim");
|
||||
}
|
||||
|
||||
// 3 bar
|
||||
if (level >= 3) {
|
||||
bars[2].classList.add("active-3");
|
||||
bars[2].classList.remove("dim");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user