Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 16 additions & 17 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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("/")
Expand All @@ -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():
Expand Down
181 changes: 47 additions & 134 deletions backend/routes/admin_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}")
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))
43 changes: 43 additions & 0 deletions backend/tests/test_security_admin_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Binary file added frontend/public/fonts/Inter-Bold.woff2
Binary file not shown.
Binary file added frontend/public/fonts/Inter-Medium.woff2
Binary file not shown.
Binary file added frontend/public/fonts/Inter-Regular.woff2
Binary file not shown.
Binary file added frontend/public/fonts/Inter-SemiBold.woff2
Binary file not shown.
Loading
Loading