diff --git a/webui/web/css/main.css b/webui/web/css/main.css index 363abe6..02a617b 100644 --- a/webui/web/css/main.css +++ b/webui/web/css/main.css @@ -79,6 +79,37 @@ body { overflow-x: hidden; } +/* ===== MOCK MODE BADGE ===== */ +.mock-badge { + position: fixed; + top: 1rem; + right: 1rem; + z-index: 9999; + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(245, 158, 11, 0.15); + border: 1px solid var(--warning); + border-radius: 6px; + color: var(--warning); + font-size: var(--text-xs); + font-weight: 600; + letter-spacing: 0.05em; + box-shadow: 0 4px 12px rgba(245, 158, 11, 0.2); + backdrop-filter: blur(10px); + animation: fadeIn var(--transition-base); +} + +.mock-badge svg { + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + /* ===== LAYOUT ===== */ #app { min-height: 100vh; diff --git a/webui/web/dev-server.sh b/webui/web/dev-server.sh new file mode 100755 index 0000000..7388a61 --- /dev/null +++ b/webui/web/dev-server.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Simple development server for Strix WebUI +# This allows you to test the UI without running the Go backend + +PORT=${1:-8080} + +echo "Starting development server on port $PORT" +echo "Open: http://localhost:$PORT?mock=true" +echo "" +echo "Press Ctrl+C to stop" + +# Use Python's built-in HTTP server +cd "$(dirname "$0")" +python3 -m http.server $PORT diff --git a/webui/web/index.html b/webui/web/index.html index 2e915be..b74d6fc 100644 --- a/webui/web/index.html +++ b/webui/web/index.html @@ -8,6 +8,15 @@ + + +
diff --git a/webui/web/js/main.js b/webui/web/js/main.js index 689bb8b..30f531b 100644 --- a/webui/web/js/main.js +++ b/webui/web/js/main.js @@ -1,5 +1,7 @@ import { CameraSearchAPI } from './api/camera-search.js'; import { StreamDiscoveryAPI } from './api/stream-discovery.js'; +import { MockCameraAPI } from './mock/mock-camera-api.js'; +import { MockStreamAPI } from './mock/mock-stream-api.js'; import { SearchForm } from './ui/search-form.js'; import { StreamCarousel } from './ui/stream-carousel.js'; import { ConfigPanel } from './ui/config-panel.js'; @@ -8,12 +10,24 @@ import { showToast } from './utils/toast.js'; class StrixApp { constructor() { - // Check if mock mode is enabled via URL parameter (?mock=true) + // Check if mock mode is enabled via URL parameter const urlParams = new URLSearchParams(window.location.search); - const useMock = urlParams.get('mock') === 'true'; + const isMockMode = urlParams.get('mock') === 'true'; - this.cameraAPI = new CameraSearchAPI(null, useMock); - this.streamAPI = new StreamDiscoveryAPI(null, useMock); + if (isMockMode) { + console.log('🎭 Mock mode enabled - using fake data'); + this.cameraAPI = new MockCameraAPI(); + this.streamAPI = new MockStreamAPI(); + + // Show mock mode badge + const mockBadge = document.getElementById('mock-mode-badge'); + if (mockBadge) { + mockBadge.classList.remove('hidden'); + } + } else { + this.cameraAPI = new CameraSearchAPI(); + this.streamAPI = new StreamDiscoveryAPI(); + } this.searchForm = new SearchForm(); this.carousel = new StreamCarousel(); @@ -24,7 +38,6 @@ class StrixApp { this.selectedMainStream = null; this.selectedSubStream = null; this.isSelectingSubStream = false; - this.useMock = useMock; this.init(); } @@ -32,31 +45,6 @@ class StrixApp { init() { this.setupEventListeners(); this.showScreen('address'); - - // Show mock mode indicator if enabled - if (this.useMock) { - this.showMockIndicator(); - } - } - - showMockIndicator() { - const indicator = document.createElement('div'); - indicator.id = 'mock-indicator'; - indicator.style.cssText = ` - position: fixed; - top: 10px; - right: 10px; - background: #7E57C2; - color: white; - padding: 8px 16px; - border-radius: 8px; - font-size: 14px; - font-weight: 600; - z-index: 9999; - box-shadow: 0 2px 8px rgba(0,0,0,0.3); - `; - indicator.textContent = 'MOCK MODE'; - document.body.appendChild(indicator); } setupEventListeners() { diff --git a/webui/web/js/mock/mock-camera-api.js b/webui/web/js/mock/mock-camera-api.js new file mode 100644 index 0000000..8c4f124 --- /dev/null +++ b/webui/web/js/mock/mock-camera-api.js @@ -0,0 +1,49 @@ +// Mock implementation of CameraSearchAPI for development +export class MockCameraAPI { + constructor() { + this.mockCameras = [ + { brand: "Hikvision", model: "DS-2CD2042WD-I" }, + { brand: "Hikvision", model: "DS-2CD2142FWD-I" }, + { brand: "Hikvision", model: "DS-2CD2032-I" }, + { brand: "Hikvision", model: "DS-2CD2385G1-I" }, + { brand: "Dahua", model: "IPC-HFW4431R-Z" }, + { brand: "Dahua", model: "IPC-HDBW4433R-ZS" }, + { brand: "Dahua", model: "DH-IPC-HFW2431S-S-S2" }, + { brand: "Dahua", model: "IPC-HDW2531T-AS-S2" }, + { brand: "Axis", model: "M3046-V" }, + { brand: "Axis", model: "P3245-LVE" }, + { brand: "Axis", model: "M5525-E" }, + { brand: "Uniview", model: "IPC322SR3-DVS28-F" }, + { brand: "Uniview", model: "IPC2124SR3-DPF40" }, + { brand: "Reolink", model: "RLC-410" }, + { brand: "Reolink", model: "RLC-520A" }, + { brand: "Reolink", model: "RLC-810A" }, + { brand: "TP-Link", model: "VIGI C300HP-4" }, + { brand: "TP-Link", model: "VIGI C540V" }, + { brand: "Amcrest", model: "IP8M-2496EW" }, + { brand: "Amcrest", model: "IP4M-1041B" }, + { brand: "Foscam", model: "FI9900P" }, + { brand: "Foscam", model: "R5" }, + ]; + } + + async search(query, limit = 10) { + // Simulate network delay + await this.delay(150); + + const lowerQuery = query.toLowerCase(); + const filtered = this.mockCameras.filter(camera => { + const searchText = `${camera.brand} ${camera.model}`.toLowerCase(); + return searchText.includes(lowerQuery); + }); + + return { + cameras: filtered.slice(0, limit), + total: filtered.length + }; + } + + delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} diff --git a/webui/web/js/mock/mock-stream-api.js b/webui/web/js/mock/mock-stream-api.js new file mode 100644 index 0000000..94dcc71 --- /dev/null +++ b/webui/web/js/mock/mock-stream-api.js @@ -0,0 +1,175 @@ +// Mock implementation of StreamDiscoveryAPI for development +export class MockStreamAPI { + constructor() { + this.mockStreams = [ + { + url: "rtsp://192.168.1.100:554/Streaming/Channels/101", + path: "/Streaming/Channels/101", + type: "FFMPEG", + resolution: "1920x1080", + codec: "H.264", + fps: 25, + bitrate: 4096000, + has_audio: true + }, + { + url: "http://192.168.1.100/snap.jpg", + path: "/snap.jpg", + type: "JPEG", + resolution: "1920x1080", + codec: "JPEG", + fps: 1, + bitrate: 0, + has_audio: false + }, + { + url: "http://192.168.1.100/video.mjpg", + path: "/video.mjpg", + type: "MJPEG", + resolution: "1280x720", + codec: "MJPEG", + fps: 10, + bitrate: 2048000, + has_audio: false + }, + { + url: "http://192.168.1.100/stream/live.m3u8", + path: "/stream/live.m3u8", + type: "HLS", + resolution: "1920x1080", + codec: "H.264", + fps: 25, + bitrate: 3072000, + has_audio: true + }, + { + url: "http://192.168.1.100/videostream.cgi?user=admin&pwd=12345", + path: "/videostream.cgi?user=admin&pwd=12345", + type: "HTTP_VIDEO", + resolution: "1280x960", + codec: "H.264", + fps: 20, + bitrate: 2048000, + has_audio: false + }, + { + url: "rtsp://192.168.1.100:554/Streaming/Channels/102", + path: "/Streaming/Channels/102", + type: "FFMPEG", + resolution: "640x480", + codec: "H.264", + fps: 15, + bitrate: 512000, + has_audio: false + }, + { + url: "rtsp://192.168.1.100:554/cam/realmonitor?channel=1&subtype=0", + path: "/cam/realmonitor?channel=1&subtype=0", + type: "ONVIF", + resolution: "2560x1440", + codec: "H.265", + fps: 30, + bitrate: 6144000, + has_audio: true + }, + { + url: "rtsp://192.168.1.100:554/h264Preview_01_main", + path: "/h264Preview_01_main", + type: "FFMPEG", + resolution: "1920x1080", + codec: "H.264", + fps: 20, + bitrate: 3072000, + has_audio: true + }, + { + url: "rtsp://192.168.1.100:554/live/ch0", + path: "/live/ch0", + type: "ONVIF", + resolution: "2688x1520", + codec: "H.265", + fps: 25, + bitrate: 5120000, + has_audio: true + }, + { + url: "rtsp://192.168.1.100:554/stream1", + path: "/stream1", + type: "FFMPEG", + resolution: "3840x2160", + codec: "H.265", + fps: 30, + bitrate: 8192000, + has_audio: true + } + ]; + } + + discover(request, callbacks) { + const totalToScan = 150; + const streamsToFind = this.mockStreams; + let tested = 0; + let found = 0; + + const startTime = Date.now(); + + // Simulate progressive discovery + const interval = setInterval(() => { + const increment = Math.floor(Math.random() * 8) + 3; + tested = Math.min(tested + increment, totalToScan); + const remaining = totalToScan - tested; + + // Send progress event + if (callbacks.onProgress) { + callbacks.onProgress({ + tested: tested, + found: found, + remaining: remaining + }); + } + + // Randomly find streams + if (found < streamsToFind.length && Math.random() > 0.6) { + const stream = streamsToFind[found]; + found++; + + if (callbacks.onStreamFound) { + callbacks.onStreamFound({ + stream: stream + }); + } + } + + // Complete when done + if (tested >= totalToScan) { + clearInterval(interval); + + // Send any remaining streams + while (found < streamsToFind.length) { + const stream = streamsToFind[found]; + found++; + + if (callbacks.onStreamFound) { + callbacks.onStreamFound({ + stream: stream + }); + } + } + + const duration = (Date.now() - startTime) / 1000; + + if (callbacks.onComplete) { + callbacks.onComplete({ + total_tested: totalToScan, + total_found: found, + duration: duration + }); + } + } + }, 400); + } + + close() { + // Nothing to close in mock mode + } +}