diff --git a/cmd/server/main.go b/cmd/server/main.go index 70a6a8d..9368ae8 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -24,8 +24,7 @@ import ( ) // ===== EMBED WEB ===== -// -//go:embed web/dist web/css web/*.html web/*.png +//go:embed web/dist web/css web/fonts web/*.html web/*.png var webFiles embed.FS type API struct { diff --git a/cmd/server/web/css/cards.css b/cmd/server/web/css/cards.css index fa588f0..f76bdca 100644 --- a/cmd/server/web/css/cards.css +++ b/cmd/server/web/css/cards.css @@ -94,9 +94,10 @@ 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; @@ -109,6 +110,15 @@ border-radius: 3px; } +/* ===== LAST BYTE ===== */ +#last-byte { + font-family: var(--font-tech); + font-size: var(--font-size-xl); + font-weight: var(--font-weight-bold); + color: #00ff88; + text-shadow: 0 0 20px rgba(0, 255, 136, 0.2); +} + /* ===== КАНАЛЫ ===== */ .channels-header { display: flex; @@ -181,6 +191,10 @@ border-color: rgba(0, 255, 136, 0.25); background: rgba(0, 255, 136, 0.06); } +.channel-item.channel-active { + border-color: rgba(0, 255, 136, 0.4); + background: rgba(0, 255, 136, 0.08); +} .channel-header { display: flex; justify-content: space-between; @@ -230,16 +244,34 @@ } .channel-progress .progress { flex: 1; - height: 8px; + height: 12px; padding: 2px; - border-width: 2px; + background: #222; + border: 2px solid #444; + border-radius: 4px; + overflow: hidden; + box-shadow: inset 0 2px 4px rgba(0,0,0,0.4); } + +/* ===== ЯРКИЙ КОНТРАСТНЫЙ ПРОГРЕСС-БАР ===== */ .channel-progress .progress .fill { height: 100%; - background: linear-gradient(90deg, #0a0, #0f0, #ff0, #f80); + background: #e6852d; border-radius: 2px; transition: width 0.15s ease; + width: 0%; + box-shadow: + inset 0 1px 2px rgba(255,255,255,0.3), + 0 0 10px rgba(230,133,45,0.5); } + +/* Активный канал - более яркий */ +.channel-item.channel-active .fill { + box-shadow: + inset 0 1px 2px rgba(255,255,255,0.4), + 0 0 15px rgba(230,133,45,0.6); +} + .channel-stats { font-size: var(--font-size-xs); color: #666; @@ -248,17 +280,117 @@ letter-spacing: 0.02em; } -/* ===== ЗВУК И ВЫСОТА ===== */ -.card:has(#bar2) .fill, -.card:has(#bar3) .fill { - background: linear-gradient(90deg, #0a0, #0f0, #ff0, #f80); - transition: width 0.15s ease; +/* ===== ОСНОВНЫЕ ПРОГРЕСС-БАРЫ (bar1, bar2, bar3) ===== */ +.progress-row { + margin-bottom: 16px; } + +.progress-label { + font-family: var(--font-mono); + font-size: var(--font-size-base); + margin-bottom: 6px; + color: #ffffff; + opacity: 0.9; + letter-spacing: 0.03em; +} + +.progress { + width: 100%; + height: 34px; + padding: 4px; + background: #222; + border: 2px solid #444; + border-radius: 4px; + 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%; + background: #e6852d; + border-radius: 2px; + box-shadow: + inset 0 1px 2px rgba(255,255,255,0.3), + 0 0 12px rgba(230,133,45,0.5); + transition: width 0.25s linear; +} + +.fill.no-connection { + background: #555555 !important; + box-shadow: none !important; + width: 0 !important; +} + +/* ===== ЗВУК И ВЫСОТА ===== */ .card:has(#bar2) .progress, .card:has(#bar3) .progress { height: 34px; padding: 6px; - border-width: 3px; + background: #222; + border: 2px solid #444; + border-radius: 4px; +} + +.card:has(#bar2) .fill, +.card:has(#bar3) .fill { + background: #e6852d; + box-shadow: + inset 0 1px 2px rgba(255,255,255,0.3), + 0 0 12px rgba(230,133,45,0.5); +} + +/* ===== ИНДИКАТОРЫ СИГНАЛА ===== */ +.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: 2px solid #e6852d; + 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; } /* ===== АДАПТИВ ===== */ @@ -268,7 +400,7 @@ gap: 4px; max-height: none; overflow-y: visible; - overscroll-behavior: auto; + overscroll-behavior: none; } .channels-header { flex-direction: column; @@ -285,8 +417,9 @@ font-size: var(--font-size-sm); } .channel-progress .progress { - height: 6px; + height: 10px; padding: 1px; + border-width: 2px; } #history { max-height: 300px; @@ -297,6 +430,11 @@ padding: 4px; border-width: 2px; } + .progress { + height: 28px; + padding: 4px; + border-width: 2px; + } } @media (max-width: 480px) { @@ -322,6 +460,11 @@ padding: 3px; border-width: 2px; } + .progress { + height: 24px; + padding: 3px; + border-width: 2px; + } } /* ===== АНИМАЦИЯ ===== */ diff --git a/cmd/server/web/css/fonts.css b/cmd/server/web/css/fonts.css index 374f46f..c49d544 100644 --- a/cmd/server/web/css/fonts.css +++ b/cmd/server/web/css/fonts.css @@ -3,58 +3,56 @@ /* JetBrains Mono */ @font-face { font-family: 'JetBrains Mono'; - font-style: normal; + src: url('/fonts/JetBrainsMono-Regular.woff2') format('woff2'); font-weight: 400; - src: url('/fonts/JetBrainsMono/ttf/JetBrainsMono-Regular.ttf') format('truetype'); + font-style: normal; font-display: swap; } @font-face { font-family: 'JetBrains Mono'; - font-style: normal; - font-weight: 600; - src: url('/fonts/JetBrainsMono/ttf/JetBrainsMono-Bold.ttf') format('truetype'); - font-display: swap; -} - -@font-face { - font-family: 'JetBrains Mono'; - font-style: normal; + src: url('/fonts/JetBrainsMono-Bold.woff2') format('woff2'); font-weight: 700; - src: url('/fonts/JetBrainsMono/ttf/JetBrainsMono-ExtraBold.ttf') format('truetype'); + font-style: normal; font-display: swap; } -/* Orbitron - для заголовков */ +/* Orbitron - используем существующий файл */ @font-face { font-family: 'Orbitron'; - font-style: normal; - font-weight: 400; - src: url('/fonts/Orbitron/static/Orbitron-Regular.ttf') format('truetype'); - font-display: swap; -} - -@font-face { - font-family: 'Orbitron'; - font-style: normal; - font-weight: 600; - src: url('/fonts/Orbitron/static/Orbitron-SemiBold.ttf') format('truetype'); - font-display: swap; -} - -@font-face { - font-family: 'Orbitron'; - font-style: normal; + src: url('/fonts/orbitron-bold-webfont.woff') format('woff'); font-weight: 700; - src: url('/fonts/Orbitron/static/Orbitron-Bold.ttf') format('truetype'); + font-style: normal; font-display: swap; } -/* Share Tech Mono - для статусов и значений */ +/* Share Tech Mono */ @font-face { font-family: 'Share Tech Mono'; - font-style: normal; - font-weight: 400; src: url('/fonts/ShareTechMono-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; font-display: swap; +} + +/* ===== ПЕРЕМЕННЫЕ СО ШРИФТАМИ ===== */ +:root { + --font-mono: 'JetBrains Mono', 'Share Tech Mono', 'Courier New', monospace; + --font-display: 'Orbitron', 'Share Tech Mono', 'Arial Black', sans-serif; + --font-tech: 'Share Tech Mono', 'JetBrains Mono', 'Courier New', monospace; + + /* Размеры шрифтов */ + --font-size-xs: 11px; + --font-size-sm: 13px; + --font-size-base: 15px; + --font-size-lg: 18px; + --font-size-xl: 22px; + --font-size-2xl: 28px; + --font-size-3xl: 36px; + + /* Веса шрифтов */ + --font-weight-normal: 400; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --font-weight-extrabold: 800; } \ No newline at end of file diff --git a/cmd/server/web/css/layout.css b/cmd/server/web/css/layout.css index 4403bf8..77b5148 100644 --- a/cmd/server/web/css/layout.css +++ b/cmd/server/web/css/layout.css @@ -20,7 +20,7 @@ .left { flex: 1 1 auto; min-width: 300px; - padding: 10px; + padding: 10px 10%; /* ← X% от ширины экрана */ overflow-y: auto; overflow-x: hidden; height: 100%; @@ -66,6 +66,8 @@ grid-template-columns: 1fr; gap: 12px; align-items: stretch; + max-width: 1200px; + margin: 0 auto; } /* ===== МОБИЛЬНАЯ ВЕРСИЯ ===== */ @@ -76,6 +78,10 @@ min-height: 100vh; gap: 8px; overflow: visible !important; + padding: 0 8px; + width: 100%; + max-width: 100%; + margin: 0; } .panel { height: auto !important; @@ -86,12 +92,18 @@ .left { flex: 1 1 auto !important; min-width: 0 !important; - padding: 0; + padding: 10px 16px !important; width: 100% !important; overflow-y: visible !important; height: auto !important; padding-bottom: 20px; } + + .grid { + max-width: 100%; + margin: 0; + } + .right { flex: 0 0 auto !important; min-width: 100% !important; diff --git a/cmd/server/web/dashboard.html b/cmd/server/web/dashboard.html index 946a43b..2c78524 100644 --- a/cmd/server/web/dashboard.html +++ b/cmd/server/web/dashboard.html @@ -27,7 +27,6 @@ Журнал
--:--:--
🔇
- @@ -45,7 +44,7 @@
-

КАНАЛЫ СИГНАЛОВ (8)

+

КАНАЛЫ СИГНАЛОВ

@@ -56,7 +55,6 @@
-

🔊 ЗВУК

diff --git a/cmd/server/web/js/app.ts b/cmd/server/web/js/app.ts index 3c185cf..0831602 100644 --- a/cmd/server/web/js/app.ts +++ b/cmd/server/web/js/app.ts @@ -19,13 +19,13 @@ import { setBars, setSignal, setAudioIndicator, - DOM + DOM, + updateAllChannels } from "./ui.js"; import { initAccordion } from "./accordion.js"; import { drawChart } from "./chart.js"; import { initResize } from "./layout.js"; import { initCamera } from "./camera.js"; -import { updateAllChannels } from './ui.js'; import { audioEngine } from "./audio.js"; let lastChart: number = 0; @@ -56,24 +56,20 @@ window.addEventListener("DOMContentLoaded", () => { if (signalLevel !== lastSignalLevel) { lastSignalLevel = signalLevel; audioEngine.updateLevel(signalLevel); - - // Обновляем индикатор звука - const isActive = signalLevel > 0; - setAudioIndicator(isActive, signalLevel); + setAudioIndicator(signalLevel > 0, signalLevel); } } }); }); document.addEventListener('DOMContentLoaded', () => { - // Обработчик для кнопки закрытия камеры const closeBtn = document.getElementById('camCloseBtn'); const toggleBtn = document.getElementById('camToggle'); if (closeBtn) { closeBtn.addEventListener('click', () => { if (toggleBtn) { - toggleBtn.click(); // Эмулируем нажатие кнопки переключения + toggleBtn.click(); } }); } @@ -104,17 +100,13 @@ document.addEventListener("keydown", (e: KeyboardEvent) => { } break; case 'm': - // Toggle звука if (lastSignalLevel > 0) { - // Временно отключаем audioEngine.updateLevel(0); setAudioIndicator(false); console.log('[Audio] Звук отключён (M)'); - // Сохраняем уровень для восстановления window._mutedLevel = lastSignalLevel; lastSignalLevel = 0; } else { - // Восстанавливаем уровень const level = window._mutedLevel || state.peakSignalLevel; if (level > 0) { audioEngine.updateLevel(level); @@ -164,23 +156,33 @@ async function update(): Promise { const arr = Array.isArray(history.bytes) ? history.bytes : []; const raw = arr[arr.length - 1] ?? 0; + // Обновляем историю сигналов в state if (arr.length > 0 && isOnline) { const bufferSize = health.stats?.filled ?? 0; addSignals(arr, bufferSize); - updateAllChannels(arr); } - const value = raw & 0x3F; - const level = (raw >> 6) & 0x3; - const percent = (value / 63) * 100; + // ===== ПАРСИНГ ДАННЫХ ===== + // Биты 0-5: значение (0-63) + // Биты 6-7: уровень сигнала (0-3) + const value = raw & 0x3F; // 0-63 + const level = (raw >> 6) & 0x3; // 0-3 + const percent = (value / 63) * 100; // 0-100% + + // ===== ОБНОВЛЯЕМ КАНАЛЫ ===== + // Передаем сырые данные для отображения в каналах + updateAllChannels(arr); if (isOnline) { + // ===== Обновляем Peak Holder (проценты) ==== const displayValue = updatePeakHolder(percent); setBars(displayValue); + // Обновляем уровень сигнала с Peak Holder const displaySignalLevel = updateSignalPeakHolder(level); setSignal(displaySignalLevel); + // Обновляем звук const currentSignalLevel = state.peakSignalLevel; if (currentSignalLevel !== lastSignalLevel) { lastSignalLevel = currentSignalLevel; @@ -239,8 +241,6 @@ async function update(): Promise { if (consecutiveErrors >= MAX_ERRORS && pollTimer) { clearInterval(pollTimer); - pollTimer = window.setInterval(update, 2000); - console.log("[app] Замедлили опрос до 2с"); } } } @@ -253,7 +253,6 @@ function startPolling(): void { window.addEventListener("focus", () => { if (consecutiveErrors >= MAX_ERRORS) { - console.log("[app] Окно в фокусе, ускоряем опрос"); consecutiveErrors = 0; startPolling(); } @@ -270,4 +269,4 @@ declare global { } startPolling(); -update(); \ No newline at end of file +update(); diff --git a/cmd/server/web/js/state.ts b/cmd/server/web/js/state.ts index 1228c34..429c229 100644 --- a/cmd/server/web/js/state.ts +++ b/cmd/server/web/js/state.ts @@ -237,7 +237,6 @@ function scheduleSignalPeakRelease(): void { state.peakSignalLevel = state.currentSignalLevel; state.signalPeakTimer = null; state.hasPeak = false; - console.log(`[SignalPeak] Сброс пика до: ${state.peakSignalLevel}`); notifyPeakHolderChange(); }, state.signalPeakHoldTime); } \ No newline at end of file diff --git a/cmd/server/web/js/ui.ts b/cmd/server/web/js/ui.ts index 71416a8..b103410 100644 --- a/cmd/server/web/js/ui.ts +++ b/cmd/server/web/js/ui.ts @@ -1,4 +1,6 @@ -// ui.ts (DOM слой) +// ui.ts - ИСПРАВЛЕННАЯ ВЕРСИЯ +// Канал 1 - реальные данные, каналы 2-8 - неактивны + export interface DOMElements { serverTime: HTMLElement | null; status: HTMLElement | null; @@ -81,6 +83,7 @@ export function setMeta(v: string): void { if (DOM.meta) DOM.meta.textContent = v; } +// ===== ОСНОВНОЙ ПРОГРЕСС-БАР ===== export function setBars(p: number, noConnection: boolean = false): void { const el = DOM.barFill; if (!el) return; @@ -90,10 +93,16 @@ export function setBars(p: number, noConnection: boolean = false): void { el.classList.add("no-connection"); } else { el.classList.remove("no-connection"); - el.style.width = `${Math.max(0, Math.min(100, p))}%`; + const percent = Math.max(0, Math.min(100, p)); + el.style.width = `${percent}%`; + // Принудительно устанавливаем рыжий цвет + el.style.background = '#e6852d'; + el.style.backgroundColor = '#e6852d'; + el.style.backgroundImage = 'none'; } } +// ===== ИНДИКАТОРЫ УРОВНЯ СИГНАЛА ===== export function setSignal(level: number, noConnection: boolean = false): void { const bars = DOM.bars; if (!bars || bars.length < 3) return; @@ -187,6 +196,11 @@ export function initChannels() { item.className = 'channel-item'; item.dataset.channel = String(i); + // Только канал 1 активен по умолчанию + if (i === 0) { + item.classList.add('channel-active'); + } + item.innerHTML = `
Канал ${i + 1} @@ -198,7 +212,7 @@ export function initChannels() {
-
+
@@ -250,14 +264,28 @@ export function setChannelValue(index: number, value: number, strength: number) const ch = channels[index]; const el = channelElements[index]; - if (!el) return; + if (!el) { + console.warn(`[UI] Channel ${index} element not found`); + return; + } ch.value = value; ch.strength = strength; - const percent = (value / 31) * 100; - el.bar.style.width = `${percent}%`; - el.value.textContent = value.toString(); + const maxValue = 63; + const percent = (value / maxValue) * 100; + + // Принудительно устанавливаем стили для прогресс-бара + if (el.bar) { + el.bar.style.width = `${Math.min(100, percent)}%`; + el.bar.style.background = '#e6852d'; + el.bar.style.backgroundColor = '#e6852d'; + el.bar.style.backgroundImage = 'none'; + } + + if (el.value) { + el.value.textContent = value.toString(); + } const bars = el.signal?.querySelectorAll('.sig-bar'); if (bars) { @@ -268,22 +296,67 @@ export function setChannelValue(index: number, value: number, strength: number) } const strengthNames = ['Отсутствует', 'Низкая', 'Средняя', 'Высокая']; - el.stats.textContent = `${value}/31 | ${strengthNames[strength] || '---'}`; - el.item.classList.remove('no-data'); -} - -export function updateAllChannels(rawData: number[]) { - const latest = new Map(); - for (const byte of rawData) { - const channel = (byte >> 5) & 0x07; - const value = byte & 0x1F; - latest.set(channel, value); + if (el.stats) { + el.stats.textContent = `${value}/${maxValue} | ${strengthNames[strength] || '---'}`; } + if (el.item) { + el.item.classList.remove('no-data'); + } +} + +/** + * ОБНОВЛЕНИЕ ВСЕХ КАНАЛОВ + * Канал 1 - реальные данные из эмулятора + * Каналы 2-8 - неактивны (0) + */ +export function updateAllChannels(rawData?: number[]) { + // Сначала сбрасываем все каналы в 0 for (let i = 0; i < 8; i++) { - const value = latest.get(i) ?? 0; - const strength = value >= 25 ? 3 : value >= 15 ? 2 : value >= 5 ? 1 : 0; - setChannelValue(i, value, strength); + setChannelValue(i, 0, 0); + if (channelElements[i]) { + channelElements[i].item.classList.remove('channel-active'); + } + } + + // Если есть данные - обновляем КАНАЛ 1 + if (rawData && rawData.length > 0) { + // Берем последний байт (самый свежий) + const lastByte = rawData[rawData.length - 1]; + + // ПРАВИЛЬНЫЙ ПАРСИНГ: + // Биты 0-5: значение (количество сигналов) 0-63 + // Биты 6-7: уровень сигнала (амплитуда/сила) 0-3 + const value = lastByte & 0x3F; // маска 0b00111111 → 0-63 + const strength = (lastByte >> 6) & 0x3; // маска 0b00000011 → 0-3 + + // Обновляем КАНАЛ 1 с реальными данными + setChannelValue(0, value, strength); + if (channelElements[0]) { + channelElements[0].item.classList.add('channel-active'); + } + + // Обновляем last-byte + if (DOM.lastByte) { + DOM.lastByte.textContent = value.toString(); + } + + // Обновляем основной прогресс-бар (для синхронизации) + const percent = (value / 63) * 100; + if (DOM.barFill) { + DOM.barFill.style.width = `${percent}%`; + DOM.barFill.style.background = '#e6852d'; + DOM.barFill.style.backgroundColor = '#e6852d'; + DOM.barFill.style.backgroundImage = 'none'; + } + } else { + // Если данных нет - канал 1 тоже неактивен + if (DOM.lastByte) { + DOM.lastByte.textContent = '--'; + } + if (DOM.barFill) { + DOM.barFill.style.width = '0%'; + } } } @@ -291,6 +364,9 @@ export function updateAllChannels(rawData: number[]) { document.addEventListener('DOMContentLoaded', () => { initChannels(); + // Принудительно обновляем каналы при загрузке + updateAllChannels(); + const expandAll = document.getElementById('expandAllBtn'); const collapseAll = document.getElementById('collapseAllBtn');