Compare commits

...

2 Commits

Author SHA1 Message Date
Maxim
649bb01645 Доработка индикатора уровня сигнала 2026-05-07 15:21:22 +03:00
Maxim
ee1c29c34f промежуточная работа 2026-05-07 14:58:37 +03:00
4 changed files with 135 additions and 46 deletions

View File

@@ -33,9 +33,9 @@ func (a *API) HandleHealth(w http.ResponseWriter, r *http.Request) {
latest, ok := a.buf.GetLatest()
idle := time.Since(a.buf.LastWriteTime())
status := "ok"
status := "На связи"
if idle > 15*time.Second {
status = "degraded"
status = "Нет связи"
}
writeJSON(w, map[string]any{

View File

@@ -196,3 +196,77 @@ h1 {
font-size: 15px;
}
}
.accordion-header {
cursor: pointer;
user-select: none;
}
.accordion-header:hover {
opacity: 0.7;
}
.accordion-body {
max-height: 520px;
overflow: hidden;
transition: max-height 0.3s ease;
}
.accordion-body.collapsed {
max-height: 0;
}
.progress-with-signal {
display: flex;
align-items: center;
gap: 0;
}
.progress-90 {
flex: 0 0 86%;
margin-right: 10px;
}
/* ===== SIGNAL ICON ===== */
.signal-icon {
display: flex;
align-items: flex-end;
gap: 4px;
width: 34px;
height: 34px;
flex-shrink: 0;
}
.sig-bar {
flex: 1;
border-radius: 2px;
min-width: 5px;
border: 1.5px solid #00ff88;
background-color: transparent;
transition: background-color 0.3s, border-color 0.3s;
}
.sig-bar-1 { height: 33%; }
.sig-bar-2 { height: 66%; }
.sig-bar-3 { height: 100%; }
/* Активные — зелёная заливка */
.sig-bar.active-1,
.sig-bar.active-2,
.sig-bar.active-3,
.sig-bar.active-4 {
background-color: #00ff88;
border-color: #00ff88;
}
/* Неактивные — прозрачные, только контур */
.sig-bar.dim {
background-color: transparent;
border-color: #1a3a1a;
}
#last-byte {
font-size: 18px;
font-weight: bold;
color: #00ff88;
}

View File

@@ -21,19 +21,20 @@
<div id="meta"></div>
</div>
<div class="card">
<h3>ПОСЛЕДНИЙ СИГНАЛ</h3>
<div id="latest">--</div>
<div id="bits"></div>
</div>
<div class="card wide">
<h3>УРОВНИ КАНАЛОВ</h3>
<div class="progress-row">
<div class="progress-label">Канал 1</div>
<div class="progress">
<div class="fill" id="bar1"></div>
<div class="progress-label">Канал 1<span id="last-byte">0</span></div>
<div class="progress-with-signal">
<div class="progress progress-90">
<div class="fill" id="bar1"></div>
</div>
<div class="signal-icon" id="signal-icon" aria-label="Уровень сигнала" role="img">
<div class="sig-bar sig-bar-1"></div>
<div class="sig-bar sig-bar-2"></div>
<div class="sig-bar sig-bar-3"></div>
</div>
</div>
</div>
@@ -53,8 +54,10 @@
</div>
<div class="card wide">
<h3>ПОТОК ДАННЫХ (последние 32)</h3>
<div id="history"></div>
<h3 class="accordion-header" id="history-header">▶ ПРИНЯТЫЕ ДАННЫЕ</h3>
<div class="accordion-body" id="history-body">
<div id="history"></div>
</div>
</div>
</div>

View File

@@ -1,19 +1,14 @@
// ===== BIT RENDER =====
function renderBits(byte) {
const bits = document.getElementById("bits");
bits.innerHTML = "";
// ===== ACCORDION =====
const historyHeader = document.getElementById("history-header");
const historyBody = document.getElementById("history-body");
let accordionOpen = false;
historyBody.classList.add("collapsed"); // изначально свёрнут
byte = Number(byte || 0);
for (let i = 7; i >= 0; i--) {
const v = (byte >> i) & 1;
const d = document.createElement("div");
d.className = "bit" + (v ? " on" : "");
d.textContent = v;
bits.appendChild(d);
}
}
historyHeader.addEventListener("click", () => {
accordionOpen = !accordionOpen;
historyHeader.textContent = (accordionOpen ? "▼" : "▶") + " ПРИНЯТЫЕ ДАННЫЕ";
historyBody.classList.toggle("collapsed", !accordionOpen);
});
// ===== PROGRESS BAR =====
function setBar(id, value) {
@@ -21,6 +16,26 @@ function setBar(id, value) {
document.getElementById(id).style.width = value + "%";
}
// ===== SIGNAL ICON (0-3 уровня) =====
function setSignal(level) {
const bars = document.querySelectorAll(".sig-bar");
bars.forEach(b => {
b.classList.remove("active-1", "active-2", "active-3", "active-4");
b.classList.add("dim");
});
if (level >= 3) {
bars[0].classList.add("active-1"); bars[0].classList.remove("dim");
bars[1].classList.add("active-2"); bars[1].classList.remove("dim");
bars[2].classList.add("active-3"); bars[2].classList.remove("dim");
} else if (level >= 2) {
bars[0].classList.add("active-1"); bars[0].classList.remove("dim");
bars[1].classList.add("active-2"); bars[1].classList.remove("dim");
} else if (level >= 1) {
bars[0].classList.add("active-1"); bars[0].classList.remove("dim");
}
}
// ===== SMOOTHING =====
let lastValue = 0;
@@ -58,13 +73,7 @@ async function update() {
const uptime = Math.round(h.uptime_sec || 0);
document.getElementById("meta").textContent =
"аптайм: " + uptime + "s | буфер: " + filled;
// ===== LATEST =====
const latest = Number(h.latest || 0);
document.getElementById("latest").textContent = latest;
renderBits(latest);
"работа: " + uptime + "s | буфер: " + filled;
// ===== HISTORY =====
const arr = Array.isArray(hist.bytes) ? hist.bytes : [];
@@ -76,26 +85,29 @@ async function update() {
" = значение: " + (Number(b) & 0xFF)
).join("<br>");
// ===== FILTER (последние 9 значений) =====
const last = arr.slice(-9).map(v => Number(v) & 0xFF);
// ===== ПОСЛЕДНЕЕ ЗНАЧЕНИЕ (без фильтра) =====
const raw = arr.length > 0 ? (arr[arr.length - 1] & 0xFF) : 0;
const filtered = median(last);
// ===== ПАРСИНГ БИТ =====
const barValue = raw & 0x3F; // биты 0-5 (0-63)
const signalLevel = (raw >> 6) & 0x3; // биты 6-7 (0-3)
// ===== NORMALIZE =====
const percent = (filtered / 255) * 100;
// ===== SMOOTH =====
// ===== PROGRESS BAR (0-63 -> 0-100%) =====
const percent = (barValue / 63) * 100;
const smooth = smoothBar(percent);
// ===== ONLY CHANNEL 1 =====
setBar("bar1", smooth);
// ===== SIGNAL ICON =====
setSignal(signalLevel, raw);
// ===== LAST BYTE =====
document.getElementById("last-byte").textContent = barValue;
} catch (e) {
console.error("UPDATE ERROR:", e);
document.getElementById("status").textContent = "СЕРВЕР НЕДОСТУПЕН";
}
}
// быстрее и плавнее
setInterval(update, 500);
update();
update();