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/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() 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);