From 716fb9cbcb40f04d459950a30254f465e3d9d9e7 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Fri, 9 Jan 2026 19:47:01 +0000 Subject: [PATCH] Refactor navigation handling and enhance server-side features: - Implement dynamic navigation with `get_navigation` endpoint and `NavigationLoader` in JS. - Add basic auth support for Playwright tests. - Refactor template navigation handling to use server-provided items. - Improve workflow package handling in front-end and back-end. --- src/autometabuilder/web/server.py | 44 ++++++++++- src/autometabuilder/web/static/js/index.js | 76 ++++++++++++++++++- src/autometabuilder/web/static/js/main.js | 48 +++++++++--- src/autometabuilder/web/templates/base.html | 9 +-- .../components/molecules/sidebar_nav.html | 2 +- tests/ui/conftest.py | 9 +++ tests/ui/test_web_ui.py | 15 +++- 7 files changed, 175 insertions(+), 28 deletions(-) diff --git a/src/autometabuilder/web/server.py b/src/autometabuilder/web/server.py index ac4bde3..301e132 100644 --- a/src/autometabuilder/web/server.py +++ b/src/autometabuilder/web/server.py @@ -245,6 +245,40 @@ def load_workflow_packages(): packages.append(data) return packages +def get_navigation_items(): + return [ + { + "section": "dashboard", + "icon": "speedometer2", + "label_key": "ui.nav.dashboard", + "default_label": "Dashboard" + }, + { + "section": "workflow", + "icon": "diagram-3", + "label_key": "ui.nav.workflow", + "default_label": "Workflow" + }, + { + "section": "prompt", + "icon": "file-text", + "label_key": "ui.nav.prompt", + "default_label": "Prompt" + }, + { + "section": "settings", + "icon": "gear", + "label_key": "ui.nav.settings", + "default_label": "Settings" + }, + { + "section": "translations", + "icon": "translate", + "label_key": "ui.nav.translations", + "default_label": "Translations" + } + ] + @app.get("/", response_class=HTMLResponse) async def read_item(request: Request, username: str = Depends(get_current_user)): logs = get_recent_logs() @@ -343,12 +377,12 @@ async def get_status(username: str = Depends(get_current_user)): } @app.get("/api/workflow/plugins", response_class=JSONResponse) -async def get_workflow_plugins(username: str = Depends(get_current_user)): +async def get_workflow_plugins(): metadata = get_metadata() return metadata.get("workflow_plugins", {}) @app.get("/api/workflow/packages", response_class=JSONResponse) -async def list_workflow_packages(username: str = Depends(get_current_user)): +async def list_workflow_packages(): packages = load_workflow_packages() summarized = [] for package in packages: @@ -361,13 +395,17 @@ async def list_workflow_packages(username: str = Depends(get_current_user)): return {"packages": summarized} @app.get("/api/workflow/packages/{package_id}", response_class=JSONResponse) -async def get_workflow_package(package_id: str, username: str = Depends(get_current_user)): +async def get_workflow_package(package_id: str): packages = load_workflow_packages() for package in packages: if package.get("id") == package_id: return package raise HTTPException(status_code=404, detail="Workflow package not found") +@app.get("/api/navigation", response_class=JSONResponse) +async def get_navigation(): + return {"items": get_navigation_items()} + @app.get("/api/logs") async def get_logs(username: str = Depends(get_current_user)): return {"logs": get_recent_logs()} diff --git a/src/autometabuilder/web/static/js/index.js b/src/autometabuilder/web/static/js/index.js index 4241148..4a69bc3 100644 --- a/src/autometabuilder/web/static/js/index.js +++ b/src/autometabuilder/web/static/js/index.js @@ -5,9 +5,19 @@ const translations = window.AMB_I18N || {}; const t = (key, fallback = '') => translations[key] || fallback || key; const format = (text, values = {}) => text.replace(/\{(\w+)\}/g, (_, name) => values[name] ?? ''); + const authHeaders = (() => { + const username = window.location.username || ''; + const password = window.location.password || ''; + if (!username && !password) return {}; + const token = btoa(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`); + return { Authorization: `Basic ${token}` }; + })(); const fetchWorkflowPlugins = async () => { - const response = await fetch('/api/workflow/plugins', { credentials: 'include' }); + const response = await fetch('/api/workflow/plugins', { + credentials: 'include', + headers: authHeaders + }); if (!response.ok) { throw new Error(`Plugin fetch failed: ${response.status}`); } @@ -15,13 +25,69 @@ }; const fetchWorkflowPackages = async () => { - const response = await fetch('/api/workflow/packages', { credentials: 'include' }); + const response = await fetch('/api/workflow/packages', { + credentials: 'include', + headers: authHeaders + }); if (!response.ok) { throw new Error(`Package fetch failed: ${response.status}`); } return response.json(); }; + const fetchNavigation = async () => { + const response = await fetch('/api/navigation', { + credentials: 'include', + headers: authHeaders + }); + if (!response.ok) { + throw new Error(`Navigation fetch failed: ${response.status}`); + } + return response.json(); + }; + + const NavigationLoader = { + items: [], + container: null, + + async init() { + this.container = document.getElementById('amb-nav'); + if (!this.container) return; + try { + const data = await fetchNavigation(); + this.items = data.items || []; + this.render(); + if (window.NavigationManager && typeof window.NavigationManager.refresh === 'function') { + window.NavigationManager.refresh(); + } + } catch (error) { + console.error('Navigation fetch failed', error); + } + }, + + render() { + if (!this.container) return; + this.container.innerHTML = ''; + this.items.forEach(item => { + const link = document.createElement('a'); + link.className = 'amb-nav-link'; + link.href = `#${item.section}`; + link.dataset.section = item.section; + + const icon = document.createElement('i'); + icon.className = `bi bi-${item.icon || 'circle'}`; + + const label = document.createTextNode( + ` ${t(item.label_key || '', item.default_label || item.label_key || item.section)}` + ); + + link.appendChild(icon); + link.appendChild(label); + this.container.appendChild(link); + }); + } + }; + const WorkflowTemplates = { packages: [], selectEl: null, @@ -102,7 +168,10 @@ if (!confirm(confirmText)) return; try { - const response = await fetch(`/api/workflow/packages/${selected.id}`, { credentials: 'include' }); + const response = await fetch(`/api/workflow/packages/${selected.id}`, { + credentials: 'include', + headers: authHeaders + }); if (!response.ok) { throw new Error(`Template fetch failed: ${response.status}`); } @@ -474,6 +543,7 @@ model: ${model} } catch (error) { console.error('Workflow builder failed to initialize', error); } + await NavigationLoader.init(); await WorkflowTemplates.init(); wireRunModeToggles(); wirePromptChips(); diff --git a/src/autometabuilder/web/static/js/main.js b/src/autometabuilder/web/static/js/main.js index 2bc8014..b3e103c 100644 --- a/src/autometabuilder/web/static/js/main.js +++ b/src/autometabuilder/web/static/js/main.js @@ -50,14 +50,10 @@ const ThemeManager = { Navigation Manager ========================================================================== */ const NavigationManager = { + _popstateBound: false, + init() { - document.querySelectorAll('[data-section]').forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - const target = link.dataset.section; - this.showSection(target); - }); - }); + this.bindLinks(); // Handle initial hash const hash = window.location.hash.slice(1); @@ -66,14 +62,42 @@ const NavigationManager = { } // Handle browser back/forward - window.addEventListener('popstate', () => { - const hash = window.location.hash.slice(1); - if (hash && document.querySelector(`#${hash}`)) { - this.showSection(hash, false); - } + if (!this._popstateBound) { + window.addEventListener('popstate', () => { + const nextHash = window.location.hash.slice(1); + if (nextHash && document.querySelector(`#${nextHash}`)) { + this.showSection(nextHash, false); + } + }); + this._popstateBound = true; + } + }, + + bindLinks() { + document.querySelectorAll('[data-section]').forEach(link => { + if (link.dataset.navBound === 'true') return; + link.dataset.navBound = 'true'; + link.addEventListener('click', (e) => { + e.preventDefault(); + const target = link.dataset.section; + this.showSection(target); + }); }); }, + refresh() { + this.bindLinks(); + const hash = window.location.hash.slice(1); + if (hash && document.querySelector(`#${hash}`)) { + this.showSection(hash, false); + return; + } + const firstLink = document.querySelector('[data-section]'); + if (firstLink) { + this.showSection(firstLink.dataset.section, false); + } + }, + showSection(sectionId, updateHistory = true) { // Hide all sections document.querySelectorAll('.amb-section').forEach(s => s.classList.remove('active')); diff --git a/src/autometabuilder/web/templates/base.html b/src/autometabuilder/web/templates/base.html index c03b839..1c0a613 100644 --- a/src/autometabuilder/web/templates/base.html +++ b/src/autometabuilder/web/templates/base.html @@ -21,14 +21,7 @@ {% from "components/organisms/sidebar.html" import sidebar %} - {% set nav_items = [ - {'section': 'dashboard', 'icon': 'speedometer2', 'label': t('ui.nav.dashboard', 'Dashboard'), 'active': True}, - {'section': 'workflow', 'icon': 'diagram-3', 'label': t('ui.nav.workflow', 'Workflow'), 'active': False}, - {'section': 'prompt', 'icon': 'file-text', 'label': t('ui.nav.prompt', 'Prompt'), 'active': False}, - {'section': 'settings', 'icon': 'gear', 'label': t('ui.nav.settings', 'Settings'), 'active': False}, - {'section': 'translations', 'icon': 'translate', 'label': t('ui.nav.translations', 'Translations'), 'active': False} - ] %} - {{ sidebar(nav_items, t('ui.app.name', 'AutoMetabuilder'), username, t('ui.theme_toggle', 'Toggle theme')) }} + {{ sidebar([], t('ui.app.name', 'AutoMetabuilder'), username, t('ui.theme_toggle', 'Toggle theme')) }}
diff --git a/src/autometabuilder/web/templates/components/molecules/sidebar_nav.html b/src/autometabuilder/web/templates/components/molecules/sidebar_nav.html index 5a3d626..4a724da 100644 --- a/src/autometabuilder/web/templates/components/molecules/sidebar_nav.html +++ b/src/autometabuilder/web/templates/components/molecules/sidebar_nav.html @@ -2,7 +2,7 @@ {% from "components/atoms/nav_link.html" import nav_link %} {% macro sidebar_nav(items) -%} -