Add mock mode for web UI development and testing

- Add mock data module with simulated camera search and stream discovery
- Enable mock mode via ?mock=true URL parameter
- Show MOCK MODE indicator when enabled
- Remove statistics cards from discovery screen, keep only progress bar
- Mock mode works independently from Go backend for easier UI testing
This commit is contained in:
eduard256
2025-11-21 22:40:38 +03:00
parent 8036d3e9be
commit 8bf92e6598
5 changed files with 265 additions and 19 deletions
-15
View File
@@ -207,21 +207,6 @@
<p id="progress-text" class="progress-text">Starting scan...</p>
</div>
<div class="stats">
<div class="stat">
<span class="stat-value" id="stat-tested">0</span>
<span class="stat-label">Tested</span>
</div>
<div class="stat">
<span class="stat-value stat-primary" id="stat-found">0</span>
<span class="stat-label">Found</span>
</div>
<div class="stat">
<span class="stat-value" id="stat-remaining">0</span>
<span class="stat-label">Remaining</span>
</div>
</div>
<div id="streams-section" class="streams-section hidden">
<h3 class="section-title">Found Connections</h3>
+10 -1
View File
@@ -1,14 +1,23 @@
import { MockCameraSearch } from '../mock/mock-data.js';
export class CameraSearchAPI {
constructor(baseURL = null) {
constructor(baseURL = null, useMock = false) {
// Use relative URLs since API and UI are on the same port
if (!baseURL) {
this.baseURL = '';
} else {
this.baseURL = baseURL;
}
this.useMock = useMock;
this.mockAPI = useMock ? new MockCameraSearch() : null;
}
async search(query, limit = 10) {
// Use mock API if enabled
if (this.useMock) {
return await this.mockAPI.search(query, limit);
}
const response = await fetch(`${this.baseURL}api/v1/cameras/search`, {
method: 'POST',
headers: {
+14 -1
View File
@@ -1,5 +1,7 @@
import { MockStreamDiscovery } from '../mock/mock-data.js';
export class StreamDiscoveryAPI {
constructor(baseURL = null) {
constructor(baseURL = null, useMock = false) {
// Use relative URLs since API and UI are on the same port
if (!baseURL) {
this.baseURL = '';
@@ -7,11 +9,19 @@ export class StreamDiscoveryAPI {
this.baseURL = baseURL;
}
this.eventSource = null;
this.useMock = useMock;
this.mockAPI = useMock ? new MockStreamDiscovery() : null;
}
discover(request, callbacks) {
this.close();
// Use mock API if enabled
if (this.useMock) {
this.mockAPI.discover(request, callbacks);
return;
}
fetch(`${this.baseURL}api/v1/streams/discover`, {
method: 'POST',
headers: {
@@ -91,6 +101,9 @@ export class StreamDiscoveryAPI {
}
close() {
if (this.useMock && this.mockAPI) {
this.mockAPI.close();
}
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
+32 -2
View File
@@ -8,8 +8,12 @@ import { showToast } from './utils/toast.js';
class StrixApp {
constructor() {
this.cameraAPI = new CameraSearchAPI();
this.streamAPI = new StreamDiscoveryAPI();
// Check if mock mode is enabled via URL parameter (?mock=true)
const urlParams = new URLSearchParams(window.location.search);
const useMock = urlParams.get('mock') === 'true';
this.cameraAPI = new CameraSearchAPI(null, useMock);
this.streamAPI = new StreamDiscoveryAPI(null, useMock);
this.searchForm = new SearchForm();
this.carousel = new StreamCarousel();
@@ -20,6 +24,7 @@ class StrixApp {
this.selectedMainStream = null;
this.selectedSubStream = null;
this.isSelectingSubStream = false;
this.useMock = useMock;
this.init();
}
@@ -27,6 +32,31 @@ 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() {
+209
View File
@@ -0,0 +1,209 @@
// Mock data for development and testing
export const MOCK_CAMERAS = [
{ brand: "Hikvision", model: "DS-2CD2143G0-I" },
{ brand: "Hikvision", model: "DS-2CD2385G1-I" },
{ brand: "Hikvision", model: "DS-2CD2T85G1-I8" },
{ brand: "Dahua", model: "IPC-HFW5831E-Z5E" },
{ brand: "Dahua", model: "IPC-HDW5831R-ZE" },
{ brand: "Axis", model: "M3046-V" },
{ brand: "Axis", model: "P3245-LVE" },
{ brand: "Uniview", model: "IPC2324LB-ADZK-G" },
{ brand: "Reolink", model: "RLC-810A" },
{ brand: "TP-Link", model: "VIGI C540V" }
];
export const MOCK_STREAMS = [
{
url: "rtsp://admin:password@192.168.1.100:554/stream1",
type: "FFMPEG",
resolution: "1920x1080",
codec: "h264",
fps: 25,
bitrate: 4096,
audio: true
},
{
url: "rtsp://admin:password@192.168.1.100:554/stream2",
type: "FFMPEG",
resolution: "640x360",
codec: "h264",
fps: 15,
bitrate: 512,
audio: true
},
{
url: "http://admin:password@192.168.1.100:80/onvif/device_service",
type: "ONVIF",
resolution: "1920x1080",
codec: "h264",
fps: 25,
bitrate: 4096,
audio: false
},
{
url: "rtsp://admin:password@192.168.1.100/live/main",
type: "FFMPEG",
resolution: "2560x1440",
codec: "h265",
fps: 30,
bitrate: 6144,
audio: true
},
{
url: "rtsp://admin:password@192.168.1.100/live/sub",
type: "FFMPEG",
resolution: "704x576",
codec: "h264",
fps: 15,
bitrate: 768,
audio: false
},
{
url: "rtsp://admin:password@192.168.1.100:554/ch01/0",
type: "FFMPEG",
resolution: "3840x2160",
codec: "h265",
fps: 25,
bitrate: 8192,
audio: true
},
{
url: "rtsp://admin:password@192.168.1.100:554/ch01/1",
type: "FFMPEG",
resolution: "1280x720",
codec: "h264",
fps: 20,
bitrate: 2048,
audio: false
},
{
url: "http://admin:password@192.168.1.100:8080/video.mjpeg",
type: "MJPEG",
resolution: "1920x1080",
codec: "mjpeg",
fps: 10,
bitrate: 3072,
audio: false
},
{
url: "rtsp://admin:password@192.168.1.100/h264_stream",
type: "FFMPEG",
resolution: "1920x1080",
codec: "h264",
fps: 30,
bitrate: 4096,
audio: true
},
{
url: "http://admin:password@192.168.1.100:8081/stream.m3u8",
type: "HLS",
resolution: "1920x1080",
codec: "h264",
fps: 25,
bitrate: 4096,
audio: true
}
];
// Mock Camera Search API
export class MockCameraSearch {
async search(query, limit = 10) {
// Simulate network delay
await this.delay(100);
const results = MOCK_CAMERAS.filter(camera => {
const searchStr = `${camera.brand} ${camera.model}`.toLowerCase();
return searchStr.includes(query.toLowerCase());
});
return {
cameras: results.slice(0, limit),
total: results.length
};
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Mock Stream Discovery API
export class MockStreamDiscovery {
constructor() {
this.isRunning = false;
this.timeoutId = null;
}
discover(request, callbacks) {
this.isRunning = true;
let tested = 0;
const totalToTest = 516;
const foundStreams = [...MOCK_STREAMS];
// Initial progress
callbacks.onProgress({
tested: 0,
found: 0,
remaining: totalToTest
});
// Simulate progressive testing
const progressInterval = setInterval(() => {
if (!this.isRunning) {
clearInterval(progressInterval);
return;
}
tested += Math.floor(Math.random() * 30) + 20;
if (tested > totalToTest) tested = totalToTest;
callbacks.onProgress({
tested: tested,
found: foundStreams.length,
remaining: totalToTest - tested
});
if (tested >= totalToTest) {
clearInterval(progressInterval);
}
}, 200);
// Send found streams progressively
let streamIndex = 0;
const streamInterval = setInterval(() => {
if (!this.isRunning) {
clearInterval(streamInterval);
return;
}
if (streamIndex < foundStreams.length) {
callbacks.onStreamFound({
stream: foundStreams[streamIndex]
});
streamIndex++;
} else {
clearInterval(streamInterval);
}
}, 800);
// Complete after ~7.7 seconds
this.timeoutId = setTimeout(() => {
if (!this.isRunning) return;
callbacks.onComplete({
total_found: foundStreams.length,
duration: 7.7
});
this.isRunning = false;
}, 7700);
}
close() {
this.isRunning = false;
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
}