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
+ }
+}