from microdot import Microdot, send_file from microdot.session import with_session from models.tab import Tab from models.profile import Profile import json import os import time controller = Microdot() tabs = Tab() 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 get_profile_tab_order(profile_id): """Get the tab order for a profile.""" if not profile_id: return [] profile = profiles.read(profile_id) if profile: # Support both "tab_order" (old) and "tabs" (new) format return profile.get("tabs", profile.get("tab_order", [])) return [] def get_current_tab_id(request, session=None): """Get the current tab ID from cookie.""" # Read from cookie first current_tab = request.cookies.get('current_tab') if current_tab: return current_tab # Fallback to first tab in current profile profile_id = get_current_profile_id(session) if profile_id: profile = profiles.read(profile_id) if profile: # Support both "tabs" (new) and "tab_order" (old) format tabs_list = profile.get("tabs", profile.get("tab_order", [])) if tabs_list: return tabs_list[0] return None def _render_tabs_list_fragment(request, session): """Helper function to render tabs list HTML fragment.""" profile_id = get_current_profile_id(session) # #region agent log try: os.makedirs('/home/pi/led-controller/.cursor', exist_ok=True) with open('/home/pi/led-controller/.cursor/debug.log', 'a') as _log: _log.write(json.dumps({ "sessionId": "debug-session", "runId": "tabs-pre-fix", "hypothesisId": "H1", "location": "src/controllers/tab.py:_render_tabs_list_fragment", "message": "tabs list fragment", "data": { "profile_id": profile_id, "profile_count": len(profiles.list()) }, "timestamp": int(time.time() * 1000) }) + "\n") except Exception: pass # #endregion if not profile_id: return '
No profile selected
', 200, {'Content-Type': 'text/html'} tab_order = get_profile_tab_order(profile_id) current_tab_id = get_current_tab_id(request, session) html = '
' for tab_id in tab_order: tab_data = tabs.read(tab_id) if tab_data: active_class = 'active' if str(tab_id) == str(current_tab_id) else '' tab_name = tab_data.get('name', 'Tab ' + str(tab_id)) html += ( '' ) html += '
' return html, 200, {'Content-Type': 'text/html'} def _render_tab_content_fragment(request, session, id): """Helper function to render tab content HTML fragment.""" # Handle 'current' as a special case if id == 'current': current_tab_id = get_current_tab_id(request, session) if not current_tab_id: accept_header = request.headers.get('Accept', '') wants_html = 'text/html' in accept_header if wants_html: return '
No current tab set
', 404, {'Content-Type': 'text/html'} return json.dumps({"error": "No current tab set"}), 404 id = current_tab_id tab = tabs.read(id) if not tab: return '
Tab not found
', 404, {'Content-Type': 'text/html'} # Set this tab as the current tab in session session['current_tab'] = str(id) session.save() # If this is a direct page load (not HTMX), return full UI so CSS loads. if not request.headers.get('HX-Request'): return send_file('templates/index.html') tab_name = tab.get('name', 'Tab ' + str(id)) html = ( '
' '

Presets

' '
' '
' '' '
' '
' ) return html, 200, {'Content-Type': 'text/html'} @controller.get('') @with_session async def list_tabs(request, session): """List all tabs with current tab info.""" profile_id = get_current_profile_id(session) current_tab_id = get_current_tab_id(request, session) # Get tab order for current profile tab_order = get_profile_tab_order(profile_id) if profile_id else [] # Build tabs list with metadata tabs_data = {} for tab_id in tabs.list(): tab_data = tabs.read(tab_id) if tab_data: tabs_data[tab_id] = tab_data return json.dumps({ "tabs": tabs_data, "tab_order": tab_order, "current_tab_id": current_tab_id, "profile_id": profile_id }), 200, {'Content-Type': 'application/json'} # Get current tab - returns JSON with tab data and content info @controller.get('/current') @with_session async def get_current_tab(request, session): """Get the current tab from session.""" current_tab_id = get_current_tab_id(request, session) if not current_tab_id: return json.dumps({"error": "No current tab set", "tab": None, "tab_id": None}), 404 tab = tabs.read(current_tab_id) if tab: return json.dumps({ "tab": tab, "tab_id": current_tab_id }), 200, {'Content-Type': 'application/json'} return json.dumps({"error": "Tab not found", "tab": None, "tab_id": None}), 404 @controller.post('//set-current') async def set_current_tab(request, id): """Set a tab as the current tab in cookie.""" tab = tabs.read(id) if not tab: return json.dumps({"error": "Tab not found"}), 404 # Set cookie with current tab response_data = json.dumps({"message": "Current tab set", "tab_id": id}) response = response_data, 200, { 'Content-Type': 'application/json', 'Set-Cookie': f'current_tab={id}; Path=/; Max-Age=31536000' # 1 year expiry } return response @controller.get('/') async def get_tab(request, id): """Get a specific tab by ID.""" tab = tabs.read(id) if tab: return json.dumps(tab), 200, {'Content-Type': 'application/json'} return json.dumps({"error": "Tab not found"}), 404 @controller.put('/') async def update_tab(request, id): """Update an existing tab.""" try: data = request.json if tabs.update(id, data): return json.dumps(tabs.read(id)), 200, {'Content-Type': 'application/json'} return json.dumps({"error": "Tab not found"}), 404 except Exception as e: return json.dumps({"error": str(e)}), 400 @controller.delete('/') @with_session async def delete_tab(request, session, id): """Delete a tab.""" try: # Handle 'current' tab ID if id == 'current': current_tab_id = get_current_tab_id(request, session) if current_tab_id: id = current_tab_id else: return json.dumps({"error": "No current tab to delete"}), 404 if tabs.delete(id): # Remove from profile's tabs profile_id = get_current_profile_id(session) if profile_id: profile = profiles.read(profile_id) if profile: # Support both "tabs" (new) and "tab_order" (old) format tabs_list = profile.get('tabs', profile.get('tab_order', [])) if id in tabs_list: tabs_list.remove(id) profile['tabs'] = tabs_list # Remove old tab_order if it exists if 'tab_order' in profile: del profile['tab_order'] profiles.update(profile_id, profile) # Clear cookie if the deleted tab was the current tab current_tab_id = get_current_tab_id(request, session) if current_tab_id == id: response_data = json.dumps({"message": "Tab deleted successfully"}) response = response_data, 200, { 'Content-Type': 'application/json', 'Set-Cookie': 'current_tab=; Path=/; Max-Age=0' # Clear cookie } return response return json.dumps({"message": "Tab deleted successfully"}), 200, {'Content-Type': 'application/json'} return json.dumps({"error": "Tab not found"}), 404 except Exception as e: import sys try: sys.print_exception(e) except: pass return json.dumps({"error": str(e)}), 500, {'Content-Type': 'application/json'} @controller.post('') @with_session async def create_tab(request, session): """Create a new tab.""" try: # Handle form data or JSON if request.form: name = request.form.get('name', '').strip() ids_str = request.form.get('ids', '1').strip() names = [id.strip() for id in ids_str.split(',') if id.strip()] preset_ids = None else: data = request.json or {} name = data.get("name", "") names = data.get("names", None) preset_ids = data.get("presets", None) if not name: return json.dumps({"error": "Tab name cannot be empty"}), 400 tab_id = tabs.create(name, names, preset_ids) # Add to current profile's tabs profile_id = get_current_profile_id(session) if profile_id: profile = profiles.read(profile_id) if profile: # Support both "tabs" (new) and "tab_order" (old) format tabs_list = profile.get('tabs', profile.get('tab_order', [])) if tab_id not in tabs_list: tabs_list.append(tab_id) profile['tabs'] = tabs_list # Remove old tab_order if it exists if 'tab_order' in profile: del profile['tab_order'] profiles.update(profile_id, profile) # Return JSON response with tab ID tab_data = tabs.read(tab_id) return json.dumps({tab_id: tab_data}), 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_tab(request, session, id): """Clone an existing tab and add it to the current profile.""" try: source = tabs.read(id) if not source: return json.dumps({"error": "Tab not found"}), 404 data = request.json or {} source_name = source.get("name") or f"Tab {id}" new_name = data.get("name") or f"{source_name} Copy" clone_id = tabs.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: tabs.update(clone_id, extra) profile_id = get_current_profile_id(session) if profile_id: profile = profiles.read(profile_id) if profile: tabs_list = profile.get('tabs', profile.get('tab_order', [])) if clone_id not in tabs_list: tabs_list.append(clone_id) profile['tabs'] = tabs_list if 'tab_order' in profile: del profile['tab_order'] profiles.update(profile_id, profile) tab_data = tabs.read(clone_id) return json.dumps({clone_id: tab_data}), 201, {'Content-Type': 'application/json'} except Exception as e: import sys try: sys.print_exception(e) except: pass return json.dumps({"error": str(e)}), 400