Files
led-controller/src/templates/index.html
Jimmy c1c3e5d71b feat(ui): edit tab zones, audio readout, live reload
- Zones/presets/sequence strip and Pipfile dev command fix
- Optional live reload and beat test audio asset + generator

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 00:44:20 +12:00

793 lines
48 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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="zones-container">
<div id="zones-list">
Loading zones...
</div>
</div>
<div class="header-end">
<div id="audio-top-indicator" class="audio-top-indicator" title="Live audio BPM">
<div class="audio-top-indicator-main">
<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>
</div>
</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 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">
<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" data-target="help-btn">Help</button>
</div>
</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 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">
<div class="modal-actions" style="margin-bottom: 1rem;">
<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>
<label>Zone Name:</label>
<input type="text" id="edit-zone-name" placeholder="Enter zone name" required>
<label class="zone-devices-label">Device groups in this zone</label>
<div id="edit-zone-devices-editor" class="zone-devices-editor"></div>
<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>
<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>
</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>
</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 + WiFi driver defaults (zones reference groups) -->
<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 WiFi defaults once per group, then attach groups to zones.</p>
<div class="profiles-actions zone-modal-create-row">
<input type="text" id="new-group-name" placeholder="Group name">
<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">
<div class="modal-actions" style="margin-bottom: 1rem;">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" id="edit-group-close-btn">Cancel</button>
</div>
<label for="edit-group-name">Group name</label>
<input type="text" id="edit-group-name" required autocomplete="off">
<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 (0255)</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;">WiFi 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>
</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 WiFi 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 (0255)</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 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="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 class="preset-editor-field">
<label for="sequence-editor-advance-mode">Advance</label>
<select id="sequence-editor-advance-mode" style="max-width:16rem;">
<option value="time">Time (ms between steps)</option>
<option value="beats">Audio beats (requires Audio detector)</option>
</select>
</div>
<div class="preset-editor-field" id="sequence-editor-duration-wrap">
<label for="sequence-editor-duration">Step duration (ms), all lanes together</label>
<div style="display:flex;align-items:center;gap:0.6rem;flex-wrap:wrap;">
<input type="number" id="sequence-editor-duration" min="200" max="600000" value="3000" style="width:8rem;">
<span id="sequence-editor-time-bpm-hint" class="muted-text" style="font-size:0.9em;"></span>
</div>
</div>
<div class="preset-editor-field" id="sequence-editor-transition-wrap">
<label for="sequence-editor-transition">Pause before next step (ms)</label>
<input type="number" id="sequence-editor-transition" min="0" max="60000" value="500" style="width:8rem;">
</div>
<div id="sequence-editor-beats-panel" style="display:none;margin:0 0 0.75rem 0;">
<p id="sequence-editor-bpm-live" class="muted-text" style="font-size:0.85em;margin:0;"></p>
</div>
<div id="sequence-editor-lanes"></div>
<div class="modal-actions" style="margin-top:0.75rem;">
<button type="button" class="btn btn-secondary btn-small" id="sequence-editor-add-lane-btn">Add lane</button>
</div>
<div class="modal-actions preset-editor-modal-actions">
<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 (0255)</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>
<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="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 &amp; 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, WiFi 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 WiFi 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 id="audio-modal-beat-readout" class="audio-modal-beat-readout muted-text" aria-live="polite"></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>Flash on beat</label>
<div id="audio-beat-flash" class="audio-beat-flash" aria-hidden="true"></div>
</div>
<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 and sequenced beats so they line up with what you hear (saved in this browser).</small>
</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 (111) 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 -->
<div id="led-tool-modal" class="modal">
<div class="modal-content">
<h2>LED Tool (USB)</h2>
<p class="muted-text" style="margin-top: 0;">Configure a driver connected over USB using <code>led-tool</code>.</p>
<div id="led-tool-message" class="message" style="margin-bottom: 0.75rem;"></div>
<form id="led-tool-form">
<div class="form-group">
<label for="led-tool-port">Serial port</label>
<div class="profiles-actions" style="gap: 0.5rem;">
<select id="led-tool-port" required style="flex:1;">
<option value="">Select a serial port</option>
</select>
<button type="button" class="btn btn-secondary" id="led-tool-refresh-ports-btn">Refresh</button>
</div>
</div>
<div class="form-group">
<label for="led-tool-name">Name</label>
<input type="text" id="led-tool-name" placeholder="led-abcdef123456">
</div>
<div class="profiles-actions">
<div class="preset-editor-field">
<label for="led-tool-num-leds">Num LEDs</label>
<input type="number" id="led-tool-num-leds" min="1" max="5000" placeholder="60">
</div>
<div class="preset-editor-field">
<label for="led-tool-led-pin">LED pin</label>
<input type="number" id="led-tool-led-pin" min="0" max="48" placeholder="4">
</div>
</div>
<div class="profiles-actions">
<div class="preset-editor-field">
<label for="led-tool-brightness">Brightness</label>
<input type="number" id="led-tool-brightness" min="0" max="255" placeholder="255">
</div>
<div class="preset-editor-field">
<label for="led-tool-wifi-channel">WiFi channel</label>
<input type="number" id="led-tool-wifi-channel" min="1" max="11" placeholder="6">
</div>
</div>
<div class="profiles-actions">
<div class="preset-editor-field">
<label for="led-tool-transport">Transport</label>
<select id="led-tool-transport">
<option value="">(no change)</option>
<option value="espnow">espnow</option>
<option value="wifi">wifi</option>
</select>
</div>
<div class="preset-editor-field">
<label for="led-tool-default">Default preset</label>
<input type="text" id="led-tool-default" placeholder="on">
</div>
</div>
<div class="form-group">
<label for="led-tool-ssid">SSID</label>
<input type="text" id="led-tool-ssid" placeholder="Your WiFi SSID">
</div>
<div class="form-group">
<label for="led-tool-password">WiFi password</label>
<input type="password" id="led-tool-password" placeholder="WiFi password">
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" id="led-tool-read-btn">Read</button>
<button type="button" class="btn btn-secondary" id="led-tool-reset-btn">Reset</button>
<button type="submit" class="btn btn-primary">Apply via USB</button>
<button type="button" class="btn btn-secondary" id="led-tool-close-btn">Close</button>
</div>
</form>
<label for="led-tool-output" style="margin-top:0.5rem; display:block;">Command output</label>
<textarea id="led-tool-output" rows="12" readonly style="width:100%; font-family:monospace; resize:vertical;"></textarea>
</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/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>
</body>
</html>