304fa24389
Safety & Correctness: - Add confirmation dialogs to Stop All, turnOffDevice - i18n confirm dialog (title, yes, no buttons) - Fix duplicate tutorial-overlay ID - Define missing CSS variables (--radius, --text-primary, --hover-bg, --input-bg) - Fix toast z-index conflict with confirm dialog (2500 → 3000) UX Consistency: - Add backdrop-close to test modals - Add device clone feature (only entity without it) - Add sync clocks to command palette - Replace 20+ hardcoded accent colors with CSS vars/color-mix() - Remove dead .badge duplicate from components.css - Make calibration elements keyboard-accessible (div → button) - Add aria-labels to color picker swatches - Fix pattern canvas mobile horizontal scroll - Fix graph editor mobile bottom clipping Polish: - Add empty-state messages to all CardSection instances - Convert 21 px font-sizes to rem - Add scroll-behavior: smooth with reduced-motion override - Add @media print styles - Add :focus-visible to 4 missing interactive elements - Fix settings modal close label (Cancel → Close) - Fix api-key submit button i18n New Features: - Command palette actions: start/stop targets, activate scenes, enable/disable - Bulk start/stop API endpoints (POST /output-targets/bulk/start|stop) - OS notification history viewer modal - Scene "used by" automation reference count on cards - Clock elapsed time display on Streams tab cards - Device "last seen" relative timestamp on cards - Audio device refresh button in edit modal - Composite layer drag-to-reorder - MQTT settings panel (broker config with JSON persistence) - WebSocket log viewer with level filtering and ring buffer - Runtime log-level adjustment (GET/PUT endpoints + settings UI) - Animated value source waveform canvas preview - Gradient custom preset save/delete (localStorage) - API key read-only display in settings - Backup metadata (file size, auto/manual badges) - Server restart button with confirm + overlay - Partial config export/import per entity type - Progressive disclosure in target editor (Advanced section) CSS Architecture: - Define radius scale tokens (--radius-sm/md/lg/pill) - Scope .cs-filter selectors to remove 7 !important overrides - Consolidate duplicate toggle switch (filter-list → settings-toggle) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
318 lines
25 KiB
HTML
318 lines
25 KiB
HTML
<!-- Value Source Editor Modal -->
|
|
<div id="value-source-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="value-source-modal-title">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 id="value-source-modal-title" data-i18n="value_source.add">Add Value Source</h2>
|
|
<button class="modal-close-btn" onclick="closeValueSourceModal()" data-i18n-aria-label="aria.close">✕</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="value-source-form" onsubmit="return false;">
|
|
<input type="hidden" id="value-source-id">
|
|
|
|
<div id="value-source-error" class="error-message" style="display: none;"></div>
|
|
|
|
<!-- Name -->
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-name" data-i18n="value_source.name">Name:</label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.name.hint">A descriptive name for this value source</small>
|
|
<input type="text" id="value-source-name" data-i18n-placeholder="value_source.name.placeholder" placeholder="Brightness Pulse" required>
|
|
<div id="value-source-tags-container"></div>
|
|
</div>
|
|
|
|
<!-- Type (hidden in edit mode since type is immutable) -->
|
|
<div id="value-source-type-group" class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-type" data-i18n="value_source.type">Type:</label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.type.hint">Static outputs a constant value. Animated cycles through a waveform. Audio reacts to sound input.</small>
|
|
<select id="value-source-type" onchange="onValueSourceTypeChange()">
|
|
<option value="static" data-i18n="value_source.type.static">Static</option>
|
|
<option value="animated" data-i18n="value_source.type.animated">Animated</option>
|
|
<option value="audio" data-i18n="value_source.type.audio">Audio</option>
|
|
<option value="adaptive_time" data-i18n="value_source.type.adaptive_time">Adaptive (Time of Day)</option>
|
|
<option value="adaptive_scene" data-i18n="value_source.type.adaptive_scene">Adaptive (Scene)</option>
|
|
<option value="daylight" data-i18n="value_source.type.daylight">Daylight Cycle</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Static fields -->
|
|
<div id="value-source-static-section">
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-value"><span data-i18n="value_source.value">Value:</span> <span id="value-source-value-display">1.0</span></label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.value.hint">Constant output value (0.0 = off, 1.0 = full brightness)</small>
|
|
<input type="range" id="value-source-value" min="0" max="1" step="0.01" value="1.0"
|
|
oninput="document.getElementById('value-source-value-display').textContent = this.value">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Animated fields -->
|
|
<div id="value-source-animated-section" style="display:none">
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-waveform" data-i18n="value_source.waveform">Waveform:</label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.waveform.hint">Shape of the brightness animation cycle</small>
|
|
<select id="value-source-waveform">
|
|
<option value="sine" data-i18n="value_source.waveform.sine">Sine</option>
|
|
<option value="triangle" data-i18n="value_source.waveform.triangle">Triangle</option>
|
|
<option value="square" data-i18n="value_source.waveform.square">Square</option>
|
|
<option value="sawtooth" data-i18n="value_source.waveform.sawtooth">Sawtooth</option>
|
|
</select>
|
|
<canvas id="value-source-waveform-preview" width="200" height="60"
|
|
style="display:block;margin-top:8px;border-radius:6px;background:var(--surface-2,#1e1e2e);width:100%;max-width:200px;height:60px;"></canvas>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-speed"><span data-i18n="value_source.speed">Speed (cpm):</span> <span id="value-source-speed-display">10</span></label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.speed.hint">Cycles per minute — how fast the waveform repeats (1 = very slow, 120 = very fast)</small>
|
|
<input type="range" id="value-source-speed" min="1" max="120" step="1" value="10"
|
|
oninput="document.getElementById('value-source-speed-display').textContent = this.value">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-min-value"><span data-i18n="value_source.min_value">Min Value:</span> <span id="value-source-min-value-display">0</span></label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.min_value.hint">Minimum output of the waveform cycle</small>
|
|
<input type="range" id="value-source-min-value" min="0" max="1" step="0.01" value="0"
|
|
oninput="document.getElementById('value-source-min-value-display').textContent = this.value">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-max-value"><span data-i18n="value_source.max_value">Max Value:</span> <span id="value-source-max-value-display">1</span></label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.max_value.hint">Maximum output of the waveform cycle</small>
|
|
<input type="range" id="value-source-max-value" min="0" max="1" step="0.01" value="1"
|
|
oninput="document.getElementById('value-source-max-value-display').textContent = this.value">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audio fields -->
|
|
<div id="value-source-audio-section" style="display:none">
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-audio-source" data-i18n="value_source.audio_source">Audio Source:</label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.audio_source.hint">Mono audio source to read audio levels from</small>
|
|
<select id="value-source-audio-source">
|
|
<!-- populated dynamically with mono audio sources -->
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-mode" data-i18n="value_source.mode">Mode:</label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.mode.hint">RMS measures average volume. Peak tracks loudest moments. Beat triggers on rhythm.</small>
|
|
<select id="value-source-mode">
|
|
<option value="rms" data-i18n="value_source.mode.rms">RMS (Volume)</option>
|
|
<option value="peak" data-i18n="value_source.mode.peak">Peak</option>
|
|
<option value="beat" data-i18n="value_source.mode.beat">Beat</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-auto-gain" data-i18n="value_source.auto_gain">Auto Gain:</label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.auto_gain.hint">Automatically normalize audio levels so output uses the full range, regardless of input volume</small>
|
|
<label class="toggle-label">
|
|
<input type="checkbox" id="value-source-auto-gain">
|
|
<span data-i18n="value_source.auto_gain.enable">Enable auto-gain</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-sensitivity"><span data-i18n="value_source.sensitivity">Sensitivity:</span> <span id="value-source-sensitivity-display">1.0</span></label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.sensitivity.hint">Gain multiplier for the audio signal (higher = more reactive)</small>
|
|
<input type="range" id="value-source-sensitivity" min="0.1" max="20" step="0.1" value="1.0"
|
|
oninput="document.getElementById('value-source-sensitivity-display').textContent = this.value">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-smoothing"><span data-i18n="value_source.smoothing">Smoothing:</span> <span id="value-source-smoothing-display">0.3</span></label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.smoothing.hint">Temporal smoothing (0 = instant response, 1 = very smooth/slow)</small>
|
|
<input type="range" id="value-source-smoothing" min="0" max="1" step="0.05" value="0.3"
|
|
oninput="document.getElementById('value-source-smoothing-display').textContent = this.value">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-audio-min-value"><span data-i18n="value_source.audio_min_value">Min Value:</span> <span id="value-source-audio-min-value-display">0</span></label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.audio_min_value.hint">Output when audio is silent (e.g. 0.3 = 30% brightness floor)</small>
|
|
<input type="range" id="value-source-audio-min-value" min="0" max="1" step="0.01" value="0"
|
|
oninput="document.getElementById('value-source-audio-min-value-display').textContent = this.value">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-audio-max-value"><span data-i18n="value_source.audio_max_value">Max Value:</span> <span id="value-source-audio-max-value-display">1</span></label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.audio_max_value.hint">Output at maximum audio level</small>
|
|
<input type="range" id="value-source-audio-max-value" min="0" max="1" step="0.01" value="1"
|
|
oninput="document.getElementById('value-source-audio-max-value-display').textContent = this.value">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Adaptive Time of Day fields -->
|
|
<div id="value-source-adaptive-time-section" style="display:none">
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label data-i18n="value_source.schedule">Schedule:</label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.schedule.hint">Define at least 2 time points. Brightness interpolates linearly between them, wrapping at midnight.</small>
|
|
<div id="value-source-schedule-list" class="schedule-list"></div>
|
|
<button type="button" class="btn btn-secondary btn-sm" onclick="addSchedulePoint()" data-i18n="value_source.schedule.add">+ Add Point</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Adaptive Scene fields -->
|
|
<div id="value-source-adaptive-scene-section" style="display:none">
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-picture-source" data-i18n="value_source.picture_source">Picture Source:</label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.picture_source.hint">The picture source whose frames will be analyzed for average brightness.</small>
|
|
<select id="value-source-picture-source">
|
|
<!-- populated dynamically -->
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-scene-behavior" data-i18n="value_source.scene_behavior">Behavior:</label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.scene_behavior.hint">Complement: dark scene = high brightness (ideal for ambient backlight). Match: bright scene = high brightness.</small>
|
|
<select id="value-source-scene-behavior">
|
|
<option value="complement" data-i18n="value_source.scene_behavior.complement">Complement (dark → bright)</option>
|
|
<option value="match" data-i18n="value_source.scene_behavior.match">Match (bright → bright)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-scene-sensitivity"><span data-i18n="value_source.sensitivity">Sensitivity:</span> <span id="value-source-scene-sensitivity-display">1.0</span></label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.scene_sensitivity.hint">Gain multiplier for the luminance signal (higher = more reactive to brightness changes)</small>
|
|
<input type="range" id="value-source-scene-sensitivity" min="0.1" max="5" step="0.1" value="1.0"
|
|
oninput="document.getElementById('value-source-scene-sensitivity-display').textContent = this.value">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-scene-smoothing"><span data-i18n="value_source.smoothing">Smoothing:</span> <span id="value-source-scene-smoothing-display">0.3</span></label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.smoothing.hint">Temporal smoothing (0 = instant response, 1 = very smooth transitions)</small>
|
|
<input type="range" id="value-source-scene-smoothing" min="0" max="1" step="0.05" value="0.3"
|
|
oninput="document.getElementById('value-source-scene-smoothing-display').textContent = this.value">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Daylight fields -->
|
|
<div id="value-source-daylight-section" style="display:none">
|
|
<div id="value-source-daylight-speed-group" class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-daylight-speed"><span data-i18n="value_source.daylight.speed">Speed:</span> <span id="value-source-daylight-speed-display">1.0</span></label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.daylight.speed.hint">Cycle speed multiplier. 1.0 = full day/night cycle in ~4 minutes.</small>
|
|
<input type="range" id="value-source-daylight-speed" min="0.1" max="10" step="0.1" value="1.0"
|
|
oninput="document.getElementById('value-source-daylight-speed-display').textContent = parseFloat(this.value).toFixed(1)">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-daylight-real-time" data-i18n="value_source.daylight.use_real_time">Use Real Time:</label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.daylight.use_real_time.hint">When enabled, brightness follows the actual time of day. Speed is ignored.</small>
|
|
<label class="toggle-label">
|
|
<input type="checkbox" id="value-source-daylight-real-time" onchange="onDaylightVSRealTimeChange()">
|
|
<span data-i18n="value_source.daylight.enable_real_time">Follow wall clock</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-daylight-latitude"><span data-i18n="value_source.daylight.latitude">Latitude:</span> <span id="value-source-daylight-latitude-display">50</span>°</label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.daylight.latitude.hint">Your geographic latitude (-90 to 90). Affects sunrise/sunset timing in real-time mode.</small>
|
|
<input type="range" id="value-source-daylight-latitude" min="-90" max="90" step="1" value="50"
|
|
oninput="document.getElementById('value-source-daylight-latitude-display').textContent = parseInt(this.value)">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Shared adaptive output range (shown for adaptive and daylight types) -->
|
|
<div id="value-source-adaptive-range-section" style="display:none">
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-adaptive-min-value"><span data-i18n="value_source.adaptive_min_value">Min Value:</span> <span id="value-source-adaptive-min-value-display">0</span></label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.adaptive_min_value.hint">Minimum output brightness</small>
|
|
<input type="range" id="value-source-adaptive-min-value" min="0" max="1" step="0.01" value="0"
|
|
oninput="document.getElementById('value-source-adaptive-min-value-display').textContent = this.value">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-adaptive-max-value"><span data-i18n="value_source.adaptive_max_value">Max Value:</span> <span id="value-source-adaptive-max-value-display">1</span></label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.adaptive_max_value.hint">Maximum output brightness</small>
|
|
<input type="range" id="value-source-adaptive-max-value" min="0" max="1" step="0.01" value="1"
|
|
oninput="document.getElementById('value-source-adaptive-max-value-display').textContent = this.value">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="value-source-description" data-i18n="value_source.description">Description (optional):</label>
|
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
|
</div>
|
|
<small class="input-hint" style="display:none" data-i18n="value_source.description.hint">Optional notes about this value source</small>
|
|
<input type="text" id="value-source-description" data-i18n-placeholder="value_source.description.placeholder" placeholder="Describe this value source...">
|
|
</div>
|
|
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-icon btn-secondary" onclick="closeValueSourceModal()" title="Cancel" data-i18n-title="settings.button.cancel" data-i18n-aria-label="aria.cancel">✕</button>
|
|
<button class="btn btn-icon btn-primary" onclick="saveValueSource()" title="Save" data-i18n-title="settings.button.save" data-i18n-aria-label="aria.save">✓</button>
|
|
</div>
|
|
</div>
|
|
</div>
|