diff --git a/backend/main.py b/backend/main.py index e6fcc12..93ebdd3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -49,22 +49,10 @@ async def websocket_update_callback(websocket: WebSocket, message: dict): mtype = message.get("type") if mtype == "intermediate_update": utype = message.get("update_type") or message.get("data", {}).get("update_type") - if utype == "canvas_files": - files = (message.get("data") or {}).get("files") or [] - # logger.info( - # "WS SEND: intermediate_update canvas_files count=%d files=%s display=%s", - # len(files), - # [f.get("filename") for f in files if isinstance(f, dict)], - # (message.get("data") or {}).get("display"), - # ) - elif utype == "files_update": - files = (message.get("data") or {}).get("files") or [] - # logger.info( - # "WS SEND: intermediate_update files_update total=%d", - # len(files), - # ) - # else: - # logger.info("WS SEND: intermediate_update update_type=%s", utype) + # Handle specific update types (canvas_files, files_update) + # Logging disabled for these message types - see git history if needed + if utype in ("canvas_files", "files_update"): + pass elif mtype == "canvas_content": content = message.get("content") clen = len(content) if isinstance(content, str) else "obj" @@ -142,7 +130,8 @@ async def lifespan(app: FastAPI): app.include_router(files_router) # Serve frontend build (Vite) -static_dir = Path(__file__).parent.parent / "frontend" / "dist" +project_root = Path(__file__).resolve().parents[1] +static_dir = project_root / "frontend" / "dist" if static_dir.exists(): # Serve the SPA entry @app.get("/") @@ -154,6 +143,16 @@ async def read_root(): if assets_dir.exists(): app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") + # Serve webfonts from Vite build (placed via frontend/public/fonts) + fonts_dir = static_dir / "fonts" + if fonts_dir.exists(): + app.mount("/fonts", StaticFiles(directory=fonts_dir), name="fonts") + else: + # Fallback to unbuilt public fonts if dist/fonts is missing + public_fonts = project_root / "frontend" / "public" / "fonts" + if public_fonts.exists(): + app.mount("/fonts", StaticFiles(directory=public_fonts), name="fonts") + # Common top-level static files in the Vite build @app.get("/favicon.ico") async def favicon(): diff --git a/backend/routes/admin_routes.py b/backend/routes/admin_routes.py index cd113ab..d5d8dfb 100644 --- a/backend/routes/admin_routes.py +++ b/backend/routes/admin_routes.py @@ -598,140 +598,53 @@ async def download_logs(admin_user: str = Depends(require_admin)): raise HTTPException(status_code=500, detail="Error preparing log download") -# # --- System Status --- +# --- System Status (minimal) --- -# @admin_router.get("/system-status") -# async def get_system_status(admin_user: str = Depends(require_admin)): -# """Get overall system status including MCP servers and LLM health.""" -# try: -# status_info = [] - -# # Check if configfilesadmin exists and has files -# admin_config_dir = Path("configfilesadmin") -# config_status = "healthy" if admin_config_dir.exists() and any(admin_config_dir.iterdir()) else "warning" -# status_info.append(SystemStatus( -# component="Configuration", -# status=config_status, -# details={ -# "admin_config_dir": str(admin_config_dir), -# "files_count": len(list(admin_config_dir.glob("*"))) if admin_config_dir.exists() else 0 -# } -# )) - -# # Check log file -# from otel_config import get_otel_config -# otel_cfg = get_otel_config() -# log_file = otel_cfg.get_log_file_path() if otel_cfg else Path("logs/app.jsonl") -# log_status = "healthy" if log_file.exists() else "warning" -# status_info.append(SystemStatus( -# component="Logging", -# status=log_status, -# details={ -# "log_file": str(log_file), -# "exists": log_file.exists(), -# "size_bytes": log_file.stat().st_size if log_file.exists() else 0 -# } -# )) - -# # Check MCP server health -# mcp_health = get_mcp_health_status() -# mcp_status = mcp_health.get("overall_status", "unknown") -# status_info.append(SystemStatus( -# component="MCP Servers", -# status=mcp_status, -# details={ -# "healthy_count": mcp_health.get("healthy_count", 0), -# "total_count": mcp_health.get("total_count", 0), -# "last_check": mcp_health.get("last_check"), -# "check_interval": mcp_health.get("check_interval", 300) -# } -# )) - -# return { -# "overall_status": "healthy" if all(s.status == "healthy" for s in status_info) else "warning", -# "components": [s.model_dump() for s in status_info], -# "checked_by": admin_user, -# "timestamp": log_file.stat().st_mtime if log_file.exists() else None -# } -# except Exception as e: -# logger.error(f"Error getting system status: {e}") -# raise HTTPException(status_code=500, detail=str(e)) - - -# # --- Health Check Trigger --- - -# @admin_router.get("/mcp-health") -# async def get_mcp_health(admin_user: str = Depends(require_admin)): -# """Get detailed MCP server health information.""" -# try: -# health_summary = get_mcp_health_status() -# return { -# "health_summary": health_summary, -# "checked_by": admin_user -# } -# except Exception as e: -# logger.error(f"Error getting MCP health: {e}") -# raise HTTPException(status_code=500, detail=str(e)) - - -# @admin_router.post("/trigger-health-check") -# async def trigger_health_check(admin_user: str = Depends(require_admin)): -# """Manually trigger MCP server health checks.""" -# try: -# # Try to get the MCP manager from main application state -# mcp_manager = None -# try: -# from main import mcp_manager as main_mcp_manager -# mcp_manager = main_mcp_manager -# except ImportError: -# # In test environment, mcp_manager might not be available -# logger.warning("MCP manager not available for health check") - -# # Trigger health check -# health_results = await trigger_mcp_health_check(mcp_manager) - -# # Get summary -# health_summary = get_mcp_health_status() - -# logger.info(f"Health check triggered by {admin_user}") -# return { -# "message": "MCP server health check completed", -# "triggered_by": admin_user, -# "summary": health_summary, -# "details": health_results -# } -# except Exception as e: -# logger.error(f"Error triggering health check: {e}") -# raise HTTPException(status_code=500, detail=f"Error triggering health check: {str(e)}") +@admin_router.get("/system-status") +async def get_system_status(admin_user: str = Depends(require_admin)): + """Minimal system status endpoint for the Admin UI. + Returns basic configuration and logging status; avoids heavy checks. + """ + try: + # Configuration status: overrides directory and file count + overrides_root = Path(os.getenv("APP_CONFIG_OVERRIDES", "config/overrides")) + overrides_root.mkdir(parents=True, exist_ok=True) + config_files = list(overrides_root.glob("*")) + config_status = "healthy" if config_files else "warning" + + # Logging status + log_dir = _log_base_dir() + log_file = log_dir / "app.jsonl" + log_exists = log_file.exists() + logging_status = "healthy" if log_exists else "warning" + + components = [ + { + "component": "Configuration", + "status": config_status, + "details": { + "overrides_dir": str(overrides_root), + "files_count": len(config_files), + }, + }, + { + "component": "Logging", + "status": logging_status, + "details": { + "log_file": str(log_file), + "exists": log_exists, + "size_bytes": log_file.stat().st_size if log_exists else 0, + }, + }, + ] -# @admin_router.post("/reload-config") -# async def reload_configuration(admin_user: str = Depends(require_admin)): -# """Reload configuration from configfilesadmin files.""" -# try: -# # Reload configuration from files -# config_manager.reload_configs() - -# # Validate the reloaded configurations -# validation_status = config_manager.validate_config() - -# # Get the updated configurations for verification -# llm_models = list(config_manager.llm_config.models.keys()) -# mcp_servers = list(config_manager.mcp_config.servers.keys()) - -# logger.info(f"Configuration reloaded by {admin_user}") -# logger.info(f"Reloaded LLM models: {llm_models}") -# logger.info(f"Reloaded MCP servers: {mcp_servers}") - -# return { -# "message": "Configuration reloaded successfully", -# "reloaded_by": admin_user, -# "validation_status": validation_status, -# "llm_models_count": len(llm_models), -# "mcp_servers_count": len(mcp_servers), -# "llm_models": llm_models, -# "mcp_servers": mcp_servers -# } -# except Exception as e: -# logger.error(f"Error reloading config: {e}") -# raise HTTPException(status_code=500, detail=f"Error reloading configuration: {str(e)}") \ No newline at end of file + overall = "healthy" if all(c["status"] == "healthy" for c in components) else "warning" + return { + "overall_status": overall, + "components": components, + "checked_by": admin_user, + } + except Exception as e: # noqa: BLE001 + logger.error(f"Error getting system status: {e}") + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/backend/tests/test_security_admin_routes.py b/backend/tests/test_security_admin_routes.py index ea3b579..24a5f44 100644 --- a/backend/tests/test_security_admin_routes.py +++ b/backend/tests/test_security_admin_routes.py @@ -17,3 +17,46 @@ def test_admin_routes_require_admin(monkeypatch): assert r2.status_code == 200 data = r2.json() assert data.get("available_endpoints") is not None + + +def test_system_status_endpoint(): + """Test the system status endpoint returns expected data structure.""" + client = TestClient(app) + + # Test with admin user + r = client.get("/admin/system-status", headers={"X-User-Email": "admin@example.com"}) + assert r.status_code == 200 + + data = r.json() + + # Check response structure + assert "overall_status" in data + assert "components" in data + assert "checked_by" in data + + # Overall status should be "healthy" or "warning" + assert data["overall_status"] in ("healthy", "warning") + + # Components should be a list + assert isinstance(data["components"], list) + + # Check that expected components are present + component_names = [c["component"] for c in data["components"]] + assert "Configuration" in component_names + assert "Logging" in component_names + + # Each component should have required fields + for component in data["components"]: + assert "component" in component + assert "status" in component + assert "details" in component + assert component["status"] in ("healthy", "warning", "error") + + +def test_system_status_requires_admin(): + """Test that system status endpoint requires admin access.""" + client = TestClient(app) + + # Non-admin user should be denied + r = client.get("/admin/system-status", headers={"X-User-Email": "user@example.com"}) + assert r.status_code in (302, 403) diff --git a/frontend/public/fonts/Inter-Bold.woff2 b/frontend/public/fonts/Inter-Bold.woff2 new file mode 100644 index 0000000..0f1b157 Binary files /dev/null and b/frontend/public/fonts/Inter-Bold.woff2 differ diff --git a/frontend/public/fonts/Inter-Medium.woff2 b/frontend/public/fonts/Inter-Medium.woff2 new file mode 100644 index 0000000..0fd2ee7 Binary files /dev/null and b/frontend/public/fonts/Inter-Medium.woff2 differ diff --git a/frontend/public/fonts/Inter-Regular.woff2 b/frontend/public/fonts/Inter-Regular.woff2 new file mode 100644 index 0000000..b8699af Binary files /dev/null and b/frontend/public/fonts/Inter-Regular.woff2 differ diff --git a/frontend/public/fonts/Inter-SemiBold.woff2 b/frontend/public/fonts/Inter-SemiBold.woff2 new file mode 100644 index 0000000..95c48b1 Binary files /dev/null and b/frontend/public/fonts/Inter-SemiBold.woff2 differ diff --git a/frontend/src/components/LogViewer.jsx b/frontend/src/components/LogViewer.jsx index 46df86c..8a80be6 100644 --- a/frontend/src/components/LogViewer.jsx +++ b/frontend/src/components/LogViewer.jsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState, useCallback, useRef } from 'react'; -import { X, Filter, ChevronDown, ChevronUp, ToggleLeft, ToggleRight } from 'lucide-react'; +import { useEffect, useState, useCallback, useRef } from 'react'; +import { Filter, ChevronDown, ChevronUp, ToggleLeft, ToggleRight } from 'lucide-react'; const DEFAULT_POLL_INTERVAL = 60000; // 60s refresh @@ -42,11 +42,14 @@ export default function LogViewer() { }) .then(data => { const newEntries = data.entries || []; - // Reset to page 0 if auto-scroll is enabled and new entries were added - if (autoScrollEnabled && newEntries.length > entries.length) { - setPage(0); - } - setEntries(newEntries); + // Use functional state update to access previous entries without dependency + setEntries(prevEntries => { + // Reset to page 0 if auto-scroll is enabled and new entries were added + if (autoScrollEnabled && newEntries.length > prevEntries.length) { + setPage(0); + } + return newEntries; + }); setError(null); // After updating entries, scroll to bottom if auto-scroll is enabled and user hasn't scrolled up requestAnimationFrame(() => { @@ -57,7 +60,7 @@ export default function LogViewer() { }) .catch(err => setError(err)) .finally(() => setLoading(false)); - }, [levelFilter, moduleFilter, autoScrollEnabled, entries.length]); // Dependencies for fetchLogs + }, [levelFilter, moduleFilter, autoScrollEnabled]); // Removed entries.length to prevent infinite loop // Function to clear all logs const clearLogs = useCallback(() => { @@ -212,16 +215,16 @@ export default function LogViewer() {