From 8bf92e65988ac5e6151ca3724fddcda2cf298806 Mon Sep 17 00:00:00 2001 From: eduard256 Date: Fri, 21 Nov 2025 22:40:38 +0300 Subject: [PATCH 1/7] 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 --- webui/web/index.html | 15 -- webui/web/js/api/camera-search.js | 11 +- webui/web/js/api/stream-discovery.js | 15 +- webui/web/js/main.js | 34 ++++- webui/web/js/mock/mock-data.js | 209 +++++++++++++++++++++++++++ 5 files changed, 265 insertions(+), 19 deletions(-) create mode 100644 webui/web/js/mock/mock-data.js diff --git a/webui/web/index.html b/webui/web/index.html index 021137a..2e915be 100644 --- a/webui/web/index.html +++ b/webui/web/index.html @@ -207,21 +207,6 @@

Starting scan...

-
-
- 0 - Tested -
-
- 0 - Found -
-
- 0 - Remaining -
-
- diff --git a/webui/web/js/main.js b/webui/web/js/main.js index 30f531b..6a9181b 100644 --- a/webui/web/js/main.js +++ b/webui/web/js/main.js @@ -3,7 +3,7 @@ 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 { StreamList } from './ui/stream-list.js'; import { ConfigPanel } from './ui/config-panel.js'; import { FrigateGenerator } from './config-generators/frigate/index.js'; import { showToast } from './utils/toast.js'; @@ -30,7 +30,7 @@ class StrixApp { } this.searchForm = new SearchForm(); - this.carousel = new StreamCarousel(); + this.streamList = new StreamList(); this.configPanel = new ConfigPanel(); this.currentAddress = ''; @@ -95,24 +95,6 @@ class StrixApp { this.showScreen('config'); }); - // Carousel navigation - document.getElementById('carousel-prev').addEventListener('click', () => { - this.carousel.prev(); - }); - - document.getElementById('carousel-next').addEventListener('click', () => { - this.carousel.next(); - }); - - // Keyboard navigation - document.addEventListener('keydown', (e) => { - const currentScreen = document.querySelector('.screen.active').id; - if (currentScreen === 'screen-discovery') { - if (e.key === 'ArrowLeft') this.carousel.prev(); - if (e.key === 'ArrowRight') this.carousel.next(); - } - }); - // Screen 4: Configuration output document.getElementById('btn-back-to-streams').addEventListener('click', () => { this.isSelectingSubStream = false; @@ -303,9 +285,6 @@ class StrixApp { resetDiscoveryUI() { document.getElementById('progress-fill').style.width = '0%'; document.getElementById('progress-text').textContent = 'Starting scan...'; - document.getElementById('stat-tested').textContent = '0'; - document.getElementById('stat-found').textContent = '0'; - document.getElementById('stat-remaining').textContent = '0'; document.getElementById('streams-section').classList.add('hidden'); this.currentStreams = []; } @@ -316,9 +295,6 @@ class StrixApp { document.getElementById('progress-fill').style.width = `${percentage}%`; document.getElementById('progress-text').textContent = `Testing streams... ${Math.round(percentage)}%`; - document.getElementById('stat-tested').textContent = data.tested; - document.getElementById('stat-found').textContent = data.found; - document.getElementById('stat-remaining').textContent = data.remaining; } handleStreamFound(data) { @@ -330,8 +306,8 @@ class StrixApp { streamsSection.classList.remove('hidden'); } - // Update carousel - this.carousel.render(this.currentStreams, (stream, index) => { + // Update stream list + this.streamList.render(this.currentStreams, (stream, index) => { this.selectStream(stream, index); }); } diff --git a/webui/web/js/ui/stream-list.js b/webui/web/js/ui/stream-list.js new file mode 100644 index 0000000..bddc934 --- /dev/null +++ b/webui/web/js/ui/stream-list.js @@ -0,0 +1,111 @@ +export class StreamList { + constructor() { + this.listContainer = document.getElementById('streams-list'); + this.streams = []; + this.onUseCallback = null; + this.expandedIndex = null; + } + + render(streams, onUseCallback) { + this.streams = streams; + this.onUseCallback = onUseCallback; + + // Render stream items + this.listContainer.innerHTML = streams.map((stream, index) => this.renderItem(stream, index)).join(''); + + // Attach event listeners + this.attachEventListeners(); + } + + renderItem(stream, index) { + const icon = this.getStreamIcon(stream.type); + const isExpanded = this.expandedIndex === index; + const truncatedUrl = this.truncateURL(stream.url, 60); + + return ` +
+
+
+
+
+ ${icon} + ${stream.type} +
+
${truncatedUrl}
+
+ +
+ +
+
+
${stream.url}
+ ${stream.resolution ? `
Resolution: ${stream.resolution}
` : ''} + ${stream.codec ? `
Codec: ${stream.codec}${stream.fps ? ` • ${stream.fps} fps` : ''}${stream.bitrate ? ` • ${Math.round(stream.bitrate / 1000)} Kbps` : ''}
` : ''} + ${stream.has_audio ? '
Audio: Yes
' : ''} +
+
+ `; + } + + truncateURL(url, maxLength = 60) { + if (url.length <= maxLength) { + return url; + } + return url.substring(0, maxLength) + '...'; + } + + getStreamIcon(type) { + const icons = { + 'FFMPEG': '', + 'ONVIF': '', + 'JPEG': '', + 'MJPEG': '', + 'HLS': '', + 'HTTP_VIDEO': '' + }; + return icons[type] || icons['FFMPEG']; + } + + attachEventListeners() { + // Click on header to toggle + this.listContainer.querySelectorAll('.stream-item-header').forEach(header => { + header.addEventListener('click', (e) => { + // Don't toggle if clicking "Use Stream" button + if (e.target.closest('.btn-use-stream')) { + return; + } + + const index = parseInt(header.dataset.index); + this.toggleExpand(index); + }); + }); + + // Use Stream buttons + this.listContainer.querySelectorAll('.btn-use-stream').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); // Prevent toggle + const index = parseInt(e.target.dataset.index); + if (this.onUseCallback) { + this.onUseCallback(this.streams[index], index); + } + }); + }); + } + + toggleExpand(index) { + if (this.expandedIndex === index) { + // Collapse if already expanded + this.expandedIndex = null; + } else { + // Expand new item + this.expandedIndex = index; + } + + // Re-render to update state + this.render(this.streams, this.onUseCallback); + } +} From 596cf1ccdc9224fbeba1273d656e3c1400f390d4 Mon Sep 17 00:00:00 2001 From: eduard256 Date: Fri, 21 Nov 2025 23:37:02 +0300 Subject: [PATCH 5/7] Add interactive tooltips to camera configuration form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлены информационные тултипы для всех полей формы настройки камеры с подробными описаниями, примерами использования и рекомендациями. Улучшает пользовательский опыт и помогает пользователям правильно заполнить форму. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- webui/web/css/main.css | 122 ++++++++++++++++++++++++++++++ webui/web/index.html | 167 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 272 insertions(+), 17 deletions(-) diff --git a/webui/web/css/main.css b/webui/web/css/main.css index d8eb3e3..e11ca20 100644 --- a/webui/web/css/main.css +++ b/webui/web/css/main.css @@ -1043,6 +1043,128 @@ body { display: none; } +/* ===== TOOLTIPS ===== */ +.label-with-info { + display: flex; + align-items: center; + gap: var(--space-2); +} + +.info-icon { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + cursor: help; + color: var(--text-tertiary); + transition: color var(--transition-fast); +} + +.info-icon:hover { + color: var(--purple-primary); +} + +.info-icon svg { + width: 16px; + height: 16px; +} + +.tooltip { + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + background: var(--bg-elevated); + border: 1px solid var(--purple-primary); + border-radius: 8px; + padding: var(--space-4); + width: 320px; + max-width: 90vw; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--purple-glow); + z-index: 1000; + opacity: 0; + visibility: hidden; + transition: opacity var(--transition-fast), visibility var(--transition-fast); + pointer-events: none; +} + +/* Tooltip opens downward */ +.tooltip.tooltip-down { + bottom: auto; + top: calc(100% + 8px); +} + +.info-icon:hover .tooltip { + opacity: 1; + visibility: visible; +} + +.tooltip::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 6px solid transparent; + border-top-color: var(--purple-primary); +} + +/* Arrow for downward tooltip */ +.tooltip.tooltip-down::after { + top: auto; + bottom: 100%; + border-top-color: transparent; + border-bottom-color: var(--purple-primary); +} + +.tooltip-title { + font-weight: 600; + color: var(--purple-primary); + margin-bottom: var(--space-2); + font-size: var(--text-sm); +} + +.tooltip-text { + font-size: var(--text-xs); + line-height: 1.5; + color: var(--text-secondary); + margin-bottom: var(--space-3); +} + +.tooltip-text:last-child { + margin-bottom: 0; +} + +.tooltip-examples { + margin-top: var(--space-3); + padding-top: var(--space-3); + border-top: 1px solid var(--border-color); +} + +.tooltip-examples-title { + font-weight: 600; + color: var(--text-primary); + font-size: var(--text-xs); + margin-bottom: var(--space-2); +} + +.tooltip-example { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--purple-light); + background: var(--bg-secondary); + padding: var(--space-1) var(--space-2); + border-radius: 4px; + margin-bottom: var(--space-1); + display: block; +} + +.tooltip-example:last-child { + margin-bottom: 0; +} + /* ===== UTILITIES ===== */ .hidden { display: none !important; diff --git a/webui/web/index.html b/webui/web/index.html index 90783a8..df4dd7c 100644 --- a/webui/web/index.html +++ b/webui/web/index.html @@ -82,7 +82,26 @@

Camera Configuration

- +
- +
- +
- +
+
+ + +
+
Advanced
-
- - -
- +
- + Date: Sat, 22 Nov 2025 00:03:54 +0300 Subject: [PATCH 6/7] Improve WebUI UX with tooltips, auto-fill and button visibility - Add informational tooltips to all configuration fields - Reorder tabs: Frigate first, then Go2RTC, then URL - Hide Copy/Download buttons on Frigate tab until config is generated - Auto-fill username field with "admin" as default value - Smart pre-fill network address based on server IP (first 3 octets) - Add tooltips for Main Stream, Sub Stream, and all buttons - Improve user guidance throughout the configuration flow --- webui/web/css/main.css | 78 ++++++++++++++ webui/web/index.html | 239 ++++++++++++++++++++++++++++++++++++----- webui/web/js/main.js | 60 ++++++++++- 3 files changed, 348 insertions(+), 29 deletions(-) diff --git a/webui/web/css/main.css b/webui/web/css/main.css index e11ca20..83079ba 100644 --- a/webui/web/css/main.css +++ b/webui/web/css/main.css @@ -774,6 +774,9 @@ body { } .stream-label { + display: flex; + align-items: center; + gap: var(--space-2); font-size: var(--text-xs); font-weight: 600; color: var(--text-tertiary); @@ -1032,6 +1035,81 @@ body { transform: translateY(0); } +/* Button with tooltip wrapper */ +.button-with-tooltip { + position: relative; + width: 100%; +} + +.button-with-tooltip .btn-generate { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); +} + +/* Button with tooltip in secondary-actions */ +.secondary-actions .button-with-tooltip { + flex: 1.2; + width: auto; +} + +.secondary-actions .button-with-tooltip .btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); +} + +.secondary-actions .button-with-tooltip:last-child { + flex: 0.8; +} + +/* Info icon inside button */ +.info-icon-button { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + cursor: help; + color: rgba(255, 255, 255, 0.7); + transition: color var(--transition-fast); +} + +.info-icon-button:hover { + color: rgba(255, 255, 255, 1); +} + +.info-icon-button svg { + width: 18px; + height: 18px; +} + +/* Info icon inside outline button */ +.info-icon-button-outline { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + cursor: help; + color: var(--text-tertiary); + transition: color var(--transition-fast); +} + +.info-icon-button-outline:hover { + color: var(--purple-primary); +} + +.info-icon-button-outline svg { + width: 18px; + height: 18px; +} + .frigate-output-section { margin-top: var(--space-6); padding-top: var(--space-6); diff --git a/webui/web/index.html b/webui/web/index.html index df4dd7c..21d573d 100644 --- a/webui/web/index.html +++ b/webui/web/index.html @@ -42,7 +42,30 @@
- + @@ -372,13 +396,51 @@
-

Main Stream

+
+ Main Stream + + + + + +
+
Main Stream
+

The primary high-resolution video stream from your camera. This stream is typically used for recording and high-quality viewing.

+
+
Common uses:
+ Recording to disk + Live HD viewing + High-quality playback +
+

Resolution is usually 1080p (1920×1080) or higher. Higher resolution means better quality but requires more bandwidth and storage.

+
+
+