Add mock mode for WebUI development

- Add mock API classes for camera search and stream discovery
- Add mock mode toggle via ?mock=true URL parameter
- Add visual mock mode indicator badge
- Add dev-server.sh script for local development
- Mock data includes 10 diverse streams (FFMPEG, ONVIF, JPEG, MJPEG, HLS, HTTP_VIDEO)
This commit is contained in:
eduard256
2025-11-21 22:57:37 +03:00
parent 8bf92e6598
commit 56c06dfa98
6 changed files with 296 additions and 30 deletions
+31
View File
@@ -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;
+14
View File
@@ -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
+9
View File
@@ -8,6 +8,15 @@
<link rel="stylesheet" href="css/main.css">
</head>
<body>
<!-- Mock Mode Indicator -->
<div id="mock-mode-badge" class="mock-badge hidden">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M8 1v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
MOCK MODE
</div>
<div id="app">
<!-- Screen 1: Initial Address Input -->
<div id="screen-address" class="screen active">
+18 -30
View File
@@ -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() {
+49
View File
@@ -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));
}
}
+175
View File
@@ -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
}
}