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
This commit is contained in:
@@ -25,7 +25,8 @@ async def create_preset(request):
|
|||||||
data = request.json
|
data = request.json
|
||||||
preset_id = presets.create()
|
preset_id = presets.create()
|
||||||
if presets.update(preset_id, data):
|
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
|
return json.dumps({"error": "Failed to create preset"}), 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return json.dumps({"error": str(e)}), 400
|
return json.dumps({"error": str(e)}), 400
|
||||||
|
|||||||
@@ -7,9 +7,29 @@ controller = Microdot()
|
|||||||
profiles = Profile()
|
profiles = Profile()
|
||||||
|
|
||||||
@controller.get('')
|
@controller.get('')
|
||||||
async def list_profiles(request):
|
@with_session
|
||||||
"""List all profiles."""
|
async def list_profiles(request, session):
|
||||||
return json.dumps(profiles), 200, {'Content-Type': 'application/json'}
|
"""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')
|
@controller.get('/current')
|
||||||
@with_session
|
@with_session
|
||||||
@@ -27,8 +47,13 @@ async def get_current_profile(request, session):
|
|||||||
return json.dumps({"error": "No profile available"}), 404
|
return json.dumps({"error": "No profile available"}), 404
|
||||||
|
|
||||||
@controller.get('/<id>')
|
@controller.get('/<id>')
|
||||||
async def get_profile(request, id):
|
@with_session
|
||||||
|
async def get_profile(request, id, session):
|
||||||
"""Get a specific profile by ID."""
|
"""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)
|
profile = profiles.read(id)
|
||||||
if profile:
|
if profile:
|
||||||
return json.dumps(profile), 200, {'Content-Type': 'application/json'}
|
return json.dumps(profile), 200, {'Content-Type': 'application/json'}
|
||||||
@@ -53,7 +78,8 @@ async def create_profile(request):
|
|||||||
profile_id = profiles.create(name)
|
profile_id = profiles.create(name)
|
||||||
if data:
|
if data:
|
||||||
profiles.update(profile_id, 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:
|
except Exception as e:
|
||||||
return json.dumps({"error": str(e)}), 400
|
return json.dumps({"error": str(e)}), 400
|
||||||
|
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ def get_profile_tab_order(profile_id):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def get_current_tab_id(request, session=None):
|
def get_current_tab_id(request, session=None):
|
||||||
"""Get the current tab ID from session."""
|
"""Get the current tab ID from cookie."""
|
||||||
if session:
|
# Read from cookie first
|
||||||
current_tab = session.get('current_tab')
|
current_tab = request.cookies.get('current_tab')
|
||||||
if current_tab:
|
if current_tab:
|
||||||
return 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)
|
profile_id = get_current_profile_id(session)
|
||||||
if profile_id:
|
if profile_id:
|
||||||
profile = profiles.read(profile_id)
|
profile = profiles.read(profile_id)
|
||||||
@@ -50,16 +50,8 @@ def get_current_tab_id(request, session=None):
|
|||||||
return tabs_list[0]
|
return tabs_list[0]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@controller.get('')
|
def _render_tabs_list_fragment(request, session):
|
||||||
async def list_tabs(request):
|
"""Helper function to render tabs list HTML fragment."""
|
||||||
"""List all tabs."""
|
|
||||||
return json.dumps(tabs), 200, {'Content-Type': 'application/json'}
|
|
||||||
|
|
||||||
# HTML Fragment endpoints for htmx - must be before /<id> route
|
|
||||||
@controller.get('/list-fragment')
|
|
||||||
@with_session
|
|
||||||
async def tabs_list_fragment(request, session):
|
|
||||||
"""Return HTML fragment for the tabs list."""
|
|
||||||
profile_id = get_current_profile_id(session)
|
profile_id = get_current_profile_id(session)
|
||||||
# #region agent log
|
# #region agent log
|
||||||
try:
|
try:
|
||||||
@@ -69,7 +61,7 @@ async def tabs_list_fragment(request, session):
|
|||||||
"sessionId": "debug-session",
|
"sessionId": "debug-session",
|
||||||
"runId": "tabs-pre-fix",
|
"runId": "tabs-pre-fix",
|
||||||
"hypothesisId": "H1",
|
"hypothesisId": "H1",
|
||||||
"location": "src/controllers/tab.py:tabs_list_fragment",
|
"location": "src/controllers/tab.py:_render_tabs_list_fragment",
|
||||||
"message": "tabs list fragment",
|
"message": "tabs list fragment",
|
||||||
"data": {
|
"data": {
|
||||||
"profile_id": profile_id,
|
"profile_id": profile_id,
|
||||||
@@ -106,32 +98,10 @@ async def tabs_list_fragment(request, session):
|
|||||||
html += '</div>'
|
html += '</div>'
|
||||||
return html, 200, {'Content-Type': 'text/html'}
|
return html, 200, {'Content-Type': 'text/html'}
|
||||||
|
|
||||||
@controller.get('/create-form-fragment')
|
def _render_tab_content_fragment(request, session, id):
|
||||||
async def create_tab_form_fragment(request):
|
"""Helper function to render tab content HTML fragment."""
|
||||||
"""Return the create tab form HTML fragment."""
|
# Handle 'current' as a special case
|
||||||
html = '''
|
if id == 'current':
|
||||||
<h2>Add New Tab</h2>
|
|
||||||
<form hx-post="/tabs"
|
|
||||||
hx-target="#tabs-list"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
hx-headers='{"Accept": "text/html"}'
|
|
||||||
hx-on::after-request="if(event.detail.successful) { document.getElementById('add-tab-modal').classList.remove('active'); document.body.dispatchEvent(new Event('tabs-updated')); }">
|
|
||||||
<label>Tab Name:</label>
|
|
||||||
<input type="text" name="name" placeholder="Enter tab name" required>
|
|
||||||
<label>Device IDs (comma-separated):</label>
|
|
||||||
<input type="text" name="ids" placeholder="1,2,3" value="1">
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button type="submit" class="btn btn-primary">Add</button>
|
|
||||||
<button type="button" class="btn btn-secondary" onclick="document.getElementById('add-tab-modal').classList.remove('active')">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
'''
|
|
||||||
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)
|
current_tab_id = get_current_tab_id(request, session)
|
||||||
if not current_tab_id:
|
if not current_tab_id:
|
||||||
accept_header = request.headers.get('Accept', '')
|
accept_header = request.headers.get('Accept', '')
|
||||||
@@ -139,16 +109,7 @@ async def get_current_tab(request, session):
|
|||||||
if wants_html:
|
if wants_html:
|
||||||
return '<div class="error">No current tab set</div>', 404, {'Content-Type': 'text/html'}
|
return '<div class="error">No current tab set</div>', 404, {'Content-Type': 'text/html'}
|
||||||
return json.dumps({"error": "No current tab set"}), 404
|
return json.dumps({"error": "No current tab set"}), 404
|
||||||
|
id = current_tab_id
|
||||||
return await tab_content_fragment.__wrapped__(request, session, current_tab_id)
|
|
||||||
|
|
||||||
@controller.get('/<id>/content-fragment')
|
|
||||||
@with_session
|
|
||||||
async def tab_content_fragment(request, session, id):
|
|
||||||
"""Return HTML fragment for tab content."""
|
|
||||||
# Handle 'current' as a special case
|
|
||||||
if id == 'current':
|
|
||||||
return await get_current_tab(request, session)
|
|
||||||
|
|
||||||
tab = tabs.read(id)
|
tab = tabs.read(id)
|
||||||
if not tab:
|
if not tab:
|
||||||
@@ -177,6 +138,62 @@ async def tab_content_fragment(request, session, id):
|
|||||||
)
|
)
|
||||||
return html, 200, {'Content-Type': 'text/html'}
|
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('/<id>/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('/<id>')
|
@controller.get('/<id>')
|
||||||
async def get_tab(request, id):
|
async def get_tab(request, id):
|
||||||
"""Get a specific tab by ID."""
|
"""Get a specific tab by ID."""
|
||||||
@@ -198,20 +215,15 @@ async def update_tab(request, id):
|
|||||||
|
|
||||||
@controller.delete('/<id>')
|
@controller.delete('/<id>')
|
||||||
@with_session
|
@with_session
|
||||||
async def delete_tab(request, id, session):
|
async def delete_tab(request, session, id):
|
||||||
"""Delete a tab."""
|
"""Delete a tab."""
|
||||||
# Check if this is an htmx request (wants HTML fragment)
|
try:
|
||||||
accept_header = request.headers.get('Accept', '')
|
|
||||||
wants_html = 'text/html' in accept_header
|
|
||||||
|
|
||||||
# Handle 'current' tab ID
|
# Handle 'current' tab ID
|
||||||
if id == 'current':
|
if id == 'current':
|
||||||
current_tab_id = get_current_tab_id(request, session)
|
current_tab_id = get_current_tab_id(request, session)
|
||||||
if current_tab_id:
|
if current_tab_id:
|
||||||
id = current_tab_id
|
id = current_tab_id
|
||||||
else:
|
else:
|
||||||
if wants_html:
|
|
||||||
return '<div class="error">No current tab to delete</div>', 404, {'Content-Type': 'text/html'}
|
|
||||||
return json.dumps({"error": "No current tab to delete"}), 404
|
return json.dumps({"error": "No current tab to delete"}), 404
|
||||||
|
|
||||||
if tabs.delete(id):
|
if tabs.delete(id):
|
||||||
@@ -230,52 +242,33 @@ async def delete_tab(request, id, session):
|
|||||||
del profile['tab_order']
|
del profile['tab_order']
|
||||||
profiles.update(profile_id, profile)
|
profiles.update(profile_id, profile)
|
||||||
|
|
||||||
# Clear session if the deleted tab was the current tab
|
# Clear cookie if the deleted tab was the current tab
|
||||||
current_tab_id = get_current_tab_id(request, session)
|
current_tab_id = get_current_tab_id(request, session)
|
||||||
if current_tab_id == id:
|
if current_tab_id == id:
|
||||||
if 'current_tab' in session:
|
response_data = json.dumps({"message": "Tab deleted successfully"})
|
||||||
session.pop('current_tab', None)
|
response = response_data, 200, {
|
||||||
session.save()
|
'Content-Type': 'application/json',
|
||||||
|
'Set-Cookie': 'current_tab=; Path=/; Max-Age=0' # Clear cookie
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
|
||||||
if wants_html:
|
|
||||||
return await tabs_list_fragment.__wrapped__(request, session)
|
|
||||||
else:
|
|
||||||
return json.dumps({"message": "Tab deleted successfully"}), 200, {'Content-Type': 'application/json'}
|
return json.dumps({"message": "Tab deleted successfully"}), 200, {'Content-Type': 'application/json'}
|
||||||
|
|
||||||
if wants_html:
|
|
||||||
return '<div class="error">Tab not found</div>', 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('')
|
@controller.post('')
|
||||||
@with_session
|
@with_session
|
||||||
async def create_tab(request, session):
|
async def create_tab(request, session):
|
||||||
"""Create a new tab."""
|
"""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:
|
try:
|
||||||
os.makedirs('/home/pi/led-controller/.cursor', exist_ok=True)
|
# Handle form data or JSON
|
||||||
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
|
|
||||||
if request.form:
|
if request.form:
|
||||||
name = request.form.get('name', '').strip()
|
name = request.form.get('name', '').strip()
|
||||||
ids_str = request.form.get('ids', '1').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)
|
preset_ids = data.get("presets", None)
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
if wants_html:
|
|
||||||
return '<div class="error">Tab name cannot be empty</div>', 400, {'Content-Type': 'text/html'}
|
|
||||||
return json.dumps({"error": "Tab name cannot be empty"}), 400
|
return json.dumps({"error": "Tab name cannot be empty"}), 400
|
||||||
|
|
||||||
tab_id = tabs.create(name, names, preset_ids)
|
tab_id = tabs.create(name, names, preset_ids)
|
||||||
@@ -308,36 +299,11 @@ async def create_tab(request, session):
|
|||||||
if 'tab_order' in profile:
|
if 'tab_order' in profile:
|
||||||
del profile['tab_order']
|
del profile['tab_order']
|
||||||
profiles.update(profile_id, profile)
|
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 JSON response with tab ID
|
||||||
# Return HTML fragment for tabs list
|
tab_data = tabs.read(tab_id)
|
||||||
return await tabs_list_fragment.__wrapped__(request, session)
|
return json.dumps({tab_id: tab_data}), 201, {'Content-Type': 'application/json'}
|
||||||
else:
|
|
||||||
# Return JSON response
|
|
||||||
return json.dumps(tabs.read(tab_id)), 201, {'Content-Type': 'application/json'}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import sys
|
import sys
|
||||||
sys.print_exception(e)
|
sys.print_exception(e)
|
||||||
if wants_html:
|
|
||||||
return f'<div class="error">Error: {str(e)}</div>', 400, {'Content-Type': 'text/html'}
|
|
||||||
return json.dumps({"error": str(e)}), 400
|
return json.dumps({"error": str(e)}), 400
|
||||||
|
|||||||
Reference in New Issue
Block a user