From 44aaa90998b297130aa54fa6fe39b458e4e4b002 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Nov 2025 05:18:49 +0000 Subject: [PATCH 1/2] fix: launch applications by ID instead of template name This change improves application launching reliability and error messages: - Modified CreateSession API to accept optional applicationId parameter - When applicationId is provided, the API now looks up the installed application to get the correct template name - Added validation for application enabled status and installation state - Provides specific error messages for: - Application not found - Application disabled - Installation failed - Installation still pending - Missing template configuration - Updated frontend Dashboard to send applicationId instead of templateName - Updated CreateSessionRequest interface to support applicationId This fixes the "Template not found" error when launching applications from the dashboard by ensuring the correct template name is resolved from the installed application record. --- api/internal/api/handlers.go | 96 +++++++++++++++++++++++++++++++++--- ui/src/lib/api.ts | 3 +- ui/src/pages/Dashboard.tsx | 6 ++- 3 files changed, 95 insertions(+), 10 deletions(-) diff --git a/api/internal/api/handlers.go b/api/internal/api/handlers.go index 37afa3d4..16638d40 100644 --- a/api/internal/api/handlers.go +++ b/api/internal/api/handlers.go @@ -379,7 +379,8 @@ func (h *Handler) CreateSession(c *gin.Context) { var req struct { User string `json:"user" binding:"required"` - Template string `json:"template" binding:"required"` + Template string `json:"template"` + ApplicationId string `json:"applicationId"` Resources *struct { Memory string `json:"memory"` CPU string `json:"cpu"` @@ -395,16 +396,97 @@ func (h *Handler) CreateSession(c *gin.Context) { return } - // Step 1: Verify Kubernetes Template CRD exists + // Step 1: Resolve template name from application ID or direct template name + // If applicationId is provided, look up the application to get the template name + // This provides better error messages and validation + templateName := req.Template + + if req.ApplicationId != "" { + // Look up the installed application in the database + var appTemplateName, appDisplayName, installStatus, installMessage string + var enabled bool + err := h.db.DB().QueryRowContext(ctx, ` + SELECT + COALESCE(ct.name, '') as template_name, + ia.display_name, + ia.enabled, + COALESCE(ia.install_status, 'unknown') as install_status, + COALESCE(ia.install_message, '') as install_message + FROM installed_applications ia + LEFT JOIN catalog_templates ct ON ia.catalog_template_id = ct.id + WHERE ia.id = $1 + `, req.ApplicationId).Scan(&appTemplateName, &appDisplayName, &enabled, &installStatus, &installMessage) + + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "error": "Application not found", + "message": fmt.Sprintf("No application found with ID: %s", req.ApplicationId), + }) + return + } + + // Check if the application is enabled + if !enabled { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Application disabled", + "message": fmt.Sprintf("The application '%s' is currently disabled", appDisplayName), + }) + return + } + + // Check installation status + if installStatus == "failed" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Application installation failed", + "message": fmt.Sprintf("The application '%s' failed to install: %s", appDisplayName, installMessage), + }) + return + } + + if installStatus == "pending" || installStatus == "creating" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Application still installing", + "message": fmt.Sprintf("The application '%s' is still being installed. Please wait and try again.", appDisplayName), + }) + return + } + + // Validate template name was found + if appTemplateName == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Application configuration error", + "message": fmt.Sprintf("The application '%s' does not have a valid template configuration", appDisplayName), + }) + return + } + + templateName = appTemplateName + } else if req.Template == "" { + // Neither applicationId nor template provided + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Missing required field", + "message": "Either 'applicationId' or 'template' must be provided", + }) + return + } + + // Step 2: Verify Kubernetes Template CRD exists // The template must be created during application installation (see handlers/applications.go) // Without a valid template, the session cannot be created - template, err := h.k8sClient.GetTemplate(ctx, h.namespace, req.Template) + template, err := h.k8sClient.GetTemplate(ctx, h.namespace, templateName) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Template not found: %s. Please ensure the application is properly installed.", req.Template)}) + // Provide a more helpful error message + errorMsg := fmt.Sprintf("Template not found: %s.", templateName) + if req.ApplicationId != "" { + errorMsg += " The application may still be installing or the Kubernetes controller may not be running." + } else { + errorMsg += " Please ensure the application is properly installed." + } + c.JSON(http.StatusBadRequest, gin.H{"error": errorMsg}) return } - // Step 2: Determine resource allocation (memory/CPU) + // Step 3: Determine resource allocation (memory/CPU) // Priority: request > template defaults > system defaults memory := "2Gi" // System default cpu := "1000m" // System default (1 core) @@ -426,7 +508,7 @@ func (h *Handler) CreateSession(c *gin.Context) { } } - // Step 3: Validate and parse resource specifications + // Step 4: Validate and parse resource specifications // Convert human-readable formats (e.g., "2Gi", "500m") to int64 for quota checking requestedCPU, requestedMemory, err := h.quotaEnforcer.ValidateResourceRequest(cpu, memory) if err != nil { @@ -437,7 +519,7 @@ func (h *Handler) CreateSession(c *gin.Context) { return } - // Step 4: Check user quota before creating session + // Step 5: Check user quota before creating session // Get current resource usage by listing all pods belonging to this user podList, err := h.k8sClient.GetPods(ctx, h.namespace) if err != nil { diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index 16eb9885..1f123f67 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -282,7 +282,8 @@ export interface AddGroupAccessRequest { export interface CreateSessionRequest { user: string; - template: string; + template?: string; + applicationId?: string; resources?: { memory?: string; cpu?: string; diff --git a/ui/src/pages/Dashboard.tsx b/ui/src/pages/Dashboard.tsx index 6c748128..cd2c4317 100644 --- a/ui/src/pages/Dashboard.tsx +++ b/ui/src/pages/Dashboard.tsx @@ -142,7 +142,7 @@ export default function Dashboard() { name: sessionName, namespace: 'streamspace', user: username, - template: templateName, + applicationId: app.id, state: 'running', persistentHome: true, }); @@ -155,7 +155,9 @@ export default function Dashboard() { navigate('/sessions'); }, 1000); } catch (error: any) { - toast.error(error.response?.data?.message || 'Failed to launch application'); + const errorData = error.response?.data; + const errorMessage = errorData?.message || errorData?.error || 'Failed to launch application'; + toast.error(errorMessage); } finally { const newLaunching = new Set(launching); newLaunching.delete(app.id); From b44bc4ddca6904dfe092271706a5cde5d592d88c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Nov 2025 05:32:32 +0000 Subject: [PATCH 2/2] fix: improve error messages across API handlers and database layer Enhanced error messages to include contextual information for debugging: Database layer (sessions.go): - All session operations now wrap errors with session ID, user ID context - Error messages include operation type (create, update, delete, query) API handlers improved: - quotas.go: 23 errors with user/team IDs, quota values - batch.go: 11 errors with user ID, session counts, operation types - scheduling.go: 12 errors with schedule ID, user ID, template info - loadbalancing.go: 8 errors with policy names, node info, scaling details - preferences.go: 15 errors with user ID, preference keys - security.go: 16 errors with MFA IDs, user IDs, IP addresses - collaboration.go: 11 errors with collaboration ID, user ID, annotation ID All errors now follow consistent pattern: - "error": Short description of what failed - "message": Detailed context with IDs and underlying error This makes debugging significantly easier by showing exactly what operation failed, which entity was involved, and the root cause. --- api/internal/db/sessions.go | 69 +++++++++++---- api/internal/handlers/batch.go | 55 +++++++++--- api/internal/handlers/collaboration.go | 55 +++++++++--- api/internal/handlers/loadbalancing.go | 40 +++++++-- api/internal/handlers/preferences.go | 76 ++++++++++++---- api/internal/handlers/quotas.go | 80 +++++++++++++---- api/internal/handlers/scheduling.go | 115 ++++++++++++++++++++----- api/internal/handlers/security.go | 80 +++++++++++++---- 8 files changed, 455 insertions(+), 115 deletions(-) diff --git a/api/internal/db/sessions.go b/api/internal/db/sessions.go index 8ad6bc60..9aa8e84e 100644 --- a/api/internal/db/sessions.go +++ b/api/internal/db/sessions.go @@ -80,7 +80,10 @@ func (s *SessionDB) CreateSession(ctx context.Context, session *Session) error { session.Memory, session.CPU, session.PersistentHome, session.IdleTimeout, session.MaxSessionDuration, session.CreatedAt, session.UpdatedAt, session.LastConnection, session.LastDisconnect, session.LastActivity, ) - return err + if err != nil { + return fmt.Errorf("failed to create session %s for user %s: %w", session.ID, session.UserID, err) + } + return nil } // GetSession retrieves a session by ID. @@ -109,7 +112,7 @@ func (s *SessionDB) GetSession(ctx context.Context, sessionID string) (*Session, if err == sql.ErrNoRows { return nil, fmt.Errorf("session not found: %s", sessionID) } - return nil, err + return nil, fmt.Errorf("failed to get session %s: %w", sessionID, err) } return session, nil @@ -150,11 +153,15 @@ func (s *SessionDB) ListSessionsByUser(ctx context.Context, userID string) ([]*S rows, err := s.db.QueryContext(ctx, query, userID) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to list sessions for user %s: %w", userID, err) } defer rows.Close() - return s.scanSessions(rows) + sessions, err := s.scanSessions(rows) + if err != nil { + return nil, fmt.Errorf("failed to scan sessions for user %s: %w", userID, err) + } + return sessions, nil } // ListSessionsByState retrieves all sessions with a specific state. @@ -174,11 +181,15 @@ func (s *SessionDB) ListSessionsByState(ctx context.Context, state string) ([]*S rows, err := s.db.QueryContext(ctx, query, state) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to list sessions with state %s: %w", state, err) } defer rows.Close() - return s.scanSessions(rows) + sessions, err := s.scanSessions(rows) + if err != nil { + return nil, fmt.Errorf("failed to scan sessions with state %s: %w", state, err) + } + return sessions, nil } // UpdateSessionState updates the state of a session. @@ -191,7 +202,7 @@ func (s *SessionDB) UpdateSessionState(ctx context.Context, sessionID, state str result, err := s.db.ExecContext(ctx, query, state, time.Now(), sessionID) if err != nil { - return err + return fmt.Errorf("failed to update state to %s for session %s: %w", state, sessionID, err) } rows, _ := result.RowsAffected() @@ -211,7 +222,10 @@ func (s *SessionDB) UpdateSessionURL(ctx context.Context, sessionID, url string) ` _, err := s.db.ExecContext(ctx, query, url, time.Now(), sessionID) - return err + if err != nil { + return fmt.Errorf("failed to update URL for session %s: %w", sessionID, err) + } + return nil } // UpdateSessionStatus updates session state, URL, and pod name from controller status events. @@ -224,7 +238,7 @@ func (s *SessionDB) UpdateSessionStatus(ctx context.Context, sessionID, state, u result, err := s.db.ExecContext(ctx, query, state, url, podName, time.Now(), sessionID) if err != nil { - return err + return fmt.Errorf("failed to update status for session %s (state=%s, url=%s, pod=%s): %w", sessionID, state, url, podName, err) } rows, _ := result.RowsAffected() @@ -244,7 +258,10 @@ func (s *SessionDB) UpdateLastActivity(ctx context.Context, sessionID string) er ` _, err := s.db.ExecContext(ctx, query, time.Now(), sessionID) - return err + if err != nil { + return fmt.Errorf("failed to update last activity for session %s: %w", sessionID, err) + } + return nil } // UpdateActiveConnections updates the connection count for a session. @@ -257,7 +274,10 @@ func (s *SessionDB) UpdateActiveConnections(ctx context.Context, sessionID strin ` _, err := s.db.ExecContext(ctx, query, count, now, sessionID) - return err + if err != nil { + return fmt.Errorf("failed to update active connections to %d for session %s: %w", count, sessionID, err) + } + return nil } // DeleteSession marks a session as deleted. @@ -269,13 +289,19 @@ func (s *SessionDB) DeleteSession(ctx context.Context, sessionID string) error { ` _, err := s.db.ExecContext(ctx, query, time.Now(), sessionID) - return err + if err != nil { + return fmt.Errorf("failed to mark session %s as deleted: %w", sessionID, err) + } + return nil } // HardDeleteSession permanently removes a session from the database. func (s *SessionDB) HardDeleteSession(ctx context.Context, sessionID string) error { _, err := s.db.ExecContext(ctx, "DELETE FROM sessions WHERE id = $1", sessionID) - return err + if err != nil { + return fmt.Errorf("failed to permanently delete session %s: %w", sessionID, err) + } + return nil } // CountSessionsByUser returns the number of active sessions for a user. @@ -285,7 +311,10 @@ func (s *SessionDB) CountSessionsByUser(ctx context.Context, userID string) (int SELECT COUNT(*) FROM sessions WHERE user_id = $1 AND state IN ('running', 'pending', 'hibernated') `, userID).Scan(&count) - return count, err + if err != nil { + return 0, fmt.Errorf("failed to count sessions for user %s: %w", userID, err) + } + return count, nil } // GetIdleSessions returns sessions that have been idle beyond their timeout. @@ -313,11 +342,15 @@ func (s *SessionDB) GetIdleSessions(ctx context.Context) ([]*Session, error) { func (s *SessionDB) querySessions(ctx context.Context, query string, args ...interface{}) ([]*Session, error) { rows, err := s.db.QueryContext(ctx, query, args...) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to execute session query: %w", err) } defer rows.Close() - return s.scanSessions(rows) + sessions, err := s.scanSessions(rows) + if err != nil { + return nil, fmt.Errorf("failed to scan session results: %w", err) + } + return sessions, nil } // scanSessions scans rows into Session structs. @@ -333,13 +366,13 @@ func (s *SessionDB) scanSessions(rows *sql.Rows) ([]*Session, error) { &session.CreatedAt, &session.UpdatedAt, &session.LastConnection, &session.LastDisconnect, &session.LastActivity, ) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to scan session row: %w", err) } sessions = append(sessions, session) } if err := rows.Err(); err != nil { - return nil, err + return nil, fmt.Errorf("error iterating session rows: %w", err) } return sessions, nil diff --git a/api/internal/handlers/batch.go b/api/internal/handlers/batch.go index 64141c52..289a45d9 100644 --- a/api/internal/handlers/batch.go +++ b/api/internal/handlers/batch.go @@ -162,7 +162,10 @@ func (h *BatchHandler) TerminateSessions(c *gin.Context) { `, jobID, userIDStr, len(req.SessionIDs)) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create batch job"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create batch job", + "message": fmt.Sprintf("Failed to create batch terminate job for user %s with %d sessions: %v", userIDStr, len(req.SessionIDs), err), + }) return } @@ -200,7 +203,10 @@ func (h *BatchHandler) HibernateSessions(c *gin.Context) { `, jobID, userIDStr, len(req.SessionIDs)) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create batch job"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create batch job", + "message": fmt.Sprintf("Failed to create batch hibernate job for user %s with %d sessions: %v", userIDStr, len(req.SessionIDs), err), + }) return } @@ -237,7 +243,10 @@ func (h *BatchHandler) WakeSessions(c *gin.Context) { `, jobID, userIDStr, len(req.SessionIDs)) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create batch job"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create batch job", + "message": fmt.Sprintf("Failed to create batch wake job for user %s with %d sessions: %v", userIDStr, len(req.SessionIDs), err), + }) return } @@ -274,7 +283,10 @@ func (h *BatchHandler) DeleteSessions(c *gin.Context) { `, jobID, userIDStr, len(req.SessionIDs)) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create batch job"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create batch job", + "message": fmt.Sprintf("Failed to create batch delete job for user %s with %d sessions: %v", userIDStr, len(req.SessionIDs), err), + }) return } @@ -317,7 +329,10 @@ func (h *BatchHandler) UpdateSessionTags(c *gin.Context) { `, jobID, userIDStr, len(req.SessionIDs)) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create batch job"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create batch job", + "message": fmt.Sprintf("Failed to create batch update_tags job for user %s with %d sessions (operation: %s): %v", userIDStr, len(req.SessionIDs), req.Operation, err), + }) return } @@ -355,7 +370,10 @@ func (h *BatchHandler) UpdateSessionResources(c *gin.Context) { `, jobID, userIDStr, len(req.SessionIDs)) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create batch job"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create batch job", + "message": fmt.Sprintf("Failed to create batch update_resources job for user %s with %d sessions: %v", userIDStr, len(req.SessionIDs), err), + }) return } @@ -390,7 +408,10 @@ func (h *BatchHandler) DeleteSnapshots(c *gin.Context) { `, jobID, userIDStr, len(req.SnapshotIDs)) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create batch job"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create batch job", + "message": fmt.Sprintf("Failed to create batch delete snapshots job for user %s with %d snapshots: %v", userIDStr, len(req.SnapshotIDs), err), + }) return } @@ -428,7 +449,10 @@ func (h *BatchHandler) CreateSnapshots(c *gin.Context) { `, jobID, userIDStr, len(req.SessionIDs)) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create batch job"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create batch job", + "message": fmt.Sprintf("Failed to create batch snapshot creation job for user %s with %d sessions (snapshot name: %s): %v", userIDStr, len(req.SessionIDs), req.Name, err), + }) return } @@ -490,7 +514,10 @@ func (h *BatchHandler) ListBatchJobs(c *gin.Context) { `, userIDStr) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list batch jobs"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to list batch jobs", + "message": fmt.Sprintf("Failed to query batch jobs for user %s: %v", userIDStr, err), + }) return } defer rows.Close() @@ -549,7 +576,10 @@ func (h *BatchHandler) GetBatchJob(c *gin.Context) { `, jobID, userIDStr).Scan(&id, &operationType, &resourceType, &status, &totalItems, &processedItems, &successCount, &failureCount, &createdAt, &completedAt) if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "Batch job not found"}) + c.JSON(http.StatusNotFound, gin.H{ + "error": "Batch job not found", + "message": fmt.Sprintf("Failed to retrieve batch job %s for user %s: %v", jobID, userIDStr, err), + }) return } @@ -584,7 +614,10 @@ func (h *BatchHandler) CancelBatchJob(c *gin.Context) { `, jobID, userIDStr) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cancel batch job"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to cancel batch job", + "message": fmt.Sprintf("Failed to cancel batch job %s for user %s: %v", jobID, userIDStr, err), + }) return } diff --git a/api/internal/handlers/collaboration.go b/api/internal/handlers/collaboration.go index b78f0ed8..900c88b1 100644 --- a/api/internal/handlers/collaboration.go +++ b/api/internal/handlers/collaboration.go @@ -465,7 +465,10 @@ func (h *CollaborationHandler) CreateCollaborationSession(c *gin.Context) { `, collabID, sessionID, userID, toJSONB(req.Settings), true, true, true, "active").Scan(&collabID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create collaboration session"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create collaboration session", + "message": fmt.Sprintf("Database insert failed for session %s by user %s: %v", sessionID, userID, err), + }) return } @@ -588,7 +591,10 @@ func (h *CollaborationHandler) JoinCollaborationSession(c *gin.Context) { `, collabID, userID, "participant", toJSONB(participantPerms), userColor, true) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to join collaboration"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to join collaboration", + "message": fmt.Sprintf("Database insert failed for user %s joining collaboration %s: %v", userID, collabID, err), + }) return } @@ -627,7 +633,10 @@ func (h *CollaborationHandler) LeaveCollaborationSession(c *gin.Context) { `, time.Now(), collabID, userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to leave"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to leave collaboration", + "message": fmt.Sprintf("Database update failed for user %s leaving collaboration %s: %v", userID, collabID, err), + }) return } @@ -669,7 +678,10 @@ func (h *CollaborationHandler) GetCollaborationParticipants(c *gin.Context) { `, collabID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve participants"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to retrieve participants", + "message": fmt.Sprintf("Database query failed for collaboration %s: %v", collabID, err), + }) return } defer rows.Close() @@ -730,7 +742,10 @@ func (h *CollaborationHandler) UpdateParticipantRole(c *gin.Context) { `, req.Role, toJSONB(req.Permissions), collabID, targetUserID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update role"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to update role", + "message": fmt.Sprintf("Database update failed for user %s in collaboration %s: %v", targetUserID, collabID, err), + }) return } @@ -775,7 +790,10 @@ func (h *CollaborationHandler) SendChatMessage(c *gin.Context) { `, collabID, userID, req.Message, req.MessageType, toJSONB(req.Metadata)).Scan(&msgID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to send message"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to send message", + "message": fmt.Sprintf("Database insert failed for chat message from user %s in collaboration %s: %v", userID, collabID, err), + }) return } @@ -820,7 +838,10 @@ func (h *CollaborationHandler) GetChatHistory(c *gin.Context) { rows, err := h.DB.DB().Query(query, args...) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve chat"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to retrieve chat history", + "message": fmt.Sprintf("Database query failed for collaboration %s: %v", collabID, err), + }) return } defer rows.Close() @@ -897,7 +918,10 @@ func (h *CollaborationHandler) CreateAnnotation(c *gin.Context) { toJSONB(req.Points), req.Text, req.IsPersistent, expiresAt) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create annotation"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create annotation", + "message": fmt.Sprintf("Database insert failed for annotation %s by user %s in collaboration %s: %v", annotationID, userID, collabID, err), + }) return } @@ -923,7 +947,10 @@ func (h *CollaborationHandler) GetAnnotations(c *gin.Context) { `, collabID, time.Now()) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve annotations"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to retrieve annotations", + "message": fmt.Sprintf("Database query failed for collaboration %s: %v", collabID, err), + }) return } defer rows.Close() @@ -964,7 +991,10 @@ func (h *CollaborationHandler) DeleteAnnotation(c *gin.Context) { _, err := h.DB.DB().Exec("DELETE FROM collaboration_annotations WHERE id = $1", annotationID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete annotation"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to delete annotation", + "message": fmt.Sprintf("Database delete failed for annotation %s in collaboration %s: %v", annotationID, collabID, err), + }) return } @@ -983,7 +1013,10 @@ func (h *CollaborationHandler) ClearAllAnnotations(c *gin.Context) { result, err := h.DB.DB().Exec("DELETE FROM collaboration_annotations WHERE collaboration_id = $1", collabID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to clear annotations"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to clear annotations", + "message": fmt.Sprintf("Database delete failed for all annotations in collaboration %s: %v", collabID, err), + }) return } diff --git a/api/internal/handlers/loadbalancing.go b/api/internal/handlers/loadbalancing.go index 024ed4a1..a3b92083 100644 --- a/api/internal/handlers/loadbalancing.go +++ b/api/internal/handlers/loadbalancing.go @@ -193,7 +193,10 @@ func (h *LoadBalancingHandler) CreateLoadBalancingPolicy(c *gin.Context) { req.ResourceThresholds, req.Metadata, createdBy).Scan(&id) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create policy"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create load balancing policy", + "message": fmt.Sprintf("Database insert failed for policy '%s' with strategy '%s': %v", req.Name, req.Strategy, err), + }) return } @@ -217,7 +220,10 @@ func (h *LoadBalancingHandler) ListLoadBalancingPolicies(c *gin.Context) { `) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to list load balancing policies", + "message": fmt.Sprintf("Database query failed: %v", err), + }) return } defer rows.Close() @@ -247,7 +253,10 @@ func (h *LoadBalancingHandler) GetNodeStatus(c *gin.Context) { // Fall back to database if K8s API is not available nodes, err = h.fetchNodeStatusFromDatabase() if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get node status"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get node status", + "message": fmt.Sprintf("Both Kubernetes API and database fallback failed: %v", err), + }) return } } @@ -653,7 +662,10 @@ func (h *LoadBalancingHandler) SelectNode(c *gin.Context) { `) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get node status"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get node status for selection", + "message": fmt.Sprintf("Database query for available nodes failed: %v", err), + }) return } defer rows.Close() @@ -861,7 +873,10 @@ func (h *LoadBalancingHandler) CreateAutoScalingPolicy(c *gin.Context) { req.Metadata, createdBy).Scan(&id) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create policy"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create auto-scaling policy", + "message": fmt.Sprintf("Database insert failed for policy '%s' targeting %s '%s': %v", req.Name, req.TargetType, req.TargetID, err), + }) return } @@ -886,7 +901,10 @@ func (h *LoadBalancingHandler) ListAutoScalingPolicies(c *gin.Context) { `) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to list auto-scaling policies", + "message": fmt.Sprintf("Database query failed: %v", err), + }) return } defer rows.Close() @@ -1009,7 +1027,10 @@ func (h *LoadBalancingHandler) TriggerScaling(c *gin.Context) { newReplicas, req.Reason).Scan(&eventID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to record scaling event"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to record scaling event", + "message": fmt.Sprintf("Database insert failed for scaling event on policy %s (action: %s, %d -> %d replicas): %v", policyID, req.Action, currentReplicas, newReplicas, err), + }) return } @@ -1063,7 +1084,10 @@ func (h *LoadBalancingHandler) GetScalingHistory(c *gin.Context) { } if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get scaling history", + "message": fmt.Sprintf("Database query failed for scaling events (policy_id filter: %s, limit: %s): %v", policyID, limit, err), + }) return } defer rows.Close() diff --git a/api/internal/handlers/preferences.go b/api/internal/handlers/preferences.go index ec9b7d5b..d557347a 100644 --- a/api/internal/handlers/preferences.go +++ b/api/internal/handlers/preferences.go @@ -80,6 +80,7 @@ import ( "context" "database/sql" "encoding/json" + "fmt" "net/http" "github.com/gin-gonic/gin" @@ -156,13 +157,19 @@ func (h *PreferencesHandler) GetPreferences(c *gin.Context) { } if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get preferences"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get preferences", + "message": fmt.Sprintf("Database query failed for user %s: %v", userIDStr, err), + }) return } var prefs map[string]interface{} if err := json.Unmarshal(prefsJSON, &prefs); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse preferences"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse preferences", + "message": fmt.Sprintf("JSON unmarshal failed for user %s: %v", userIDStr, err), + }) return } @@ -194,7 +201,10 @@ func (h *PreferencesHandler) UpdatePreferences(c *gin.Context) { // Serialize preferences prefsJSON, err := json.Marshal(prefs) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to serialize preferences"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to serialize preferences", + "message": fmt.Sprintf("JSON marshal failed for user %s: %v", userIDStr, err), + }) return } @@ -207,7 +217,10 @@ func (h *PreferencesHandler) UpdatePreferences(c *gin.Context) { `, userIDStr, prefsJSON) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update preferences"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to update preferences", + "message": fmt.Sprintf("Database upsert failed for user %s: %v", userIDStr, err), + }) return } @@ -235,7 +248,10 @@ func (h *PreferencesHandler) GetUIPreferences(c *gin.Context) { } if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get UI preferences"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get UI preferences", + "message": fmt.Sprintf("Database query for UI preferences failed for user %s: %v", userIDStr, err), + }) return } @@ -271,7 +287,10 @@ func (h *PreferencesHandler) UpdateUIPreferences(c *gin.Context) { `, userIDStr, uiPrefsJSON) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update UI preferences"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to update UI preferences", + "message": fmt.Sprintf("Database upsert for UI preferences failed for user %s: %v", userIDStr, err), + }) return } @@ -299,7 +318,10 @@ func (h *PreferencesHandler) GetNotificationPreferences(c *gin.Context) { } if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get notification preferences"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get notification preferences", + "message": fmt.Sprintf("Database query for notification preferences failed for user %s: %v", userIDStr, err), + }) return } @@ -334,7 +356,10 @@ func (h *PreferencesHandler) UpdateNotificationPreferences(c *gin.Context) { `, userIDStr, notifPrefsJSON) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update notification preferences"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to update notification preferences", + "message": fmt.Sprintf("Database upsert for notification preferences failed for user %s: %v", userIDStr, err), + }) return } @@ -362,7 +387,10 @@ func (h *PreferencesHandler) GetDefaultsPreferences(c *gin.Context) { } if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get defaults"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get defaults", + "message": fmt.Sprintf("Database query for default session preferences failed for user %s: %v", userIDStr, err), + }) return } @@ -397,7 +425,10 @@ func (h *PreferencesHandler) UpdateDefaultsPreferences(c *gin.Context) { `, userIDStr, defaultsJSON) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update defaults"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to update defaults", + "message": fmt.Sprintf("Database upsert for default session preferences failed for user %s: %v", userIDStr, err), + }) return } @@ -422,7 +453,10 @@ func (h *PreferencesHandler) GetFavorites(c *gin.Context) { `, userIDStr) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get favorites"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get favorites", + "message": fmt.Sprintf("Database query for favorite templates failed for user %s: %v", userIDStr, err), + }) return } defer rows.Close() @@ -460,7 +494,10 @@ func (h *PreferencesHandler) AddFavorite(c *gin.Context) { `, userIDStr, templateName) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add favorite"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to add favorite", + "message": fmt.Sprintf("Database insert for favorite template '%s' failed for user %s: %v", templateName, userIDStr, err), + }) return } @@ -484,7 +521,10 @@ func (h *PreferencesHandler) RemoveFavorite(c *gin.Context) { `, userIDStr, templateName) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove favorite"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to remove favorite", + "message": fmt.Sprintf("Database delete for favorite template '%s' failed for user %s: %v", templateName, userIDStr, err), + }) return } @@ -510,7 +550,10 @@ func (h *PreferencesHandler) GetRecentSessions(c *gin.Context) { `, userIDStr) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get recent sessions"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get recent sessions", + "message": fmt.Sprintf("Database query for recent sessions failed for user %s: %v", userIDStr, err), + }) return } defer rows.Close() @@ -547,7 +590,10 @@ func (h *PreferencesHandler) ResetPreferences(c *gin.Context) { `, userIDStr) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reset preferences"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to reset preferences", + "message": fmt.Sprintf("Database delete for user preferences failed for user %s: %v", userIDStr, err), + }) return } diff --git a/api/internal/handlers/quotas.go b/api/internal/handlers/quotas.go index 05696114..b3559b27 100644 --- a/api/internal/handlers/quotas.go +++ b/api/internal/handlers/quotas.go @@ -176,7 +176,10 @@ func (h *QuotasHandler) GetUserQuota(c *gin.Context) { return } if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user quota"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get user quota", + "message": fmt.Sprintf("Database error querying quota for user %s: %v", userID, err), + }) return } @@ -224,7 +227,10 @@ func (h *QuotasHandler) SetUserQuota(c *gin.Context) { `, id, userID, req.MaxSessions, req.MaxCPU, req.MaxMemory, req.MaxStorage) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to set user quota"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to set user quota", + "message": fmt.Sprintf("Database error setting quota for user %s (sessions=%d, cpu=%d, memory=%d, storage=%d): %v", userID, req.MaxSessions, req.MaxCPU, req.MaxMemory, req.MaxStorage, err), + }) return } @@ -249,7 +255,10 @@ func (h *QuotasHandler) DeleteUserQuota(c *gin.Context) { `, userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user quota"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to delete user quota", + "message": fmt.Sprintf("Database error deleting quota for user %s: %v", userID, err), + }) return } @@ -329,7 +338,10 @@ func (h *QuotasHandler) GetUserQuotaStatus(c *gin.Context) { maxMemory = sql.NullInt64{Int64: 8192, Valid: true} maxStorage = sql.NullInt64{Int64: 100, Valid: true} } else if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get quota"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get quota", + "message": fmt.Sprintf("Database error querying quota status for user %s: %v", userID, err), + }) return } @@ -445,7 +457,10 @@ func (h *QuotasHandler) GetTeamQuota(c *gin.Context) { return } if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get team quota"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get team quota", + "message": fmt.Sprintf("Database error querying quota for team %s: %v", teamID, err), + }) return } @@ -493,7 +508,10 @@ func (h *QuotasHandler) SetTeamQuota(c *gin.Context) { `, id, teamID, req.MaxSessions, req.MaxCPU, req.MaxMemory, req.MaxStorage) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to set team quota"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to set team quota", + "message": fmt.Sprintf("Database error setting quota for team %s (sessions=%d, cpu=%d, memory=%d, storage=%d): %v", teamID, req.MaxSessions, req.MaxCPU, req.MaxMemory, req.MaxStorage, err), + }) return } @@ -518,7 +536,10 @@ func (h *QuotasHandler) DeleteTeamQuota(c *gin.Context) { `, teamID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete team quota"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to delete team quota", + "message": fmt.Sprintf("Database error deleting quota for team %s: %v", teamID, err), + }) return } @@ -538,7 +559,10 @@ func (h *QuotasHandler) GetTeamUsage(c *gin.Context) { SELECT user_id FROM group_members WHERE group_id = $1 `, teamID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get team members"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get team members", + "message": fmt.Sprintf("Database error querying members for team %s: %v", teamID, err), + }) return } defer rows.Close() @@ -641,7 +665,10 @@ func (h *QuotasHandler) GetTeamQuotaStatus(c *gin.Context) { maxMemory = sql.NullInt64{Int64: 40960, Valid: true} maxStorage = sql.NullInt64{Int64: 500, Valid: true} } else if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get quota"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get quota", + "message": fmt.Sprintf("Database error querying quota status for team %s: %v", teamID, err), + }) return } @@ -650,7 +677,10 @@ func (h *QuotasHandler) GetTeamQuotaStatus(c *gin.Context) { SELECT user_id FROM group_members WHERE group_id = $1 `, teamID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get team members"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get team members", + "message": fmt.Sprintf("Database error querying members for team %s: %v", teamID, err), + }) return } defer rows.Close() @@ -814,7 +844,10 @@ func (h *QuotasHandler) ListAllQuotas(c *gin.Context) { rows, err := h.db.DB().QueryContext(ctx, query, args...) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list quotas"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to list quotas", + "message": fmt.Sprintf("Database error listing quotas (type=%s): %v", quotaType, err), + }) return } defer rows.Close() @@ -1004,7 +1037,10 @@ func (h *QuotasHandler) GetPolicies(c *gin.Context) { ORDER BY priority DESC, created_at DESC `) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get policies"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get policies", + "message": fmt.Sprintf("Database error listing quota policies: %v", err), + }) return } defer rows.Close() @@ -1060,7 +1096,10 @@ func (h *QuotasHandler) CreatePolicy(c *gin.Context) { `, id, req.Name, req.Description, req.Rules, req.Priority, req.Enabled) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create policy"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create policy", + "message": fmt.Sprintf("Database error creating policy '%s': %v", req.Name, err), + }) return } @@ -1091,7 +1130,10 @@ func (h *QuotasHandler) GetPolicy(c *gin.Context) { return } if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get policy"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get policy", + "message": fmt.Sprintf("Database error getting policy %s: %v", policyID, err), + }) return } @@ -1133,7 +1175,10 @@ func (h *QuotasHandler) UpdatePolicy(c *gin.Context) { `, req.Name, req.Description, req.Rules, req.Priority, req.Enabled, policyID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update policy"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to update policy", + "message": fmt.Sprintf("Database error updating policy %s: %v", policyID, err), + }) return } @@ -1150,7 +1195,10 @@ func (h *QuotasHandler) DeletePolicy(c *gin.Context) { _, err := h.db.DB().ExecContext(ctx, `DELETE FROM quota_policies WHERE id = $1`, policyID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete policy"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to delete policy", + "message": fmt.Sprintf("Database error deleting policy %s: %v", policyID, err), + }) return } diff --git a/api/internal/handlers/scheduling.go b/api/internal/handlers/scheduling.go index 011abcd1..db7621ae 100644 --- a/api/internal/handlers/scheduling.go +++ b/api/internal/handlers/scheduling.go @@ -288,7 +288,10 @@ func (h *SchedulingHandler) CreateScheduledSession(c *gin.Context) { req.NextRunAt, req.Metadata).Scan(&id) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create scheduled session"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create scheduled session", + "message": fmt.Sprintf("Database insert failed for user %s with template %s: %v", userID, req.TemplateID, err), + }) return } @@ -320,7 +323,10 @@ func (h *SchedulingHandler) ListScheduledSessions(c *gin.Context) { rows, err := h.DB.DB().Query(query, userID, role) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to list scheduled sessions", + "message": fmt.Sprintf("Database query failed for user %s: %v", userID, err), + }) return } defer rows.Close() @@ -387,11 +393,17 @@ func (h *SchedulingHandler) GetScheduledSession(c *gin.Context) { &s.CreatedAt, &s.UpdatedAt) if err == sql.ErrNoRows { - c.JSON(http.StatusNotFound, gin.H{"error": "scheduled session not found"}) + c.JSON(http.StatusNotFound, gin.H{ + "error": "Scheduled session not found", + "message": fmt.Sprintf("No scheduled session found with ID %s for user %s", scheduleID, userID), + }) return } if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get scheduled session", + "message": fmt.Sprintf("Database query failed for schedule ID %s: %v", scheduleID, err), + }) return } @@ -427,11 +439,24 @@ func (h *SchedulingHandler) UpdateScheduledSession(c *gin.Context) { var ownerID string err := h.DB.DB().QueryRow(`SELECT user_id FROM scheduled_sessions WHERE id = $1`, scheduleID).Scan(&ownerID) if err == sql.ErrNoRows { - c.JSON(http.StatusNotFound, gin.H{"error": "scheduled session not found"}) + c.JSON(http.StatusNotFound, gin.H{ + "error": "Scheduled session not found", + "message": fmt.Sprintf("No scheduled session found with ID %s", scheduleID), + }) + return + } + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to check ownership", + "message": fmt.Sprintf("Database query failed for schedule ID %s: %v", scheduleID, err), + }) return } if ownerID != userID && role != "admin" { - c.JSON(http.StatusForbidden, gin.H{"error": "access denied"}) + c.JSON(http.StatusForbidden, gin.H{ + "error": "Access denied", + "message": fmt.Sprintf("User %s does not have permission to update schedule %s", userID, scheduleID), + }) return } @@ -464,7 +489,10 @@ func (h *SchedulingHandler) UpdateScheduledSession(c *gin.Context) { req.TerminateAfter, req.PreWarm, req.PreWarmMinutes, req.NextRunAt, scheduleID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update scheduled session"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to update scheduled session", + "message": fmt.Sprintf("Database update failed for schedule ID %s: %v", scheduleID, err), + }) return } @@ -481,17 +509,33 @@ func (h *SchedulingHandler) DeleteScheduledSession(c *gin.Context) { var ownerID string err := h.DB.DB().QueryRow(`SELECT user_id FROM scheduled_sessions WHERE id = $1`, scheduleID).Scan(&ownerID) if err == sql.ErrNoRows { - c.JSON(http.StatusNotFound, gin.H{"error": "scheduled session not found"}) + c.JSON(http.StatusNotFound, gin.H{ + "error": "Scheduled session not found", + "message": fmt.Sprintf("No scheduled session found with ID %s", scheduleID), + }) + return + } + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to check ownership", + "message": fmt.Sprintf("Database query failed for schedule ID %s: %v", scheduleID, err), + }) return } if ownerID != userID && role != "admin" { - c.JSON(http.StatusForbidden, gin.H{"error": "access denied"}) + c.JSON(http.StatusForbidden, gin.H{ + "error": "Access denied", + "message": fmt.Sprintf("User %s does not have permission to delete schedule %s", userID, scheduleID), + }) return } _, err = h.DB.DB().Exec(`DELETE FROM scheduled_sessions WHERE id = $1`, scheduleID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to delete scheduled session", + "message": fmt.Sprintf("Database delete failed for schedule ID %s: %v", scheduleID, err), + }) return } @@ -509,7 +553,10 @@ func (h *SchedulingHandler) EnableScheduledSession(c *gin.Context) { `, scheduleID, userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to enable schedule"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to enable schedule", + "message": fmt.Sprintf("Database update failed for schedule ID %s, user %s: %v", scheduleID, userID, err), + }) return } @@ -527,7 +574,10 @@ func (h *SchedulingHandler) DisableScheduledSession(c *gin.Context) { `, scheduleID, userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to disable schedule"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to disable schedule", + "message": fmt.Sprintf("Database update failed for schedule ID %s, user %s: %v", scheduleID, userID, err), + }) return } @@ -654,7 +704,10 @@ func (h *SchedulingHandler) CalendarOAuthCallback(c *gin.Context) { `, state, provider, email, accessToken, refreshToken, expiry).Scan(&id) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save integration"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to save calendar integration", + "message": fmt.Sprintf("Database insert failed for user %s with provider %s: %v", state, provider, err), + }) return } @@ -677,7 +730,10 @@ func (h *SchedulingHandler) ListCalendarIntegrations(c *gin.Context) { `, userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to list calendar integrations", + "message": fmt.Sprintf("Database query failed for user %s: %v", userID, err), + }) return } defer rows.Close() @@ -721,13 +777,19 @@ func (h *SchedulingHandler) DisconnectCalendar(c *gin.Context) { `, integrationID, userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to disconnect"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to disconnect calendar", + "message": fmt.Sprintf("Database delete failed for integration ID %s, user %s: %v", integrationID, userID, err), + }) return } - rows, _ := result.RowsAffected() - if rows == 0 { - c.JSON(http.StatusNotFound, gin.H{"error": "integration not found"}) + rowsAffected, _ := result.RowsAffected() + if rowsAffected == 0 { + c.JSON(http.StatusNotFound, gin.H{ + "error": "Calendar integration not found", + "message": fmt.Sprintf("No integration found with ID %s for user %s", integrationID, userID), + }) return } @@ -749,7 +811,17 @@ func (h *SchedulingHandler) SyncCalendar(c *gin.Context) { &ci.RefreshToken, &ci.CalendarID) if err == sql.ErrNoRows { - c.JSON(http.StatusNotFound, gin.H{"error": "integration not found"}) + c.JSON(http.StatusNotFound, gin.H{ + "error": "Calendar integration not found", + "message": fmt.Sprintf("No integration found with ID %s for user %s", integrationID, userID), + }) + return + } + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get calendar integration", + "message": fmt.Sprintf("Database query failed for integration ID %s: %v", integrationID, err), + }) return } @@ -786,7 +858,10 @@ func (h *SchedulingHandler) ExportICalendar(c *gin.Context) { `, userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to export calendar", + "message": fmt.Sprintf("Database query failed for user %s scheduled sessions: %v", userID, err), + }) return } defer rows.Close() diff --git a/api/internal/handlers/security.go b/api/internal/handlers/security.go index b794f103..e8e979b3 100644 --- a/api/internal/handlers/security.go +++ b/api/internal/handlers/security.go @@ -322,7 +322,10 @@ func (h *SecurityHandler) SetupMFA(c *gin.Context) { `, userID, req.Type).Scan(&existingID) if err != nil && err != sql.ErrNoRows { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to check existing MFA methods", + "message": fmt.Sprintf("Database query failed for user %s, MFA type %s: %v", userID, req.Type, err), + }) return } @@ -340,7 +343,10 @@ func (h *SecurityHandler) SetupMFA(c *gin.Context) { AccountName: userID, }) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate TOTP secret"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate TOTP secret", + "message": fmt.Sprintf("TOTP generation failed for user %s: %v", userID, err), + }) return } @@ -357,7 +363,10 @@ func (h *SecurityHandler) SetupMFA(c *gin.Context) { `, userID, req.Type, secret, req.PhoneNumber, req.Email).Scan(&mfaID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create MFA method"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create MFA method", + "message": fmt.Sprintf("Database insert failed for user %s, MFA type %s: %v", userID, req.Type, err), + }) return } @@ -404,7 +413,10 @@ func (h *SecurityHandler) VerifyMFASetup(c *gin.Context) { return } if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to retrieve MFA method", + "message": fmt.Sprintf("Database query failed for MFA ID %s, user %s: %v", mfaID, userID, err), + }) return } @@ -423,7 +435,10 @@ func (h *SecurityHandler) VerifyMFASetup(c *gin.Context) { // Either both MFA enable AND backup codes succeed, or neither tx, err := h.DB.DB().Begin() if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to start database transaction", + "message": fmt.Sprintf("Transaction begin failed for MFA setup verification, user %s: %v", userID, err), + }) return } defer tx.Rollback() // Rollback if not committed @@ -436,7 +451,10 @@ func (h *SecurityHandler) VerifyMFASetup(c *gin.Context) { `, mfaID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to enable MFA"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to enable MFA", + "message": fmt.Sprintf("Database update failed for MFA ID %s, user %s: %v", mfaID, userID, err), + }) return } @@ -456,14 +474,20 @@ func (h *SecurityHandler) VerifyMFASetup(c *gin.Context) { `, userID, hashStr) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate backup codes"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate backup codes", + "message": fmt.Sprintf("Database insert failed for backup code %d of %d, user %s: %v", i+1, BackupCodesCount, userID, err), + }) return } } // Commit transaction if err := tx.Commit(); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to commit changes"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to commit MFA setup changes", + "message": fmt.Sprintf("Transaction commit failed for MFA ID %s, user %s: %v", mfaID, userID, err), + }) return } @@ -569,7 +593,10 @@ func (h *SecurityHandler) VerifyMFA(c *gin.Context) { return } if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to retrieve MFA secret", + "message": fmt.Sprintf("Database query failed for user %s, method type %s: %v", userID, req.MethodType, err), + }) return } @@ -617,7 +644,10 @@ func (h *SecurityHandler) ListMFAMethods(c *gin.Context) { `, userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to list MFA methods", + "message": fmt.Sprintf("Database query failed for user %s: %v", userID, err), + }) return } defer rows.Close() @@ -659,7 +689,10 @@ func (h *SecurityHandler) DisableMFA(c *gin.Context) { `, mfaID, userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to disable MFA"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to disable MFA", + "message": fmt.Sprintf("Database update failed for MFA ID %s, user %s: %v", mfaID, userID, err), + }) return } @@ -801,7 +834,10 @@ func (h *SecurityHandler) CreateIPWhitelist(c *gin.Context) { `, req.UserID, req.IPAddress, req.Description, createdBy, req.ExpiresAt).Scan(&id) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create IP whitelist entry"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create IP whitelist entry", + "message": fmt.Sprintf("Database insert failed for IP %s, created by %s: %v", req.IPAddress, createdBy, err), + }) return } @@ -894,7 +930,10 @@ func (h *SecurityHandler) ListIPWhitelist(c *gin.Context) { rows, err := h.DB.DB().Query(query, userID, role) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to list IP whitelist entries", + "message": fmt.Sprintf("Database query failed for user %s, role %s: %v", userID, role, err), + }) return } defer rows.Close() @@ -976,7 +1015,10 @@ func (h *SecurityHandler) DeleteIPWhitelist(c *gin.Context) { } if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete entry"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to delete IP whitelist entry", + "message": fmt.Sprintf("Database delete failed for entry ID %s, user %s: %v", entryID, userID, err), + }) return } @@ -1059,7 +1101,10 @@ func (h *SecurityHandler) VerifySession(c *gin.Context) { `, sessionID, userID, deviceID, ipAddress, riskScore, riskLevel, verified).Scan(&verificationID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to record verification"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to record session verification", + "message": fmt.Sprintf("Database insert failed for session %s, user %s, IP %s: %v", sessionID, userID, ipAddress, err), + }) return } @@ -1126,7 +1171,10 @@ func (h *SecurityHandler) GetSecurityAlerts(c *gin.Context) { `, userID) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to retrieve security alerts", + "message": fmt.Sprintf("Database query failed for user %s: %v", userID, err), + }) return } defer rows.Close()