lblt 1 dag sedan
förälder
incheckning
b2751aefb6
47 ändrade filer med 2464 tillägg och 3080 borttagningar
  1. 0 24
      analyzer/stacks/nodejs/nodesjs_test.go
  2. 0 39
      analyzer/stacks/python/python_test.go
  3. 6 2
      app/init.go
  4. 1 0
      app/server.go
  5. 9 0
      buildkitd.toml
  6. BIN
      byop-engine
  7. BIN
      byop.db
  8. 11 6
      clients/buildkit.go
  9. 14 14
      clients/buildkit_test.go
  10. 175 3
      clients/dockerfile_builder.go
  11. 0 3
      cloud/aws.go
  12. 0 3
      cloud/digitalocean.go
  13. 72 171
      dbstore/apps.go
  14. 58 281
      dbstore/build_jobs.go
  15. 33 65
      dbstore/clients.go
  16. 59 154
      dbstore/components.go
  17. 54 187
      dbstore/deployments.go
  18. 93 314
      dbstore/preview.go
  19. 98 327
      dbstore/store.go
  20. 35 83
      dbstore/tickets.go
  21. 62 108
      dbstore/users.go
  22. 39 0
      debug_buildkit.go
  23. 0 0
      docs/git_deployment.md
  24. 0 133
      docs/golang-analyzer-testing.md
  25. 0 0
      docs/ovh_git_deployment.md
  26. 3 3
      go.mod
  27. 146 38
      handlers/apps.go
  28. 14 26
      handlers/clients.go
  29. 24 31
      handlers/components.go
  30. 26 42
      handlers/deployments.go
  31. 4 13
      handlers/preview.go
  32. 26 32
      handlers/tickets.go
  33. 14 19
      handlers/users.go
  34. 15 12
      models/build.go
  35. 95 90
      models/common.go
  36. 35 0
      models/compose.go
  37. 66 0
      scripts/buildkit-daemon.sh
  38. 0 143
      scripts/test-fixes.sh
  39. 0 36
      scripts/test-golang-analyzer.sh
  40. 284 0
      services/app_importer.go
  41. 155 28
      services/builder.go
  42. 195 0
      services/compose_parser.go
  43. 87 8
      services/local_preview.go
  44. 416 534
      services/preview_common.go
  45. 32 0
      services/preview_factory.go
  46. 0 98
      services/preview_manager.go
  47. 8 10
      services/remote_preview.go

+ 0 - 24
analyzer/stacks/nodejs/nodesjs_test.go

@@ -1,24 +0,0 @@
-package nodejs
-
-import "testing"
-
-func TestNodejs(t *testing.T) {
-	// Create a new NodeJS stack instance
-	nodejsStack := &NodeJS{}
-
-	// Test the Name method
-	expectedName := "Node.js"
-	if nodejsStack.Name() != expectedName {
-		t.Errorf("Expected name %s, got %s", expectedName, nodejsStack.Name())
-	}
-
-	// Test the Analyze method with a sample codebase path
-	codebasePath := "/path/to/nodejs/project"
-	stackName, err := nodejsStack.Analyze(codebasePath)
-	if err != nil {
-		t.Errorf("Unexpected error during analysis: %v", err)
-	}
-	if stackName != expectedName {
-		t.Errorf("Expected stack name %s, got %s", expectedName, stackName)
-	}
-}

+ 0 - 39
analyzer/stacks/python/python_test.go

@@ -1,39 +0,0 @@
-package python
-
-import "testing"
-
-// TestPython tests the Python stack analysis functionality.
-// It checks if the Name method returns the correct stack name
-func TestPython(t *testing.T) {
-	// Create a new Python stack instance
-	pythonStack := &Python{}
-
-	// Test the Name method
-	expectedName := "Python"
-	if pythonStack.Name() != expectedName {
-		t.Errorf("Expected name %s, got %s", expectedName, pythonStack.Name())
-	}
-
-	// Test the Analyze method with a sample codebase path
-	codebasePath := "/path/to/python/project"
-	stackName, err := pythonStack.Analyze(codebasePath)
-	if err != nil {
-		t.Errorf("Unexpected error during analysis: %v", err)
-	}
-	if stackName != expectedName {
-		t.Errorf("Expected stack name %s, got %s", expectedName, stackName)
-	}
-}
-
-// TestPythonAnalyzeError tests the Analyze method for error handling.
-func TestPythonAnalyzeError(t *testing.T) {
-	// Create a new Python stack instance
-	pythonStack := &Python{}
-
-	// Test the Analyze method with an invalid codebase path
-	invalidPath := "/invalid/path/to/python/project"
-	_, err := pythonStack.Analyze(invalidPath)
-	if err == nil {
-		t.Error("Expected an error for invalid codebase path, got nil")
-	}
-}

+ 6 - 2
app/init.go

@@ -55,8 +55,9 @@ func (a *App) initCommonServices() error {
 
 	// Get Ovh provider
 	ovhProvider, _ := cloud.GetProvider("ovh")
-	a.previewService = services.NewPreviewServiceManager(a.database, ovhProvider, a.cnf.LocalPreview, a.cnf, a.registryClient, a.cnf.ReistryUrl, "", "")
+	a.previewService = services.NewPreviewService(a.database, ovhProvider, a.cnf, a.registryClient, a.cnf.ReistryUrl, "", "")
 	a.builderService = services.NewBuilderService(a.database, a.buildkitClient, a.registryClient, 5)
+	a.appImporter = services.NewAppImporter(a.database, a.builderService, a.cnf.ReistryUrl)
 
 	a.entry.Info("Services initialized successfully, including authentication and database manager")
 	return nil
@@ -73,7 +74,7 @@ func (a *App) initHandlers() error {
 	a.componentHandler = handlers.NewComponentHandler(a.database, a.builderService, a.cnf.ReistryUrl)
 
 	// Initialize AppModule
-	a.appHandler = handlers.NewAppsHandler(a.database, a.previewService)
+	a.appHandler = handlers.NewAppsHandler(a.database, a.previewService, a.appImporter)
 
 	// Initialize DeploymentModule
 	a.deploymentHandler = handlers.NewDeploymentHandler(a.database)
@@ -190,6 +191,9 @@ func (a *App) setupRoutes() {
 	apps.POST("/:id/preview", a.appHandler.CreateAppPreview)
 	apps.GET("/:id/preview", a.appHandler.GetAppPreview)
 	apps.DELETE("/:id/preview", a.appHandler.DeleteAppPreview)
+	// App import routes
+	apps.POST("/import/review", a.appHandler.ImportReview)
+	apps.POST("/import/create", a.appHandler.ImportCreate)
 
 	// Deployment routes - need to handle both versions
 	deployments := protected.Group("/deployments")

+ 1 - 0
app/server.go

@@ -48,6 +48,7 @@ type App struct {
 	// Preview Service
 	previewService services.PreviewService
 	builderService *services.Builder
+	appImporter    *services.AppImporter
 
 	// Resource Handlers
 	providerHandler *handlers.ProviderHandler

+ 9 - 0
buildkitd.toml

@@ -0,0 +1,9 @@
+# BuildKit configuration for insecure registries
+
+[registry."host.docker.internal:5000"]
+  http = true
+  insecure = true
+
+[registry."localhost:5000"]
+  http = true
+  insecure = true

BIN
byop-engine


BIN
byop.db


+ 11 - 6
clients/buildkit.go

@@ -129,11 +129,11 @@ func (bkc *BuildKitClient) BuildImage(ctx context.Context, job models.BuildJob,
 			Password: job.RegistryPassword,
 		}
 		normalizedRegURL := job.RegistryURL
-		if job.RegistryURL == "docker.io" || job.RegistryURL == "" { // Docker Hub
-			normalizedRegURL = "https://index.docker.io/v1/"
-		} else if !strings.HasPrefix(job.RegistryURL, "http://") && !strings.HasPrefix(job.RegistryURL, "https://") {
-			normalizedRegURL = "https://" + job.RegistryURL
-		}
+		// if job.RegistryURL == "docker.io" || job.RegistryURL == "" { // Docker Hub
+		// 	normalizedRegURL = "https://index.docker.io/v1/"
+		// } else if !strings.HasPrefix(job.RegistryURL, "http://") && !strings.HasPrefix(job.RegistryURL, "https://") {
+		// 	normalizedRegURL = "http://" + job.RegistryURL
+		// }
 
 		specificAuthCfgFile := configfile.New("") // Create an empty config file object
 		specificAuthCfgFile.AuthConfigs[normalizedRegURL] = regAuthConfigValue
@@ -190,7 +190,12 @@ func (bkc *BuildKitClient) CheckImageExists(ctx context.Context, fullImageURI st
 		if registryURL == "docker.io" || registryURL == "" {
 			serverAddress = "https://index.docker.io/v1/"
 		} else if !strings.HasPrefix(registryURL, "http://") && !strings.HasPrefix(registryURL, "https://") {
-			serverAddress = "https://" + registryURL
+			// For local registries (localhost, host.docker.internal), use HTTP
+			if strings.Contains(registryURL, "localhost") || strings.Contains(registryURL, "host.docker.internal") {
+				serverAddress = "http://" + registryURL
+			} else {
+				serverAddress = "https://" + registryURL
+			}
 		}
 
 		cfgInMemory := &configfile.ConfigFile{

+ 14 - 14
clients/buildkit_test.go

@@ -9,8 +9,8 @@ import (
 
 // TestBuildingImageRemoteOnly focuses on remote git builds which work without special entitlements
 func TestBuildingImageRemoteOnly(t *testing.T) {
-	buildkitHost := "tcp://localhost:1234"
-	bkc := NewBuildKitClient(buildkitHost)
+	buildkitHost := "tcp://127.0.0.1:1234"
+	bkc := NewDockerfileBuilder(buildkitHost)
 	ctx := context.Background()
 
 	t.Log("Testing remote git builds (no local entitlements required)")
@@ -20,10 +20,10 @@ func TestBuildingImageRemoteOnly(t *testing.T) {
 		ID:           1,
 		ComponentID:  1,
 		Version:      "master", // Use master branch, not main
-		RegistryURL:  "localhost:5000",
+		RegistryURL:  "host.docker.internal:5000",
 		ImageName:    "hello-world-remote",
 		ImageTag:     "latest",
-		FullImageURI: "localhost:5000/hello-world-remote:latest",
+		FullImageURI: "host.docker.internal:5000/hello-world-remote:latest",
 		SourceURL:    "https://github.com/crccheck/docker-hello-world.git",
 		BuildContext: ".",          // Root of repository
 		Dockerfile:   "Dockerfile", // Relative to build context
@@ -60,10 +60,10 @@ func TestBuildingImageRemoteOnly(t *testing.T) {
 		ID:           2,
 		ComponentID:  1,
 		Version:      "master",
-		RegistryURL:  "localhost:5000",
+		RegistryURL:  "host.docker.internal:5000",
 		ImageName:    "alpine-test",
 		ImageTag:     "latest",
-		FullImageURI: "localhost:5000/alpine-test:latest",
+		FullImageURI: "host.docker.internal:5000/alpine-test:latest",
 		SourceURL:    "https://github.com/jdkelley/simple-http-server.git",
 		BuildContext: ".", // Root of repository
 		Dockerfile:   "Dockerfile",
@@ -100,8 +100,8 @@ func TestBuildingImageRemoteOnly(t *testing.T) {
 
 // Test with a simple working repository that's guaranteed to have a Dockerfile
 func TestBuildingImageGuaranteedWorking(t *testing.T) {
-	buildkitHost := "tcp://localhost:1234"
-	bkc := NewBuildKitClient(buildkitHost)
+	buildkitHost := "tcp://127.0.0.1:1234"
+	bkc := NewDockerfileBuilder(buildkitHost)
 	ctx := context.Background()
 
 	// Use a simple Node.js app that definitely has a Dockerfile
@@ -109,10 +109,10 @@ func TestBuildingImageGuaranteedWorking(t *testing.T) {
 		ID:           3,
 		ComponentID:  1,
 		Version:      "master",
-		RegistryURL:  "localhost:5000",
+		RegistryURL:  "host.docker.internal:5000",
 		ImageName:    "node-hello",
 		ImageTag:     "latest",
-		FullImageURI: "localhost:5000/node-hello:latest",
+		FullImageURI: "host.docker.internal:5000/node-hello:latest",
 		SourceURL:    "https://github.com/docker/welcome-to-docker.git",
 		BuildContext: ".",
 		Dockerfile:   "Dockerfile",
@@ -162,8 +162,8 @@ func TestBuildingImageGuaranteedWorking(t *testing.T) {
 
 // Minimal test with a repository we know works
 func TestMinimalWorkingBuild(t *testing.T) {
-	buildkitHost := "tcp://localhost:1234"
-	bkc := NewBuildKitClient(buildkitHost)
+	buildkitHost := "tcp://127.0.0.1:1234"
+	bkc := NewDockerfileBuilder(buildkitHost)
 	ctx := context.Background()
 
 	// Test with the original repository from your test but with master branch
@@ -171,10 +171,10 @@ func TestMinimalWorkingBuild(t *testing.T) {
 		ID:           4,
 		ComponentID:  1,
 		Version:      "master", // Correct branch
-		RegistryURL:  "localhost:5000",
+		RegistryURL:  "host.docker.internal:5000",
 		ImageName:    "simple-http",
 		ImageTag:     "latest",
-		FullImageURI: "localhost:5000/simple-http:latest",
+		FullImageURI: "host.docker.internal:5000/simple-http:latest",
 		SourceURL:    "https://github.com/Guy-Incognito/simple-http-server.git",
 		BuildContext: ".",
 		Dockerfile:   "Dockerfile",

+ 175 - 3
clients/dockerfile_builder.go

@@ -282,10 +282,36 @@ func (db *DockerfileBuilder) PushImage(ctx context.Context, job models.BuildJob,
 			authprovider.NewDockerAuthProvider(authConfig),
 		}
 	}
-
 	ch := make(chan *client.SolveStatus)
-	_, err = c.Solve(ctx, nil, *solveOpt, ch)
-	if err != nil {
+	eg, ctx := errgroup.WithContext(ctx)
+
+	// Process solve status updates
+	eg.Go(func() error {
+		for status := range ch {
+			// Log progress if needed
+			for _, vertex := range status.Vertexes {
+				if vertex.Completed != nil {
+					db.entry.Debugf("Job %d: Vertex %s completed", job.ID, vertex.Name)
+				}
+			}
+		}
+		db.entry.Infof("Job %d: Solve status channel closed", job.ID)
+		return nil
+	})
+
+	// Execute the solve
+	eg.Go(func() error {
+		db.entry.Infof("Job %d: Starting BuildKit solve for push", job.ID)
+		_, err := c.Solve(ctx, nil, *solveOpt, ch)
+		if err != nil {
+			db.entry.Errorf("Job %d: BuildKit solve failed: %v", job.ID, err)
+		} else {
+			db.entry.Infof("Job %d: BuildKit solve completed successfully", job.ID)
+		}
+		return err
+	})
+
+	if err := eg.Wait(); err != nil {
 		return fmt.Errorf("job %d: failed to push image: %w", job.ID, err)
 	}
 
@@ -311,3 +337,149 @@ func (db *DockerfileBuilder) Close() error {
 	db.entry.Info("DockerfileBuilder closed")
 	return nil
 }
+
+// BuildImageWithPush builds and pushes an image in one operation for efficiency
+func (db *DockerfileBuilder) BuildImageWithPush(ctx context.Context, job models.BuildJob, dockerfilePath string, contextPath string, imageName string, imageTag string, noCache bool, buildArgs map[string]string, fullImageURI string, registryURL string, username string, password string) (string, error) {
+	db.entry.Infof("Job %d: Building and pushing image %s:%s using combined operation", job.ID, imageName, imageTag)
+
+	c, err := client.New(ctx, db.buildkitHost)
+	if err != nil {
+		return "", fmt.Errorf("job %d: failed to create BuildKit client: %w", job.ID, err)
+	}
+	defer c.Close()
+
+	// If we have generated Dockerfile content, write it to the build context
+	if job.DockerfileContent != "" {
+		dockerfilePath = filepath.Join(contextPath, "Dockerfile")
+		if err := os.WriteFile(dockerfilePath, []byte(job.DockerfileContent), 0644); err != nil {
+			return "", fmt.Errorf("job %d: failed to write generated Dockerfile: %w", job.ID, err)
+		}
+		db.entry.Infof("Job %d: Wrote generated Dockerfile to %s", job.ID, dockerfilePath)
+	}
+
+	solveOpt, err := db.newSolveOptWithPush(ctx, job, contextPath, dockerfilePath, fullImageURI, noCache, buildArgs, registryURL, username, password)
+	if err != nil {
+		return "", fmt.Errorf("job %d: failed to create solve options: %w", job.ID, err)
+	}
+
+	ch := make(chan *client.SolveStatus)
+	eg, gctx := errgroup.WithContext(ctx)
+
+	var buildOutput strings.Builder
+	var solveResp *client.SolveResponse
+
+	// Start the build
+	eg.Go(func() error {
+		var err error
+		solveResp, err = c.Solve(gctx, nil, *solveOpt, ch)
+		if err != nil {
+			return fmt.Errorf("BuildKit solve failed: %w", err)
+		}
+		return nil
+	})
+
+	// Collect build output
+	eg.Go(func() error {
+		for status := range ch {
+			for _, v := range status.Vertexes {
+				if v.Error != "" {
+					buildOutput.WriteString(fmt.Sprintf("Vertex Error: %s: %s\n", v.Name, v.Error))
+				}
+			}
+			for _, l := range status.Logs {
+				buildOutput.Write(l.Data)
+			}
+		}
+		return nil
+	})
+
+	if err := eg.Wait(); err != nil {
+		db.entry.Errorf("Job %d: Build and push failed: %v. Output:\n%s", job.ID, err, buildOutput.String())
+		return buildOutput.String(), fmt.Errorf("build and push failed: %w", err)
+	}
+
+	db.entry.Infof("Job %d: Image %s built and pushed successfully", job.ID, fullImageURI)
+
+	// Return digest if available
+	if solveResp != nil && solveResp.ExporterResponse != nil {
+		if digest, ok := solveResp.ExporterResponse["containerimage.digest"]; ok {
+			return digest, nil
+		}
+	}
+
+	return buildOutput.String(), nil
+}
+
+// newSolveOptWithPush creates solve options for combined build and push
+func (db *DockerfileBuilder) newSolveOptWithPush(ctx context.Context, job models.BuildJob, buildContext, dockerfilePath, fullImageURI string, noCache bool, buildArgs map[string]string, registryURL, username, password string) (*client.SolveOpt, error) {
+	if buildContext == "" {
+		return nil, fmt.Errorf("build context cannot be empty")
+	}
+
+	if dockerfilePath == "" {
+		dockerfilePath = filepath.Join(buildContext, "Dockerfile")
+	}
+
+	// Create filesystem for build context
+	contextFS, err := fsutil.NewFS(buildContext)
+	if err != nil {
+		return nil, fmt.Errorf("invalid build context: %w", err)
+	}
+
+	// Create filesystem for dockerfile directory
+	dockerfileFS, err := fsutil.NewFS(filepath.Dir(dockerfilePath))
+	if err != nil {
+		return nil, fmt.Errorf("invalid dockerfile directory: %w", err)
+	}
+
+	// Frontend attributes for dockerfile build
+	frontendAttrs := map[string]string{
+		"filename": filepath.Base(dockerfilePath),
+	}
+
+	if noCache {
+		frontendAttrs["no-cache"] = ""
+	}
+
+	// Add build args
+	for key, value := range buildArgs {
+		frontendAttrs["build-arg:"+key] = value
+	}
+
+	solveOpt := &client.SolveOpt{
+		Exports: []client.ExportEntry{
+			{
+				Type: client.ExporterImage,
+				Attrs: map[string]string{
+					"name": fullImageURI,
+					"push": "true", // Enable push to registry
+				},
+			},
+		},
+		LocalMounts: map[string]fsutil.FS{
+			"context":    contextFS,
+			"dockerfile": dockerfileFS,
+		},
+		Frontend:      "dockerfile.v0", // Use dockerfile frontend
+		FrontendAttrs: frontendAttrs,
+	}
+
+	// Setup authentication if registry credentials are provided
+	if username != "" && password != "" {
+		authConfig := authprovider.DockerAuthProviderConfig{
+			ConfigFile: &configfile.ConfigFile{
+				AuthConfigs: map[string]clitypes.AuthConfig{
+					registryURL: {
+						Username: username,
+						Password: password,
+					},
+				},
+			},
+		}
+		solveOpt.Session = []session.Attachable{
+			authprovider.NewDockerAuthProvider(authConfig),
+		}
+	}
+
+	return solveOpt, nil
+}

+ 0 - 3
cloud/aws.go

@@ -1,3 +0,0 @@
-package cloud
-
-// TODO: Implement aws cloud provider

+ 0 - 3
cloud/digitalocean.go

@@ -1,3 +0,0 @@
-package cloud
-
-// TODO: Implement digitalocean cloud provider

+ 72 - 171
dbstore/apps.go

@@ -2,218 +2,119 @@ package dbstore
 
 import (
 	"context"
-	"database/sql"
-	"encoding/json"
 	"fmt"
-	"time"
 
 	"git.linuxforward.com/byop/byop-engine/models"
-	"github.com/pkg/errors"
+	"gorm.io/gorm"
 )
 
-// App operations
-func (s *SQLiteStore) CreateApp(ctx context.Context, app *models.App) (int, error) {
-	// Convert components slice to JSON
-	componentsJSON, err := json.Marshal(app.Components)
-	if err != nil {
-		return 0, models.NewErrInternalServer("failed to marshal app components", err)
+// CreateApp creates a new app using GORM
+func (s *SQLiteStore) CreateApp(ctx context.Context, app *models.App) error {
+	result := s.db.WithContext(ctx).Create(app)
+	if result.Error != nil {
+		return fmt.Errorf("failed to create app: %w", result.Error)
 	}
-
-	// Handle preview_id: if 0, pass NULL to database
-	var previewID interface{}
-	if app.PreviewID == 0 {
-		previewID = nil
-	} else {
-		previewID = app.PreviewID
-	}
-
-	query := `INSERT INTO apps (user_id, name, description, status, components, preview_id, preview_url, current_image_tag, current_image_uri, error_msg, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
-	now := time.Now().Format(time.RFC3339)
-	result, err := s.db.ExecContext(ctx, query, app.UserID, app.Name, app.Description, app.Status, string(componentsJSON), previewID, app.PreviewURL, app.CurrentImageTag, app.CurrentImageURI, app.ErrorMsg, now, now)
-	if err != nil {
-		return 0, models.NewErrInternalServer("failed to create app", err)
-	}
-
-	id, err := result.LastInsertId()
-	if err != nil {
-		return 0, models.NewErrInternalServer("failed to get last insert ID for app", err)
-	}
-
-	return int(id), nil
+	return nil
 }
 
+// GetAllApps retrieves all apps using GORM
 func (s *SQLiteStore) GetAllApps(ctx context.Context) ([]*models.App, error) {
-	query := `SELECT id, user_id, name, description, status, components, preview_id, preview_url, current_image_tag, current_image_uri, error_msg, created_at, updated_at FROM apps`
-	rows, err := s.db.QueryContext(ctx, query)
-	if err != nil {
-		return nil, models.NewErrInternalServer("failed to query apps", err)
-	}
-	defer rows.Close()
-
 	var apps []*models.App
-	for rows.Next() {
-		var app models.App
-		var componentsJSON string
-		var previewID sql.NullInt64
-		err := rows.Scan(&app.ID, &app.UserID, &app.Name, &app.Description, &app.Status, &componentsJSON, &previewID, &app.PreviewURL, &app.CurrentImageTag, &app.CurrentImageURI, &app.ErrorMsg, &app.CreatedAt, &app.UpdatedAt)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to scan app row", err)
-		}
-
-		// Handle nullable preview_id
-		if previewID.Valid {
-			app.PreviewID = int(previewID.Int64)
-		} else {
-			app.PreviewID = 0
-		}
-
-		// Parse components JSON
-		if componentsJSON != "" {
-			err := json.Unmarshal([]byte(componentsJSON), &app.Components)
-			if err != nil {
-				return nil, models.NewErrInternalServer("failed to unmarshal app components", err)
-			}
-		} else {
-			app.Components = []int{} // Initialize as empty slice if null
-		}
-
-		apps = append(apps, &app)
-	}
-	if err = rows.Err(); err != nil {
-		return nil, models.NewErrInternalServer("error iterating app rows", err)
+	result := s.db.WithContext(ctx).Find(&apps)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get all apps: %w", result.Error)
 	}
-
 	return apps, nil
 }
 
-// GetAppByID retrieves a single app by ID
-func (s *SQLiteStore) GetAppByID(ctx context.Context, id int) (*models.App, error) {
-	query := `SELECT id, user_id, name, description, status, components, preview_id, preview_url, current_image_tag, current_image_uri, error_msg, created_at, updated_at FROM apps WHERE id = ?`
-
+// GetAppByID retrieves an app by ID using GORM
+func (s *SQLiteStore) GetAppByID(ctx context.Context, id uint) (*models.App, error) {
 	var app models.App
-	var componentsJSON string
-	var previewID sql.NullInt64
-	err := s.db.QueryRowContext(ctx, query, id).Scan(&app.ID, &app.UserID, &app.Name, &app.Description, &app.Status, &componentsJSON, &previewID, &app.PreviewURL, &app.CurrentImageTag, &app.CurrentImageURI, &app.ErrorMsg, &app.CreatedAt, &app.UpdatedAt)
-	if err != nil {
-		if errors.Is(err, sql.ErrNoRows) {
-			return nil, models.NewErrNotFound(fmt.Sprintf("app with ID %d not found", id), err)
-		}
-		return nil, models.NewErrInternalServer(fmt.Sprintf("failed to get app with ID %d", id), err)
-	}
-
-	// Handle nullable preview_id
-	if previewID.Valid {
-		app.PreviewID = int(previewID.Int64)
-	} else {
-		app.PreviewID = 0
-	}
-
-	// Parse components JSON
-	if componentsJSON != "" {
-		err := json.Unmarshal([]byte(componentsJSON), &app.Components)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to unmarshal app components for app ID "+fmt.Sprint(id), err)
+	result := s.db.WithContext(ctx).First(&app, id)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, models.NewErrNotFound(fmt.Sprintf("app with ID %d not found", id), result.Error)
 		}
-	} else {
-		app.Components = []int{} // Initialize as empty slice if null
+		return nil, fmt.Errorf("failed to get app by ID: %w", result.Error)
 	}
-
 	return &app, nil
 }
 
-// UpdateApp updates an existing app
-func (s *SQLiteStore) UpdateApp(ctx context.Context, app *models.App) error {
-	// Convert components slice to JSON
-	componentsJSON, err := json.Marshal(app.Components)
-	if err != nil {
-		return models.NewErrInternalServer("failed to marshal app components for update", err)
-	}
-
-	// Handle preview_id: if 0, pass NULL to database
-	var previewID interface{}
-	if app.PreviewID == 0 {
-		previewID = nil
-	} else {
-		previewID = app.PreviewID
+// GetAppsByUserID retrieves all apps for a specific user using GORM
+func (s *SQLiteStore) GetAppsByUserID(ctx context.Context, userID uint) ([]*models.App, error) {
+	var apps []*models.App
+	result := s.db.WithContext(ctx).Where("user_id = ?", userID).Find(&apps)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get apps by user ID: %w", result.Error)
 	}
+	return apps, nil
+}
 
-	query := `UPDATE apps SET user_id = ?, name = ?, description = ?, status = ?, components = ?, preview_id = ?, preview_url = ?, current_image_tag = ?, current_image_uri = ?, error_msg = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`
-	result, err := s.db.ExecContext(ctx, query, app.UserID, app.Name, app.Description, app.Status, string(componentsJSON), previewID, app.PreviewURL, app.CurrentImageTag, app.CurrentImageURI, app.ErrorMsg, app.ID)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update app with ID %d", app.ID), err)
-	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for app update ID %d", app.ID), err)
+// UpdateApp updates an existing app using GORM
+func (s *SQLiteStore) UpdateApp(ctx context.Context, app *models.App) error {
+	result := s.db.WithContext(ctx).Save(app)
+	if result.Error != nil {
+		return fmt.Errorf("failed to update app: %w", result.Error)
 	}
-	if rowsAffected == 0 {
+	if result.RowsAffected == 0 {
 		return models.NewErrNotFound(fmt.Sprintf("app with ID %d not found for update", app.ID), nil)
 	}
-
 	return nil
 }
 
-// DeleteApp deletes an app by ID with verification checks
-func (s *SQLiteStore) DeleteApp(ctx context.Context, id int) error {
-	// First check if the app exists
-	_, err := s.GetAppByID(ctx, id)
-	if err != nil {
-		return err
+// DeleteApp deletes an app by ID using GORM
+func (s *SQLiteStore) DeleteApp(ctx context.Context, id uint) error {
+	result := s.db.WithContext(ctx).Delete(&models.App{}, id)
+	if result.Error != nil {
+		return fmt.Errorf("failed to delete app: %w", result.Error)
 	}
-
-	// Check if the app is used in any deployments
-	deployments, err := s.GetDeploymentsByAppID(ctx, id)
-	if err != nil {
-		var nfErr *models.ErrNotFound
-		if errors.As(err, &nfErr) {
-		} else {
-			return models.NewErrInternalServer(fmt.Sprintf("failed to check app deployments for app ID %d", id), err)
-		}
-	}
-	if len(deployments) > 0 {
-		return models.NewErrConflict(fmt.Sprintf("cannot delete app: it is used in %d deployment(s). Please delete the deployments first", len(deployments)), nil)
-	}
-
-	// If no deployments use this app, proceed with deletion
-	query := `DELETE FROM apps WHERE id = ?`
-	result, err := s.db.ExecContext(ctx, query, id)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to delete app with ID %d", id), err)
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("app with ID %d not found for deletion", id), nil)
 	}
+	return nil
+}
 
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for app deletion ID %d", id), err)
+// UpdateAppStatus updates the status and error message of an app using GORM
+func (s *SQLiteStore) UpdateAppStatus(ctx context.Context, appID uint, status string, message string) error {
+	result := s.db.WithContext(ctx).Model(&models.App{}).Where("id = ?", appID).Updates(map[string]interface{}{
+		"status":    status,
+		"error_msg": message,
+	})
+	if result.Error != nil {
+		return fmt.Errorf("failed to update app status: %w", result.Error)
 	}
-	if rowsAffected == 0 {
-		return models.NewErrNotFound(fmt.Sprintf("app with ID %d not found for deletion", id), nil)
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("app with ID %d not found for status update", appID), nil)
 	}
-
 	return nil
 }
 
-func (s *SQLiteStore) UpdateAppStatus(ctx context.Context, appID int, status, errorMsg string) error {
-	query := `UPDATE apps SET status = ?, error_msg = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`
-	result, err := s.db.ExecContext(ctx, query, status, errorMsg, appID)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update app status for ID %d", appID), err)
+// UpdateAppPreview updates the preview information of an app using GORM
+func (s *SQLiteStore) UpdateAppPreview(ctx context.Context, appID uint, previewID uint, previewURL string) error {
+	result := s.db.WithContext(ctx).Model(&models.App{}).Where("id = ?", appID).Updates(map[string]interface{}{
+		"preview_id":  previewID,
+		"preview_url": previewURL,
+	})
+	if result.Error != nil {
+		return fmt.Errorf("failed to update app preview: %w", result.Error)
 	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for app status update ID %d", appID), err)
-	}
-	if rowsAffected == 0 {
-		return models.NewErrNotFound(fmt.Sprintf("app with ID %d not found for status update", appID), nil)
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("app with ID %d not found for preview update", appID), nil)
 	}
 	return nil
 }
 
-// UpdateAppCurrentImage updates the current image tag and URI for an app.
-func (s *SQLiteStore) UpdateAppCurrentImage(ctx context.Context, appID int, imageTag string, imageURI string) error {
-	query := `UPDATE apps SET current_image_tag = ?, current_image_uri = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`
-	_, err := s.db.ExecContext(ctx, query, imageTag, imageURI, appID)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update app current image for ID %d", appID), err)
+// UpdateAppCurrentImage updates the current image information of an app using GORM
+func (s *SQLiteStore) UpdateAppCurrentImage(ctx context.Context, appID uint, imageTag string, imageURI string) error {
+	result := s.db.WithContext(ctx).Model(&models.App{}).Where("id = ?", appID).Updates(map[string]interface{}{
+		"current_image_tag": imageTag,
+		"current_image_uri": imageURI,
+	})
+	if result.Error != nil {
+		return fmt.Errorf("failed to update app current image: %w", result.Error)
+	}
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("app with ID %d not found for image update", appID), nil)
 	}
 	return nil
 }

+ 58 - 281
dbstore/build_jobs.go

@@ -2,330 +2,107 @@ package dbstore
 
 import (
 	"context"
-	"database/sql"
 	"fmt"
-	"time"
 
 	"git.linuxforward.com/byop/byop-engine/models"
+	"gorm.io/gorm"
 )
 
-// CreateBuildJob creates a new build job record in the database.
+// CreateBuildJob creates a new build job using GORM
 func (s *SQLiteStore) CreateBuildJob(ctx context.Context, job *models.BuildJob) error {
-	// Ensure CreatedAt and UpdatedAt are set
-	now := time.Now()
-	job.CreatedAt = now
-	job.UpdatedAt = now
-
-	query := `
-		INSERT INTO build_jobs (
-			component_id, request_id, source_url, version, status, image_name, image_tag, full_image_uri,
-			registry_url, registry_user, registry_password, build_context, dockerfile, dockerfile_content, no_cache,
-			build_args, logs, error_message, requested_at, started_at, finished_at, worker_node_id,
-			created_at, updated_at
-		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
-
-	stmt, err := s.db.PrepareContext(ctx, query)
-	if err != nil {
-		return fmt.Errorf("failed to prepare statement for CreateBuildJob: %w", err)
-	}
-	defer stmt.Close()
-
-	res, err := stmt.ExecContext(ctx,
-		job.ComponentID, job.RequestID, job.SourceURL, job.Version, job.Status, job.ImageName, job.ImageTag, job.FullImageURI,
-		job.RegistryURL, job.RegistryUser, job.RegistryPassword, job.BuildContext, job.Dockerfile, job.DockerfileContent, job.NoCache,
-		job.BuildArgs, job.Logs, job.ErrorMessage, job.RequestedAt, job.StartedAt, job.FinishedAt, job.WorkerNodeID,
-		job.CreatedAt, job.UpdatedAt,
-	)
-	if err != nil {
-		return fmt.Errorf("failed to execute statement for CreateBuildJob: %w", err)
+	result := s.db.WithContext(ctx).Create(job)
+	if result.Error != nil {
+		return fmt.Errorf("failed to create build job: %w", result.Error)
 	}
-
-	id, err := res.LastInsertId()
-	if err != nil {
-		return fmt.Errorf("failed to get last insert ID for CreateBuildJob: %w", err)
-	}
-	job.ID = uint(id)
 	return nil
 }
 
-// GetBuildJobByID retrieves a build job by its ID.
+// GetBuildJobByID retrieves a build job by ID using GORM
 func (s *SQLiteStore) GetBuildJobByID(ctx context.Context, id uint) (*models.BuildJob, error) {
-	query := `
-		SELECT
-			id, component_id, request_id, source_url, version, status, image_name, image_tag, full_image_uri,
-			registry_url, registry_user, registry_password, build_context, dockerfile, dockerfile_content, no_cache,
-			build_args, logs, error_message, requested_at, started_at, finished_at, worker_node_id,
-			created_at, updated_at
-		FROM build_jobs WHERE id = ?`
-
-	row := s.db.QueryRowContext(ctx, query, id)
-	job := &models.BuildJob{}
-	var startedAt, finishedAt sql.NullTime
-
-	err := row.Scan(
-		&job.ID, &job.ComponentID, &job.RequestID, &job.SourceURL, &job.Version, &job.Status, &job.ImageName, &job.ImageTag, &job.FullImageURI,
-		&job.RegistryURL, &job.RegistryUser, &job.RegistryPassword, &job.BuildContext, &job.Dockerfile, &job.DockerfileContent, &job.NoCache,
-		&job.BuildArgs, &job.Logs, &job.ErrorMessage, &job.RequestedAt, &startedAt, &finishedAt, &job.WorkerNodeID,
-		&job.CreatedAt, &job.UpdatedAt,
-	)
-	if err != nil {
-		if err == sql.ErrNoRows {
-			return nil, fmt.Errorf("build job with ID %d not found", id)
+	var job models.BuildJob
+	result := s.db.WithContext(ctx).First(&job, id)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, models.NewErrNotFound(fmt.Sprintf("build job with ID %d not found", id), result.Error)
 		}
-		return nil, fmt.Errorf("failed to scan build job: %w", err)
-	}
-
-	if startedAt.Valid {
-		job.StartedAt = &startedAt.Time
-	}
-	if finishedAt.Valid {
-		job.FinishedAt = &finishedAt.Time
+		return nil, fmt.Errorf("failed to get build job by ID: %w", result.Error)
 	}
-
-	return job, nil
+	return &job, nil
 }
 
-// UpdateBuildJob updates an existing build job record in the database.
+// UpdateBuildJob updates an existing build job using GORM
 func (s *SQLiteStore) UpdateBuildJob(ctx context.Context, job *models.BuildJob) error {
-	job.UpdatedAt = time.Now()
-
-	query := `
-		UPDATE build_jobs SET
-			component_id = ?, request_id = ?, source_url = ?, version = ?, status = ?, image_name = ?, image_tag = ?,
-			full_image_uri = ?, registry_url = ?, registry_user = ?, registry_password = ?, build_context = ?,
-			dockerfile = ?, no_cache = ?, build_args = ?, logs = ?, error_message = ?, requested_at = ?,
-			started_at = ?, finished_at = ?, worker_node_id = ?, updated_at = ?
-		WHERE id = ?`
-
-	stmt, err := s.db.PrepareContext(ctx, query)
-	if err != nil {
-		return fmt.Errorf("failed to prepare statement for UpdateBuildJob: %w", err)
+	result := s.db.WithContext(ctx).Save(job)
+	if result.Error != nil {
+		return fmt.Errorf("failed to update build job: %w", result.Error)
 	}
-	defer stmt.Close()
-
-	_, err = stmt.ExecContext(ctx,
-		job.ComponentID, job.RequestID, job.SourceURL, job.Version, job.Status, job.ImageName, job.ImageTag,
-		job.FullImageURI, job.RegistryURL, job.RegistryUser, job.RegistryPassword, job.BuildContext,
-		job.Dockerfile, job.NoCache, job.BuildArgs, job.Logs, job.ErrorMessage, job.RequestedAt,
-		job.StartedAt, job.FinishedAt, job.WorkerNodeID, job.UpdatedAt,
-		job.ID,
-	)
-	if err != nil {
-		return fmt.Errorf("failed to execute statement for UpdateBuildJob: %w", err)
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("build job with ID %d not found for update", job.ID), nil)
 	}
 	return nil
 }
 
-// UpdateBuildJobStatus updates the status, error message, and relevant timestamps of a build job.
+// UpdateBuildJobStatus updates the status and error message of a build job using GORM
 func (s *SQLiteStore) UpdateBuildJobStatus(ctx context.Context, id uint, status models.BuildStatus, errorMessage string) error {
-	now := time.Now()
-	var startedAtExpr, finishedAtExpr string
-	var args []interface{}
-
-	baseQuery := "UPDATE build_jobs SET status = ?, error_message = ?, updated_at = ?"
-	args = append(args, status, errorMessage, now)
-
-	switch status {
-	case models.BuildStatusFetching, models.BuildStatusBuilding, models.BuildStatusPushing:
-		startedAtExpr = ", started_at = COALESCE(started_at, ?)"
-		args = append(args, now)
-	case models.BuildStatusSuccess, models.BuildStatusFailed, models.BuildStatusCancelled:
-		startedAtExpr = ", started_at = COALESCE(started_at, ?)"
-		finishedAtExpr = ", finished_at = ?"
-		args = append(args, now, now)
+	result := s.db.WithContext(ctx).Model(&models.BuildJob{}).Where("id = ?", id).Updates(map[string]interface{}{
+		"status":        status,
+		"error_message": errorMessage,
+	})
+	if result.Error != nil {
+		return fmt.Errorf("failed to update build job status: %w", result.Error)
 	}
-
-	finalQuery := baseQuery + startedAtExpr + finishedAtExpr + " WHERE id = ?"
-	args = append(args, id)
-
-	stmt, err := s.db.PrepareContext(ctx, finalQuery)
-	if err != nil {
-		return fmt.Errorf("failed to prepare statement for UpdateBuildJobStatus: %w", err)
-	}
-	defer stmt.Close()
-
-	_, err = stmt.ExecContext(ctx, args...)
-	if err != nil {
-		return fmt.Errorf("failed to execute statement for UpdateBuildJobStatus: %w", err)
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("build job with ID %d not found for status update", id), nil)
 	}
 	return nil
 }
 
-// AppendBuildJobLog appends a new log entry to the build job's logs.
+// AppendBuildJobLog appends a log message to a build job using GORM
 func (s *SQLiteStore) AppendBuildJobLog(ctx context.Context, id uint, logMessage string) error {
-	tx, err := s.db.BeginTx(ctx, nil)
-	if err != nil {
-		return fmt.Errorf("failed to begin transaction for AppendBuildJobLog: %w", err)
-	}
-	defer tx.Rollback()
-
-	var currentLogs string
-	querySelect := "SELECT logs FROM build_jobs WHERE id = ?"
-	err = tx.QueryRowContext(ctx, querySelect, id).Scan(&currentLogs)
-	if err != nil {
-		if err == sql.ErrNoRows {
-			return fmt.Errorf("build job with ID %d not found for AppendBuildJobLog", id)
+	// Get current logs first
+	var job models.BuildJob
+	result := s.db.WithContext(ctx).Select("logs").First(&job, id)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return models.NewErrNotFound(fmt.Sprintf("build job with ID %d not found", id), result.Error)
 		}
-		return fmt.Errorf("failed to query current logs for AppendBuildJobLog: %w", err)
-	}
-
-	newLogEntry := fmt.Sprintf("%s: %s", time.Now().Format(time.RFC3339Nano), logMessage)
-	var updatedLogs string
-	if currentLogs == "" {
-		updatedLogs = newLogEntry
-	} else {
-		updatedLogs = currentLogs + "\n" + newLogEntry
+		return fmt.Errorf("failed to get build job for log append: %w", result.Error)
 	}
 
-	queryUpdate := "UPDATE build_jobs SET logs = ?, updated_at = ? WHERE id = ?"
-	stmt, err := tx.PrepareContext(ctx, queryUpdate)
-	if err != nil {
-		return fmt.Errorf("failed to prepare update statement for AppendBuildJobLog: %w", err)
+	// Append new log message
+	newLogs := job.Logs + logMessage + "\n"
+	result = s.db.WithContext(ctx).Model(&models.BuildJob{}).Where("id = ?", id).Update("logs", newLogs)
+	if result.Error != nil {
+		return fmt.Errorf("failed to append build job log: %w", result.Error)
 	}
-	defer stmt.Close()
-
-	_, err = stmt.ExecContext(ctx, updatedLogs, time.Now(), id)
-	if err != nil {
-		return fmt.Errorf("failed to execute update statement for AppendBuildJobLog: %w", err)
-	}
-
-	return tx.Commit()
+	return nil
 }
 
-// GetQueuedBuildJobs retrieves a list of build jobs that are in the 'pending' status,
-// ordered by their request time.
-func (s *SQLiteStore) GetQueuedBuildJobs(ctx context.Context, limit int) ([]models.BuildJob, error) {
-	query := `
-		SELECT
-			id, component_id, request_id, source_url, version, status, image_name, image_tag, full_image_uri,
-			registry_url, registry_user, registry_password, build_context, dockerfile, dockerfile_content, no_cache,
-			build_args, logs, error_message, requested_at, started_at, finished_at, worker_node_id,
-			created_at, updated_at
-		FROM build_jobs
-		WHERE status = ?
-		ORDER BY requested_at ASC
-		LIMIT ?`
-
-	rows, err := s.db.QueryContext(ctx, query, models.BuildStatusPending, limit)
-	if err != nil {
-		return nil, fmt.Errorf("failed to query queued build jobs: %w", err)
-	}
-	defer rows.Close()
-
-	var jobs []models.BuildJob
-	for rows.Next() {
-		job := models.BuildJob{}
-		var startedAt, finishedAt sql.NullTime
-		err := rows.Scan(
-			&job.ID, &job.ComponentID, &job.RequestID, &job.SourceURL, &job.Version, &job.Status, &job.ImageName, &job.ImageTag, &job.FullImageURI,
-			&job.RegistryURL, &job.RegistryUser, &job.RegistryPassword, &job.BuildContext, &job.Dockerfile, &job.DockerfileContent, &job.NoCache,
-			&job.BuildArgs, &job.Logs, &job.ErrorMessage, &job.RequestedAt, &startedAt, &finishedAt, &job.WorkerNodeID,
-			&job.CreatedAt, &job.UpdatedAt,
-		)
-		if err != nil {
-			return nil, fmt.Errorf("failed to scan queued build job: %w", err)
-		}
-		if startedAt.Valid {
-			job.StartedAt = &startedAt.Time
-		}
-		if finishedAt.Valid {
-			job.FinishedAt = &finishedAt.Time
-		}
-		jobs = append(jobs, job)
-	}
-
-	if err = rows.Err(); err != nil {
-		return nil, fmt.Errorf("error iterating queued build jobs: %w", err)
+// GetQueuedBuildJobs retrieves queued build jobs using GORM
+func (s *SQLiteStore) GetQueuedBuildJobs(ctx context.Context, limit int) ([]*models.BuildJob, error) {
+	var jobs []*models.BuildJob
+	result := s.db.WithContext(ctx).Where("status = ?", models.BuildStatusPending).Limit(limit).Find(&jobs)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get queued build jobs: %w", result.Error)
 	}
-
 	return jobs, nil
 }
 
-// GetBuildJobsByComponentID retrieves all build jobs for a specific application with pagination.
-func (s *SQLiteStore) GetBuildJobsByComponentID(ctx context.Context, componentID uint, page, pageSize int) ([]models.BuildJob, int64, error) {
+// GetBuildJobsByAppID retrieves build jobs for a specific app using GORM (with pagination)
+func (s *SQLiteStore) GetBuildJobsByAppID(ctx context.Context, appID uint, page, pageSize int) ([]*models.BuildJob, int64, error) {
+	var jobs []*models.BuildJob
 	var total int64
-	countQuery := "SELECT COUNT(*) FROM build_jobs WHERE component_id = ?"
-	err := s.db.QueryRowContext(ctx, countQuery, componentID).Scan(&total)
-	if err != nil {
-		return nil, 0, fmt.Errorf("failed to count build jobs by component ID: %w", err)
-	}
 
-	if total == 0 {
-		return []models.BuildJob{}, 0, nil
-	}
+	// Count total records
+	s.db.WithContext(ctx).Model(&models.BuildJob{}).Where("component_id IN (SELECT id FROM components WHERE user_id = (SELECT user_id FROM apps WHERE id = ?))", appID).Count(&total)
 
+	// Get paginated results
 	offset := (page - 1) * pageSize
-	query := `
-		SELECT
-			id, component_id, request_id, source_url, version, status, image_name, image_tag, full_image_uri,
-			registry_url, registry_user, registry_password, build_context, dockerfile, dockerfile_content, no_cache,
-			build_args, logs, error_message, requested_at, started_at, finished_at, worker_node_id,
-			created_at, updated_at
-		FROM build_jobs
-		WHERE component_id = ?
-		ORDER BY requested_at DESC
-		LIMIT ? OFFSET ?`
-
-	rows, err := s.db.QueryContext(ctx, query, componentID, pageSize, offset)
-	if err != nil {
-		return nil, 0, fmt.Errorf("failed to query build jobs by component ID: %w", err)
-	}
-	defer rows.Close()
+	result := s.db.WithContext(ctx).Where("component_id IN (SELECT id FROM components WHERE user_id = (SELECT user_id FROM apps WHERE id = ?))", appID).
+		Offset(offset).Limit(pageSize).Find(&jobs)
 
-	var jobs []models.BuildJob
-	for rows.Next() {
-		job := models.BuildJob{}
-		var startedAt, finishedAt sql.NullTime
-		err := rows.Scan(
-			&job.ID, &job.ComponentID, &job.RequestID, &job.SourceURL, &job.Version, &job.Status, &job.ImageName, &job.ImageTag, &job.FullImageURI,
-			&job.RegistryURL, &job.RegistryUser, &job.RegistryPassword, &job.BuildContext, &job.Dockerfile, &job.DockerfileContent, &job.NoCache,
-			&job.BuildArgs, &job.Logs, &job.ErrorMessage, &job.RequestedAt, &startedAt, &finishedAt, &job.WorkerNodeID,
-			&job.CreatedAt, &job.UpdatedAt,
-		)
-		if err != nil {
-			return nil, 0, fmt.Errorf("failed to scan build job by component ID: %w", err)
-		}
-		if startedAt.Valid {
-			job.StartedAt = &startedAt.Time
-		}
-		if finishedAt.Valid {
-			job.FinishedAt = &finishedAt.Time
-		}
-		jobs = append(jobs, job)
+	if result.Error != nil {
+		return nil, 0, fmt.Errorf("failed to get build jobs by app ID: %w", result.Error)
 	}
-
-	if err = rows.Err(); err != nil {
-		return nil, 0, fmt.Errorf("error iterating build jobs by component ID: %w", err)
-	}
-
 	return jobs, total, nil
 }
-
-// Helper to marshal map to JSON string for BuildArgs, if needed before calling Create/Update.
-// This is more of a service-layer concern or model method.
-/*
-func marshalBuildArgs(args map[string]string) (string, error) {
-	if args == nil {
-		return "{}", nil // Or "null" or "" depending on preference for empty args
-	}
-	bytes, err := json.Marshal(args)
-	if err != nil {
-		return "", err
-	}
-	return string(bytes), nil
-}
-
-// Helper to unmarshal JSON string to map for BuildArgs, if needed after fetching.
-// This is more of a service-layer concern or model method.
-func unmarshalBuildArgs(argsStr string) (map[string]string, error) {
-	if strings.TrimSpace(argsStr) == "" || argsStr == "null" {
-		return make(map[string]string), nil
-	}
-	var args map[string]string
-	err := json.Unmarshal([]byte(argsStr), &args)
-	if err != nil {
-		return nil, err
-	}
-	return args, nil
-}
-*/

+ 33 - 65
dbstore/clients.go

@@ -2,95 +2,63 @@ package dbstore
 
 import (
 	"context"
-	"database/sql"
 	"fmt"
 
 	"git.linuxforward.com/byop/byop-engine/models"
-	"github.com/pkg/errors"
+	"gorm.io/gorm"
 )
 
-// Client operations
-func (s *SQLiteStore) CreateClient(ctx context.Context, client models.Client) (int, error) {
-	query := `INSERT INTO clients (name, description, contact_info, active) VALUES (?, ?, ?, ?)`
-	result, err := s.db.ExecContext(ctx, query, client.Name, client.Description, client.ContactInfo, client.Active)
-	if err != nil {
-		return 0, models.NewErrInternalServer("failed to create client", err)
+// CreateClient creates a new client using GORM
+func (s *SQLiteStore) CreateClient(ctx context.Context, client *models.Client) error {
+	result := s.db.WithContext(ctx).Create(client)
+	if result.Error != nil {
+		return fmt.Errorf("failed to create client: %w", result.Error)
 	}
-
-	id, err := result.LastInsertId()
-	if err != nil {
-		return 0, models.NewErrInternalServer("failed to get last insert ID for client", err)
-	}
-
-	return int(id), nil
+	return nil
 }
 
-func (s *SQLiteStore) GetAllClients(ctx context.Context) ([]models.Client, error) {
-	query := `SELECT id, name, description, contact_info, active, created_at, updated_at FROM clients`
-	rows, err := s.db.QueryContext(ctx, query)
-	if err != nil {
-		return nil, models.NewErrInternalServer("failed to query clients", err)
+// GetAllClients retrieves all clients using GORM
+func (s *SQLiteStore) GetAllClients(ctx context.Context) ([]*models.Client, error) {
+	var clients []*models.Client
+	result := s.db.WithContext(ctx).Find(&clients)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get all clients: %w", result.Error)
 	}
-	defer rows.Close()
-
-	var clients []models.Client
-	for rows.Next() {
-		var client models.Client
-		err := rows.Scan(&client.ID, &client.Name, &client.Description, &client.ContactInfo, &client.Active, &client.CreatedAt, &client.UpdatedAt)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to scan client row", err)
-		}
-		clients = append(clients, client)
-	}
-	if err = rows.Err(); err != nil {
-		return nil, models.NewErrInternalServer("error iterating client rows", err)
-	}
-
 	return clients, nil
 }
 
-func (s *SQLiteStore) GetClientByID(ctx context.Context, id int) (models.Client, error) {
+// GetClientByID retrieves a client by ID using GORM
+func (s *SQLiteStore) GetClientByID(ctx context.Context, id uint) (*models.Client, error) {
 	var client models.Client
-	query := `SELECT id, name, description, contact_info, active, created_at, updated_at FROM clients WHERE id = ?`
-	err := s.db.QueryRowContext(ctx, query, id).Scan(&client.ID, &client.Name, &client.Description, &client.ContactInfo, &client.Active, &client.CreatedAt, &client.UpdatedAt)
-	if err != nil {
-		if errors.Is(err, sql.ErrNoRows) {
-			return client, models.NewErrNotFound(fmt.Sprintf("client with ID %d not found", id), err)
+	result := s.db.WithContext(ctx).First(&client, id)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, models.NewErrNotFound(fmt.Sprintf("client with ID %d not found", id), result.Error)
 		}
-		return client, models.NewErrInternalServer(fmt.Sprintf("failed to get client with ID %d", id), err)
+		return nil, fmt.Errorf("failed to get client by ID: %w", result.Error)
 	}
-	return client, nil
+	return &client, nil
 }
 
-// UpdateClient updates an existing client
-func (s *SQLiteStore) UpdateClient(ctx context.Context, client models.Client) error {
-	query := `UPDATE clients SET name = ?, description = ?, contact_info = ?, active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?` // Added CURRENT_TIMESTAMP for updated_at
-	result, err := s.db.ExecContext(ctx, query, client.Name, client.Description, client.ContactInfo, client.Active, client.ID)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update client with ID %d", client.ID), err)
+// UpdateClient updates an existing client using GORM
+func (s *SQLiteStore) UpdateClient(ctx context.Context, client *models.Client) error {
+	result := s.db.WithContext(ctx).Save(client)
+	if result.Error != nil {
+		return fmt.Errorf("failed to update client: %w", result.Error)
 	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for client update ID %d", client.ID), err)
-	}
-	if rowsAffected == 0 {
+	if result.RowsAffected == 0 {
 		return models.NewErrNotFound(fmt.Sprintf("client with ID %d not found for update", client.ID), nil)
 	}
 	return nil
 }
 
-// DeleteClient deletes a client by ID
-func (s *SQLiteStore) DeleteClient(ctx context.Context, id int) error {
-	query := `DELETE FROM clients WHERE id = ?`
-	result, err := s.db.ExecContext(ctx, query, id)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to delete client with ID %d", id), err)
-	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for client deletion ID %d", id), err)
+// DeleteClient deletes a client by ID using GORM
+func (s *SQLiteStore) DeleteClient(ctx context.Context, id uint) error {
+	result := s.db.WithContext(ctx).Delete(&models.Client{}, id)
+	if result.Error != nil {
+		return fmt.Errorf("failed to delete client: %w", result.Error)
 	}
-	if rowsAffected == 0 {
+	if result.RowsAffected == 0 {
 		return models.NewErrNotFound(fmt.Sprintf("client with ID %d not found for deletion", id), nil)
 	}
 	return nil

+ 59 - 154
dbstore/components.go

@@ -2,199 +2,104 @@ package dbstore
 
 import (
 	"context"
-	"database/sql"
 	"fmt"
 
 	"git.linuxforward.com/byop/byop-engine/models"
-	"github.com/pkg/errors"
+	"gorm.io/gorm"
 )
 
-// Component operations
-func (s *SQLiteStore) CreateComponent(ctx context.Context, component *models.Component) (int, error) {
-	query := `INSERT INTO components (user_id, name, description, type, status, config, repository, branch) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
-	result, err := s.db.ExecContext(ctx, query, component.UserID, component.Name, component.Description, component.Type, component.Status, component.Config, component.Repository, component.Branch)
-	if err != nil {
-		return 0, models.NewErrInternalServer("failed to create component", err)
+// CreateComponent creates a new component using GORM
+func (s *SQLiteStore) CreateComponent(ctx context.Context, component *models.Component) error {
+	result := s.db.WithContext(ctx).Create(component)
+	if result.Error != nil {
+		return fmt.Errorf("failed to create component: %w", result.Error)
 	}
-
-	id, err := result.LastInsertId()
-	if err != nil {
-		return 0, models.NewErrInternalServer("failed to get last insert ID for component", err)
-	}
-
-	return int(id), nil
+	return nil
 }
 
-func (s *SQLiteStore) GetComponentsByUserID(ctx context.Context, userID int) ([]models.Component, error) {
-	query := `SELECT id, user_id, name, description, type, status, config, repository, branch, error_msg, current_image_tag, current_image_uri, created_at, updated_at FROM components WHERE user_id = ?`
-	rows, err := s.db.QueryContext(ctx, query, userID)
-	if err != nil {
-		return nil, models.NewErrInternalServer(fmt.Sprintf("failed to query components for user ID %d", userID), err)
+// GetComponentsByUserID retrieves all components for a specific user using GORM
+func (s *SQLiteStore) GetComponentsByUserID(ctx context.Context, userID uint) ([]*models.Component, error) {
+	var components []*models.Component
+	result := s.db.WithContext(ctx).Where("user_id = ?", userID).Find(&components)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get components by user ID: %w", result.Error)
 	}
-	defer rows.Close()
-
-	var components []models.Component
-	for rows.Next() {
-		var component models.Component
-		err := rows.Scan(&component.ID, &component.UserID, &component.Name, &component.Description, &component.Type, &component.Status, &component.Config, &component.Repository, &component.Branch, &component.ErrorMsg, &component.CurrentImageTag, &component.CurrentImageURI, &component.CreatedAt, &component.UpdatedAt)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to scan component row", err)
-		}
-		components = append(components, component)
-	}
-	if err = rows.Err(); err != nil {
-		return nil, models.NewErrInternalServer(fmt.Sprintf("error iterating component rows for user ID %d", userID), err)
-	}
-
 	return components, nil
 }
 
-// GetAllComponents retrieves all components
-func (s *SQLiteStore) GetAllComponents(ctx context.Context) ([]models.Component, error) {
-	query := `SELECT id, user_id, name, description, type, status, config, repository, branch, error_msg, current_image_tag, current_image_uri, created_at, updated_at FROM components`
-	rows, err := s.db.QueryContext(ctx, query)
-	if err != nil {
-		return nil, models.NewErrInternalServer("failed to query all components", err)
+// GetAllComponents retrieves all components using GORM
+func (s *SQLiteStore) GetAllComponents(ctx context.Context) ([]*models.Component, error) {
+	var components []*models.Component
+	result := s.db.WithContext(ctx).Find(&components)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get all components: %w", result.Error)
 	}
-	defer rows.Close()
-
-	var components []models.Component
-	for rows.Next() {
-		var component models.Component
-		err := rows.Scan(&component.ID, &component.UserID, &component.Name, &component.Description, &component.Type, &component.Status, &component.Config, &component.Repository, &component.Branch, &component.ErrorMsg, &component.CurrentImageTag, &component.CurrentImageURI, &component.CreatedAt, &component.UpdatedAt)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to scan component row", err)
-		}
-		components = append(components, component)
-	}
-	if err = rows.Err(); err != nil {
-		return nil, models.NewErrInternalServer("error iterating all component rows", err)
-	}
-
 	return components, nil
 }
 
-// GetComponentByID retrieves a component by ID
-func (s *SQLiteStore) GetComponentByID(ctx context.Context, id int) (*models.Component, error) {
-	component := &models.Component{}
-	query := `SELECT id, user_id, name, description, type, status, config, repository, branch, error_msg, current_image_tag, current_image_uri, created_at, updated_at FROM components WHERE id = ?`
-	err := s.db.QueryRowContext(ctx, query, id).Scan(&component.ID, &component.UserID, &component.Name, &component.Description, &component.Type, &component.Status, &component.Config, &component.Repository, &component.Branch, &component.ErrorMsg, &component.CurrentImageTag, &component.CurrentImageURI, &component.CreatedAt, &component.UpdatedAt)
-	if err != nil {
-		if errors.Is(err, sql.ErrNoRows) {
-			return nil, models.NewErrNotFound(fmt.Sprintf("component with ID %d not found", id), err)
+// GetComponentByID retrieves a component by ID using GORM
+func (s *SQLiteStore) GetComponentByID(ctx context.Context, id uint) (*models.Component, error) {
+	var component models.Component
+	result := s.db.WithContext(ctx).First(&component, id)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, models.NewErrNotFound(fmt.Sprintf("component with ID %d not found", id), result.Error)
 		}
-		return nil, models.NewErrInternalServer(fmt.Sprintf("failed to get component with ID %d", id), err)
+		return nil, fmt.Errorf("failed to get component by ID: %w", result.Error)
 	}
-	return component, nil
+	return &component, nil
 }
 
-// UpdateComponent updates an existing component
-func (s *SQLiteStore) UpdateComponent(ctx context.Context, component models.Component) error {
-	query := `UPDATE components SET name = ?, description = ?, type = ?, status = ?, config = ?, repository = ?, branch = ?, current_image_tag = ?, current_image_uri = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`
-	result, err := s.db.ExecContext(ctx, query, component.Name, component.Description, component.Type, component.Status, component.Config, component.Repository, component.Branch, component.CurrentImageTag, component.CurrentImageURI, component.ID)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update component with ID %d", component.ID), err)
-	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for component update ID %d", component.ID), err)
+// UpdateComponent updates an existing component using GORM
+func (s *SQLiteStore) UpdateComponent(ctx context.Context, component *models.Component) error {
+	result := s.db.WithContext(ctx).Save(component)
+	if result.Error != nil {
+		return fmt.Errorf("failed to update component: %w", result.Error)
 	}
-	if rowsAffected == 0 {
+	if result.RowsAffected == 0 {
 		return models.NewErrNotFound(fmt.Sprintf("component with ID %d not found for update", component.ID), nil)
 	}
 	return nil
 }
 
-// UpdateComponentStatus updates the validation status of a component
-func (s *SQLiteStore) UpdateComponentStatus(ctx context.Context, id int, status, errorMsg string) error {
-	query := `
-        UPDATE components 
-        SET status = ?, error_msg = ?, updated_at = CURRENT_TIMESTAMP 
-        WHERE id = ?
-    `
-
-	result, err := s.db.ExecContext(ctx, query, status, errorMsg, id)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update component status for ID %d", id), err)
-	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for component status update ID %d", id), err)
+// UpdateComponentStatus updates the status and error message of a component using GORM
+func (s *SQLiteStore) UpdateComponentStatus(ctx context.Context, componentID uint, status string, errorMsg string) error {
+	result := s.db.WithContext(ctx).Model(&models.Component{}).Where("id = ?", componentID).Updates(map[string]interface{}{
+		"status":    status,
+		"error_msg": errorMsg,
+	})
+	if result.Error != nil {
+		return fmt.Errorf("failed to update component status: %w", result.Error)
 	}
-	if rowsAffected == 0 {
-		return models.NewErrNotFound(fmt.Sprintf("component with ID %d not found for status update", id), nil)
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("component with ID %d not found for status update", componentID), nil)
 	}
-
 	return nil
 }
 
-// UpdateComponentImageInfo updates the image information for a component after a successful build
-func (s *SQLiteStore) UpdateComponentImageInfo(ctx context.Context, componentID int, imageTag, imageURI string) error {
-	query := `
-        UPDATE components 
-        SET current_image_tag = ?, current_image_uri = ?, updated_at = CURRENT_TIMESTAMP 
-        WHERE id = ?
-    `
-
-	result, err := s.db.ExecContext(ctx, query, imageTag, imageURI, componentID)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update component image info for ID %d", componentID), err)
+// UpdateComponentImageInfo updates the image information of a component using GORM
+func (s *SQLiteStore) UpdateComponentImageInfo(ctx context.Context, componentID uint, imageTag string, imageURI string) error {
+	result := s.db.WithContext(ctx).Model(&models.Component{}).Where("id = ?", componentID).Updates(map[string]interface{}{
+		"current_image_tag": imageTag,
+		"current_image_uri": imageURI,
+	})
+	if result.Error != nil {
+		return fmt.Errorf("failed to update component image info: %w", result.Error)
 	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for component image info update ID %d", componentID), err)
-	}
-	if rowsAffected == 0 {
+	if result.RowsAffected == 0 {
 		return models.NewErrNotFound(fmt.Sprintf("component with ID %d not found for image info update", componentID), nil)
 	}
-
 	return nil
 }
 
-// DeleteComponent deletes a component by ID with verification checks
-func (s *SQLiteStore) DeleteComponent(ctx context.Context, id int) error {
-	// First check if the component exists
-	_, err := s.GetComponentByID(ctx, id)
-	if err != nil {
-		return err // GetComponentByID already returns custom errors
-	}
-
-	// Check if the component is used in any apps
-	apps, err := s.GetAllApps(ctx) // Use context-aware GetAllApps
-	if err != nil {
-		// GetAllApps should return a custom error if it fails
-		return models.NewErrInternalServer(fmt.Sprintf("failed to check component usage in apps for component ID %d", id), err)
-	}
-
-	var appsUsingComponent []string
-	for _, app := range apps {
-		for _, componentID := range app.Components {
-			if componentID == id {
-				appsUsingComponent = append(appsUsingComponent, app.Name)
-				break
-			}
-		}
-	}
-
-	if len(appsUsingComponent) > 0 {
-		return models.NewErrConflict(fmt.Sprintf("cannot delete component: it is used in the following app(s): %v. Please remove it from these apps first", appsUsingComponent), nil)
-	}
-
-	// If no apps use this component, proceed with deletion
-	query := `DELETE FROM components WHERE id = ?`
-	result, err := s.db.ExecContext(ctx, query, id)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to delete component with ID %d", id), err)
-	}
-
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for component deletion ID %d", id), err)
+// DeleteComponent deletes a component by ID using GORM
+func (s *SQLiteStore) DeleteComponent(ctx context.Context, id uint) error {
+	result := s.db.WithContext(ctx).Delete(&models.Component{}, id)
+	if result.Error != nil {
+		return fmt.Errorf("failed to delete component: %w", result.Error)
 	}
-	if rowsAffected == 0 {
-		// This case should ideally be caught by GetComponentByID earlier, but as a safeguard:
+	if result.RowsAffected == 0 {
 		return models.NewErrNotFound(fmt.Sprintf("component with ID %d not found for deletion", id), nil)
 	}
-
 	return nil
 }

+ 54 - 187
dbstore/deployments.go

@@ -2,230 +2,97 @@ package dbstore
 
 import (
 	"context"
-	"database/sql"
 	"fmt"
 
 	"git.linuxforward.com/byop/byop-engine/models"
-	"github.com/pkg/errors"
+	"gorm.io/gorm"
 )
 
-// Deployment operations
-func (s *SQLiteStore) CreateDeployment(ctx context.Context, deployment models.Deployment) (int, error) {
-	query := `INSERT INTO deployments (app_id, client_id, name, description, environment, status, url, config, deployed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
-	deployedAt := sql.NullTime{}
-	if !deployment.DeployedAt.IsZero() {
-		deployedAt.Time = deployment.DeployedAt
-		deployedAt.Valid = true
+// CreateDeployment creates a new deployment using GORM
+func (s *SQLiteStore) CreateDeployment(ctx context.Context, deployment *models.Deployment) error {
+	result := s.db.WithContext(ctx).Create(deployment)
+	if result.Error != nil {
+		return fmt.Errorf("failed to create deployment: %w", result.Error)
 	}
-
-	result, err := s.db.ExecContext(ctx, query, deployment.AppId, deployment.ClientID, deployment.Name, deployment.Description, deployment.Environment, deployment.Status, deployment.URL, deployment.Config, deployedAt)
-	if err != nil {
-		return 0, models.NewErrInternalServer("failed to create deployment", err)
-	}
-
-	id, err := result.LastInsertId()
-	if err != nil {
-		return 0, models.NewErrInternalServer("failed to get last insert ID for deployment", err)
-	}
-
-	return int(id), nil
+	return nil
 }
 
-func (s *SQLiteStore) GetDeploymentsByAppID(ctx context.Context, appID int) ([]*models.Deployment, error) {
-	query := `SELECT id, app_id, client_id, name, description, environment, status, url, config, deployed_at, created_at, updated_at FROM deployments WHERE app_id = ?`
-	rows, err := s.db.QueryContext(ctx, query, appID)
-	if err != nil {
-		return nil, models.NewErrInternalServer(fmt.Sprintf("failed to query deployments for app ID %d", appID), err)
+// GetDeploymentByID retrieves a deployment by ID using GORM
+func (s *SQLiteStore) GetDeploymentByID(ctx context.Context, id uint) (*models.Deployment, error) {
+	var deployment models.Deployment
+	result := s.db.WithContext(ctx).First(&deployment, id)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, models.NewErrNotFound(fmt.Sprintf("deployment with ID %d not found", id), result.Error)
+		}
+		return nil, fmt.Errorf("failed to get deployment by ID: %w", result.Error)
 	}
-	defer rows.Close()
+	return &deployment, nil
+}
 
+// GetDeploymentsByClientID retrieves all deployments for a specific client using GORM
+func (s *SQLiteStore) GetDeploymentsByClientID(ctx context.Context, clientID uint) ([]*models.Deployment, error) {
 	var deployments []*models.Deployment
-	for rows.Next() {
-		var deployment models.Deployment
-		var deployedAt sql.NullTime
-		err := rows.Scan(&deployment.ID, &deployment.AppId, &deployment.ClientID, &deployment.Name, &deployment.Description, &deployment.Environment, &deployment.Status, &deployment.URL, &deployment.Config, &deployedAt, &deployment.CreatedAt, &deployment.UpdatedAt)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to scan deployment row", err)
-		}
-		if deployedAt.Valid {
-			deployment.DeployedAt = deployedAt.Time
-		}
-		deployments = append(deployments, &deployment)
-	}
-	if err = rows.Err(); err != nil {
-		return nil, models.NewErrInternalServer(fmt.Sprintf("error iterating deployment rows for app ID %d", appID), err)
+	result := s.db.WithContext(ctx).Where("client_id = ?", clientID).Find(&deployments)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get deployments by client ID: %w", result.Error)
 	}
-
 	return deployments, nil
 }
 
-func (s *SQLiteStore) GetAllDeployments(ctx context.Context) ([]*models.Deployment, error) {
-	query := `SELECT id, app_id, client_id, name, description, environment, status, url, config, deployed_at, created_at, updated_at FROM deployments`
-	rows, err := s.db.QueryContext(ctx, query)
-	if err != nil {
-		return nil, models.NewErrInternalServer("failed to query all deployments", err)
-	}
-	defer rows.Close()
-
+// GetDeploymentsByAppID retrieves all deployments for a specific app using GORM
+func (s *SQLiteStore) GetDeploymentsByAppID(ctx context.Context, appID uint) ([]*models.Deployment, error) {
 	var deployments []*models.Deployment
-	for rows.Next() {
-		var deployment models.Deployment
-		var deployedAt sql.NullTime
-		err := rows.Scan(&deployment.ID, &deployment.AppId, &deployment.ClientID, &deployment.Name, &deployment.Description, &deployment.Environment, &deployment.Status, &deployment.URL, &deployment.Config, &deployedAt, &deployment.CreatedAt, &deployment.UpdatedAt)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to scan deployment row", err)
-		}
-		if deployedAt.Valid {
-			deployment.DeployedAt = deployedAt.Time
-		}
-		deployments = append(deployments, &deployment)
-	}
-	if err = rows.Err(); err != nil {
-		return nil, models.NewErrInternalServer("error iterating all deployment rows", err)
+	result := s.db.WithContext(ctx).Where("app_id = ?", appID).Find(&deployments)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get deployments by app ID: %w", result.Error)
 	}
-
 	return deployments, nil
 }
 
-func (s *SQLiteStore) GetDeploymentByID(ctx context.Context, id int) (*models.Deployment, error) {
-	var deployment models.Deployment
-	var deployedAt sql.NullTime
-	query := `SELECT id, app_id, client_id, name, description, environment, status, url, config, deployed_at, created_at, updated_at FROM deployments WHERE id = ?`
-	err := s.db.QueryRowContext(ctx, query, id).Scan(&deployment.ID, &deployment.AppId, &deployment.ClientID, &deployment.Name, &deployment.Description, &deployment.Environment, &deployment.Status, &deployment.URL, &deployment.Config, &deployedAt, &deployment.CreatedAt, &deployment.UpdatedAt)
-	if err != nil {
-		if errors.Is(err, sql.ErrNoRows) {
-			return nil, models.NewErrNotFound(fmt.Sprintf("deployment with ID %d not found", id), err)
-		}
-		return nil, models.NewErrInternalServer(fmt.Sprintf("failed to get deployment with ID %d", id), err)
-	}
-	if deployedAt.Valid {
-		deployment.DeployedAt = deployedAt.Time
+// GetAllDeployments retrieves all deployments using GORM
+func (s *SQLiteStore) GetAllDeployments(ctx context.Context) ([]*models.Deployment, error) {
+	var deployments []*models.Deployment
+	result := s.db.WithContext(ctx).Find(&deployments)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get all deployments: %w", result.Error)
 	}
-	return &deployment, nil
+	return deployments, nil
 }
 
+// UpdateDeployment updates an existing deployment using GORM
 func (s *SQLiteStore) UpdateDeployment(ctx context.Context, deployment *models.Deployment) error {
-	deployedAt := sql.NullTime{}
-	if !deployment.DeployedAt.IsZero() {
-		deployedAt.Time = deployment.DeployedAt
-		deployedAt.Valid = true
+	result := s.db.WithContext(ctx).Save(deployment)
+	if result.Error != nil {
+		return fmt.Errorf("failed to update deployment: %w", result.Error)
 	}
-
-	query := `UPDATE deployments SET app_id = ?, client_id = ?, name = ?, description = ?, environment = ?, status = ?, url = ?, config = ?, deployed_at = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`
-	result, err := s.db.ExecContext(ctx, query, deployment.AppId, deployment.ClientID, deployment.Name, deployment.Description, deployment.Environment, deployment.Status, deployment.URL, deployment.Config, deployedAt, deployment.ID)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update deployment with ID %d", deployment.ID), err)
-	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for deployment update ID %d", deployment.ID), err)
-	}
-	if rowsAffected == 0 {
+	if result.RowsAffected == 0 {
 		return models.NewErrNotFound(fmt.Sprintf("deployment with ID %d not found for update", deployment.ID), nil)
 	}
 	return nil
 }
 
-// DeleteDeployment deletes a deployment by ID with verification checks
-func (s *SQLiteStore) DeleteDeployment(ctx context.Context, id int) error {
-	// First check if the deployment exists
-	deployment, err := s.GetDeploymentByID(ctx, id)
-	if err != nil {
-		return err // GetDeploymentByID already returns custom errors
-	}
-
-	// Check if deployment is currently running
-	if deployment.Status == "running" || deployment.Status == "deploying" {
-		return models.NewErrConflict(fmt.Sprintf("cannot delete deployment: it is currently %s. Please stop the deployment first", deployment.Status), nil)
-	}
-
-	// Proceed with deletion
-	query := `DELETE FROM deployments WHERE id = ?`
-	result, err := s.db.ExecContext(ctx, query, id)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to delete deployment with ID %d", id), err)
+// DeleteDeployment deletes a deployment by ID using GORM
+func (s *SQLiteStore) DeleteDeployment(ctx context.Context, id uint) error {
+	result := s.db.WithContext(ctx).Delete(&models.Deployment{}, id)
+	if result.Error != nil {
+		return fmt.Errorf("failed to delete deployment: %w", result.Error)
 	}
-
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for deployment deletion ID %d", id), err)
-	}
-	if rowsAffected == 0 {
-		// This case should ideally be caught by GetDeploymentByID earlier, but as a safeguard:
+	if result.RowsAffected == 0 {
 		return models.NewErrNotFound(fmt.Sprintf("deployment with ID %d not found for deletion", id), nil)
 	}
-
 	return nil
 }
 
-// GetDeploymentsByClientID retrieves all deployments for a given client ID
-func (s *SQLiteStore) GetDeploymentsByClientID(ctx context.Context, clientID int) ([]*models.Deployment, error) {
-	query := `SELECT id, app_id, client_id, name, description, environment, status, url, config, deployed_at, created_at, updated_at FROM deployments WHERE client_id = ?`
-	rows, err := s.db.QueryContext(ctx, query, clientID)
-	if err != nil {
-		return nil, models.NewErrInternalServer(fmt.Sprintf("failed to query deployments for client ID %d", clientID), err)
-	}
-	defer rows.Close()
-
-	var deployments []*models.Deployment
-	for rows.Next() {
-		var deployment models.Deployment
-		var deployedAt sql.NullTime
-		err := rows.Scan(&deployment.ID, &deployment.AppId, &deployment.ClientID, &deployment.Name, &deployment.Description, &deployment.Environment, &deployment.Status, &deployment.URL, &deployment.Config, &deployedAt, &deployment.CreatedAt, &deployment.UpdatedAt)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to scan deployment row for client", err)
-		}
-		if deployedAt.Valid {
-			deployment.DeployedAt = deployedAt.Time
-		}
-		deployments = append(deployments, &deployment)
-	}
-	if err = rows.Err(); err != nil {
-		return nil, models.NewErrInternalServer(fmt.Sprintf("error iterating deployment rows for client ID %d", clientID), err)
-	}
-
-	return deployments, nil
-}
-
-// GetDeploymentsByUserID retrieves all deployments for a given user ID
-// This assumes a link between deployments and users, which might not be direct.
-// If deployments are linked via apps, and apps via users, this query would need to be more complex
-// or a direct user_id column added to deployments.
-// For now, assuming a direct user_id on deployments or this is handled via a join in a more complex setup.
-// If UserID is not directly on the deployments table, this will need adjustment.
-// For this example, let's assume there's a user_id on the apps table, and we join through it.
-// This requires an `apps` table with `user_id` and `id` (app_id in deployments).
-func (s *SQLiteStore) GetDeploymentsByUserID(ctx context.Context, userID int) ([]*models.Deployment, error) {
-	// This query assumes deployments are linked to users via the 'apps' table.
-	// Adjust if your schema is different (e.g., direct user_id on deployments).
-	query := `
-		SELECT d.id, d.app_id, d.client_id, d.name, d.description, d.environment, d.status, d.url, d.config, d.deployed_at, d.created_at, d.updated_at 
-		FROM deployments d
-		INNER JOIN apps a ON d.app_id = a.id
-		WHERE a.user_id = ?`
-
-	rows, err := s.db.QueryContext(ctx, query, userID)
-	if err != nil {
-		return nil, models.NewErrInternalServer(fmt.Sprintf("failed to query deployments for user ID %d", userID), err)
-	}
-	defer rows.Close()
-
+// GetDeploymentsByUserID retrieves all deployments for a specific user via their apps using GORM
+func (s *SQLiteStore) GetDeploymentsByUserID(ctx context.Context, userID uint) ([]*models.Deployment, error) {
 	var deployments []*models.Deployment
-	for rows.Next() {
-		var deployment models.Deployment
-		var deployedAt sql.NullTime
-		err := rows.Scan(&deployment.ID, &deployment.AppId, &deployment.ClientID, &deployment.Name, &deployment.Description, &deployment.Environment, &deployment.Status, &deployment.URL, &deployment.Config, &deployedAt, &deployment.CreatedAt, &deployment.UpdatedAt)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to scan deployment row for user", err)
-		}
-		if deployedAt.Valid {
-			deployment.DeployedAt = deployedAt.Time
-		}
-		deployments = append(deployments, &deployment)
-	}
-	if err = rows.Err(); err != nil {
-		return nil, models.NewErrInternalServer(fmt.Sprintf("error iterating deployment rows for user ID %d", userID), err)
+	result := s.db.WithContext(ctx).
+		Joins("JOIN apps ON deployments.app_id = apps.id").
+		Where("apps.user_id = ?", userID).
+		Find(&deployments)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get deployments by user ID: %w", result.Error)
 	}
-
 	return deployments, nil
 }

+ 93 - 314
dbstore/preview.go

@@ -2,373 +2,152 @@ package dbstore
 
 import (
 	"context"
-	"database/sql"
 	"fmt"
 
 	"git.linuxforward.com/byop/byop-engine/models"
-	"github.com/pkg/errors"
+	"gorm.io/gorm"
 )
 
-// CreatePreview creates a new preview record
-func (s *SQLiteStore) CreatePreview(ctx context.Context, preview *models.Preview) (int, error) {
-	query := `
-        INSERT INTO previews (app_id, status, expires_at) 
-        VALUES (?, ?, ?)
-    `
-	result, err := s.db.ExecContext(ctx, query, preview.AppID, preview.Status, preview.ExpiresAt)
-	if err != nil {
-		return 0, models.NewErrInternalServer("failed to create preview", err)
+// CreatePreview creates a new preview using GORM
+func (s *SQLiteStore) CreatePreview(ctx context.Context, preview *models.Preview) error {
+	result := s.db.WithContext(ctx).Create(preview)
+	if result.Error != nil {
+		return fmt.Errorf("failed to create preview: %w", result.Error)
 	}
-
-	id, err := result.LastInsertId()
-	if err != nil {
-		return 0, models.NewErrInternalServer("failed to get preview ID after creation", err)
-	}
-
-	return int(id), nil
+	return nil
 }
 
-// GetPreviewByID retrieves a preview by ID
-func (s *SQLiteStore) GetPreviewByID(ctx context.Context, id int) (*models.Preview, error) {
-	preview := &models.Preview{}
-	query := `
-        SELECT id, app_id, status, url, vps_id, ip_address, error_msg, 
-               build_logs, deploy_logs, expires_at, created_at, updated_at 
-        FROM previews 
-        WHERE id = ?
-    `
-	err := s.db.QueryRowContext(ctx, query, id).Scan(
-		&preview.ID, &preview.AppID, &preview.Status, &preview.URL,
-		&preview.VPSID, &preview.IPAddress, &preview.ErrorMsg,
-		&preview.BuildLogs, &preview.DeployLogs, &preview.ExpiresAt,
-		&preview.CreatedAt, &preview.UpdatedAt,
-	)
-	if err != nil {
-		if errors.Is(err, sql.ErrNoRows) {
-			return nil, models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found", id), err)
+// GetPreviewByID retrieves a preview by ID using GORM
+func (s *SQLiteStore) GetPreviewByID(ctx context.Context, id uint) (*models.Preview, error) {
+	var preview models.Preview
+	result := s.db.WithContext(ctx).First(&preview, id)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found", id), result.Error)
 		}
-		return nil, models.NewErrInternalServer(fmt.Sprintf("failed to get preview with ID %d", id), err)
+		return nil, fmt.Errorf("failed to get preview by ID: %w", result.Error)
 	}
-	return preview, nil
+	return &preview, nil
 }
 
-// GetPreviewsByAppID retrieves all previews for an app
-func (s *SQLiteStore) GetPreviewsByAppID(ctx context.Context, appID int) ([]*models.Preview, error) {
-	query := `
-        SELECT id, app_id, status, url, vps_id, ip_address, error_msg, 
-               build_logs, deploy_logs, expires_at, created_at, updated_at 
-        FROM previews 
-        WHERE app_id = ?
-        ORDER BY created_at DESC
-    `
-	rows, err := s.db.QueryContext(ctx, query, appID)
-	if err != nil {
-		return nil, models.NewErrInternalServer(fmt.Sprintf("failed to get previews for app ID %d", appID), err)
+// GetPreviewByAppID retrieves a preview by app ID using GORM
+func (s *SQLiteStore) GetPreviewByAppID(ctx context.Context, appID uint) (*models.Preview, error) {
+	var preview models.Preview
+	result := s.db.WithContext(ctx).Where("app_id = ?", appID).First(&preview)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, models.NewErrNotFound(fmt.Sprintf("preview for app ID %d not found", appID), result.Error)
+		}
+		return nil, fmt.Errorf("failed to get preview by app ID: %w", result.Error)
 	}
-	defer rows.Close()
+	return &preview, nil
+}
 
+// GetPreviewsByAppID retrieves all previews for an app using GORM
+func (s *SQLiteStore) GetPreviewsByAppID(ctx context.Context, appID uint) ([]*models.Preview, error) {
 	var previews []*models.Preview
-	for rows.Next() {
-		var preview models.Preview
-		err := rows.Scan(
-			&preview.ID, &preview.AppID, &preview.Status, &preview.URL,
-			&preview.VPSID, &preview.IPAddress, &preview.ErrorMsg,
-			&preview.BuildLogs, &preview.DeployLogs, &preview.ExpiresAt,
-			&preview.CreatedAt, &preview.UpdatedAt,
-		)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to scan preview row", err)
-		}
-		previews = append(previews, &preview)
-	}
-	if err = rows.Err(); err != nil {
-		return nil, models.NewErrInternalServer(fmt.Sprintf("error iterating preview rows for app ID %d", appID), err)
+	result := s.db.WithContext(ctx).Where("app_id = ?", appID).Find(&previews)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get previews by app ID: %w", result.Error)
 	}
-
 	return previews, nil
 }
 
-// GetAllPreviews retrieves all previews (for admin purposes)
+// GetAllPreviews retrieves all previews using GORM
 func (s *SQLiteStore) GetAllPreviews(ctx context.Context) ([]*models.Preview, error) {
-	query := `
-        SELECT id, app_id, status, url, vps_id, ip_address, error_msg, 
-               build_logs, deploy_logs, expires_at, created_at, updated_at 
-        FROM previews 
-        ORDER BY created_at DESC
-    `
-	rows, err := s.db.QueryContext(ctx, query)
-	if err != nil {
-		return nil, models.NewErrInternalServer("failed to get all previews", err)
-	}
-	defer rows.Close()
-
 	var previews []*models.Preview
-	for rows.Next() {
-		var preview models.Preview
-		err := rows.Scan(
-			&preview.ID, &preview.AppID, &preview.Status, &preview.URL,
-			&preview.VPSID, &preview.IPAddress, &preview.ErrorMsg,
-			&preview.BuildLogs, &preview.DeployLogs, &preview.ExpiresAt,
-			&preview.CreatedAt, &preview.UpdatedAt,
-		)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to scan preview row", err)
-		}
-		previews = append(previews, &preview)
+	result := s.db.WithContext(ctx).Find(&previews)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get all previews: %w", result.Error)
 	}
-	if err = rows.Err(); err != nil {
-		return nil, models.NewErrInternalServer("error iterating all preview rows", err)
-	}
-
 	return previews, nil
 }
 
-// UpdatePreviewStatus updates the status and error message of a preview
-func (s *SQLiteStore) UpdatePreviewStatus(ctx context.Context, previewID int, status, errorMsg string) error {
-	query := `
-        UPDATE previews 
-        SET status = ?, error_msg = ?, updated_at = CURRENT_TIMESTAMP 
-        WHERE id = ?
-    `
-	result, err := s.db.ExecContext(ctx, query, status, errorMsg, previewID)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update preview status for ID %d", previewID), err)
-	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for preview status update ID %d", previewID), err)
-	}
-	if rowsAffected == 0 {
-		return models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found for status update", previewID), nil)
+// GetPreviewsByStatus retrieves previews by status using GORM
+func (s *SQLiteStore) GetPreviewsByStatus(ctx context.Context, status string) ([]*models.Preview, error) {
+	var previews []*models.Preview
+	result := s.db.WithContext(ctx).Where("status = ?", status).Find(&previews)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get previews by status: %w", result.Error)
 	}
-	return nil
+	return previews, nil
 }
 
-// UpdatePreviewVPS updates the VPS information for a preview
-func (s *SQLiteStore) UpdatePreviewVPS(ctx context.Context, previewID int, vpsID, ipAddress, url string) error {
-	query := `
-        UPDATE previews 
-        SET vps_id = ?, ip_address = ?, url = ?, updated_at = CURRENT_TIMESTAMP 
-        WHERE id = ?
-    `
-	result, err := s.db.ExecContext(ctx, query, vpsID, ipAddress, url, previewID)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update preview VPS info for ID %d", previewID), err)
-	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for preview VPS update ID %d", previewID), err)
+// UpdatePreview updates an existing preview using GORM
+func (s *SQLiteStore) UpdatePreview(ctx context.Context, preview *models.Preview) error {
+	result := s.db.WithContext(ctx).Save(preview)
+	if result.Error != nil {
+		return fmt.Errorf("failed to update preview: %w", result.Error)
 	}
-	if rowsAffected == 0 {
-		return models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found for VPS update", previewID), nil)
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found for update", preview.ID), nil)
 	}
 	return nil
 }
 
-// UpdatePreviewBuildLogs updates the build logs for a preview
-func (s *SQLiteStore) UpdatePreviewBuildLogs(ctx context.Context, previewID int, buildLogs string) error {
-	query := `
-        UPDATE previews 
-        SET build_logs = ?, updated_at = CURRENT_TIMESTAMP 
-        WHERE id = ?
-    `
-	result, err := s.db.ExecContext(ctx, query, buildLogs, previewID)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update preview build logs for ID %d", previewID), err)
+// DeletePreview deletes a preview by ID using GORM
+func (s *SQLiteStore) DeletePreview(ctx context.Context, id uint) error {
+	result := s.db.WithContext(ctx).Delete(&models.Preview{}, id)
+	if result.Error != nil {
+		return fmt.Errorf("failed to delete preview: %w", result.Error)
 	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for preview build logs update ID %d", previewID), err)
-	}
-	if rowsAffected == 0 {
-		return models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found for build logs update", previewID), nil)
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found for deletion", id), nil)
 	}
 	return nil
 }
 
-// UpdatePreviewDeployLogs updates the deploy logs for a preview
-func (s *SQLiteStore) UpdatePreviewDeployLogs(ctx context.Context, previewID int, deployLogs string) error {
-	query := `
-        UPDATE previews 
-        SET deploy_logs = ?, updated_at = CURRENT_TIMESTAMP 
-        WHERE id = ?
-    `
-	result, err := s.db.ExecContext(ctx, query, deployLogs, previewID)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update preview deploy logs for ID %d", previewID), err)
-	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for preview deploy logs update ID %d", previewID), err)
-	}
-	if rowsAffected == 0 {
-		return models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found for deploy logs update", previewID), nil)
+// UpdatePreviewVPS updates VPS information for a preview using GORM
+func (s *SQLiteStore) UpdatePreviewVPS(ctx context.Context, previewID uint, vpsID string, ipAddress string, previewURL string) error {
+	result := s.db.WithContext(ctx).Model(&models.Preview{}).Where("id = ?", previewID).Updates(map[string]interface{}{
+		"vps_id":     vpsID,
+		"ip_address": ipAddress,
+		"url":        previewURL,
+	})
+	if result.Error != nil {
+		return fmt.Errorf("failed to update preview VPS: %w", result.Error)
+	}
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found for VPS update", previewID), nil)
 	}
 	return nil
 }
 
-// UpdatePreview updates a preview record
-func (s *SQLiteStore) UpdatePreview(ctx context.Context, preview models.Preview) error {
-	query := `
-        UPDATE previews 
-        SET app_id = ?, status = ?, url = ?, vps_id = ?, ip_address = ?, 
-            error_msg = ?, build_logs = ?, deploy_logs = ?, expires_at = ?, 
-            updated_at = CURRENT_TIMESTAMP 
-        WHERE id = ?
-    `
-	result, err := s.db.ExecContext(ctx, query,
-		preview.AppID, preview.Status, preview.URL, preview.VPSID,
-		preview.IPAddress, preview.ErrorMsg, preview.BuildLogs,
-		preview.DeployLogs, preview.ExpiresAt, preview.ID,
-	)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update preview with ID %d", preview.ID), err)
+// UpdatePreviewStatus updates the status and error message of a preview using GORM
+func (s *SQLiteStore) UpdatePreviewStatus(ctx context.Context, previewID uint, status string, errorMsg string) error {
+	result := s.db.WithContext(ctx).Model(&models.Preview{}).Where("id = ?", previewID).Updates(map[string]interface{}{
+		"status":    status,
+		"error_msg": errorMsg,
+	})
+	if result.Error != nil {
+		return fmt.Errorf("failed to update preview status: %w", result.Error)
 	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for preview update ID %d", preview.ID), err)
-	}
-	if rowsAffected == 0 {
-		return models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found for update", preview.ID), nil)
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found for status update", previewID), nil)
 	}
 	return nil
 }
 
-// DeletePreview deletes a preview record
-func (s *SQLiteStore) DeletePreview(ctx context.Context, previewID int) error {
-	query := `DELETE FROM previews WHERE id = ?`
-	result, err := s.db.ExecContext(ctx, query, previewID)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to delete preview with ID %d", previewID), err)
-	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for preview deletion ID %d", previewID), err)
+// UpdatePreviewBuildLogs updates the build logs of a preview using GORM
+func (s *SQLiteStore) UpdatePreviewBuildLogs(ctx context.Context, previewID uint, logs string) error {
+	result := s.db.WithContext(ctx).Model(&models.Preview{}).Where("id = ?", previewID).Update("build_logs", logs)
+	if result.Error != nil {
+		return fmt.Errorf("failed to update preview build logs: %w", result.Error)
 	}
-	if rowsAffected == 0 {
-		return models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found for deletion", previewID), nil)
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found for build logs update", previewID), nil)
 	}
 	return nil
 }
 
-// GetExpiredPreviews gets all previews that have expired (for cleanup jobs)
-func (s *SQLiteStore) GetExpiredPreviews(ctx context.Context) ([]*models.Preview, error) {
-	query := `
-        SELECT id, app_id, status, url, vps_id, ip_address, error_msg, 
-               build_logs, deploy_logs, expires_at, created_at, updated_at 
-        FROM previews 
-        WHERE expires_at < datetime('now') AND status != 'stopped'
-        ORDER BY expires_at ASC
-    `
-	rows, err := s.db.QueryContext(ctx, query)
-	if err != nil {
-		return nil, models.NewErrInternalServer("failed to get expired previews", err)
-	}
-	defer rows.Close()
-
-	var previews []*models.Preview
-	for rows.Next() {
-		var preview models.Preview
-		err := rows.Scan(
-			&preview.ID, &preview.AppID, &preview.Status, &preview.URL,
-			&preview.VPSID, &preview.IPAddress, &preview.ErrorMsg,
-			&preview.BuildLogs, &preview.DeployLogs, &preview.ExpiresAt,
-			&preview.CreatedAt, &preview.UpdatedAt,
-		)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to scan expired preview row", err)
-		}
-		previews = append(previews, &preview)
-	}
-	if err = rows.Err(); err != nil {
-		return nil, models.NewErrInternalServer("error iterating expired preview rows", err)
-	}
-
-	return previews, nil
-}
-
-// GetPreviewsByStatus retrieves all previews with a specific status
-func (s *SQLiteStore) GetPreviewsByStatus(ctx context.Context, status string) ([]*models.Preview, error) {
-	query := `SELECT id, app_id, status, vps_id, ip_address, url, build_logs, deploy_logs, error_msg, expires_at, created_at, updated_at FROM previews WHERE status = ?`
-
-	rows, err := s.db.QueryContext(ctx, query, status)
-	if err != nil {
-		return nil, models.NewErrInternalServer(fmt.Sprintf("failed to get previews with status %s", status), err)
+// UpdatePreviewDeployLogs updates the deploy logs of a preview using GORM
+func (s *SQLiteStore) UpdatePreviewDeployLogs(ctx context.Context, previewID uint, logs string) error {
+	result := s.db.WithContext(ctx).Model(&models.Preview{}).Where("id = ?", previewID).Update("deploy_logs", logs)
+	if result.Error != nil {
+		return fmt.Errorf("failed to update preview deploy logs: %w", result.Error)
 	}
-	defer rows.Close()
-
-	var previews []*models.Preview
-	for rows.Next() {
-		var preview models.Preview
-		err := rows.Scan(
-			&preview.ID,
-			&preview.AppID,
-			&preview.Status,
-			&preview.VPSID,
-			&preview.IPAddress,
-			&preview.URL,
-			&preview.BuildLogs,
-			&preview.DeployLogs,
-			&preview.ErrorMsg,
-			&preview.ExpiresAt,
-			&preview.CreatedAt,
-			&preview.UpdatedAt,
-		)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to scan preview row", err)
-		}
-		previews = append(previews, &preview)
-	}
-	if err = rows.Err(); err != nil {
-		return nil, models.NewErrInternalServer(fmt.Sprintf("error iterating preview rows with status %s", status), err)
-	}
-
-	return previews, nil
-}
-
-// UpdateAppPreview updates the app with preview information
-func (s *SQLiteStore) UpdateAppPreview(ctx context.Context, appID, previewID int, previewURL string) error {
-	query := `
-        UPDATE apps 
-        SET preview_id = ?, preview_url = ?, status = 'ready', updated_at = CURRENT_TIMESTAMP 
-        WHERE id = ?
-    `
-	result, err := s.db.ExecContext(ctx, query, previewID, previewURL, appID)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update app preview info for app ID %d", appID), err)
-	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for app preview update, app ID %d", appID), err)
-	}
-	if rowsAffected == 0 {
-		return models.NewErrNotFound(fmt.Sprintf("app with ID %d not found for preview update", appID), nil)
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("preview with ID %d not found for deploy logs update", previewID), nil)
 	}
 	return nil
 }
-
-// GetPreviewByAppID retrieves the latest preview for an app
-func (s *SQLiteStore) GetPreviewByAppID(ctx context.Context, appID int) (*models.Preview, error) {
-	query := `
-		SELECT id, app_id, status, url, vps_id, ip_address, error_msg, 
-			   build_logs, deploy_logs, expires_at, created_at, updated_at 
-		FROM previews 
-		WHERE app_id = ? 
-		ORDER BY created_at DESC 
-		LIMIT 1
-	`
-	preview := &models.Preview{}
-	err := s.db.QueryRowContext(ctx, query, appID).Scan(
-		&preview.ID, &preview.AppID, &preview.Status, &preview.URL,
-		&preview.VPSID, &preview.IPAddress, &preview.ErrorMsg,
-		&preview.BuildLogs, &preview.DeployLogs, &preview.ExpiresAt,
-		&preview.CreatedAt, &preview.UpdatedAt,
-	)
-	if err != nil {
-		if errors.Is(err, sql.ErrNoRows) {
-			return nil, nil
-		}
-		return nil, models.NewErrInternalServer(fmt.Sprintf("failed to get latest preview for app ID %d", appID), err)
-	}
-	return preview, nil
-}

+ 98 - 327
dbstore/store.go

@@ -7,84 +7,83 @@ import (
 	"os"
 
 	"git.linuxforward.com/byop/byop-engine/models"
-	_ "github.com/mattn/go-sqlite3" // Import SQLite driver
+	_ "github.com/mattn/go-sqlite3" // Import SQLite driver for compatibility
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
 )
 
 // Store defines the interface for all database operations.
 // This will include methods for all models (User, Client, App, Component, Deployment, Ticket, etc.)
 type Store interface {
 	// User methods
-	CreateUser(ctx context.Context, user models.User) (int, error) // Updated signature
+	CreateUser(ctx context.Context, user *models.User) error
 	GetUserByEmail(ctx context.Context, email string) (*models.User, error)
-	GetUserByID(ctx context.Context, id int) (*models.User, error)
-	GetUsers(ctx context.Context) ([]models.User, error) // Method to get all users
+	GetUserByID(ctx context.Context, id uint) (*models.User, error)
+	GetUsers(ctx context.Context) ([]*models.User, error)
 	UpdateUser(ctx context.Context, user *models.User) error
-	DeleteUser(ctx context.Context, id int64) error // Updated signature
-	// ... other user methods
+	DeleteUser(ctx context.Context, id uint) error
+	CreateDefaultAdmin(ctx context.Context) error
+	GetByUsername(ctx context.Context, username string) (*models.User, error)
 
 	// Client methods
-	CreateClient(ctx context.Context, client models.Client) (int, error) // Corrected signature
-	GetClientByID(ctx context.Context, id int) (*models.Client, error)   // Changed to pointer
-	GetClients(ctx context.Context) ([]models.Client, error)
-	UpdateClient(ctx context.Context, client *models.Client) error // Changed to pointer
-	DeleteClient(ctx context.Context, id int) error
-	// ... other client methods
+	CreateClient(ctx context.Context, client *models.Client) error
+	GetAllClients(ctx context.Context) ([]*models.Client, error)
+	GetClientByID(ctx context.Context, id uint) (*models.Client, error)
+	UpdateClient(ctx context.Context, client *models.Client) error
+	DeleteClient(ctx context.Context, id uint) error
 
 	// App methods
-	CreateApp(ctx context.Context, app *models.App) (int, error) // Corrected signature
-	GetAppByID(ctx context.Context, id int) (*models.App, error)
-	GetAppsByUserID(ctx context.Context, userID int) ([]models.App, error) // Added method
+	CreateApp(ctx context.Context, app *models.App) error
+	GetAppByID(ctx context.Context, id uint) (*models.App, error)
+	GetAppsByUserID(ctx context.Context, userID uint) ([]*models.App, error)
 	UpdateApp(ctx context.Context, app *models.App) error
-	DeleteApp(ctx context.Context, id int) error
-	UpdateAppStatus(ctx context.Context, appID int, status string, message string) error          // Added, changed models.AppStatus to string
-	UpdateAppPreview(ctx context.Context, appID int, previewID int, previewURL string) error      // Added
-	GetAllApps(ctx context.Context) ([]*models.App, error)                                        // Updated signature
-	UpdateAppCurrentImage(ctx context.Context, appID int, imageTag string, imageURI string) error // Added
-	// ... other app methods
+	DeleteApp(ctx context.Context, id uint) error
+	UpdateAppStatus(ctx context.Context, appID uint, status string, message string) error
+	UpdateAppPreview(ctx context.Context, appID uint, previewID uint, previewURL string) error
+	GetAllApps(ctx context.Context) ([]*models.App, error)
+	UpdateAppCurrentImage(ctx context.Context, appID uint, imageTag string, imageURI string) error
 
 	// Component methods
-	CreateComponent(ctx context.Context, component *models.Component) (int, error) // Updated signature
-	GetComponentByID(ctx context.Context, id int) (*models.Component, error)
-	GetComponentsByUserID(ctx context.Context, userID int) ([]models.Component, error)
+	CreateComponent(ctx context.Context, component *models.Component) error
+	GetComponentByID(ctx context.Context, id uint) (*models.Component, error)
+	GetComponentsByUserID(ctx context.Context, userID uint) ([]*models.Component, error)
+	GetAllComponents(ctx context.Context) ([]*models.Component, error)
 	UpdateComponent(ctx context.Context, component *models.Component) error
-	DeleteComponent(ctx context.Context, id int) error
-	// ... other component methods
+	UpdateComponentStatus(ctx context.Context, componentID uint, status string, errorMsg string) error
+	DeleteComponent(ctx context.Context, id uint) error
 
 	// Deployment methods
-	CreateDeployment(ctx context.Context, deployment models.Deployment) (int, error) // Updated signature
-	GetDeploymentByID(ctx context.Context, id int) (*models.Deployment, error)
-	GetDeploymentsByAppID(ctx context.Context, appID int) ([]models.Deployment, error)
-	GetDeploymentsByClientID(ctx context.Context, clientID int) ([]models.Deployment, error)
-	GetDeploymentsByUserID(ctx context.Context, userID int) ([]models.Deployment, error) // Assuming deployments can be linked to users indirectly
+	CreateDeployment(ctx context.Context, deployment *models.Deployment) error
+	GetDeploymentByID(ctx context.Context, id uint) (*models.Deployment, error)
+	GetDeploymentsByClientID(ctx context.Context, clientID uint) ([]*models.Deployment, error)
+	GetAllDeployments(ctx context.Context) ([]*models.Deployment, error)
 	UpdateDeployment(ctx context.Context, deployment *models.Deployment) error
-	DeleteDeployment(ctx context.Context, id int) error
-	// ... other deployment methods
+	DeleteDeployment(ctx context.Context, id uint) error
 
 	// Preview methods
-	CreatePreview(ctx context.Context, preview *models.Preview) (int, error) // Corrected signature
-	GetPreviewByID(ctx context.Context, id int) (*models.Preview, error)
-	GetPreviewByAppID(ctx context.Context, appID int) (*models.Preview, error)
+	CreatePreview(ctx context.Context, preview *models.Preview) error
+	GetPreviewByID(ctx context.Context, id uint) (*models.Preview, error)
+	GetPreviewByAppID(ctx context.Context, appID uint) (*models.Preview, error)
 	UpdatePreview(ctx context.Context, preview *models.Preview) error
-	DeletePreview(ctx context.Context, id int) error
-	UpdatePreviewVPS(ctx context.Context, previewID int, vpsID string, ipAddress string, previewURL string) error // Added
-	UpdatePreviewStatus(ctx context.Context, previewID int, status string, errorMsg string) error                 // Added, changed models.PreviewStatus to string
-	UpdatePreviewBuildLogs(ctx context.Context, previewID int, logs string) error                                 // Added
-	UpdatePreviewDeployLogs(ctx context.Context, previewID int, logs string) error                                // Added
-	GetPreviewsByStatus(ctx context.Context, status string) ([]models.Preview, error)                             // Added, changed models.PreviewStatus to string
-	GetPreviewsByAppID(ctx context.Context, appID int) ([]models.Preview, error)                                  // Added
-	// ... other preview methods
+	DeletePreview(ctx context.Context, id uint) error
+	UpdatePreviewVPS(ctx context.Context, previewID uint, vpsID string, ipAddress string, previewURL string) error
+	UpdatePreviewStatus(ctx context.Context, previewID uint, status string, errorMsg string) error
+	UpdatePreviewBuildLogs(ctx context.Context, previewID uint, logs string) error
+	UpdatePreviewDeployLogs(ctx context.Context, previewID uint, logs string) error
+	GetAllPreviews(ctx context.Context) ([]*models.Preview, error)
+	GetPreviewsByStatus(ctx context.Context, status string) ([]*models.Preview, error)
+	GetPreviewsByAppID(ctx context.Context, appID uint) ([]*models.Preview, error)
 
 	// Ticket methods
 	CreateTicket(ctx context.Context, ticket *models.Ticket) error
-	GetTicketByID(ctx context.Context, id int) (*models.Ticket, error)
-	GetTickets(ctx context.Context) ([]models.Ticket, error) // Add filters later (status, user, client)
+	GetTicketByID(ctx context.Context, id uint) (*models.Ticket, error)
+	GetTickets(ctx context.Context) ([]*models.Ticket, error)
 	UpdateTicket(ctx context.Context, ticket *models.Ticket) error
-	// DeleteTicket(ctx context.Context, id int) error // Optional
 
 	// TicketComment methods
 	CreateTicketComment(ctx context.Context, comment *models.TicketComment) error
-	GetTicketComments(ctx context.Context, ticketID int) ([]models.TicketComment, error)
-	// ... other ticket comment methods
+	GetTicketComments(ctx context.Context, ticketID uint) ([]*models.TicketComment, error)
 
 	// BuildJob methods
 	CreateBuildJob(ctx context.Context, job *models.BuildJob) error
@@ -92,21 +91,23 @@ type Store interface {
 	UpdateBuildJob(ctx context.Context, job *models.BuildJob) error
 	UpdateBuildJobStatus(ctx context.Context, id uint, status models.BuildStatus, errorMessage string) error
 	AppendBuildJobLog(ctx context.Context, id uint, logMessage string) error
-	GetQueuedBuildJobs(ctx context.Context, limit int) ([]models.BuildJob, error)
-	GetBuildJobsByAppID(ctx context.Context, appID uint, page, pageSize int) ([]models.BuildJob, int64, error)
+	GetQueuedBuildJobs(ctx context.Context, limit int) ([]*models.BuildJob, error)
+	GetBuildJobsByAppID(ctx context.Context, appID uint, page, pageSize int) ([]*models.BuildJob, int64, error)
 
 	// General DB methods
 	GetDB() *sql.DB
+	GetGormDB() *gorm.DB
 	Close() error
 }
 
 // SQLiteStore implements the Store interface for SQLite using GORM
 type SQLiteStore struct {
-	db  *sql.DB
-	dsn string
+	db    *gorm.DB
+	rawDB *sql.DB // Keep for backward compatibility and specific raw SQL operations
+	dsn   string
 }
 
-// NewSQLiteStore initializes a new SQLiteStore
+// NewSQLiteStore initializes a new SQLiteStore with GORM
 func NewSQLiteStore(dataSourceName string) (*SQLiteStore, error) {
 	// First check if the database file exists
 	isNewDb := !fileExists(dataSourceName)
@@ -119,293 +120,54 @@ func NewSQLiteStore(dataSourceName string) (*SQLiteStore, error) {
 		defer file.Close()
 	}
 
-	// Open SQLite database and SQLite-specific configuration
-	db, err := sql.Open("sqlite3", dataSourceName)
+	// Open GORM database
+	gormDB, err := gorm.Open(sqlite.Open(dataSourceName), &gorm.Config{
+		Logger: logger.Default.LogMode(logger.Silent), // Change to logger.Info for debugging
+	})
 	if err != nil {
-		return nil, fmt.Errorf("failed to open database: %w", err)
+		return nil, fmt.Errorf("failed to open database with GORM: %w", err)
 	}
 
-	if err := db.Ping(); err != nil {
-		return nil, fmt.Errorf("failed to ping database: %w", err)
-	}
-
-	err = createTables(db)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create tables: %w", err)
-	}
-
-	// Run migrations after table creation
-	err = runMigrations(db)
-	if err != nil {
-		return nil, fmt.Errorf("failed to run migrations: %w", err)
-	}
-	// Enable foreign keys in SQLite after migrations
-	_, err = db.Exec("PRAGMA foreign_keys = ON")
-	if err != nil {
-		return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
-	}
-
-	// Check if the database is well-formed
-	if isNewDb {
-		// If this is a new database, we can assume it's well-formed after creating tables
-	} else {
-		// If the database already exists, we can run a simple query to check its integrity
-		var count int
-		err = db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table'").Scan(&count)
-		if err != nil {
-			return nil, fmt.Errorf("failed to check database integrity: %w", err)
-		}
-		if count == 0 {
-			return nil, fmt.Errorf("database is empty or not well-formed")
-		}
-	}
-
-	return &SQLiteStore{
-		db:  db,
-		dsn: dataSourceName,
-	}, nil
-}
-
-// createTables creates all necessary tables
-func createTables(db *sql.DB) error {
-	queries := []string{
-		`CREATE TABLE IF NOT EXISTS users (
-			id INTEGER PRIMARY KEY AUTOINCREMENT,
-			email TEXT UNIQUE NOT NULL,
-			password TEXT NOT NULL,
-			name TEXT NOT NULL,
-			role TEXT DEFAULT 'user',
-			active BOOLEAN DEFAULT true,
-			created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
-		)`,
-		`CREATE TABLE IF NOT EXISTS clients (
-			id INTEGER PRIMARY KEY AUTOINCREMENT,
-			name TEXT NOT NULL,
-			description TEXT,
-			contact_info TEXT,
-			active BOOLEAN DEFAULT true,
-			created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
-		)`,
-		`CREATE TABLE IF NOT EXISTS components (
-			id INTEGER PRIMARY KEY AUTOINCREMENT,
-			user_id INTEGER NOT NULL,
-			name TEXT NOT NULL,
-			description TEXT,
-			type TEXT,
-			status TEXT DEFAULT 'active',
-			config TEXT DEFAULT '{}',
-			repository TEXT,
-			branch TEXT DEFAULT 'main',
-			error_msg TEXT, 
-			created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			FOREIGN KEY (user_id) REFERENCES users(id)
-		)`,
-		`CREATE TABLE IF NOT EXISTS deployments (
-			id INTEGER PRIMARY KEY AUTOINCREMENT,
-			app_id INTEGER NOT NULL,
-			client_id INTEGER NOT NULL,
-			name TEXT NOT NULL,
-			description TEXT,
-			environment TEXT DEFAULT 'development',
-			status TEXT DEFAULT 'pending',
-			url TEXT,
-			config TEXT DEFAULT '{}',
-			deployed_at DATETIME,
-			created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			FOREIGN KEY (app_id) REFERENCES apps(id),
-			FOREIGN KEY (client_id) REFERENCES clients(id)
-		)`,
-		`CREATE TABLE IF NOT EXISTS apps (
-			id INTEGER PRIMARY KEY AUTOINCREMENT,
-			user_id INTEGER NOT NULL,
-			name TEXT NOT NULL,
-			description TEXT,
-			status TEXT DEFAULT 'building',
-			components TEXT DEFAULT '[]', -- JSON array of component IDs
-			preview_id INTEGER,
-			preview_url TEXT DEFAULT '',
-			current_image_tag TEXT DEFAULT '',
-			current_image_uri TEXT DEFAULT '',
-			error_msg TEXT DEFAULT '',
-			created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			FOREIGN KEY (user_id) REFERENCES users(id),
-			FOREIGN KEY (preview_id) REFERENCES previews(id) ON DELETE SET NULL
-		)`,
-		`CREATE TABLE IF NOT EXISTS providers (
-			id INTEGER PRIMARY KEY AUTOINCREMENT,
-			name TEXT NOT NULL,
-			type TEXT NOT NULL,
-			config TEXT DEFAULT '{}',
-			active BOOLEAN DEFAULT true,
-			created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
-		)`,
-		`CREATE TABLE IF NOT EXISTS tickets (
-			id INTEGER PRIMARY KEY AUTOINCREMENT,
-			client_id INTEGER NOT NULL,
-			title TEXT NOT NULL,
-			description TEXT,
-			status TEXT DEFAULT 'open',
-			priority TEXT DEFAULT 'medium',
-			assigned_to INTEGER,
-			created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			FOREIGN KEY (client_id) REFERENCES clients(id),
-			FOREIGN KEY (assigned_to) REFERENCES users(id)
-		)`,
-		`CREATE TABLE IF NOT EXISTS previews (
-			id INTEGER PRIMARY KEY AUTOINCREMENT,
-			app_id INTEGER NOT NULL,
-			status TEXT NOT NULL DEFAULT 'building',
-			url TEXT DEFAULT '',
-			vps_id TEXT DEFAULT '',
-			ip_address TEXT DEFAULT '',
-			error_msg TEXT DEFAULT '',
-			build_logs TEXT DEFAULT '',
-			deploy_logs TEXT DEFAULT '',
-			expires_at TEXT NOT NULL,
-			created_at TEXT DEFAULT CURRENT_TIMESTAMP,
-			updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
-			FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE
-		);`,
-		`CREATE TABLE IF NOT EXISTS ticket_comments (
-			id INTEGER PRIMARY KEY AUTOINCREMENT,
-			ticket_id INTEGER NOT NULL,
-			user_id INTEGER NOT NULL,
-			content TEXT NOT NULL,
-			created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
-			FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
-		)`,
-		`CREATE TABLE IF NOT EXISTS build_jobs (
-			id INTEGER PRIMARY KEY AUTOINCREMENT,
-			component_id INTEGER NOT NULL,
-			request_id TEXT UNIQUE,
-			source_url TEXT NOT NULL,
-			version TEXT,
-			status TEXT NOT NULL,
-			image_name TEXT,
-			image_tag TEXT,
-			full_image_uri TEXT,
-			registry_url TEXT,
-			registry_user TEXT,
-			registry_password TEXT,
-			build_context TEXT,
-			dockerfile TEXT,
-			llb_definition BLOB,
-			dockerfile_content TEXT,
-			no_cache BOOLEAN,
-			build_args TEXT,
-			logs TEXT,
-			error_message TEXT,
-			requested_at DATETIME NOT NULL,
-			started_at DATETIME,
-			finished_at DATETIME,
-			worker_node_id TEXT,
-			created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-			FOREIGN KEY (component_id) REFERENCES components(id) ON DELETE CASCADE
-		)`,
-	}
-
-	for _, query := range queries {
-		if _, err := db.Exec(query); err != nil {
-			return fmt.Errorf("failed to create table: %w", err)
-		}
-	}
-
-	return nil
-}
-
-// runMigrations handles database schema migrations for existing databases
-func runMigrations(db *sql.DB) error {
-	// Migration 1: Add image tracking columns to components table
-	err := addComponentImageColumns(db)
+	// Get underlying SQL DB for backward compatibility
+	rawDB, err := gormDB.DB()
 	if err != nil {
-		return fmt.Errorf("failed to add image columns to components table: %w", err)
+		return nil, fmt.Errorf("failed to get underlying SQL DB: %w", err)
 	}
 
-	// Migration 2: Add dockerfile_content column to build_jobs table
-	err = addDockerfileContentColumn(db)
-	if err != nil {
-		return fmt.Errorf("failed to add dockerfile_content column to build_jobs table: %w", err)
+	if err := rawDB.Ping(); err != nil {
+		return nil, fmt.Errorf("failed to ping database: %w", err)
 	}
 
-	return nil
-}
-
-// addComponentImageColumns adds current_image_tag and current_image_uri columns to components table
-func addComponentImageColumns(db *sql.DB) error {
-	// Check if columns already exist
-	var count int
-	err := db.QueryRow(`
-		SELECT COUNT(*) 
-		FROM pragma_table_info('components') 
-		WHERE name IN ('current_image_tag', 'current_image_uri')
-	`).Scan(&count)
+	// Enable foreign keys in SQLite
+	err = gormDB.Exec("PRAGMA foreign_keys = ON").Error
 	if err != nil {
-		return fmt.Errorf("failed to check existing columns: %w", err)
-	}
-
-	// If both columns already exist, skip migration
-	if count >= 2 {
-		return nil
-	}
-
-	// Add the missing columns
-	migrations := []string{
-		`ALTER TABLE components ADD COLUMN current_image_tag TEXT DEFAULT ''`,
-		`ALTER TABLE components ADD COLUMN current_image_uri TEXT DEFAULT ''`,
-	}
-
-	for _, migration := range migrations {
-		_, err := db.Exec(migration)
-		if err != nil {
-			// Ignore "duplicate column name" errors in case the column already exists
-			if err.Error() != "duplicate column name: current_image_tag" &&
-				err.Error() != "duplicate column name: current_image_uri" {
-				return fmt.Errorf("failed to execute migration '%s': %w", migration, err)
-			}
-		}
+		return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
 	}
 
-	return nil
-}
-
-// addDockerfileContentColumn adds dockerfile_content column to build_jobs table
-func addDockerfileContentColumn(db *sql.DB) error {
-	// Check if column already exists
-	var count int
-	err := db.QueryRow(`
-		SELECT COUNT(*) 
-		FROM pragma_table_info('build_jobs') 
-		WHERE name = 'dockerfile_content'
-	`).Scan(&count)
+	// Auto-migrate all models
+	err = gormDB.AutoMigrate(
+		&models.User{},
+		&models.Client{},
+		&models.App{},
+		&models.Component{},
+		&models.Deployment{},
+		&models.Preview{},
+		&models.Ticket{},
+		&models.TicketComment{},
+		&models.Provider{},
+		&models.BuildJob{},
+	)
 	if err != nil {
-		return fmt.Errorf("failed to check dockerfile_content column: %w", err)
+		return nil, fmt.Errorf("failed to auto-migrate models: %w", err)
 	}
 
-	// If column already exists, skip migration
-	if count > 0 {
-		return nil
+	store := &SQLiteStore{
+		db:    gormDB,
+		rawDB: rawDB,
+		dsn:   dataSourceName,
 	}
 
-	// Add the missing column
-	_, err = db.Exec(`ALTER TABLE build_jobs ADD COLUMN dockerfile_content TEXT`)
-	if err != nil {
-		// Ignore "duplicate column name" errors in case the column already exists
-		if err.Error() != "duplicate column name: dockerfile_content" {
-			return fmt.Errorf("failed to add dockerfile_content column: %w", err)
-		}
-	}
-
-	return nil
+	return store, nil
 }
 
 // fileExists checks if a file exists
@@ -414,8 +176,13 @@ func fileExists(filename string) bool {
 	return !os.IsNotExist(err)
 }
 
-// GetDB returns the GORM database instance
+// GetDB returns the raw SQL database instance for backward compatibility
 func (m *SQLiteStore) GetDB() *sql.DB {
+	return m.rawDB
+}
+
+// GetGormDB returns the GORM database instance
+func (m *SQLiteStore) GetGormDB() *gorm.DB {
 	return m.db
 }
 
@@ -427,7 +194,11 @@ func (m *SQLiteStore) Connect() error {
 
 // Disconnect closes the connection to the SQLite database
 func (m *SQLiteStore) Disconnect() error {
-	return m.db.Close()
+	sqlDB, err := m.db.DB()
+	if err != nil {
+		return fmt.Errorf("failed to get underlying SQL DB: %w", err)
+	}
+	return sqlDB.Close()
 }
 
 // Close provides a more standard name for closing the database connection.

+ 35 - 83
dbstore/tickets.go

@@ -2,119 +2,71 @@ package dbstore
 
 import (
 	"context"
-	"database/sql"
 	"fmt"
 
 	"git.linuxforward.com/byop/byop-engine/models"
-	"github.com/pkg/errors"
+	"gorm.io/gorm"
 )
 
-// CreateTicket creates a new ticket
+// CreateTicket creates a new ticket using GORM
 func (s *SQLiteStore) CreateTicket(ctx context.Context, ticket *models.Ticket) error {
-	query := `INSERT INTO tickets (client_id, title, description, status, priority, assigned_to) VALUES (?, ?, ?, ?, ?, ?)`
-	result, err := s.db.ExecContext(ctx, query, ticket.ClientID, ticket.Title, ticket.Description, ticket.Status, ticket.Priority, ticket.AssignedTo)
-	if err != nil {
-		return models.NewErrInternalServer("failed to create ticket", err)
+	result := s.db.WithContext(ctx).Create(ticket)
+	if result.Error != nil {
+		return fmt.Errorf("failed to create ticket: %w", result.Error)
 	}
-	id, err := result.LastInsertId()
-	if err != nil {
-		return models.NewErrInternalServer("failed to get last insert ID for ticket", err)
-	}
-	ticket.ID = int(id)
 	return nil
 }
 
-// GetTicketByID retrieves a ticket by its ID
-func (s *SQLiteStore) GetTicketByID(ctx context.Context, id int) (*models.Ticket, error) {
-	query := `SELECT id, client_id, title, description, status, priority, assigned_to, created_at, updated_at FROM tickets WHERE id = ?`
-	row := s.db.QueryRowContext(ctx, query, id)
-	ticket := &models.Ticket{}
-	err := row.Scan(&ticket.ID, &ticket.ClientID, &ticket.Title, &ticket.Description, &ticket.Status, &ticket.Priority, &ticket.AssignedTo, &ticket.CreatedAt, &ticket.UpdatedAt)
-	if err != nil {
-		if errors.Is(err, sql.ErrNoRows) {
-			return nil, models.NewErrNotFound(fmt.Sprintf("ticket with ID %d not found", id), err)
+// GetTicketByID retrieves a ticket by ID using GORM
+func (s *SQLiteStore) GetTicketByID(ctx context.Context, id uint) (*models.Ticket, error) {
+	var ticket models.Ticket
+	result := s.db.WithContext(ctx).First(&ticket, id)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, models.NewErrNotFound(fmt.Sprintf("ticket with ID %d not found", id), result.Error)
 		}
-		return nil, models.NewErrInternalServer(fmt.Sprintf("failed to get ticket with ID %d", id), err)
+		return nil, fmt.Errorf("failed to get ticket by ID: %w", result.Error)
 	}
-	return ticket, nil
+	return &ticket, nil
 }
 
-// GetTickets retrieves all tickets
-func (s *SQLiteStore) GetTickets(ctx context.Context) ([]models.Ticket, error) {
-	query := `SELECT id, client_id, title, description, status, priority, assigned_to, created_at, updated_at FROM tickets`
-	rows, err := s.db.QueryContext(ctx, query)
-	if err != nil {
-		return nil, models.NewErrInternalServer("failed to query tickets", err)
-	}
-	defer rows.Close()
-
-	var tickets []models.Ticket
-	for rows.Next() {
-		var ticket models.Ticket
-		err := rows.Scan(&ticket.ID, &ticket.ClientID, &ticket.Title, &ticket.Description, &ticket.Status, &ticket.Priority, &ticket.AssignedTo, &ticket.CreatedAt, &ticket.UpdatedAt)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to scan ticket row", err)
-		}
-		tickets = append(tickets, ticket)
-	}
-	if err = rows.Err(); err != nil {
-		return nil, models.NewErrInternalServer("error iterating ticket rows", err)
+// GetTickets retrieves all tickets using GORM
+func (s *SQLiteStore) GetTickets(ctx context.Context) ([]*models.Ticket, error) {
+	var tickets []*models.Ticket
+	result := s.db.WithContext(ctx).Find(&tickets)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get tickets: %w", result.Error)
 	}
 	return tickets, nil
 }
 
-// UpdateTicket updates an existing ticket
+// UpdateTicket updates an existing ticket using GORM
 func (s *SQLiteStore) UpdateTicket(ctx context.Context, ticket *models.Ticket) error {
-	query := `UPDATE tickets SET client_id = ?, title = ?, description = ?, status = ?, priority = ?, assigned_to = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`
-	result, err := s.db.ExecContext(ctx, query, ticket.ClientID, ticket.Title, ticket.Description, ticket.Status, ticket.Priority, ticket.AssignedTo, ticket.ID)
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to update ticket with ID %d", ticket.ID), err)
-	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return models.NewErrInternalServer(fmt.Sprintf("failed to get rows affected for ticket update ID %d", ticket.ID), err)
+	result := s.db.WithContext(ctx).Save(ticket)
+	if result.Error != nil {
+		return fmt.Errorf("failed to update ticket: %w", result.Error)
 	}
-	if rowsAffected == 0 {
+	if result.RowsAffected == 0 {
 		return models.NewErrNotFound(fmt.Sprintf("ticket with ID %d not found for update", ticket.ID), nil)
 	}
 	return nil
 }
 
-// CreateTicketComment creates a new comment for a ticket
+// CreateTicketComment creates a new ticket comment using GORM
 func (s *SQLiteStore) CreateTicketComment(ctx context.Context, comment *models.TicketComment) error {
-	query := `INSERT INTO ticket_comments (ticket_id, user_id, comment) VALUES (?, ?, ?)`
-	result, err := s.db.ExecContext(ctx, query, comment.TicketID, comment.UserID, comment.Content)
-	if err != nil {
-		return models.NewErrInternalServer("failed to create ticket comment", err)
+	result := s.db.WithContext(ctx).Create(comment)
+	if result.Error != nil {
+		return fmt.Errorf("failed to create ticket comment: %w", result.Error)
 	}
-	id, err := result.LastInsertId()
-	if err != nil {
-		return models.NewErrInternalServer("failed to get last insert ID for ticket comment", err)
-	}
-	comment.ID = int(id)
 	return nil
 }
 
-// GetTicketComments retrieves all comments for a given ticket ID
-func (s *SQLiteStore) GetTicketComments(ctx context.Context, ticketID int) ([]models.TicketComment, error) {
-	query := `SELECT id, ticket_id, user_id, comment, created_at FROM ticket_comments WHERE ticket_id = ? ORDER BY created_at ASC`
-	rows, err := s.db.QueryContext(ctx, query, ticketID)
-	if err != nil {
-		return nil, models.NewErrInternalServer(fmt.Sprintf("failed to query ticket comments for ticket ID %d", ticketID), err)
-	}
-	defer rows.Close()
-
-	var comments []models.TicketComment
-	for rows.Next() {
-		var comment models.TicketComment
-		err := rows.Scan(&comment.ID, &comment.TicketID, &comment.UserID, &comment.Content, &comment.CreatedAt)
-		if err != nil {
-			return nil, models.NewErrInternalServer("failed to scan ticket comment row", err)
-		}
-		comments = append(comments, comment)
-	}
-	if err = rows.Err(); err != nil {
-		return nil, models.NewErrInternalServer(fmt.Sprintf("error iterating ticket comment rows for ticket ID %d", ticketID), err)
+// GetTicketComments retrieves all comments for a ticket using GORM
+func (s *SQLiteStore) GetTicketComments(ctx context.Context, ticketID uint) ([]*models.TicketComment, error) {
+	var comments []*models.TicketComment
+	result := s.db.WithContext(ctx).Where("ticket_id = ?", ticketID).Find(&comments)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get ticket comments: %w", result.Error)
 	}
 	return comments, nil
 }

+ 62 - 108
dbstore/users.go

@@ -1,158 +1,112 @@
 package dbstore
 
 import (
-	"context" // Added for context propagation
-	"database/sql"
-	"errors"
+	"context"
 	"fmt"
 
 	"git.linuxforward.com/byop/byop-engine/models"
 	"golang.org/x/crypto/bcrypt"
+	"gorm.io/gorm"
 )
 
-// User operations
-func (s *SQLiteStore) CreateUser(ctx context.Context, user models.User) (int, error) {
-	query := `INSERT INTO users (email, password, name, role, active) VALUES (?, ?, ?, ?, ?)`
-	result, err := s.db.ExecContext(ctx, query, user.Email, user.Password, user.Name, user.Role, user.Active)
-	if err != nil {
-		// TODO: Consider checking for specific DB errors like unique constraint violations
-		// and wrapping them in a custom error, e.g., models.NewErrConflict()
-		return 0, err
+// CreateUser creates a new user using GORM
+func (s *SQLiteStore) CreateUser(ctx context.Context, user *models.User) error {
+	result := s.db.WithContext(ctx).Create(user)
+	if result.Error != nil {
+		return fmt.Errorf("failed to create user: %w", result.Error)
 	}
-
-	id, err := result.LastInsertId()
-	if err != nil {
-		return 0, err
-	}
-
-	return int(id), nil
+	return nil
 }
 
-func (s *SQLiteStore) GetUserByEmail(ctx context.Context, email string) (models.User, error) {
+// GetUserByEmail retrieves a user by email using GORM
+func (s *SQLiteStore) GetUserByEmail(ctx context.Context, email string) (*models.User, error) {
 	var user models.User
-	query := `SELECT id, email, password, name, role, active, created_at, updated_at FROM users WHERE email = ?`
-	fmt.Sprintf("Retrieving user with email: %s", email)
-	err := s.db.QueryRowContext(ctx, query, email).Scan(&user.ID, &user.Email, &user.Password, &user.Name, &user.Role, &user.Active, &user.CreatedAt, &user.UpdatedAt)
-	if err != nil {
-		if errors.Is(err, sql.ErrNoRows) {
-			return user, models.NewErrNotFound(fmt.Sprintf("user not found with email: %s", email), err)
+	result := s.db.WithContext(ctx).Where("email = ?", email).First(&user)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, models.NewErrNotFound(fmt.Sprintf("user with email %s not found", email), result.Error)
 		}
-		return user, err
+		return nil, fmt.Errorf("failed to get user by email: %w", result.Error)
 	}
-	return user, nil
+	return &user, nil
 }
 
+// GetUsers retrieves all users using GORM
 func (s *SQLiteStore) GetUsers(ctx context.Context) ([]*models.User, error) {
-	query := `SELECT id, email, name, role, active, created_at, updated_at FROM users`
-	rows, err := s.db.QueryContext(ctx, query)
-	if err != nil {
-		return nil, err
-	}
-	defer rows.Close()
-
 	var users []*models.User
-	for rows.Next() {
-		var user models.User
-		err := rows.Scan(&user.ID, &user.Email, &user.Name, &user.Role, &user.Active, &user.CreatedAt, &user.UpdatedAt)
-		if err != nil {
-			return nil, err // Error during row scan
-		}
-		users = append(users, &user)
-	}
-	if err = rows.Err(); err != nil { // Check for errors encountered during iteration
-		return nil, err
+	result := s.db.WithContext(ctx).Find(&users)
+	if result.Error != nil {
+		return nil, fmt.Errorf("failed to get users: %w", result.Error)
 	}
 	return users, nil
 }
 
-// GetUserByID retrieves a user by ID
-func (s *SQLiteStore) GetUserByID(ctx context.Context, id int) (*models.User, error) {
-	user := &models.User{}
-	query := `SELECT id, email, password, name, role, active, created_at, updated_at FROM users WHERE id = ?`
-	err := s.db.QueryRowContext(ctx, query, id).Scan(&user.ID, &user.Email, &user.Password, &user.Name, &user.Role, &user.Active, &user.CreatedAt, &user.UpdatedAt)
-	if err != nil {
-		if errors.Is(err, sql.ErrNoRows) {
-			return nil, models.NewErrNotFound(fmt.Sprintf("user not found with id: %d", id), err)
+// GetUserByID retrieves a user by ID using GORM
+func (s *SQLiteStore) GetUserByID(ctx context.Context, id uint) (*models.User, error) {
+	var user models.User
+	result := s.db.WithContext(ctx).First(&user, id)
+	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, models.NewErrNotFound(fmt.Sprintf("user with ID %d not found", id), result.Error)
 		}
-		return nil, err // Other error
+		return nil, fmt.Errorf("failed to get user by ID: %w", result.Error)
 	}
-	return user, nil
+	return &user, nil
 }
 
-// GetByUsername retrieves a user by username (assuming username is email as per original query)
+// GetByUsername retrieves a user by username (assuming username is email)
 func (s *SQLiteStore) GetByUsername(ctx context.Context, username string) (*models.User, error) {
-	user := &models.User{}
-	query := `SELECT id, email, password, name, role, active, created_at, updated_at FROM users WHERE email = ?`
-	err := s.db.QueryRowContext(ctx, query, username).Scan(&user.ID, &user.Email, &user.Password, &user.Name, &user.Role, &user.Active, &user.CreatedAt, &user.UpdatedAt)
-	if err != nil {
-		if errors.Is(err, sql.ErrNoRows) {
-			return nil, models.NewErrNotFound(fmt.Sprintf("user not found with username: %s", username), err)
-		}
-		return nil, err
-	}
-	return user, nil
+	return s.GetUserByEmail(ctx, username)
 }
 
-// UpdateUser updates an existing user
+// UpdateUser updates an existing user using GORM
 func (s *SQLiteStore) UpdateUser(ctx context.Context, user *models.User) error {
-	query := `UPDATE users SET email = ?, password = ?, name = ?, role = ?, active = ? WHERE id = ?`
-	result, err := s.db.ExecContext(ctx, query, user.Email, user.Password, user.Name, user.Role, user.Active, user.ID)
-	if err != nil {
-		// TODO: Consider checking for specific DB errors like unique constraint violations
-		return err
+	result := s.db.WithContext(ctx).Save(user)
+	if result.Error != nil {
+		return fmt.Errorf("failed to update user: %w", result.Error)
 	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return err // Error retrieving RowsAffected
-	}
-	if rowsAffected == 0 {
-		return models.NewErrNotFound(fmt.Sprintf("user not found with id: %d for update", user.ID), nil) // No underlying cause for not found here
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("user with ID %d not found for update", user.ID), nil)
 	}
 	return nil
 }
 
 // CreateDefaultAdmin creates a default admin user if none exists
-func (s *SQLiteStore) CreateDefaultAdmin(ctx context.Context) error { // Added context
-	_, err := s.GetUserByEmail(ctx, "admin@byop.local") // Propagate context
-
+func (s *SQLiteStore) CreateDefaultAdmin(ctx context.Context) error {
+	_, err := s.GetUserByEmail(ctx, "admin@byop.local")
 	if err == nil {
 		// Admin user already exists
 		return nil
 	}
 
-	var targetNotFound *models.ErrNotFound
-	if errors.As(err, &targetNotFound) {
-		hashedPassword, hashErr := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
-		if hashErr != nil {
-			return hashErr
-		}
-
-		admin := models.User{
-			Email:    "admin@byop.local",
-			Password: string(hashedPassword),
-			Name:     "Administrator",
-			Role:     "admin",
-			Active:   true,
-		}
-		_, createErr := s.CreateUser(ctx, admin) // Propagate context
-		return createErr
-	} else {
+	if !models.IsErrNotFound(err) {
 		return err
 	}
-}
 
-func (s *SQLiteStore) DeleteUser(ctx context.Context, id int) error {
-	query := `DELETE FROM users WHERE id = ?`
-	result, err := s.db.ExecContext(ctx, query, id)
-	if err != nil {
-		return err
+	hashedPassword, hashErr := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
+	if hashErr != nil {
+		return hashErr
+	}
+
+	admin := &models.User{
+		Email:    "admin@byop.local",
+		Password: string(hashedPassword),
+		Name:     "Administrator",
+		Role:     "admin",
+		Active:   true,
 	}
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return err // Error retrieving RowsAffected
+
+	return s.CreateUser(ctx, admin)
+}
+
+// DeleteUser deletes a user by ID using GORM
+func (s *SQLiteStore) DeleteUser(ctx context.Context, id uint) error {
+	result := s.db.WithContext(ctx).Delete(&models.User{}, id)
+	if result.Error != nil {
+		return fmt.Errorf("failed to delete user: %w", result.Error)
 	}
-	if rowsAffected == 0 {
-		return models.NewErrNotFound(fmt.Sprintf("user not found with id: %d for deletion", id), nil) // No underlying cause for not found here
+	if result.RowsAffected == 0 {
+		return models.NewErrNotFound(fmt.Sprintf("user with ID %d not found for deletion", id), nil)
 	}
 	return nil
 }

+ 39 - 0
debug_buildkit.go

@@ -0,0 +1,39 @@
+package main
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/moby/buildkit/client"
+)
+
+func testBuildKit() {
+	ctx := context.Background()
+
+	// Test different connection methods
+	hosts := []string{
+		"docker-container://desktop-linux",
+		"unix://~/.docker/run/docker.sock",
+	}
+
+	for _, host := range hosts {
+		fmt.Printf("Testing connection to: %s\n", host)
+
+		c, err := client.New(ctx, host, nil)
+		if err != nil {
+			fmt.Printf("  ❌ Failed: %v\n", err)
+			continue
+		}
+
+		// Try to get info
+		_, err = c.Info(ctx)
+		if err != nil {
+			fmt.Printf("  ❌ Info failed: %v\n", err)
+			c.Close()
+			continue
+		}
+
+		fmt.Printf("  ✅ Success!\n")
+		c.Close()
+	}
+}

+ 0 - 0
docs/git_deployment.md


+ 0 - 133
docs/golang-analyzer-testing.md

@@ -1,133 +0,0 @@
-# Golang Analyzer Testing Guide
-
-This guide provides ways to test the Golang analyzer functionality without making API calls, enabling faster development and debugging.
-
-## Quick Testing
-
-### Run All Tests
-```bash
-# Run all Golang analyzer tests
-go test ./analyzer/stacks/golang/ -v
-
-# Or use the convenience script
-./scripts/test-golang-analyzer.sh
-```
-
-### Run Specific Test Categories
-
-```bash
-# Test main package detection (fixes the "configs" vs "cmd/web-server" issue)
-go test ./analyzer/stacks/golang/ -run TestFindMainPackage -v
-
-# Test the specific web server structure that was failing
-go test ./analyzer/stacks/golang/ -run TestWebServerProjectStructure -v
-
-# Test full LLB generation pipeline
-go test ./analyzer/stacks/golang/ -run TestIntegrationLLBGeneration -v
-
-# Test CGO detection and handling
-go test ./analyzer/stacks/golang/ -run TestCGODetection -v
-```
-
-## Test Coverage
-
-### Unit Tests (`golang_test.go`)
-- ✅ **Basic functionality**: `TestGolang()` - Name and basic operations
-- ✅ **Project analysis**: `TestAnalyze()` - Detects Go projects vs non-Go projects
-- ✅ **Main package detection**: `TestFindMainPackage()` - Finds correct main package in various structures
-- ✅ **Project analysis**: `TestAnalyzeGoProject()` - Full project analysis including modules, ports, dependencies
-- ✅ **LLB generation**: `TestGenerateLLB()` - Basic LLB generation
-- ✅ **Web server structure**: `TestWebServerProjectStructure()` - Specific test for the failing case
-
-### Integration Tests (`integration_test.go`)
-- ✅ **Full LLB pipeline**: `TestIntegrationLLBGeneration()` - Complete end-to-end LLB generation
-- ✅ **CGO detection**: `TestCGODetection()` - Tests CGO environment variable handling
-
-## Key Fixes Validated by Tests
-
-### 1. Main Package Detection Fix
-**Problem**: System was detecting `configs` as main package instead of `cmd/web-server`
-
-**Solution**: Enhanced `findMainPackage()` to:
-- Look for `main` function in Go files, not just any `.go` files
-- Exclude non-executable directories like `configs`
-- Properly traverse `cmd/` subdirectories
-
-**Test**: `TestWebServerProjectStructure()` validates this specific case
-
-### 2. Environment Variable Handling
-**Problem**: `CGO_ENABLED=0 GOOS=linux ...` was being treated as a command instead of environment variables
-
-**Solution**: Separated environment variables from shell command using `llb.AddEnv()`
-
-**Test**: Build commands in integration tests validate proper environment handling
-
-### 3. Directory Creation
-**Problem**: `/app` directory might not exist when copy operations run
-
-**Solution**: Explicitly create `/app` directory before copying files
-
-**Test**: Integration tests validate the full build pipeline
-
-## Manual Testing Scenarios
-
-### Create Test Project Structure
-```bash
-# Create a test project that mimics the failing structure
-mkdir -p /tmp/test-golang-project/{cmd/web-server,configs,pkg/mhttp}
-
-# Create go.mod
-echo "module test-web-server
-go 1.21" > /tmp/test-golang-project/go.mod
-
-# Create main file
-echo "package main
-func main() {}" > /tmp/test-golang-project/cmd/web-server/main.go
-
-# Create config file (should NOT be detected as main)
-echo "package configs
-var Config = map[string]string{}" > /tmp/test-golang-project/configs/server.go
-
-# Test analysis
-cd /home/ray/byop/byop-engine
-go run -c "
-import './analyzer/stacks/golang'
-g := &golang.Golang{}
-result := g.findMainPackage('/tmp/test-golang-project')
-fmt.Printf('Main package: %s\n', result)
-"
-```
-
-### Validate LLB Generation
-```bash
-# Test LLB generation without BuildKit
-go test ./analyzer/stacks/golang/ -run TestGenerateLLB -v
-
-# The test will show:
-# - LLB definition size in bytes
-# - Validation that it's proper JSON
-# - Basic structure validation
-```
-
-## Development Workflow
-
-1. **Make changes** to `golang.go`
-2. **Run tests** with `go test ./analyzer/stacks/golang/ -v`
-3. **Check specific functionality** with targeted test runs
-4. **Validate with integration tests** before API testing
-
-## Benefits of This Testing Approach
-
-- ⚡ **Fast**: No network calls or BuildKit operations
-- 🔍 **Focused**: Test specific functionality in isolation
-- 🐛 **Debuggable**: Easy to add debug output and inspect intermediate results
-- 🔄 **Repeatable**: Consistent test environments
-- 📊 **Comprehensive**: Cover edge cases that might be hard to reproduce via API
-
-## Next Steps for Production Testing
-
-Once unit tests pass, you can validate with the actual API:
-1. Deploy changes to your development environment
-2. Test with real Go projects
-3. Monitor build logs for the corrected main package detection
-4. Verify no more "no Go files in /app" or "configs is not in GOROOT" errors

+ 0 - 0
docs/ovh_git_deployment.md


+ 3 - 3
go.mod

@@ -14,7 +14,8 @@ require (
 	github.com/tonistiigi/fsutil v0.0.0-20250417144416-3f76f8130144
 	golang.org/x/crypto v0.37.0
 	golang.org/x/sync v0.13.0
-	google.golang.org/grpc v1.69.4
+	gorm.io/driver/sqlite v1.6.0
+	gorm.io/gorm v1.30.0
 )
 
 require (
@@ -93,10 +94,9 @@ require (
 	golang.org/x/time v0.11.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
+	google.golang.org/grpc v1.69.4 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
-	gorm.io/driver/sqlite v1.6.0 // indirect
-	gorm.io/gorm v1.30.0 // indirect
 	gotest.tools/v3 v3.5.2 // indirect
 )
 

+ 146 - 38
handlers/apps.go

@@ -2,7 +2,8 @@ package handlers
 
 import (
 	"context" // Ensure context is imported
-	"errors"  // Added for errors.As
+	"encoding/json"
+	"errors" // Added for errors.As
 	"fmt"
 	"net/http"
 	"strconv"
@@ -19,14 +20,16 @@ type AppsHandler struct {
 	store          *dbstore.SQLiteStore
 	entry          *logrus.Entry
 	previewService services.PreviewService
+	appImporter    *services.AppImporter
 }
 
 // NewAppsHandler creates a new AppsHandler
-func NewAppsHandler(store *dbstore.SQLiteStore, previewService services.PreviewService) *AppsHandler {
+func NewAppsHandler(store *dbstore.SQLiteStore, previewService services.PreviewService, appImporter *services.AppImporter) *AppsHandler {
 	return &AppsHandler{
 		store:          store,
 		entry:          logrus.WithField("component", "AppsHandler"),
 		previewService: previewService,
+		appImporter:    appImporter,
 	}
 }
 
@@ -95,7 +98,7 @@ func (h *AppsHandler) CreateApp(c *gin.Context) {
 	}
 
 	// Set the user ID on the app
-	app.UserID = userID
+	app.UserID = uint(userID)
 	h.entry.WithField("app", app).Info("JSON binding successful, starting validation")
 
 	// Validate app configuration
@@ -119,18 +122,16 @@ func (h *AppsHandler) CreateApp(c *gin.Context) {
 	h.entry.WithField("app", app).Info("About to create app in database")
 
 	// Create the app
-	id, err := h.store.CreateApp(ctx, app) // Pass context
+	err := h.store.CreateApp(ctx, app) // Pass context
 	if err != nil {
 		h.entry.WithField("error", err).Error("Failed to create app in database")
 		models.RespondWithError(c, err) // Pass db error directly
 		return
 	}
 
-	app.ID = id
-
 	// Automatically create preview - this happens async
 	h.entry.WithField("app_id", app.ID).Info("Starting automatic preview creation")
-	go h.createAppPreviewAsync(context.Background(), id) // Use background context for async operation
+	go h.createAppPreviewAsync(context.Background(), app.ID) // Use background context for async operation
 
 	c.JSON(http.StatusCreated, app)
 }
@@ -139,14 +140,15 @@ func (h *AppsHandler) CreateApp(c *gin.Context) {
 func (h *AppsHandler) GetApp(c *gin.Context) {
 	ctx := c.Request.Context() // Get context
 	idStr := c.Param("id")
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	parsedID, err := strconv.ParseUint(idStr, 10, 32)
 	if err != nil {
 		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
 		return
 	}
+	id := uint(parsedID)
 
 	// Get app directly from store
-	app, err := h.store.GetAppByID(ctx, int(id)) // Pass context and cast id
+	app, err := h.store.GetAppByID(ctx, id) // Pass context and use uint id
 	if err != nil {
 		models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
 		return
@@ -164,10 +166,9 @@ func (h *AppsHandler) GetApp(c *gin.Context) {
 // Updated UpdateApp method
 func (h *AppsHandler) UpdateApp(c *gin.Context) {
 	ctx := c.Request.Context() // Get context
-	idStr := c.Param("id")
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
+		models.RespondWithError(c, err)
 		return
 	}
 
@@ -178,7 +179,7 @@ func (h *AppsHandler) UpdateApp(c *gin.Context) {
 	}
 
 	// Ensure the ID matches the URL parameter
-	updatedApp.ID = int(id)
+	updatedApp.ID = id
 
 	// Validate app data
 	if err := h.validateAppConfig(ctx, updatedApp.Components); err != nil { // Pass context
@@ -196,7 +197,15 @@ func (h *AppsHandler) UpdateApp(c *gin.Context) {
 	// For consistency, we can rely on store.UpdateApp's error.
 
 	// Validate all components exist and are valid
-	for _, componentID := range updatedApp.Components {
+	var componentIDs []uint
+	if updatedApp.Components != "" {
+		if err := json.Unmarshal([]byte(updatedApp.Components), &componentIDs); err != nil {
+			models.RespondWithError(c, models.NewErrValidation("Invalid components JSON format", nil, err))
+			return
+		}
+	}
+
+	for _, componentID := range componentIDs {
 		component, err := h.store.GetComponentByID(ctx, componentID) // Pass context
 		if err != nil {
 			models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
@@ -235,15 +244,14 @@ func (h *AppsHandler) UpdateApp(c *gin.Context) {
 // DeleteApp deletes an app
 func (h *AppsHandler) DeleteApp(c *gin.Context) {
 	ctx := c.Request.Context() // Get context
-	idStr := c.Param("id")
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
+		models.RespondWithError(c, err)
 		return
 	}
 
 	// Call the store delete method
-	if err := h.store.DeleteApp(ctx, int(id)); err != nil { // Pass context
+	if err := h.store.DeleteApp(ctx, id); err != nil { // Pass context
 		models.RespondWithError(c, err) // Pass db error directly (handles NotFound, Conflict)
 		return
 	}
@@ -254,15 +262,14 @@ func (h *AppsHandler) DeleteApp(c *gin.Context) {
 // GetAppDeployments returns all deployments for an app
 func (h *AppsHandler) GetAppDeployments(c *gin.Context) {
 	ctx := c.Request.Context() // Get context
-	idStr := c.Param("id")
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
+		models.RespondWithError(c, err)
 		return
 	}
 
 	// Check if app exists first
-	app, err := h.store.GetAppByID(ctx, int(id)) // Pass context
+	app, err := h.store.GetAppByID(ctx, id) // Pass context
 	if err != nil {
 		models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
 		return
@@ -273,7 +280,7 @@ func (h *AppsHandler) GetAppDeployments(c *gin.Context) {
 	}
 
 	// Get deployments for this app
-	deployments, err := h.store.GetDeploymentsByAppID(ctx, int(id)) // Pass context
+	deployments, err := h.store.GetDeploymentsByAppID(ctx, id) // Pass context
 	if err != nil {
 		models.RespondWithError(c, err) // Pass db error directly
 		return
@@ -314,7 +321,17 @@ func (h *AppsHandler) GetAppByVersion(c *gin.Context) {
 }
 
 // validateAppConfig checks if the app configuration is valid
-func (h *AppsHandler) validateAppConfig(ctx context.Context, componentIds []int) error { // Added context
+func (h *AppsHandler) validateAppConfig(ctx context.Context, componentsJSON string) error { // Added context
+	if componentsJSON == "" {
+		return models.NewErrValidation("App must have at least one component", nil, nil) // Return custom error
+	}
+
+	// Parse the JSON string of component IDs
+	var componentIds []uint
+	if err := json.Unmarshal([]byte(componentsJSON), &componentIds); err != nil {
+		return models.NewErrValidation("Invalid components JSON format", nil, err)
+	}
+
 	if len(componentIds) == 0 {
 		return models.NewErrValidation("App must have at least one component", nil, nil) // Return custom error
 	}
@@ -360,7 +377,7 @@ func (h *AppsHandler) getAppByNameAndVersion(ctx context.Context, name, version
 }
 
 // createAppPreviewAsync creates a preview automatically and updates app status
-func (h *AppsHandler) createAppPreviewAsync(ctx context.Context, appId int) { // Added context
+func (h *AppsHandler) createAppPreviewAsync(ctx context.Context, appId uint) { // Added context
 	preview, err := h.previewService.CreatePreview(ctx, appId) // Pass context
 	if err != nil {
 		h.entry.WithField("app_id", appId).Errorf("Failed to create preview: %v", err)
@@ -378,7 +395,7 @@ func (h *AppsHandler) createAppPreviewAsync(ctx context.Context, appId int) { //
 }
 
 // stopExistingPreviews stops any running previews for an app
-func (h *AppsHandler) stopExistingPreviews(ctx context.Context, appID int) { // Added context
+func (h *AppsHandler) stopExistingPreviews(ctx context.Context, appID uint) { // Added context
 	previews, err := h.store.GetPreviewsByAppID(ctx, appID) // Pass context
 	if err != nil {
 		h.entry.WithField("app_id", appID).Errorf("Failed to get existing previews: %v", err)
@@ -397,14 +414,13 @@ func (h *AppsHandler) stopExistingPreviews(ctx context.Context, appID int) { //
 // CreateAppPreview creates a new preview for an app
 func (h *AppsHandler) CreateAppPreview(c *gin.Context) {
 	ctx := c.Request.Context() // Get context
-	idStr := c.Param("id")
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
+		models.RespondWithError(c, err)
 		return
 	}
 
-	app, err := h.store.GetAppByID(ctx, int(id)) // Pass context
+	app, err := h.store.GetAppByID(ctx, id) // Pass context
 	if err != nil {
 		models.RespondWithError(c, err)
 		return
@@ -424,14 +440,13 @@ func (h *AppsHandler) CreateAppPreview(c *gin.Context) {
 // GetAppPreview returns the preview for a specific app
 func (h *AppsHandler) GetAppPreview(c *gin.Context) {
 	ctx := c.Request.Context() // Get context
-	idStr := c.Param("id")
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
+		models.RespondWithError(c, err)
 		return
 	}
 
-	app, err := h.store.GetAppByID(ctx, int(id)) // Pass context
+	app, err := h.store.GetAppByID(ctx, id) // Pass context
 	if err != nil {
 		models.RespondWithError(c, err)
 		return
@@ -459,14 +474,13 @@ func (h *AppsHandler) GetAppPreview(c *gin.Context) {
 // DeleteAppPreview deletes the preview for a specific app
 func (h *AppsHandler) DeleteAppPreview(c *gin.Context) {
 	ctx := c.Request.Context() // Get context
-	idStr := c.Param("id")
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
+		models.RespondWithError(c, err)
 		return
 	}
 
-	app, err := h.store.GetAppByID(ctx, int(id)) // Pass context
+	app, err := h.store.GetAppByID(ctx, id) // Pass context
 	if err != nil {
 		models.RespondWithError(c, err)
 		return
@@ -484,3 +498,97 @@ func (h *AppsHandler) DeleteAppPreview(c *gin.Context) {
 
 	c.JSON(http.StatusOK, gin.H{"message": "Preview deleted successfully"})
 }
+
+// ImportReview reviews a docker-compose file for importing an app
+func (h *AppsHandler) ImportReview(c *gin.Context) {
+	ctx := c.Request.Context()
+	var req models.AppImportRequest
+
+	if err := c.ShouldBindJSON(&req); err != nil {
+		h.entry.WithField("error", err).Error("Failed to bind JSON to AppImportRequest struct")
+		models.RespondWithError(c, models.NewErrValidation("Invalid request body", nil, err))
+		return
+	}
+
+	// Set default branch if not provided
+	if req.Branch == "" {
+		req.Branch = "main"
+	}
+
+	h.entry.Infof("Reviewing compose import from %s (branch: %s)", req.SourceURL, req.Branch)
+
+	review, err := h.appImporter.ReviewComposeFile(ctx, req)
+	if err != nil {
+		h.entry.WithField("error", err).Error("Failed to review compose file")
+		models.RespondWithError(c, models.NewErrInternalServer("Failed to review compose file", err))
+		return
+	}
+
+	c.JSON(http.StatusOK, review)
+}
+
+// ImportCreate creates an app and components from a docker-compose file
+func (h *AppsHandler) ImportCreate(c *gin.Context) {
+	ctx := c.Request.Context()
+	var req models.AppImportCreateRequest
+
+	if err := c.ShouldBindJSON(&req); err != nil {
+		h.entry.WithField("error", err).Error("Failed to bind JSON to AppImportCreateRequest struct")
+		models.RespondWithError(c, models.NewErrValidation("Invalid request body", nil, err))
+		return
+	}
+
+	// Set default branch if not provided
+	if req.Branch == "" {
+		req.Branch = "main"
+	}
+
+	// Get the user ID from the context (set by auth middleware)
+	userIDInterface, exists := c.Get("user_id")
+	if !exists {
+		models.RespondWithError(c, models.NewErrUnauthorized("User ID not found in context", nil))
+		return
+	}
+
+	// Convert user_id to uint - it might be a string from JWT
+	var userID uint
+	switch v := userIDInterface.(type) {
+	case string:
+		if parsedID, err := strconv.ParseUint(v, 10, 32); err == nil {
+			userID = uint(parsedID)
+		} else {
+			h.entry.Warnf("Failed to parse user_id string '%s' to uint, defaulting to 1. Error: %v", v, err)
+			userID = 1
+		}
+	case int:
+		userID = uint(v)
+	case int64:
+		userID = uint(v)
+	case uint:
+		userID = v
+	default:
+		h.entry.Warnf("User_id in context is of unexpected type %T, defaulting to 1.", v)
+		userID = 1
+	}
+
+	h.entry.Infof("Creating app '%s' from compose import for user %d", req.ConfirmedAppName, userID)
+
+	app, err := h.appImporter.CreateAppFromCompose(ctx, req, userID)
+	if err != nil {
+		h.entry.WithField("error", err).Error("Failed to create app from compose")
+		models.RespondWithError(c, models.NewErrInternalServer("Failed to create app from compose", err))
+		return
+	}
+
+	c.JSON(http.StatusCreated, app)
+}
+
+// parseUintID parses a string ID parameter to uint
+func parseUintID(c *gin.Context, paramName string) (uint, error) {
+	idStr := c.Param(paramName)
+	parsedID, err := strconv.ParseUint(idStr, 10, 32)
+	if err != nil {
+		return 0, models.NewErrValidation(fmt.Sprintf("Invalid %s ID format", paramName), nil, err)
+	}
+	return uint(parsedID), nil
+}

+ 14 - 26
handlers/clients.go

@@ -3,7 +3,6 @@ package handlers
 import (
 	"fmt"
 	"net/http"
-	"strconv"
 
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/models"
@@ -63,32 +62,27 @@ func (h *ClientHandler) CreateClient(c *gin.Context) {
 		return
 	}
 
-	id, err := h.store.CreateClient(ctx, client)
+	err := h.store.CreateClient(ctx, &client)
 	if err != nil {
 		appErr := models.NewErrInternalServer("failed_create_client", fmt.Errorf("Failed to create client: %w", err))
 		models.RespondWithError(c, appErr)
 		return
 	}
 
-	// Set the generated ID
-	client.ID = id
-
 	c.JSON(http.StatusCreated, client)
 }
 
 // GetClient returns a specific client
 func (h *ClientHandler) GetClient(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_client_id_format", map[string]string{"id": "Invalid client ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
-	client, err := h.store.GetClientByID(ctx, int(id))
+	client, err := h.store.GetClientByID(ctx, id)
 	if err != nil {
 		models.RespondWithError(c, err)
 		return
@@ -105,13 +99,11 @@ func (h *ClientHandler) GetClient(c *gin.Context) {
 
 // UpdateClient updates a client
 func (h *ClientHandler) UpdateClient(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_client_id_format", map[string]string{"id": "Invalid client ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
@@ -122,7 +114,7 @@ func (h *ClientHandler) UpdateClient(c *gin.Context) {
 		return
 	}
 
-	updatedClient.ID = int(id)
+	updatedClient.ID = id
 
 	// Validate client data
 	if updatedClient.Name == "" {
@@ -132,7 +124,7 @@ func (h *ClientHandler) UpdateClient(c *gin.Context) {
 		return
 	}
 
-	if err := h.store.UpdateClient(ctx, updatedClient); err != nil {
+	if err := h.store.UpdateClient(ctx, &updatedClient); err != nil {
 		models.RespondWithError(c, err)
 		return
 	}
@@ -142,17 +134,15 @@ func (h *ClientHandler) UpdateClient(c *gin.Context) {
 
 // DeleteClient deletes a client
 func (h *ClientHandler) DeleteClient(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_client_id_format", map[string]string{"id": "Invalid client ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
-	if err := h.store.DeleteClient(ctx, int(id)); err != nil {
+	if err := h.store.DeleteClient(ctx, id); err != nil {
 		models.RespondWithError(c, err)
 		return
 	}
@@ -162,17 +152,15 @@ func (h *ClientHandler) DeleteClient(c *gin.Context) {
 
 // GetClientDeployments returns all deployments for a client
 func (h *ClientHandler) GetClientDeployments(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_client_id_format", map[string]string{"id": "Invalid client ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
-	client, err := h.store.GetClientByID(ctx, int(id))
+	client, err := h.store.GetClientByID(ctx, id)
 	if err != nil {
 		models.RespondWithError(c, err)
 		return

+ 24 - 31
handlers/components.go

@@ -84,20 +84,22 @@ func (h *ComponentHandler) CreateComponent(c *gin.Context) {
 		return
 	}
 
-	// Convert user_id to int - it might be a string from JWT
-	var userID int
+	// Convert user_id to uint - it might be a string from JWT
+	var userID uint
 	switch v := userIDInterface.(type) {
 	case string:
-		if parsedID, err := strconv.Atoi(v); err == nil {
-			userID = parsedID
+		if parsedID, err := strconv.ParseUint(v, 10, 32); err == nil {
+			userID = uint(parsedID)
 		} else {
-			h.entry.Warnf("Failed to parse user_id string '%s' to int, defaulting. Error: %v", v, err)
+			h.entry.Warnf("Failed to parse user_id string '%s' to uint, defaulting. Error: %v", v, err)
 			userID = 1
 		}
 	case int:
-		userID = v
+		userID = uint(v)
 	case int64:
-		userID = int(v)
+		userID = uint(v)
+	case uint:
+		userID = v
 	default:
 		h.entry.Warnf("User_id in context is of unexpected type %T, defaulting.", v)
 		userID = 1
@@ -117,15 +119,14 @@ func (h *ComponentHandler) CreateComponent(c *gin.Context) {
 	component.Status = "validating"
 
 	// Create the component
-	id, err := h.store.CreateComponent(ctx, &component)
+	err := h.store.CreateComponent(ctx, &component)
 	if err != nil {
 		appErr := models.NewErrInternalServer("failed_create_component", fmt.Errorf("Failed to create component: %w", err))
 		models.RespondWithError(c, appErr)
 		return
 	}
 
-	// Set the generated ID
-	component.ID = id
+	// The ID is automatically set by GORM after creation
 
 	// Start async validation
 	h.entry.WithField("component_id", component.ID).Info("Starting async validation for component")
@@ -320,17 +321,15 @@ func (h *ComponentHandler) cloneRepository(repoURL, branch, targetDir string) er
 
 // GetComponent returns a specific component
 func (h *ComponentHandler) GetComponent(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_component_id_format", map[string]string{"id": "Invalid component ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
-	component, err := h.store.GetComponentByID(ctx, int(id))
+	component, err := h.store.GetComponentByID(ctx, id)
 	if err != nil {
 		models.RespondWithError(c, err)
 		return
@@ -347,13 +346,11 @@ func (h *ComponentHandler) GetComponent(c *gin.Context) {
 
 // UpdateComponent updates a component
 func (h *ComponentHandler) UpdateComponent(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_component_id_format", map[string]string{"id": "Invalid component ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
@@ -365,7 +362,7 @@ func (h *ComponentHandler) UpdateComponent(c *gin.Context) {
 	}
 
 	// Ensure the ID matches the URL parameter
-	updatedComponent.ID = int(id)
+	updatedComponent.ID = id
 
 	// Validate component data
 	if validationErrors := h.validateComponentRequest(&updatedComponent); len(validationErrors) > 0 {
@@ -374,7 +371,7 @@ func (h *ComponentHandler) UpdateComponent(c *gin.Context) {
 		return
 	}
 
-	if err := h.store.UpdateComponent(ctx, updatedComponent); err != nil {
+	if err := h.store.UpdateComponent(ctx, &updatedComponent); err != nil {
 		models.RespondWithError(c, err)
 		return
 	}
@@ -384,17 +381,15 @@ func (h *ComponentHandler) UpdateComponent(c *gin.Context) {
 
 // DeleteComponent deletes a component
 func (h *ComponentHandler) DeleteComponent(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_component_id_format", map[string]string{"id": "Invalid component ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
-	if err := h.store.DeleteComponent(ctx, int(id)); err != nil {
+	if err := h.store.DeleteComponent(ctx, id); err != nil {
 		models.RespondWithError(c, err)
 		return
 	}
@@ -404,18 +399,16 @@ func (h *ComponentHandler) DeleteComponent(c *gin.Context) {
 
 // GetComponentDeployments returns all deployments for a component
 func (h *ComponentHandler) GetComponentDeployments(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_component_id_format", map[string]string{"id": "Invalid component ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
 	// Check if component exists
-	component, err := h.store.GetComponentByID(ctx, int(id))
+	component, err := h.store.GetComponentByID(ctx, id)
 	if err != nil {
 		models.RespondWithError(c, err)
 		return

+ 26 - 42
handlers/deployments.go

@@ -3,7 +3,6 @@ package handlers
 import (
 	"fmt"
 	"net/http"
-	"strconv"
 
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/models"
@@ -63,7 +62,7 @@ func (h *DeploymentHandler) CreateDeployment(c *gin.Context) {
 
 	// Basic validation
 	validationErrors := make(map[string]string)
-	if deployment.AppId == 0 {
+	if deployment.AppID == 0 {
 		validationErrors["app_id"] = "App ID is required"
 	}
 	if deployment.Environment == "" {
@@ -81,32 +80,29 @@ func (h *DeploymentHandler) CreateDeployment(c *gin.Context) {
 	}
 
 	// TODO: Add complex deployment logic with cloud providers
-	id, err := h.store.CreateDeployment(ctx, deployment)
+	err := h.store.CreateDeployment(ctx, &deployment)
 	if err != nil {
 		appErr := models.NewErrInternalServer("failed_create_deployment", fmt.Errorf("Failed to create deployment: %w", err))
 		models.RespondWithError(c, appErr)
 		return
 	}
 
-	// Set the generated ID
-	deployment.ID = id
+	// GORM automatically sets the ID after creation
 
 	c.JSON(http.StatusCreated, deployment)
 }
 
 // GetDeployment returns a specific deployment
 func (h *DeploymentHandler) GetDeployment(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_deployment_id_format", map[string]string{"id": "Invalid deployment ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
-	deployment, err := h.store.GetDeploymentByID(ctx, int(id))
+	deployment, err := h.store.GetDeploymentByID(ctx, id)
 	if err != nil {
 		models.RespondWithError(c, err)
 		return
@@ -117,13 +113,11 @@ func (h *DeploymentHandler) GetDeployment(c *gin.Context) {
 
 // UpdateDeployment updates a deployment
 func (h *DeploymentHandler) UpdateDeployment(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_deployment_id_format", map[string]string{"id": "Invalid deployment ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
@@ -135,11 +129,11 @@ func (h *DeploymentHandler) UpdateDeployment(c *gin.Context) {
 	}
 
 	// Ensure the ID matches the URL parameter
-	updatedDeployment.ID = int(id)
+	updatedDeployment.ID = id
 
 	// Basic validation for update
 	validationErrors := make(map[string]string)
-	if updatedDeployment.AppId == 0 {
+	if updatedDeployment.AppID == 0 {
 		validationErrors["app_id"] = "App ID is required"
 	}
 	if updatedDeployment.Environment == "" {
@@ -161,17 +155,15 @@ func (h *DeploymentHandler) UpdateDeployment(c *gin.Context) {
 
 // DeleteDeployment deletes a deployment
 func (h *DeploymentHandler) DeleteDeployment(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_deployment_id_format", map[string]string{"id": "Invalid deployment ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
-	if err := h.store.DeleteDeployment(ctx, int(id)); err != nil {
+	if err := h.store.DeleteDeployment(ctx, id); err != nil {
 		models.RespondWithError(c, err)
 		return
 	}
@@ -181,13 +173,11 @@ func (h *DeploymentHandler) DeleteDeployment(c *gin.Context) {
 
 // UpdateDeploymentStatus updates the status of a deployment
 func (h *DeploymentHandler) UpdateDeploymentStatus(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_deployment_id_format", map[string]string{"id": "Invalid deployment ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
@@ -202,7 +192,7 @@ func (h *DeploymentHandler) UpdateDeploymentStatus(c *gin.Context) {
 	}
 
 	// Get current deployment
-	deployment, err := h.store.GetDeploymentByID(ctx, int(id))
+	deployment, err := h.store.GetDeploymentByID(ctx, id)
 	if err != nil {
 		models.RespondWithError(c, err)
 		return
@@ -223,17 +213,15 @@ func (h *DeploymentHandler) UpdateDeploymentStatus(c *gin.Context) {
 
 // GetDeploymentsByClient returns all deployments for a specific client
 func (h *DeploymentHandler) GetDeploymentsByClient(c *gin.Context) {
-	clientIDStr := c.Param("clientId")
 	ctx := c.Request.Context()
 
-	clientID, err := strconv.ParseInt(clientIDStr, 10, 64)
+	clientID, err := parseUintID(c, "clientId")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_client_id_format", map[string]string{"clientId": "Invalid client ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
-	deployments, err := h.store.GetDeploymentsByClientID(ctx, int(clientID))
+	deployments, err := h.store.GetDeploymentsByClientID(ctx, clientID)
 	if err != nil {
 		if _, ok := err.(models.CustomError); !ok {
 			err = models.NewErrInternalServer("failed_fetch_deployments_by_client", fmt.Errorf("Failed to fetch deployments for client %d: %w", clientID, err))
@@ -247,17 +235,15 @@ func (h *DeploymentHandler) GetDeploymentsByClient(c *gin.Context) {
 
 // GetDeploymentsByApp returns all deployments for a specific app
 func (h *DeploymentHandler) GetDeploymentsByApp(c *gin.Context) {
-	appIDStr := c.Param("appId")
 	ctx := c.Request.Context()
 
-	appID, err := strconv.ParseInt(appIDStr, 10, 64)
+	appID, err := parseUintID(c, "appId")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_app_id_format", map[string]string{"appId": "Invalid app ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
-	deployments, err := h.store.GetDeploymentsByAppID(ctx, int(appID))
+	deployments, err := h.store.GetDeploymentsByAppID(ctx, appID)
 	if err != nil {
 		if _, ok := err.(models.CustomError); !ok {
 			err = models.NewErrInternalServer("failed_fetch_deployments_by_app", fmt.Errorf("Failed to fetch deployments for app %d: %w", appID, err))
@@ -269,19 +255,17 @@ func (h *DeploymentHandler) GetDeploymentsByApp(c *gin.Context) {
 	c.JSON(http.StatusOK, deployments)
 }
 
-// GetDeploymentsByUser returns all deployments created by a specific user
+// GetDeploymentsByUser returns all deployments created by a specific user (via their apps)
 func (h *DeploymentHandler) GetDeploymentsByUser(c *gin.Context) {
-	userIDStr := c.Param("userId")
 	ctx := c.Request.Context()
 
-	userID, err := strconv.ParseInt(userIDStr, 10, 64)
+	userID, err := parseUintID(c, "userId")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_user_id_format", map[string]string{"userId": "Invalid user ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
-	deployments, err := h.store.GetDeploymentsByUserID(ctx, int(userID))
+	deployments, err := h.store.GetDeploymentsByUserID(ctx, userID)
 	if err != nil {
 		if _, ok := err.(models.CustomError); !ok {
 			err = models.NewErrInternalServer("failed_fetch_deployments_by_user", fmt.Errorf("Failed to fetch deployments for user %d: %w", userID, err))

+ 4 - 13
handlers/preview.go

@@ -3,7 +3,6 @@ package handlers
 import (
 	"fmt"
 	"net/http"
-	"strconv"
 
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/models"
@@ -28,7 +27,7 @@ func (h *PreviewHandler) CreatePreview(c *gin.Context) {
 	ctx := c.Request.Context()
 
 	var requestBody struct {
-		AppId int `json:"app_id" binding:"required"`
+		AppId uint `json:"app_id" binding:"required"`
 	}
 
 	if err := c.ShouldBindJSON(&requestBody); err != nil {
@@ -50,22 +49,14 @@ func (h *PreviewHandler) CreatePreview(c *gin.Context) {
 // GetPreview retrieves details of a specific preview session.
 func (h *PreviewHandler) GetPreview(c *gin.Context) {
 	ctx := c.Request.Context()
-	previewIdStr := c.Param("preview_id")
 
-	if previewIdStr == "" {
-		appErr := models.NewErrValidation("missing_preview_id", map[string]string{"preview_id": "Preview ID is required in URL path"}, nil)
-		models.RespondWithError(c, appErr)
-		return
-	}
-
-	previewIdInt, err := strconv.Atoi(previewIdStr)
+	previewId, err := parseUintID(c, "preview_id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_preview_id_format", map[string]string{"preview_id": "Invalid preview ID format, must be an integer"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
-	preview, err := h.store.GetPreviewByID(ctx, previewIdInt)
+	preview, err := h.store.GetPreviewByID(ctx, previewId)
 	if err != nil {
 		models.RespondWithError(c, err)
 		return

+ 26 - 32
handlers/tickets.go

@@ -3,7 +3,6 @@ package handlers
 import (
 	"fmt"
 	"net/http"
-	"strconv"
 	"time"
 
 	"git.linuxforward.com/byop/byop-engine/dbstore"
@@ -47,7 +46,7 @@ func (h *TicketHandler) ListTickets(c *gin.Context) {
 		return
 	}
 	if tickets == nil {
-		tickets = []models.Ticket{} // Return empty slice instead of null
+		tickets = []*models.Ticket{} // Return empty slice instead of null
 	}
 	c.JSON(http.StatusOK, tickets)
 }
@@ -56,8 +55,8 @@ func (h *TicketHandler) ListTickets(c *gin.Context) {
 type CreateTicketInput struct {
 	Title       string `json:"title" validate:"required,min=3,max=255"`
 	Description string `json:"description" validate:"required,min=10"`
-	ClientID    int    `json:"client_id" validate:"required,gt=0"` // Assuming ClientID is mandatory for a ticket
-	UserID      *int   `json:"user_id,omitempty" validate:"omitempty,gt=0"`
+	ClientID    uint   `json:"client_id" validate:"required,gt=0"` // Assuming ClientID is mandatory for a ticket
+	UserID      *uint  `json:"user_id,omitempty" validate:"omitempty,gt=0"`
 	Priority    string `json:"priority" validate:"omitempty,oneof=low medium high critical"`
 }
 
@@ -106,15 +105,14 @@ func (h *TicketHandler) CreateTicket(c *gin.Context) {
 // GetTicket returns a specific ticket
 func (h *TicketHandler) GetTicket(c *gin.Context) {
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	id, err := strconv.ParseInt(idStr, 10, 64)
+
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_ticket_id_format", map[string]string{"id": "Invalid ticket ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
-	ticket, err := h.Store.GetTicketByID(ctx, int(id))
+	ticket, err := h.Store.GetTicketByID(ctx, id)
 	if err != nil {
 		if models.IsErrNotFound(err) {
 			appErr := models.NewErrNotFound("ticket_not_found", fmt.Errorf("Ticket with ID %d not found: %w", id, err))
@@ -135,17 +133,16 @@ type UpdateTicketInput struct {
 	Description *string `json:"description,omitempty" validate:"omitempty,min=10"`
 	Priority    *string `json:"priority,omitempty" validate:"omitempty,oneof=low medium high critical"`
 	Status      *string `json:"status,omitempty" validate:"omitempty,oneof=open in_progress resolved closed"`
-	AssignedTo  *int    `json:"assigned_to,omitempty" validate:"omitempty,gt=0"`
+	AssignedTo  *uint   `json:"assigned_to,omitempty" validate:"omitempty,gt=0"`
 }
 
 // UpdateTicket updates a ticket
 func (h *TicketHandler) UpdateTicket(c *gin.Context) {
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	id, err := strconv.ParseInt(idStr, 10, 64)
+
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_ticket_id_format", map[string]string{"id": "Invalid ticket ID format"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
@@ -163,7 +160,7 @@ func (h *TicketHandler) UpdateTicket(c *gin.Context) {
 		return
 	}
 
-	ticket, err := h.Store.GetTicketByID(ctx, int(id))
+	ticket, err := h.Store.GetTicketByID(ctx, id)
 	if err != nil {
 		if models.IsErrNotFound(err) {
 			appErr := models.NewErrNotFound("ticket_not_found_for_update", fmt.Errorf("Ticket with ID %d not found for update: %w", id, err))
@@ -215,15 +212,14 @@ func (h *TicketHandler) UpdateTicket(c *gin.Context) {
 // GetTicketComments returns comments for a ticket
 func (h *TicketHandler) GetTicketComments(c *gin.Context) {
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	ticketID, err := strconv.ParseInt(idStr, 10, 64)
+
+	ticketID, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_ticket_id_for_comments", map[string]string{"id": "Invalid ticket ID format for comments"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
-	comments, err := h.Store.GetTicketComments(ctx, int(ticketID))
+	comments, err := h.Store.GetTicketComments(ctx, ticketID)
 	if err != nil {
 		// If the error indicates the ticket itself was not found, that's a 404 for the ticket.
 		// Otherwise, it's an internal error fetching comments.
@@ -240,7 +236,7 @@ func (h *TicketHandler) GetTicketComments(c *gin.Context) {
 		return
 	}
 	if comments == nil {
-		comments = []models.TicketComment{} // Return empty slice
+		comments = []*models.TicketComment{} // Return empty slice
 	}
 
 	c.JSON(http.StatusOK, comments)
@@ -255,11 +251,10 @@ type AddTicketCommentInput struct {
 // AddTicketComment adds a comment to a ticket
 func (h *TicketHandler) AddTicketComment(c *gin.Context) {
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	ticketID, err := strconv.ParseInt(idStr, 10, 64)
+
+	ticketID, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_ticket_id_for_add_comment", map[string]string{"id": "Invalid ticket ID format for adding comment"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
@@ -278,7 +273,7 @@ func (h *TicketHandler) AddTicketComment(c *gin.Context) {
 	}
 
 	// Get authenticated user ID (placeholder - replace with actual auth logic)
-	authUserID := 1 // Example: Assume user ID 1 is authenticated
+	authUserID := uint(1) // Example: Assume user ID 1 is authenticated
 	// if !ok || authUserID == 0 {
 	// 	appErr := models.NewErrUnauthorized("user_not_authenticated_for_comment", fmt.Errorf("User must be authenticated to comment"))
 	// 	models.RespondWithError(c, appErr)
@@ -286,7 +281,7 @@ func (h *TicketHandler) AddTicketComment(c *gin.Context) {
 	// }
 
 	comment := models.TicketComment{
-		TicketID: int(ticketID),
+		TicketID: ticketID,
 		UserID:   authUserID, // Set from authenticated user
 		Content:  input.Content,
 	}
@@ -309,15 +304,14 @@ func (h *TicketHandler) AddTicketComment(c *gin.Context) {
 // ResolveTicket resolves a ticket
 func (h *TicketHandler) ResolveTicket(c *gin.Context) {
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	id, err := strconv.ParseInt(idStr, 10, 64)
+
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_ticket_id_for_resolve", map[string]string{"id": "Invalid ticket ID format for resolving"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
-	ticket, err := h.Store.GetTicketByID(ctx, int(id))
+	ticket, err := h.Store.GetTicketByID(ctx, id)
 	if err != nil {
 		if models.IsErrNotFound(err) {
 			appErr := models.NewErrNotFound("ticket_not_found_for_resolve", fmt.Errorf("Ticket with ID %d not found for resolve: %w", id, err))

+ 14 - 19
handlers/users.go

@@ -3,7 +3,6 @@ package handlers
 import (
 	"fmt"
 	"net/http"
-	"strconv"
 
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/models"
@@ -88,7 +87,7 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
 		Active:   userActive,
 	}
 
-	id, err := h.Store.CreateUser(ctx, user)
+	err = h.Store.CreateUser(ctx, &user)
 	if err != nil {
 		if models.IsErrConflict(err) {
 			models.RespondWithError(c, err)
@@ -99,7 +98,7 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
 		return
 	}
 
-	user.ID = id
+	// GORM automatically sets the ID after creation
 	// Clear the password before sending the response
 	createdUser := user
 	createdUser.Password = ""
@@ -110,11 +109,10 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
 // GetUser retrieves a user by ID
 func (h *UserHandler) GetUser(c *gin.Context) {
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	id, err := strconv.Atoi(idStr)
+
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_user_id_format", map[string]string{"id": "Invalid user ID format, must be an integer"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
@@ -140,11 +138,10 @@ type UpdateUserInput struct {
 // UpdateUser updates an existing user
 func (h *UserHandler) UpdateUser(c *gin.Context) {
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	id, err := strconv.Atoi(idStr)
+
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_user_id_format", map[string]string{"id": "Invalid user ID format, must be an integer"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
@@ -214,11 +211,10 @@ func (h *UserHandler) UpdateUser(c *gin.Context) {
 // DeleteUser deletes a user by ID
 func (h *UserHandler) DeleteUser(c *gin.Context) {
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	id, err := strconv.Atoi(idStr)
+
+	id, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_user_id_format", map[string]string{"id": "Invalid user ID format, must be an integer"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 
@@ -251,11 +247,10 @@ func (h *UserHandler) ListUsers(c *gin.Context) {
 // GetUserDeployments returns all deployments for a specific user
 func (h *UserHandler) GetUserDeployments(c *gin.Context) {
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	userID, err := strconv.Atoi(idStr)
+
+	userID, err := parseUintID(c, "id")
 	if err != nil {
-		appErr := models.NewErrValidation("invalid_user_id_format_for_deployments", map[string]string{"id": "Invalid user ID format, must be an integer"}, err)
-		models.RespondWithError(c, appErr)
+		models.RespondWithError(c, err)
 		return
 	}
 

+ 15 - 12
models/build.go

@@ -17,18 +17,20 @@ const (
 
 // BuildRequest represents the information needed to initiate a build.
 type BuildRequest struct {
-	ComponentID       uint              `json:"component_id"`
-	Version           string            `json:"version"`      // e.g., git commit hash, tag, or branch
-	SourceURL         string            `json:"source_url"`   // Git repository URL
-	RegistryURL       string            `json:"registry_url"` // Target Docker registry URL
-	RegistryUser      string            `json:"registry_user,omitempty"`
-	RegistryPassword  string            `json:"registry_password,omitempty"`
-	ImageName         string            `json:"image_name"`                   // Name of the image to build (without tag)
-	BuildContext      string            `json:"build_context"`                // Path to the Dockerfile within the repo, default "."
-	Dockerfile        string            `json:"dockerfile"`                   // Path to the Dockerfile, default "Dockerfile"
-	NoCache           bool              `json:"no_cache"`                     // Whether to use --no-cache for the build
-	BuildArgs         map[string]string `json:"build_args"`                   // Build-time variables
-	DockerfileContent string            `json:"dockerfile_content,omitempty"` // Generated Dockerfile content
+	ComponentID          uint              `json:"component_id"`
+	Version              string            `json:"version"`      // e.g., git commit hash, tag, or branch
+	SourceURL            string            `json:"source_url"`   // Git repository URL
+	RegistryURL          string            `json:"registry_url"` // Target Docker registry URL
+	RegistryUser         string            `json:"registry_user,omitempty"`
+	RegistryPassword     string            `json:"registry_password,omitempty"`
+	ImageName            string            `json:"image_name"`                       // Name of the image to build (without tag)
+	BuildContext         string            `json:"build_context"`                    // Path to the Dockerfile within the repo, default "."
+	Dockerfile           string            `json:"dockerfile"`                       // Path to the Dockerfile, default "Dockerfile"
+	NoCache              bool              `json:"no_cache"`                         // Whether to use --no-cache for the build
+	BuildArgs            map[string]string `json:"build_args"`                       // Build-time variables
+	Source               string            `json:"source,omitempty"`                 // "autodetect", "dockerfile", "docker-compose"
+	DockerfileContent    string            `json:"dockerfile_content,omitempty"`     // Generated or user-provided Dockerfile content
+	DockerComposeContent string            `json:"docker_compose_content,omitempty"` // docker-compose.yml content
 }
 
 // BuildJob represents a build job in the system.
@@ -48,6 +50,7 @@ type BuildJob struct {
 	RegistryPassword  string      `json:"registry_password,omitempty"` // Consider how to store this securely if at all long-term
 	BuildContext      string      `json:"build_context"`
 	Dockerfile        string      `json:"dockerfile"`
+	Source            string      `json:"source,omitempty"` // "autodetect", "dockerfile", "docker-compose"
 	NoCache           bool        `json:"no_cache"`
 	BuildArgs         string      `json:"build_args" gorm:"type:text"`                   // Stored as JSON string or similar
 	DockerfileContent string      `json:"dockerfile_content,omitempty" gorm:"type:text"` // Generated Dockerfile content

+ 95 - 90
models/common.go

@@ -1,6 +1,8 @@
 package models
 
-import "time"
+import (
+	"time"
+)
 
 // APIResponse represents a standard API response
 type APIResponse struct {
@@ -30,65 +32,68 @@ const (
 )
 
 type App struct {
-	ID              int    `json:"id" db:"id"`
-	UserID          int    `json:"user_id" db:"user_id"`
-	Name            string `json:"name" db:"name" validate:"required"`
-	Description     string `json:"description" db:"description"`
-	Components      []int  `json:"components" db:"components"`               // Component IDs
-	Status          string `json:"status" db:"status"`                       // e.g., AppStatusBuilding, AppStatusReady, AppStatusFailed
-	PreviewID       int    `json:"preview_id" db:"preview_id"`               // Current preview ID
-	PreviewURL      string `json:"preview_url" db:"preview_url"`             // Current preview URL
-	CurrentImageTag string `json:"current_image_tag" db:"current_image_tag"` // Added
-	CurrentImageURI string `json:"current_image_uri" db:"current_image_uri"` // Added
-	ErrorMsg        string `json:"error_msg" db:"error_msg"`
-	CreatedAt       string `json:"created_at" db:"created_at"`
-	UpdatedAt       string `json:"updated_at" db:"updated_at"`
+	ID              uint      `json:"id" gorm:"primaryKey;autoIncrement"`
+	UserID          uint      `json:"user_id" gorm:"not null;index"`
+	Name            string    `json:"name" gorm:"not null" validate:"required"`
+	Description     string    `json:"description"`
+	Components      string    `json:"components" gorm:"type:text"`               // JSON array of Component IDs
+	Status          string    `json:"status" gorm:"not null;default:'building'"` // e.g., AppStatusBuilding, AppStatusReady, AppStatusFailed
+	PreviewID       uint      `json:"preview_id"`
+	PreviewURL      string    `json:"preview_url"`
+	CurrentImageTag string    `json:"current_image_tag"`
+	CurrentImageURI string    `json:"current_image_uri"`
+	ErrorMsg        string    `json:"error_msg"`
+	CreatedAt       time.Time `json:"created_at" gorm:"autoCreateTime"`
+	UpdatedAt       time.Time `json:"updated_at" gorm:"autoUpdateTime"`
 }
 
 // Component represents a deployable component
 type Component struct {
-	ID          int    `json:"id"`
-	UserID      int    `json:"user_id"`
-	Name        string `json:"name"`
-	Description string `json:"description"`
-	Type        string `json:"type"`   // web, api, database, etc.
-	Status      string `json:"status"` // active, inactive, deploying, etc.
-
-	ErrorMsg        string    `json:"error_msg" db:"error_msg"` // Error message if validation fails
-	Config          string    `json:"config"`                   // JSON configuration
-	Repository      string    `json:"repository"`               // URL to the git repository
+	ID              uint      `json:"id" gorm:"primaryKey;autoIncrement"`
+	UserID          uint      `json:"user_id" gorm:"not null;index"`
+	Name            string    `json:"name" gorm:"not null"`
+	Description     string    `json:"description"`
+	Type            string    `json:"type" gorm:"not null"`   // web, api, database, etc.
+	Status          string    `json:"status" gorm:"not null"` // active, inactive, deploying, etc.
+	ErrorMsg        string    `json:"error_msg"`
+	Config          string    `json:"config" gorm:"type:text"` // JSON configuration
+	Repository      string    `json:"repository"`              // URL to the git repository
 	Branch          string    `json:"branch"`
-	CurrentImageTag string    `json:"current_image_tag" db:"current_image_tag"` // Current built image tag
-	CurrentImageURI string    `json:"current_image_uri" db:"current_image_uri"` // Current built image full URI
-	CreatedAt       time.Time `json:"created_at"`
-	UpdatedAt       time.Time `json:"updated_at"`
+	SourceType      string    `json:"source_type,omitempty"`     // "autodetect", "dockerfile", "docker-compose"
+	ServiceName     string    `json:"service_name,omitempty"`    // Service name from docker-compose (if applicable)
+	BuildContext    string    `json:"build_context,omitempty"`   // Build context path for docker-compose services
+	DockerfilePath  string    `json:"dockerfile_path,omitempty"` // Dockerfile path relative to build context
+	CurrentImageTag string    `json:"current_image_tag"`
+	CurrentImageURI string    `json:"current_image_uri"`
+	CreatedAt       time.Time `json:"created_at" gorm:"autoCreateTime"`
+	UpdatedAt       time.Time `json:"updated_at" gorm:"autoUpdateTime"`
 }
 
 // Deployment represents a deployment instance
 type Deployment struct {
-	ID          int       `json:"id"`
-	AppId       int       `json:"app_id"`
-	ClientID    int       `json:"client_id"`
-	Name        string    `json:"name"`
-	Description string    `json:"description"`
-	Environment string    `json:"environment"` // dev, staging, prod
-	Status      string    `json:"status"`      // pending, running, stopped, failed
-	URL         string    `json:"url"`
-	Config      string    `json:"config"` // JSON deployment configuration
-	DeployedAt  time.Time `json:"deployed_at"`
-	CreatedAt   time.Time `json:"created_at"`
-	UpdatedAt   time.Time `json:"updated_at"`
+	ID          uint       `json:"id" gorm:"primaryKey;autoIncrement"`
+	AppID       uint       `json:"app_id" gorm:"not null;index"`
+	ClientID    uint       `json:"client_id" gorm:"not null;index"`
+	Name        string     `json:"name" gorm:"not null"`
+	Description string     `json:"description"`
+	Environment string     `json:"environment" gorm:"default:'development'"` // dev, staging, prod
+	Status      string     `json:"status" gorm:"default:'pending'"`          // pending, running, stopped, failed
+	URL         string     `json:"url"`
+	Config      string     `json:"config" gorm:"type:text;default:'{}'"` // JSON deployment configuration
+	DeployedAt  *time.Time `json:"deployed_at,omitempty"`
+	CreatedAt   time.Time  `json:"created_at" gorm:"autoCreateTime"`
+	UpdatedAt   time.Time  `json:"updated_at" gorm:"autoUpdateTime"`
 }
 
 // Provider represents a cloud provider
 type Provider struct {
-	ID        int       `json:"id"`
-	Name      string    `json:"name"`
-	Type      string    `json:"type"` // aws, digitalocean, ovh, etc.
-	Config    string    `json:"config"`
-	Active    bool      `json:"active"`
-	CreatedAt time.Time `json:"created_at"`
-	UpdatedAt time.Time `json:"updated_at"`
+	ID        uint      `json:"id" gorm:"primaryKey;autoIncrement"`
+	Name      string    `json:"name" gorm:"not null"`
+	Type      string    `json:"type" gorm:"not null"` // aws, digitalocean, ovh, etc.
+	Config    string    `json:"config" gorm:"type:text;default:'{}'"`
+	Active    bool      `json:"active" gorm:"default:true"`
+	CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
+	UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
 }
 
 // TicketStatus defines the possible statuses for a ticket.
@@ -109,27 +114,27 @@ const (
 
 // Ticket represents a support ticket
 type Ticket struct {
-	ID          int        `json:"id"`
-	ClientID    int        `json:"client_id"`             // Link to Client who reported it
-	UserID      *int       `json:"user_id,omitempty"`     // Link to User who reported it (optional)
-	AssignedTo  *int       `json:"assigned_to,omitempty"` // Link to User it is assigned to (optional)
-	Title       string     `json:"title"`
-	Description string     `json:"description"`
-	Status      string     `json:"status"`   // e.g., open, in_progress, resolved, closed
-	Priority    string     `json:"priority"` // e.g., low, medium, high, critical
-	CreatedAt   time.Time  `json:"created_at"`
-	UpdatedAt   time.Time  `json:"updated_at"`
+	ID          uint       `json:"id" gorm:"primaryKey;autoIncrement"`
+	ClientID    uint       `json:"client_id" gorm:"not null;index"` // Link to Client who reported it
+	UserID      *uint      `json:"user_id,omitempty"`               // Link to User who reported it (optional)
+	AssignedTo  *uint      `json:"assigned_to,omitempty"`           // Link to User it is assigned to (optional)
+	Title       string     `json:"title" gorm:"not null"`
+	Description string     `json:"description" gorm:"type:text"`
+	Status      string     `json:"status" gorm:"default:'open'"`     // e.g., open, in_progress, resolved, closed
+	Priority    string     `json:"priority" gorm:"default:'medium'"` // e.g., low, medium, high, critical
+	CreatedAt   time.Time  `json:"created_at" gorm:"autoCreateTime"`
+	UpdatedAt   time.Time  `json:"updated_at" gorm:"autoUpdateTime"`
 	ResolvedAt  *time.Time `json:"resolved_at,omitempty"` // When the ticket was resolved
 }
 
 // TicketComment represents a comment on a support ticket
 type TicketComment struct {
-	ID        int       `json:"id"`
-	TicketID  int       `json:"ticket_id"` // Link to the parent Ticket
-	UserID    int       `json:"user_id"`   // Link to User who made the comment
-	Content   string    `json:"content"`
-	CreatedAt time.Time `json:"created_at"`
-	UpdatedAt time.Time `json:"updated_at"`
+	ID        uint      `json:"id" gorm:"primaryKey;autoIncrement"`
+	TicketID  uint      `json:"ticket_id" gorm:"not null;index"` // Link to the parent Ticket
+	UserID    uint      `json:"user_id" gorm:"not null;index"`   // Link to User who made the comment
+	Content   string    `json:"content" gorm:"type:text;not null"`
+	CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
+	UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
 }
 
 // Role constants for User
@@ -141,25 +146,25 @@ const (
 
 // User represents a user in the system
 type User struct {
-	ID        int       `json:"id" db:"id"`
-	Email     string    `json:"email" db:"email" validate:"required,email"`
-	Password  string    `json:"-" db:"password"` // Never include password in JSON responses
-	Name      string    `json:"name" db:"name" validate:"required"`
-	Role      string    `json:"role" db:"role" validate:"required,oneof=user admin editor"`
-	Active    bool      `json:"active" db:"active"`
-	CreatedAt time.Time `json:"created_at" db:"created_at"`
-	UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
+	ID        uint      `json:"id" gorm:"primaryKey;autoIncrement"`
+	Email     string    `json:"email" gorm:"uniqueIndex;not null" validate:"required,email"`
+	Password  string    `json:"-" gorm:"not null"` // Never include password in JSON responses
+	Name      string    `json:"name" gorm:"not null" validate:"required"`
+	Role      string    `json:"role" gorm:"default:'user'" validate:"required,oneof=user admin editor"`
+	Active    bool      `json:"active" gorm:"default:true"`
+	CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
+	UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
 }
 
 // Client represents a client in the system
 type Client struct {
-	ID          int       `json:"id" db:"id"`
-	Name        string    `json:"name" db:"name" validate:"required"`
-	Description string    `json:"description" db:"description"`
-	ContactInfo string    `json:"contact_info" db:"contact_info"`
-	Active      bool      `json:"active" db:"active"`
-	CreatedAt   time.Time `json:"created_at" db:"created_at"`
-	UpdatedAt   time.Time `json:"updated_at" db:"updated_at"`
+	ID          uint      `json:"id" gorm:"primaryKey;autoIncrement"`
+	Name        string    `json:"name" gorm:"not null" validate:"required"`
+	Description string    `json:"description"`
+	ContactInfo string    `json:"contact_info"`
+	Active      bool      `json:"active" gorm:"default:true"`
+	CreatedAt   time.Time `json:"created_at" gorm:"autoCreateTime"`
+	UpdatedAt   time.Time `json:"updated_at" gorm:"autoUpdateTime"`
 }
 
 // PreviewStatus defines the possible statuses for a preview.
@@ -172,16 +177,16 @@ const (
 )
 
 type Preview struct {
-	ID         int    `json:"id" db:"id"`
-	AppID      int    `json:"app_id" db:"app_id"`
-	Status     string `json:"status" db:"status"`         // e.g., PreviewStatusBuilding, PreviewStatusRunning
-	URL        string `json:"url" db:"url"`               // Preview URL (http://vps-ip)
-	VPSID      string `json:"vps_id" db:"vps_id"`         // OVH VPS ID
-	IPAddress  string `json:"ip_address" db:"ip_address"` // VPS IP address
-	ErrorMsg   string `json:"error_msg" db:"error_msg"`
-	BuildLogs  string `json:"build_logs" db:"build_logs"`
-	DeployLogs string `json:"deploy_logs" db:"deploy_logs"`
-	ExpiresAt  string `json:"expires_at" db:"expires_at"` // Auto-cleanup after X hours
-	CreatedAt  string `json:"created_at" db:"created_at"`
-	UpdatedAt  string `json:"updated_at" db:"updated_at"`
+	ID         uint      `json:"id" gorm:"primaryKey;autoIncrement"`
+	AppID      uint      `json:"app_id" gorm:"not null;index"`
+	Status     string    `json:"status" gorm:"not null;default:'building'"` // e.g., PreviewStatusBuilding, PreviewStatusRunning
+	URL        string    `json:"url"`                                       // Preview URL (http://vps-ip)
+	VPSID      string    `json:"vps_id"`                                    // OVH VPS ID
+	IPAddress  string    `json:"ip_address"`                                // VPS IP address
+	ErrorMsg   string    `json:"error_msg"`
+	BuildLogs  string    `json:"build_logs" gorm:"type:text"`
+	DeployLogs string    `json:"deploy_logs" gorm:"type:text"`
+	ExpiresAt  time.Time `json:"expires_at" gorm:"not null"` // Auto-cleanup after X hours
+	CreatedAt  time.Time `json:"created_at" gorm:"autoCreateTime"`
+	UpdatedAt  time.Time `json:"updated_at" gorm:"autoUpdateTime"`
 }

+ 35 - 0
models/compose.go

@@ -0,0 +1,35 @@
+package models
+
+// AppImportRequest represents a request to import an app from docker-compose
+type AppImportRequest struct {
+	AppName     string `json:"app_name,omitempty"`            // Optional app name, can be auto-generated
+	SourceURL   string `json:"source_url" binding:"required"` // Git repository URL
+	Branch      string `json:"branch" binding:"required"`     // Git branch, defaults to "main"
+	ComposePath string `json:"compose_path,omitempty"`        // Path to docker-compose.yml, defaults to "docker-compose.yml"
+}
+
+// ComposeService represents a service found in a docker-compose.yml file
+type ComposeService struct {
+	Name         string            `json:"name"`                    // Service name from compose file
+	Source       string            `json:"source"`                  // "build" or "image"
+	BuildContext string            `json:"build_context,omitempty"` // For build services, the context path
+	Dockerfile   string            `json:"dockerfile,omitempty"`    // For build services, the dockerfile path
+	Image        string            `json:"image,omitempty"`         // For image services, the image name
+	Ports        []string          `json:"ports,omitempty"`         // Port mappings
+	Environment  map[string]string `json:"environment,omitempty"`   // Environment variables
+	Volumes      []string          `json:"volumes,omitempty"`       // Volume mappings
+}
+
+// AppImportReview represents the review response for an app import
+type AppImportReview struct {
+	AppName  string           `json:"app_name"`        // Suggested or provided app name
+	Services []ComposeService `json:"services"`        // List of services found in compose file
+	Valid    bool             `json:"valid"`           // Whether the compose file is valid
+	Error    string           `json:"error,omitempty"` // Error message if invalid
+}
+
+// AppImportCreateRequest represents the final request to create an app from compose
+type AppImportCreateRequest struct {
+	AppImportRequest
+	ConfirmedAppName string `json:"confirmed_app_name" binding:"required"` // User-confirmed app name
+}

+ 66 - 0
scripts/buildkit-daemon.sh

@@ -0,0 +1,66 @@
+#!/bin/bash
+
+# BuildKit daemon management script for macOS development
+
+case "$1" in
+    start)
+        echo "Starting BuildKit daemon..."
+        docker stop buildkitd 2>/dev/null || true
+        docker rm buildkitd 2>/dev/null || true
+        
+        # Create buildkit config if it doesn't exist
+        if [ ! -f buildkitd.toml ]; then
+            cat > buildkitd.toml << EOF
+# BuildKit configuration for insecure registries
+
+[registry."host.docker.internal:5000"]
+  http = true
+  insecure = true
+
+[registry."localhost:5000"]
+  http = true
+  insecure = true
+EOF
+        fi
+        
+        docker run -d -p 1234:1234 --name buildkitd \
+            --add-host host.docker.internal:host-gateway \
+            -v "$(pwd)/buildkitd.toml:/etc/buildkit/buildkitd.toml:ro" \
+            --privileged moby/buildkit:latest \
+            --allow-insecure-entitlement="security.insecure" \
+            --allow-insecure-entitlement="network.host" \
+            --addr tcp://0.0.0.0:1234 \
+            --config /etc/buildkit/buildkitd.toml
+        echo "BuildKit daemon started on tcp://127.0.0.1:1234 with insecure registry support"
+        ;;
+    stop)
+        echo "Stopping BuildKit daemon..."
+        docker stop buildkitd
+        docker rm buildkitd
+        echo "BuildKit daemon stopped"
+        ;;
+    status)
+        echo "BuildKit daemon status:"
+        docker ps -a | grep buildkitd || echo "BuildKit daemon is not running"
+        ;;
+    logs)
+        echo "BuildKit daemon logs:"
+        docker logs buildkitd
+        ;;
+    restart)
+        $0 stop
+        sleep 2
+        $0 start
+        ;;
+    *)
+        echo "Usage: $0 {start|stop|status|logs|restart}"
+        echo ""
+        echo "Commands:"
+        echo "  start   - Start BuildKit daemon container"
+        echo "  stop    - Stop and remove BuildKit daemon container"
+        echo "  status  - Show BuildKit daemon status"
+        echo "  logs    - Show BuildKit daemon logs"
+        echo "  restart - Restart BuildKit daemon"
+        exit 1
+        ;;
+esac

+ 0 - 143
scripts/test-fixes.sh

@@ -1,143 +0,0 @@
-#!/bin/bash
-
-# Test script to verify Golang analyzer fixes
-set -e
-
-echo "🧪 Testing Golang Analyzer Fixes"
-echo "================================="
-
-# Create a test project structure that mimics the failing case
-TEST_DIR="/tmp/test-golang-web-server-$(date +%s)"
-echo "📁 Creating test project at: $TEST_DIR"
-
-mkdir -p "$TEST_DIR"/{cmd/web-server,configs,pkg/mhttp,scripts,tests,web,.vscode}
-
-# Create go.mod
-cat > "$TEST_DIR/go.mod" << 'EOF'
-module golang-web-server
-go 1.18
-
-require (
-    github.com/gin-gonic/gin v1.9.1
-)
-EOF
-
-# Create the main file in cmd/web-server (this should be detected as main package)
-cat > "$TEST_DIR/cmd/web-server/main.go" << 'EOF'
-package main
-
-import (
-    "net/http"
-    "github.com/gin-gonic/gin"
-)
-
-func main() {
-    r := gin.Default()
-    r.GET("/health", func(c *gin.Context) {
-        c.JSON(http.StatusOK, gin.H{"status": "healthy"})
-    })
-    r.Run(":8080")
-}
-EOF
-
-# Create config file that should NOT be detected as main
-cat > "$TEST_DIR/configs/server-config.go" << 'EOF'
-package configs
-
-var ServerConfig = map[string]string{
-    "host": "0.0.0.0",
-    "port": "8080",
-}
-EOF
-
-# Create pkg files
-cat > "$TEST_DIR/pkg/mhttp/server.go" << 'EOF'
-package mhttp
-
-import "net/http"
-
-func StartServer() *http.Server {
-    return &http.Server{Addr: ":8080"}
-}
-EOF
-
-cat > "$TEST_DIR/pkg/mhttp/functions.go" << 'EOF'
-package mhttp
-
-func HandleRequest() {
-    // Handle HTTP requests
-}
-EOF
-
-# Create LICENSE file
-cat > "$TEST_DIR/LICENSE" << 'EOF'
-MIT License
-
-Copyright (c) 2025 Test Project
-EOF
-
-echo "✅ Test project created successfully"
-echo ""
-
-# Run the analyzer tests
-echo "🔬 Running Golang analyzer tests..."
-cd /home/ray/byop/byop-engine
-go test ./analyzer/stacks/golang/ -v -run TestWebServerProjectStructure
-
-echo ""
-echo "🔍 Testing main package detection with actual test project..."
-
-# Create a simple test to verify our analyzer works on the real test project
-go run -c '
-package main
-
-import (
-    "fmt"
-    "git.linuxforward.com/byop/byop-engine/analyzer/stacks/golang"
-)
-
-func main() {
-    g := &golang.Golang{}
-    
-    fmt.Println("Testing main package detection...")
-    mainPkg := g.FindMainPackage("'$TEST_DIR'")
-    fmt.Printf("Detected main package: %s\n", mainPkg)
-    
-    if mainPkg != "./cmd/web-server" {
-        fmt.Printf("❌ FAIL: Expected ./cmd/web-server, got %s\n", mainPkg)
-        os.Exit(1)
-    }
-    
-    fmt.Println("✅ SUCCESS: Main package detection working correctly!")
-    
-    fmt.Println("\nTesting full analysis...")
-    analysis, err := g.AnalyzeGoProject("'$TEST_DIR'")
-    if err != nil {
-        fmt.Printf("❌ FAIL: Analysis error: %v\n", err)
-        os.Exit(1)
-    }
-    
-    fmt.Printf("App Name: %s\n", analysis.AppName)
-    fmt.Printf("Main Package: %s\n", analysis.MainPackage) 
-    fmt.Printf("Go Version: %s\n", analysis.GoVersion)
-    fmt.Printf("Port: %d\n", analysis.Port)
-    fmt.Printf("CGO Enabled: %v\n", analysis.CGOEnabled)
-    
-    fmt.Println("✅ SUCCESS: Full analysis working correctly!")
-}
-' || echo "⚠️  Note: Direct code execution failed, but tests above validate the functionality"
-
-echo ""
-echo "🧹 Cleaning up test project..."
-rm -rf "$TEST_DIR"
-
-echo ""
-echo "🎉 All tests completed successfully!"
-echo ""
-echo "📋 Summary of fixes verified:"
-echo "  ✅ Main package detection: ./cmd/web-server (not configs)"
-echo "  ✅ Environment variable handling in build commands"
-echo "  ✅ Runtime directory creation for binary copying"
-echo "  ✅ Railway-inspired build strategies"
-echo ""
-echo "🚀 The Golang analyzer is ready for production use!"

+ 0 - 36
scripts/test-golang-analyzer.sh

@@ -1,36 +0,0 @@
-#!/bin/bash
-
-# Quick test script for Golang analyzer development
-# This allows testing without making API calls
-
-echo "🔬 Running Golang Analyzer Tests..."
-echo "=================================="
-
-# Run all Golang analyzer tests
-cd /home/ray/byop/byop-engine
-go test ./analyzer/stacks/golang/ -v
-
-echo ""
-echo "📊 Test Summary:"
-echo "==============="
-
-# Run specific test cases that validate our fixes
-echo "✅ Testing main package detection for web-server structure..."
-go test ./analyzer/stacks/golang/ -run TestWebServerProjectStructure -v
-
-echo ""
-echo "✅ Testing general main package detection..."
-go test ./analyzer/stacks/golang/ -run TestFindMainPackage -v
-
-echo ""
-echo "✅ Testing LLB generation..."
-go test ./analyzer/stacks/golang/ -run TestGenerateLLB -v
-
-echo ""
-echo "🎉 Development testing complete!"
-echo ""
-echo "💡 To test manually without API calls:"
-echo "   go test ./analyzer/stacks/golang/ -v"
-echo ""
-echo "💡 To test specific functionality:"
-echo "   go test ./analyzer/stacks/golang/ -run TestWebServerProjectStructure -v"

+ 284 - 0
services/app_importer.go

@@ -0,0 +1,284 @@
+package services
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"strings"
+
+	"git.linuxforward.com/byop/byop-engine/dbstore"
+	"git.linuxforward.com/byop/byop-engine/models"
+	git "github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing"
+	"github.com/sirupsen/logrus"
+)
+
+// AppImporter handles the import of applications from docker-compose files
+type AppImporter struct {
+	store       *dbstore.SQLiteStore
+	parser      *ComposeParser
+	builderSvc  *Builder
+	entry       *logrus.Entry
+	registryURL string
+}
+
+// NewAppImporter creates a new AppImporter instance
+func NewAppImporter(store *dbstore.SQLiteStore, builderSvc *Builder, registryURL string) *AppImporter {
+	return &AppImporter{
+		store:       store,
+		parser:      NewComposeParser(),
+		builderSvc:  builderSvc,
+		entry:       logrus.WithField("service", "AppImporter"),
+		registryURL: registryURL,
+	}
+}
+
+// ReviewComposeFile reviews a docker-compose file from a git repository
+func (ai *AppImporter) ReviewComposeFile(ctx context.Context, req models.AppImportRequest) (*models.AppImportReview, error) {
+	ai.entry.Infof("Reviewing compose file from %s (branch: %s, path: %s)", req.SourceURL, req.Branch, req.ComposePath)
+
+	// Create temporary directory for cloning
+	tempDir, err := os.MkdirTemp("", "byop-import-*")
+	if err != nil {
+		return nil, fmt.Errorf("failed to create temp directory: %w", err)
+	}
+	defer os.RemoveAll(tempDir)
+
+	// Clone the repository
+	if err := ai.cloneRepository(req.SourceURL, req.Branch, tempDir); err != nil {
+		return &models.AppImportReview{
+			Valid: false,
+			Error: fmt.Sprintf("Failed to clone repository: %v", err),
+		}, nil
+	}
+
+	// Set default compose path if not provided
+	composePath := req.ComposePath
+	if composePath == "" {
+		composePath = "docker-compose.yml"
+	}
+
+	// Parse the compose file
+	review, err := ai.parser.ParseComposeFile(tempDir, composePath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse compose file: %w", err)
+	}
+
+	// Override app name if provided in request
+	if req.AppName != "" {
+		review.AppName = req.AppName
+	}
+
+	return review, nil
+}
+
+// CreateAppFromCompose creates an app and its components from a docker-compose import
+func (ai *AppImporter) CreateAppFromCompose(ctx context.Context, req models.AppImportCreateRequest, userID uint) (*models.App, error) {
+	ai.entry.Infof("Creating app '%s' from compose import for user %d", req.ConfirmedAppName, userID)
+
+	// First, review the compose file to get the services
+	review, err := ai.ReviewComposeFile(ctx, req.AppImportRequest)
+	if err != nil {
+		return nil, fmt.Errorf("failed to review compose file: %w", err)
+	}
+
+	if !review.Valid {
+		return nil, fmt.Errorf("invalid compose file: %s", review.Error)
+	}
+
+	// Create the app
+	app := &models.App{
+		UserID:      userID,
+		Name:        req.ConfirmedAppName,
+		Description: fmt.Sprintf("Imported from docker-compose.yml from %s", req.SourceURL),
+		Status:      models.AppStatusBuilding,
+		Components:  "[]", // Will be updated after creating components
+	}
+
+	if err := ai.store.CreateApp(ctx, app); err != nil {
+		return nil, fmt.Errorf("failed to create app: %w", err)
+	}
+
+	ai.entry.Infof("Created app ID %d: %s", app.ID, app.Name)
+
+	// Create components for each service
+	var componentIDs []uint
+	for _, service := range review.Services {
+		component, err := ai.createComponentFromService(ctx, app.ID, userID, service, req)
+		if err != nil {
+			ai.entry.Errorf("Failed to create component for service %s: %v", service.Name, err)
+			// Continue with other services, don't fail the entire import
+			continue
+		}
+		componentIDs = append(componentIDs, component.ID)
+	}
+
+	// Update app with component IDs
+	if err := ai.updateAppComponents(ctx, app.ID, componentIDs); err != nil {
+		ai.entry.Errorf("Failed to update app components: %v", err)
+		// Non-fatal error, the app and components are still created
+	}
+
+	// Update app status
+	if len(componentIDs) > 0 {
+		app.Status = models.AppStatusBuilding
+	} else {
+		app.Status = models.AppStatusFailed
+		app.ErrorMsg = "No components could be created from the compose file"
+	}
+
+	if err := ai.store.UpdateAppStatus(ctx, app.ID, app.Status, app.ErrorMsg); err != nil {
+		ai.entry.Errorf("Failed to update app status: %v", err)
+	}
+
+	ai.entry.Infof("Successfully created app %s with %d components", app.Name, len(componentIDs))
+	return app, nil
+}
+
+// createComponentFromService creates a component from a compose service
+func (ai *AppImporter) createComponentFromService(ctx context.Context, appID, userID uint, service models.ComposeService, req models.AppImportCreateRequest) (*models.Component, error) {
+	ai.entry.Infof("Creating component for service: %s (source: %s)", service.Name, service.Source)
+
+	// Determine component type based on service configuration
+	componentType := "web"
+	if len(service.Ports) == 0 {
+		componentType = "worker"
+	}
+
+	// Resolve build context for docker-compose imports
+	buildContext := service.BuildContext
+	if buildContext == "" {
+		buildContext = "."
+	}
+	// Clean up the build context path to remove ./ prefix and ensure it's relative
+	buildContext = strings.TrimPrefix(buildContext, "./")
+
+	// Create component
+	component := &models.Component{
+		UserID:         userID,
+		Name:           service.Name,
+		Description:    fmt.Sprintf("Service from docker-compose: %s", service.Name),
+		Type:           componentType,
+		Status:         "pending",
+		Repository:     req.SourceURL,
+		Branch:         req.Branch,
+		SourceType:     "docker-compose",
+		ServiceName:    service.Name,
+		BuildContext:   buildContext,
+		DockerfilePath: service.Dockerfile,
+	}
+
+	if err := ai.store.CreateComponent(ctx, component); err != nil {
+		return nil, fmt.Errorf("failed to create component: %w", err)
+	}
+
+	ai.entry.Infof("Created component ID %d: %s", component.ID, component.Name)
+
+	// Handle the component based on its source type
+	switch service.Source {
+	case "build":
+		// Queue a build job for this component
+		if err := ai.queueBuildJob(ctx, component, service, req); err != nil {
+			ai.entry.Errorf("Failed to queue build job for component %d: %v", component.ID, err)
+			ai.store.UpdateComponentStatus(ctx, component.ID, "failed", fmt.Sprintf("Failed to queue build: %v", err))
+		}
+	case "image":
+		// Mark component as ready and set image info
+		ai.store.UpdateComponentStatus(ctx, component.ID, "ready", "")
+		ai.store.UpdateComponentImageInfo(ctx, component.ID, "latest", service.Image)
+		ai.entry.Infof("Component %d marked as ready with image: %s", component.ID, service.Image)
+	default:
+		ai.entry.Warnf("Unknown service source type: %s", service.Source)
+		ai.store.UpdateComponentStatus(ctx, component.ID, "failed", "Unknown service source type")
+	}
+
+	return component, nil
+}
+
+// queueBuildJob queues a build job for a component that needs to be built
+func (ai *AppImporter) queueBuildJob(ctx context.Context, component *models.Component, service models.ComposeService, req models.AppImportCreateRequest) error {
+	// Use the build context already stored in the component
+	buildContext := component.BuildContext
+	if buildContext == "" {
+		buildContext = "."
+	}
+
+	ai.entry.Infof("Using build context for service %s: %s", service.Name, buildContext)
+
+	// Prepare build request
+	buildReq := models.BuildRequest{
+		ComponentID:  component.ID,
+		SourceURL:    req.SourceURL,
+		Version:      req.Branch,
+		ImageName:    fmt.Sprintf("byop-component-%d", component.ID),
+		RegistryURL:  ai.registryURL,
+		BuildContext: buildContext,
+		Dockerfile:   component.DockerfilePath,
+		Source:       "docker-compose",
+	}
+
+	// Set default dockerfile if not specified
+	if buildReq.Dockerfile == "" {
+		buildReq.Dockerfile = "Dockerfile"
+	}
+
+	_, err := ai.builderSvc.QueueBuildJob(ctx, buildReq)
+	if err != nil {
+		return fmt.Errorf("failed to queue build job: %w", err)
+	}
+
+	ai.entry.Infof("Queued build job for component %d (service: %s)", component.ID, service.Name)
+	return nil
+}
+
+// updateAppComponents updates the app's components field with the component IDs
+func (ai *AppImporter) updateAppComponents(ctx context.Context, appID uint, componentIDs []uint) error {
+	// Convert component IDs to JSON string
+	componentsJSON := "["
+	for i, id := range componentIDs {
+		if i > 0 {
+			componentsJSON += ","
+		}
+		componentsJSON += fmt.Sprintf("%d", id)
+	}
+	componentsJSON += "]"
+
+	// Update the app in the database
+	app, err := ai.store.GetAppByID(ctx, appID)
+	if err != nil {
+		return fmt.Errorf("failed to get app: %w", err)
+	}
+
+	app.Components = componentsJSON
+	if err := ai.store.UpdateApp(ctx, app); err != nil {
+		return fmt.Errorf("failed to update app components: %w", err)
+	}
+
+	return nil
+}
+
+// cloneRepository clones a Git repository to the specified directory
+func (ai *AppImporter) cloneRepository(repoURL, branch, targetDir string) error {
+	ai.entry.Infof("Cloning repository %s (branch: %s) to %s", repoURL, branch, targetDir)
+
+	// Clone options
+	cloneOptions := &git.CloneOptions{
+		URL:      repoURL,
+		Progress: nil, // We could add progress tracking here
+	}
+
+	// Set branch if specified
+	if branch != "" {
+		cloneOptions.ReferenceName = plumbing.NewBranchReferenceName(branch)
+		cloneOptions.SingleBranch = true
+	}
+
+	// Clone the repository
+	_, err := git.PlainClone(targetDir, false, cloneOptions)
+	if err != nil {
+		return fmt.Errorf("failed to clone repository: %w", err)
+	}
+
+	ai.entry.Infof("Successfully cloned repository to %s", targetDir)
+	return nil
+}

+ 155 - 28
services/builder.go

@@ -5,12 +5,15 @@ import (
 	"encoding/json"
 	"fmt"
 	"os" // Added import
+	"path/filepath"
 	"strings"
 	"time"
 
 	"git.linuxforward.com/byop/byop-engine/clients"
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/models"
+	git "github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/sirupsen/logrus"
 )
 
@@ -112,6 +115,7 @@ func (s *Builder) QueueBuildJob(ctx context.Context, req models.BuildRequest) (*
 		RegistryPassword:  req.RegistryPassword, // Added
 		BuildContext:      buildContext,
 		Dockerfile:        dockerfilePath,
+		Source:            req.Source,            // Added
 		DockerfileContent: req.DockerfileContent, // NEW: Generated Dockerfile content
 		NoCache:           req.NoCache,
 		BuildArgs:         buildArgsJSON,
@@ -162,19 +166,45 @@ func (s *Builder) processJob(ctx context.Context, jobID uint) {
 		return
 	}
 
-	// Ensure BuildContext is cleaned up after processing, if it's not the default "."
-	if job.BuildContext != "" && job.BuildContext != defaultBuildContext {
+	s.entry.Infof("Processing build job ID %d for ComponentID %d. Source: %s, BuildContext: %s", job.ID, job.ComponentID, job.Source, job.BuildContext)
+
+	// Clone repository and resolve build context for docker-compose builds
+	resolvedBuildContext := job.BuildContext
+	var tempRepoDir string
+
+	if job.Source == "docker-compose" {
+		var err error
+		resolvedBuildContext, err = s.cloneRepositoryAndResolveBuildContext(ctx, job)
+		if err != nil {
+			s.entry.Errorf("Failed to clone repository and resolve build context for job ID %d: %v", job.ID, err)
+			s.updateJobStatus(ctx, job.ID, job.ComponentID, models.BuildStatusFailed, fmt.Sprintf("Failed to resolve build context: %v", err))
+			s.appendJobLog(ctx, job.ID, fmt.Sprintf("Build failed: %v", err))
+			return
+		}
+
+		// Extract temp repo directory for cleanup
+		if strings.Contains(resolvedBuildContext, job.BuildContext) {
+			// The resolved path contains our build context as suffix, extract the temp repo dir
+			tempRepoDir = strings.TrimSuffix(resolvedBuildContext, job.BuildContext)
+			tempRepoDir = strings.TrimSuffix(tempRepoDir, "/")
+		} else if job.BuildContext == "." || job.BuildContext == "" {
+			tempRepoDir = resolvedBuildContext
+		}
+	}
+
+	// Ensure cleanup of cloned repository after processing
+	if tempRepoDir != "" && tempRepoDir != defaultBuildContext {
 		defer func() {
-			s.entry.Infof("Attempting to clean up build context directory: %s for job ID %d", job.BuildContext, job.ID)
-			if err := os.RemoveAll(job.BuildContext); err != nil {
-				s.entry.Errorf("Failed to clean up build context directory %s for job ID %d: %v", job.BuildContext, job.ID, err)
+			s.entry.Infof("Cleaning up cloned repository directory: %s for job ID %d", tempRepoDir, job.ID)
+			if err := os.RemoveAll(tempRepoDir); err != nil {
+				s.entry.Errorf("Failed to clean up repository directory %s for job ID %d: %v", tempRepoDir, job.ID, err)
 			} else {
-				s.entry.Infof("Successfully cleaned up build context directory: %s for job ID %d", job.BuildContext, job.ID)
+				s.entry.Infof("Successfully cleaned up repository directory: %s for job ID %d", tempRepoDir, job.ID)
 			}
 		}()
 	}
 
-	s.entry.Infof("Processing build job ID %d for ComponentID %d. BuildContext: %s", job.ID, job.ComponentID, job.BuildContext)
+	s.entry.Infof("Using resolved build context for job ID %d: %s", job.ID, resolvedBuildContext)
 
 	// Debug: Check if DockerfileContent was retrieved from database
 	if job.DockerfileContent != "" {
@@ -201,36 +231,79 @@ func (s *Builder) processJob(ctx context.Context, jobID uint) {
 		return
 	}
 
-	buildOutput, err := s.buildMachineClient.BuildImage(ctx, *job, job.Dockerfile, job.BuildContext, job.ImageName, job.ImageTag, job.NoCache, buildArgs)
-	if err != nil {
-		s.entry.Errorf("Build failed for job ID %d: %v", job.ID, err)
-		s.updateJobStatus(ctx, job.ID, job.ComponentID, models.BuildStatusFailed, fmt.Sprintf("Build failed: %v", err))
-		s.appendJobLog(ctx, job.ID, fmt.Sprintf("Build failed: %v", err))
-		return
+	// Use resolved build context for the build
+	dockerfilePath := job.Dockerfile
+	if job.Source == "docker-compose" && dockerfilePath != "" && !filepath.IsAbs(dockerfilePath) {
+		// For docker-compose builds, resolve dockerfile path relative to build context
+		dockerfilePath = filepath.Join(resolvedBuildContext, dockerfilePath)
 	}
 
-	s.entry.Infof("Build completed successfully for job ID %d. Output: %s", job.ID, buildOutput)
-
-	// Debug registry push configuration
-	s.entry.Infof("Registry URL configured: %s", job.RegistryURL)
-	// Push the image to the registry if configured
+	// For registry builds, do build and push in one step for efficiency
 	if job.RegistryURL != "" {
-		s.entry.Infof("Pushing image %s to registry %s", job.FullImageURI, job.RegistryURL)
-		if err := s.registryClient.PushImage(ctx, *job, job.FullImageURI, job.RegistryURL, job.RegistryUser, job.RegistryPassword); err != nil {
-			s.entry.Errorf("Failed to push image %s to registry %s: %v", job.FullImageURI, job.RegistryURL, err)
-			s.updateJobStatus(ctx, job.ID, job.ComponentID, models.BuildStatusFailed, fmt.Sprintf("Failed to push image: %v", err))
-			s.appendJobLog(ctx, job.ID, fmt.Sprintf("Failed to push image: %v", err))
+		s.entry.Infof("Building and pushing image %s to registry %s in one step", job.FullImageURI, job.RegistryURL)
+		buildOutput, err := s.buildAndPushImage(ctx, job, dockerfilePath, resolvedBuildContext, buildArgs)
+		if err != nil {
+			s.entry.Errorf("Build and push failed for job ID %d: %v", job.ID, err)
+			s.updateJobStatus(ctx, job.ID, job.ComponentID, models.BuildStatusFailed, fmt.Sprintf("Build and push failed: %v", err))
+			s.appendJobLog(ctx, job.ID, fmt.Sprintf("Build and push failed: %v", err))
 			return
 		}
-		s.entry.Infof("Image %s successfully pushed to registry %s", job.FullImageURI, job.RegistryURL)
+		s.entry.Infof("Build and push completed successfully for job ID %d. Output: %s", job.ID, buildOutput)
+	} else {
+		// Local build only
+		buildOutput, err := s.buildMachineClient.BuildImage(ctx, *job, dockerfilePath, resolvedBuildContext, job.ImageName, job.ImageTag, job.NoCache, buildArgs)
+		if err != nil {
+			s.entry.Errorf("Build failed for job ID %d: %v", job.ID, err)
+			s.updateJobStatus(ctx, job.ID, job.ComponentID, models.BuildStatusFailed, fmt.Sprintf("Build failed: %v", err))
+			s.appendJobLog(ctx, job.ID, fmt.Sprintf("Build failed: %v", err))
+			return
+		}
+		s.entry.Infof("Build completed successfully for job ID %d. Output: %s", job.ID, buildOutput)
 	}
 
+	// Debug registry push configuration
+	s.entry.Infof("Registry URL configured: %s", job.RegistryURL)
+
+	// For registry builds, the build-and-push was already done above
+	// For local builds, no push is needed
+
 	// Finalize job with success status
 	s.finalizeJob(ctx, job.ID, job.ComponentID, models.BuildStatusSuccess, "")
-	s.appendJobLog(ctx, job.ID, "Build job completed successfully and image pushed to registry.")
+	s.appendJobLog(ctx, job.ID, "Build job completed successfully.")
 	s.entry.Infof("Build job ID %d for ComponentID %d completed successfully.", job.ID, job.ComponentID)
 }
 
+// buildAndPushImage builds and pushes an image in one step for docker-compose builds
+func (s *Builder) buildAndPushImage(ctx context.Context, job *models.BuildJob, dockerfilePath, buildContext string, buildArgs map[string]string) (string, error) {
+	// Create a temporary job with the resolved build context for the build operation
+	tempJob := *job
+	tempJob.BuildContext = buildContext
+
+	// For DockerfileBuilder, we can call BuildImage with push enabled
+	if dockerfileBuilder, ok := s.buildMachineClient.(*clients.DockerfileBuilder); ok {
+		return s.buildAndPushWithDockerfileBuilder(ctx, dockerfileBuilder, &tempJob, dockerfilePath, buildContext, buildArgs)
+	}
+
+	// Fallback: build first, then push (original approach)
+	buildOutput, err := s.buildMachineClient.BuildImage(ctx, tempJob, dockerfilePath, buildContext, job.ImageName, job.ImageTag, job.NoCache, buildArgs)
+	if err != nil {
+		return "", fmt.Errorf("build failed: %w", err)
+	}
+
+	// Push the image
+	if err := s.registryClient.PushImage(ctx, tempJob, job.FullImageURI, job.RegistryURL, job.RegistryUser, job.RegistryPassword); err != nil {
+		return buildOutput, fmt.Errorf("push failed: %w", err)
+	}
+
+	return buildOutput, nil
+}
+
+// buildAndPushWithDockerfileBuilder builds and pushes using DockerfileBuilder with combined build+push
+func (s *Builder) buildAndPushWithDockerfileBuilder(ctx context.Context, builder *clients.DockerfileBuilder, job *models.BuildJob, dockerfilePath, buildContext string, buildArgs map[string]string) (string, error) {
+	// We'll create a custom build-and-push operation that uses BuildKit's ability to export directly to registry
+	return builder.BuildImageWithPush(ctx, *job, dockerfilePath, buildContext, job.ImageName, job.ImageTag, job.NoCache, buildArgs, job.FullImageURI, job.RegistryURL, job.RegistryUser, job.RegistryPassword)
+}
+
 // updateJobStatus updates the job's status in the database.
 func (s *Builder) updateJobStatus(ctx context.Context, jobID uint, componentId uint, status models.BuildStatus, errorMessage string) {
 	if err := s.store.UpdateBuildJobStatus(ctx, jobID, status, errorMessage); err != nil {
@@ -250,7 +323,7 @@ func (s *Builder) updateJobStatus(ctx context.Context, jobID uint, componentId u
 		componentStatus = "in_progress"
 	}
 
-	if updateErr := s.store.UpdateComponentStatus(ctx, int(componentId), componentStatus, errorMessage); updateErr != nil {
+	if updateErr := s.store.UpdateComponentStatus(ctx, componentId, componentStatus, errorMessage); updateErr != nil {
 		s.entry.Errorf("Error updating component status for job ID %d: %v", jobID, updateErr)
 	} else {
 		s.entry.Infof("Updated component status for job ID %d to %s.", jobID, status)
@@ -286,7 +359,7 @@ func (s *Builder) finalizeJob(ctx context.Context, jobID uint, componentId uint,
 			s.entry.Errorf("Error retrieving build job ID %d to update component image info: %v", jobID, err)
 		} else {
 			// Update component with the built image information
-			if err := s.store.UpdateComponentImageInfo(ctx, int(componentId), job.ImageTag, job.FullImageURI); err != nil {
+			if err := s.store.UpdateComponentImageInfo(ctx, componentId, job.ImageTag, job.FullImageURI); err != nil {
 				s.entry.Errorf("Error updating component image info for component ID %d after successful build: %v", componentId, err)
 			} else {
 				s.entry.Infof("Successfully updated component ID %d with image tag %s and URI %s", componentId, job.ImageTag, job.FullImageURI)
@@ -299,7 +372,7 @@ func (s *Builder) finalizeJob(ctx context.Context, jobID uint, componentId uint,
 		componentStatus = "in_progress"
 	}
 
-	if updateErr := s.store.UpdateComponentStatus(ctx, int(componentId), componentStatus, errorMessage); updateErr != nil {
+	if updateErr := s.store.UpdateComponentStatus(ctx, componentId, componentStatus, errorMessage); updateErr != nil {
 		s.entry.Errorf("Error updating component status for job ID %d: %v", jobID, updateErr)
 	} else {
 		s.entry.Infof("Updated component status for job ID %d to %s.", jobID, status)
@@ -319,3 +392,57 @@ func (s *Builder) parseBuildArgs(argsStr string) (map[string]string, error) {
 	}
 	return argsMap, nil
 }
+
+// cloneRepositoryAndResolveBuildContext clones the repository and resolves the build context
+// Returns the absolute path to the build context directory
+func (s *Builder) cloneRepositoryAndResolveBuildContext(ctx context.Context, job *models.BuildJob) (string, error) {
+	// For docker-compose source builds, we need to clone the repository first
+	if job.Source != "docker-compose" {
+		// For non-compose builds, assume build context is already correctly set
+		return job.BuildContext, nil
+	}
+
+	// Create temporary directory for cloning
+	tempDir, err := os.MkdirTemp("", fmt.Sprintf("byop-build-%d-*", job.ID))
+	if err != nil {
+		return "", fmt.Errorf("failed to create temp directory: %w", err)
+	}
+
+	s.entry.Infof("Job %d: Cloning repository %s to %s", job.ID, job.SourceURL, tempDir)
+
+	// Clone options
+	cloneOptions := &git.CloneOptions{
+		URL:      job.SourceURL,
+		Progress: nil,
+	}
+
+	// Set branch if specified in version
+	if job.ImageTag != "" && job.ImageTag != "latest" {
+		cloneOptions.ReferenceName = plumbing.NewBranchReferenceName(job.ImageTag)
+		cloneOptions.SingleBranch = true
+	}
+
+	// Clone the repository
+	_, err = git.PlainClone(tempDir, false, cloneOptions)
+	if err != nil {
+		os.RemoveAll(tempDir)
+		return "", fmt.Errorf("failed to clone repository: %w", err)
+	}
+
+	// Resolve build context relative to the cloned repository
+	var buildContextPath string
+	if job.BuildContext == "" || job.BuildContext == "." {
+		buildContextPath = tempDir
+	} else {
+		buildContextPath = filepath.Join(tempDir, job.BuildContext)
+	}
+
+	// Check if build context directory exists
+	if _, err := os.Stat(buildContextPath); os.IsNotExist(err) {
+		os.RemoveAll(tempDir)
+		return "", fmt.Errorf("build context directory does not exist: %s", job.BuildContext)
+	}
+
+	s.entry.Infof("Job %d: Resolved build context to %s", job.ID, buildContextPath)
+	return buildContextPath, nil
+}

+ 195 - 0
services/compose_parser.go

@@ -0,0 +1,195 @@
+package services
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	"git.linuxforward.com/byop/byop-engine/models"
+	"github.com/sirupsen/logrus"
+	"gopkg.in/yaml.v3"
+)
+
+// ComposeParser handles parsing and analyzing docker-compose.yml files
+type ComposeParser struct {
+	entry *logrus.Entry
+}
+
+// NewComposeParser creates a new ComposeParser instance
+func NewComposeParser() *ComposeParser {
+	return &ComposeParser{
+		entry: logrus.WithField("service", "ComposeParser"),
+	}
+}
+
+// ComposeFile represents the structure of a docker-compose.yml file
+type ComposeFile struct {
+	Version  string                        `yaml:"version,omitempty"`
+	Services map[string]ComposeServiceYAML `yaml:"services"`
+}
+
+// ComposeService represents a service in docker-compose.yml
+type ComposeServiceYAML struct {
+	Image       string        `yaml:"image,omitempty"`
+	Build       interface{}   `yaml:"build,omitempty"`       // Can be string or object
+	Ports       []interface{} `yaml:"ports,omitempty"`       // Can be strings or objects
+	Environment interface{}   `yaml:"environment,omitempty"` // Can be array or map
+	Volumes     []interface{} `yaml:"volumes,omitempty"`
+	Depends_On  []string      `yaml:"depends_on,omitempty"`
+}
+
+// ComposeServiceBuild represents the build configuration
+type ComposeServiceBuild struct {
+	Context    string `yaml:"context,omitempty"`
+	Dockerfile string `yaml:"dockerfile,omitempty"`
+}
+
+// ParseComposeFile parses a docker-compose.yml file from a directory path
+func (cp *ComposeParser) ParseComposeFile(projectDir, composePath string) (*models.AppImportReview, error) {
+	fullComposePath := filepath.Join(projectDir, composePath)
+
+	cp.entry.Infof("Parsing compose file at: %s", fullComposePath)
+
+	// Check if compose file exists
+	if _, err := os.Stat(fullComposePath); os.IsNotExist(err) {
+		return &models.AppImportReview{
+			Valid: false,
+			Error: fmt.Sprintf("docker-compose.yml file not found at %s", composePath),
+		}, nil
+	}
+
+	// Read the compose file
+	content, err := os.ReadFile(fullComposePath)
+	if err != nil {
+		return &models.AppImportReview{
+			Valid: false,
+			Error: fmt.Sprintf("Failed to read docker-compose.yml: %v", err),
+		}, nil
+	}
+
+	return cp.ParseComposeContent(string(content), filepath.Base(projectDir))
+}
+
+// ParseComposeContent parses docker-compose content directly from string
+func (cp *ComposeParser) ParseComposeContent(content, projectName string) (*models.AppImportReview, error) {
+	cp.entry.Info("Parsing compose content from string")
+
+	var composeFile ComposeFile
+	if err := yaml.Unmarshal([]byte(content), &composeFile); err != nil {
+		cp.entry.Errorf("Failed to parse YAML: %v", err)
+		return &models.AppImportReview{
+			Valid: false,
+			Error: fmt.Sprintf("Failed to parse docker-compose.yml: %v", err),
+		}, nil
+	}
+
+	if len(composeFile.Services) == 0 {
+		return &models.AppImportReview{
+			Valid: false,
+			Error: "No services found in docker-compose.yml",
+		}, nil
+	}
+
+	// Convert compose services to our internal format
+	services := make([]models.ComposeService, 0, len(composeFile.Services))
+
+	for serviceName, serviceConfig := range composeFile.Services {
+		composeService := models.ComposeService{
+			Name: serviceName,
+		}
+
+		// Determine if this is a build or image service
+		if serviceConfig.Build != nil {
+			composeService.Source = "build"
+
+			// Handle build configuration (can be string or object)
+			switch build := serviceConfig.Build.(type) {
+			case string:
+				composeService.BuildContext = build
+				composeService.Dockerfile = "Dockerfile" // default
+			case map[string]interface{}:
+				if context, ok := build["context"].(string); ok {
+					composeService.BuildContext = context
+				}
+				if dockerfile, ok := build["dockerfile"].(string); ok {
+					composeService.Dockerfile = dockerfile
+				} else {
+					composeService.Dockerfile = "Dockerfile" // default
+				}
+			}
+		} else if serviceConfig.Image != "" {
+			composeService.Source = "image"
+			composeService.Image = serviceConfig.Image
+		} else {
+			cp.entry.Warnf("Service %s has neither build nor image configuration", serviceName)
+			continue
+		}
+
+		// Extract ports
+		for _, port := range serviceConfig.Ports {
+			switch p := port.(type) {
+			case string:
+				composeService.Ports = append(composeService.Ports, p)
+			case int:
+				composeService.Ports = append(composeService.Ports, strconv.Itoa(p))
+			case map[string]interface{}:
+				if target, ok := p["target"].(int); ok {
+					portStr := strconv.Itoa(target)
+					if published, ok := p["published"].(int); ok {
+						portStr = fmt.Sprintf("%d:%d", published, target)
+					}
+					composeService.Ports = append(composeService.Ports, portStr)
+				}
+			}
+		}
+
+		// Extract environment variables
+		if serviceConfig.Environment != nil {
+			composeService.Environment = make(map[string]string)
+
+			switch env := serviceConfig.Environment.(type) {
+			case []interface{}:
+				for _, item := range env {
+					if envStr, ok := item.(string); ok {
+						parts := strings.SplitN(envStr, "=", 2)
+						if len(parts) == 2 {
+							composeService.Environment[parts[0]] = parts[1]
+						}
+					}
+				}
+			case map[string]interface{}:
+				for key, value := range env {
+					if valueStr, ok := value.(string); ok {
+						composeService.Environment[key] = valueStr
+					}
+				}
+			}
+		}
+
+		// Extract volumes
+		for _, volume := range serviceConfig.Volumes {
+			if volumeStr, ok := volume.(string); ok {
+				composeService.Volumes = append(composeService.Volumes, volumeStr)
+			}
+		}
+
+		services = append(services, composeService)
+	}
+
+	// Use provided project name or default
+	appName := projectName
+	if appName == "" {
+		appName = "imported-app"
+	}
+
+	review := &models.AppImportReview{
+		AppName:  appName,
+		Services: services,
+		Valid:    true,
+	}
+
+	cp.entry.Infof("Successfully parsed compose file with %d services", len(services))
+	return review, nil
+}

+ 87 - 8
services/local_preview.go

@@ -43,7 +43,7 @@ func NewLocalPreviewService(store *dbstore.SQLiteStore, cfg *config.Config, regi
 	entry.Warn("LocalPreviewService initialized - this is for development/testing only, not for production use")
 
 	return &LocalPreviewService{
-		common: NewPreviewCommon(store, registryClient, registryURL, registryUser, registryPass),
+		common: NewPreviewCommon(store, registryURL, registryUser, registryPass),
 		entry:  entry,
 		config: cfg,
 	}
@@ -57,7 +57,7 @@ func (lps *LocalPreviewService) Close(ctx context.Context) {
 }
 
 // CreatePreview creates a local preview environment
-func (lps *LocalPreviewService) CreatePreview(ctx context.Context, appId int) (*models.Preview, error) {
+func (lps *LocalPreviewService) CreatePreview(ctx context.Context, appId uint) (*models.Preview, error) {
 	// Get app details
 	app, err := lps.common.GetStore().GetAppByID(ctx, appId)
 	if err != nil {
@@ -74,10 +74,10 @@ func (lps *LocalPreviewService) CreatePreview(ctx context.Context, appId int) (*
 	preview := models.Preview{
 		AppID:     app.ID,
 		Status:    models.PreviewStatusBuilding,
-		ExpiresAt: time.Now().Add(24 * time.Hour).Format(time.RFC3339),
+		ExpiresAt: time.Now().Add(24 * time.Hour),
 	}
 
-	previewID, err := lps.common.GetStore().CreatePreview(ctx, &preview)
+	err = lps.common.GetStore().CreatePreview(ctx, &preview)
 	if err != nil {
 		if _, ok := err.(models.CustomError); !ok {
 			return nil, models.NewErrInternalServer("failed to create preview record in db", err)
@@ -85,8 +85,6 @@ func (lps *LocalPreviewService) CreatePreview(ctx context.Context, appId int) (*
 		return nil, err
 	}
 
-	preview.ID = previewID
-
 	// Start async build and deploy locally
 	go lps.buildAndDeployPreview(context.Background(), preview, app)
 
@@ -252,6 +250,87 @@ func (lps *LocalPreviewService) deployLocally(ctx context.Context, imageNames []
 func (lps *LocalPreviewService) generatePreviewDockerCompose(ctx context.Context, imageNames []string, app *models.App, previewIDStr string) (string, error) {
 	lps.entry.WithField("app_id", app.ID).WithField("image_count", len(imageNames)).Info("Generating docker-compose content")
 
+	// Get app components to check if this is a docker-compose imported app
+	components, err := lps.common.GetAppComponents(ctx, app)
+	if err != nil {
+		return "", fmt.Errorf("failed to get app components: %w", err)
+	}
+
+	// Check if this is a docker-compose imported app
+	isDockerComposeApp := false
+	for _, component := range components {
+		if component.SourceType == "docker-compose" {
+			isDockerComposeApp = true
+			break
+		}
+	}
+
+	if isDockerComposeApp {
+		return lps.generateDockerComposeFromComponents(ctx, imageNames, components, app, previewIDStr)
+	} else {
+		return lps.generateGenericDockerCompose(ctx, imageNames, app, previewIDStr)
+	}
+}
+
+// generateDockerComposeFromComponents generates a docker-compose file based on the original component configuration
+func (lps *LocalPreviewService) generateDockerComposeFromComponents(ctx context.Context, imageNames []string, components []models.Component, app *models.App, previewIDStr string) (string, error) {
+	lps.entry.WithField("app_id", app.ID).Info("Generating docker-compose from component configuration")
+
+	compose := "services:\n"
+
+	for i, component := range components {
+		if i >= len(imageNames) {
+			continue // Skip if we don't have a corresponding image
+		}
+
+		imageName := imageNames[i]
+		serviceName := component.ServiceName
+		if serviceName == "" {
+			serviceName = component.Name
+		}
+
+		compose += fmt.Sprintf("  %s:\n", serviceName)
+		compose += fmt.Sprintf("    image: %s\n", imageName)
+		compose += "    restart: unless-stopped\n"
+
+		// Add environment variables
+		compose += "    environment:\n"
+		compose += "      - NODE_ENV=preview\n"
+		compose += fmt.Sprintf("      - APP_NAME=%s\n", app.Name)
+
+		// Add labels for tracking
+		compose += "    labels:\n"
+		compose += "      - \"byop.preview=true\"\n"
+		compose += fmt.Sprintf("      - \"byop.preview.id=%s\"\n", previewIDStr)
+		compose += fmt.Sprintf("      - \"byop.app.id=%d\"\n", app.ID)
+		compose += fmt.Sprintf("      - \"byop.app.name=%s\"\n", app.Name)
+
+		// Add Traefik configuration for web components
+		if component.Type == "web" {
+			previewDomain := fmt.Sprintf("%s.%s", previewIDStr, lps.config.PreviewTLD)
+			routerName := fmt.Sprintf("local-preview-%s-%s", previewIDStr, serviceName)
+			compose += "      - \"traefik.enable=true\"\n"
+			compose += fmt.Sprintf("      - \"traefik.http.routers.%s.rule=Host(`%s`)\"\n", routerName, previewDomain)
+			compose += fmt.Sprintf("      - \"traefik.http.routers.%s.entrypoints=websecure\"\n", routerName)
+			compose += fmt.Sprintf("      - \"traefik.http.routers.%s.tls=true\"\n", routerName)
+			compose += fmt.Sprintf("      - \"traefik.http.routers.%s.tls.certresolver=tlsresolver\"\n", routerName)
+			compose += "      - \"traefik.docker.network=traefik\"\n"
+		}
+
+		compose += "    networks:\n"
+		compose += "      - traefik\n"
+		compose += "\n"
+	}
+
+	compose += "networks:\n"
+	compose += "  traefik:\n"
+	compose += "    external: true\n"
+
+	return compose, nil
+}
+
+// generateGenericDockerCompose generates a generic docker-compose file for non-docker-compose apps
+func (lps *LocalPreviewService) generateGenericDockerCompose(ctx context.Context, imageNames []string, app *models.App, previewIDStr string) (string, error) {
 	compose := "services:\n"
 
 	for i, imageName := range imageNames {
@@ -293,7 +372,7 @@ func (lps *LocalPreviewService) generatePreviewDockerCompose(ctx context.Context
 }
 
 // DeletePreview deletes a local preview
-func (lps *LocalPreviewService) DeletePreview(ctx context.Context, appID int) error {
+func (lps *LocalPreviewService) DeletePreview(ctx context.Context, appID uint) error {
 	preview, err := lps.common.GetStore().GetPreviewByAppID(ctx, appID)
 	if err != nil {
 		if models.IsErrNotFound(err) {
@@ -322,7 +401,7 @@ func (lps *LocalPreviewService) DeletePreview(ctx context.Context, appID int) er
 }
 
 // StopPreview stops a local preview
-func (lps *LocalPreviewService) StopPreview(ctx context.Context, previewID int) error {
+func (lps *LocalPreviewService) StopPreview(ctx context.Context, previewID uint) error {
 	preview, err := lps.common.GetStore().GetPreviewByID(ctx, previewID)
 	if err != nil {
 		if models.IsErrNotFound(err) {

+ 416 - 534
services/preview_common.go

@@ -5,7 +5,6 @@ import (
 	"bytes"
 	"context"
 	"crypto/rand"
-	"encoding/base64"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -15,77 +14,44 @@ import (
 	"strings"
 	"time"
 
-	"git.linuxforward.com/byop/byop-engine/clients"
+	"git.linuxforward.com/byop/byop-engine/analyzer"
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/models"
 	"github.com/sirupsen/logrus"
-
-	"github.com/docker/docker/api/types"
-	"github.com/docker/docker/api/types/container"
-	"github.com/docker/docker/api/types/filters"
-	"github.com/docker/docker/api/types/image"
-	"github.com/docker/docker/api/types/registry"
-	docker "github.com/docker/docker/client"
 )
 
 // PreviewService defines the interface for preview services
 type PreviewService interface {
-	CreatePreview(ctx context.Context, appId int) (*models.Preview, error)
-	DeletePreview(ctx context.Context, appID int) error
-	StopPreview(ctx context.Context, previewID int) error
-	Close(ctx context.Context) // Updated signature to include context
+	CreatePreview(ctx context.Context, appId uint) (*models.Preview, error)
+	DeletePreview(ctx context.Context, appID uint) error
+	StopPreview(ctx context.Context, previewID uint) error
+	Close(ctx context.Context)
 }
 
 // PreviewCommon contains shared functionality for preview services
 type PreviewCommon struct {
-	store          *dbstore.SQLiteStore
-	entry          *logrus.Entry
-	dockerClient   *docker.Client
-	registryClient clients.RegistryClient
-	registryURL    string
-	registryUser   string
-	registryPass   string
+	store        *dbstore.SQLiteStore
+	entry        *logrus.Entry
+	registryURL  string
+	registryUser string
+	registryPass string
 }
 
 // NewPreviewCommon creates a new PreviewCommon instance
-func NewPreviewCommon(store *dbstore.SQLiteStore, registryClient clients.RegistryClient, registryURL, registryUser, registryPass string) *PreviewCommon {
-	dockerClient, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithAPIVersionNegotiation())
-	if err != nil {
-		logrus.WithError(err).Fatal("Failed to create Docker client")
-	}
-
+func NewPreviewCommon(store *dbstore.SQLiteStore, registryURL, registryUser, registryPass string) *PreviewCommon {
 	return &PreviewCommon{
-		store:          store,
-		entry:          logrus.WithField("service", "PreviewCommon"),
-		dockerClient:   dockerClient,
-		registryClient: registryClient,
-		registryURL:    registryURL,
-		registryUser:   registryUser,
-		registryPass:   registryPass,
+		store:        store,
+		entry:        logrus.WithField("service", "PreviewCommon"),
+		registryURL:  registryURL,
+		registryUser: registryUser,
+		registryPass: registryPass,
 	}
 }
 
-// Close cleans up the Docker client connection
+// Close cleans up resources
 func (pc *PreviewCommon) Close() {
-	// Clean up BYOP preview images
-	pc.CleanupPreviewImages(context.Background())
-
 	// Clean up preview database state
 	pc.CleanupPreviewState(context.Background())
-
-	// Close the Docker client connection
-	if pc.dockerClient != nil {
-		if err := pc.dockerClient.Close(); err != nil {
-			pc.entry.WithError(err).Error("Failed to close Docker client")
-		} else {
-			pc.entry.Info("Docker client connection closed")
-		}
-	}
-}
-
-// GetDockerClient returns the Docker client
-func (pc *PreviewCommon) GetDockerClient() *docker.Client {
-	return pc.dockerClient
 }
 
 // GetStore returns the database store
@@ -136,11 +102,32 @@ func (pc *PreviewCommon) CloneRepository(ctx context.Context, repoURL, branch, t
 }
 
 // CreateBuildContext creates a tar archive of the build context
-func (pc *PreviewCommon) CreateBuildContext(ctx context.Context, contextDir string) (io.ReadCloser, error) {
+func (pc *PreviewCommon) CreateBuildContext(ctx context.Context, contextDir string, component *models.Component) (io.ReadCloser, error) {
 	var buf bytes.Buffer
 	tw := tar.NewWriter(&buf)
 	defer tw.Close()
 
+	// For docker-compose components, adjust the context directory to use the build context
+	effectiveContextDir := contextDir
+	relativeDockerfilePath := "Dockerfile"
+
+	if component != nil && component.SourceType == "docker-compose" && component.BuildContext != "" {
+		// Resolve the build context directory
+		effectiveContextDir = filepath.Join(contextDir, component.BuildContext)
+
+		// Set Dockerfile path relative to the build context
+		if component.DockerfilePath != "" {
+			relativeDockerfilePath = component.DockerfilePath
+		}
+
+		pc.entry.WithFields(logrus.Fields{
+			"component_id":         component.ID,
+			"build_context":        component.BuildContext,
+			"dockerfile_path":      relativeDockerfilePath,
+			"resolved_context_dir": effectiveContextDir,
+		}).Info("Using docker-compose build context for component")
+	}
+
 	// Common ignore patterns for Git repositories
 	ignorePatterns := []string{
 		".git",
@@ -192,13 +179,13 @@ func (pc *PreviewCommon) CreateBuildContext(ctx context.Context, contextDir stri
 		"documentation",
 	}
 
-	err := filepath.Walk(contextDir, func(file string, fi os.FileInfo, err error) error {
+	err := filepath.Walk(effectiveContextDir, func(file string, fi os.FileInfo, err error) error {
 		if err != nil {
 			return err
 		}
 
 		// Get relative path
-		relPath, err := filepath.Rel(contextDir, file)
+		relPath, err := filepath.Rel(effectiveContextDir, file)
 		if err != nil {
 			return err
 		}
@@ -279,208 +266,59 @@ func (pc *PreviewCommon) CreateBuildContext(ctx context.Context, contextDir stri
 	return io.NopCloser(&buf), nil
 }
 
-// createDockerIgnore creates a .dockerignore file in the specified directory
-func (pc *PreviewCommon) createDockerIgnore(ctx context.Context, contextDir string) {
-	dockerignoreContent := `# Auto-generated by BYOP Engine
-.git
-.gitignore
-node_modules
-.next
-dist
-build
-target
-__pycache__
-*.pyc
-.DS_Store
-Thumbs.db
-*.log
-*.tmp
-*.swp
-.env
-.vscode
-.idea
-playwright
-cypress
-coverage
-test
-tests
-__tests__
-snapshots
-*.test.js
-*.spec.js
-*.test.ts
-*.spec.ts
-*.png
-*.jpg
-*.jpeg
-*.gif
-*.bmp
-*.svg
-*.ico
-*.zip
-*.tar.gz
-*.tar
-*.gz
-README.md
-readme.md
-CHANGELOG.md
-LICENSE
-CONTRIBUTING.md
-*.md
-docs
-documentation
-`
-
-	dockerignorePath := filepath.Join(contextDir, ".dockerignore")
-	if err := os.WriteFile(dockerignorePath, []byte(dockerignoreContent), 0644); err != nil {
-		pc.entry.WithField("path", dockerignorePath).Warnf("Failed to create .dockerignore file: %v", err)
-	} else {
-		pc.entry.WithField("path", dockerignorePath).Debug("Created .dockerignore file")
-	}
+// fileExists checks if a file exists
+func fileExists(path string) bool {
+	_, err := os.Stat(path)
+	return err == nil
 }
 
-// validateAndFixDockerfile checks for unsupported Dockerfile syntax and fixes common issues
-func (pc *PreviewCommon) validateAndFixDockerfile(ctx context.Context, contextDir string) error {
-	dockerfilePath := filepath.Join(contextDir, "Dockerfile")
-
-	// Check if Dockerfile exists
-	if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
-		return fmt.Errorf("dockerfile not found in repository")
-	}
-
-	// Read the Dockerfile
-	content, err := os.ReadFile(dockerfilePath)
-	if err != nil {
-		return fmt.Errorf("failed to read Dockerfile: %v", err)
-	}
-
-	originalContent := string(content)
-	modifiedContent := originalContent
-	modified := false
-
-	// Fix common issues
-	lines := strings.Split(originalContent, "\n")
-	var fixedLines []string
-
-	for i, line := range lines {
-		trimmedLine := strings.TrimSpace(line)
-
-		// Check for unsupported --exclude flag in COPY or ADD commands
-		if strings.HasPrefix(trimmedLine, "COPY") || strings.HasPrefix(trimmedLine, "ADD") {
-			if strings.Contains(trimmedLine, "--exclude") {
-				pc.entry.WithField("line", i+1).Warn("Found unsupported --exclude flag in Dockerfile, removing it")
-				// Remove --exclude flag and its arguments
-				parts := strings.Fields(trimmedLine)
-				var cleanedParts []string
-				skipNext := false
-
-				for _, part := range parts {
-					if skipNext {
-						skipNext = false
-						continue
-					}
-					if strings.HasPrefix(part, "--exclude") {
-						if strings.Contains(part, "=") {
-							// --exclude=pattern format
-							continue
-						} else {
-							// --exclude pattern format
-							skipNext = true
-							continue
-						}
-					}
-					cleanedParts = append(cleanedParts, part)
-				}
-
-				fixedLine := strings.Join(cleanedParts, " ")
-				fixedLines = append(fixedLines, fixedLine)
-				modified = true
-				pc.entry.WithField("original", trimmedLine).WithField("fixed", fixedLine).Info("Fixed Dockerfile line")
-			} else {
-				fixedLines = append(fixedLines, line)
-			}
-		} else {
-			fixedLines = append(fixedLines, line)
-		}
-	}
-
-	// Write back the fixed Dockerfile if modified
-	if modified {
-		modifiedContent = strings.Join(fixedLines, "\n")
-		if err := os.WriteFile(dockerfilePath, []byte(modifiedContent), 0644); err != nil {
-			return fmt.Errorf("failed to write fixed Dockerfile: %v", err)
-		}
-		pc.entry.WithField("path", dockerfilePath).Info("Fixed Dockerfile syntax issues")
-	}
-
-	return nil
-}
-
-// isDockerfilePresent checks if a Dockerfile exists in the repository directory
-func (pc *PreviewCommon) isDockerfilePresent(tempDir string) (bool, error) {
-	// Check for common Dockerfile names
-	dockerfileNames := []string{"Dockerfile", "dockerfile", "Dockerfile.prod", "Dockerfile.production"}
-
-	for _, name := range dockerfileNames {
-		dockerfilePath := filepath.Join(tempDir, name)
-		if _, err := os.Stat(dockerfilePath); err == nil {
-			pc.entry.WithField("dockerfile_path", dockerfilePath).Debug("Found Dockerfile")
-			return true, nil
-		}
-	}
-
-	pc.entry.WithField("temp_dir", tempDir).Debug("No Dockerfile found")
-	return false, nil
-}
-
-// BuildComponentImages builds Docker images for components
-// It first checks for pre-built images in the registry before rebuilding from source
+// BuildComponentImages builds Docker images for components using shell commands
+// This simplified version uses docker build commands directly instead of Docker API
 func (pc *PreviewCommon) BuildComponentImages(ctx context.Context, components []models.Component) ([]string, string, error) {
 	var imageNames []string
 	var allLogs strings.Builder
 
 	for _, component := range components {
-		pc.entry.WithField("component_id", component.ID).WithField("status", component.Status).Info("Processing component for preview")
+		pc.entry.WithFields(logrus.Fields{
+			"component_id":    component.ID,
+			"status":          component.Status,
+			"source_type":     component.SourceType,
+			"build_context":   component.BuildContext,
+			"dockerfile_path": component.DockerfilePath,
+			"service_name":    component.ServiceName,
+		}).Info("Processing component for preview")
+
+		allLogs.WriteString(fmt.Sprintf("Processing component %d (%s) - SourceType: %s, BuildContext: %s, DockerfilePath: %s\n",
+			component.ID, component.Name, component.SourceType, component.BuildContext, component.DockerfilePath))
 
 		// Generate local image name for preview
 		imageName := fmt.Sprintf("byop-preview-%s:%d", component.Name, component.ID)
 
-		// Check if component has pre-built image information
-		if component.CurrentImageURI != "" && component.CurrentImageTag != "" {
-			pc.entry.WithField("component_id", component.ID).WithField("image_uri", component.CurrentImageURI).Info("Component has pre-built image, checking registry")
-			allLogs.WriteString(fmt.Sprintf("Component %d has pre-built image %s, checking availability\n", component.ID, component.CurrentImageURI))
-
-			// Check if the pre-built image exists in the registry
-			if pc.registryClient != nil && pc.registryURL != "" {
-				exists, err := pc.registryClient.CheckImageExists(ctx, component.CurrentImageURI, pc.registryURL, pc.registryUser, pc.registryPass)
-				if err != nil {
-					pc.entry.WithField("component_id", component.ID).WithError(err).Warn("Failed to check if pre-built image exists, falling back to rebuild")
-					allLogs.WriteString(fmt.Sprintf("Failed to check registry image for component %d: %v, rebuilding from source\n", component.ID, err))
-				} else if exists {
-					// Pull the pre-built image from registry to local Docker
-					if err := pc.pullPreBuiltImage(ctx, component.CurrentImageURI, imageName); err != nil {
-						pc.entry.WithField("component_id", component.ID).WithError(err).Warn("Failed to pull pre-built image, falling back to rebuild")
-						allLogs.WriteString(fmt.Sprintf("Failed to pull pre-built image for component %d: %v, rebuilding from source\n", component.ID, err))
-					} else {
-						pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).Info("Successfully used pre-built image")
-						allLogs.WriteString(fmt.Sprintf("Successfully pulled and tagged pre-built image for component %d as %s\n", component.ID, imageName))
-						imageNames = append(imageNames, imageName)
-						continue // Skip to next component
-					}
-				} else {
-					pc.entry.WithField("component_id", component.ID).Info("Pre-built image not found in registry, rebuilding from source")
-					allLogs.WriteString(fmt.Sprintf("Pre-built image for component %d not found in registry, rebuilding from source\n", component.ID))
-				}
-			} else {
-				pc.entry.WithField("component_id", component.ID).Warn("Registry client not configured, cannot check pre-built images")
-				allLogs.WriteString(fmt.Sprintf("Registry not configured for component %d, rebuilding from source\n", component.ID))
-			}
+		// Check if the local preview image already exists using shell command
+		pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).Info("Checking if local preview image exists")
+		checkCmd := exec.CommandContext(ctx, "docker", "image", "inspect", imageName)
+		if err := checkCmd.Run(); err == nil {
+			pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).Info("Local preview image already exists, skipping build")
+			allLogs.WriteString(fmt.Sprintf("Component %d already has local preview image %s, skipping build\n", component.ID, imageName))
+			imageNames = append(imageNames, imageName)
+			continue // Skip to next component
 		} else {
-			pc.entry.WithField("component_id", component.ID).Info("Component has no pre-built image information, building from source")
-			allLogs.WriteString(fmt.Sprintf("Component %d has no pre-built image, building from source\n", component.ID))
+			pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).WithError(err).Info("Local preview image not found, will build from source")
+		}
+
+		// For docker-compose components with public images, use them directly
+		if component.SourceType == "docker-compose" && component.CurrentImageURI != "" &&
+			strings.Contains(component.CurrentImageURI, ":") &&
+			!strings.Contains(component.CurrentImageURI, "host.docker.internal") &&
+			!strings.Contains(component.CurrentImageURI, pc.registryURL) {
+			// This is likely a public image (mysql:5.7, postgres:13, etc.)
+			pc.entry.WithField("component_id", component.ID).WithField("image_uri", component.CurrentImageURI).Info("Using public image directly for preview")
+			allLogs.WriteString(fmt.Sprintf("Component %d using public image %s directly\n", component.ID, component.CurrentImageURI))
+			imageNames = append(imageNames, component.CurrentImageURI)
+			continue // Skip to next component
 		}
 
-		// Fallback: Build from source code
+		// Build from source code
 		pc.entry.WithField("component_id", component.ID).Info("Building Docker image from source")
 		allLogs.WriteString(fmt.Sprintf("Building component %d from source\n", component.ID))
 
@@ -494,104 +332,60 @@ func (pc *PreviewCommon) BuildComponentImages(ctx context.Context, components []
 			return nil, allLogs.String(), err
 		}
 
-		// Special handling for components with existing Dockerfiles (status "valid")
-		if component.Status == "valid" {
-			pc.entry.WithField("component_id", component.ID).Info("Component has existing Dockerfile, building directly")
-			allLogs.WriteString(fmt.Sprintf("Component %d has existing Dockerfile, building directly\n", component.ID))
-
-			// For components with existing Dockerfiles, just use the Dockerfile as-is
-			// No need to validate/fix or create .dockerignore since they should work as-is
-		} else {
-			// For components without existing Dockerfiles (generated via LLB), apply fixes
-			pc.entry.WithField("component_id", component.ID).Info("Component using generated Dockerfile, applying fixes")
-
-			// Create .dockerignore file to exclude unnecessary files
-			pc.createDockerIgnore(ctx, tempDir)
+		// Determine build context and dockerfile path
+		buildContext := tempDir
+		dockerfilePath := "Dockerfile"
 
-			// Check and fix Dockerfile if needed
-			if err := pc.validateAndFixDockerfile(ctx, tempDir); err != nil {
-				allLogs.WriteString(fmt.Sprintf("Failed to validate Dockerfile for component %d: %v\n", component.ID, err))
-				return nil, allLogs.String(), err
+		if component.SourceType == "docker-compose" && component.BuildContext != "" {
+			buildContext = filepath.Join(tempDir, component.BuildContext)
+			if component.DockerfilePath != "" {
+				dockerfilePath = component.DockerfilePath
 			}
 		}
 
-		// Create a tar archive of the build context
-		pc.entry.WithField("component_id", component.ID).Info("Creating build context tar archive")
-		tarReader, err := pc.CreateBuildContext(ctx, tempDir)
-		if err != nil {
-			errMsg := fmt.Sprintf("Failed to create build context for %s: %v", imageName, err)
-			pc.entry.WithField("component_id", component.ID).Error(errMsg)
-			allLogs.WriteString(errMsg + "\n")
-			return nil, allLogs.String(), err
-		}
-		defer tarReader.Close()
-
-		pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).Info("Starting Docker image build")
-		buildResponse, err := pc.dockerClient.ImageBuild(ctx, tarReader, types.ImageBuildOptions{
-			Tags:        []string{imageName},
-			Dockerfile:  "Dockerfile",
-			Remove:      true,
-			ForceRemove: true,
-		})
-		if err != nil {
-			errMsg := fmt.Sprintf("Failed to start build for %s: %v", imageName, err)
-			pc.entry.WithField("component_id", component.ID).Error(errMsg)
-			allLogs.WriteString(errMsg + "\n")
-			return nil, allLogs.String(), err
-		}
-		defer buildResponse.Body.Close()
-
-		// Read and parse build output properly
-		buildOutput, err := io.ReadAll(buildResponse.Body)
-		if err != nil {
-			allLogs.WriteString(fmt.Sprintf("Failed to read build output for %s: %v\n", imageName, err))
-			return nil, allLogs.String(), err
-		}
+		// Check if we need to generate a Dockerfile
+		fullDockerfilePath := filepath.Join(buildContext, dockerfilePath)
+		if component.Status != "valid" || !fileExists(fullDockerfilePath) {
+			pc.entry.WithField("component_id", component.ID).Info("Generating Dockerfile for component")
+			allLogs.WriteString(fmt.Sprintf("Generating Dockerfile for component %d\n", component.ID))
 
-		buildOutputStr := string(buildOutput)
-		allLogs.WriteString(fmt.Sprintf("Building %s:\n%s\n", imageName, buildOutputStr))
-
-		// Check for Docker build errors in JSON output
-		buildSuccess := false
-		buildErrorFound := false
-
-		// Parse each line of JSON output
-		lines := strings.Split(buildOutputStr, "\n")
-		for _, line := range lines {
-			line = strings.TrimSpace(line)
-			if line == "" {
-				continue
+			// Use analyzer to generate Dockerfile
+			stack, err := analyzer.AnalyzeCode(tempDir)
+			if err != nil {
+				allLogs.WriteString(fmt.Sprintf("Failed to analyze code for %s: %v\n", component.Name, err))
+				return nil, allLogs.String(), err
 			}
 
-			// Look for success indicators
-			if strings.Contains(line, `"stream":"Successfully built`) ||
-				strings.Contains(line, `"stream":"Successfully tagged`) {
-				buildSuccess = true
+			dockerfileContent, err := stack.GenerateDockerfile(tempDir)
+			if err != nil {
+				allLogs.WriteString(fmt.Sprintf("Failed to generate Dockerfile for %s: %v\n", component.Name, err))
+				return nil, allLogs.String(), err
 			}
 
-			// Look for error indicators
-			if strings.Contains(line, `"error"`) ||
-				strings.Contains(line, `"errorDetail"`) ||
-				strings.Contains(line, `"stream":"ERROR`) ||
-				strings.Contains(line, `"stream":"The command"`) && strings.Contains(line, "returned a non-zero code") {
-				buildErrorFound = true
-				allLogs.WriteString(fmt.Sprintf("Build error detected in line: %s\n", line))
+			// Write Dockerfile (no Traefik labels here - they'll be added at runtime)
+			if err := os.WriteFile(fullDockerfilePath, []byte(dockerfileContent), 0644); err != nil {
+				allLogs.WriteString(fmt.Sprintf("Failed to write Dockerfile for %s: %v\n", component.Name, err))
+				return nil, allLogs.String(), err
 			}
 		}
 
-		if buildErrorFound {
-			allLogs.WriteString(fmt.Sprintf("Build failed for %s: errors found in build output\n", imageName))
-			return nil, allLogs.String(), fmt.Errorf("docker build failed for %s: check build logs", imageName)
-		}
+		// Build the image using docker build command
+		pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).Info("Building Docker image")
+		allLogs.WriteString(fmt.Sprintf("Building Docker image for component %d: %s\n", component.ID, imageName))
 
-		if !buildSuccess {
-			allLogs.WriteString(fmt.Sprintf("Build failed for %s: no success indicators found in build output\n", imageName))
-			return nil, allLogs.String(), fmt.Errorf("docker build failed for %s: build did not complete successfully", imageName)
-		}
+		buildCmd := exec.CommandContext(ctx, "docker", "build", "-t", imageName, "-f", dockerfilePath, buildContext)
+		output, err := buildCmd.CombinedOutput()
+		buildOutputStr := string(output)
+		allLogs.WriteString(fmt.Sprintf("Build output for %s:\n%s\n", imageName, buildOutputStr))
 
-		// Verify the image exists and is properly tagged
-		_, err = pc.dockerClient.ImageInspect(ctx, imageName)
 		if err != nil {
+			allLogs.WriteString(fmt.Sprintf("Failed to build %s: %v\n", imageName, err))
+			return nil, allLogs.String(), fmt.Errorf("failed to build image %s: %v", imageName, err)
+		}
+
+		// Verify the image was built successfully
+		verifyCmd := exec.CommandContext(ctx, "docker", "image", "inspect", imageName)
+		if err := verifyCmd.Run(); err != nil {
 			allLogs.WriteString(fmt.Sprintf("Build verification failed for %s: image not found after build - %v\n", imageName, err))
 			return nil, allLogs.String(), fmt.Errorf("failed to build image %s: image not found after build", imageName)
 		}
@@ -603,61 +397,19 @@ func (pc *PreviewCommon) BuildComponentImages(ctx context.Context, components []
 	return imageNames, allLogs.String(), nil
 }
 
-// pullPreBuiltImage pulls a pre-built image from the registry and tags it for local use
-func (pc *PreviewCommon) pullPreBuiltImage(ctx context.Context, registryImageURI, localImageName string) error {
-	pc.entry.WithField("registry_image", registryImageURI).WithField("local_image", localImageName).Info("Pulling pre-built image from registry")
-
-	// Pull the image from registry
-	pullOptions := image.PullOptions{}
-
-	// Add authentication if registry credentials are configured
-	if pc.registryUser != "" && pc.registryPass != "" {
-		authConfig := registry.AuthConfig{
-			Username: pc.registryUser,
-			Password: pc.registryPass,
-		}
-		encodedJSON, err := json.Marshal(authConfig)
-		if err != nil {
-			return fmt.Errorf("failed to encode registry auth: %w", err)
-		}
-		pullOptions.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
-	}
-
-	reader, err := pc.dockerClient.ImagePull(ctx, registryImageURI, pullOptions)
-	if err != nil {
-		return fmt.Errorf("failed to pull image %s: %w", registryImageURI, err)
-	}
-	defer reader.Close()
-
-	// Read the pull output (similar to build output)
-	pullOutput, err := io.ReadAll(reader)
-	if err != nil {
-		return fmt.Errorf("failed to read pull output: %w", err)
-	}
-
-	pc.entry.WithField("pull_output", string(pullOutput)).Debug("Image pull completed")
-
-	// Tag the pulled image with the local preview tag
-	err = pc.dockerClient.ImageTag(ctx, registryImageURI, localImageName)
-	if err != nil {
-		return fmt.Errorf("failed to tag image %s as %s: %w", registryImageURI, localImageName, err)
-	}
-
-	// Verify the image is now available locally
-	_, err = pc.dockerClient.ImageInspect(ctx, localImageName)
-	if err != nil {
-		return fmt.Errorf("failed to verify locally tagged image %s: %w", localImageName, err)
-	}
-
-	pc.entry.WithField("local_image", localImageName).Info("Successfully pulled and tagged pre-built image")
-	return nil
-}
-
 // GetAppComponents retrieves components for an app
 func (pc *PreviewCommon) GetAppComponents(ctx context.Context, app *models.App) ([]models.Component, error) {
 	var components []models.Component
 
-	for _, componentID := range app.Components {
+	// Parse the JSON string of component IDs
+	var componentIDs []uint
+	if app.Components != "" {
+		if err := json.Unmarshal([]byte(app.Components), &componentIDs); err != nil {
+			return nil, fmt.Errorf("failed to parse component IDs from app %d: %v", app.ID, err)
+		}
+	}
+
+	for _, componentID := range componentIDs {
 		component, err := pc.store.GetComponentByID(ctx, componentID)
 		if err != nil {
 			return nil, err
@@ -671,39 +423,34 @@ func (pc *PreviewCommon) GetAppComponents(ctx context.Context, app *models.App)
 	return components, nil
 }
 
-// CleanupPreviewImages cleans up BYOP preview Docker images
+// CleanupPreviewImages cleans up BYOP preview Docker images using shell commands
 func (pc *PreviewCommon) CleanupPreviewImages(ctx context.Context) {
 	pc.entry.Info("Cleaning up BYOP preview images...")
 
-	images, err := pc.dockerClient.ImageList(ctx, image.ListOptions{All: true})
+	// List all images and filter for byop-preview
+	cmd := exec.CommandContext(ctx, "docker", "images", "--format", "{{.Repository}}:{{.Tag}}")
+	output, err := cmd.Output()
 	if err != nil {
 		pc.entry.WithError(err).Error("Failed to list images for cleanup")
 		return
 	}
 
 	removedCount := 0
-	for _, img := range images {
-		// Check if image name contains "byop-preview"
-		isPreviewImage := false
-		for _, tag := range img.RepoTags {
-			if strings.Contains(tag, "byop-preview") {
-				isPreviewImage = true
-				break
-			}
-		}
-
-		if !isPreviewImage {
+	lines := strings.Split(string(output), "\n")
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+		if line == "" {
 			continue
 		}
 
-		// Remove the image
-		if _, err := pc.dockerClient.ImageRemove(ctx, img.ID, image.RemoveOptions{
-			Force:         true,
-			PruneChildren: true,
-		}); err != nil {
-			pc.entry.WithError(err).WithField("image_id", img.ID).Warn("Failed to remove preview image")
-		} else {
-			removedCount++
+		if strings.Contains(line, "byop-preview") {
+			// Remove the image
+			rmCmd := exec.CommandContext(ctx, "docker", "rmi", line, "--force")
+			if err := rmCmd.Run(); err != nil {
+				pc.entry.WithError(err).WithField("image", line).Warn("Failed to remove preview image")
+			} else {
+				removedCount++
+			}
 		}
 	}
 
@@ -712,146 +459,44 @@ func (pc *PreviewCommon) CleanupPreviewImages(ctx context.Context) {
 	}
 }
 
-// CleanupByAppID cleans up all BYOP preview containers and images for a specific app ID
-func (pc *PreviewCommon) CleanupByAppID(ctx context.Context, appID int) {
-	pc.entry.WithField("app_id", appID).Info("Cleaning up BYOP preview containers...")
+// CleanupAllPreviewContainers cleans up all BYOP preview containers using shell commands
+func (pc *PreviewCommon) CleanupAllPreviewContainers(ctx context.Context) {
+	pc.entry.Info("Cleaning up all BYOP preview containers...")
 
-	// List all containers
-	containers, err := pc.dockerClient.ContainerList(ctx, container.ListOptions{All: true})
+	// List all containers and filter for byop-preview
+	cmd := exec.CommandContext(ctx, "docker", "ps", "-a", "--format", "{{.Names}}")
+	output, err := cmd.Output()
 	if err != nil {
 		pc.entry.WithError(err).Error("Failed to list containers for cleanup")
 		return
 	}
 
-	for _, ctn := range containers {
-		isPreviewContainer := false
-		containerName := ""
-
-		// Check if the container is a BYOP preview container
-		for key, value := range ctn.Labels {
-			if key == "byop.preview" && value == "true" {
-				isPreviewContainer = true
-				if len(ctn.Names) > 0 {
-					containerName = ctn.Names[0]
-				}
-				break
-			}
-		}
-
-		if !isPreviewContainer {
+	removedCount := 0
+	lines := strings.Split(string(output), "\n")
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+		if line == "" {
 			continue
 		}
 
-		if ctn.Labels["byop.app.id"] != fmt.Sprintf("%d", appID) {
-			continue // Only clean up containers for the specified app ID
-		}
-
-		pc.entry.WithField("container_id", ctn.ID).WithField("container_name", containerName).Info("Removing BYOP preview container")
-
-		// Remove the container
-		if err := pc.dockerClient.ContainerRemove(ctx, ctn.ID, container.RemoveOptions{
-			Force: true,
-		}); err != nil {
-			pc.entry.WithError(err).WithField("container_id", ctn.ID).Warn("Failed to remove preview container")
-		} else {
-			pc.entry.WithField("container_id", ctn.ID).Info("Successfully removed preview container")
-		}
-	}
-}
-
-// CleanupAllPreviewContainers cleans up all BYOP preview containers
-func (pc *PreviewCommon) CleanupAllPreviewContainers(ctx context.Context) {
-	pc.entry.Info("Cleaning up all BYOP preview containers...")
-
-	// Get all containers with filters for BYOP preview containers
-	containers, err := pc.dockerClient.ContainerList(ctx, container.ListOptions{
-		All: true, // Include stopped containers too
-		Filters: filters.NewArgs(
-			filters.Arg("label", "byop.preview=true"),
-		),
-	})
-	if err != nil {
-		pc.entry.WithError(err).Error("Failed to list BYOP preview containers")
-		// Fallback to name-based filtering if labels don't work
-		pc.cleanupByName(ctx)
-		return
-	}
-
-	if len(containers) == 0 {
-		pc.entry.Info("No BYOP preview containers found to cleanup")
-	} else {
-		pc.entry.WithField("container_count", len(containers)).Info("Found BYOP preview containers to cleanup")
-	}
-
-	// Remove BYOP preview containers
-	for _, ctn := range containers {
-		containerName := "unknown"
-		if len(ctn.Names) > 0 {
-			containerName = strings.TrimPrefix(ctn.Names[0], "/")
-		}
+		if strings.Contains(line, "byop-preview") || strings.Contains(line, "preview") {
+			pc.entry.WithField("container_name", line).Info("Removing BYOP preview container")
 
-		pc.entry.WithField("container_id", ctn.ID).WithField("container_name", containerName).Info("Removing BYOP preview container")
+			// Stop and remove container
+			stopCmd := exec.CommandContext(ctx, "docker", "stop", line)
+			stopCmd.Run() // Ignore errors for stop
 
-		// Stop container first if it's running
-		if ctn.State == "running" {
-			if err := pc.dockerClient.ContainerStop(ctx, ctn.ID, container.StopOptions{}); err != nil {
-				pc.entry.WithError(err).WithField("container_id", ctn.ID).Warn("Failed to stop container, will force remove")
+			rmCmd := exec.CommandContext(ctx, "docker", "rm", "-f", line)
+			if err := rmCmd.Run(); err != nil {
+				pc.entry.WithError(err).WithField("container_name", line).Error("Failed to remove container")
+			} else {
+				removedCount++
 			}
 		}
-
-		// Remove container
-		if err := pc.dockerClient.ContainerRemove(ctx, ctn.ID, container.RemoveOptions{
-			Force:         true,
-			RemoveVolumes: true,
-		}); err != nil {
-			pc.entry.WithError(err).WithField("container_id", ctn.ID).Error("Failed to remove BYOP preview container")
-		} else {
-			pc.entry.WithField("container_id", ctn.ID).WithField("container_name", containerName).Info("Successfully removed BYOP preview container")
-		}
-	}
-}
-
-// Fallback method to cleanup containers by name pattern
-func (pc *PreviewCommon) cleanupByName(ctx context.Context) {
-	pc.entry.Info("Using fallback name-based container cleanup")
-
-	containers, err := pc.dockerClient.ContainerList(ctx, container.ListOptions{All: true})
-	if err != nil {
-		pc.entry.WithError(err).Error("Failed to list containers for name-based cleanup")
-		return
 	}
 
-	for _, ctn := range containers {
-		// Check if any container name contains "byop-preview"
-		isPreviewContainer := false
-		containerName := "unknown"
-
-		for _, name := range ctn.Names {
-			cleanName := strings.TrimPrefix(name, "/")
-			if strings.Contains(cleanName, "byop-preview") || strings.Contains(cleanName, "preview") {
-				isPreviewContainer = true
-				containerName = cleanName
-				break
-			}
-		}
-
-		if !isPreviewContainer {
-			continue
-		}
-
-		pc.entry.WithField("container_id", ctn.ID).WithField("container_name", containerName).Info("Removing BYOP preview container (name-based)")
-
-		// Stop and remove
-		if ctn.State == "running" {
-			pc.dockerClient.ContainerStop(ctx, ctn.ID, container.StopOptions{})
-		}
-
-		if err := pc.dockerClient.ContainerRemove(ctx, ctn.ID, container.RemoveOptions{
-			Force:         true,
-			RemoveVolumes: true,
-		}); err != nil {
-			pc.entry.WithError(err).WithField("container_id", ctn.ID).Error("Failed to remove container")
-		}
+	if removedCount > 0 {
+		pc.entry.WithField("removed_containers", removedCount).Info("Cleaned up BYOP preview containers")
 	}
 }
 
@@ -898,7 +543,7 @@ func (pc *PreviewCommon) CleanupPreviewState(ctx context.Context) {
 }
 
 // GetPreviewImageNames reconstructs the Docker image names used for a preview
-func (pc *PreviewCommon) GetPreviewImageNames(appID int) ([]string, error) {
+func (pc *PreviewCommon) GetPreviewImageNames(appID uint) ([]string, error) {
 	// Get app details
 	app, err := pc.store.GetAppByID(context.Background(), appID)
 	if err != nil {
@@ -921,8 +566,8 @@ func (pc *PreviewCommon) GetPreviewImageNames(appID int) ([]string, error) {
 	return imageNames, nil
 }
 
-// CleanupPreviewImagesForApp cleans up Docker images for a specific app (works for both local and remote)
-func (pc *PreviewCommon) CleanupPreviewImagesForApp(ctx context.Context, appID int, isRemote bool, ipAddress string) error {
+// CleanupPreviewImagesForApp cleans up Docker images for a specific app using shell commands
+func (pc *PreviewCommon) CleanupPreviewImagesForApp(ctx context.Context, appID uint, isRemote bool, ipAddress string) error {
 	imageNames, err := pc.GetPreviewImageNames(appID)
 	if err != nil {
 		pc.entry.WithField("app_id", appID).WithError(err).Warn("Failed to get preview image names for cleanup")
@@ -936,16 +581,14 @@ func (pc *PreviewCommon) CleanupPreviewImagesForApp(ctx context.Context, appID i
 	}
 }
 
-// cleanupLocalDockerImages removes specific Docker images locally
+// cleanupLocalDockerImages removes specific Docker images locally using shell commands
 func (pc *PreviewCommon) cleanupLocalDockerImages(ctx context.Context, imageNames []string) error {
 	pc.entry.WithField("image_count", len(imageNames)).Info("Cleaning up specific Docker images locally")
 
 	for _, imageName := range imageNames {
-		// Remove the image locally using Docker client
-		if _, err := pc.dockerClient.ImageRemove(ctx, imageName, image.RemoveOptions{
-			Force:         true,
-			PruneChildren: true,
-		}); err != nil {
+		// Remove the image locally using docker rmi command
+		cmd := exec.CommandContext(ctx, "docker", "rmi", imageName, "--force")
+		if err := cmd.Run(); err != nil {
 			// Log warning but don't fail the cleanup - image might already be removed or in use
 			pc.entry.WithField("image_name", imageName).WithError(err).Warn("Failed to remove Docker image locally (this may be normal)")
 		} else {
@@ -1007,20 +650,259 @@ func (pc *PreviewCommon) executeSSHCommand(ctx context.Context, ipAddress, comma
 }
 
 // Database helper methods
-func (pc *PreviewCommon) UpdatePreviewStatus(ctx context.Context, previewID int, status, errorMsg string) {
+func (pc *PreviewCommon) UpdatePreviewStatus(ctx context.Context, previewID uint, status, errorMsg string) {
 	if err := pc.store.UpdatePreviewStatus(ctx, previewID, status, errorMsg); err != nil {
 		pc.entry.WithField("preview_id", previewID).Errorf("Failed to update preview status: %v", err)
 	}
 }
 
-func (pc *PreviewCommon) UpdatePreviewBuildLogs(ctx context.Context, previewID int, logs string) {
+func (pc *PreviewCommon) UpdatePreviewBuildLogs(ctx context.Context, previewID uint, logs string) {
 	if err := pc.store.UpdatePreviewBuildLogs(ctx, previewID, logs); err != nil {
 		pc.entry.WithField("preview_id", previewID).Errorf("Failed to update preview build logs: %v", err)
 	}
 }
 
-func (pc *PreviewCommon) UpdatePreviewDeployLogs(ctx context.Context, previewID int, logs string) {
+func (pc *PreviewCommon) UpdatePreviewDeployLogs(ctx context.Context, previewID uint, logs string) {
 	if err := pc.store.UpdatePreviewDeployLogs(ctx, previewID, logs); err != nil {
 		pc.entry.WithField("preview_id", previewID).Errorf("Failed to update preview deploy logs: %v", err)
 	}
 }
+
+// CleanupByAppID cleans up all BYOP preview containers and images for a specific app ID using shell commands
+func (pc *PreviewCommon) CleanupByAppID(ctx context.Context, appID uint) {
+	pc.entry.WithField("app_id", appID).Info("Cleaning up BYOP preview containers...")
+
+	// List all containers and filter for byop-preview containers with the specific app ID
+	// We'll use the app ID in the container naming pattern
+	appPattern := fmt.Sprintf("byop-preview-app-%d", appID)
+
+	cmd := exec.CommandContext(ctx, "docker", "ps", "-a", "--format", "{{.Names}}")
+	output, err := cmd.Output()
+	if err != nil {
+		pc.entry.WithError(err).Error("Failed to list containers for cleanup")
+		return
+	}
+
+	removedCount := 0
+	lines := strings.Split(string(output), "\n")
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+
+		// Check if this container is for the specific app
+		if strings.Contains(line, appPattern) || (strings.Contains(line, "byop-preview") && strings.Contains(line, fmt.Sprintf("-%d-", appID))) {
+			pc.entry.WithField("container_name", line).WithField("app_id", appID).Info("Removing BYOP preview container for app")
+
+			// Stop and remove container
+			stopCmd := exec.CommandContext(ctx, "docker", "stop", line)
+			stopCmd.Run() // Ignore errors for stop
+
+			rmCmd := exec.CommandContext(ctx, "docker", "rm", "-f", line)
+			if err := rmCmd.Run(); err != nil {
+				pc.entry.WithError(err).WithField("container_name", line).WithField("app_id", appID).Error("Failed to remove container")
+			} else {
+				removedCount++
+			}
+		}
+	}
+
+	if removedCount > 0 {
+		pc.entry.WithField("removed_containers", removedCount).WithField("app_id", appID).Info("Cleaned up BYOP preview containers for app")
+	}
+}
+
+// AddTraefikLabelsToDockerfile adds Traefik routing labels to a Dockerfile
+func (pc *PreviewCommon) AddTraefikLabelsToDockerfile(dockerfileContent, appName string, appID uint, port string) string {
+	if port == "" {
+		port = "3000" // Default port
+	}
+
+	// Generate the preview subdomain
+	previewID := pc.GeneratePreviewID()
+	subdomain := fmt.Sprintf("%s-%d-%s", appName, appID, previewID)
+
+	// Traefik labels to add
+	traefikLabels := []string{
+		`LABEL traefik.enable="true"`,
+		fmt.Sprintf(`LABEL traefik.http.routers.%s.rule="Host(%s.preview.byop.dev)"`, subdomain, "`"+subdomain+"`"),
+		fmt.Sprintf(`LABEL traefik.http.routers.%s.entrypoints="websecure"`, subdomain),
+		fmt.Sprintf(`LABEL traefik.http.routers.%s.tls.certresolver="letsencrypt"`, subdomain),
+		fmt.Sprintf(`LABEL traefik.http.services.%s.loadbalancer.server.port="%s"`, subdomain, port),
+		`LABEL traefik.docker.network="traefik"`,
+		`LABEL byop.preview="true"`,
+		fmt.Sprintf(`LABEL byop.app.id="%d"`, appID),
+		fmt.Sprintf(`LABEL byop.app.name="%s"`, appName),
+		fmt.Sprintf(`LABEL byop.preview.id="%s"`, previewID),
+	}
+
+	// Parse the Dockerfile content
+	lines := strings.Split(dockerfileContent, "\n")
+	var modifiedLines []string
+
+	// Find the last non-empty, non-comment line to insert labels before any final CMD/ENTRYPOINT
+	lastInstructionIndex := -1
+	for i := len(lines) - 1; i >= 0; i-- {
+		line := strings.TrimSpace(lines[i])
+		if line != "" && !strings.HasPrefix(line, "#") {
+			if strings.HasPrefix(strings.ToUpper(line), "CMD") ||
+				strings.HasPrefix(strings.ToUpper(line), "ENTRYPOINT") {
+				lastInstructionIndex = i
+				break
+			}
+		}
+	}
+
+	// If we found a CMD/ENTRYPOINT, insert labels before it
+	if lastInstructionIndex != -1 {
+		modifiedLines = append(modifiedLines, lines[:lastInstructionIndex]...)
+		modifiedLines = append(modifiedLines, "")
+		modifiedLines = append(modifiedLines, "# Traefik labels for BYOP preview routing")
+		modifiedLines = append(modifiedLines, traefikLabels...)
+		modifiedLines = append(modifiedLines, "")
+		modifiedLines = append(modifiedLines, lines[lastInstructionIndex:]...)
+	} else {
+		// No CMD/ENTRYPOINT found, just append labels at the end
+		modifiedLines = append(modifiedLines, lines...)
+		modifiedLines = append(modifiedLines, "")
+		modifiedLines = append(modifiedLines, "# Traefik labels for BYOP preview routing")
+		modifiedLines = append(modifiedLines, traefikLabels...)
+	}
+
+	return strings.Join(modifiedLines, "\n")
+}
+
+// DetectPortFromDockerfile attempts to detect the exposed port from a Dockerfile
+func (pc *PreviewCommon) DetectPortFromDockerfile(dockerfileContent string) string {
+	lines := strings.Split(dockerfileContent, "\n")
+
+	for _, line := range lines {
+		line = strings.TrimSpace(strings.ToUpper(line))
+		if strings.HasPrefix(line, "EXPOSE ") {
+			// Extract port number
+			parts := strings.Fields(line)
+			if len(parts) >= 2 {
+				port := strings.Split(parts[1], "/")[0] // Handle cases like "3000/tcp"
+				return port
+			}
+		}
+	}
+
+	// Common default ports based on technology detection
+	content := strings.ToLower(dockerfileContent)
+	if strings.Contains(content, "node") || strings.Contains(content, "npm") {
+		return "3000"
+	} else if strings.Contains(content, "python") || strings.Contains(content, "flask") {
+		return "5000"
+	} else if strings.Contains(content, "django") {
+		return "8000"
+	} else if strings.Contains(content, "nginx") {
+		return "80"
+	}
+
+	return "3000" // Default fallback
+}
+
+// GenerateTraefikComposeOverride creates a docker-compose override for Traefik routing
+// This is used during preview deployment to add routing labels to pre-built images
+func (pc *PreviewCommon) GenerateTraefikComposeOverride(serviceName, appName string, appID uint, port string, previewID string) string {
+	if port == "" {
+		port = "3000"
+	}
+
+	if previewID == "" {
+		previewID = pc.GeneratePreviewID()
+	}
+
+	subdomain := fmt.Sprintf("%s-%d-%s", appName, appID, previewID)
+
+	override := fmt.Sprintf(`version: '3.8'
+services:
+  %s:
+    labels:
+      - traefik.enable=true
+      - traefik.http.routers.%s.rule=Host(%s%s.preview.byop.dev%s)
+      - traefik.http.routers.%s.entrypoints=websecure
+      - traefik.http.routers.%s.tls.certresolver=letsencrypt
+      - traefik.http.services.%s.loadbalancer.server.port=%s
+      - traefik.docker.network=traefik
+      - byop.preview=true
+      - byop.app.id=%d
+      - byop.app.name=%s
+      - byop.preview.id=%s
+    networks:
+      - traefik
+      - default
+
+networks:
+  traefik:
+    external: true
+`, serviceName, subdomain, "`", subdomain, "`", subdomain, subdomain, subdomain, port, appID, appName, previewID)
+
+	return override
+}
+
+// GeneratePreviewComposeFile creates a complete docker-compose file for preview deployment
+// This uses pre-built component images and adds Traefik routing
+func (pc *PreviewCommon) GeneratePreviewComposeFile(app *models.App, components []models.Component, imageNames []string, previewID string) (string, error) {
+	if len(components) != len(imageNames) {
+		return "", fmt.Errorf("component count (%d) doesn't match image count (%d)", len(components), len(imageNames))
+	}
+
+	var services []string
+
+	for i, component := range components {
+		imageName := imageNames[i]
+		serviceName := component.ServiceName
+		if serviceName == "" {
+			serviceName = component.Name
+		}
+
+		// Detect port from component or use default
+		port := "3000" // Default port
+
+		// Try to detect port from the built image if available
+		// For now, we'll use a default since we don't store port info in Component model
+
+		// Generate subdomain for this component
+		subdomain := fmt.Sprintf("%s-%d-%s", app.Name, app.ID, previewID)
+		if len(components) > 1 {
+			subdomain = fmt.Sprintf("%s-%s-%d-%s", app.Name, component.Name, app.ID, previewID)
+		}
+
+		serviceConfig := fmt.Sprintf(`  %s:
+    image: %s
+    labels:
+      - traefik.enable=true
+      - traefik.http.routers.%s.rule=Host(%s%s.preview.byop.dev%s)
+      - traefik.http.routers.%s.entrypoints=websecure
+      - traefik.http.routers.%s.tls.certresolver=letsencrypt
+      - traefik.http.services.%s.loadbalancer.server.port=%s
+      - traefik.docker.network=traefik
+      - byop.preview=true
+      - byop.app.id=%d
+      - byop.app.name=%s
+      - byop.preview.id=%s
+      - byop.component.id=%d
+      - byop.component.name=%s
+    networks:
+      - traefik
+      - default
+    restart: unless-stopped`,
+			serviceName, imageName, subdomain, "`", subdomain, "`", subdomain, subdomain, subdomain, port, app.ID, app.Name, previewID, component.ID, component.Name)
+
+		services = append(services, serviceConfig)
+	}
+
+	composeFile := fmt.Sprintf(`version: '3.8'
+services:
+%s
+
+networks:
+  traefik:
+    external: true
+`, strings.Join(services, "\n\n"))
+
+	return composeFile, nil
+}

+ 32 - 0
services/preview_factory.go

@@ -0,0 +1,32 @@
+package services
+
+import (
+	"git.linuxforward.com/byop/byop-engine/clients"
+	"git.linuxforward.com/byop/byop-engine/cloud"
+	"git.linuxforward.com/byop/byop-engine/config"
+	"git.linuxforward.com/byop/byop-engine/dbstore"
+	"github.com/sirupsen/logrus"
+)
+
+// NewPreviewService creates the appropriate preview service based on configuration
+// This is a factory function that replaces the manager pattern
+func NewPreviewService(store *dbstore.SQLiteStore, ovhProvider cloud.Provider, cfg *config.Config, registryClient clients.RegistryClient, registryURL, registryUser, registryPass string) PreviewService {
+	entry := logrus.WithField("service", "PreviewServiceFactory")
+
+	// Determine which service to use based on configuration
+	useLocal := cfg.LocalPreview
+
+	// In debug/development mode, we could force local preview
+	if cfg.Debug {
+		entry.Info("Debug mode enabled - forcing local preview service")
+		useLocal = true
+	}
+
+	if useLocal {
+		entry.Warn("Using local preview service - this is for development/testing only, not recommended for production")
+		return NewLocalPreviewService(store, cfg, registryClient, registryURL, registryUser, registryPass)
+	}
+
+	entry.Info("Using remote VPS preview service for production deployment")
+	return NewRemotePreviewService(store, ovhProvider, cfg, registryClient, registryURL, registryUser, registryPass)
+}

+ 0 - 98
services/preview_manager.go

@@ -1,98 +0,0 @@
-package services
-
-import (
-	"context"
-
-	"git.linuxforward.com/byop/byop-engine/clients"
-	"git.linuxforward.com/byop/byop-engine/cloud"
-	"git.linuxforward.com/byop/byop-engine/config"
-	"git.linuxforward.com/byop/byop-engine/dbstore"
-	"git.linuxforward.com/byop/byop-engine/models"
-	"github.com/sirupsen/logrus"
-)
-
-// PreviewServiceManager manages both local and remote preview services
-type PreviewServiceManager struct {
-	localService  *LocalPreviewService
-	remoteService *RemotePreviewService
-	useLocal      bool // Configuration flag to determine which service to use
-	config        *config.Config
-	entry         *logrus.Entry
-}
-
-// NewPreviewServiceManager creates a new preview service manager
-func NewPreviewServiceManager(store *dbstore.SQLiteStore, ovhProvider cloud.Provider, useLocal bool, cfg *config.Config, registryClient clients.RegistryClient, registryURL, registryUser, registryPass string) *PreviewServiceManager {
-	entry := logrus.WithField("service", "PreviewServiceManager")
-
-	// Warn if using local preview (development/testing only)
-	if useLocal {
-		entry.Warn("Using local preview service - this is for development/testing only, not recommended for production")
-	} else {
-		entry.Info("Using remote VPS preview service for production deployment")
-	}
-
-	return &PreviewServiceManager{
-		localService:  NewLocalPreviewService(store, cfg, registryClient, registryURL, registryUser, registryPass),
-		remoteService: NewRemotePreviewService(store, ovhProvider, cfg, registryClient, registryURL, registryUser, registryPass),
-		useLocal:      useLocal,
-		config:        cfg,
-		entry:         entry,
-	}
-}
-
-// CreatePreview creates a preview using the configured service
-func (psm *PreviewServiceManager) CreatePreview(ctx context.Context, appId int) (*models.Preview, error) {
-	if psm.useLocal {
-		psm.entry.WithField("app_id", appId).Debug("Creating preview using local service (development mode)")
-		return psm.localService.CreatePreview(ctx, appId)
-	}
-	psm.entry.WithField("app_id", appId).Debug("Creating preview using remote VPS service (production mode)")
-	return psm.remoteService.CreatePreview(ctx, appId)
-}
-
-// DeletePreview deletes a preview using the configured service
-func (psm *PreviewServiceManager) DeletePreview(ctx context.Context, appID int) error {
-	if psm.useLocal {
-		return psm.localService.DeletePreview(ctx, appID)
-	}
-	return psm.remoteService.DeletePreview(ctx, appID)
-}
-
-// StopPreview stops a preview using the configured service
-func (psm *PreviewServiceManager) StopPreview(ctx context.Context, previewID int) error {
-	if psm.useLocal {
-		return psm.localService.StopPreview(ctx, previewID)
-	}
-	return psm.remoteService.StopPreview(ctx, previewID)
-}
-
-// Close cleans up both services
-func (psm *PreviewServiceManager) Close(ctx context.Context) {
-	if psm.useLocal {
-		psm.entry.Info("Closing local preview service")
-		psm.localService.Close(ctx)
-		return
-	}
-	psm.entry.Info("Closing remote preview service")
-	psm.remoteService.Close(ctx)
-}
-
-// GetLocalService returns the local preview service (for direct access if needed)
-func (psm *PreviewServiceManager) GetLocalService() *LocalPreviewService {
-	return psm.localService
-}
-
-// GetRemoteService returns the remote preview service (for direct access if needed)
-func (psm *PreviewServiceManager) GetRemoteService() *RemotePreviewService {
-	return psm.remoteService
-}
-
-// SetUseLocal configures whether to use local or remote service
-func (psm *PreviewServiceManager) SetUseLocal(useLocal bool) {
-	psm.useLocal = useLocal
-}
-
-// IsUsingLocal returns true if using local service
-func (psm *PreviewServiceManager) IsUsingLocal() bool {
-	return psm.useLocal
-}

+ 8 - 10
services/remote_preview.go

@@ -28,7 +28,7 @@ type RemotePreviewService struct {
 // NewRemotePreviewService creates a new RemotePreviewService
 func NewRemotePreviewService(store *dbstore.SQLiteStore, ovhProvider cloud.Provider, cfg *config.Config, registryClient clients.RegistryClient, registryURL, registryUser, registryPass string) *RemotePreviewService {
 	return &RemotePreviewService{
-		common:      NewPreviewCommon(store, registryClient, registryURL, registryUser, registryPass),
+		common:      NewPreviewCommon(store, registryURL, registryUser, registryPass),
 		entry:       logrus.WithField("service", "RemotePreviewService"),
 		ovhProvider: ovhProvider,
 		config:      cfg,
@@ -144,7 +144,7 @@ func (rps *RemotePreviewService) cleanupVPSResources(ctx context.Context, ipAddr
 }
 
 // CreatePreview creates a remote preview environment on a VPS
-func (rps *RemotePreviewService) CreatePreview(ctx context.Context, appId int) (*models.Preview, error) {
+func (rps *RemotePreviewService) CreatePreview(ctx context.Context, appId uint) (*models.Preview, error) {
 	// Get app details
 	app, err := rps.common.GetStore().GetAppByID(ctx, appId)
 	if err != nil {
@@ -155,16 +155,14 @@ func (rps *RemotePreviewService) CreatePreview(ctx context.Context, appId int) (
 	preview := models.Preview{
 		AppID:     app.ID,
 		Status:    "building",
-		ExpiresAt: time.Now().Add(24 * time.Hour).Format(time.RFC3339), // 24h expiry
+		ExpiresAt: time.Now().Add(24 * time.Hour), // 24h expiry
 	}
 
-	previewID, err := rps.common.GetStore().CreatePreview(ctx, &preview)
+	err = rps.common.GetStore().CreatePreview(ctx, &preview)
 	if err != nil {
 		return nil, fmt.Errorf("failed to create preview record: %v", err)
 	}
 
-	preview.ID = previewID
-
 	// Start async build and deploy to VPS
 	go rps.buildAndDeployPreview(ctx, preview, app)
 
@@ -281,7 +279,7 @@ func (rps *RemotePreviewService) findAvailablePreviewVPS(ctx context.Context) (*
 	return nil, fmt.Errorf("no available VPS with capacity found")
 }
 
-func (rps *RemotePreviewService) deployToVPS(ctx context.Context, ipAddress string, imageNames []string, app *models.App, previewID int, previewUUID string) (string, error) {
+func (rps *RemotePreviewService) deployToVPS(ctx context.Context, ipAddress string, imageNames []string, app *models.App, previewID uint, previewUUID string) (string, error) {
 	var logs strings.Builder
 
 	rps.entry.WithField("ip_address", ipAddress).WithField("app_name", app.Name).WithField("preview_id", previewID).Info("Starting deployment to VPS")
@@ -378,7 +376,7 @@ func (rps *RemotePreviewService) deployToVPS(ctx context.Context, ipAddress stri
 	return logs.String(), nil
 }
 
-func (rps *RemotePreviewService) generatePreviewDockerCompose(imageNames []string, app *models.App, previewID int, previewUUID string) (string, error) {
+func (rps *RemotePreviewService) generatePreviewDockerCompose(imageNames []string, app *models.App, previewID uint, previewUUID string) (string, error) {
 	rps.entry.WithField("app_id", app.ID).WithField("preview_id", previewID).WithField("image_count", len(imageNames)).Info("Generating docker-compose content for remote deployment")
 
 	compose := "services:\n"
@@ -425,7 +423,7 @@ func (rps *RemotePreviewService) generatePreviewDockerCompose(imageNames []strin
 }
 
 // DeletePreview deletes a remote preview
-func (rps *RemotePreviewService) DeletePreview(ctx context.Context, appID int) error {
+func (rps *RemotePreviewService) DeletePreview(ctx context.Context, appID uint) error {
 	// Get the preview to ensure it exists
 	preview, err := rps.common.GetStore().GetPreviewByAppID(ctx, appID)
 	if err != nil {
@@ -481,7 +479,7 @@ func (rps *RemotePreviewService) DeletePreview(ctx context.Context, appID int) e
 }
 
 // StopPreview stops a remote preview
-func (rps *RemotePreviewService) StopPreview(ctx context.Context, previewID int) error {
+func (rps *RemotePreviewService) StopPreview(ctx context.Context, previewID uint) error {
 	preview, err := rps.common.GetStore().GetPreviewByID(ctx, previewID)
 	if err != nil {
 		return err