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

@@ -19,7 +19,11 @@ def _maybe_migrate_tab_json_to_zone():
class Zone(Model):
"""Preset layout row (stored in ``db/zone.json``); legacy storage was ``tab.json`` / Tab."""
"""Preset layout row (stored in ``db/zone.json``); legacy storage was ``tab.json`` / Tab.
Optional ``content_kind`` on a row: ``\"presets\"`` (preset tiles only) or ``\"sequences\"``
(sequence tiles only). Omitted or unknown => both (legacy behaviour).
"""
def __init__(self):
if not getattr(Zone, "_migration_checked", False):
@@ -42,12 +46,12 @@ class Zone(Model):
if changed:
self.save()
def create(self, name="", names=None, presets=None, group_ids=None):
def create(self, name="", names=None, presets=None, group_ids=None, content_kind=None):
next_id = self.get_next_id()
gid_list = []
if isinstance(group_ids, list):
gid_list = [str(x) for x in group_ids if x is not None]
self[next_id] = {
gid_list = [str(x).strip() for x in group_ids if x is not None and str(x).strip()]
doc = {
"name": name,
"names": names if names else [],
"group_ids": gid_list,
@@ -56,6 +60,9 @@ class Zone(Model):
"default_preset": None,
"brightness": 255,
}
if content_kind in ("presets", "sequences"):
doc["content_kind"] = content_kind
self[next_id] = doc
self.save()
return next_id