From 1bf9510c7d11f142225543b488441b553ee8fcdb Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 4.5" Date: Sun, 8 Feb 2026 12:21:11 +0000 Subject: [PATCH] WIP: Various UI updates from previous session - Update UI server module - Update suite index and JavaScript files - Add public directory Co-Authored-By: Claude Sonnet 4.5 --- src/ui_server/mod.rs | 75 +++++++++++++++---- ui/public/themes/dark.css | 73 +++++++++++++++++++ ui/public/themes/light.css | 73 +++++++++++++++++++ ui/public/themes/y2kglow.css | 117 ++++++++++++++++++++++++++++++ ui/suite/index.html | 4 +- ui/suite/js/error-reporter.js | 97 +++++++++++++++++++------ ui/suite/js/security-bootstrap.js | 6 +- ui/suite/js/suite_app.js | 5 +- 8 files changed, 410 insertions(+), 40 deletions(-) create mode 100644 ui/public/themes/dark.css create mode 100644 ui/public/themes/light.css create mode 100644 ui/public/themes/y2kglow.css diff --git a/src/ui_server/mod.rs b/src/ui_server/mod.rs index 1ff2217..80cd69e 100644 --- a/src/ui_server/mod.rs +++ b/src/ui_server/mod.rs @@ -163,15 +163,19 @@ pub async fn index(OriginalUri(uri): OriginalUri) -> Response { let fs_path = if path_parts.len() > 1 { let mut start_idx = 1; let known_dirs = ["suite", "js", "css", "vendor", "assets", "public", "partials", "settings", "auth", "about", "drive", "chat", "tasks", "admin", "mail", "calendar", "meet", "docs", "sheet", "slides", "paper", "research", "sources", "learn", "analytics", "dashboards", "monitoring", "people", "crm", "tickets", "billing", "products", "video", "player", "canvas", "social", "project", "goals", "workspace", "designer"]; - + + // Special case: /auth/suite/* should map to suite/* (auth is a route, not a directory) + if path_parts.get(1) == Some(&"auth") && path_parts.get(2) == Some(&"suite") { + start_idx = 2; + } // Skip bot name if present (first segment is not a known dir, second segment is) - if path_parts.len() > start_idx + 1 - && !known_dirs.contains(&path_parts[start_idx]) + else if path_parts.len() > start_idx + 1 + && !known_dirs.contains(&path_parts[start_idx]) && known_dirs.contains(&path_parts[start_idx + 1]) { start_idx += 1; } - + path_parts[start_idx..].join("/") } else { path.to_string() @@ -338,25 +342,39 @@ pub async fn serve_suite(bot_name: Option) -> impl IntoResponse { // Inject base tag and bot_name into the page if let Some(head_end) = html.find("") { + // Check if bot_name is actually an auth page (login.html, register.html, etc.) + // These are not actual bots, so we should use "/" as base href + let is_auth_page = bot_name.as_ref() + .map(|n| n.ends_with(".html") || n == "login" || n == "register" || n == "forgot-password" || n == "reset-password") + .unwrap_or(false); + // Set base href to include bot context if present (e.g., /edu/) - let base_href = if let Some(ref name) = bot_name { + // But NOT for auth pages - those use root + let base_href = if is_auth_page { + "/".to_string() + } else if let Some(ref name) = bot_name { format!("/{}/", name) } else { "/".to_string() }; let base_tag = format!(r#""#, base_href); html.insert_str(head_end, &base_tag); - - if let Some(name) = bot_name { - info!("serve_suite: Injecting bot_name '{}' into page with base href='{}'", name, base_href); - let bot_script = format!( - r#""#, - &name - ); - html.insert_str(head_end + base_tag.len(), &bot_script); - info!("serve_suite: Successfully injected base tag and bot_name script"); + + // Only inject bot_name script for actual bots, not auth pages + if !is_auth_page { + if let Some(name) = bot_name { + info!("serve_suite: Injecting bot_name '{}' into page with base href='{}'", name, base_href); + let bot_script = format!( + r#""#, + &name + ); + html.insert_str(head_end + base_tag.len(), &bot_script); + info!("serve_suite: Successfully injected base tag and bot_name script"); + } else { + info!("serve_suite: Successfully injected base tag (no bot_name)"); + } } else { - info!("serve_suite: Successfully injected base tag (no bot_name)"); + info!("serve_suite: Auth page detected, skipping bot_name injection (base href='{}')", base_href); } } else { error!("serve_suite: Failed to find tag to inject content"); @@ -1150,6 +1168,23 @@ async fn handle_embedded_root_asset( } } +#[cfg(feature = "embed-ui")] +async fn handle_auth_asset(axum::extract::Path(path): axum::extract::Path) -> impl IntoResponse { + let normalized_path = path.strip_prefix('/').unwrap_or(&path); + let asset_path = format!("suite/auth/{}", normalized_path); + match Assets::get(&asset_path) { + Some(content) => { + let mime = mime_guess::from_path(&asset_path).first_or_octet_stream(); + ( + [(axum::http::header::CONTENT_TYPE, mime.as_ref())], + content.data, + ) + .into_response() + } + None => StatusCode::NOT_FOUND.into_response(), + } +} + fn add_static_routes(router: Router, _suite_path: &Path) -> Router { #[cfg(feature = "embed-ui")] { @@ -1195,6 +1230,16 @@ pub fn configure_router() -> Router { .route("/minimal", get(serve_minimal)) .route("/suite", get(serve_suite)); + #[cfg(not(feature = "embed-ui"))] + { + router = router.nest_service("/auth", ServeDir::new(suite_path.join("auth"))); + } + + #[cfg(feature = "embed-ui")] + { + router = router.route("/auth/*path", get(handle_auth_asset)); + } + router = add_static_routes(router, &suite_path); router.fallback(get(index)).with_state(state) diff --git a/ui/public/themes/dark.css b/ui/public/themes/dark.css new file mode 100644 index 0000000..f2f2efa --- /dev/null +++ b/ui/public/themes/dark.css @@ -0,0 +1,73 @@ +/* Dark Theme for General Bots */ +:root { + --color-primary: #d4f505; + --color-secondary: #00d4aa; + --color-accent: #818cf8; + + --color-bg: #0f172a; + --color-bg-secondary: #1e293b; + --color-bg-tertiary: #334155; + + --color-text: #f1f5f9; + --color-text-secondary: #cbd5e1; + --color-text-muted: #64748b; + + --color-border: #334155; + --color-border-light: #1e293b; + + --color-success: #22c55e; + --color-warning: #f59e0b; + --color-error: #ef4444; + --color-info: #3b82f6; + + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3); + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.4), 0 1px 2px -1px rgb(0 0 0 / 0.4); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.4); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5), 0 4px 6px -4px rgb(0 0 0 / 0.5); + + --radius-sm: 0.25rem; + --radius: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; +} + +body { + background-color: var(--color-bg); + color: var(--color-text); +} + +a { + color: var(--color-accent); +} + +a:hover { + color: var(--color-primary); +} + +.btn-primary { + background-color: var(--color-primary); + color: var(--color-bg); +} + +.btn-primary:hover { + background-color: var(--color-secondary); +} + +.card { + background-color: var(--color-bg-secondary); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-sm); +} + +input, textarea, select { + background-color: var(--color-bg-secondary); + border: 1px solid var(--color-border); + color: var(--color-text); +} + +input:focus, textarea:focus, select:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(212, 245, 5, 0.1); +} diff --git a/ui/public/themes/light.css b/ui/public/themes/light.css new file mode 100644 index 0000000..828a0c1 --- /dev/null +++ b/ui/public/themes/light.css @@ -0,0 +1,73 @@ +/* Light Theme for General Bots */ +:root { + --color-primary: #d4f505; + --color-secondary: #00d4aa; + --color-accent: #6366f1; + + --color-bg: #ffffff; + --color-bg-secondary: #f8fafc; + --color-bg-tertiary: #f1f5f9; + + --color-text: #0f172a; + --color-text-secondary: #475569; + --color-text-muted: #94a3b8; + + --color-border: #e2e8f0; + --color-border-light: #f1f5f9; + + --color-success: #22c55e; + --color-warning: #f59e0b; + --color-error: #ef4444; + --color-info: #3b82f6; + + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + + --radius-sm: 0.25rem; + --radius: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; +} + +body { + background-color: var(--color-bg); + color: var(--color-text); +} + +a { + color: var(--color-accent); +} + +a:hover { + color: var(--color-primary); +} + +.btn-primary { + background-color: var(--color-primary); + color: var(--color-text); +} + +.btn-primary:hover { + background-color: var(--color-secondary); +} + +.card { + background-color: var(--color-bg); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-sm); +} + +input, textarea, select { + background-color: var(--color-bg); + border: 1px solid var(--color-border); + color: var(--color-text); +} + +input:focus, textarea:focus, select:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(212, 245, 5, 0.1); +} diff --git a/ui/public/themes/y2kglow.css b/ui/public/themes/y2kglow.css new file mode 100644 index 0000000..8f8ee92 --- /dev/null +++ b/ui/public/themes/y2kglow.css @@ -0,0 +1,117 @@ +/* Y2K Glow Theme for General Bots */ +:root { + --color-primary: #ff00ff; + --color-secondary: #00ffff; + --color-accent: #ffff00; + + --color-bg: #0a0a1a; + --color-bg-secondary: #1a0a2e; + --color-bg-tertiary: #2d1b4e; + + --color-text: #00ff00; + --color-text-secondary: #ff00ff; + --color-text-muted: #00ffff; + + --color-border: #ff00ff; + --color-border-light: #00ffff; + + --color-success: #00ff00; + --color-warning: #ffff00; + --color-error: #ff0066; + --color-info: #00ffff; + + --shadow-glow: 0 0 10px #ff00ff, 0 0 20px #ff00ff, 0 0 30px #ff00ff; + --shadow-sm: 0 0 5px rgba(255, 0, 255, 0.5); + --shadow: 0 0 10px rgba(255, 0, 255, 0.7); + --shadow-md: 0 0 15px rgba(255, 0, 255, 0.8); + --shadow-lg: 0 0 25px rgba(255, 0, 255, 0.9); + + --radius-sm: 0.25rem; + --radius: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; +} + +body { + background-color: var(--color-bg); + color: var(--color-text); + text-shadow: 0 0 5px var(--color-text); +} + +a { + color: var(--color-secondary); + text-shadow: 0 0 5px var(--color-secondary); +} + +a:hover { + color: var(--color-primary); + text-shadow: 0 0 10px var(--color-primary), 0 0 20px var(--color-primary); +} + +.btn-primary { + background: linear-gradient(45deg, var(--color-primary), var(--color-secondary)); + color: var(--color-bg); + border: 2px solid var(--color-primary); + box-shadow: var(--shadow-glow); + text-shadow: none; +} + +.btn-primary:hover { + background: linear-gradient(45deg, var(--color-secondary), var(--color-accent)); + border-color: var(--color-secondary); + box-shadow: 0 0 15px var(--color-secondary), 0 0 30px var(--color-secondary); +} + +.card { + background: linear-gradient(135deg, var(--color-bg-secondary), var(--color-bg-tertiary)); + border: 2px solid var(--color-primary); + box-shadow: var(--shadow); + animation: glow 2s ease-in-out infinite alternate; +} + +@keyframes glow { + from { + box-shadow: 0 0 5px var(--color-primary), 0 0 10px var(--color-primary); + } + to { + box-shadow: 0 0 10px var(--color-secondary), 0 0 20px var(--color-secondary); + } +} + +input, textarea, select { + background: var(--color-bg-secondary); + border: 2px solid var(--color-border); + color: var(--color-text); + box-shadow: 0 0 5px var(--color-border); +} + +input:focus, textarea:focus, select:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 10px var(--color-accent), 0 0 20px var(--color-accent), 0 0 30px var(--color-accent); +} + +input::placeholder, textarea::placeholder { + color: var(--color-text-muted); + text-shadow: 0 0 3px var(--color-text-muted); +} + +::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +::-webkit-scrollbar-track { + background: var(--color-bg); +} + +::-webkit-scrollbar-thumb { + background: linear-gradient(var(--color-primary), var(--color-secondary)); + border-radius: 6px; + box-shadow: 0 0 10px var(--color-primary); +} + +::-webkit-scrollbar-thumb:hover { + background: linear-gradient(var(--color-secondary), var(--color-accent)); +} diff --git a/ui/suite/index.html b/ui/suite/index.html index 9c20ff3..ed63efd 100644 --- a/ui/suite/index.html +++ b/ui/suite/index.html @@ -50,10 +50,10 @@ - + - + diff --git a/ui/suite/js/error-reporter.js b/ui/suite/js/error-reporter.js index f47db35..1938b42 100644 --- a/ui/suite/js/error-reporter.js +++ b/ui/suite/js/error-reporter.js @@ -36,13 +36,15 @@ if (!response.ok) { console.warn('[ErrorReporter] Failed to send errors:', response.status); + } else { + console.log('[ErrorReporter] Sent', errorsToReport.length, 'errors to server'); } } catch (e) { console.warn('[ErrorReporter] Failed to send errors:', e.message); errorQueue.unshift(...errorsToReport); } finally { isReporting = false; - + if (errorQueue.length > 0) { setTimeout(reportErrors, 1000); } @@ -76,6 +78,15 @@ report: function(error, context) { queueError(formatError(error, context)); }, + reportNetworkError: function(url, status, statusText) { + queueError({ + type: 'NetworkError', + message: `Failed to load ${url}: ${status} ${statusText}`, + url: window.location.href, + timestamp: new Date().toISOString(), + context: { url, status, statusText } + }); + }, flush: function() { reportErrors(); } @@ -101,7 +112,7 @@ url: window.location.href, timestamp: new Date().toISOString() }; - + queueError({ name: 'Navigation', message: `${method}: ${from} -> ${to}`, @@ -110,26 +121,70 @@ } }; - document.body.addEventListener('click', function(e) { - const target = e.target.closest('[data-section]'); - if (target) { - const section = target.getAttribute('data-section'); - const currentHash = window.location.hash.slice(1) || ''; - if (section !== currentHash) { - setTimeout(() => { - window.NavigationLogger.log(currentHash || 'home', section, 'click'); - }, 100); - } + function initNavigationTracking() { + if (!document.body) { + setTimeout(initNavigationTracking, 50); + return; } - }, true); - window.addEventListener('hashchange', function(e) { - const oldURL = new URL(e.oldURL); - const newURL = new URL(e.newURL); - const fromHash = oldURL.hash.slice(1) || ''; - const toHash = newURL.hash.slice(1) || ''; - window.NavigationLogger.log(fromHash || 'home', toHash, 'hashchange'); + if (document.body) { + document.body.addEventListener('click', function(e) { + const target = e.target.closest('[data-section]'); + if (target) { + const section = target.getAttribute('data-section'); + const currentHash = window.location.hash.slice(1) || ''; + if (section !== currentHash) { + setTimeout(() => { + window.NavigationLogger.log(currentHash || 'home', section, 'click'); + }, 100); + } + } + }, true); + } + + window.addEventListener('hashchange', function(e) { + const oldURL = new URL(e.oldURL); + const newURL = new URL(e.newURL); + const fromHash = oldURL.hash.slice(1) || ''; + const toHash = newURL.hash.slice(1) || ''; + window.NavigationLogger.log(fromHash || 'home', toHash, 'hashchange'); + }); + + console.log('[NavigationLogger] Navigation tracking initialized'); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initNavigationTracking); + } else { + initNavigationTracking(); + } + + // Intercept link onload/onerror events to catch CSS/image load failures + const originalCreateElement = document.createElement; + document.createElement = function(tagName) { + const element = originalCreateElement.call(document, tagName); + if (tagName.toLowerCase() === 'link') { + element.addEventListener('error', function() { + if (this.href && window.ErrorReporter && window.ErrorReporter.reportNetworkError) { + window.ErrorReporter.reportNetworkError(this.href, 'LOAD_FAILED', 'Resource failed to load'); + } + }); + } + return element; + }; + + window.addEventListener('load', () => { + setTimeout(() => { + const failedResources = performance.getEntriesByType('resource').filter(entry => + entry.transferSize === 0 && entry.decodedBodySize > 0 && !entry.name.includes('anon') && entry.duration > 100 + ); + + if (failedResources.length > 0) { + console.warn('[ErrorReporter] Detected potentially failed resources:', failedResources); + failedResources.forEach(resource => { + window.ErrorReporter.reportNetworkError(resource.name, 'FAILED', 'Resource load timeout/failure'); + }); + } + }, 5000); }); - - console.log('[NavigationLogger] Navigation tracking initialized'); })(); diff --git a/ui/suite/js/security-bootstrap.js b/ui/suite/js/security-bootstrap.js index 2bb253e..fe41bde 100644 --- a/ui/suite/js/security-bootstrap.js +++ b/ui/suite/js/security-bootstrap.js @@ -210,10 +210,14 @@ return originalFetch .call(window, input, init) .then(function (response) { + var url = typeof input === "string" ? input : input.url; + if (response.status === 401) { - var url = typeof input === "string" ? input : input.url; self.handleUnauthorized(url); + } else if (!response.ok && window.ErrorReporter && window.ErrorReporter.reportNetworkError) { + window.ErrorReporter.reportNetworkError(url, response.status, response.statusText); } + return response; }); }; diff --git a/ui/suite/js/suite_app.js b/ui/suite/js/suite_app.js index b7a0ee6..2f52c50 100644 --- a/ui/suite/js/suite_app.js +++ b/ui/suite/js/suite_app.js @@ -1024,7 +1024,10 @@ document.addEventListener("DOMContentLoaded", () => { } } - if (document.readyState === "complete") { + // Skip SPA initialization on auth pages (login, register, etc.) + if (window.location.pathname.startsWith("/auth/")) { + console.log("[SPA] Skipping initialization on auth page"); + } else if (document.readyState === "complete") { setTimeout(initialLoad, 50); } else { window.addEventListener("load", () => {