diff --git a/cmd/server/main.go b/cmd/server/main.go index 9368ae8..c706c66 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -435,7 +435,7 @@ func (a *API) HandleLatest(w http.ResponseWriter, r *http.Request) { } func (a *API) HandleHistory(w http.ResponseWriter, r *http.Request) { - raw := a.buf.GetLast(100) + raw := a.buf.GetLast(300) out := make([]int, len(raw)) for i, v := range raw { diff --git a/cmd/server/web/css/cards.css b/cmd/server/web/css/cards.css index c783da2..c965186 100644 --- a/cmd/server/web/css/cards.css +++ b/cmd/server/web/css/cards.css @@ -94,10 +94,9 @@ padding-right: 4px; margin: 0; letter-spacing: 0.02em; - overscroll-behavior: contain; /* ← ДОБАВИТЬ ЭТО */ + overscroll-behavior: contain; -webkit-overflow-scrolling: touch; } - #history::-webkit-scrollbar { width: 6px; height: 6px; @@ -156,15 +155,17 @@ .channels-toggle-all:active { transform: scale(0.95); } + +/* ===== СЕТКА КАНАЛОВ ===== */ .channels-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; - overscroll-behavior: contain; max-height: 500px; overflow-y: auto; overflow-x: visible; -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; padding-right: 4px; } .channels-grid::-webkit-scrollbar { @@ -256,8 +257,6 @@ overflow: hidden; box-shadow: inset 0 2px 4px rgba(0,0,0,0.6); } - -/* ===== ЯРКИЙ КОНТРАСТНЫЙ ПРОГРЕСС-БАР ===== */ .channel-progress .progress .fill { height: 100%; background: #e6852d; @@ -268,14 +267,11 @@ inset 0 1px 2px rgba(255,255,255,0.3), 0 0 15px rgba(255, 165, 0, 0.5); } - -/* Активный канал - более яркий */ .channel-item.channel-active .fill { box-shadow: inset 0 1px 2px rgba(255,255,255,0.4), 0 0 25px rgba(255, 165, 0, 0.7); } - .channel-stats { font-size: var(--font-size-xs); color: #666; @@ -288,7 +284,6 @@ .progress-row { margin-bottom: 16px; } - .progress-label { font-family: var(--font-mono); font-size: var(--font-size-base); @@ -297,7 +292,6 @@ opacity: 0.9; letter-spacing: 0.03em; } - .progress { width: 100%; height: 34px; @@ -308,19 +302,15 @@ overflow: hidden; box-shadow: inset 0 2px 4px rgba(0,0,0,0.4); } - .progress-with-signal { display: flex; align-items: center; gap: 0; } - .progress-90 { flex: 0 0 86%; margin-right: 10px; } - -/* ЯРКИЙ ЗАПОЛНЕННЫЙ БАР */ .fill { height: 100%; width: 0%; @@ -331,7 +321,6 @@ 0 0 12px rgba(230,133,45,0.5); transition: width 0.25s linear; } - .fill.no-connection { background: #555555 !important; box-shadow: none !important; @@ -347,7 +336,6 @@ border: 2px solid #444; border-radius: 4px; } - .card:has(#bar2) .fill, .card:has(#bar3) .fill { background: #e6852d; @@ -365,7 +353,6 @@ height: 34px; flex-shrink: 0; } - .sig-bar { flex: 1; border-radius: 2px; @@ -374,37 +361,73 @@ 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.dim { background-color: transparent; border-color: #1a3a1a; } - .sig-bar.active-1, .sig-bar.active-2, .sig-bar.active-3 { background-color: #e6852d; border-color: #c46b1f; } - .sig-bar.no-connection { background-color: #555555 !important; border-color: #555555 !important; opacity: 0.3 !important; } -/* ===== АДАПТИВ ===== */ +/* ===== АНИМАЦИЯ ===== */ +.channel-item.updating .channel-value { + animation: pulse-value 0.15s ease; +} +@keyframes pulse-value { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.2); color: #ff0; } +} + +/* ===== КАРТОЧКИ НА ДЕСКТОПЕ ===== */ +@media (min-width: 769px) { + .card { + max-height: 100%; + overflow: visible; + } + .channels-card { + max-height: 600px; + overflow: visible; + display: flex; + flex-direction: column; + } + .channels-card .channels-grid { + max-height: 500px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + .history-card { + max-height: 500px; + overflow: visible; + display: flex; + flex-direction: column; + } + .history-card #history { + max-height: 400px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } +} + +/* ===== МОБИЛЬНАЯ ВЕРСИЯ ===== */ @media (max-width: 768px) { .channels-grid { grid-template-columns: 1fr; gap: 4px; - max-height: none; - overflow-y: visible; - overscroll-behavior: none; + max-height: 400px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; } .channels-header { flex-direction: column; @@ -420,29 +443,18 @@ .channel-value { font-size: var(--font-size-sm); } - .channel-progress .progress { - height: 10px; - padding: 1px; - border-width: 2px; - } #history { max-height: 300px; } - .card:has(#bar2) .progress, - .card:has(#bar3) .progress { - height: 28px; - padding: 4px; - border-width: 2px; - } - .progress { - height: 28px; - padding: 4px; - border-width: 2px; + .history-card #history { + max-height: 280px; + overflow-y: auto; } } @media (max-width: 480px) { .channels-grid { + max-height: 350px; gap: 3px; } .channel-header { @@ -458,73 +470,7 @@ max-height: 200px; font-size: var(--font-size-xs); } - .card:has(#bar2) .progress, - .card:has(#bar3) .progress { - height: 24px; - padding: 3px; - border-width: 2px; - } - .progress { - height: 24px; - padding: 3px; - border-width: 2px; - } -} - -/* ===== АНИМАЦИЯ ===== */ -.channel-item.updating .channel-value { - animation: pulse-value 0.15s ease; -} -@keyframes pulse-value { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.2); color: #ff0; } -} - -/* Карточки на десктопе */ -@media (min-width: 769px) { - .card { - max-height: 100%; - overflow: visible; - } - .channels-card { - max-height: 600px; - overflow: hidden; - } - .channels-card .channels-grid { - max-height: 500px; - overflow-y: auto; - } - .history-card { - max-height: 500px; - overflow: hidden; - } .history-card #history { - max-height: 400px; - overflow-y: auto; - } -} - -@media (max-width: 768px) { - .card { - overflow: visible !important; - height: auto !important; - min-height: unset; - max-height: none !important; - } - .channels-card { - max-height: none !important; - overflow: visible !important; - } - .channels-card .channels-grid { - max-height: none !important; - overflow-y: visible !important; - } - .history-card { - max-height: none !important; - overflow: visible !important; - } - .history-card #history { - max-height: 300px; - overflow-y: auto; + max-height: 180px; } } \ No newline at end of file diff --git a/cmd/server/web/dashboard.html b/cmd/server/web/dashboard.html index 2c78524..2b9e427 100644 --- a/cmd/server/web/dashboard.html +++ b/cmd/server/web/dashboard.html @@ -25,8 +25,8 @@

G-Port Warden

Журнал -
--:--:--
🔇
+
--:--:--
diff --git a/cmd/server/web/js/app.ts b/cmd/server/web/js/app.ts index 5b886cb..664be08 100644 --- a/cmd/server/web/js/app.ts +++ b/cmd/server/web/js/app.ts @@ -1,8 +1,8 @@ // app.ts - Оркестратор приложения import { - fetchHealth, - fetchHistory + fetchHealth, + fetchHistory } from "./data.js"; import { state, @@ -22,6 +22,7 @@ import { setAudioIndicator, DOM, setChannelValue, + setChannelCurrentValue, channelElements } from "./ui.js"; import { initAccordion } from "./accordion.js"; @@ -159,7 +160,8 @@ async function update(): Promise { // ===== НАХОДИМ МАКСИМУМ ИЗ БУФЕРА ===== let maxSignal = 0; let maxLevel = 0; - let lastSignal = 0; + let currentSignal = 0; + let currentLevel = 0; if (isOnline && arr.length > 0) { for (const byte of arr) { @@ -174,7 +176,8 @@ async function update(): Promise { } } - lastSignal = raw & 0x3F; + currentSignal = raw & 0x3F; + currentLevel = (raw >> 6) & 0x3; } const percent = (maxSignal / 63) * 100; @@ -182,9 +185,11 @@ async function update(): Promise { // ===== ОБНОВЛЯЕМ КАНАЛЫ ===== if (isOnline && arr.length > 0) { setChannelValue(0, maxSignal, maxLevel); + setChannelCurrentValue(0, currentSignal, currentLevel); for (let i = 1; i < 8; i++) { setChannelValue(i, 0, 0); + setChannelCurrentValue(i, 0, 0); if (channelElements[i]) { channelElements[i].item.classList.remove('channel-active'); } @@ -195,11 +200,12 @@ async function update(): Promise { } if (DOM.lastByte) { - DOM.lastByte.textContent = lastSignal.toString(); + DOM.lastByte.textContent = currentSignal.toString(); } } else { for (let i = 0; i < 8; i++) { setChannelValue(i, 0, 0); + setChannelCurrentValue(i, 0, 0); if (channelElements[i]) { channelElements[i].item.classList.remove('channel-active'); } @@ -228,10 +234,22 @@ async function update(): Promise { setMeta(`Работа: ${uptime}s | Буфер: ${filled} | Приём: ${Bps} B/s`); + // ===== ИСТОРИЯ: НОВЫЕ СВЕРХУ ===== if (DOM.history) { - DOM.history.innerHTML = arr - .map((b, i) => `[${i}] ${(b & 0xFF).toString(2).padStart(8, "0")} = ${b & 0xFF}`) - .join("
"); + if (arr.length === 0) { + DOM.history.innerHTML = 'Нет данных'; + } else { + // Разворачиваем массив для отображения новых сверху + const reversed = [...arr].reverse(); + DOM.history.innerHTML = reversed + .map((b, i) => { + const originalIndex = arr.length - 1 - i; + // Форматируем: индекс, бинарное представление, десятичное значение + const binary = (b & 0xFF).toString(2).padStart(8, "0"); + return `[${originalIndex}] ${binary} = ${b & 0xFF}`; + }) + .join("
"); + } } const now = performance.now(); @@ -257,6 +275,10 @@ async function update(): Promise { if (DOM.lastByte) DOM.lastByte.textContent = "--"; + if (DOM.history) { + DOM.history.innerHTML = "Ошибка загрузки данных"; + } + if (consecutiveErrors >= MAX_ERRORS && pollTimer) { clearInterval(pollTimer); } diff --git a/cmd/server/web/js/logs.ts b/cmd/server/web/js/logs.ts index 3b83663..c6e4e2f 100644 --- a/cmd/server/web/js/logs.ts +++ b/cmd/server/web/js/logs.ts @@ -556,13 +556,13 @@ function initDataPaginationControls(): void {
- Страница 1 из 1 + Страница 1 из 1
`; @@ -618,13 +618,13 @@ function initEventsPaginationControls(): void {
- Страница 1 из 1 + Страница 1 из 1
`; diff --git a/cmd/server/web/js/state.ts b/cmd/server/web/js/state.ts index 1d119fa..5267843 100644 --- a/cmd/server/web/js/state.ts +++ b/cmd/server/web/js/state.ts @@ -1,4 +1,4 @@ -// state.ts - Peak Holder с сбросом до текущего максимума +// state.ts - Peak Holder с фиксированным временем удержания interface AppState { maxPoints: number; @@ -10,12 +10,14 @@ interface AppState { currentValue: number; // Текущий максимум из буфера peakTimer: number | null; // Таймер для сброса пика амплитуды peakHoldTime: number; // Время удержания пика в миллисекундах + peakLocked: boolean; // Заблокирован ли пик (не сбрасывается до таймера) // Peak Holder для уровня сигнала peakSignalLevel: number; - currentSignalLevel: number; // Текущий максимум уровня из буфера + currentSignalLevel: number; signalPeakTimer: number | null; signalPeakHoldTime: number; + signalPeakLocked: boolean; hasPeak: boolean; lastMaxSignal: number; @@ -32,11 +34,13 @@ export const state: AppState = { currentValue: 0, peakTimer: null, peakHoldTime: 3000, + peakLocked: false, peakSignalLevel: 0, currentSignalLevel: 0, signalPeakTimer: null, signalPeakHoldTime: 3000, + signalPeakLocked: false, hasPeak: false, lastMaxSignal: 0, @@ -67,14 +71,22 @@ export function updatePeakHolder(currentMax: number): number { state.currentValue = currentMax; state.lastMaxSignal = currentMax; - if (currentMax >= state.peakValue) { + // Если пик заблокирован - ничего не меняем, только обновляем currentValue + if (state.peakLocked) { + return state.peakValue; + } + + // Если новое значение БОЛЬШЕ текущего пика - обновляем пик и блокируем + if (currentMax > state.peakValue) { state.peakValue = currentMax; state.hasPeak = true; + state.peakLocked = true; scheduleAmplitudePeakRelease(); notifyPeakHolderChange(); return state.peakValue; } + // Если новое значение МЕНЬШЕ или РАВНО - ничего не делаем return state.peakValue; } @@ -86,9 +98,14 @@ export function updatePeakHolder(currentMax: number): number { export function updateSignalPeakHolder(currentMaxLevel: number): number { state.currentSignalLevel = currentMaxLevel; - if (currentMaxLevel >= state.peakSignalLevel) { + if (state.signalPeakLocked) { + return state.peakSignalLevel; + } + + if (currentMaxLevel > state.peakSignalLevel) { state.peakSignalLevel = currentMaxLevel; state.hasPeak = true; + state.signalPeakLocked = true; scheduleSignalPeakRelease(); notifyPeakHolderChange(); return state.peakSignalLevel; @@ -108,9 +125,11 @@ function scheduleAmplitudePeakRelease(): void { } state.peakTimer = window.setTimeout(() => { + // Сбрасываем пик до текущего значения state.peakValue = state.currentValue; state.peakTimer = null; state.hasPeak = false; + state.peakLocked = false; notifyPeakHolderChange(); }, state.peakHoldTime); } @@ -125,6 +144,7 @@ function scheduleSignalPeakRelease(): void { state.peakSignalLevel = state.currentSignalLevel; state.signalPeakTimer = null; state.hasPeak = false; + state.signalPeakLocked = false; notifyPeakHolderChange(); }, state.signalPeakHoldTime); } @@ -161,6 +181,7 @@ export function forceResetSignalPeak(): void { state.currentSignalLevel = 0; state.hasPeak = false; state.lastMaxSignal = 0; + state.signalPeakLocked = false; notifyPeakHolderChange(); } @@ -173,6 +194,7 @@ export function forceResetPeak(): void { state.currentValue = 0; state.hasPeak = false; state.lastMaxSignal = 0; + state.peakLocked = false; forceResetSignalPeak(); notifyPeakHolderChange(); } diff --git a/cmd/server/web/js/ui.ts b/cmd/server/web/js/ui.ts index 58bc8d6..ff5d57d 100644 --- a/cmd/server/web/js/ui.ts +++ b/cmd/server/web/js/ui.ts @@ -169,14 +169,18 @@ export function setHeightLevel(level: number): void { // ===== 8 КАНАЛОВ ===== export interface ChannelState { - value: number; - strength: number; + value: number; // Максимальное значение (для прогресс-бара) + currentValue: number; // Текущее значение (для отображения) + strength: number; // Максимальный уровень + currentStrength: number; // Текущий уровень expanded: boolean; } export const channels: ChannelState[] = Array(8).fill(null).map(() => ({ value: 0, + currentValue: 0, strength: 0, + currentStrength: 0, expanded: true })); @@ -260,6 +264,10 @@ export function getChannelElement(index: number) { return channelElements[index] || null; } +/** + * Установка МАКСИМАЛЬНОГО значения для прогресс-бара + * (используется для отображения пика) + */ export function setChannelValue(index: number, value: number, strength: number) { if (index < 0 || index > 7) return; @@ -273,6 +281,7 @@ export function setChannelValue(index: number, value: number, strength: number) const maxValue = 63; const percent = (value / maxValue) * 100; + // Обновляем прогресс-бар if (el.bar) { el.bar.style.width = `${Math.min(100, percent)}%`; el.bar.style.background = '#e6852d'; @@ -280,10 +289,7 @@ export function setChannelValue(index: number, value: number, strength: number) el.bar.style.backgroundImage = 'none'; } - if (el.value) { - el.value.textContent = value.toString(); - } - + // Обновляем индикатор сигнала (уровень) const bars = el.signal?.querySelectorAll('.sig-bar'); if (bars) { bars.forEach((bar: Element, i: number) => { @@ -292,9 +298,10 @@ export function setChannelValue(index: number, value: number, strength: number) }); } + // Обновляем статистику const strengthNames = ['Отсутствует', 'Низкая', 'Средняя', 'Высокая']; if (el.stats) { - el.stats.textContent = `${value}/${maxValue} | ${strengthNames[strength] || '---'}`; + el.stats.textContent = `${ch.currentValue}/${maxValue} | ${strengthNames[ch.currentStrength] || '---'} (пик: ${value})`; } if (el.item) { @@ -302,9 +309,53 @@ export function setChannelValue(index: number, value: number, strength: number) } } +/** + * Установка ТЕКУЩЕГО значения для отображения в цифрах + * (используется для показа реального состояния) + */ +export function setChannelCurrentValue(index: number, value: number, strength: number) { + if (index < 0 || index > 7) return; + + const ch = channels[index]; + const el = channelElements[index]; + if (!el) return; + + ch.currentValue = value; + ch.currentStrength = strength; + + // Обновляем числовое значение + if (el.value) { + el.value.textContent = value.toString(); + } + + // Обновляем статистику (показываем текущее значение и пик) + const maxValue = 63; + const strengthNames = ['Отсутствует', 'Низкая', 'Средняя', 'Высокая']; + if (el.stats) { + el.stats.textContent = `${value}/${maxValue} | ${strengthNames[strength] || '---'} (пик: ${ch.value})`; + } + + // Обновляем индикатор сигнала (текущий уровень) + const bars = el.signal?.querySelectorAll('.sig-bar'); + if (bars) { + bars.forEach((bar: Element, i: number) => { + const isActive = i < strength; + // Если текущий уровень меньше максимального - добавляем класс dim + if (isActive) { + (bar as HTMLElement).classList.remove('dim'); + (bar as HTMLElement).classList.add('active-' + (i + 1)); + } else { + (bar as HTMLElement).classList.add('dim'); + (bar as HTMLElement).classList.remove('active-' + (i + 1)); + } + }); + } +} + export function updateAllChannels(rawData?: number[]) { for (let i = 0; i < 8; i++) { setChannelValue(i, 0, 0); + setChannelCurrentValue(i, 0, 0); if (channelElements[i]) { channelElements[i].item.classList.remove('channel-active'); } @@ -316,6 +367,7 @@ export function updateAllChannels(rawData?: number[]) { const strength = (lastByte >> 6) & 0x3; setChannelValue(0, value, strength); + setChannelCurrentValue(0, value, strength); if (channelElements[0]) { channelElements[0].item.classList.add('channel-active'); }