From 8398832960363f865dbe0d4ac255b93a41fdac64 Mon Sep 17 00:00:00 2001 From: eduard256 Date: Wed, 8 Apr 2026 08:54:40 +0000 Subject: [PATCH 1/9] Redesign HomeKit page, add design system reference Rebuild homekit.html with centered layout, cleaner PIN input, and consistent styling matching the rest of the frontend. Add www/design-system.html as a living component reference for all UI elements used across the Strix frontend. --- www/design-system.html | 2037 ++++++++++++++++++++++++++++++++++++++++ www/homekit.html | 263 +++--- 2 files changed, 2143 insertions(+), 157 deletions(-) create mode 100644 www/design-system.html diff --git a/www/design-system.html b/www/design-system.html new file mode 100644 index 0000000..7c80c4d --- /dev/null +++ b/www/design-system.html @@ -0,0 +1,2037 @@ + + + + + + + Strix - Design System + + + + +
+
+

Strix Design System

+

All components used across the Strix frontend

+
+ + +
+
1. Colors
+
CSS variables from :root
+ +
+
+
Backgrounds
+
+
+
+
+
+
+
+
+
Purple Accent
+
+
+
+
+
+
+
+
Semantic
+
+
+
+
+
+
+
+ +
+
+
Text Hierarchy
+
--text-primary: Main content text
+
--text-secondary: Labels, descriptions
+
--text-tertiary: Hints, placeholders
+
--text-disabled: Disabled elements
+
+
+
+ + +
+
2. Typography
+
Headings, labels, mono text
+ +
+

STRIX

+
Page Title (.page-title)
+
Screen Title (.screen-title)
+
Section Title (.section-title)
+
Label (.label)
+
Field Label (.field-label)
+
+
Monospace text (--font-mono)
+
+
+ + +
+
3. Buttons
+
All button variants
+ +
+
Primary
+
+ + +
+ +
Primary Large (full width)
+
+ +
+ +
Outline
+
+ +
+ +
Small Buttons
+
+ + +
+ +
Stop / Danger
+
+ + +
+ +
Add Sub / Secondary Action
+
+ +
+ +
Save (Green)
+
+ +
+ +
Back Navigation
+ + +
+ +
Add (+) Button
+
+ +
+ +
Use as Stream Button (card action)
+
+ +
+
+
+ + +
+
4. Form Elements
+
Inputs, selects, toggles
+ +
+
+
+
Standard Input
+
+ + +

IP address or stream URL

+
+
+
+
Large Input
+
+ +
+
+
+ +
+
+
Validated Input (readonly)
+
+
+ + + + +
+
+
+
+
Password with Toggle
+
+
+ + +
+
+
+
+ +
+
+
Select Dropdown
+
+
HW Acceleration
+ +
+
+
+
Small Inputs in Row
+
+
FPS
+
Width
+
Height
+
+
+
+ +
Add Row (input + button)
+
+ + +
+ +
Toggle Switches
+
+
Enabled (on)
+
Disabled (off)
+
+ +
+
Optional Label
+
+ + +
+
+
+ + +
+
5. Badges & Tags
+
Status indicators, type labels
+ +
+
Type Badges
+
+ standard + homekit + unreachable +
+ +
Status Badges
+
+
running
+
done
+
+ +
Mode Badge
+
+ Stream Testing + sub +
+ +
Tags (selected items)
+
+ + preset + Top 1000 Stream Patterns + + + + brand + Hikvision + + +
+ +
Card Tags (codec overlays)
+
+ H264 + AAC + MJPEG +
+ +
Latency Colors
+
+ 42ms (fast) + 320ms (medium) + 850ms (slow) +
+
+
+ + +
+
6. Cards
+
Stream cards, media cards
+ +
+
Stream Card (info)
+
+
+
Main Stream
+
rtsp://admin:pass@192.168.1.100:554/stream1
+
+
+
Sub Stream
+
rtsp://admin:pass@192.168.1.100:554/stream2
+
+
+ +
Media Card (test result)
+
+
+
+
+ + + + + +
+
+ H264 + AAC +
+ 1920x1080 +
+
+
rtsp://admin:***@192.168.1.100:554/stream1
+
+ 42ms + 1920x1080 +
+
+
+ +
+
+ +
+
+
+ + + + + +
+
+ H265 +
+ 640x480 +
+
+
rtsp://admin:***@192.168.1.100:554/stream2
+
+ 280ms + 640x480 +
+
+
+ +
+
+
+
+
+ + +
+
7. Progress & Status
+
Status bars, counters, progress, spinners
+ +
+
Status Bar (complete component)
+
+
+
running
+ sess_abc123 +
+
+
48
total
+
32
tested
+
5
alive
+
3
screenshots
+
+
+
+
+
+ +
+ +
Loading Spinner
+
+
+ Building stream URLs... +
+
+
+ + +
+
8. Collapsible Sections
+
Expandable settings, grouped results
+ +
+
Expand Button (settings)
+ + + +
+ +
Group with Collapse (results)
+
+
+ + Recommended - Main + (3) +
+
+
Cards would go here...
+
+
+
+
+ + +
+
9. Feedback
+
Toasts, errors, results, info boxes, notices
+ +
+
+
+
Error Box
+
Connection error: timeout after 5s
+
+
+
Info Box
+
+
How to use
+
Add these URLs to your NVR software.
+
+
+
+ +
+ +
+
+
Result OK
+
Added to go2rtc: camera_main. Open go2rtc UI
+
+
+
Result Error
+
camera_main: go2rtc not found
+
+
+ +
+ +
Notice Banner
+
+
Frigate NVR not detected
+
If you have Frigate NVR, we recommend running Strix on the same server.
+
+ +
+ +
Toast (click to demo)
+ +
+
+ + +
+
10. Config Panel
+
Code display with diff highlighting
+ +
+
+ Generated Config +
+ + +
+
+
go2rtc: + streams: + camera_main: + - rtsp://admin:pass@192.168.1.100:554/stream1 + camera_sub: + - rtsp://admin:pass@192.168.1.100:554/stream2 + +cameras: + camera: + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/camera_sub + input_args: preset-rtsp-restream + roles: + - detect + - path: rtsp://127.0.0.1:8554/camera_main + input_args: preset-rtsp-restream + roles: + - record
+
+ +
+ +
Config Example Block (copyable)
+
+
go2rtc.yaml
+
streams: + 'camera_main': + - rtsp://admin:pass@192.168.1.100:554/stream1
+ +
+
+ + +
+
11. Lists & Tables
+
Stream lists, device info tables
+ +
+
+
+
Streams Box (scrollable list)
+
3 streams
+
+
rtsp://admin:pass@192.168.1.100:554/stream1
+
rtsp://admin:pass@192.168.1.100:554/stream2
+
http://192.168.1.100:80/video.cgi
+
+
+
+
Device Info Table
+
+
IP Address192.168.1.100
+
ModelDS-2CD2043G2-I
+
VendorHikvision
+
MACAA:BB:CC:DD:EE:FF
+
+
+
+ +
+ +
Stream URL with Copy Button
+
+
Main Stream
+
+
rtsp://admin:pass@192.168.1.100:554/stream1
+ +
+
+
+
+ + +
+
12. Mobile Tabs
+
Tab switcher for two-column layouts on mobile
+ +
+
+
+ + +
+
+
+
+ + +
+
13. Sticky Bottom Bar
+
Fixed action bar at page bottom (not shown fixed in this demo)
+ +
+
+ +
+
+
+ + +
+
14. Autocomplete Dropdown
+
Searchable dropdown with type badges
+ +
+
+ +
+
+ + preset + Top 1000 + + +
+
+ brand + Hikvision +
+
+ model + Hikvision DS-2CD2043G2-I +
+
+ model + Hikvision DS-2CD2347G2-LU +
+
+
+
+
+ + +
+
15. SVG Icons
+
All inline SVG icons used across pages (never use emoji or icon fonts)
+ +
+
+
+ +
back
+
+
+ +
info
+
+
+ +
add
+
+
+ +
close
+
+
+ +
chevron
+
+
+ +
chevron-down
+
+
+ +
check
+
+
+ + + + +
copy
+
+
+ + + +
eye
+
+
+ + + + + +
image
+
+
+
+
+
+ + + + + + + + diff --git a/www/homekit.html b/www/homekit.html index deb1b84..acd90e3 100644 --- a/www/homekit.html +++ b/www/homekit.html @@ -42,103 +42,84 @@ } .screen { - min-height: 100vh; padding: 1.5rem; - display: flex; align-items: flex-start; justify-content: center; + min-height: 100vh; + padding: 1.5rem; + display: flex; + align-items: center; + justify-content: center; animation: fadeIn var(--transition-base); } - .container { max-width: 520px; width: 100%; margin-top: 6vh; } + .container { max-width: 480px; width: 100%; } @media (min-width: 768px) { .screen { padding: 3rem 1.5rem; } + .container { max-width: 540px; } } - .btn-back { - display: inline-flex; align-items: center; gap: 0.5rem; - background: none; border: none; - color: var(--text-secondary); font-size: 0.875rem; - font-family: var(--font-primary); cursor: pointer; - padding: 0.5rem 0; margin-bottom: 2rem; - transition: color var(--transition-fast); - } - .btn-back:hover { color: var(--purple-primary); } + .hero { text-align: center; margin-bottom: 3rem; } - .card { - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 12px; - padding: 2rem; - text-align: center; + .title { + font-size: 4rem; font-weight: 700; + letter-spacing: 0.1em; margin-bottom: 0.5rem; + background: linear-gradient(135deg, var(--purple-light), var(--purple-primary)); + -webkit-background-clip: text; -webkit-text-fill-color: transparent; + background-clip: text; } - .card-icon { - width: 48px; height: 48px; - margin: 0 auto 1.25rem; - color: var(--purple-light); + .subtitle { font-size: 0.875rem; color: var(--text-secondary); } + + .form-group { margin-bottom: 1.5rem; } + + .label { + display: flex; align-items: center; gap: 0.5rem; + font-size: 0.875rem; font-weight: 500; + color: var(--text-secondary); margin-bottom: 0.75rem; + justify-content: center; } - .card-title { - font-size: 1.25rem; font-weight: 600; - margin-bottom: 0.5rem; + /* Info icon + tooltip */ + .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); } - .card-badge { - display: inline-block; - padding: 0.25rem 0.75rem; - background: rgba(139, 92, 246, 0.15); - border: 1px solid rgba(139, 92, 246, 0.3); - border-radius: 6px; - font-size: 0.75rem; font-weight: 600; - color: var(--purple-light); - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 1.5rem; + .info-icon:hover { color: var(--purple-primary); } + .info-icon svg { width: 16px; height: 16px; } + + .tooltip { + position: absolute; top: calc(100% + 8px); left: 50%; + transform: translateX(-50%); + background: var(--bg-elevated); + border: 1px solid var(--purple-primary); + border-radius: 8px; padding: 1rem; + 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; } - .device-info { - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 1rem; - margin-bottom: 1.5rem; - text-align: left; + .tooltip::after { + content: ''; position: absolute; bottom: 100%; left: 50%; + transform: translateX(-50%); + border: 6px solid transparent; border-bottom-color: var(--purple-primary); } - .device-row { - display: flex; justify-content: space-between; - padding: 0.375rem 0; - font-size: 0.8125rem; - } + .info-icon:hover .tooltip { opacity: 1; visibility: visible; } - .device-row:not(:last-child) { - border-bottom: 1px solid rgba(139, 92, 246, 0.07); - } - - .device-label { color: var(--text-tertiary); } - .device-value { color: var(--text-primary); font-family: var(--font-mono); font-size: 0.75rem; } + .tooltip-title { font-weight: 600; color: var(--purple-primary); margin-bottom: 0.5rem; font-size: 0.875rem; } + .tooltip-text { font-size: 0.75rem; line-height: 1.5; color: var(--text-secondary); } /* PIN input */ - .pin-section { margin-bottom: 1.5rem; } - - .pin-label { - font-size: 0.875rem; font-weight: 500; - color: var(--text-secondary); - margin-bottom: 0.75rem; - } - - .pin-hint { - font-size: 0.75rem; color: var(--text-tertiary); - margin-top: 0.625rem; - } - .pin-row { display: flex; align-items: center; justify-content: center; gap: 0; } - .pin-group { - display: flex; gap: 0.375rem; - } + .pin-group { display: flex; gap: 0.375rem; } .pin-separator { font-size: 1.5rem; font-weight: 300; @@ -149,8 +130,8 @@ } .pin-digit { - width: 44px; height: 56px; - background: var(--bg-primary); + width: 57px; height: 69px; + background: var(--bg-secondary); border: 2px solid var(--border-color); border-radius: 8px; color: var(--text-primary); @@ -195,27 +176,28 @@ .pin-group { gap: 0.25rem; } } - /* Error message */ - .error-msg { - margin-top: 1rem; padding: 0.75rem 1rem; - background: rgba(239, 68, 68, 0.08); - border: 1px solid rgba(239, 68, 68, 0.2); + /* Error */ + .error-box { + padding: 1rem; + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 8px; - font-size: 0.8125rem; color: var(--error); - text-align: left; + color: var(--error); + font-size: 0.875rem; + margin-bottom: 1.5rem; display: none; animation: fadeIn var(--transition-fast); } - .error-msg.visible { display: block; } + .error-box.visible { display: block; } /* Buttons */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; padding: 1rem 1.5rem; border-radius: 8px; - font-size: 0.9375rem; font-weight: 600; font-family: var(--font-primary); + font-size: 1rem; font-weight: 600; font-family: var(--font-primary); cursor: pointer; transition: all var(--transition-fast); - border: none; outline: none; width: 100%; + border: none; outline: none; } .btn-primary { @@ -229,14 +211,20 @@ .btn-primary:active:not(:disabled) { transform: translateY(0); } .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } + .btn-large { width: 100%; padding: 1.5rem; font-size: 1.125rem; } + .btn-outline { + display: inline-flex; align-items: center; justify-content: center; + gap: 0.5rem; padding: 1rem 1.5rem; border-radius: 8px; + font-size: 1rem; font-weight: 600; font-family: var(--font-primary); + cursor: pointer; transition: all var(--transition-fast); background: transparent; color: var(--text-secondary); - border: 1px solid var(--border-color); + border: 1px solid var(--border-color); width: 100%; margin-top: 0.75rem; } .btn-outline:hover { border-color: var(--purple-primary); color: var(--purple-light); } - /* Pairing state */ + /* Pairing spinner */ .pairing-spinner { width: 20px; height: 20px; border: 2px solid rgba(255, 255, 255, 0.3); @@ -258,43 +246,38 @@
- - -
- - - - - -

HomeKit Camera

-
Apple HomeKit
- -
- -
-
Enter the 8-digit code from your camera
- -
-
- - -
- - -
-
- -
Printed on the camera or in the manual
-
- -
- - - +
+

HOME KIT

+ +
+ + +
+
+ - +
+ - +
+
+
+ +
+ + +
@@ -315,34 +298,6 @@ var mdnsPaired = params.get('mdns_paired') || ''; var mdnsDeviceId = params.get('mdns_device_id') || ''; - // title - if (mdnsName) document.getElementById('card-title').textContent = mdnsName; - - // device info - var infoDiv = document.getElementById('device-info'); - var rows = []; - if (ip) rows.push(['IP Address', ip]); - if (mdnsModel) rows.push(['Model', mdnsModel]); - if (mdnsCategory) rows.push(['Category', mdnsCategory]); - if (vendor) rows.push(['Vendor', vendor]); - if (mac) rows.push(['MAC', mac]); - - rows.forEach(function(r) { - var row = document.createElement('div'); - row.className = 'device-row'; - var label = document.createElement('span'); - label.className = 'device-label'; - label.textContent = r[0]; - var value = document.createElement('span'); - value.className = 'device-value'; - value.textContent = r[1]; - row.appendChild(label); - row.appendChild(value); - infoDiv.appendChild(row); - }); - - if (rows.length === 0) infoDiv.style.display = 'none'; - // PIN input -- 8 digits: 3-2-3 var pinGroups = [3, 2, 3]; var inputs = []; @@ -444,14 +399,14 @@ } function showError(msg) { - var el = document.getElementById('error-msg'); + var el = document.getElementById('error-box'); el.textContent = msg; el.classList.add('visible'); inputs.forEach(function(input) { input.classList.add('error'); }); } function hideError() { - document.getElementById('error-msg').classList.remove('visible'); + document.getElementById('error-box').classList.remove('visible'); } function showSuccess() { @@ -515,9 +470,7 @@ var data = await r.json(); showSuccess(); - btnPair.textContent = ''; - var check = document.createTextNode('Paired!'); - btnPair.appendChild(check); + btnPair.textContent = 'Paired!'; // redirect to create.html with the homekit URL setTimeout(function() { @@ -540,10 +493,6 @@ } // navigation - document.getElementById('btn-back').addEventListener('click', function() { - window.location.href = 'index.html'; - }); - document.getElementById('btn-standard').addEventListener('click', function() { var p = new URLSearchParams(); if (ip) p.set('ip', ip); From 89c5d83a6fe3d0447aade4a8826f1ac58299d85f Mon Sep 17 00:00:00 2001 From: eduard256 Date: Wed, 8 Apr 2026 09:30:31 +0000 Subject: [PATCH 2/9] Refine HomeKit page: add Apple HomeKit logo, centered layout, back button Replace text-only header with official HomeKit house icon and "Apple HomeKit" label. Pin input centered on screen, back button aligned to container edge. Remove device info table and decorative elements for a cleaner look matching the rest of the frontend. --- www/homekit.html | 65 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/www/homekit.html b/www/homekit.html index acd90e3..ba5c5b9 100644 --- a/www/homekit.html +++ b/www/homekit.html @@ -57,17 +57,41 @@ .container { max-width: 540px; } } - .hero { text-align: center; margin-bottom: 3rem; } - - .title { - font-size: 4rem; font-weight: 700; - letter-spacing: 0.1em; margin-bottom: 0.5rem; - background: linear-gradient(135deg, var(--purple-light), var(--purple-primary)); - -webkit-background-clip: text; -webkit-text-fill-color: transparent; - background-clip: text; + .back-wrapper { + position: absolute; top: 1.5rem; + left: 50%; transform: translateX(-50%); + width: 100%; max-width: 480px; + padding: 0 1.5rem; + z-index: 10; } - .subtitle { font-size: 0.875rem; color: var(--text-secondary); } + @media (min-width: 768px) { + .back-wrapper { max-width: 540px; } + } + + .btn-back { + display: inline-flex; align-items: center; gap: 0.5rem; + background: none; border: none; + color: var(--text-secondary); font-size: 0.875rem; + font-family: var(--font-primary); cursor: pointer; + padding: 0.5rem 0; + transition: color var(--transition-fast); + } + .btn-back:hover { color: var(--purple-primary); } + + .hero { text-align: center; margin-bottom: 2.5rem; } + + .title { + font-size: 1.25rem; font-weight: 600; + letter-spacing: 0.03em; + color: var(--text-primary); + } + + .homekit-logo { + width: 72px; height: 72px; + margin: 0 auto; + filter: drop-shadow(0 4px 16px rgba(255, 171, 31, 0.3)); + } .form-group { margin-bottom: 1.5rem; } @@ -75,7 +99,6 @@ display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; font-weight: 500; color: var(--text-secondary); margin-bottom: 0.75rem; - justify-content: center; } /* Info icon + tooltip */ @@ -244,10 +267,26 @@ +
+ +
+
-

HOME KIT

+ +

Apple HomeKit

@@ -493,6 +532,10 @@ } // navigation + document.getElementById('btn-back').addEventListener('click', function() { + window.location.href = 'index.html'; + }); + document.getElementById('btn-standard').addEventListener('click', function() { var p = new URLSearchParams(); if (ip) p.set('ip', ip); From 699ddda39bf2a81e6073c483637832e211da056a Mon Sep 17 00:00:00 2001 From: eduard256 Date: Wed, 8 Apr 2026 09:39:36 +0000 Subject: [PATCH 3/9] Update design system with centered layout, PIN input, floating back button Add true-center layout pattern, back-wrapper for floating navigation, PIN digit input component with all states, and centered page demo with HomeKit logo example. Document PIN input JS pattern. --- www/design-system.html | 216 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 214 insertions(+), 2 deletions(-) diff --git a/www/design-system.html b/www/design-system.html index 7c80c4d..739463f 100644 --- a/www/design-system.html +++ b/www/design-system.html @@ -88,8 +88,8 @@ 2. PAGE LAYOUTS ============================================================ */ - /* --- 2a. Centered layout (index.html, homekit.html) --- - For single-purpose pages: one form, one action. */ + /* --- 2a. Centered layout (index.html) --- + For entry pages: one form, one action. Content near top. */ .screen-centered { min-height: 100vh; padding: 1.5rem; @@ -110,6 +110,43 @@ .container-narrow { max-width: 540px; } } + /* --- 2a-alt. True-centered layout (homekit.html) --- + For single-action pages: content vertically centered on screen. + Back button floats outside the container via .back-wrapper. */ + .screen-true-center { + min-height: 100vh; + padding: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn var(--transition-base); + } + + .container-true-center { + max-width: 480px; + width: 100%; + } + + @media (min-width: 768px) { + .screen-true-center { padding: 3rem 1.5rem; } + .container-true-center { max-width: 540px; } + } + + /* --- 2e. Floating back button (for centered layouts) --- + Positioned at top, horizontally aligned wider than container + so it sits outside the content area. */ + .back-wrapper { + position: absolute; top: 1.5rem; + left: 50%; transform: translateX(-50%); + width: 100%; max-width: 600px; + padding: 0 1.5rem; + z-index: 10; + } + + @media (min-width: 768px) { + .back-wrapper { max-width: 660px; } + } + /* --- 2b. Standard layout (most pages) --- For content pages with back button and scrollable content. */ .screen { @@ -451,6 +488,70 @@ .toggle.on::after { transform: translateX(16px); } + /* --- 5i. PIN digit input (homekit.html) --- */ + .pin-row { + display: flex; align-items: center; justify-content: center; + gap: 0; + } + + .pin-group { display: flex; gap: 0.375rem; } + + .pin-separator { + font-size: 1.5rem; font-weight: 300; + color: var(--text-tertiary); + padding: 0 0.5rem; + line-height: 1; + user-select: none; + } + + .pin-digit { + width: 57px; height: 69px; + background: var(--bg-secondary); + border: 2px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-family: var(--font-mono); + font-size: 1.375rem; font-weight: 600; + text-align: center; + outline: none; + transition: all var(--transition-fast); + caret-color: var(--purple-primary); + -moz-appearance: textfield; + } + + .pin-digit::-webkit-inner-spin-button, + .pin-digit::-webkit-outer-spin-button { + -webkit-appearance: none; margin: 0; + } + + .pin-digit:focus { + border-color: var(--purple-primary); + box-shadow: 0 0 0 3px var(--purple-glow); + } + + .pin-digit.filled { + border-color: var(--purple-light); + background: rgba(139, 92, 246, 0.06); + } + + .pin-digit.error { + border-color: var(--error); + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2); + animation: shake 0.4s ease; + } + + .pin-digit.success { + border-color: var(--success); + box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2); + } + + @media (max-width: 400px) { + .pin-digit { width: 36px; height: 48px; font-size: 1.125rem; } + .pin-separator { padding: 0 0.25rem; font-size: 1.25rem; } + .pin-group { gap: 0.25rem; } + } + + /* ============================================================ 6. TOOLTIPS & INFO ICONS ============================================================ */ @@ -1397,6 +1498,46 @@
+ +
+
4b. PIN Digit Input
+
Segmented code entry (homekit.html pairing)
+ +
+
PIN Input (3-2-3 format)
+
+
+ + + +
+ - +
+ + +
+ - +
+ + + +
+
+ +
States
+
+ + empty + + filled + + error + + success +
+
+
+
5. Badges & Tags
@@ -1800,6 +1941,37 @@ cameras:
+ +
+
15. Centered Page Pattern
+
True-center layout with floating back button (homekit.html)
+ +
+ +
+ +
+ + +
+
+ + + + + + + +
Apple HomeKit
+
+
Content centered on screen, back button floats at top
+
+
+
+
15. SVG Icons
@@ -2030,6 +2202,46 @@ cameras: document.getElementById('btn-back').addEventListener('click', function() { history.back(); }); + + For centered layouts (homekit.html), the back button is placed + OUTSIDE the container in a .back-wrapper div with position:absolute, + aligned wider than the content so it floats at the top-left of + the content area but not the screen edge. + ================================================================ */ + + /* ================================================================ + PIN INPUT PATTERN + For segmented code entry. Each digit is a separate . + Auto-advance on input, backspace goes to previous field. + Supports paste (distributes digits across fields). + + HTML structure: +
+
+ - +
+ - +
+
+ + JS creates inputs dynamically: + var pinGroups = [3, 2, 3]; // digits per group + var inputs = []; + pinGroups.forEach(function(count, gi) { + var group = document.getElementById('group-' + gi); + for (var i = 0; i < count; i++) { + var input = document.createElement('input'); + input.type = 'text'; + input.inputMode = 'numeric'; + input.maxLength = 1; + input.className = 'pin-digit'; + input.autocomplete = 'off'; + group.appendChild(input); + inputs.push(input); + } + }); + + States: .filled (has value), .error (wrong), .success (paired) ================================================================ */ From 1291e6a5b6f6a71d68531ed899f8b80ee461f951 Mon Sep 17 00:00:00 2001 From: eduard256 Date: Wed, 8 Apr 2026 09:49:25 +0000 Subject: [PATCH 4/9] Add frontend_design_strix skill for UI page creation Design guide with principles, layout patterns, component usage, navigation rules, and checklist. References homekit.html as the design gold standard and design-system.html for components. --- .claude/skills/frontend_design_strix/SKILL.md | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 .claude/skills/frontend_design_strix/SKILL.md diff --git a/.claude/skills/frontend_design_strix/SKILL.md b/.claude/skills/frontend_design_strix/SKILL.md new file mode 100644 index 0000000..68c529a --- /dev/null +++ b/.claude/skills/frontend_design_strix/SKILL.md @@ -0,0 +1,211 @@ +--- +name: frontend_design_strix +description: Create or redesign frontend pages for Strix. Use when building new HTML pages, redesigning existing ones, or working on any UI task in the www/ directory. Covers design principles, layout patterns, and component usage. +disable-model-invocation: true +--- + +# Strix Frontend Design + +You are creating or modifying a frontend page for Strix. Your goal is to produce a page that looks **identical in quality** to the existing pages, especially `www/homekit.html` which is the design reference. + +## Before you start + +Read these files completely: + +1. **`www/design-system.html`** -- All CSS variables, every component, JS patterns. This is your component library. +2. **`www/homekit.html`** -- The design reference. This page is the gold standard. Study its structure, spacing, how little text it uses, how the back button is positioned. +3. **`www/index.html`** -- The entry point. Understand the probe flow and how data is passed between pages via URL params. + +If you need to understand backend APIs or the probe system, read: +- `www/standard.html` -- how probe data flows into a configuration page +- `www/test.html` -- how polling and real-time updates work +- `www/config.html` -- complex two-column layout with live preview + +## Design Philosophy + +### Radical minimalism + +Every element on screen must earn its place. If something doesn't help the user complete their task, remove it. + +- **10% text, 90% meaning.** A label that says "Pairing Code" with an info-icon is better than a paragraph explaining what a pairing code is. +- **Hide details behind info-icons.** Long explanations go into tooltips (the `(i)` icon pattern). The user who needs the explanation can hover. The user who doesn't is not bothered. +- **No decorative elements without function.** No ornamental icons, no badges that don't convey information, no cards-as-decoration. +- **One action per screen.** Each page should have one primary thing the user does. Everything else is secondary. + +### How we think about design decisions + +When building homekit.html, we went through this process: + +1. **Started with all the data** -- device info table, long descriptions, badges, decorative icons +2. **Asked "does the user need this?"** for every element +3. **Removed everything that wasn't essential** -- the device info table (IP, MAC, vendor) was removed because the user doesn't need it to enter a PIN code +4. **Moved explanations into tooltips** -- "This camera supports Apple HomeKit. Enter the 8-digit pairing code printed on your camera or included in the manual" became just a label "Pairing Code" with a tooltip +5. **Removed format hints** -- "Format: XXX-XX-XXX" was removed because the input fields themselves make the format obvious +6. **Made the primary action obvious** -- big button, full width, impossible to miss + +Apply this same thinking to every page you create. + +### Visual rules + +- Dark theme with purple accent -- never deviate from the color palette in `:root` +- All icons are inline SVG -- never use emoji, never use icon fonts, never use external icon libraries +- Fonts: system font stack for UI, monospace for technical values (URLs, IPs, codes) +- Borders are subtle: `rgba(139, 92, 246, 0.15)` -- barely visible purple tint +- Glow effects on focus and hover, never on static elements (except logos) +- Animations are fast (150ms) and subtle -- translateY(-2px) on hover, fadeIn on page load +- No rounded corners larger than 8px (except special cases like toggle switches) + +## Layout Patterns + +### Pages after probe (like homekit.html) -- TRUE CENTER + +This is the most common case for new pages. Content is vertically centered on screen. + +``` +.screen { + min-height: 100vh; + display: flex; + align-items: center; /* TRUE CENTER -- not flex-start */ + justify-content: center; +} +.container { max-width: 480px; width: 100%; } +``` + +**Back button** is positioned OUTSIDE the container, wider than content, using `.back-wrapper`: + +``` +.back-wrapper { + position: absolute; top: 1.5rem; + left: 50%; transform: translateX(-50%); + width: 100%; max-width: 600px; /* wider than container */ + padding: 0 1.5rem; + z-index: 10; +} +``` + +This is MANDATORY for all centered layout pages. The back button must NOT be inside the centered container. + +### Entry page (like index.html) -- TOP CENTER + +Content is near the top with `margin-top: 8vh`. Used for the main entry point only. + +### Content pages (like standard.html, create.html) -- STANDARD + +Back button at top, then title, then content flowing down. `max-width: 600px`, no vertical centering. + +### Data-heavy pages (like test.html) -- WIDE + +`max-width: 1200px` with card grids. + +### Two-column (like config.html) -- SPLIT + +Settings left, live preview right. Collapses to tabs on mobile. + +## Hero Section Pattern + +For centered pages, the hero contains a logo/icon + short title: + +```html +
+ ... +

Short Name

+
+``` + +- The icon should be recognizable and relevant (Strix owl for main, HomeKit house for HomeKit) +- The title is SHORT -- one or two words max +- No subtitles unless absolutely necessary +- Glow effect on the icon via `filter: drop-shadow()` + +## Component Usage + +All components are documented with live examples in `www/design-system.html`. Key ones: + +- **Buttons**: `.btn .btn-primary .btn-large` for primary action (full width), `.btn-outline` for secondary +- **Inputs**: `.input` with `.label` and optional `.info-icon` with `.tooltip` +- **Toast**: Every page needs `` and the `showToast()` function +- **Error box**: `.error-box` with `.visible` class toggled +- **Info icon + tooltip**: For hiding explanations -- always prefer this over visible text + +## Navigation -- CRITICAL + +### Always pass ALL known data forward + +When navigating to another page, pass every piece of data you have. This is non-negotiable. Future pages may need any of these values. + +```javascript +function navigateNext() { + var p = new URLSearchParams(); + p.set('primary_data', value); + // Pass through EVERYTHING known: + if (ip) p.set('ip', ip); + if (mac) p.set('mac', mac); + if (vendor) p.set('vendor', vendor); + if (model) p.set('model', model); + if (server) p.set('server', server); + if (hostname) p.set('hostname', hostname); + if (ports) p.set('ports', ports); + if (user) p.set('user', user); + if (channel) p.set('channel', channel); + // ... any other params from probe + window.location.href = 'next.html?' + p.toString(); +} +``` + +### Page init always reads all params + +```javascript +var params = new URLSearchParams(location.search); +var ip = params.get('ip') || ''; +var mac = params.get('mac') || ''; +var vendor = params.get('vendor') || ''; +// ... read ALL possible params even if this page doesn't use them +// They need to be available for passing to the next page +``` + +## JavaScript Rules + +- Use `var`, not `let`/`const` -- ES5 compatible +- Build DOM with `document.createElement`, not innerHTML +- Use `async function` + `fetch()` for API calls +- Always handle errors: check `!r.ok`, catch exceptions, show toast +- Debounce input handlers if they trigger API calls (300ms) +- Use `addEventListener`, never inline event handlers in HTML + +## API Pattern + +```javascript +async function doSomething() { + try { + var r = await fetch('api/endpoint', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + if (!r.ok) { + var text = await r.text(); + showToast(text || 'Error ' + r.status); + return; + } + var data = await r.json(); + // success... + } catch (e) { + showToast('Connection error: ' + e.message); + } +} +``` + +## Checklist before finishing + +- [ ] Page uses correct layout pattern for its type +- [ ] Back button positioned correctly (`.back-wrapper` for centered, inline for standard) +- [ ] All CSS variables from `:root` -- no hardcoded colors +- [ ] No unnecessary text -- everything possible hidden behind info-icons +- [ ] All known URL params are read at init and passed forward on navigation +- [ ] Toast element present, showToast function included +- [ ] Error states handled (API errors, validation) +- [ ] Mobile responsive (test at 375px width) +- [ ] No emoji anywhere +- [ ] All icons are inline SVG +- [ ] Primary action is obvious and full-width +- [ ] Page looks like it belongs with homekit.html and index.html From 5be8d4aa0063388348f102fa04baa0696435f001 Mon Sep 17 00:00:00 2001 From: eduard256 Date: Wed, 8 Apr 2026 10:31:46 +0000 Subject: [PATCH 5/9] Add ONVIF probe detector via unicast WS-Discovery - Add ProbeONVIF() prober: sends unicast WS-Discovery to ip:3702, parses XAddrs, Name, Hardware from response (no auth needed) - Add ONVIFResult struct to probe models - Register ONVIF detector with highest priority (before HomeKit) - Fix homekit.html back-wrapper max-width to match design system --- internal/probe/probe.go | 14 +++++ pkg/probe/models.go | 8 +++ pkg/probe/onvif.go | 126 ++++++++++++++++++++++++++++++++++++++++ www/homekit.html | 4 +- 4 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 pkg/probe/onvif.go diff --git a/internal/probe/probe.go b/internal/probe/probe.go index 90e7838..63bd0d7 100644 --- a/internal/probe/probe.go +++ b/internal/probe/probe.go @@ -33,6 +33,14 @@ func Init() { } ports = loadPorts() + // ONVIF detector (highest priority -- auto-discovers all streams) + detectors = append(detectors, func(r *probe.Response) string { + if r.Probes.ONVIF != nil { + return "onvif" + } + return "" + }) + // HomeKit detector detectors = append(detectors, func(r *probe.Response) string { if r.Probes.MDNS != nil { @@ -115,6 +123,12 @@ func runProbe(parent context.Context, ip string) *probe.Response { resp.Probes.HTTP = r mu.Unlock() }) + run(func() { + r, _ := probe.ProbeONVIF(fastCtx, ip) + mu.Lock() + resp.Probes.ONVIF = r + mu.Unlock() + }) wg.Wait() diff --git a/pkg/probe/models.go b/pkg/probe/models.go index a67e6df..42afc12 100644 --- a/pkg/probe/models.go +++ b/pkg/probe/models.go @@ -14,6 +14,7 @@ type Probes struct { ARP *ARPResult `json:"arp"` MDNS *MDNSResult `json:"mdns"` HTTP *HTTPResult `json:"http"` + ONVIF *ONVIFResult `json:"onvif"` } type PortsResult struct { @@ -43,3 +44,10 @@ type HTTPResult struct { StatusCode int `json:"status_code"` Server string `json:"server"` } + +type ONVIFResult struct { + URL string `json:"url"` + Port int `json:"port"` + Name string `json:"name,omitempty"` + Hardware string `json:"hardware,omitempty"` +} diff --git a/pkg/probe/onvif.go b/pkg/probe/onvif.go new file mode 100644 index 0000000..e7c3517 --- /dev/null +++ b/pkg/probe/onvif.go @@ -0,0 +1,126 @@ +package probe + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "net" + "net/url" + "regexp" + "strings" + "time" +) + +// ProbeONVIF sends unicast WS-Discovery probe to ip:3702. +// Returns nil, nil if the device does not support ONVIF. +func ProbeONVIF(ctx context.Context, ip string) (*ONVIFResult, error) { + conn, err := net.ListenPacket("udp4", ":0") + if err != nil { + return nil, err + } + defer conn.Close() + + deadline, ok := ctx.Deadline() + if !ok { + deadline = time.Now().Add(100 * time.Millisecond) + } + _ = conn.SetDeadline(deadline) + + // WS-Discovery Probe message + // https://www.onvif.org/wp-content/uploads/2016/12/ONVIF_Feature_Discovery_Specification_16.07.pdf + msg := ` + + + http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe + urn:uuid:` + randUUID() + ` + urn:schemas-xmlsoap-org:ws:2005:04:discovery + + + + + + + +` + + addr := &net.UDPAddr{IP: net.ParseIP(ip), Port: 3702} + if _, err = conn.WriteTo([]byte(msg), addr); err != nil { + return nil, err + } + + buf := make([]byte, 8192) + for { + n, _, err := conn.ReadFrom(buf) + if err != nil { + return nil, nil // timeout -- device doesn't support ONVIF + } + + body := string(buf[:n]) + if !strings.Contains(body, "onvif") { + continue + } + + xaddrs := findXMLTag(body, "XAddrs") + if xaddrs == "" { + continue + } + + // fix buggy cameras reporting 0.0.0.0 + // ex. http://0.0.0.0:8080/onvif/device_service + if s, ok := strings.CutPrefix(xaddrs, "http://0.0.0.0"); ok { + xaddrs = "http://" + ip + s + } + + port := 80 + if u, err := url.Parse(xaddrs); err == nil && u.Port() != "" { + fmt.Sscanf(u.Port(), "%d", &port) + } + + scopes := findXMLTag(body, "Scopes") + + return &ONVIFResult{ + URL: xaddrs, + Port: port, + Name: findScope(scopes, "onvif://www.onvif.org/name/"), + Hardware: findScope(scopes, "onvif://www.onvif.org/hardware/"), + }, nil + } +} + +// internals + +var reXMLTag = map[string]*regexp.Regexp{} + +func findXMLTag(s, tag string) string { + re, ok := reXMLTag[tag] + if !ok { + re = regexp.MustCompile(`(?s)<(?:\w+:)?` + tag + `\b[^>]*>([^<]+)`) + reXMLTag[tag] = re + } + m := re.FindStringSubmatch(s) + if len(m) != 2 { + return "" + } + return m[1] +} + +func findScope(s, prefix string) string { + i := strings.Index(s, prefix) + if i < 0 { + return "" + } + s = s[i+len(prefix):] + if j := strings.IndexByte(s, ' '); j >= 0 { + s = s[:j] + } + s, _ = url.QueryUnescape(s) + return s +} + +func randUUID() string { + b := make([]byte, 16) + rand.Read(b) + s := hex.EncodeToString(b) + return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:] +} diff --git a/www/homekit.html b/www/homekit.html index ba5c5b9..390ce77 100644 --- a/www/homekit.html +++ b/www/homekit.html @@ -60,13 +60,13 @@ .back-wrapper { position: absolute; top: 1.5rem; left: 50%; transform: translateX(-50%); - width: 100%; max-width: 480px; + width: 100%; max-width: 600px; padding: 0 1.5rem; z-index: 10; } @media (min-width: 768px) { - .back-wrapper { max-width: 540px; } + .back-wrapper { max-width: 660px; } } .btn-back { From ce4b777e98ddaf1ca2e78127d72a2256eeeef0f2 Mon Sep 17 00:00:00 2001 From: eduard256 Date: Wed, 8 Apr 2026 10:50:05 +0000 Subject: [PATCH 6/9] Add ONVIF camera page and probe routing - Add onvif.html: credentials form, Discover Streams button, fallback to Standard Discovery and HomeKit Pairing - Update index.html routing: onvif type -> onvif.html with all probe params (onvif_url, onvif_port, onvif_name, onvif_hardware, mdns_* for HomeKit fallback) --- www/index.html | 49 +++++- www/onvif.html | 420 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 467 insertions(+), 2 deletions(-) create mode 100644 www/onvif.html diff --git a/www/index.html b/www/index.html index bb2ec3b..17f39ae 100644 --- a/www/index.html +++ b/www/index.html @@ -317,8 +317,8 @@ const data = await r.json(); - if (data.type === 'standard' || (data.reachable && data.type !== 'homekit')) { - navigateStandard(ip, data); + if (data.type === 'onvif') { + navigateOnvif(ip, data); return; } @@ -327,6 +327,11 @@ return; } + if (data.type === 'standard' || data.reachable) { + navigateStandard(ip, data); + return; + } + if (data.type === 'unreachable') { showUnreachable(ip); return; @@ -340,6 +345,46 @@ } } + function navigateOnvif(ip, data) { + var p = new URLSearchParams(); + p.set('ip', ip); + + var probes = data.probes || {}; + + if (probes.ports && probes.ports.open && probes.ports.open.length) { + p.set('ports', probes.ports.open.join(',')); + } + if (probes.arp) { + if (probes.arp.mac) p.set('mac', probes.arp.mac); + if (probes.arp.vendor) p.set('vendor', probes.arp.vendor); + } + if (probes.http && probes.http.server) { + p.set('server', probes.http.server); + } + if (probes.dns && probes.dns.hostname) { + p.set('hostname', probes.dns.hostname); + } + if (probes.ping && probes.ping.latency_ms) { + p.set('latency', Math.round(probes.ping.latency_ms)); + } + if (probes.onvif) { + if (probes.onvif.url) p.set('onvif_url', probes.onvif.url); + if (probes.onvif.port) p.set('onvif_port', probes.onvif.port); + if (probes.onvif.name) p.set('onvif_name', probes.onvif.name); + if (probes.onvif.hardware) p.set('onvif_hardware', probes.onvif.hardware); + } + if (probes.mdns) { + if (probes.mdns.name) p.set('mdns_name', probes.mdns.name); + if (probes.mdns.model) p.set('mdns_model', probes.mdns.model); + if (probes.mdns.category) p.set('mdns_category', probes.mdns.category); + if (probes.mdns.device_id) p.set('mdns_device_id', probes.mdns.device_id); + if (probes.mdns.port) p.set('mdns_port', probes.mdns.port); + p.set('mdns_paired', probes.mdns.paired ? '1' : '0'); + } + + window.location.href = 'onvif.html?' + p.toString(); + } + function navigateStandard(ip, data) { var p = new URLSearchParams(); p.set('ip', ip); diff --git a/www/onvif.html b/www/onvif.html new file mode 100644 index 0000000..a9c31cd --- /dev/null +++ b/www/onvif.html @@ -0,0 +1,420 @@ + + + + + + + Strix - ONVIF Camera + + + + +
+ +
+ +
+
+
+
Onvif
+

ONVIF Camera

+

+
+ +
+ + +
+ +
+ +
+ + +
+
+ + + + +
+
+ + + + + From 0fb7356a5eec92a70311d28f100234c379040d0e Mon Sep 17 00:00:00 2001 From: eduard256 Date: Wed, 8 Apr 2026 11:00:32 +0000 Subject: [PATCH 7/9] Add ONVIF stream handler for tester - Add testOnvif(): resolves all profiles via ONVIF client, tests each RTSP stream, returns two Results per profile (onvif + rtsp) with shared screenshot - Route onvif:// URLs in worker.go alongside homekit:// - Classify onvif:// streams as recommended in test.html - Harden create.html against undefined/null URL values --- pkg/tester/source_onvif.go | 104 +++++++++++++++++++++++++++++++++++++ pkg/tester/worker.go | 5 ++ www/create.html | 11 ++-- www/test.html | 6 +-- 4 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 pkg/tester/source_onvif.go diff --git a/pkg/tester/source_onvif.go b/pkg/tester/source_onvif.go new file mode 100644 index 0000000..99c04b6 --- /dev/null +++ b/pkg/tester/source_onvif.go @@ -0,0 +1,104 @@ +package tester + +import ( + "fmt" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/onvif" +) + +// testOnvif resolves all ONVIF profiles, tests each via RTSP, +// and adds two Results per profile (onvif:// + rtsp://). +// ex. "onvif://admin:pass@10.0.20.111" or "onvif://admin:pass@10.0.20.119:2020" +func testOnvif(s *Session, rawURL string) { + client, err := onvif.NewClient(rawURL) + if err != nil { + return + } + + tokens, err := client.GetProfilesTokens() + if err != nil { + return + } + + for _, token := range tokens { + profileURL := rawURL + "?subtype=" + token + + pc, err := onvif.NewClient(profileURL) + if err != nil { + continue + } + + rtspURI, err := pc.GetURI() + if err != nil { + continue + } + + testOnvifProfile(s, profileURL, rtspURI) + } +} + +// testOnvifProfile tests a single RTSP stream and adds two Results (onvif + rtsp) +func testOnvifProfile(s *Session, onvifURL, rtspURL string) { + start := time.Now() + + prod, err := rtspHandler(rtspURL) + if err != nil { + return + } + defer func() { _ = prod.Stop() }() + + latency := time.Since(start).Milliseconds() + + var codecs []string + for _, media := range prod.GetMedias() { + if media.Direction != core.DirectionRecvonly { + continue + } + for _, codec := range media.Codecs { + codecs = append(codecs, codec.Name) + } + } + + // capture screenshot + var screenshotPath string + var width, height int + + if raw, codecName := getScreenshot(prod); raw != nil { + var jpeg []byte + + switch codecName { + case core.CodecH264, core.CodecH265: + jpeg = toJPEG(raw) + default: + jpeg = raw + } + + if jpeg != nil { + idx := s.AddScreenshot(jpeg) + screenshotPath = fmt.Sprintf("api/test/screenshot?id=%s&i=%d", s.ID, idx) + width, height = jpegSize(jpeg) + } + } + + // add onvif:// result + s.AddResult(&Result{ + Source: onvifURL, + Screenshot: screenshotPath, + Codecs: codecs, + Width: width, + Height: height, + LatencyMs: latency, + }) + + // add rtsp:// result (same screenshot, same codecs) + s.AddResult(&Result{ + Source: rtspURL, + Screenshot: screenshotPath, + Codecs: codecs, + Width: width, + Height: height, + LatencyMs: latency, + }) +} diff --git a/pkg/tester/worker.go b/pkg/tester/worker.go index 0512486..49a6069 100644 --- a/pkg/tester/worker.go +++ b/pkg/tester/worker.go @@ -56,6 +56,11 @@ func testURL(s *Session, rawURL string) { return } + if strings.HasPrefix(rawURL, "onvif://") { + testOnvif(s, rawURL) + return + } + handler := GetHandler(rawURL) if handler == nil { return diff --git a/www/create.html b/www/create.html index 5ba7e4a..c80c3d9 100644 --- a/www/create.html +++ b/www/create.html @@ -328,8 +328,9 @@ // Pre-populate custom streams from "url" query parameter (supports multiple) params.getAll('url').forEach(function(u) { + if (!u || typeof u !== 'string') return; u = u.trim(); - if (u && u.indexOf('://') !== -1 && customStreams.indexOf(u) === -1) { + if (u && u !== 'undefined' && u !== 'null' && u.indexOf('://') !== -1 && customStreams.indexOf(u) === -1) { customStreams.push(u); } }); @@ -395,7 +396,7 @@ addInput.type = 'text'; addInput.placeholder = 'rtsp://user:pass@host/path or bubble://...'; addInput.spellcheck = false; - addInput.value = pendingInput; + addInput.value = pendingInput || ''; var addBtn = document.createElement('button'); addBtn.className = 'btn-add'; @@ -404,7 +405,7 @@ function addCustom() { var v = addInput.value.trim(); - if (!v) return; + if (!v || v === 'undefined' || v === 'null') return; if (v.indexOf('://') === -1) { showToast('URL must include protocol (rtsp://, http://, bubble://, ...)'); return; @@ -592,7 +593,7 @@ addInput.type = 'text'; addInput.placeholder = 'rtsp://user:pass@host/path or bubble://...'; addInput.spellcheck = false; - addInput.value = pendingInput; + addInput.value = pendingInput || ''; var addBtn = document.createElement('button'); addBtn.className = 'btn-add'; addBtn.type = 'button'; @@ -600,7 +601,7 @@ function addCustom() { var v = addInput.value.trim(); - if (!v) return; + if (!v || v === 'undefined' || v === 'null') return; if (v.indexOf('://') === -1) { showToast('URL must include protocol (rtsp://, http://, bubble://, ...)'); return; } if (customStreams.indexOf(v) !== -1 || dbStreams.indexOf(v) !== -1) { showToast('This URL is already in the list'); return; } customStreams.push(v); diff --git a/www/test.html b/www/test.html index da2bbd6..5d65c81 100644 --- a/www/test.html +++ b/www/test.html @@ -423,11 +423,11 @@ function classifyResult(r) { var scheme = r.source.split('://')[0] || ''; - var isRtsp = scheme === 'rtsp' || scheme === 'rtsps'; + var isRecommended = scheme === 'rtsp' || scheme === 'rtsps' || scheme === 'onvif'; var isHD = r.width >= 1280; - if (isRtsp && isHD) return 'rec-main'; - if (isRtsp) return 'rec-sub'; + if (isRecommended && isHD) return 'rec-main'; + if (isRecommended) return 'rec-sub'; return 'alt'; } From e47c0f7ce62c97fa92549fb87b2811bbea55edef Mon Sep 17 00:00:00 2001 From: eduard256 Date: Wed, 8 Apr 2026 11:27:08 +0000 Subject: [PATCH 8/9] Add top-1000 checkbox to ONVIF page, classify JPEG streams as alternative - Add checked-by-default checkbox to also test popular stream patterns - Move JPEG-only streams (no H264/H265) to Alternative group in test results --- www/onvif.html | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ www/test.html | 2 ++ 2 files changed, 51 insertions(+) diff --git a/www/onvif.html b/www/onvif.html index a9c31cd..0c081cf 100644 --- a/www/onvif.html +++ b/www/onvif.html @@ -217,6 +217,41 @@ } .btn-outline:hover { border-color: var(--purple-primary); color: var(--purple-light); } + /* Checkbox */ + .checkbox-row { + margin-bottom: 1.5rem; + } + + .checkbox-label { + display: flex; align-items: center; gap: 0.625rem; + font-size: 0.875rem; color: var(--text-secondary); + cursor: pointer; user-select: none; + } + + .checkbox-label input { display: none; } + + .checkbox-custom { + width: 18px; height: 18px; flex-shrink: 0; + border: 2px solid var(--border-color); + border-radius: 4px; + background: var(--bg-secondary); + transition: all var(--transition-fast); + position: relative; + } + + .checkbox-label input:checked + .checkbox-custom { + background: var(--purple-primary); + border-color: var(--purple-primary); + } + + .checkbox-label input:checked + .checkbox-custom::after { + content: ''; + position: absolute; top: 2px; left: 5px; + width: 5px; height: 9px; + border: solid white; border-width: 0 2px 2px 0; + transform: rotate(45deg); + } + @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @@ -281,6 +316,14 @@
+
+ +
+ @@ -372,6 +415,12 @@ if (hostname) p.set('hostname', hostname); if (ports) p.set('ports', ports); + if (document.getElementById('cb-top1000').checked) { + p.set('ids', 'p:top-1000'); + p.set('user', user); + if (pass) p.set('pass', pass); + } + window.location.href = 'create.html?' + p.toString(); }); diff --git a/www/test.html b/www/test.html index 5d65c81..85ddc25 100644 --- a/www/test.html +++ b/www/test.html @@ -424,8 +424,10 @@ function classifyResult(r) { var scheme = r.source.split('://')[0] || ''; var isRecommended = scheme === 'rtsp' || scheme === 'rtsps' || scheme === 'onvif'; + var isJpegOnly = r.codecs && r.codecs.length > 0 && r.codecs.indexOf('H264') === -1 && r.codecs.indexOf('H265') === -1; var isHD = r.width >= 1280; + if (isJpegOnly) return 'alt'; if (isRecommended && isHD) return 'rec-main'; if (isRecommended) return 'rec-sub'; return 'alt'; From 29e03ce85a07195819dd42680d72d8d1b5a5b595 Mon Sep 17 00:00:00 2001 From: eduard256 Date: Wed, 8 Apr 2026 11:30:58 +0000 Subject: [PATCH 9/9] Release v2.1.0 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f86588..72e1ca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [2.1.0] - 2026-04-08 + +### Added +- ONVIF protocol support: auto-discovery via unicast WS-Discovery, stream resolution through ONVIF profiles +- ONVIF probe detector: detects ONVIF cameras during network probe (4-7ms response time, no auth required) +- ONVIF camera page (onvif.html): credentials form with option to also test popular stream patterns +- ONVIF stream handler: resolves all camera profiles, tests each via RTSP, returns paired results (onvif:// + rtsp://) with shared screenshots +- Design system reference (design-system.html) with all UI components documented + +### Changed +- ONVIF has highest probe priority (above HomeKit and Standard) +- JPEG-only streams (no H264/H265) are classified as Alternative in test results +- HomeKit page redesigned: Apple HomeKit logo, centered layout, floating back button +- Hardened create.html against undefined/null URL values in query parameters + ## [2.0.0] - 2025-04-05 ### Added