From a7e921805a7f6a4c5ff46beb0cfa36771f653669 Mon Sep 17 00:00:00 2001 From: jimmy Date: Tue, 27 Jan 2026 13:05:01 +1300 Subject: [PATCH] Update controllers to return JSON and fix parameter handling - Fix decorator parameter order issues with @with_session - Return JSON responses instead of HTML fragments - Add proper error handling with JSON error responses - Fix route parameter conflicts in delete and update endpoints --- src/controllers/preset.py | 3 +- src/controllers/profile.py | 36 ++++- src/controllers/tab.py | 280 ++++++++++++++++--------------------- 3 files changed, 156 insertions(+), 163 deletions(-) diff --git a/src/controllers/preset.py b/src/controllers/preset.py index 8178ef9..081fb8f 100644 --- a/src/controllers/preset.py +++ b/src/controllers/preset.py @@ -25,7 +25,8 @@ async def create_preset(request): data = request.json preset_id = presets.create() if presets.update(preset_id, data): - return json.dumps(presets.read(preset_id)), 201, {'Content-Type': 'application/json'} + preset_data = presets.read(preset_id) + return json.dumps({preset_id: preset_data}), 201, {'Content-Type': 'application/json'} return json.dumps({"error": "Failed to create preset"}), 400 except Exception as e: return json.dumps({"error": str(e)}), 400 diff --git a/src/controllers/profile.py b/src/controllers/profile.py index fff19c0..859379c 100644 --- a/src/controllers/profile.py +++ b/src/controllers/profile.py @@ -7,9 +7,29 @@ controller = Microdot() profiles = Profile() @controller.get('') -async def list_profiles(request): - """List all profiles.""" - return json.dumps(profiles), 200, {'Content-Type': 'application/json'} +@with_session +async def list_profiles(request, session): + """List all profiles with current profile info.""" + profile_list = profiles.list() + current_id = session.get('current_profile') + + # If no current profile in session, use first one + if not current_id and profile_list: + current_id = profile_list[0] + session['current_profile'] = str(current_id) + session.save() + + # Build profiles object + profiles_data = {} + for profile_id in profile_list: + profile_data = profiles.read(profile_id) + if profile_data: + profiles_data[profile_id] = profile_data + + return json.dumps({ + "profiles": profiles_data, + "current_profile_id": current_id + }), 200, {'Content-Type': 'application/json'} @controller.get('/current') @with_session @@ -27,8 +47,13 @@ async def get_current_profile(request, session): return json.dumps({"error": "No profile available"}), 404 @controller.get('/') -async def get_profile(request, id): +@with_session +async def get_profile(request, id, session): """Get a specific profile by ID.""" + # Handle 'current' as a special case + if id == 'current': + return await get_current_profile(request, session) + profile = profiles.read(id) if profile: return json.dumps(profile), 200, {'Content-Type': 'application/json'} @@ -53,7 +78,8 @@ async def create_profile(request): profile_id = profiles.create(name) if data: profiles.update(profile_id, data) - return json.dumps(profiles.read(profile_id)), 201, {'Content-Type': 'application/json'} + profile_data = profiles.read(profile_id) + return json.dumps({profile_id: profile_data}), 201, {'Content-Type': 'application/json'} except Exception as e: return json.dumps({"error": str(e)}), 400 diff --git a/src/controllers/tab.py b/src/controllers/tab.py index ea0f741..ed176eb 100644 --- a/src/controllers/tab.py +++ b/src/controllers/tab.py @@ -33,13 +33,13 @@ def get_profile_tab_order(profile_id): return [] def get_current_tab_id(request, session=None): - """Get the current tab ID from session.""" - if session: - current_tab = session.get('current_tab') - if current_tab: - return current_tab + """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 if no session + # Fallback to first tab in current profile profile_id = get_current_profile_id(session) if profile_id: profile = profiles.read(profile_id) @@ -50,16 +50,8 @@ def get_current_tab_id(request, session=None): return tabs_list[0] return None -@controller.get('') -async def list_tabs(request): - """List all tabs.""" - return json.dumps(tabs), 200, {'Content-Type': 'application/json'} - -# HTML Fragment endpoints for htmx - must be before / route -@controller.get('/list-fragment') -@with_session -async def tabs_list_fragment(request, session): - """Return HTML fragment for the tabs list.""" +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: @@ -69,7 +61,7 @@ async def tabs_list_fragment(request, session): "sessionId": "debug-session", "runId": "tabs-pre-fix", "hypothesisId": "H1", - "location": "src/controllers/tab.py:tabs_list_fragment", + "location": "src/controllers/tab.py:_render_tabs_list_fragment", "message": "tabs list fragment", "data": { "profile_id": profile_id, @@ -106,49 +98,18 @@ async def tabs_list_fragment(request, session): html += '' return html, 200, {'Content-Type': 'text/html'} -@controller.get('/create-form-fragment') -async def create_tab_form_fragment(request): - """Return the create tab form HTML fragment.""" - html = ''' -

Add New Tab

-
- - - - - -
- ''' - return html, 200, {'Content-Type': 'text/html'} - -@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: - 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 - - return await tab_content_fragment.__wrapped__(request, session, current_tab_id) - -@controller.get('//content-fragment') -@with_session -async def tab_content_fragment(request, session, id): - """Return HTML fragment for tab content.""" +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': - return await get_current_tab(request, session) + 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: @@ -177,6 +138,62 @@ async def tab_content_fragment(request, session, id): ) 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.""" @@ -198,84 +215,60 @@ async def update_tab(request, id): @controller.delete('/') @with_session -async def delete_tab(request, id, session): +async def delete_tab(request, session, id): """Delete a tab.""" - # Check if this is an htmx request (wants HTML fragment) - accept_header = request.headers.get('Accept', '') - wants_html = 'text/html' in accept_header - - # 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: - if wants_html: - return '
No current tab to delete
', 404, {'Content-Type': 'text/html'} - 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) + 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 - # Clear session if the deleted tab was the current tab - current_tab_id = get_current_tab_id(request, session) - if current_tab_id == id: - if 'current_tab' in session: - session.pop('current_tab', None) - session.save() - - if wants_html: - return await tabs_list_fragment.__wrapped__(request, session) - else: + 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'} - - if wants_html: - return '
Tab not found
', 404, {'Content-Type': 'text/html'} - return json.dumps({"error": "Tab not found"}), 404 + + 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.""" - # Check if this is an htmx request (wants HTML fragment) - accept_header = request.headers.get('Accept', '') - wants_html = 'text/html' in accept_header - # #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": "H3", - "location": "src/controllers/tab.py:create_tab_htmx", - "message": "create tab with session", - "data": { - "wants_html": wants_html, - "has_form": bool(request.form), - "accept": accept_header - }, - "timestamp": int(time.time() * 1000) - }) + "\n") - except Exception: - pass - # #endregion - - try: - # Handle form data (htmx) or JSON + # Handle form data or JSON if request.form: name = request.form.get('name', '').strip() ids_str = request.form.get('ids', '1').strip() @@ -288,8 +281,6 @@ async def create_tab(request, session): preset_ids = data.get("presets", None) if not name: - if wants_html: - return '
Tab name cannot be empty
', 400, {'Content-Type': 'text/html'} return json.dumps({"error": "Tab name cannot be empty"}), 400 tab_id = tabs.create(name, names, preset_ids) @@ -308,36 +299,11 @@ async def create_tab(request, session): if 'tab_order' in profile: del profile['tab_order'] profiles.update(profile_id, profile) - # #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": "H4", - "location": "src/controllers/tab.py:create_tab_htmx", - "message": "tab created and profile updated", - "data": { - "tab_id": tab_id, - "profile_id": profile_id, - "profile_tabs": tabs_list if profile_id and profile else None - }, - "timestamp": int(time.time() * 1000) - }) + "\n") - except Exception: - pass - # #endregion - if wants_html: - # Return HTML fragment for tabs list - return await tabs_list_fragment.__wrapped__(request, session) - else: - # Return JSON response - return json.dumps(tabs.read(tab_id)), 201, {'Content-Type': 'application/json'} + # 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) - if wants_html: - return f'
Error: {str(e)}
', 400, {'Content-Type': 'text/html'} return json.dumps({"error": str(e)}), 400