Serve led-tool static editor at /led-tool/editor, filter host serial ports, and load the modal via iframe instead of the legacy form. Co-authored-by: Cursor <cursoragent@cursor.com>
796 lines
50 KiB
HTML
796 lines
50 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>LED Controller - Zone Mode</title>
|
||
<link rel="stylesheet" href="/static/style.css">
|
||
</head>
|
||
<body>
|
||
<div class="app-container">
|
||
<header>
|
||
<div class="header-end">
|
||
<div class="nav-slide-toggle-wrap seq-switch-toggle-wrap edit-mode-only" id="seq-switch-toggle-wrap">
|
||
<span class="nav-slide-toggle-side-label nav-slide-toggle-side-label--beat">Beat</span>
|
||
<button type="button" role="switch" class="nav-slide-toggle-switch seq-switch-toggle" id="seq-switch-toggle" aria-pressed="false" aria-label="Switch sequence on beat" title="When starting a sequence: wait for beat or downbeat">
|
||
<span class="nav-slide-toggle-track" aria-hidden="true"><span class="nav-slide-toggle-thumb"></span></span>
|
||
</button>
|
||
<span class="nav-slide-toggle-side-label nav-slide-toggle-side-label--downbeat">Downbeat</span>
|
||
</div>
|
||
<div id="audio-top-indicator" class="audio-top-indicator">
|
||
<button type="button" id="audio-top-beat-sync" class="audio-top-beat-sync" disabled title="Sync step to music (S)">
|
||
<span class="audio-top-indicator-label">BPM</span>
|
||
<span id="audio-top-bpm-value" class="audio-top-indicator-value">--</span>
|
||
<span id="audio-top-beat-readout" class="audio-top-beat-readout" aria-live="polite"></span>
|
||
<span id="audio-top-bar-phase" class="audio-top-bar-phase" aria-live="polite" title="Bar phase (beat in bar)"></span>
|
||
</button>
|
||
</div>
|
||
<div class="header-actions">
|
||
<div class="header-brightness-control">
|
||
<label for="header-brightness-slider">Brightness</label>
|
||
<input type="range" id="header-brightness-slider" min="0" max="255" value="255">
|
||
</div>
|
||
<button class="btn btn-secondary" id="profiles-btn">Profiles</button>
|
||
<button class="btn btn-secondary edit-mode-only" id="devices-btn">Devices</button>
|
||
<button class="btn btn-secondary edit-mode-only" id="groups-btn">Groups</button>
|
||
<button class="btn btn-secondary edit-mode-only" id="zones-btn">Zones</button>
|
||
<button class="btn btn-secondary edit-mode-only" id="presets-btn">Presets</button>
|
||
<button class="btn btn-secondary edit-mode-only" id="sequences-btn">Sequences</button>
|
||
<button class="btn btn-secondary edit-mode-only" id="patterns-btn">Patterns</button>
|
||
<button class="btn btn-secondary edit-mode-only" id="color-palette-btn">Colour Palette</button>
|
||
<button class="btn btn-secondary edit-mode-only" id="send-profile-presets-btn">Send Presets</button>
|
||
<button class="btn btn-secondary edit-mode-only" id="led-tool-btn">LED Tool</button>
|
||
<button class="btn btn-secondary" id="audio-btn">Audio</button>
|
||
<button type="button" class="btn btn-secondary" id="audio-nav-reset-btn" hidden title="Clear stuck BPM / beat tracking">Reset detector</button>
|
||
<button class="btn btn-secondary" id="help-btn">Help</button>
|
||
<button type="button" class="btn btn-secondary ui-mode-toggle" id="ui-mode-toggle" aria-pressed="false" title="Switch preset strip mode — label is the mode you will switch to">Edit mode</button>
|
||
</div>
|
||
<div class="header-menu-mobile">
|
||
<button class="btn btn-secondary" id="main-menu-btn">Menu</button>
|
||
<div id="main-menu-dropdown" class="main-menu-dropdown">
|
||
<div class="nav-slide-toggle-wrap nav-slide-toggle-wrap--mobile seq-switch-toggle-wrap" id="seq-switch-toggle-wrap-mobile">
|
||
<span class="nav-slide-toggle-side-label nav-slide-toggle-side-label--beat">Beat</span>
|
||
<button type="button" role="switch" class="nav-slide-toggle-switch seq-switch-toggle" id="seq-switch-toggle-mobile" aria-pressed="false" aria-label="Switch sequence on beat" title="Beat or downbeat">
|
||
<span class="nav-slide-toggle-track" aria-hidden="true"><span class="nav-slide-toggle-thumb"></span></span>
|
||
</button>
|
||
<span class="nav-slide-toggle-side-label nav-slide-toggle-side-label--downbeat">Downbeat</span>
|
||
</div>
|
||
<button type="button" class="ui-mode-toggle" id="ui-mode-toggle-mobile" aria-pressed="false">Edit mode</button>
|
||
<div class="menu-brightness-control">
|
||
<label for="menu-brightness-slider">Brightness</label>
|
||
<input type="range" id="menu-brightness-slider" min="0" max="255" value="255">
|
||
</div>
|
||
<button type="button" data-target="profiles-btn">Profiles</button>
|
||
<button type="button" class="edit-mode-only" data-target="devices-btn">Devices</button>
|
||
<button type="button" class="edit-mode-only" data-target="groups-btn">Groups</button>
|
||
<button type="button" class="edit-mode-only" data-target="zones-btn">Tabs</button>
|
||
<button type="button" class="edit-mode-only" data-target="presets-btn">Presets</button>
|
||
<button type="button" class="edit-mode-only" data-target="sequences-btn">Sequences</button>
|
||
<button type="button" class="edit-mode-only" data-target="patterns-btn">Patterns</button>
|
||
<button type="button" class="edit-mode-only" data-target="color-palette-btn">Colour Palette</button>
|
||
<button type="button" class="edit-mode-only" data-target="send-profile-presets-btn">Send Presets</button>
|
||
<button type="button" class="edit-mode-only" data-target="led-tool-btn">LED Tool</button>
|
||
<button type="button" data-target="audio-btn">Audio</button>
|
||
<button type="button" id="audio-nav-reset-mobile" data-target="audio-nav-reset-btn" hidden>Reset detector</button>
|
||
<button type="button" data-target="help-btn">Help</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="zones-container">
|
||
<div id="zones-list">
|
||
Loading zones...
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="main-content">
|
||
<div id="zone-content" class="zone-content">
|
||
<div class="zone-content-placeholder">
|
||
Select a zone to get started
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tabs Modal -->
|
||
<div id="zones-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Tabs</h2>
|
||
<div class="profiles-actions zone-modal-create-row">
|
||
<input type="text" id="new-zone-name" placeholder="Zone name">
|
||
<button class="btn btn-primary" id="create-zone-btn">Create</button>
|
||
</div>
|
||
<div class="zone-content-kind-row muted-text">
|
||
<label><input type="radio" name="new-zone-content-kind" value="presets" checked> Presets</label>
|
||
<label><input type="radio" name="new-zone-content-kind" value="sequences"> Sequences</label>
|
||
</div>
|
||
<div id="zones-list-modal" class="profiles-list"></div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" id="zones-close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit Zone Modal -->
|
||
<div id="edit-zone-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Edit Zone</h2>
|
||
<form id="edit-zone-form">
|
||
<input type="hidden" id="edit-zone-id">
|
||
<label>Zone Name:</label>
|
||
<input type="text" id="edit-zone-name" placeholder="Enter zone name" required>
|
||
<div class="zone-content-kind-row muted-text">
|
||
<label><input type="radio" name="edit-zone-content-kind" value="presets" checked> Presets</label>
|
||
<label><input type="radio" name="edit-zone-content-kind" value="sequences"> Sequences</label>
|
||
</div>
|
||
<div id="edit-zone-block-groups">
|
||
<label class="zone-devices-label">Device groups on this zone</label>
|
||
<div id="edit-zone-groups-editor" class="zone-devices-editor"></div>
|
||
</div>
|
||
<div id="edit-zone-block-presets">
|
||
<label class="zone-presets-section-label">Presets on this zone</label>
|
||
<div id="edit-zone-presets-current" class="profiles-list edit-zone-presets-scroll"></div>
|
||
<label class="zone-presets-section-label">Add presets to this zone</label>
|
||
<div id="edit-zone-presets-list" class="profiles-list edit-zone-presets-scroll"></div>
|
||
</div>
|
||
<div id="edit-zone-block-sequences">
|
||
<label class="zone-presets-section-label">Sequences on this zone</label>
|
||
<div id="edit-zone-sequences-current" class="profiles-list edit-zone-presets-scroll"></div>
|
||
<label class="zone-presets-section-label">Add a sequence to this zone</label>
|
||
<div id="edit-zone-sequences-list" class="profiles-list edit-zone-presets-scroll"></div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button type="submit" class="btn btn-primary">Save</button>
|
||
<button type="button" class="btn btn-secondary" onclick="document.getElementById('edit-zone-modal').classList.remove('active')">Close</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Profiles Modal -->
|
||
<div id="profiles-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Profiles</h2>
|
||
<div class="profiles-actions">
|
||
<input type="text" id="new-profile-name" placeholder="Profile name">
|
||
<button class="btn btn-primary" id="create-profile-btn">Create</button>
|
||
<button type="button" class="btn btn-secondary" id="import-profile-btn">Import</button>
|
||
</div>
|
||
<div class="form-group" style="margin-top: 0.5rem; margin-bottom: 0;">
|
||
<label style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0;">
|
||
<input type="checkbox" id="new-profile-seed-dj">
|
||
DJ zone
|
||
</label>
|
||
</div>
|
||
<div id="profiles-list" class="profiles-list"></div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" id="profiles-close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Devices Modal (registry: Wi-Fi drivers appear when they connect over TCP) -->
|
||
<div id="devices-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Devices</h2>
|
||
<div id="devices-list-modal" class="profiles-list"></div>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-secondary" id="devices-close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Device groups: members + Wi‑Fi driver defaults (zones reference groups for presets) -->
|
||
<div id="groups-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Device groups</h2>
|
||
<p class="muted-text" style="margin-top:0;">Assign drivers to a group, set Wi‑Fi defaults once per group, then attach groups to a zone for standalone presets (sequences use each lane’s groups only). By default new groups are <strong>shared</strong> across all profiles; tick “this profile only” to hide a group from other profiles.</p>
|
||
<div class="profiles-actions zone-modal-create-row">
|
||
<input type="text" id="new-group-name" placeholder="Group name">
|
||
<label class="muted-text" style="display:inline-flex;align-items:center;gap:0.35rem;white-space:nowrap;">
|
||
<input type="checkbox" id="new-group-profile-only"> This profile only
|
||
</label>
|
||
<button class="btn btn-primary" id="create-group-btn">Create</button>
|
||
</div>
|
||
<div id="groups-list-modal" class="profiles-list"></div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" id="groups-close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="edit-group-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Edit device group</h2>
|
||
<form id="edit-group-form">
|
||
<input type="hidden" id="edit-group-id">
|
||
<label for="edit-group-name">Group name</label>
|
||
<input type="text" id="edit-group-name" required autocomplete="off">
|
||
<label class="muted-text" style="display:flex;align-items:flex-start;gap:0.5rem;margin-top:0.5rem;">
|
||
<input type="checkbox" id="edit-group-share-all-profiles" style="margin-top:0.2rem;">
|
||
<span>Share with all profiles (untick to keep this group on the <strong>current profile only</strong>)</span>
|
||
</label>
|
||
<label class="zone-devices-label">Devices in this group</label>
|
||
<div id="edit-group-devices-editor" class="zone-devices-editor"></div>
|
||
<div class="profiles-actions" style="margin-top: 0.5rem;">
|
||
<button type="button" class="btn btn-secondary btn-small" id="edit-group-identify-btn">Identify devices in group</button>
|
||
</div>
|
||
<p class="muted-text" style="margin-top:0.25rem;">Runs identify on every driver in the group at the same time so they blink together.</p>
|
||
<label for="edit-group-output-brightness" style="margin-top:0.75rem;display:block;">Group output brightness (0–255)</label>
|
||
<div class="profiles-actions" style="align-items: center; gap: 0.75rem;">
|
||
<input type="range" id="edit-group-output-brightness" min="0" max="255" value="255" style="flex:1;">
|
||
<span id="edit-group-output-brightness-value" class="muted-text" style="min-width:2.5rem;">255</span>
|
||
</div>
|
||
<p class="muted-text" style="margin-top:0.75rem;margin-bottom:0.35rem;">Wi‑Fi driver defaults (apply to all members via <strong>Apply defaults to drivers</strong> on the list)</p>
|
||
<label for="edit-group-wifi-driver-name">Display name</label>
|
||
<input type="text" id="edit-group-wifi-driver-name" placeholder="hello / discovery name" autocomplete="off">
|
||
<label for="edit-group-wifi-num-leds" style="margin-top:0.5rem;display:block;">Number of LEDs</label>
|
||
<input type="number" id="edit-group-wifi-num-leds" min="1" max="2048" step="1" placeholder="119">
|
||
<label for="edit-group-wifi-color-order" style="margin-top:0.5rem;display:block;">Colour order</label>
|
||
<select id="edit-group-wifi-color-order">
|
||
<option value="rgb">RGB</option>
|
||
<option value="rbg">RBG</option>
|
||
<option value="grb">GRB</option>
|
||
<option value="gbr">GBR</option>
|
||
<option value="brg">BRG</option>
|
||
<option value="bgr">BGR</option>
|
||
</select>
|
||
<label for="edit-group-wifi-startup-mode" style="margin-top:0.5rem;display:block;">Power-on pattern</label>
|
||
<select id="edit-group-wifi-startup-mode">
|
||
<option value="default">Default preset</option>
|
||
<option value="last">Last preset</option>
|
||
<option value="off">Off</option>
|
||
</select>
|
||
<label for="edit-group-debug" style="margin-top:1rem;display:block;">Debug</label>
|
||
<small class="muted-text" style="display:block;margin-bottom:0.35rem;">Stored row and the JSON preview for <strong>Save</strong> (updates as you edit).</small>
|
||
<textarea id="edit-group-debug" rows="8" readonly spellcheck="false" style="width:100%;font-family:monospace;resize:vertical;"></textarea>
|
||
<div class="modal-actions">
|
||
<button type="submit" class="btn btn-primary">Save</button>
|
||
<button type="button" class="btn btn-secondary" id="edit-group-close-btn">Cancel</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="edit-device-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Edit device</h2>
|
||
<form id="edit-device-form">
|
||
<input type="hidden" id="edit-device-id">
|
||
<p class="muted-text" style="margin-bottom:0.75rem;">MAC (id): <code id="edit-device-storage-id"></code></p>
|
||
<label for="edit-device-name">Name</label>
|
||
<input type="text" id="edit-device-name" required autocomplete="off">
|
||
<label for="edit-device-type" style="margin-top:0.75rem;display:block;">Type</label>
|
||
<select id="edit-device-type">
|
||
<option value="led">LED</option>
|
||
</select>
|
||
<label for="edit-device-transport" style="margin-top:0.75rem;display:block;">Transport</label>
|
||
<select id="edit-device-transport">
|
||
<option value="espnow">ESP-NOW</option>
|
||
<option value="wifi">WiFi</option>
|
||
</select>
|
||
<div id="edit-device-address-espnow" style="margin-top:0.75rem;">
|
||
<label class="device-field-label">MAC (12 hex, optional)</label>
|
||
<div id="edit-device-address-boxes" class="hex-address-row" aria-label="MAC address"></div>
|
||
</div>
|
||
<div id="edit-device-address-wifi-wrap" style="margin-top:0.75rem;" hidden>
|
||
<label for="edit-device-address-wifi">Address (IP or hostname)</label>
|
||
<input type="text" id="edit-device-address-wifi" placeholder="192.168.1.50" autocomplete="off">
|
||
</div>
|
||
<div id="edit-device-wifi-driver-wrap" hidden>
|
||
<p class="muted-text" style="margin-top:0.75rem;margin-bottom:0.35rem;">On-device settings (sent over Wi‑Fi when connected). For shared defaults across several drivers, use <strong>Groups</strong>.</p>
|
||
<label for="edit-device-wifi-driver-name">Display name</label>
|
||
<input type="text" id="edit-device-wifi-driver-name" placeholder="hello / discovery name" autocomplete="off">
|
||
<label for="edit-device-wifi-num-leds" style="margin-top:0.5rem;display:block;">Number of LEDs</label>
|
||
<input type="number" id="edit-device-wifi-num-leds" min="1" max="2048" step="1" placeholder="119">
|
||
<label for="edit-device-wifi-color-order" style="margin-top:0.5rem;display:block;">Colour order</label>
|
||
<select id="edit-device-wifi-color-order">
|
||
<option value="rgb">RGB</option>
|
||
<option value="rbg">RBG</option>
|
||
<option value="grb">GRB</option>
|
||
<option value="gbr">GBR</option>
|
||
<option value="brg">BRG</option>
|
||
<option value="bgr">BGR</option>
|
||
</select>
|
||
<label for="edit-device-wifi-startup-mode" style="margin-top:0.5rem;display:block;">Power-on pattern</label>
|
||
<select id="edit-device-wifi-startup-mode">
|
||
<option value="default">Default preset</option>
|
||
<option value="last">Last preset</option>
|
||
<option value="off">Off</option>
|
||
</select>
|
||
</div>
|
||
<label for="edit-device-output-brightness" style="margin-top:0.75rem;display:block;">Output brightness (0–255)</label>
|
||
<div class="profiles-actions" style="align-items: center; gap: 0.75rem;">
|
||
<input type="range" id="edit-device-output-brightness" min="0" max="255" value="255" style="flex:1;">
|
||
<span id="edit-device-output-brightness-value" class="muted-text" style="min-width:2.5rem;">255</span>
|
||
</div>
|
||
<small class="muted-text" style="display:block;margin-top:0.25rem;">Saved on the device; use <strong>Save</strong> to push to the driver (when connected).</small>
|
||
<label for="edit-device-debug" style="margin-top:1rem;display:block;">Debug</label>
|
||
<small class="muted-text" style="display:block;margin-bottom:0.35rem;">Stored registry row and the JSON preview for <strong>Save</strong> (updates as you edit).</small>
|
||
<textarea id="edit-device-debug" rows="8" readonly spellcheck="false" style="width:100%;font-family:monospace;resize:vertical;"></textarea>
|
||
<div class="modal-actions">
|
||
<button type="submit" class="btn btn-primary">Save</button>
|
||
<button type="button" class="btn btn-secondary" id="edit-device-close-btn">Cancel</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Presets Modal -->
|
||
<div id="presets-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Presets</h2>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-primary" id="preset-add-btn">Add</button>
|
||
<button type="button" class="btn btn-secondary" id="import-preset-btn">Import</button>
|
||
<button class="btn btn-danger" id="preset-clear-device-btn">Clear Device Presets</button>
|
||
</div>
|
||
<div id="presets-list" class="profiles-list"></div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" id="presets-close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sequences Modal -->
|
||
<div id="sequences-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Sequences</h2>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-primary" id="sequence-add-btn">Add</button>
|
||
<button type="button" class="btn btn-secondary" id="import-sequence-btn">Import</button>
|
||
<button type="button" class="btn btn-secondary" id="sequences-open-presets-btn">Presets</button>
|
||
</div>
|
||
<div id="sequences-list" class="profiles-list"></div>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-secondary" id="sequences-close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sequence Editor Modal -->
|
||
<div id="sequence-editor-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Sequence</h2>
|
||
<div class="preset-editor-field">
|
||
<label for="sequence-editor-name">Name</label>
|
||
<input type="text" id="sequence-editor-name" placeholder="Sequence name" style="width:100%;max-width:24rem;">
|
||
</div>
|
||
<div id="sequence-editor-beats-panel" style="margin:0 0 0.75rem 0;">
|
||
<p class="muted-text" style="font-size:0.85em;margin:0 0 0.5rem 0;">
|
||
Each step runs for the number of <strong>beats</strong> you set on that step.
|
||
When the header <strong>Audio</strong> detector is running, real beats advance the sequence.
|
||
When it is stopped, the server uses <strong>simulated</strong> beats at the BPM below.
|
||
</p>
|
||
<label for="sequence-editor-simulated-bpm" style="display:block;margin-bottom:0.25rem;">Simulated BPM (when audio is off)</label>
|
||
<input type="number" id="sequence-editor-simulated-bpm" min="30" max="300" value="120" style="width:6rem;" title="Used only while the audio detector is stopped">
|
||
<p id="sequence-editor-bpm-live" class="muted-text" style="font-size:0.85em;margin:0.5rem 0 0 0;">—</p>
|
||
<label style="display:block;margin-top:0.65rem;">
|
||
<input type="checkbox" id="sequence-editor-loop" checked>
|
||
Loop sequence (restart from the first step after the last)
|
||
</label>
|
||
</div>
|
||
<div id="sequence-editor-lanes"></div>
|
||
<div class="modal-actions preset-editor-modal-actions">
|
||
<button type="button" class="btn btn-secondary btn-small" id="sequence-editor-add-lane-btn">Add lane</button>
|
||
<button type="button" class="btn btn-danger" id="sequence-editor-delete-btn">Delete</button>
|
||
<button type="button" class="btn btn-primary" id="sequence-editor-save-btn">Save</button>
|
||
<button type="button" class="btn btn-secondary" id="sequence-editor-close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Preset Editor Modal -->
|
||
<div id="preset-editor-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Preset</h2>
|
||
<div class="profiles-actions">
|
||
<input type="text" id="preset-name-input" placeholder="Preset name">
|
||
<select id="preset-pattern-input">
|
||
<option value="">Pattern</option>
|
||
</select>
|
||
</div>
|
||
<label>Colours</label>
|
||
<div id="preset-colors-container" class="preset-colors-container"></div>
|
||
<div class="profiles-actions">
|
||
<input type="color" id="preset-new-color" value="#ffffff" title="Choose colour (auto-adds)">
|
||
<button class="btn btn-secondary btn-small" id="preset-add-from-palette-btn">From Palette</button>
|
||
</div>
|
||
<div class="profiles-actions">
|
||
<div class="preset-editor-field">
|
||
<label for="preset-brightness-input">Brightness (0–255)</label>
|
||
<input type="number" id="preset-brightness-input" placeholder="Brightness" min="0" max="255" value="0">
|
||
</div>
|
||
<div class="preset-editor-field">
|
||
<label for="preset-delay-input">Delay (ms)</label>
|
||
<input type="number" id="preset-delay-input" placeholder="Delay" min="0" max="10000" value="0">
|
||
</div>
|
||
<div class="preset-editor-field">
|
||
<label for="preset-background-input">Background</label>
|
||
<div class="profiles-actions" style="gap: 0.4rem;">
|
||
<button type="button" class="btn btn-secondary btn-small" id="preset-background-btn" title="Choose background colour">#000000</button>
|
||
<button type="button" class="btn btn-secondary btn-small" id="preset-background-from-palette-btn">From Palette</button>
|
||
<input type="color" id="preset-background-input" value="#000000" title="Background colour used in patterns with background support" style="position:absolute;opacity:0;pointer-events:none;width:1px;height:1px;">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-group" style="margin-top: 0.5rem; margin-bottom: 0;">
|
||
<label id="preset-manual-mode-label" style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0;">
|
||
<input type="checkbox" id="preset-manual-mode-input">
|
||
Manual mode (single-shot where supported)
|
||
</label>
|
||
<p id="preset-manual-mode-hint" class="muted-text" style="display: none; margin-top: 0.35rem; font-size: 0.85em;"></p>
|
||
<div id="preset-manual-beat-n-wrap" class="preset-editor-field" style="display: none; margin-top: 0.5rem;">
|
||
<label for="preset-manual-beat-n-input">Audio beat: every</label>
|
||
<input type="number" id="preset-manual-beat-n-input" min="1" max="64" value="1" style="width: 4rem;" title="Controller only; not sent to pattern logic" autocomplete="off">
|
||
<span class="muted-text" style="font-size: 0.85em;">beats (this app only)</span>
|
||
</div>
|
||
</div>
|
||
<div class="preset-editor-field" id="preset-reverse-group" hidden>
|
||
<label for="preset-reverse-input" style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0;">
|
||
<input type="checkbox" id="preset-reverse-input">
|
||
Reverse direction (strip installed upside down)
|
||
</label>
|
||
</div>
|
||
<div class="preset-editor-field preset-mode-field" id="preset-mode-group" hidden>
|
||
<label for="preset-mode-input" id="preset-mode-label">Mode</label>
|
||
<select id="preset-mode-input" class="preset-mode-input"></select>
|
||
</div>
|
||
<div class="n-params-grid">
|
||
<div class="n-param-group">
|
||
<label for="preset-n1-input" id="preset-n1-label">n1:</label>
|
||
<input type="number" id="preset-n1-input" min="0" max="255" value="0" class="n-input">
|
||
</div>
|
||
<div class="n-param-group">
|
||
<label for="preset-n2-input" id="preset-n2-label">n2:</label>
|
||
<input type="number" id="preset-n2-input" min="0" max="255" value="0" class="n-input">
|
||
</div>
|
||
<div class="n-param-group">
|
||
<label for="preset-n3-input" id="preset-n3-label">n3:</label>
|
||
<input type="number" id="preset-n3-input" min="0" max="255" value="0" class="n-input">
|
||
</div>
|
||
<div class="n-param-group">
|
||
<label for="preset-n4-input" id="preset-n4-label">n4:</label>
|
||
<input type="number" id="preset-n4-input" min="0" max="255" value="0" class="n-input">
|
||
</div>
|
||
<div class="n-param-group">
|
||
<label for="preset-n5-input" id="preset-n5-label">n5:</label>
|
||
<input type="number" id="preset-n5-input" min="0" max="255" value="0" class="n-input">
|
||
</div>
|
||
<div class="n-param-group">
|
||
<label for="preset-n6-input" id="preset-n6-label">n6:</label>
|
||
<input type="number" id="preset-n6-input" min="0" max="255" value="0" class="n-input">
|
||
</div>
|
||
<div class="n-param-group">
|
||
<label for="preset-n7-input" id="preset-n7-label">n7:</label>
|
||
<input type="number" id="preset-n7-input" min="0" max="255" value="0" class="n-input">
|
||
</div>
|
||
<div class="n-param-group">
|
||
<label for="preset-n8-input" id="preset-n8-label">n8:</label>
|
||
<input type="number" id="preset-n8-input" min="0" max="255" value="0" class="n-input">
|
||
</div>
|
||
</div>
|
||
<div class="modal-actions preset-editor-modal-actions">
|
||
<button class="btn btn-secondary" id="preset-send-btn">Try</button>
|
||
<button class="btn btn-secondary" id="preset-default-btn">Default</button>
|
||
<button type="button" class="btn btn-danger" id="preset-remove-from-zone-btn" hidden>Remove from zone</button>
|
||
<button class="btn btn-primary" id="preset-save-btn">Save & Send</button>
|
||
<button class="btn btn-secondary" id="preset-editor-close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Patterns Modal -->
|
||
<div id="patterns-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Patterns</h2>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-primary" id="pattern-add-btn">Add</button>
|
||
<button type="button" class="btn btn-secondary" id="pattern-send-all-btn">Send All Patterns</button>
|
||
</div>
|
||
<div id="patterns-list" class="profiles-list"></div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" id="patterns-close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Pattern Editor Modal -->
|
||
<div id="pattern-editor-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Pattern</h2>
|
||
<p class="muted-text" style="margin: 0 0 0.75rem 0;">Add a driver <code>.py</code> file and editor metadata (stored in the pattern database).</p>
|
||
<div class="profiles-row" style="flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||
<label for="pattern-create-name" style="min-width: 7rem;">Name</label>
|
||
<input type="text" id="pattern-create-name" class="preset-name-like" placeholder="e.g. sparkle" pattern="[a-zA-Z_][a-zA-Z0-9_]*" style="flex: 1; min-width: 12rem;" autocomplete="off">
|
||
</div>
|
||
<div id="pattern-create-n-section" class="n-params-section" style="margin-bottom: 0.5rem;">
|
||
<h3 class="muted-text">Readable parameter names</h3>
|
||
<p id="pattern-create-n-empty" class="muted-text" style="display: none; margin: 0 0 0.5rem 0;">No parameter names are stored for this pattern.</p>
|
||
<div class="n-params-grid">
|
||
<div class="n-param-group">
|
||
<label for="pattern-create-n1"></label>
|
||
<input type="text" id="pattern-create-n1" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
|
||
</div>
|
||
<div class="n-param-group">
|
||
<label for="pattern-create-n2"></label>
|
||
<input type="text" id="pattern-create-n2" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
|
||
</div>
|
||
<div class="n-param-group">
|
||
<label for="pattern-create-n3"></label>
|
||
<input type="text" id="pattern-create-n3" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
|
||
</div>
|
||
<div class="n-param-group">
|
||
<label for="pattern-create-n4"></label>
|
||
<input type="text" id="pattern-create-n4" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
|
||
</div>
|
||
<div class="n-param-group">
|
||
<label for="pattern-create-n5"></label>
|
||
<input type="text" id="pattern-create-n5" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
|
||
</div>
|
||
<div class="n-param-group">
|
||
<label for="pattern-create-n6"></label>
|
||
<input type="text" id="pattern-create-n6" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
|
||
</div>
|
||
<div class="n-param-group">
|
||
<label for="pattern-create-n7"></label>
|
||
<input type="text" id="pattern-create-n7" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
|
||
</div>
|
||
<div class="n-param-group">
|
||
<label for="pattern-create-n8"></label>
|
||
<input type="text" id="pattern-create-n8" class="n-input pattern-n-readable-input" placeholder="" autocomplete="off">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="profiles-row pattern-editor-meta-row" style="flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||
<label for="pattern-create-min-delay" style="min-width: 7rem;">Min delay (ms)</label>
|
||
<input type="number" id="pattern-create-min-delay" min="0" value="10">
|
||
<label for="pattern-create-max-delay">Max delay (ms)</label>
|
||
<input type="number" id="pattern-create-max-delay" min="0" value="10000">
|
||
<label for="pattern-create-max-colors">Max colours</label>
|
||
<input type="number" id="pattern-create-max-colors" min="0" value="10">
|
||
</div>
|
||
<div class="profiles-row" style="flex-direction: column; align-items: stretch; gap: 0.35rem; margin-bottom: 0.5rem;">
|
||
<label for="pattern-create-file">Pattern file</label>
|
||
<input type="file" id="pattern-create-file" accept=".py,text/x-python,.PY">
|
||
<label for="pattern-create-code" class="muted-text" style="font-size: 0.85em;">Or paste Python source (if no file chosen)</label>
|
||
<textarea id="pattern-create-code" rows="5" style="width: 100%; font-family: monospace; font-size: 0.85rem;" placeholder="# class MyPattern: ..."></textarea>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<label style="display: inline-flex; align-items: center; gap: 0.35rem; margin-right: auto;">
|
||
<input type="checkbox" id="pattern-create-overwrite" checked>
|
||
<span>Overwrite existing file</span>
|
||
</label>
|
||
<button type="button" class="btn btn-primary" id="pattern-create-btn">Save</button>
|
||
<button type="button" class="btn btn-secondary" id="pattern-editor-close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Colour Palette Modal -->
|
||
<div id="color-palette-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Colour Palette</h2>
|
||
<p class="muted-text">Profile: <span id="palette-current-profile-name">None</span></p>
|
||
<div id="palette-container" class="profiles-list"></div>
|
||
<div class="profiles-actions">
|
||
<input type="color" id="palette-new-color" value="#ffffff">
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" id="color-palette-close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Help Modal -->
|
||
<div id="help-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Help</h2>
|
||
<p class="muted-text">How to use the LED controller UI.</p>
|
||
|
||
<h3>Run mode</h3>
|
||
<ul>
|
||
<li><strong>Select zone</strong>: left-click a zone button in the top bar.</li>
|
||
<li><strong>Select preset</strong>: left-click a preset tile to send a <code>select</code> message to all devices in the zone.</li>
|
||
<li><strong>Profiles</strong>: open <strong>Profiles</strong> to apply a profile. Profile editing actions are hidden in Run mode.</li>
|
||
<li><strong>Devices</strong>: open <strong>Devices</strong> to see drivers (Wi-Fi clients appear when they connect); edit addresses or remove rows.</li>
|
||
<li><strong>Groups</strong>: define device groups, Wi‑Fi driver defaults, then assign groups to zones.</li>
|
||
<li><strong>Send all presets</strong>: this action is available in <strong>Edit mode</strong> and pushes every preset used in the current zone to all zone devices.</li>
|
||
<li><strong>Switch modes</strong>: use the mode button in the menu. The button label shows the mode you will switch to.</li>
|
||
</ul>
|
||
|
||
<h3>Edit mode</h3>
|
||
<ul>
|
||
<li><strong>Tabs</strong>: create, edit, and manage zones and which <strong>device groups</strong> each zone drives.</li>
|
||
<li><strong>Presets</strong>: create/manage reusable presets and edit preset details.</li>
|
||
<li><strong>Preset tiles</strong>: each tile shows <strong>Edit</strong> and <strong>Remove</strong> controls in Edit mode.</li>
|
||
<li><strong>Reorder presets</strong>: drag and drop preset tiles to save zone order.</li>
|
||
<li><strong>Profiles</strong>: create/clone/delete profiles. New profiles get a populated <strong>default</strong> zone and can optionally seed a <strong>DJ zone</strong>.</li>
|
||
<li><strong>Devices</strong>: registry rows are keyed by <strong>MAC</strong>; edit a device for transport/IP and per-driver Wi‑Fi settings, or use <strong>Groups</strong> for shared defaults.</li>
|
||
<li><strong>Colour Palette</strong>: build profile colours and use <strong>From Palette</strong> in preset editor to add linked colours (badge <strong>P</strong>) that update when palette colours change.</li>
|
||
</ul>
|
||
|
||
<h3>What led-tool does</h3>
|
||
<ul>
|
||
<li><strong>USB device setup</strong>: updates <code>settings.json</code> on ESP32 drivers over serial (for example name, pin, LED count, Wi-Fi credentials).</li>
|
||
<li><strong>Deploy and maintenance</strong>: uploads driver files, flashes firmware, resets device, and follows serial logs.</li>
|
||
<li><strong>Scope</strong>: led-tool configures devices directly; this web UI controls profiles/zones/presets and sends runtime messages.</li>
|
||
</ul>
|
||
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" id="help-close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Audio Modal -->
|
||
<div id="audio-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Audio Beat Detection</h2>
|
||
<p class="muted-text">Select an input device and start beat detection.</p>
|
||
<div class="form-group">
|
||
<label for="audio-device-select">Input device</label>
|
||
<div class="profiles-actions">
|
||
<select id="audio-device-select" style="flex: 1;">
|
||
<option value="">Default input</option>
|
||
</select>
|
||
<button type="button" class="btn btn-secondary" id="audio-refresh-btn">Refresh</button>
|
||
</div>
|
||
<small>Tip: for Pulse/pipewire playback capture, use a source containing <code>monitor</code>.</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="audio-device-override">Manual device override (optional)</label>
|
||
<input type="text" id="audio-device-override" placeholder='e.g. 3 or "alsa_output....monitor"'>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Current BPM</label>
|
||
<div class="audio-bpm-row">
|
||
<div id="audio-bpm-value" class="audio-bpm-readout">--</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Detected hit type</label>
|
||
<div id="audio-hit-type-value" class="audio-hit-type-readout">unknown</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Bar phase</label>
|
||
<div class="audio-bpm-row">
|
||
<div id="audio-bar-phase-value" class="audio-bpm-readout" title="Beat in bar (kick hints downbeat)">--</div>
|
||
</div>
|
||
<small class="muted-text">Bar uses kick-heavy hits (default 4/4). Tap <strong>Sync</strong> on a downbeat to lock bar phase.</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Flash on beat</label>
|
||
<div id="audio-beat-flash" class="audio-beat-flash" aria-hidden="true"></div>
|
||
</div>
|
||
|
||
<div class="settings-section audio-settings-section">
|
||
<h3>Audio settings</h3>
|
||
<div class="form-group">
|
||
<label for="audio-beat-phase-ms">Beat phase shift (ms)</label>
|
||
<input type="number" id="audio-beat-phase-ms" min="0" max="500" step="5" value="0" style="width:6rem;">
|
||
<small class="muted-text">Delays beat flashes so they line up with what you hear (saved on the controller).</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Beat sync</label>
|
||
<button type="button" id="audio-modal-beat-readout" class="audio-modal-beat-readout muted-text" disabled title="Sync step to music (S)" aria-live="polite"></button>
|
||
<small class="muted-text">While a sequence is playing, tap the BPM/beat button in the header on a downbeat to align the step counter. Shortcut: <kbd>S</kbd>.</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Sequence alignment</label>
|
||
<div class="profiles-actions" style="flex-wrap: wrap;">
|
||
<button type="button" class="btn btn-secondary" id="audio-sync-pass-btn">Restart pass</button>
|
||
</div>
|
||
<small class="muted-text"><strong>Restart pass</strong> jumps to step 1 of the sequence (<kbd>Shift+S</kbd>). Use <strong>Reset detector</strong> in the header (while audio is running) to clear stuck BPM/beat tracking without stopping audio.</small>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-primary" id="audio-start-btn">Start</button>
|
||
<button type="button" class="btn btn-secondary" id="audio-stop-btn">Stop</button>
|
||
<button type="button" class="btn btn-secondary" id="audio-close-btn">Close</button>
|
||
</div>
|
||
<div class="form-group" style="margin-top: 0.75rem;">
|
||
<label for="audio-devices-debug">Detected devices (Python)</label>
|
||
<textarea id="audio-devices-debug" rows="8" readonly style="width:100%; font-family:monospace; resize:vertical;"></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Settings Modal -->
|
||
<div id="settings-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h2>Device Settings</h2>
|
||
<p class="muted-text">Configure WiFi Access Point and device settings.</p>
|
||
|
||
<div id="settings-message" class="message"></div>
|
||
|
||
<!-- Device Name -->
|
||
<div class="settings-section">
|
||
<h3>Device</h3>
|
||
<form id="device-form">
|
||
<div class="form-group">
|
||
<label for="device-name-input">Device Name</label>
|
||
<input type="text" id="device-name-input" name="device_name" placeholder="e.g. led-controller" required>
|
||
<small>This name may be used for mDNS (e.g. <code>name.local</code>) and UI display.</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="wifi-channel-input">WiFi channel (ESP-NOW)</label>
|
||
<input type="number" id="wifi-channel-input" name="wifi_channel" min="1" max="11" required>
|
||
<small>STA channel (1–11) for LED drivers and the serial bridge. Use the same value everywhere.</small>
|
||
</div>
|
||
<div class="btn-group">
|
||
<button type="submit" class="btn btn-primary btn-full">Save device settings</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- WiFi Access Point Settings -->
|
||
<div class="settings-section ap-settings-section">
|
||
<h3>WiFi Access Point</h3>
|
||
|
||
<div id="ap-status" class="status-info">
|
||
<h4>AP Status</h4>
|
||
<p>Loading...</p>
|
||
</div>
|
||
|
||
<form id="ap-form">
|
||
<div class="form-group">
|
||
<label for="ap-ssid">AP SSID (Network Name)</label>
|
||
<input type="text" id="ap-ssid" name="ssid" placeholder="Enter AP name" required>
|
||
<small>The name of the WiFi access point this device creates</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="ap-password">AP Password</label>
|
||
<input type="password" id="ap-password" name="password" placeholder="Enter AP password (min 8 chars)" data-bwignore>
|
||
<small>Leave empty for open network (min 8 characters if set)</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="ap-channel">Channel (1-11)</label>
|
||
<input type="number" id="ap-channel" name="channel" min="1" max="11" placeholder="Auto">
|
||
<small>WiFi channel (1-11 for 2.4GHz). Leave empty for auto.</small>
|
||
</div>
|
||
|
||
<div class="btn-group">
|
||
<button type="submit" class="btn btn-primary btn-full">Configure AP</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" id="settings-close-btn">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- LED Tool Modal (led-tool/static settings editor) -->
|
||
<div id="led-tool-modal" class="modal">
|
||
<div class="modal-content" style="max-width: 960px; width: 95vw;">
|
||
<div class="modal-actions" style="margin-bottom: 0.5rem;">
|
||
<h2 style="margin: 0; flex: 1;">LED Tool — device settings</h2>
|
||
<button type="button" class="btn btn-secondary" id="led-tool-close-btn">Close</button>
|
||
</div>
|
||
<iframe id="led-tool-iframe" title="LED device settings editor" src="about:blank" allow="serial" style="width:100%;height:min(75vh,720px);border:1px solid #4a4a4a;border-radius:4px;background:#0b1020;"></iframe>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Styles moved to /static/style.css -->
|
||
<script src="/static/groups.js"></script>
|
||
<script src="/static/zones.js"></script>
|
||
<script src="/static/help.js"></script>
|
||
<script src="/static/led_tool.js"></script>
|
||
<script src="/static/color_palette.js"></script>
|
||
<script src="/static/bundle_io.js"></script>
|
||
<script src="/static/profiles.js"></script>
|
||
<script src="/static/zone_palette.js"></script>
|
||
<script src="/static/patterns.js"></script>
|
||
<script src="/static/presets.js"></script>
|
||
<script src="/static/sequences.js"></script>
|
||
<script src="/static/devices.js"></script>
|
||
<script src="/static/audio.js"></script>
|
||
<script src="/static/numpad.js"></script>
|
||
</body>
|
||
</html>
|