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 ( '
No profile selected
', 200, {"Content-Type": "text/html"}, ) zone_order = get_profile_zone_order(profile_id) current_zone_id = get_current_zone_id(request, session) html = '
' 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 += ( '" ) html += "
" 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 ( '
No current zone set
', 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 '
Zone not found
', 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 = ( '
' "

Presets

" '
' '
' "" "
" "
" ) return html, 200, {"Content-Type": "text/html"} @controller.get("//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("//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("/") 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("/") 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("/") @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("//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