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