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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 97 additions & 1 deletion internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package database
import (
"database/sql"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/google/uuid"
_ "github.com/mattn/go-sqlite3"
Expand Down Expand Up @@ -108,7 +110,7 @@ func (db *Database) initTables() error {
createServicesTable := `
CREATE TABLE IF NOT EXISTS services (
id TEXT PRIMARY KEY, -- UUID
name TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
dir TEXT NOT NULL,
extra_env TEXT,
java_opts TEXT,
Expand Down Expand Up @@ -342,6 +344,11 @@ func (db *Database) initTables() error {
return fmt.Errorf("failed to migrate services to UUID: %w", err)
}

// Remove global UNIQUE constraint on service names to allow profile-scoped uniqueness
if err := db.migrateRemoveServiceNameUniqueConstraint(); err != nil {
return fmt.Errorf("failed to migrate service name constraint: %w", err)
}

return nil
}

Expand Down Expand Up @@ -381,6 +388,95 @@ func (db *Database) migrateServicesToUUID() error {
return rows.Err()
}

// migrateRemoveServiceNameUniqueConstraint removes the global UNIQUE constraint on service names
// to allow profile-scoped uniqueness instead of global uniqueness
func (db *Database) migrateRemoveServiceNameUniqueConstraint() error {
// First, check if the services table has the UNIQUE constraint on name
var hasUniqueConstraint bool

// Query the table schema to check for UNIQUE constraint
var sql string
err := db.QueryRow("SELECT sql FROM sqlite_master WHERE type='table' AND name='services'").Scan(&sql)
if err != nil {
return fmt.Errorf("failed to get services table schema: %w", err)
}

// Check if the schema contains "name TEXT UNIQUE"
hasUniqueConstraint = strings.Contains(sql, "name TEXT UNIQUE")

if !hasUniqueConstraint {
// Migration already applied or not needed
return nil
}

log.Println("[INFO] Migrating services table to remove global UNIQUE constraint on name")

// Begin transaction
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()

// Create new services table without UNIQUE constraint
createNewServicesTable := `
CREATE TABLE services_new (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
dir TEXT NOT NULL,
extra_env TEXT,
java_opts TEXT,
status TEXT DEFAULT 'stopped',
health_status TEXT DEFAULT 'unknown',
health_url TEXT,
port INTEGER,
pid INTEGER DEFAULT 0,
service_order INTEGER,
last_started DATETIME,
description TEXT,
is_enabled BOOLEAN DEFAULT TRUE,
build_system TEXT DEFAULT 'auto',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);`

if _, err := tx.Exec(createNewServicesTable); err != nil {
return fmt.Errorf("failed to create new services table: %w", err)
}

// Copy data from old table to new table
copyData := `
INSERT INTO services_new (id, name, dir, extra_env, java_opts, status, health_status,
health_url, port, pid, service_order, last_started, description, is_enabled,
build_system, created_at, updated_at)
SELECT id, name, dir, extra_env, java_opts, status, health_status,
health_url, port, pid, service_order, last_started, description, is_enabled,
build_system, created_at, updated_at
FROM services;`

if _, err := tx.Exec(copyData); err != nil {
return fmt.Errorf("failed to copy services data: %w", err)
}

// Drop the old table
if _, err := tx.Exec("DROP TABLE services"); err != nil {
return fmt.Errorf("failed to drop old services table: %w", err)
}

// Rename new table to original name
if _, err := tx.Exec("ALTER TABLE services_new RENAME TO services"); err != nil {
return fmt.Errorf("failed to rename new services table: %w", err)
}

// Commit transaction
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit migration transaction: %w", err)
}

log.Println("[INFO] Successfully migrated services table - service names are now profile-scoped")
return nil
}

// GetGlobalEnvVars retrieves all global environment variables
func (db *Database) GetGlobalEnvVars() (map[string]string, error) {
query := `SELECT var_name, var_value FROM global_env_vars ORDER BY var_name`
Expand Down
120 changes: 116 additions & 4 deletions internal/handlers/service_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,35 @@ func (h *Handler) getLogsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")

// Check authentication
claims, ok := extractClaimsFromRequest(r, h.authService)
if !ok || claims == nil {
http.Error(w, "Authentication required", http.StatusUnauthorized)
return
}

// Get user's active profile
profile, err := h.profileService.GetActiveProfile(claims.UserID)
if err != nil {
log.Printf("[ERROR] Failed to get active profile for logs: %v", err)
http.Error(w, "Failed to get active profile", http.StatusInternalServerError)
return
}

// Check if the service belongs to the current profile
serviceInProfile := false
for _, profileServiceID := range profile.Services {
if profileServiceID == serviceUUID {
serviceInProfile = true
break
}
}

if !serviceInProfile {
http.Error(w, "Service not found in current profile", http.StatusForbidden)
return
}

service, exists := h.serviceManager.GetServiceByUUID(serviceUUID)
if !exists {
http.Error(w, "Service not found", http.StatusNotFound)
Expand All @@ -628,6 +657,35 @@ func (h *Handler) clearLogsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")

// Check authentication
claims, ok := extractClaimsFromRequest(r, h.authService)
if !ok || claims == nil {
http.Error(w, "Authentication required", http.StatusUnauthorized)
return
}

// Get user's active profile
profile, err := h.profileService.GetActiveProfile(claims.UserID)
if err != nil {
log.Printf("[ERROR] Failed to get active profile for clear logs: %v", err)
http.Error(w, "Failed to get active profile", http.StatusInternalServerError)
return
}

// Check if the service belongs to the current profile
serviceInProfile := false
for _, profileServiceID := range profile.Services {
if profileServiceID == serviceUUID {
serviceInProfile = true
break
}
}

if !serviceInProfile {
http.Error(w, "Service not found in current profile", http.StatusForbidden)
return
}

service, exists := h.serviceManager.GetServiceByUUID(serviceUUID)
if !exists {
http.Error(w, fmt.Sprintf("Service '%s' not found", serviceUUID), http.StatusNotFound)
Expand All @@ -649,16 +707,70 @@ func (h *Handler) clearAllLogsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")

// Check authentication
claims, ok := extractClaimsFromRequest(r, h.authService)
if !ok || claims == nil {
http.Error(w, "Authentication required", http.StatusUnauthorized)
return
}

// Get user's active profile
profile, err := h.profileService.GetActiveProfile(claims.UserID)
if err != nil {
log.Printf("[ERROR] Failed to get active profile for clear all logs: %v", err)
http.Error(w, "Failed to get active profile", http.StatusInternalServerError)
return
}

var request struct {
ServiceNames []string `json:"serviceNames,omitempty"`
}

if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
// If no body or invalid JSON, clear all logs
// If no body or invalid JSON, clear logs for all services in the profile
request.ServiceNames = []string{}
}

results := h.serviceManager.ClearAllLogs(request.ServiceNames)
// If no specific services requested, use all services from the current profile
// If specific services requested, filter them to only include those in the current profile
var targetServiceIDs []string

if len(request.ServiceNames) == 0 {
// Clear logs for all services in the current profile
targetServiceIDs = profile.Services
} else {
// Filter requested service names to only include those in the current profile
profileServiceMap := make(map[string]bool)
for _, serviceID := range profile.Services {
profileServiceMap[serviceID] = true
}

// Convert service names to service IDs and filter by profile
allServices := h.serviceManager.GetServices()
for _, service := range allServices {
// Check if this service name was requested AND is in the current profile
for _, requestedName := range request.ServiceNames {
if service.Name == requestedName && profileServiceMap[service.ID] {
targetServiceIDs = append(targetServiceIDs, service.ID)
break
}
}
}
}

// Convert service IDs back to names for the ClearAllLogs function
var targetServiceNames []string
allServices := h.serviceManager.GetServices()
for _, serviceID := range targetServiceIDs {
for _, service := range allServices {
if service.ID == serviceID {
targetServiceNames = append(targetServiceNames, service.Name)
break
}
}
}

results := h.serviceManager.ClearAllLogs(targetServiceNames)

successCount := 0
errorCount := 0
Expand All @@ -678,9 +790,9 @@ func (h *Handler) clearAllLogsHandler(w http.ResponseWriter, r *http.Request) {
}

if len(request.ServiceNames) == 0 {
response["message"] = fmt.Sprintf("Cleared logs for all %d services", successCount)
response["message"] = fmt.Sprintf("Cleared logs for all %d services in current profile", successCount)
} else {
response["message"] = fmt.Sprintf("Cleared logs for %d of %d specified services", successCount, len(request.ServiceNames))
response["message"] = fmt.Sprintf("Cleared logs for %d of %d specified services in current profile", successCount, len(request.ServiceNames))
}

json.NewEncoder(w).Encode(response)
Expand Down
30 changes: 28 additions & 2 deletions internal/handlers/uptime_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,43 @@ func registerUptimeRoutes(h *Handler, r *mux.Router) {
r.HandleFunc("/api/uptime/statistics/{id}", h.getServiceUptimeStatisticsHandler).Methods("GET")
}

// getUptimeStatisticsHandler returns uptime statistics for all services
// getUptimeStatisticsHandler returns uptime statistics for services in the current active profile
func (h *Handler) getUptimeStatisticsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")

// Get user from JWT token to determine active profile
claims, ok := extractClaimsFromRequest(r, h.authService)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}

uptimeTracker := services.GetUptimeTracker()
allStats := uptimeTracker.GetAllUptimeStats()

// Get services from active profile, fallback to all services if no active profile
var services []models.Service
activeProfile, err := h.profileService.GetActiveProfile(claims.UserID)
if err != nil || activeProfile == nil {
// No active profile, show all services (fallback for backward compatibility)
services = h.serviceManager.GetServices()
} else {
// Filter services by active profile
allServices := h.serviceManager.GetServices()
for _, service := range allServices {
// Check if service is in the active profile
for _, serviceUUID := range activeProfile.Services {
if service.ID == serviceUUID {
services = append(services, service)
break
}
}
}
}

// Enhance with service names
serviceStats := make(map[string]interface{})
services := h.serviceManager.GetServices()

for _, service := range services {
if stats, exists := allStats[service.ID]; exists {
Expand Down
Loading