362 lines
11 KiB
Python
362 lines
11 KiB
Python
from microdot import Microdot, send_file
|
|
from microdot.session import with_session
|
|
from models.zone import Zone
|
|
from models.profile import Profile
|
|
import json
|
|
|
|
controller = Microdot()
|
|
zones = Zone()
|
|
profiles = Profile()
|
|
|
|
|
|
def get_current_profile_id(session=None):
|
|
"""Get the current active profile ID from session or fallback to first."""
|
|
profile_list = profiles.list()
|
|
session_profile = None
|
|
if session is not None:
|
|
session_profile = session.get("current_profile")
|
|
if session_profile and session_profile in profile_list:
|
|
return session_profile
|
|
if profile_list:
|
|
return profile_list[0]
|
|
return None
|
|
|
|
|
|
def _profile_zone_id_list(profile):
|
|
"""Ordered zone ids for a profile (``zones``, legacy ``tabs``, or ``zone_order``)."""
|
|
if not profile or not isinstance(profile, dict):
|
|
return []
|
|
z = profile.get("zones")
|
|
if isinstance(z, list) and z:
|
|
return list(z)
|
|
t = profile.get("zones")
|
|
if isinstance(t, list) and t:
|
|
return list(t)
|
|
o = profile.get("zone_order")
|
|
if isinstance(o, list) and o:
|
|
return list(o)
|
|
return []
|
|
|
|
|
|
def get_profile_zone_order(profile_id):
|
|
if not profile_id:
|
|
return []
|
|
profile = profiles.read(profile_id)
|
|
return _profile_zone_id_list(profile)
|
|
|
|
|
|
def _set_profile_zone_order(profile, ids):
|
|
profile["zones"] = list(ids)
|
|
profile.pop("tabs", None)
|
|
profile.pop("zone_order", None)
|
|
|
|
|
|
def get_current_zone_id(request, session=None):
|
|
"""Cookie ``current_zone``, legacy ``current_zone``, then first zone in profile."""
|
|
z = request.cookies.get("current_zone") or request.cookies.get("current_zone")
|
|
if z:
|
|
return z
|
|
profile_id = get_current_profile_id(session)
|
|
if profile_id:
|
|
profile = profiles.read(profile_id)
|
|
order = _profile_zone_id_list(profile)
|
|
if order:
|
|
return order[0]
|
|
return None
|
|
|
|
|
|
def _render_zones_list_fragment(request, session):
|
|
"""Render zone strip HTML for HTMX / JS."""
|
|
profile_id = get_current_profile_id(session)
|
|
if not profile_id:
|
|
return (
|
|
'<div class="zones-list">No profile selected</div>',
|
|
200,
|
|
{"Content-Type": "text/html"},
|
|
)
|
|
|
|
zone_order = get_profile_zone_order(profile_id)
|
|
current_zone_id = get_current_zone_id(request, session)
|
|
|
|
html = '<div class="zones-list">'
|
|
for zid in zone_order:
|
|
zdata = zones.read(zid)
|
|
if zdata:
|
|
active_class = "active" if str(zid) == str(current_zone_id) else ""
|
|
zname = zdata.get("name", "Zone " + str(zid))
|
|
html += (
|
|
'<button class="zone-button ' + active_class + '" '
|
|
'hx-get="/zones/' + str(zid) + '/content-fragment" '
|
|
'hx-target="#zone-content" '
|
|
'hx-swap="innerHTML" '
|
|
'hx-push-url="true" '
|
|
'hx-trigger="click" '
|
|
'onclick="document.querySelectorAll(\'.zone-button\').forEach(b => b.classList.remove(\'active\')); this.classList.add(\'active\');">'
|
|
+ zname
|
|
+ "</button>"
|
|
)
|
|
html += "</div>"
|
|
return html, 200, {"Content-Type": "text/html"}
|
|
|
|
|
|
def _render_zone_content_fragment(request, session, id):
|
|
if id == "current":
|
|
current_zone_id = get_current_zone_id(request, session)
|
|
if not current_zone_id:
|
|
accept_header = request.headers.get("Accept", "")
|
|
wants_html = "text/html" in accept_header
|
|
if wants_html:
|
|
return (
|
|
'<div class="error">No current zone set</div>',
|
|
404,
|
|
{"Content-Type": "text/html"},
|
|
)
|
|
return json.dumps({"error": "No current zone set"}), 404
|
|
id = current_zone_id
|
|
|
|
z = zones.read(id)
|
|
if not z:
|
|
return '<div>Zone not found</div>', 404, {"Content-Type": "text/html"}
|
|
|
|
session["current_zone"] = str(id)
|
|
session.save()
|
|
|
|
if not request.headers.get("HX-Request"):
|
|
return send_file("templates/index.html")
|
|
|
|
html = (
|
|
'<div class="presets-section" data-zone-id="' + str(id) + '">'
|
|
"<h3>Presets</h3>"
|
|
'<div class="profiles-actions" style="margin-bottom: 1rem;"></div>'
|
|
'<div id="presets-list-zone" class="presets-list">'
|
|
"<!-- Presets will be loaded here -->"
|
|
"</div>"
|
|
"</div>"
|
|
)
|
|
return html, 200, {"Content-Type": "text/html"}
|
|
|
|
|
|
@controller.get("/<id>/content-fragment")
|
|
@with_session
|
|
async def zone_content_fragment(request, session, id):
|
|
return _render_zone_content_fragment(request, session, id)
|
|
|
|
|
|
@controller.get("")
|
|
@with_session
|
|
async def list_zones(request, session):
|
|
profile_id = get_current_profile_id(session)
|
|
current_zone_id = get_current_zone_id(request, session)
|
|
zone_order = get_profile_zone_order(profile_id) if profile_id else []
|
|
|
|
zones_data = {}
|
|
for zid in zones.list():
|
|
zdata = zones.read(zid)
|
|
if zdata:
|
|
zones_data[zid] = zdata
|
|
|
|
return (
|
|
json.dumps(
|
|
{
|
|
"zones": zones_data,
|
|
"zone_order": zone_order,
|
|
"current_zone_id": current_zone_id,
|
|
"profile_id": profile_id,
|
|
}
|
|
),
|
|
200,
|
|
{"Content-Type": "application/json"},
|
|
)
|
|
|
|
|
|
@controller.get("/current")
|
|
@with_session
|
|
async def get_current_zone(request, session):
|
|
current_zone_id = get_current_zone_id(request, session)
|
|
if not current_zone_id:
|
|
return (
|
|
json.dumps({"error": "No current zone set", "zone": None, "zone_id": None}),
|
|
404,
|
|
)
|
|
|
|
z = zones.read(current_zone_id)
|
|
if z:
|
|
return (
|
|
json.dumps({"zone": z, "zone_id": current_zone_id}),
|
|
200,
|
|
{"Content-Type": "application/json"},
|
|
)
|
|
return (
|
|
json.dumps({"error": "Zone not found", "zone": None, "zone_id": None}),
|
|
404,
|
|
)
|
|
|
|
|
|
@controller.post("/<id>/set-current")
|
|
async def set_current_zone(request, id):
|
|
z = zones.read(id)
|
|
if not z:
|
|
return json.dumps({"error": "Zone not found"}), 404
|
|
|
|
response_data = json.dumps({"message": "Current zone set", "zone_id": id})
|
|
return (
|
|
response_data,
|
|
200,
|
|
{
|
|
"Content-Type": "application/json",
|
|
"Set-Cookie": (
|
|
f"current_zone={id}; Path=/; Max-Age=31536000; SameSite=Lax"
|
|
),
|
|
},
|
|
)
|
|
|
|
|
|
@controller.get("/<id>")
|
|
async def get_zone(request, id):
|
|
z = zones.read(id)
|
|
if z:
|
|
return json.dumps(z), 200, {"Content-Type": "application/json"}
|
|
return json.dumps({"error": "Zone not found"}), 404
|
|
|
|
|
|
@controller.put("/<id>")
|
|
async def update_zone(request, id):
|
|
try:
|
|
data = request.json
|
|
if zones.update(id, data):
|
|
return json.dumps(zones.read(id)), 200, {"Content-Type": "application/json"}
|
|
return json.dumps({"error": "Zone not found"}), 404
|
|
except Exception as e:
|
|
return json.dumps({"error": str(e)}), 400
|
|
|
|
|
|
@controller.delete("/<id>")
|
|
@with_session
|
|
async def delete_zone(request, session, id):
|
|
try:
|
|
if id == "current":
|
|
current_zone_id = get_current_zone_id(request, session)
|
|
if current_zone_id:
|
|
id = current_zone_id
|
|
else:
|
|
return json.dumps({"error": "No current zone to delete"}), 404
|
|
|
|
if zones.delete(id):
|
|
profile_id = get_current_profile_id(session)
|
|
if profile_id:
|
|
profile = profiles.read(profile_id)
|
|
if profile:
|
|
zlist = _profile_zone_id_list(profile)
|
|
if id in zlist:
|
|
zlist.remove(id)
|
|
_set_profile_zone_order(profile, zlist)
|
|
profiles.update(profile_id, profile)
|
|
|
|
current_zone_id = get_current_zone_id(request, session)
|
|
if current_zone_id == id:
|
|
response_data = json.dumps({"message": "Zone deleted successfully"})
|
|
return (
|
|
response_data,
|
|
200,
|
|
{
|
|
"Content-Type": "application/json",
|
|
"Set-Cookie": (
|
|
"current_zone=; Path=/; Max-Age=0; SameSite=Lax"
|
|
),
|
|
},
|
|
)
|
|
|
|
return json.dumps({"message": "Zone deleted successfully"}), 200, {
|
|
"Content-Type": "application/json"
|
|
}
|
|
|
|
return json.dumps({"error": "Zone not found"}), 404
|
|
except Exception as e:
|
|
import sys
|
|
|
|
try:
|
|
sys.print_exception(e)
|
|
except Exception:
|
|
pass
|
|
return json.dumps({"error": str(e)}), 500, {"Content-Type": "application/json"}
|
|
|
|
|
|
@controller.post("")
|
|
@with_session
|
|
async def create_zone(request, session):
|
|
try:
|
|
if request.form:
|
|
name = request.form.get("name", "").strip()
|
|
ids_str = request.form.get("ids", "1").strip()
|
|
names = [i.strip() for i in ids_str.split(",") if i.strip()]
|
|
preset_ids = None
|
|
else:
|
|
data = request.json or {}
|
|
name = data.get("name", "")
|
|
names = data.get("names")
|
|
if names is None:
|
|
names = data.get("ids")
|
|
preset_ids = data.get("presets", None)
|
|
|
|
if not name:
|
|
return json.dumps({"error": "Zone name cannot be empty"}), 400
|
|
|
|
zid = zones.create(name, names, preset_ids)
|
|
|
|
profile_id = get_current_profile_id(session)
|
|
if profile_id:
|
|
profile = profiles.read(profile_id)
|
|
if profile:
|
|
zlist = _profile_zone_id_list(profile)
|
|
if zid not in zlist:
|
|
zlist.append(zid)
|
|
_set_profile_zone_order(profile, zlist)
|
|
profiles.update(profile_id, profile)
|
|
|
|
zdata = zones.read(zid)
|
|
return json.dumps({zid: zdata}), 201, {"Content-Type": "application/json"}
|
|
except Exception as e:
|
|
import sys
|
|
|
|
sys.print_exception(e)
|
|
return json.dumps({"error": str(e)}), 400
|
|
|
|
|
|
@controller.post("/<id>/clone")
|
|
@with_session
|
|
async def clone_zone(request, session, id):
|
|
try:
|
|
source = zones.read(id)
|
|
if not source:
|
|
return json.dumps({"error": "Zone not found"}), 404
|
|
|
|
data = request.json or {}
|
|
source_name = source.get("name") or f"Zone {id}"
|
|
new_name = data.get("name") or f"{source_name} Copy"
|
|
clone_id = zones.create(new_name, source.get("names"), source.get("presets"))
|
|
extra = {k: v for k, v in source.items() if k not in ("name", "names", "presets")}
|
|
if extra:
|
|
zones.update(clone_id, extra)
|
|
|
|
profile_id = get_current_profile_id(session)
|
|
if profile_id:
|
|
profile = profiles.read(profile_id)
|
|
if profile:
|
|
zlist = _profile_zone_id_list(profile)
|
|
if clone_id not in zlist:
|
|
zlist.append(clone_id)
|
|
_set_profile_zone_order(profile, zlist)
|
|
profiles.update(profile_id, profile)
|
|
|
|
zdata = zones.read(clone_id)
|
|
return json.dumps({clone_id: zdata}), 201, {"Content-Type": "application/json"}
|
|
except Exception as e:
|
|
import sys
|
|
|
|
try:
|
|
sys.print_exception(e)
|
|
except Exception:
|
|
pass
|
|
return json.dumps({"error": str(e)}), 400
|
|
|