diff --git a/Dockerfile b/Dockerfile index 9739f51..2f1e541 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,9 @@ # Use nginx alpine for minimal size FROM nginx:alpine +# Copy custom nginx config +COPY default.conf /etc/nginx/conf.d/default.conf + # Copy static files to nginx html directory COPY index.html /usr/share/nginx/html/ COPY style.css /usr/share/nginx/html/ diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index 4fd26e7..29122c0 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -1,3 +1,4 @@ +import logging from datetime import datetime, timedelta, timezone import jwt @@ -13,129 +14,258 @@ from ..models.schemas import ( UserResponse, ) +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('/app/auth.log'), + logging.StreamHandler() + ] +) + +logger = logging.getLogger(__name__) + router = APIRouter(prefix="/api/auth", tags=["auth"]) security = HTTPBearer(auto_error=False) def get_msal_app(): """Create MSAL confidential client application""" - if not all( - [ - settings.ENTRA_TENANT_ID, - settings.ENTRA_CLIENT_ID, - settings.ENTRA_CLIENT_SECRET, - ] - ): + logger.info("Checking MSAL configuration") + + required_settings = [ + ("ENTRA_TENANT_ID", settings.ENTRA_TENANT_ID), + ("ENTRA_CLIENT_ID", settings.ENTRA_CLIENT_ID), + ("ENTRA_CLIENT_SECRET", settings.ENTRA_CLIENT_SECRET), + ] + + missing_settings = [name for name, value in required_settings if not value] + if missing_settings: + logger.error(f"Missing required Entra ID settings: {missing_settings}") return None - return msal.ConfidentialClientApplication( - client_id=settings.ENTRA_CLIENT_ID, - client_credential=settings.ENTRA_CLIENT_SECRET, - authority=f"https://login.microsoftonline.com/{settings.ENTRA_TENANT_ID}", - ) + logger.info("All Entra ID settings present, creating MSAL app") + try: + msal_app = msal.ConfidentialClientApplication( + client_id=settings.ENTRA_CLIENT_ID, + client_credential=settings.ENTRA_CLIENT_SECRET, + authority=f"https://login.microsoftonline.com/{settings.ENTRA_TENANT_ID}", + ) + logger.info("MSAL application created successfully") + return msal_app + except Exception as e: + logger.error(f"Failed to create MSAL application: {e}") + return None def create_jwt_token(user_data: dict) -> str: """Create JWT token with user data""" - payload = { - "sub": user_data.get("oid") or user_data.get("sub"), - "name": user_data.get("name"), - "email": user_data.get("preferred_username"), - "exp": datetime.now(timezone.utc) + timedelta(hours=settings.JWT_EXPIRY_HOURS), - "iat": datetime.now(timezone.utc), - } - return jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM) + logger.info("Creating JWT token", { + "user_id": user_data.get("oid") or user_data.get("sub"), + "user_name": user_data.get("name"), + "user_email": user_data.get("preferred_username") + }) + + try: + payload = { + "sub": user_data.get("oid") or user_data.get("sub"), + "name": user_data.get("name"), + "email": user_data.get("preferred_username"), + "exp": datetime.now(timezone.utc) + timedelta(hours=settings.JWT_EXPIRY_HOURS), + "iat": datetime.now(timezone.utc), + } + + token = jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM) + logger.info("JWT token created successfully", { + "expires_in_hours": settings.JWT_EXPIRY_HOURS, + "algorithm": settings.JWT_ALGORITHM + }) + return token + except Exception as e: + logger.error(f"Failed to create JWT token: {e}") + raise def decode_jwt_token(token: str) -> dict: """Decode and validate JWT token""" + logger.info("Decoding JWT token") + try: payload = jwt.decode( token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM] ) + logger.info("JWT token decoded successfully", { + "user_id": payload.get("sub"), + "user_name": payload.get("name"), + "expires_at": datetime.fromtimestamp(payload.get("exp", 0), timezone.utc).isoformat() + }) return payload - except jwt.ExpiredSignatureError: + except jwt.ExpiredSignatureError as e: + logger.warning("JWT token expired", {"error": str(e)}) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired" ) - except jwt.InvalidTokenError: + except jwt.InvalidTokenError as e: + logger.warning("Invalid JWT token", {"error": str(e)}) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token" ) + except Exception as e: + logger.error(f"Unexpected error decoding JWT token: {e}") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Token validation failed" + ) async def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(security), ) -> dict: """Dependency to get current user from JWT token""" + logger.info("Getting current user from credentials") + if not credentials: + logger.warning("No credentials provided") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated" ) + logger.info("Credentials found, decoding token") return decode_jwt_token(credentials.credentials) @router.get("/login", response_model=AuthUrlResponse) async def login(): """Get Microsoft OAuth2 authorization URL""" + logger.info("Login endpoint called") + msal_app = get_msal_app() if not msal_app: + logger.error("MSAL app not available for login") raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Authentication not configured. Please set ENTRA_TENANT_ID, ENTRA_CLIENT_ID, and ENTRA_CLIENT_SECRET.", ) - auth_url = msal_app.get_authorization_request_url( - scopes=["User.Read"], redirect_uri=settings.ENTRA_REDIRECT_URI - ) + try: + logger.info("Generating authorization URL", { + "scopes": ["User.Read"], + "redirect_uri": settings.ENTRA_REDIRECT_URI + }) + auth_url = msal_app.get_authorization_request_url( + scopes=["User.Read"], redirect_uri=settings.ENTRA_REDIRECT_URI + ) - return AuthUrlResponse(auth_url=auth_url) + logger.info("Authorization URL generated successfully", { + "url_length": len(auth_url), + "url_start": auth_url[:100] + "..." if len(auth_url) > 100 else auth_url + }) + + return AuthUrlResponse(auth_url=auth_url) + except Exception as e: + logger.error(f"Failed to generate authorization URL: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to generate authorization URL" + ) @router.post("/callback", response_model=AuthCallbackResponse) async def callback(request: AuthCallbackRequest): """Exchange authorization code for tokens""" + logger.info("Callback endpoint called", { + "code_length": len(request.code) if request.code else 0, + "code_start": request.code[:50] + "..." if request.code and len(request.code) > 50 else request.code + }) + msal_app = get_msal_app() if not msal_app: + logger.error("MSAL app not available for callback") raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Authentication not configured", ) - result = msal_app.acquire_token_by_authorization_code( - code=request.code, - scopes=["User.Read"], - redirect_uri=settings.ENTRA_REDIRECT_URI, - ) + try: + logger.info("Exchanging authorization code for tokens", { + "scopes": ["User.Read"], + "redirect_uri": settings.ENTRA_REDIRECT_URI + }) - if "error" in result: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Authentication failed: {result.get('error_description', result.get('error'))}", + result = msal_app.acquire_token_by_authorization_code( + code=request.code, + scopes=["User.Read"], + redirect_uri=settings.ENTRA_REDIRECT_URI, ) - # Extract user info from ID token claims - id_token_claims = result.get("id_token_claims", {}) + logger.info("Token exchange result", { + "has_access_token": "access_token" in result, + "has_id_token": "id_token" in result, + "has_error": "error" in result, + "error": result.get("error"), + "error_description": result.get("error_description") + }) - # Create our JWT token - token = create_jwt_token(id_token_claims) + if "error" in result: + logger.error("Token exchange failed", { + "error": result.get("error"), + "error_description": result.get("error_description"), + "correlation_id": result.get("correlation_id") + }) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Authentication failed: {result.get('error_description', result.get('error'))}", + ) - return AuthCallbackResponse( - token=token, - user=UserResponse( - id=id_token_claims.get("oid") or id_token_claims.get("sub"), - name=id_token_claims.get("name"), - email=id_token_claims.get("preferred_username"), - ), - ) + # Extract user info from ID token claims + id_token_claims = result.get("id_token_claims", {}) + logger.info("ID token claims extracted", { + "claims_keys": list(id_token_claims.keys()), + "user_id": id_token_claims.get("oid") or id_token_claims.get("sub"), + "user_name": id_token_claims.get("name"), + "user_email": id_token_claims.get("preferred_username") + }) + + # Create our JWT token + token = create_jwt_token(id_token_claims) + + response_data = AuthCallbackResponse( + token=token, + user=UserResponse( + id=id_token_claims.get("oid") or id_token_claims.get("sub"), + name=id_token_claims.get("name"), + email=id_token_claims.get("preferred_username"), + ), + ) + + logger.info("Callback completed successfully", { + "user_id": response_data.user.id, + "user_name": response_data.user.name + }) + + return response_data + + except HTTPException: + # Re-raise HTTP exceptions as-is + raise + except Exception as e: + logger.error(f"Unexpected error in callback: {e}", {"traceback": str(e)}) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal server error during authentication" + ) @router.get("/me", response_model=UserResponse) async def me(current_user: dict = Depends(get_current_user)): """Get current user info""" + logger.info("Me endpoint called", { + "user_id": current_user.get("sub"), + "user_name": current_user.get("name") + }) + return UserResponse( id=current_user.get("sub"), name=current_user.get("name"), @@ -146,12 +276,15 @@ async def me(current_user: dict = Depends(get_current_user)): @router.post("/logout") async def logout(): """Logout (client should clear token)""" + logger.info("Logout endpoint called") return {"message": "Logged out successfully"} @router.get("/status") async def auth_status(): """Check if authentication is configured""" + logger.info("Auth status endpoint called") + configured = all( [ settings.ENTRA_TENANT_ID, @@ -159,4 +292,14 @@ async def auth_status(): settings.ENTRA_CLIENT_SECRET, ] ) + + status_info = { + "configured": configured, + "has_tenant_id": bool(settings.ENTRA_TENANT_ID), + "has_client_id": bool(settings.ENTRA_CLIENT_ID), + "has_client_secret": bool(settings.ENTRA_CLIENT_SECRET), + } + + logger.info("Auth status checked", status_info) + return {"configured": configured} diff --git a/backend/app/main.py b/backend/app/main.py index 847c8bb..98e0e13 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -9,7 +9,14 @@ from .config import settings from .services.provider_manager import provider_manager # Setup logging -logging.basicConfig(level=logging.INFO) +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('/app/devden.log'), + logging.StreamHandler() + ] +) logger = logging.getLogger(__name__) app = FastAPI( @@ -30,9 +37,25 @@ app.include_router(auth.router) app.include_router(chat.router) +@app.exception_handler(Exception) +async def global_exception_handler(request, exc): + """Global exception handler to log all errors""" + logger.error(f"Unhandled exception: {exc}", { + "url": str(request.url), + "method": request.method, + "headers": dict(request.headers), + "traceback": str(exc) + }) + return JSONResponse( + status_code=500, + content={"detail": "Internal server error"} + ) + + @app.get("/health") async def health_check(): """Health check endpoint""" + logger.info("Health check requested") return JSONResponse( content={ "status": "healthy", @@ -41,6 +64,18 @@ async def health_check(): ) +@app.get("/logs") +async def get_logs(): + """Get recent log entries (for debugging)""" + try: + with open('/app/devden.log', 'r') as f: + lines = f.readlines()[-50:] # Last 50 lines + return {"logs": lines} + except Exception as e: + logger.error(f"Failed to read logs: {e}") + return {"error": "Failed to read logs"} + + @app.on_event("startup") async def startup_event(): logger.info("DevDen API starting up...") diff --git a/default.conf b/default.conf new file mode 100644 index 0000000..e2dff01 --- /dev/null +++ b/default.conf @@ -0,0 +1,24 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Enable SPA routing - try to serve the file, then directory, then fallback to index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets for better performance + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} \ No newline at end of file diff --git a/index.html b/index.html index ded0238..276859b 100644 --- a/index.html +++ b/index.html @@ -53,6 +53,16 @@ + + + diff --git a/script.js b/script.js index 98dfdcf..55bdb9c 100644 --- a/script.js +++ b/script.js @@ -1,3 +1,69 @@ +// Enhanced logging utility +class Logger { + constructor() { + this.logs = []; + this.maxLogs = 1000; + } + + log(level, message, data = null) { + const timestamp = new Date().toISOString(); + const logEntry = { + timestamp, + level, + message, + data, + url: window.location.href + }; + + this.logs.push(logEntry); + if (this.logs.length > this.maxLogs) { + this.logs.shift(); + } + + const consoleMethod = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log'; + console[consoleMethod](`[${timestamp}] ${level.toUpperCase()}: ${message}`, data || ''); + + // Save to localStorage for persistence + try { + localStorage.setItem('devden_logs', JSON.stringify(this.logs)); + } catch (e) { + console.warn('Failed to save logs to localStorage:', e); + } + } + + info(message, data = null) { this.log('info', message, data); } + warn(message, data = null) { this.log('warn', message, data); } + error(message, data = null) { this.log('error', message, data); } + + exportLogs() { + const blob = new Blob([JSON.stringify(this.logs, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `devden_logs_${new Date().toISOString().split('T')[0]}.json`; + a.click(); + URL.revokeObjectURL(url); + } + + clearLogs() { + this.logs = []; + localStorage.removeItem('devden_logs'); + } +} + +const logger = new Logger(); + +// Load existing logs from localStorage +try { + const savedLogs = localStorage.getItem('devden_logs'); + if (savedLogs) { + logger.logs = JSON.parse(savedLogs); + logger.info('Loaded existing logs from localStorage', { count: logger.logs.length }); + } +} catch (e) { + logger.warn('Failed to load existing logs from localStorage:', e); +} + const loginScreen = document.getElementById("loginScreen"); const welcomeScreen = document.getElementById("welcomeScreen"); const chatScreen = document.getElementById("chatScreen"); @@ -12,91 +78,166 @@ let isInChat = false; // Auth functions function getToken() { - return localStorage.getItem("devden_token"); + try { + const token = localStorage.getItem("devden_token"); + logger.info('Retrieved token from localStorage', { hasToken: !!token }); + return token; + } catch (error) { + logger.error('Failed to retrieve token from localStorage', error); + return null; + } } function setToken(token) { - localStorage.setItem("devden_token", token); + try { + localStorage.setItem("devden_token", token); + logger.info('Token saved to localStorage'); + } catch (error) { + logger.error('Failed to save token to localStorage', error); + } } function clearToken() { - localStorage.removeItem("devden_token"); + try { + localStorage.removeItem("devden_token"); + logger.info('Token cleared from localStorage'); + } catch (error) { + logger.error('Failed to clear token from localStorage', error); + } } function showLoginScreen() { - loginScreen.classList.remove("hidden"); - welcomeScreen.classList.add("hidden"); - chatScreen.classList.add("hidden"); + try { + loginScreen.classList.remove("hidden"); + welcomeScreen.classList.add("hidden"); + chatScreen.classList.add("hidden"); + logger.info('Switched to login screen'); + } catch (error) { + logger.error('Failed to show login screen', error); + } } function showWelcomeScreen() { - loginScreen.classList.add("hidden"); - welcomeScreen.classList.remove("hidden"); - chatScreen.classList.add("hidden"); - welcomeInput.focus(); + try { + loginScreen.classList.add("hidden"); + welcomeScreen.classList.remove("hidden"); + chatScreen.classList.add("hidden"); + welcomeInput.focus(); + logger.info('Switched to welcome screen'); + } catch (error) { + logger.error('Failed to show welcome screen', error); + } } function switchToChat() { - loginScreen.classList.add("hidden"); - welcomeScreen.classList.add("hidden"); - chatScreen.classList.remove("hidden"); - chatInput.focus(); - isInChat = true; + try { + loginScreen.classList.add("hidden"); + welcomeScreen.classList.add("hidden"); + chatScreen.classList.remove("hidden"); + chatInput.focus(); + isInChat = true; + logger.info('Switched to chat screen'); + } catch (error) { + logger.error('Failed to switch to chat screen', error); + } } async function checkAuth() { + logger.info('Starting auth check'); const token = getToken(); if (!token) { + logger.info('No token found, showing login screen'); showLoginScreen(); return; } try { + logger.info('Making auth check request to /api/auth/me'); const response = await fetch(`${API_URL}/api/auth/me`, { headers: { Authorization: `Bearer ${token}` }, }); + logger.info('Auth check response received', { + status: response.status, + statusText: response.statusText, + ok: response.ok + }); + if (response.ok) { + const userData = await response.json(); + logger.info('Auth check successful', { user: userData }); showWelcomeScreen(); } else { + const errorText = await response.text(); + logger.warn('Auth check failed', { + status: response.status, + statusText: response.statusText, + response: errorText + }); clearToken(); showLoginScreen(); } } catch (error) { - console.error("Auth check failed:", error); + logger.error('Auth check request failed', { + error: error.message, + stack: error.stack + }); showLoginScreen(); } } async function handleLogin() { - loginBtn.disabled = true; - loginBtn.textContent = "Redirecting..."; + logger.info('Login button clicked, starting login process'); try { + loginBtn.disabled = true; + loginBtn.textContent = "Checking auth config..."; + logger.info('Disabled login button and updated text'); + // Check if auth is configured + logger.info('Checking auth configuration via /api/auth/status'); const statusResponse = await fetch(`${API_URL}/api/auth/status`); const statusData = await statusResponse.json(); + logger.info('Auth status response', { + status: statusResponse.status, + configured: statusData.configured + }); + if (!statusData.configured) { - alert( - "Authentication not configured. Please set ENTRA_TENANT_ID, ENTRA_CLIENT_ID, and ENTRA_CLIENT_SECRET in your .env file.", - ); + const errorMsg = "Authentication not configured. Please set ENTRA_TENANT_ID, ENTRA_CLIENT_ID, and ENTRA_CLIENT_SECRET in your .env file."; + logger.error('Auth not configured', { response: statusData }); + alert(errorMsg); loginBtn.disabled = false; loginBtn.textContent = "Sign in with Microsoft"; return; } + loginBtn.textContent = "Getting auth URL..."; + logger.info('Auth configured, requesting login URL from /api/auth/login'); + // Get auth URL and redirect const response = await fetch(`${API_URL}/api/auth/login`); const data = await response.json(); + logger.info('Login URL response', { + status: response.status, + hasAuthUrl: !!data.auth_url, + authUrl: data.auth_url ? data.auth_url.substring(0, 100) + '...' : null + }); + if (data.auth_url) { + loginBtn.textContent = "Redirecting..."; + logger.info('Redirecting to Microsoft OAuth URL'); window.location.href = data.auth_url; } else { throw new Error("No auth URL returned"); } } catch (error) { - console.error("Login failed:", error); + logger.error('Login process failed', { + error: error.message, + stack: error.stack + }); alert("Login failed: " + error.message); loginBtn.disabled = false; loginBtn.textContent = "Sign in with Microsoft"; @@ -104,32 +245,81 @@ async function handleLogin() { } async function handleCallback() { + logger.info('Starting OAuth callback processing', { url: window.location.href }); + const params = new URLSearchParams(window.location.search); const code = params.get("code"); + const sessionState = params.get("session_state"); + const error = params.get("error"); + const errorDescription = params.get("error_description"); - if (!code) return false; + logger.info('Parsed URL parameters', { + hasCode: !!code, + hasSessionState: !!sessionState, + hasError: !!error, + codeLength: code ? code.length : 0, + sessionState: sessionState, + error: error, + errorDescription: errorDescription + }); + + // Check for OAuth errors + if (error) { + logger.error('OAuth error in callback URL', { + error: error, + errorDescription: errorDescription + }); + alert(`Authentication failed: ${error} - ${errorDescription || 'Unknown error'}`); + return false; + } + + if (!code) { + logger.warn('No authorization code found in callback URL'); + return false; + } try { + logger.info('Sending authorization code to backend /api/auth/callback'); const response = await fetch(`${API_URL}/api/auth/callback`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code }), }); + logger.info('Callback response received', { + status: response.status, + statusText: response.statusText, + ok: response.ok + }); + if (!response.ok) { - const error = await response.json(); - throw new Error(error.detail || "Callback failed"); + const errorData = await response.json(); + logger.error('Callback request failed', { + status: response.status, + error: errorData + }); + throw new Error(errorData.detail || "Callback failed"); } const data = await response.json(); + logger.info('Callback successful', { + hasToken: !!data.token, + hasUser: !!data.user, + user: data.user + }); + setToken(data.token); // Clean up URL + logger.info('Cleaning up URL (removing query parameters)'); window.history.replaceState({}, "", "/"); return true; } catch (error) { - console.error("Callback failed:", error); + logger.error('Callback processing failed', { + error: error.message, + stack: error.stack + }); alert("Authentication failed: " + error.message); return false; } @@ -288,14 +478,158 @@ chatInput.addEventListener("keydown", (e) => { // Initialize async function init() { - // Check for OAuth callback first - const callbackSuccess = await handleCallback(); + logger.info('Application initialization started', { + userAgent: navigator.userAgent, + url: window.location.href, + timestamp: new Date().toISOString() + }); - if (callbackSuccess) { - showWelcomeScreen(); + // Check for CSS loading + const styleLink = document.querySelector('link[rel="stylesheet"]'); + if (styleLink) { + logger.info('CSS link found', { href: styleLink.href }); + styleLink.addEventListener('load', () => { + logger.info('CSS loaded successfully'); + }); + styleLink.addEventListener('error', (e) => { + logger.error('CSS failed to load', { href: styleLink.href, error: e }); + }); } else { - await checkAuth(); + logger.error('CSS link not found in document'); } + + // Check DOM elements + const elements = ['loginScreen', 'welcomeScreen', 'chatScreen', 'chatMessages', 'welcomeInput', 'chatInput', 'loginBtn']; + elements.forEach(id => { + const el = document.getElementById(id); + if (!el) { + logger.error(`Required DOM element not found: ${id}`); + } else { + logger.info(`DOM element found: ${id}`); + } + }); + + try { + // Check for OAuth callback first + logger.info('Checking for OAuth callback parameters'); + const callbackSuccess = await handleCallback(); + + if (callbackSuccess) { + logger.info('OAuth callback processed successfully, showing welcome screen'); + showWelcomeScreen(); + } else { + logger.info('No OAuth callback or callback failed, checking existing auth'); + await checkAuth(); + } + } catch (error) { + logger.error('Initialization failed', { + error: error.message, + stack: error.stack + }); + } + + logger.info('Application initialization completed'); } +// Add global error handler +window.addEventListener('error', (event) => { + logger.error('Global JavaScript error', { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + error: event.error + }); +}); + +window.addEventListener('unhandledrejection', (event) => { + logger.error('Unhandled promise rejection', { + reason: event.reason, + promise: event.promise + }); +}); + +// Debug panel functionality +const debugPanel = document.getElementById('debugPanel'); +const exportLogsBtn = document.getElementById('exportLogsBtn'); +const clearLogsBtn = document.getElementById('clearLogsBtn'); +const viewBackendLogsBtn = document.getElementById('viewBackendLogsBtn'); +const closeDebugBtn = document.getElementById('closeDebugBtn'); +const debugOutput = document.getElementById('debugOutput'); + +function showDebugPanel() { + debugPanel.style.display = 'block'; + logger.info('Debug panel opened'); +} + +function hideDebugPanel() { + debugPanel.style.display = 'none'; + logger.info('Debug panel closed'); +} + +function updateDebugOutput(text) { + debugOutput.textContent = text; + debugOutput.scrollTop = debugOutput.scrollHeight; +} + +// Event listeners for debug panel +exportLogsBtn.addEventListener('click', () => { + logger.exportLogs(); + updateDebugOutput('Frontend logs exported to download'); + logger.info('Logs exported via debug panel'); +}); + +clearLogsBtn.addEventListener('click', () => { + logger.clearLogs(); + updateDebugOutput('Logs cleared'); + logger.info('Logs cleared via debug panel'); +}); + +viewBackendLogsBtn.addEventListener('click', async () => { + try { + updateDebugOutput('Loading backend logs...'); + const response = await fetch(`${API_URL}/logs`); + if (response.ok) { + const data = await response.json(); + if (data.logs) { + updateDebugOutput(data.logs.join('\n')); + } else if (data.error) { + updateDebugOutput(`Error: ${data.error}`); + } + } else { + updateDebugOutput(`Failed to load backend logs: ${response.status}`); + } + } catch (error) { + updateDebugOutput(`Error loading backend logs: ${error.message}`); + logger.error('Failed to load backend logs', error); + } +}); + +closeDebugBtn.addEventListener('click', hideDebugPanel); + +// Keyboard shortcuts +document.addEventListener('keydown', (e) => { + // Ctrl+Shift+L: Export logs + if (e.ctrlKey && e.shiftKey && e.key === 'L') { + e.preventDefault(); + logger.exportLogs(); + logger.info('Logs exported via keyboard shortcut'); + } + + // Ctrl+Shift+D: Toggle debug panel + if (e.ctrlKey && e.shiftKey && e.key === 'D') { + e.preventDefault(); + if (debugPanel.style.display === 'none') { + showDebugPanel(); + } else { + hideDebugPanel(); + } + } + + // Escape: Close debug panel + if (e.key === 'Escape' && debugPanel.style.display !== 'none') { + hideDebugPanel(); + } +}); + init();