feat(zones): profile-scoped groups, zone modes, sequence brightness

- Optional profile_id on groups; UI and API for shared vs profile-only groups\n- Zone content_kind (presets vs sequences); edit modal shows matching sections; devices via groups only\n- Server sequence playback folds zone brightness into preset wire b (per MAC where needed)\n- Related preset/sequence/audio/beat-route and client updates

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-13 01:58:00 +12:00
parent c1c3e5d71b
commit 6c9e06f33b
21 changed files with 1034 additions and 604 deletions

View File

@@ -83,6 +83,11 @@
<input type="text" id="new-zone-name" placeholder="Zone name">
<button class="btn btn-primary" id="create-zone-btn">Create</button>
</div>
<fieldset class="muted-text" style="margin:0.35rem 0 0.75rem;border:none;padding:0;">
<legend style="font-size:0.85em;margin-bottom:0.35rem;">This zone is for</legend>
<label style="margin-right:1rem;"><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>
</fieldset>
<div id="zones-list-modal" class="profiles-list"></div>
<div class="modal-actions">
<button class="btn btn-secondary" id="zones-close-btn">Close</button>
@@ -102,16 +107,22 @@
</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>
<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>
</form>
</div>
</div>
@@ -148,13 +159,16 @@
</div>
</div>
<!-- Device groups: members + WiFi driver defaults (zones reference groups) -->
<!-- Device groups: members + WiFi 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 WiFi defaults once per group, then attach groups to zones.</p>
<p class="muted-text" style="margin-top:0;">Assign drivers to a group, set WiFi defaults once per group, then attach groups to a zone for standalone presets (sequences use each lanes 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>
@@ -175,6 +189,10 @@
</div>
<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;">
@@ -315,26 +333,15 @@
<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 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>
</div>
<div id="sequence-editor-lanes"></div>
<div class="modal-actions" style="margin-top:0.75rem;">
@@ -377,6 +384,7 @@
<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>