Корректировка ошибок. Стили, прокрутка, шрифты

This commit is contained in:
Maxim
2026-06-22 10:41:29 +03:00
parent 5d15f215be
commit 827210d29a
8 changed files with 318 additions and 94 deletions

View File

@@ -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 {

View File

@@ -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;
}
}
/* ===== АНИМАЦИЯ ===== */

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -27,7 +27,6 @@
<a href="/logs.html" class="header-link">Журнал</a>
<div id="server-time" class="header-time">--:--:--</div>
<div id="audio-indicator" class="audio-indicator">🔇</div>
<button id="camToggle" class="cam-btn">КАМЕРА</button>
</div>
</div>
@@ -45,7 +44,7 @@
<!-- Карточка каналов (8 штук) -->
<div class="card wide channels-card">
<div class="channels-header">
<h3>КАНАЛЫ СИГНАЛОВ (8)</h3>
<h3>КАНАЛЫ СИГНАЛОВ</h3>
<div class="channels-controls">
<button class="channels-toggle-all" id="expandAllBtn"></button>
<button class="channels-toggle-all" id="collapseAllBtn"></button>
@@ -56,7 +55,6 @@
</div>
</div>
<!-- ===== ВОЗВРАЩАЕМ ЗВУК И ВЫСОТУ ===== -->
<!-- Карточка со звуком -->
<div class="card wide">
<h3>🔊 ЗВУК</h3>

View File

@@ -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<void> {
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<void> {
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();
update();

View File

@@ -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);
}

View File

@@ -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 = `
<div class="channel-header" data-channel="${i}">
<span class="channel-label">Канал ${i + 1}</span>
@@ -198,7 +212,7 @@ export function initChannels() {
<div class="channel-body" id="ch-body-${i}">
<div class="channel-progress">
<div class="progress">
<div class="fill" id="ch-bar-${i}" style="width:0%"></div>
<div class="fill" id="ch-bar-${i}" style="width:0%; background: #e6852d;"></div>
</div>
<div class="signal-icon" id="ch-signal-${i}">
<div class="sig-bar sig-bar-1"></div>
@@ -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<number, number>();
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');