This keeps data isolated per profile while letting users duplicate setups quickly. Co-authored-by: Cursor <cursoragent@cursor.com>
347 lines
13 KiB
Python
347 lines
13 KiB
Python
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 '<div class="tabs-list">No profile selected</div>', 200, {'Content-Type': 'text/html'}
|
|
|
|
tab_order = get_profile_tab_order(profile_id)
|
|
current_tab_id = get_current_tab_id(request, session)
|
|
|
|
html = '<div class="tabs-list">'
|
|
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 += (
|
|
'<button class="tab-button ' + active_class + '" '
|
|
'hx-get="/tabs/' + str(tab_id) + '/content-fragment" '
|
|
'hx-target="#tab-content" '
|
|
'hx-swap="innerHTML" '
|
|
'hx-push-url="true" '
|
|
'hx-trigger="click" '
|
|
'onclick="document.querySelectorAll(\'.tab-button\').forEach(b => b.classList.remove(\'active\')); this.classList.add(\'active\');">'
|
|
+ tab_name +
|
|
'</button>'
|
|
)
|
|
html += '</div>'
|
|
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 '<div class="error">No current tab set</div>', 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 '<div>Tab not found</div>', 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 = (
|
|
'<div class="presets-section" data-tab-id="' + str(id) + '">'
|
|
'<h3>Presets</h3>'
|
|
'<div class="profiles-actions" style="margin-bottom: 1rem;"></div>'
|
|
'<div id="presets-list-tab" class="presets-list">'
|
|
'<!-- Presets will be loaded here -->'
|
|
'</div>'
|
|
'</div>'
|
|
)
|
|
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>')
|
|
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('/<id>')
|
|
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('/<id>')
|
|
@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('/<id>/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
|