From 361ffc5578112c95a9432329baa77f80e6eb50d0 Mon Sep 17 00:00:00 2001
From: Tot Maxim
Date: Mon, 11 May 2026 16:18:26 +0300
Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?=
=?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BA=D1=80=D0=B5=D1=81=D1=82=D0=B0=20?=
=?UTF-8?q?=D0=BC=D0=B5=D1=82=D0=BA=D0=B8=20=D0=BD=D0=B0=20=D0=B2=D0=B8?=
=?UTF-8?q?=D0=B4=D0=B5=D0=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
cmd/server/web/css/style.css | 64 +++++++++++++++++++++++
cmd/server/web/dashboard.html | 14 +++++-
cmd/server/web/js/camera.js | 95 +++++++++++++++++++++++++++++++++--
3 files changed, 167 insertions(+), 6 deletions(-)
diff --git a/cmd/server/web/css/style.css b/cmd/server/web/css/style.css
index 73c4b5e..50d7fb0 100644
--- a/cmd/server/web/css/style.css
+++ b/cmd/server/web/css/style.css
@@ -341,6 +341,70 @@ h1 {
object-fit: contain;
background: black;
}
+/* ===== CAMERA OVERLAY ===== */
+.cam-frame-wrapper {
+ position: relative;
+ flex: 1;
+ width: 100%;
+ background: black;
+ overflow: hidden;
+}
+
+.cam-frame {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ background: black;
+}
+
+/* Перекрестие (crosshair) */
+.cam-crosshair {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 10;
+}
+
+/* Вертикальная линия */
+.crosshair-v {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 1px;
+ height: 100%;
+ background: #00ff88;
+ box-shadow: 0 0 4px rgba(0, 255, 136, 0.3);
+}
+
+/* Горизонтальная линия */
+.crosshair-h {
+ position: absolute;
+ top: 50%;
+ left: 0;
+ transform: translateY(-50%);
+ width: 100%;
+ height: 1px;
+ background: #00ff88;
+ box-shadow: 0 0 4px rgba(0, 255, 136, 0.3);
+}
+
+/* Центральный квадрат */
+.crosshair-box {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 120px;
+ height: 120px;
+ border: 2px solid #00ff88;
+ box-shadow:
+ 0 0 10px rgba(0, 255, 136, 0.3),
+ inset 0 0 10px rgba(0, 255, 136, 0.1);
+}
/* ===== MOBILE ===== */
@media (max-width: 500px) {
diff --git a/cmd/server/web/dashboard.html b/cmd/server/web/dashboard.html
index 0dc2555..7687d29 100644
--- a/cmd/server/web/dashboard.html
+++ b/cmd/server/web/dashboard.html
@@ -86,7 +86,19 @@
-
+
+
![Camera]()
+
+
+
+
diff --git a/cmd/server/web/js/camera.js b/cmd/server/web/js/camera.js
index 6abd18e..3f33fa0 100644
--- a/cmd/server/web/js/camera.js
+++ b/cmd/server/web/js/camera.js
@@ -3,14 +3,17 @@ import { updateResizerVisibility } from './layout.js';
let rightPanel;
let camImage;
-let camClose;
-
+let crosshair;
let isOpen = false;
+let streamCheckInterval = null;
export function initCamera() {
rightPanel = document.getElementById("rightPanel");
camImage = document.getElementById("camImage");
- camClose = document.getElementById("camClose");
+ crosshair = document.getElementById("crosshair");
+
+ // Проверяем доступность потока
+ checkStreamAvailability();
const btn = document.getElementById("camToggle");
@@ -23,6 +26,11 @@ export function initCamera() {
rightPanel.classList.remove("hidden");
+ // Показываем перекрестие
+ if (crosshair) {
+ crosshair.style.display = "block";
+ }
+
camImage.src = streamUrl;
if (btn) {
@@ -32,6 +40,9 @@ export function initCamera() {
console.log("[camera] opened");
updateResizerVisibility();
+
+ // Запускаем мониторинг потока
+ startStreamMonitoring();
}
function close() {
@@ -39,6 +50,11 @@ export function initCamera() {
isOpen = false;
+ // Скрываем перекрестие
+ if (crosshair) {
+ crosshair.style.display = "none";
+ }
+
// остановка stream
camImage.src = "";
@@ -51,6 +67,9 @@ export function initCamera() {
console.log("[camera] closed");
updateResizerVisibility();
+
+ // Останавливаем мониторинг
+ stopStreamMonitoring();
}
function toggle() {
@@ -63,14 +82,80 @@ export function initCamera() {
camImage.onload = () => {
console.log("[camera] stream started");
+ if (crosshair) {
+ crosshair.style.opacity = '1';
+ }
};
camImage.onerror = (e) => {
console.error("[camera] stream error", e);
+ if (crosshair) {
+ crosshair.style.opacity = '0.3';
+ }
};
btn?.addEventListener("click", toggle);
- // автозапуск
- open();
+ // Проверка доступности камеры
+ async function checkStreamAvailability() {
+ try {
+ const response = await fetch('/api/stream');
+ const data = await response.json();
+
+ if (data.available && data.cam) {
+ console.log("[camera] stream available:", data.source);
+ // Автозапуск если камера доступна
+ if (!isOpen) {
+ open();
+ }
+ } else {
+ console.log("[camera] stream not available");
+ if (crosshair) {
+ crosshair.style.display = 'none';
+ }
+ }
+ } catch (error) {
+ console.log("[camera] check failed:", error);
+ }
+ }
+
+ // Мониторинг состояния потока
+ function startStreamMonitoring() {
+ stopStreamMonitoring();
+ streamCheckInterval = setInterval(() => {
+ if (isOpen && camImage) {
+ if (!camImage.complete || camImage.naturalWidth === 0) {
+ if (crosshair) {
+ crosshair.style.opacity = '0.3';
+ }
+ } else {
+ if (crosshair) {
+ crosshair.style.opacity = '1';
+ }
+ }
+ }
+ }, 2000);
+ }
+
+ function stopStreamMonitoring() {
+ if (streamCheckInterval) {
+ clearInterval(streamCheckInterval);
+ streamCheckInterval = null;
+ }
+ }
+}
+
+// Экспортируем для возможности ручного управления
+export function showCrosshair(show = true) {
+ const crosshair = document.getElementById("crosshair");
+ if (crosshair) {
+ crosshair.style.display = show ? "block" : "none";
+ }
+}
+
+export function setCrosshairOpacity(opacity) {
+ const crosshair = document.getElementById("crosshair");
+ if (crosshair) {
+ crosshair.style.opacity = opacity;
+ }
}
\ No newline at end of file