package handlers import ( "database/sql" "encoding/json" "fmt" "net/http" "strings" "aamos-admin/models" ) var moduleOrder = []string{ "academy", "finance", "compliance", "people", "operations", "commerce", "identity", "connect", "governance", "analytics", } var moduleSeed = []models.Module{ {ID: "academy", Name: "Academy", Description: "Learning & Training Platform", Icon: "graduation-cap", Status: models.ModuleStatusStopped}, {ID: "finance", Name: "Finance", Description: "Financial Management & Reporting", Icon: "chart-line", Status: models.ModuleStatusStopped}, {ID: "compliance", Name: "Compliance", Description: "Regulatory Compliance & Auditing", Icon: "shield-check", Status: models.ModuleStatusStopped}, {ID: "people", Name: "People", Description: "HR & People Management", Icon: "users", Status: models.ModuleStatusStopped}, {ID: "operations", Name: "Operations", Description: "Operations & Process Management", Icon: "cogs", Status: models.ModuleStatusStopped}, {ID: "commerce", Name: "Commerce", Description: "Sales & Commerce Platform", Icon: "shopping-cart", Status: models.ModuleStatusStopped}, {ID: "identity", Name: "Identity", Description: "Identity & Access Management", Icon: "fingerprint", Status: models.ModuleStatusStopped}, {ID: "connect", Name: "Connect", Description: "Communications & Integration Hub", Icon: "network-wired", Status: models.ModuleStatusStopped}, {ID: "governance", Name: "Governance", Description: "Corporate Governance & Policy", Icon: "landmark", Status: models.ModuleStatusStopped}, {ID: "analytics", Name: "Analytics", Description: "Data Analytics & Business Intelligence", Icon: "chart-bar", Status: models.ModuleStatusStopped}, } // ModulesHandler handles GET /api/v1/modules and PUT /api/v1/modules/{id}/toggle. type ModulesHandler struct { db *sql.DB } // NewModulesHandler seeds the 10 Ouroboros modules (idempotent) and returns the handler. func NewModulesHandler(db *sql.DB) (*ModulesHandler, error) { h := &ModulesHandler{db: db} if err := h.seed(); err != nil { return nil, fmt.Errorf("modules seed: %w", err) } return h, nil } func (h *ModulesHandler) seed() error { for _, m := range moduleSeed { _, err := h.db.Exec( `INSERT INTO modules (id, name, enabled, description, icon, status) VALUES ($1, $2, FALSE, $3, $4, $5) ON CONFLICT (id) DO NOTHING`, m.ID, m.Name, m.Description, m.Icon, string(m.Status), ) if err != nil { return fmt.Errorf("seed %s: %w", m.ID, err) } } return nil } // List handles GET /api/v1/modules — returns all 10 modules in canonical Ouroboros order. func (h *ModulesHandler) List(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } rows, err := h.db.Query( `SELECT id, name, enabled, description, icon, status FROM modules`, ) if err != nil { http.Error(w, fmt.Sprintf("db query: %v", err), http.StatusInternalServerError) return } defer rows.Close() byID := make(map[string]models.Module, len(moduleOrder)) for rows.Next() { var m models.Module var status string if err := rows.Scan(&m.ID, &m.Name, &m.Enabled, &m.Description, &m.Icon, &status); err != nil { http.Error(w, fmt.Sprintf("db scan: %v", err), http.StatusInternalServerError) return } m.Status = models.ModuleStatus(status) byID[m.ID] = m } if err := rows.Err(); err != nil { http.Error(w, fmt.Sprintf("db rows: %v", err), http.StatusInternalServerError) return } result := make([]models.Module, 0, len(moduleOrder)) for _, id := range moduleOrder { if m, ok := byID[id]; ok { result = append(result, m) } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) } // Toggle handles PUT /api/v1/modules/{id}/toggle — flips enabled and syncs status. func (h *ModulesHandler) Toggle(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPut { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } // Extract {id} from /api/v1/modules/{id}/toggle parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") if len(parts) < 2 || parts[len(parts)-1] != "toggle" { http.Error(w, "invalid path", http.StatusBadRequest) return } id := parts[len(parts)-2] if id == "" || id == "modules" { http.Error(w, "missing module id", http.StatusBadRequest) return } // SET expressions use original row values, so NOT enabled correctly toggles: // false → true + 'running', true → false + 'stopped'. var m models.Module var status string err := h.db.QueryRow( `UPDATE modules SET enabled = NOT enabled, status = CASE WHEN NOT enabled THEN $2 ELSE $3 END WHERE id = $1 RETURNING id, name, enabled, description, icon, status`, id, string(models.ModuleStatusRunning), string(models.ModuleStatusStopped), ).Scan(&m.ID, &m.Name, &m.Enabled, &m.Description, &m.Icon, &status) if err == sql.ErrNoRows { http.Error(w, "module not found", http.StatusNotFound) return } if err != nil { http.Error(w, fmt.Sprintf("db update: %v", err), http.StatusInternalServerError) return } m.Status = models.ModuleStatus(status) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(m) }