diff --git a/cmd/server/web/css/base.css b/cmd/server/web/css/base.css index c5a3240..c995f58 100644 --- a/cmd/server/web/css/base.css +++ b/cmd/server/web/css/base.css @@ -120,6 +120,33 @@ html { overscroll-behavior: contain; } +/* ===== ГЛОБАЛЬНЫЕ СТИЛИ СКРОЛЛБАРОВ ===== */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: rgba(0, 255, 136, 0.05); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: rgba(0, 255, 136, 0.3); + border-radius: 4px; + transition: background 0.3s ease; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(0, 255, 136, 0.5); +} + +/* Firefox */ +* { + scrollbar-width: thin; + scrollbar-color: rgba(0, 255, 136, 0.3) rgba(0, 255, 136, 0.05); +} + /* Убираем скролл-бары на мобильных где не нужно */ @media (max-width: 768px) { .panel::-webkit-scrollbar, @@ -128,3 +155,4 @@ html { width: 3px; } } + diff --git a/cmd/server/web/css/cards.css b/cmd/server/web/css/cards.css index 5da8b8e..fa588f0 100644 --- a/cmd/server/web/css/cards.css +++ b/cmd/server/web/css/cards.css @@ -1,21 +1,20 @@ -/* Стили карточек (card, accordion, статусы) */ +/* Стили карточек, каналов, статусов и истории */ + /* ===== CARD ===== */ .card { border: 1px solid #00ff88; border-radius: 10px; padding: 14px; background: rgba(0,255,136,0.05); - overflow: hidden; + overflow: visible; height: 100%; display: flex; flex-direction: column; transition: all 0.3s ease; } - .card:hover { box-shadow: 0 0 30px rgba(0, 255, 136, 0.05); } - .card h3 { margin: 0 0 10px; font-size: var(--font-size-lg); @@ -24,7 +23,7 @@ font-weight: var(--font-weight-bold); } -/* ===== STATUS ===== */ +/* ===== СТАТУС ===== */ #status { font-family: var(--font-tech); font-size: var(--font-size-3xl); @@ -32,23 +31,19 @@ margin-bottom: 8px; letter-spacing: 0.05em; } - .status-ok { color: #00ff88 !important; text-shadow: 0 0 30px rgba(0, 255, 136, 0.3); } - .status-lost { color: #ffffff !important; animation: blink 1.5s infinite; text-shadow: 0 0 30px rgba(255, 255, 255, 0.1); } - .status-error { color: #ff4444 !important; text-shadow: 0 0 30px rgba(255, 68, 68, 0.3); } - @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } @@ -62,7 +57,7 @@ letter-spacing: 0.02em; } -/* ===== ACCORDION ===== */ +/* ===== АККОРДЕОН ===== */ .accordion-header { cursor: pointer; user-select: none; @@ -73,54 +68,316 @@ letter-spacing: 0.05em; transition: all 0.3s ease; } - .accordion-header:hover { opacity: 0.7; text-shadow: 0 0 20px rgba(0, 255, 136, 0.2); } - .accordion-body { max-height: 520px; overflow: hidden; transition: max-height 0.3s ease, opacity 0.3s ease; } - .accordion-body.collapsed { max-height: 0; opacity: 0; } -/* ===== 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); -} - -/* ===== HISTORY ===== */ +/* ===== ИСТОРИЯ ===== */ #history { font-family: var(--font-mono); font-size: var(--font-size-sm); line-height: 1.35; - max-height: 420px; + max-height: 400px; overflow-y: auto; + overflow-x: auto; white-space: nowrap; padding-right: 4px; margin: 0; letter-spacing: 0.02em; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; } - #history::-webkit-scrollbar { width: 6px; + height: 6px; } - #history::-webkit-scrollbar-track { background: rgba(0, 255, 136, 0.05); } - #history::-webkit-scrollbar-thumb { background: #00ff88; border-radius: 3px; +} + +/* ===== КАНАЛЫ ===== */ +.channels-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + flex-wrap: wrap; + gap: 8px; +} +.channels-header h3 { + margin: 0; + font-size: var(--font-size-lg); +} +.channels-controls { + display: flex; + gap: 4px; +} +.channels-toggle-all { + background: rgba(0, 255, 136, 0.08); + border: 1px solid rgba(0, 255, 136, 0.2); + color: #00ff88; + padding: 4px 10px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-family: var(--font-mono); + transition: all 0.2s ease; + line-height: 1.4; + min-height: 30px; +} +.channels-toggle-all:hover { + background: rgba(0, 255, 136, 0.15); + border-color: #00ff88; +} +.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; + padding-right: 4px; +} +.channels-grid::-webkit-scrollbar { + width: 6px; + height: 6px; +} +.channels-grid::-webkit-scrollbar-track { + background: rgba(0, 255, 136, 0.05); +} +.channels-grid::-webkit-scrollbar-thumb { + background: rgba(0, 255, 136, 0.3); + border-radius: 3px; +} + +/* Карточка канала */ +.channel-item { + background: rgba(0, 255, 136, 0.03); + border: 1px solid rgba(0, 255, 136, 0.1); + border-radius: 6px; + overflow: hidden; + transition: all 0.2s ease; +} +.channel-item:hover { + border-color: rgba(0, 255, 136, 0.25); + background: rgba(0, 255, 136, 0.06); +} +.channel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 10px; + cursor: pointer; + user-select: none; + transition: background 0.2s; +} +.channel-header:hover { + background: rgba(0, 255, 136, 0.05); +} +.channel-label { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: #88ffbb; + font-family: var(--font-tech); +} +.channel-value { + font-size: var(--font-size-base); + font-weight: var(--font-weight-bold); + color: #00ff88; + font-family: var(--font-tech); + min-width: 24px; + text-align: right; +} +.channel-toggle { + color: #666; + font-size: 10px; + transition: transform 0.3s; + margin-left: 6px; +} +.channel-toggle.collapsed { + transform: rotate(-90deg); +} +.channel-body { + padding: 0 10px 10px 10px; + transition: all 0.3s ease; +} +.channel-body.hidden { + display: none; +} +.channel-progress { + display: flex; + align-items: center; + gap: 8px; +} +.channel-progress .progress { + flex: 1; + height: 8px; + padding: 2px; + border-width: 2px; +} +.channel-progress .progress .fill { + height: 100%; + background: linear-gradient(90deg, #0a0, #0f0, #ff0, #f80); + border-radius: 2px; + transition: width 0.15s ease; +} +.channel-stats { + font-size: var(--font-size-xs); + color: #666; + margin-top: 4px; + font-family: var(--font-mono); + 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; +} +.card:has(#bar2) .progress, +.card:has(#bar3) .progress { + height: 34px; + padding: 6px; + border-width: 3px; +} + +/* ===== АДАПТИВ ===== */ +@media (max-width: 768px) { + .channels-grid { + grid-template-columns: 1fr; + gap: 4px; + max-height: none; + overflow-y: visible; + overscroll-behavior: auto; + } + .channels-header { + flex-direction: column; + align-items: stretch; + gap: 6px; + } + .channels-controls { + justify-content: flex-end; + } + .channel-label { + font-size: var(--font-size-xs); + } + .channel-value { + font-size: var(--font-size-sm); + } + .channel-progress .progress { + height: 6px; + padding: 1px; + } + #history { + max-height: 300px; + } + .card:has(#bar2) .progress, + .card:has(#bar3) .progress { + height: 28px; + padding: 4px; + border-width: 2px; + } +} + +@media (max-width: 480px) { + .channels-grid { + gap: 3px; + } + .channel-header { + padding: 4px 8px; + } + .channel-body { + padding: 0 8px 8px 8px; + } + .channel-stats { + font-size: 9px; + } + #history { + max-height: 200px; + font-size: var(--font-size-xs); + } + .card:has(#bar2) .progress, + .card:has(#bar3) .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; + } } \ No newline at end of file diff --git a/cmd/server/web/css/layout.css b/cmd/server/web/css/layout.css index 771c720..4403bf8 100644 --- a/cmd/server/web/css/layout.css +++ b/cmd/server/web/css/layout.css @@ -5,23 +5,28 @@ width: 100%; margin: 0; padding: 0; + overflow: hidden; } -/* панели */ .panel { height: 100%; overflow-y: auto; overflow-x: hidden; + -webkit-overflow-scrolling: touch; + padding-right: 4px; + overscroll-behavior: auto; } -/* LEFT */ .left { flex: 1 1 auto; min-width: 300px; padding: 10px; + overflow-y: auto; + overflow-x: hidden; + height: 100%; + overscroll-behavior: auto; } -/* RIGHT */ .right { flex: 0 0 40%; min-width: 0; @@ -29,7 +34,6 @@ position: relative; overflow: hidden; } - .right.hidden { flex: 0 0 0 !important; width: 0 !important; @@ -38,7 +42,6 @@ pointer-events: none; } -/* ===== RESIZER ===== */ .resizer { width: 8px; min-width: 8px; @@ -48,11 +51,9 @@ flex-shrink: 0; z-index: 10; } - .resizer:hover { opacity: 0.8; } - .resizer.hidden { width: 0 !important; min-width: 0 !important; @@ -60,7 +61,6 @@ pointer-events: none; } -/* ===== GRID ===== */ .grid { display: grid; grid-template-columns: 1fr; @@ -68,9 +68,87 @@ align-items: stretch; } -@media (min-width: 900px) { +/* ===== МОБИЛЬНАЯ ВЕРСИЯ ===== */ +@media (max-width: 768px) { + .layout { + flex-direction: column; + height: auto !important; + min-height: 100vh; + gap: 8px; + overflow: visible !important; + } + .panel { + height: auto !important; + overflow-y: visible !important; + -webkit-overflow-scrolling: auto; + padding-right: 0; + } + .left { + flex: 1 1 auto !important; + min-width: 0 !important; + padding: 0; + width: 100% !important; + overflow-y: visible !important; + height: auto !important; + padding-bottom: 20px; + } + .right { + flex: 0 0 auto !important; + min-width: 100% !important; + width: 100% !important; + height: 40vh; + min-height: 300px; + opacity: 1 !important; + pointer-events: auto !important; + overflow: hidden; + flex-shrink: 0; + } + .right.hidden { + display: none !important; + flex: 0 0 0 !important; + height: 0 !important; + min-height: 0 !important; + } + .resizer { + display: none !important; + } +} + +@media (max-width: 480px) { + .layout { + min-height: 100vh; + gap: 6px; + } + .right { + height: 35vh; + min-height: 200px; + } +} + +/* ===== АЛЬБОМНАЯ ОРИЕНТАЦИЯ ===== */ +@media (max-width: 768px) and (orientation: landscape) { + .layout { + flex-direction: row; + height: calc(100vh - 80px) !important; + min-height: auto; + overflow: hidden !important; + } + .left { + flex: 1 1 60% !important; + min-width: 0 !important; + padding-right: 8px; + overflow-y: auto !important; + height: 100% !important; + } + .right { + flex: 0 0 40% !important; + min-width: 0 !important; + height: 100% !important; + min-height: 0; + } .grid { grid-template-columns: 1fr 1fr; + gap: 6px; } .wide { grid-column: span 2; diff --git a/cmd/server/web/css/responsive.css b/cmd/server/web/css/responsive.css index e96ce01..cd838e7 100644 --- a/cmd/server/web/css/responsive.css +++ b/cmd/server/web/css/responsive.css @@ -1,119 +1,63 @@ /* ===== РЕСПОНСИВНЫЙ ДИЗАЙН ===== */ -/* ============================================ - ПЛАНШЕТЫ (768px - 1024px) - ============================================ */ +/* ===== ПЛАНШЕТЫ ===== */ @media (max-width: 1024px) { - /* Уменьшаем отступы */ - body { - padding: 10px; - } - - /* Хедер */ - .header-bar { - gap: 10px; - grid-template-columns: 1fr auto; - } - - .header-bar h1 { - font-size: var(--font-size-xl); - } - - .header-time { - font-size: var(--font-size-sm); - padding: 4px 10px; - } - - .cam-btn { - font-size: var(--font-size-sm); - padding: 6px 12px; - } - - /* Карточки */ - .card { - padding: 12px 14px; - } - - .card h3 { - font-size: var(--font-size-base); - } - - #status { - font-size: var(--font-size-2xl); - } - - /* Прогресс-бары */ - .progress { - height: 28px; - padding: 4px; - } - - .progress-label { - font-size: var(--font-size-sm); - } - - /* Сигнальные иконки */ - .signal-icon { - width: 28px; - height: 28px; - } + body { padding: 10px; } + .header-bar { gap: 10px; grid-template-columns: 1fr auto; } + .header-bar h1 { font-size: var(--font-size-xl); } + .header-time { font-size: var(--font-size-sm); padding: 4px 10px; } + .cam-btn { font-size: var(--font-size-sm); padding: 6px 12px; } + .card { padding: 12px 14px; } + .card h3 { font-size: var(--font-size-base); } + #status { font-size: var(--font-size-2xl); } + .progress { height: 28px; padding: 4px; } + .progress-label { font-size: var(--font-size-sm); } + .signal-icon { width: 28px; height: 28px; } } -/* ============================================ - МОБИЛЬНЫЕ УСТРОЙСТВА (до 768px) - ============================================ */ +/* ===== МОБИЛЬНЫЕ ===== */ @media (max-width: 768px) { - /* ===== ОБЩИЕ ===== */ body { padding: 8px; font-size: var(--font-size-base); + overflow-y: auto; + min-height: 100vh; + height: auto; } - - /* Увеличиваем область касания для всех интерактивных элементов */ - button, - .cam-btn, - .tab-btn, - .pagination-btn, - .log-file-item { + button, .cam-btn, .tab-btn, .pagination-btn, .log-file-item { min-height: 44px; min-width: 44px; } - - /* ===== ХЕДЕР ===== */ .header-bar { display: flex; flex-direction: column; align-items: stretch; gap: 8px; margin-bottom: 12px; + flex-shrink: 0; } - .header-bar h1 { font-size: var(--font-size-lg); text-align: center; letter-spacing: 0.05em; } - .header-bar > div { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; } - .header-time { font-size: var(--font-size-sm); padding: 4px 10px; flex: 1; min-width: 80px; } - #audio-indicator { font-size: var(--font-size-base); padding: 4px 10px; min-width: 40px; } - .cam-btn { font-size: var(--font-size-sm); padding: 8px 14px; @@ -121,8 +65,6 @@ flex: 1; min-width: 80px; } - - /* Ссылка "Журнал" */ .header-bar a[href="/logs.html"] { font-size: var(--font-size-sm); padding: 6px 12px; @@ -130,438 +72,125 @@ flex: 1; min-width: 60px; } - - /* ===== МАКЕТ ===== */ - .layout { - flex-direction: column; - height: auto; - gap: 8px; - } - - .panel { - height: auto; - overflow-y: visible; - } - - .left { - flex: 1 1 auto !important; - min-width: 0 !important; - padding: 0; - width: 100% !important; - } - - .right { - flex: 0 0 auto !important; - min-width: 100% !important; - width: 100% !important; - height: 40vh; - min-height: 300px; - opacity: 1 !important; - pointer-events: auto !important; - } - - .right.hidden { - display: none !important; - flex: 0 0 0 !important; - height: 0 !important; - min-height: 0 !important; - } - - .resizer { - display: none !important; - } - - /* ===== СЕТКА ===== */ .grid { grid-template-columns: 1fr; gap: 8px; } - - .wide { - grid-column: span 1; - } - - /* ===== КАРТОЧКИ ===== */ - .card { - padding: 12px; - border-radius: 8px; - } - - .card h3 { - font-size: var(--font-size-base); - margin-bottom: 8px; - } - - #status { - font-size: var(--font-size-2xl); - } - - #meta { - font-size: var(--font-size-sm); - } - - /* ===== ПРОГРЕСС-БАРЫ ===== */ - .progress-row { - margin-bottom: 12px; - } - - .progress-label { - font-size: var(--font-size-sm); - margin-bottom: 4px; - } - - .progress { - height: 30px; - padding: 4px; - border-width: 2px; - } - - .progress-with-signal { - flex-wrap: nowrap; - } - - .progress-90 { - flex: 1; - margin-right: 8px; - } - - .signal-icon { - width: 30px; - height: 30px; - flex-shrink: 0; - } - - .sig-bar { - min-width: 4px; - border-width: 1.5px; - } - - /* ===== АККОРДЕОН ===== */ - .accordion-header { - padding: 8px 0; - font-size: var(--font-size-base); - } - - .accordion-body { - max-height: 400px; - } - - #history { - font-size: var(--font-size-sm); - max-height: 300px; - line-height: 1.5; - } - - /* ===== КАМЕРА ===== */ - .cam-modal { - height: 100%; - min-height: 300px; - } - - .cam-header { - padding: 6px 10px; - font-size: var(--font-size-sm); - } - - .cam-frame { - object-fit: cover; - } - - .crosshair-box { - width: 80px; - height: 80px; - border-width: 1.5px; - } - - /* ===== ГОРЯЧИЕ КЛАВИШИ ===== */ - .hotkey-hint { - font-size: var(--font-size-xs); - padding: 4px 0; - } - - .hotkey-hint kbd { - padding: 2px 6px; - font-size: var(--font-size-xs); - } + .wide { grid-column: span 1; } + .card { padding: 12px; border-radius: 8px; overflow: visible !important; height: auto !important; } + .card h3 { font-size: var(--font-size-base); margin-bottom: 8px; } + #status { font-size: var(--font-size-2xl); } + #meta { font-size: var(--font-size-sm); } + .progress-row { margin-bottom: 12px; } + .progress-label { font-size: var(--font-size-sm); margin-bottom: 4px; } + .progress { height: 30px; padding: 4px; border-width: 2px; } + .progress-with-signal { flex-wrap: nowrap; } + .progress-90 { flex: 1; margin-right: 8px; } + .signal-icon { width: 30px; height: 30px; flex-shrink: 0; } + .sig-bar { min-width: 4px; border-width: 1.5px; } + .accordion-header { padding: 8px 0; font-size: var(--font-size-base); } + .accordion-body { max-height: 400px; overflow-y: auto; -webkit-overflow-scrolling: touch; } + .cam-modal { height: 100%; min-height: 300px; } + .cam-header { padding: 6px 10px; font-size: var(--font-size-sm); } + .cam-frame { object-fit: cover; } + .crosshair-box { width: 80px; height: 80px; border-width: 1.5px; } + .hotkey-hint { font-size: var(--font-size-xs); padding: 4px 0; flex-shrink: 0; } + .hotkey-hint kbd { padding: 2px 6px; font-size: var(--font-size-xs); } + .channels-grid { overscroll-behavior: auto !important; max-height: none !important; overflow-y: visible !important; } } -/* ============================================ - МАЛЕНЬКИЕ ТЕЛЕФОНЫ (до 480px) - ============================================ */ +/* ===== МАЛЕНЬКИЕ ТЕЛЕФОНЫ ===== */ @media (max-width: 480px) { - body { - padding: 6px; - } - - /* Хедер */ - .header-bar h1 { - font-size: var(--font-size-base); - } - - .header-bar > div { - flex-wrap: wrap; - } - - .header-time { - font-size: var(--font-size-xs); - padding: 4px 8px; - min-width: 60px; - } - - #audio-indicator { - font-size: var(--font-size-sm); - padding: 4px 8px; - min-width: 35px; - } - - .cam-btn { - font-size: var(--font-size-xs); - padding: 6px 10px; - min-width: 60px; - } - - /* Карточки */ - .card { - padding: 10px; - border-radius: 6px; - } - - .card h3 { - font-size: var(--font-size-sm); - } - - #status { - font-size: var(--font-size-xl); - } - - #meta { - font-size: var(--font-size-xs); - } - - /* Прогресс-бары */ - .progress { - height: 24px; - padding: 3px; - border-width: 2px; - } - - .progress-label { - font-size: var(--font-size-xs); - } - - .signal-icon { - width: 24px; - height: 24px; - } - - .sig-bar { - min-width: 3px; - border-width: 1px; - } - + body { padding: 6px; } + .header-bar h1 { font-size: var(--font-size-base); } + .header-bar > div { flex-wrap: wrap; } + .header-time { font-size: var(--font-size-xs); padding: 4px 8px; min-width: 60px; } + #audio-indicator { font-size: var(--font-size-sm); padding: 4px 8px; min-width: 35px; } + .cam-btn { font-size: var(--font-size-xs); padding: 6px 10px; min-width: 60px; } + .card { padding: 10px; border-radius: 6px; } + .card h3 { font-size: var(--font-size-sm); } + #status { font-size: var(--font-size-xl); } + #meta { font-size: var(--font-size-xs); } + .progress { height: 24px; padding: 3px; border-width: 2px; } + .progress-label { font-size: var(--font-size-xs); } + .signal-icon { width: 24px; height: 24px; } + .sig-bar { min-width: 3px; border-width: 1px; } .sig-bar-1 { height: 30%; } .sig-bar-2 { height: 60%; } .sig-bar-3 { height: 90%; } - - /* Камера */ - .right { - height: 35vh; - min-height: 200px; - } - - .cam-header { - padding: 4px 8px; - font-size: var(--font-size-xs); - } - - .crosshair-box { - width: 60px; - height: 60px; - } - - /* Аккордеон */ - #history { - font-size: var(--font-size-xs); - max-height: 200px; - } + .right { height: 35vh; min-height: 200px; } + .cam-header { padding: 4px 8px; font-size: var(--font-size-xs); } + .crosshair-box { width: 60px; height: 60px; } + #history { font-size: var(--font-size-xs); max-height: 200px; } } -/* ============================================ - ОРИЕНТАЦИЯ (ALBUM / PORTRAIT) - ============================================ */ -@media (max-width: 768px) and (orientation: landscape) { - .layout { - flex-direction: row; - height: calc(100vh - 80px); - } - - .left { - flex: 1 1 60% !important; - min-width: 0 !important; - padding-right: 8px; - overflow-y: auto; - height: 100%; - } - - .right { - flex: 0 0 40% !important; - min-width: 0 !important; - height: 100%; - min-height: 0; - } - - .grid { - grid-template-columns: 1fr 1fr; - gap: 6px; - } - - .wide { - grid-column: span 2; - } - - #status { - font-size: var(--font-size-xl); - } -} - -/* ============================================ - УЛУЧШЕНИЯ ДЛЯ ТАЧ-ИНТЕРФЕЙСА - ============================================ */ +/* ===== ТАЧ-ИНТЕРФЕЙС ===== */ @media (hover: none) and (pointer: coarse) { - /* Увеличиваем область касания */ - button, - .cam-btn, - .tab-btn, - .pagination-btn, - .log-file-item, - .accordion-header, - .pagination-goto-btn { + button, .cam-btn, .tab-btn, .pagination-btn, .log-file-item, + .accordion-header, .pagination-goto-btn { min-height: 44px; min-width: 44px; padding: 8px 16px; } - - /* Убираем hover-эффекты, оставляем active */ - .cam-btn:hover, - .tab-btn:hover, - .pagination-btn:hover, - .log-file-item:hover { + .cam-btn:hover, .tab-btn:hover, .pagination-btn:hover, .log-file-item:hover { transform: none; } - - .cam-btn:active, - .tab-btn:active, - .pagination-btn:active, - .log-file-item:active { + .cam-btn:active, .tab-btn:active, .pagination-btn:active, .log-file-item:active { transform: scale(0.95); background: rgba(0, 255, 136, 0.15); } - - /* Увеличиваем отступы для скролла */ - .panel, - .logs-content, - .logs-sidebar { + .panel, .logs-content, .logs-sidebar, #history, #dataTableContainer, .accordion-body { -webkit-overflow-scrolling: touch; scroll-behavior: smooth; + overscroll-behavior: contain; } - - /* Увеличиваем размер полей ввода */ - .pagination-input { - min-height: 44px; - font-size: var(--font-size-base); - padding: 8px 12px; - } - - /* Увеличиваем чекбоксы и переключатели */ - input[type="checkbox"], - input[type="radio"] { - width: 20px; - height: 20px; - } + .pagination-input { min-height: 44px; font-size: var(--font-size-base); padding: 8px 12px; } + input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; } + .header-link, .cam-btn, .cam-close-btn { min-height: 44px !important; min-width: 44px !important; padding: 8px 14px !important; } + .header-link:active, .cam-btn:active { transform: scale(0.95); } } -/* ============================================ - ТЁМНАЯ ТЕМА (опционально) - ============================================ */ +/* ===== ТЁМНАЯ ТЕМА ===== */ @media (prefers-color-scheme: dark) { - body { - background: #0a0f0a; - } + body { background: #0a0f0a; } } -/* ============================================ - ПЕЧАТЬ - ============================================ */ +/* ===== ПЕЧАТЬ ===== */ @media print { - .header-bar, - .cam-btn, - .resizer, - .right, - .pagination-container, - .hotkey-hint { + .header-bar, .cam-btn, .resizer, .right, .pagination-container, .hotkey-hint { display: none !important; } - - .layout { - display: block !important; - height: auto !important; - } - - .panel { - height: auto !important; - overflow: visible !important; - } - - .card { - break-inside: avoid; - border: 1px solid #00ff88; - } -} -/* ===== ДОПОЛНИТЕЛЬНЫЕ УЛУЧШЕНИЯ ДЛЯ ХЕДЕРА ===== */ -@media (max-width: 768px) { - .header-bar { - display: flex !important; - flex-direction: column !important; - align-items: stretch !important; - gap: 8px !important; - } - - .header-controls { - display: flex !important; - flex-wrap: wrap !important; - justify-content: center !important; - gap: 6px !important; - } + .layout { display: block !important; height: auto !important; } + .panel { height: auto !important; overflow: visible !important; } + .card { break-inside: avoid; border: 1px solid #00ff88; } } -@media (max-width: 480px) { - .header-controls { - gap: 4px !important; - } +/* ===== СКРОЛЛБАРЫ ===== */ +::-webkit-scrollbar { width: 8px; height: 8px; } +::-webkit-scrollbar-track { background: rgba(0, 255, 136, 0.05); border-radius: 4px; } +::-webkit-scrollbar-thumb { background: rgba(0, 255, 136, 0.3); border-radius: 4px; transition: background 0.3s ease; } +::-webkit-scrollbar-thumb:hover { background: rgba(0, 255, 136, 0.5); } +* { scrollbar-width: thin; scrollbar-color: rgba(0, 255, 136, 0.3) rgba(0, 255, 136, 0.05); } + +.panel::-webkit-scrollbar, .logs-sidebar::-webkit-scrollbar, +.logs-content::-webkit-scrollbar, #history::-webkit-scrollbar, +.accordion-body::-webkit-scrollbar, .channels-grid::-webkit-scrollbar { + width: 6px; } - -/* ===== УЛУЧШЕНИЯ ДЛЯ ТАЧ-ИНТЕРФЕЙСА ===== */ -@media (hover: none) and (pointer: coarse) { - .header-link, - .cam-btn, - .cam-close-btn { - min-height: 44px !important; - min-width: 44px !important; - padding: 8px 14px !important; - } - - .header-link:active, - .cam-btn:active { - transform: scale(0.95); - } +.panel::-webkit-scrollbar-track, .logs-sidebar::-webkit-scrollbar-track, +.logs-content::-webkit-scrollbar-track, #history::-webkit-scrollbar-track, +.accordion-body::-webkit-scrollbar-track, .channels-grid::-webkit-scrollbar-track { + background: rgba(0, 255, 136, 0.05); + border-radius: 3px; } - -/* ===== КНОПКА ЗАКРЫТИЯ КАМЕРЫ НА МОБИЛЬНЫХ ===== */ -@media (max-width: 768px) { - .cam-close-btn { - font-size: var(--font-size-lg); - padding: 6px 14px; - min-height: 44px; - min-width: 44px; - } +.panel::-webkit-scrollbar-thumb, .logs-sidebar::-webkit-scrollbar-thumb, +.logs-content::-webkit-scrollbar-thumb, #history::-webkit-scrollbar-thumb, +.accordion-body::-webkit-scrollbar-thumb, .channels-grid::-webkit-scrollbar-thumb { + background: rgba(0, 255, 136, 0.3); + border-radius: 3px; +} +.panel::-webkit-scrollbar-thumb:hover, .logs-sidebar::-webkit-scrollbar-thumb:hover, +.logs-content::-webkit-scrollbar-thumb:hover, #history::-webkit-scrollbar-thumb:hover, +.accordion-body::-webkit-scrollbar-thumb:hover, .channels-grid::-webkit-scrollbar-thumb:hover { + background: rgba(0, 255, 136, 0.5); } \ No newline at end of file diff --git a/cmd/server/web/dashboard.html b/cmd/server/web/dashboard.html index a66e75f..946a43b 100644 --- a/cmd/server/web/dashboard.html +++ b/cmd/server/web/dashboard.html @@ -9,11 +9,7 @@ G-Port Панель - - - - @@ -23,7 +19,6 @@ -
@@ -37,57 +32,66 @@
- +
- -
+ +

СОСТОЯНИЕ

...
-
-

КАНАЛ

- -
-
- Канал 1 — 0 -
-
-
-
-
- + +
+
+

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

+
+ +
- -
-
🔊 Звук
-
-
-
-
- -
-
📐 Высота
-
-
-
+
+
+ +
+

🔊 ЗВУК

+
+
Уровень звука
+
+
+
+
+
+ 0 / 100 +
+
+ + +
+

📐 ВЫСОТА

+
+
Уровень высоты
+
+
+
+
+
+ 0 / 100 +
+
+ + +

▶ ПРИНЯТЫЕ ДАННЫЕ

-
@@ -103,8 +107,6 @@
Camera - -
@@ -113,15 +115,12 @@
-
-
(ENG lang key) C — камера  |  M — звук  |  Esc — сброс
- \ No newline at end of file diff --git a/cmd/server/web/js/app.ts b/cmd/server/web/js/app.ts index 600201b..3c185cf 100644 --- a/cmd/server/web/js/app.ts +++ b/cmd/server/web/js/app.ts @@ -25,6 +25,7 @@ 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; @@ -166,6 +167,7 @@ async function update(): Promise { if (arr.length > 0 && isOnline) { const bufferSize = health.stats?.filled ?? 0; addSignals(arr, bufferSize); + updateAllChannels(arr); } const value = raw & 0x3F; diff --git a/cmd/server/web/js/ui.ts b/cmd/server/web/js/ui.ts index e0616da..71416a8 100644 --- a/cmd/server/web/js/ui.ts +++ b/cmd/server/web/js/ui.ts @@ -9,6 +9,10 @@ export interface DOMElements { canvas: HTMLCanvasElement | null; bars: NodeListOf; barFill: HTMLElement | null; + bar2: HTMLElement | null; + bar3: HTMLElement | null; + soundLevel: HTMLElement | null; + heightLevel: HTMLElement | null; historyHeader: HTMLElement | null; historyBody: HTMLElement | null; audioIndicator: HTMLElement | null; @@ -24,6 +28,10 @@ export const DOM: DOMElements = { canvas: null, bars: document.querySelectorAll(".sig-bar"), barFill: null, + bar2: null, + bar3: null, + soundLevel: null, + heightLevel: null, historyHeader: null, historyBody: null, audioIndicator: null, @@ -38,6 +46,10 @@ export function initDOM(): void { DOM.history = document.getElementById("history"); DOM.canvas = document.getElementById("signalChart") as HTMLCanvasElement; DOM.barFill = document.getElementById("bar1"); + DOM.bar2 = document.getElementById("bar2"); + DOM.bar3 = document.getElementById("bar3"); + DOM.soundLevel = document.getElementById("sound-level"); + DOM.heightLevel = document.getElementById("height-level"); DOM.historyHeader = document.getElementById("history-header"); DOM.historyBody = document.getElementById("history-body"); DOM.bars = document.querySelectorAll(".sig-bar"); @@ -51,9 +63,9 @@ export function setServerTime(v: string): void { export function setStatus(v: string, isError: boolean = false): void { if (!DOM.status) return; DOM.status.textContent = v; - + DOM.status.classList.remove("status-ok", "status-error", "status-lost"); - + if (isError) { if (v === "Нет связи") { DOM.status.classList.add("status-lost"); @@ -72,7 +84,7 @@ export function setMeta(v: string): void { export function setBars(p: number, noConnection: boolean = false): void { const el = DOM.barFill; if (!el) return; - + if (noConnection) { el.style.width = "0%"; el.classList.add("no-connection"); @@ -85,7 +97,7 @@ export function setBars(p: number, noConnection: boolean = false): void { export function setSignal(level: number, noConnection: boolean = false): void { const bars = DOM.bars; if (!bars || bars.length < 3) return; - + bars.forEach(bar => { bar.classList.remove("active-1", "active-2", "active-3", "dim", "no-connection"); if (noConnection) { @@ -94,9 +106,9 @@ export function setSignal(level: number, noConnection: boolean = false): void { bar.classList.add("dim"); } }); - + if (noConnection) return; - + if (level >= 1) { bars[0].classList.add("active-1"); bars[0].classList.remove("dim"); @@ -111,7 +123,6 @@ export function setSignal(level: number, noConnection: boolean = false): void { } } -// ФУНКЦИЮ ДЛЯ ИНДИКАТОРА ЗВУКА export function setAudioIndicator(active: boolean, level: number = 0): void { const el = DOM.audioIndicator; if (!el) return; @@ -120,7 +131,6 @@ export function setAudioIndicator(active: boolean, level: number = 0): void { el.textContent = `🔊 ${level}`; el.style.color = '#00ff88'; el.style.opacity = '1'; - // Анимация пульсации el.style.animation = 'pulse 1s ease-in-out infinite'; } else { el.textContent = '🔇'; @@ -128,4 +138,175 @@ export function setAudioIndicator(active: boolean, level: number = 0): void { el.style.opacity = '0.5'; el.style.animation = 'none'; } -} \ No newline at end of file +} + +export function setSoundLevel(level: number): void { + if (DOM.bar2) { + const percent = Math.max(0, Math.min(100, level)); + DOM.bar2.style.width = `${percent}%`; + } + if (DOM.soundLevel) { + DOM.soundLevel.textContent = Math.round(level).toString(); + } +} + +export function setHeightLevel(level: number): void { + if (DOM.bar3) { + const percent = Math.max(0, Math.min(100, level)); + DOM.bar3.style.width = `${percent}%`; + } + if (DOM.heightLevel) { + DOM.heightLevel.textContent = Math.round(level).toString(); + } +} + +// ===== 8 КАНАЛОВ ===== +export interface ChannelState { + value: number; + strength: number; + expanded: boolean; +} + +export const channels: ChannelState[] = Array(8).fill(null).map(() => ({ + value: 0, + strength: 0, + expanded: true +})); + +let channelElements: any[] = []; + +export function initChannels() { + const container = document.getElementById('channelsContainer'); + if (!container) return; + + container.innerHTML = ''; + channelElements = []; + + for (let i = 0; i < 8; i++) { + const item = document.createElement('div'); + item.className = 'channel-item'; + item.dataset.channel = String(i); + + item.innerHTML = ` +
+ Канал ${i + 1} + + -- + + +
+
+
+
+
+
+
+
+
+
+
+
+
--
+
+ `; + + container.appendChild(item); + + channelElements[i] = { + item: item, + value: document.getElementById(`ch-val-${i}`), + bar: document.getElementById(`ch-bar-${i}`), + signal: document.getElementById(`ch-signal-${i}`), + stats: document.getElementById(`ch-stats-${i}`), + toggle: document.getElementById(`ch-toggle-${i}`), + body: document.getElementById(`ch-body-${i}`) + }; + + const header = item.querySelector('.channel-header'); + header?.addEventListener('click', () => { + toggleChannel(i); + }); + } +} + +export function toggleChannel(index: number) { + const ch = channels[index]; + ch.expanded = !ch.expanded; + + const el = channelElements[index]; + if (!el) return; + + if (ch.expanded) { + el.body.classList.remove('hidden'); + el.toggle.textContent = '▼'; + } else { + el.body.classList.add('hidden'); + el.toggle.textContent = '▶'; + } +} + +export function setChannelValue(index: number, value: number, strength: number) { + if (index < 0 || index > 7) return; + + const ch = channels[index]; + const el = channelElements[index]; + if (!el) return; + + ch.value = value; + ch.strength = strength; + + const percent = (value / 31) * 100; + el.bar.style.width = `${percent}%`; + el.value.textContent = value.toString(); + + const bars = el.signal?.querySelectorAll('.sig-bar'); + if (bars) { + bars.forEach((bar: Element, i: number) => { + const isActive = i < strength; + (bar as HTMLElement).style.opacity = isActive ? '1' : '0.2'; + }); + } + + 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); + } + + 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); + } +} + +// Инициализация при загрузке +document.addEventListener('DOMContentLoaded', () => { + initChannels(); + + const expandAll = document.getElementById('expandAllBtn'); + const collapseAll = document.getElementById('collapseAllBtn'); + + if (expandAll) { + expandAll.addEventListener('click', () => { + channels.forEach((ch, i) => { + if (!ch.expanded) toggleChannel(i); + }); + }); + } + + if (collapseAll) { + collapseAll.addEventListener('click', () => { + channels.forEach((ch, i) => { + if (ch.expanded) toggleChannel(i); + }); + }); + } +}); \ No newline at end of file diff --git a/internal/logger/rotation.go b/internal/logger/rotation.go index cbddd70..ab70b3e 100644 --- a/internal/logger/rotation.go +++ b/internal/logger/rotation.go @@ -70,7 +70,7 @@ func (r *RotatingLogger) rotate() error { dataLogger, err := NewDataLogger(filename) if err != nil { if r.eventLogger != nil { - r.eventLogger.Event("ROTATION_FAILED") + r.eventLogger.Event("ПЛАНОВАЯ_РОТАЦИЯ_НЕУДАЧНА") } return err } @@ -78,7 +78,7 @@ func (r *RotatingLogger) rotate() error { r.dataLogger = dataLogger r.currentHour = newHour if r.eventLogger != nil { - r.eventLogger.Event("ROTATION_COMPLETE") + r.eventLogger.Event("ПЛАНОВАЯ_РОТАЦИЯ") } return nil }