lblt hai 1 día
pai
achega
b2751aefb6
Modificáronse 47 ficheiros con 2464 adicións e 3080 borrados
  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=BIN
      byop-engine
  7. BIN=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
 	// Get Ovh provider
 	ovhProvider, _ := cloud.GetProvider("ovh")
 	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.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")
 	a.entry.Info("Services initialized successfully, including authentication and database manager")
 	return nil
 	return nil
@@ -73,7 +74,7 @@ func (a *App) initHandlers() error {
 	a.componentHandler = handlers.NewComponentHandler(a.database, a.builderService, a.cnf.ReistryUrl)
 	a.componentHandler = handlers.NewComponentHandler(a.database, a.builderService, a.cnf.ReistryUrl)
 
 
 	// Initialize AppModule
 	// Initialize AppModule
-	a.appHandler = handlers.NewAppsHandler(a.database, a.previewService)
+	a.appHandler = handlers.NewAppsHandler(a.database, a.previewService, a.appImporter)
 
 
 	// Initialize DeploymentModule
 	// Initialize DeploymentModule
 	a.deploymentHandler = handlers.NewDeploymentHandler(a.database)
 	a.deploymentHandler = handlers.NewDeploymentHandler(a.database)
@@ -190,6 +191,9 @@ func (a *App) setupRoutes() {
 	apps.POST("/:id/preview", a.appHandler.CreateAppPreview)
 	apps.POST("/:id/preview", a.appHandler.CreateAppPreview)
 	apps.GET("/:id/preview", a.appHandler.GetAppPreview)
 	apps.GET("/:id/preview", a.appHandler.GetAppPreview)
 	apps.DELETE("/:id/preview", a.appHandler.DeleteAppPreview)
 	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
 	// Deployment routes - need to handle both versions
 	deployments := protected.Group("/deployments")
 	deployments := protected.Group("/deployments")

+ 1 - 0
app/server.go

@@ -48,6 +48,7 @@ type App struct {
 	// Preview Service
 	// Preview Service
 	previewService services.PreviewService
 	previewService services.PreviewService
 	builderService *services.Builder
 	builderService *services.Builder
+	appImporter    *services.AppImporter
 
 
 	// Resource Handlers
 	// Resource Handlers
 	providerHandler *handlers.ProviderHandler
 	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=BIN
byop-engine


BIN=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,
 			Password: job.RegistryPassword,
 		}
 		}
 		normalizedRegURL := job.RegistryURL
 		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 := configfile.New("") // Create an empty config file object
 		specificAuthCfgFile.AuthConfigs[normalizedRegURL] = regAuthConfigValue
 		specificAuthCfgFile.AuthConfigs[normalizedRegURL] = regAuthConfigValue
@@ -190,7 +190,12 @@ func (bkc *BuildKitClient) CheckImageExists(ctx context.Context, fullImageURI st
 		if registryURL == "docker.io" || registryURL == "" {
 		if registryURL == "docker.io" || registryURL == "" {
 			serverAddress = "https://index.docker.io/v1/"
 			serverAddress = "https://index.docker.io/v1/"
 		} else if !strings.HasPrefix(registryURL, "http://") && !strings.HasPrefix(registryURL, "https://") {
 		} 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{
 		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
 // TestBuildingImageRemoteOnly focuses on remote git builds which work without special entitlements
 func TestBuildingImageRemoteOnly(t *testing.T) {
 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()
 	ctx := context.Background()
 
 
 	t.Log("Testing remote git builds (no local entitlements required)")
 	t.Log("Testing remote git builds (no local entitlements required)")
@@ -20,10 +20,10 @@ func TestBuildingImageRemoteOnly(t *testing.T) {
 		ID:           1,
 		ID:           1,
 		ComponentID:  1,
 		ComponentID:  1,
 		Version:      "master", // Use master branch, not main
 		Version:      "master", // Use master branch, not main
-		RegistryURL:  "localhost:5000",
+		RegistryURL:  "host.docker.internal:5000",
 		ImageName:    "hello-world-remote",
 		ImageName:    "hello-world-remote",
 		ImageTag:     "latest",
 		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",
 		SourceURL:    "https://github.com/crccheck/docker-hello-world.git",
 		BuildContext: ".",          // Root of repository
 		BuildContext: ".",          // Root of repository
 		Dockerfile:   "Dockerfile", // Relative to build context
 		Dockerfile:   "Dockerfile", // Relative to build context
@@ -60,10 +60,10 @@ func TestBuildingImageRemoteOnly(t *testing.T) {
 		ID:           2,
 		ID:           2,
 		ComponentID:  1,
 		ComponentID:  1,
 		Version:      "master",
 		Version:      "master",
-		RegistryURL:  "localhost:5000",
+		RegistryURL:  "host.docker.internal:5000",
 		ImageName:    "alpine-test",
 		ImageName:    "alpine-test",
 		ImageTag:     "latest",
 		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",
 		SourceURL:    "https://github.com/jdkelley/simple-http-server.git",
 		BuildContext: ".", // Root of repository
 		BuildContext: ".", // Root of repository
 		Dockerfile:   "Dockerfile",
 		Dockerfile:   "Dockerfile",
@@ -100,8 +100,8 @@ func TestBuildingImageRemoteOnly(t *testing.T) {
 
 
 // Test with a simple working repository that's guaranteed to have a Dockerfile
 // Test with a simple working repository that's guaranteed to have a Dockerfile
 func TestBuildingImageGuaranteedWorking(t *testing.T) {
 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()
 	ctx := context.Background()
 
 
 	// Use a simple Node.js app that definitely has a Dockerfile
 	// Use a simple Node.js app that definitely has a Dockerfile
@@ -109,10 +109,10 @@ func TestBuildingImageGuaranteedWorking(t *testing.T) {
 		ID:           3,
 		ID:           3,
 		ComponentID:  1,
 		ComponentID:  1,
 		Version:      "master",
 		Version:      "master",
-		RegistryURL:  "localhost:5000",
+		RegistryURL:  "host.docker.internal:5000",
 		ImageName:    "node-hello",
 		ImageName:    "node-hello",
 		ImageTag:     "latest",
 		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",
 		SourceURL:    "https://github.com/docker/welcome-to-docker.git",
 		BuildContext: ".",
 		BuildContext: ".",
 		Dockerfile:   "Dockerfile",
 		Dockerfile:   "Dockerfile",
@@ -162,8 +162,8 @@ func TestBuildingImageGuaranteedWorking(t *testing.T) {
 
 
 // Minimal test with a repository we know works
 // Minimal test with a repository we know works
 func TestMinimalWorkingBuild(t *testing.T) {
 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()
 	ctx := context.Background()
 
 
 	// Test with the original repository from your test but with master branch
 	// Test with the original repository from your test but with master branch
@@ -171,10 +171,10 @@ func TestMinimalWorkingBuild(t *testing.T) {
 		ID:           4,
 		ID:           4,
 		ComponentID:  1,
 		ComponentID:  1,
 		Version:      "master", // Correct branch
 		Version:      "master", // Correct branch
-		RegistryURL:  "localhost:5000",
+		RegistryURL:  "host.docker.internal:5000",
 		ImageName:    "simple-http",
 		ImageName:    "simple-http",
 		ImageTag:     "latest",
 		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",
 		SourceURL:    "https://github.com/Guy-Incognito/simple-http-server.git",
 		BuildContext: ".",
 		BuildContext: ".",
 		Dockerfile:   "Dockerfile",
 		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),
 			authprovider.NewDockerAuthProvider(authConfig),
 		}
 		}
 	}
 	}
-
 	ch := make(chan *client.SolveStatus)
 	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)
 		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")
 	db.entry.Info("DockerfileBuilder closed")
 	return nil
 	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 (
 import (
 	"context"
 	"context"
-	"database/sql"
-	"encoding/json"
 	"fmt"
 	"fmt"
-	"time"
 
 
 	"git.linuxforward.com/byop/byop-engine/models"
 	"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) {
 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
 	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
 	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 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
 	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 models.NewErrNotFound(fmt.Sprintf("app with ID %d not found for update", app.ID), nil)
 	}
 	}
-
 	return 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
 	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
 	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
 	return nil
 }
 }

+ 58 - 281
dbstore/build_jobs.go

@@ -2,330 +2,107 @@ package dbstore
 
 
 import (
 import (
 	"context"
 	"context"
-	"database/sql"
 	"fmt"
 	"fmt"
-	"time"
 
 
 	"git.linuxforward.com/byop/byop-engine/models"
 	"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 {
 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
 	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) {
 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 {
 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
 	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 {
 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
 	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 {
 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
 	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
 	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
 	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
 	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 (
 import (
 	"context"
 	"context"
-	"database/sql"
 	"fmt"
 	"fmt"
 
 
 	"git.linuxforward.com/byop/byop-engine/models"
 	"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
 	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
 	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 models.NewErrNotFound(fmt.Sprintf("client with ID %d not found for update", client.ID), nil)
 	}
 	}
 	return 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 models.NewErrNotFound(fmt.Sprintf("client with ID %d not found for deletion", id), nil)
 	}
 	}
 	return nil
 	return nil

+ 59 - 154
dbstore/components.go

@@ -2,199 +2,104 @@ package dbstore
 
 
 import (
 import (
 	"context"
 	"context"
-	"database/sql"
 	"fmt"
 	"fmt"
 
 
 	"git.linuxforward.com/byop/byop-engine/models"
 	"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
 	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
 	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 models.NewErrNotFound(fmt.Sprintf("component with ID %d not found for update", component.ID), nil)
 	}
 	}
 	return 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
 	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 models.NewErrNotFound(fmt.Sprintf("component with ID %d not found for image info update", componentID), nil)
 	}
 	}
-
 	return 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 models.NewErrNotFound(fmt.Sprintf("component with ID %d not found for deletion", id), nil)
 	}
 	}
-
 	return nil
 	return nil
 }
 }

+ 54 - 187
dbstore/deployments.go

@@ -2,230 +2,97 @@ package dbstore
 
 
 import (
 import (
 	"context"
 	"context"
-	"database/sql"
 	"fmt"
 	"fmt"
 
 
 	"git.linuxforward.com/byop/byop-engine/models"
 	"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
 	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
 	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
 	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
 	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 {
 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 models.NewErrNotFound(fmt.Sprintf("deployment with ID %d not found for update", deployment.ID), nil)
 	}
 	}
 	return 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 models.NewErrNotFound(fmt.Sprintf("deployment with ID %d not found for deletion", id), nil)
 	}
 	}
-
 	return 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
 	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
 	return deployments, nil
 }
 }

+ 93 - 314
dbstore/preview.go

@@ -2,373 +2,152 @@ package dbstore
 
 
 import (
 import (
 	"context"
 	"context"
-	"database/sql"
 	"fmt"
 	"fmt"
 
 
 	"git.linuxforward.com/byop/byop-engine/models"
 	"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
 	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
 	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) {
 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
 	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
 	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
 	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
 	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
 	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
 	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
 	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
 	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"
 	"os"
 
 
 	"git.linuxforward.com/byop/byop-engine/models"
 	"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.
 // Store defines the interface for all database operations.
 // This will include methods for all models (User, Client, App, Component, Deployment, Ticket, etc.)
 // This will include methods for all models (User, Client, App, Component, Deployment, Ticket, etc.)
 type Store interface {
 type Store interface {
 	// User methods
 	// 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)
 	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
 	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
 	// 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
 	// 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
 	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
 	// 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
 	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
 	// 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
 	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
 	// 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
 	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
 	// Ticket methods
 	CreateTicket(ctx context.Context, ticket *models.Ticket) error
 	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
 	UpdateTicket(ctx context.Context, ticket *models.Ticket) error
-	// DeleteTicket(ctx context.Context, id int) error // Optional
 
 
 	// TicketComment methods
 	// TicketComment methods
 	CreateTicketComment(ctx context.Context, comment *models.TicketComment) error
 	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
 	// BuildJob methods
 	CreateBuildJob(ctx context.Context, job *models.BuildJob) error
 	CreateBuildJob(ctx context.Context, job *models.BuildJob) error
@@ -92,21 +91,23 @@ type Store interface {
 	UpdateBuildJob(ctx context.Context, job *models.BuildJob) error
 	UpdateBuildJob(ctx context.Context, job *models.BuildJob) error
 	UpdateBuildJobStatus(ctx context.Context, id uint, status models.BuildStatus, errorMessage string) error
 	UpdateBuildJobStatus(ctx context.Context, id uint, status models.BuildStatus, errorMessage string) error
 	AppendBuildJobLog(ctx context.Context, id uint, logMessage 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
 	// General DB methods
 	GetDB() *sql.DB
 	GetDB() *sql.DB
+	GetGormDB() *gorm.DB
 	Close() error
 	Close() error
 }
 }
 
 
 // SQLiteStore implements the Store interface for SQLite using GORM
 // SQLiteStore implements the Store interface for SQLite using GORM
 type SQLiteStore struct {
 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) {
 func NewSQLiteStore(dataSourceName string) (*SQLiteStore, error) {
 	// First check if the database file exists
 	// First check if the database file exists
 	isNewDb := !fileExists(dataSourceName)
 	isNewDb := !fileExists(dataSourceName)
@@ -119,293 +120,54 @@ func NewSQLiteStore(dataSourceName string) (*SQLiteStore, error) {
 		defer file.Close()
 		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 {
 	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 {
 	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 {
 	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 {
 	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
 // fileExists checks if a file exists
@@ -414,8 +176,13 @@ func fileExists(filename string) bool {
 	return !os.IsNotExist(err)
 	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 {
 func (m *SQLiteStore) GetDB() *sql.DB {
+	return m.rawDB
+}
+
+// GetGormDB returns the GORM database instance
+func (m *SQLiteStore) GetGormDB() *gorm.DB {
 	return m.db
 	return m.db
 }
 }
 
 
@@ -427,7 +194,11 @@ func (m *SQLiteStore) Connect() error {
 
 
 // Disconnect closes the connection to the SQLite database
 // Disconnect closes the connection to the SQLite database
 func (m *SQLiteStore) Disconnect() error {
 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.
 // Close provides a more standard name for closing the database connection.

+ 35 - 83
dbstore/tickets.go

@@ -2,119 +2,71 @@ package dbstore
 
 
 import (
 import (
 	"context"
 	"context"
-	"database/sql"
 	"fmt"
 	"fmt"
 
 
 	"git.linuxforward.com/byop/byop-engine/models"
 	"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 {
 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
 	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
 	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 {
 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 models.NewErrNotFound(fmt.Sprintf("ticket with ID %d not found for update", ticket.ID), nil)
 	}
 	}
 	return 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 {
 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
 	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
 	return comments, nil
 }
 }

+ 62 - 108
dbstore/users.go

@@ -1,158 +1,112 @@
 package dbstore
 package dbstore
 
 
 import (
 import (
-	"context" // Added for context propagation
-	"database/sql"
-	"errors"
+	"context"
 	"fmt"
 	"fmt"
 
 
 	"git.linuxforward.com/byop/byop-engine/models"
 	"git.linuxforward.com/byop/byop-engine/models"
 	"golang.org/x/crypto/bcrypt"
 	"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
 	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) {
 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
 	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
 	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) {
 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 {
 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
 	return nil
 }
 }
 
 
 // CreateDefaultAdmin creates a default admin user if none exists
 // 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 {
 	if err == nil {
 		// Admin user already exists
 		// Admin user already exists
 		return nil
 		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
 		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
 	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
 	github.com/tonistiigi/fsutil v0.0.0-20250417144416-3f76f8130144
 	golang.org/x/crypto v0.37.0
 	golang.org/x/crypto v0.37.0
 	golang.org/x/sync v0.13.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 (
 require (
@@ -93,10 +94,9 @@ require (
 	golang.org/x/time v0.11.0 // indirect
 	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/api v0.0.0-20241021214115-324edc3d5d38 // indirect
 	google.golang.org/genproto/googleapis/rpc 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/ini.v1 v1.67.0 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // 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
 	gotest.tools/v3 v3.5.2 // indirect
 )
 )
 
 

+ 146 - 38
handlers/apps.go

@@ -2,7 +2,8 @@ package handlers
 
 
 import (
 import (
 	"context" // Ensure context is imported
 	"context" // Ensure context is imported
-	"errors"  // Added for errors.As
+	"encoding/json"
+	"errors" // Added for errors.As
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
 	"strconv"
 	"strconv"
@@ -19,14 +20,16 @@ type AppsHandler struct {
 	store          *dbstore.SQLiteStore
 	store          *dbstore.SQLiteStore
 	entry          *logrus.Entry
 	entry          *logrus.Entry
 	previewService services.PreviewService
 	previewService services.PreviewService
+	appImporter    *services.AppImporter
 }
 }
 
 
 // NewAppsHandler creates a new AppsHandler
 // 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{
 	return &AppsHandler{
 		store:          store,
 		store:          store,
 		entry:          logrus.WithField("component", "AppsHandler"),
 		entry:          logrus.WithField("component", "AppsHandler"),
 		previewService: previewService,
 		previewService: previewService,
+		appImporter:    appImporter,
 	}
 	}
 }
 }
 
 
@@ -95,7 +98,7 @@ func (h *AppsHandler) CreateApp(c *gin.Context) {
 	}
 	}
 
 
 	// Set the user ID on the app
 	// 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")
 	h.entry.WithField("app", app).Info("JSON binding successful, starting validation")
 
 
 	// Validate app configuration
 	// 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")
 	h.entry.WithField("app", app).Info("About to create app in database")
 
 
 	// Create the app
 	// Create the app
-	id, err := h.store.CreateApp(ctx, app) // Pass context
+	err := h.store.CreateApp(ctx, app) // Pass context
 	if err != nil {
 	if err != nil {
 		h.entry.WithField("error", err).Error("Failed to create app in database")
 		h.entry.WithField("error", err).Error("Failed to create app in database")
 		models.RespondWithError(c, err) // Pass db error directly
 		models.RespondWithError(c, err) // Pass db error directly
 		return
 		return
 	}
 	}
 
 
-	app.ID = id
-
 	// Automatically create preview - this happens async
 	// Automatically create preview - this happens async
 	h.entry.WithField("app_id", app.ID).Info("Starting automatic preview creation")
 	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)
 	c.JSON(http.StatusCreated, app)
 }
 }
@@ -139,14 +140,15 @@ func (h *AppsHandler) CreateApp(c *gin.Context) {
 func (h *AppsHandler) GetApp(c *gin.Context) {
 func (h *AppsHandler) GetApp(c *gin.Context) {
 	ctx := c.Request.Context() // Get context
 	ctx := c.Request.Context() // Get context
 	idStr := c.Param("id")
 	idStr := c.Param("id")
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	parsedID, err := strconv.ParseUint(idStr, 10, 32)
 	if err != nil {
 	if err != nil {
 		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
 		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
 		return
 		return
 	}
 	}
+	id := uint(parsedID)
 
 
 	// Get app directly from store
 	// 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 {
 	if err != nil {
 		models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
 		models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
 		return
 		return
@@ -164,10 +166,9 @@ func (h *AppsHandler) GetApp(c *gin.Context) {
 // Updated UpdateApp method
 // Updated UpdateApp method
 func (h *AppsHandler) UpdateApp(c *gin.Context) {
 func (h *AppsHandler) UpdateApp(c *gin.Context) {
 	ctx := c.Request.Context() // Get 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 {
 	if err != nil {
-		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
+		models.RespondWithError(c, err)
 		return
 		return
 	}
 	}
 
 
@@ -178,7 +179,7 @@ func (h *AppsHandler) UpdateApp(c *gin.Context) {
 	}
 	}
 
 
 	// Ensure the ID matches the URL parameter
 	// Ensure the ID matches the URL parameter
-	updatedApp.ID = int(id)
+	updatedApp.ID = id
 
 
 	// Validate app data
 	// Validate app data
 	if err := h.validateAppConfig(ctx, updatedApp.Components); err != nil { // Pass context
 	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.
 	// For consistency, we can rely on store.UpdateApp's error.
 
 
 	// Validate all components exist and are valid
 	// 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
 		component, err := h.store.GetComponentByID(ctx, componentID) // Pass context
 		if err != nil {
 		if err != nil {
 			models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
 			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
 // DeleteApp deletes an app
 func (h *AppsHandler) DeleteApp(c *gin.Context) {
 func (h *AppsHandler) DeleteApp(c *gin.Context) {
 	ctx := c.Request.Context() // Get 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 {
 	if err != nil {
-		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
+		models.RespondWithError(c, err)
 		return
 		return
 	}
 	}
 
 
 	// Call the store delete method
 	// 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)
 		models.RespondWithError(c, err) // Pass db error directly (handles NotFound, Conflict)
 		return
 		return
 	}
 	}
@@ -254,15 +262,14 @@ func (h *AppsHandler) DeleteApp(c *gin.Context) {
 // GetAppDeployments returns all deployments for an app
 // GetAppDeployments returns all deployments for an app
 func (h *AppsHandler) GetAppDeployments(c *gin.Context) {
 func (h *AppsHandler) GetAppDeployments(c *gin.Context) {
 	ctx := c.Request.Context() // Get 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 {
 	if err != nil {
-		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
+		models.RespondWithError(c, err)
 		return
 		return
 	}
 	}
 
 
 	// Check if app exists first
 	// 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 {
 	if err != nil {
 		models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
 		models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
 		return
 		return
@@ -273,7 +280,7 @@ func (h *AppsHandler) GetAppDeployments(c *gin.Context) {
 	}
 	}
 
 
 	// Get deployments for this app
 	// 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 {
 	if err != nil {
 		models.RespondWithError(c, err) // Pass db error directly
 		models.RespondWithError(c, err) // Pass db error directly
 		return
 		return
@@ -314,7 +321,17 @@ func (h *AppsHandler) GetAppByVersion(c *gin.Context) {
 }
 }
 
 
 // validateAppConfig checks if the app configuration is valid
 // 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 {
 	if len(componentIds) == 0 {
 		return models.NewErrValidation("App must have at least one component", nil, nil) // Return custom error
 		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
 // 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
 	preview, err := h.previewService.CreatePreview(ctx, appId) // Pass context
 	if err != nil {
 	if err != nil {
 		h.entry.WithField("app_id", appId).Errorf("Failed to create preview: %v", err)
 		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
 // 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
 	previews, err := h.store.GetPreviewsByAppID(ctx, appID) // Pass context
 	if err != nil {
 	if err != nil {
 		h.entry.WithField("app_id", appID).Errorf("Failed to get existing previews: %v", err)
 		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
 // CreateAppPreview creates a new preview for an app
 func (h *AppsHandler) CreateAppPreview(c *gin.Context) {
 func (h *AppsHandler) CreateAppPreview(c *gin.Context) {
 	ctx := c.Request.Context() // Get 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 {
 	if err != nil {
-		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
+		models.RespondWithError(c, err)
 		return
 		return
 	}
 	}
 
 
-	app, err := h.store.GetAppByID(ctx, int(id)) // Pass context
+	app, err := h.store.GetAppByID(ctx, id) // Pass context
 	if err != nil {
 	if err != nil {
 		models.RespondWithError(c, err)
 		models.RespondWithError(c, err)
 		return
 		return
@@ -424,14 +440,13 @@ func (h *AppsHandler) CreateAppPreview(c *gin.Context) {
 // GetAppPreview returns the preview for a specific app
 // GetAppPreview returns the preview for a specific app
 func (h *AppsHandler) GetAppPreview(c *gin.Context) {
 func (h *AppsHandler) GetAppPreview(c *gin.Context) {
 	ctx := c.Request.Context() // Get 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 {
 	if err != nil {
-		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
+		models.RespondWithError(c, err)
 		return
 		return
 	}
 	}
 
 
-	app, err := h.store.GetAppByID(ctx, int(id)) // Pass context
+	app, err := h.store.GetAppByID(ctx, id) // Pass context
 	if err != nil {
 	if err != nil {
 		models.RespondWithError(c, err)
 		models.RespondWithError(c, err)
 		return
 		return
@@ -459,14 +474,13 @@ func (h *AppsHandler) GetAppPreview(c *gin.Context) {
 // DeleteAppPreview deletes the preview for a specific app
 // DeleteAppPreview deletes the preview for a specific app
 func (h *AppsHandler) DeleteAppPreview(c *gin.Context) {
 func (h *AppsHandler) DeleteAppPreview(c *gin.Context) {
 	ctx := c.Request.Context() // Get 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 {
 	if err != nil {
-		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
+		models.RespondWithError(c, err)
 		return
 		return
 	}
 	}
 
 
-	app, err := h.store.GetAppByID(ctx, int(id)) // Pass context
+	app, err := h.store.GetAppByID(ctx, id) // Pass context
 	if err != nil {
 	if err != nil {
 		models.RespondWithError(c, err)
 		models.RespondWithError(c, err)
 		return
 		return
@@ -484,3 +498,97 @@ func (h *AppsHandler) DeleteAppPreview(c *gin.Context) {
 
 
 	c.JSON(http.StatusOK, gin.H{"message": "Preview deleted successfully"})
 	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 (
 import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
-	"strconv"
 
 
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/models"
 	"git.linuxforward.com/byop/byop-engine/models"
@@ -63,32 +62,27 @@ func (h *ClientHandler) CreateClient(c *gin.Context) {
 		return
 		return
 	}
 	}
 
 
-	id, err := h.store.CreateClient(ctx, client)
+	err := h.store.CreateClient(ctx, &client)
 	if err != nil {
 	if err != nil {
 		appErr := models.NewErrInternalServer("failed_create_client", fmt.Errorf("Failed to create client: %w", err))
 		appErr := models.NewErrInternalServer("failed_create_client", fmt.Errorf("Failed to create client: %w", err))
 		models.RespondWithError(c, appErr)
 		models.RespondWithError(c, appErr)
 		return
 		return
 	}
 	}
 
 
-	// Set the generated ID
-	client.ID = id
-
 	c.JSON(http.StatusCreated, client)
 	c.JSON(http.StatusCreated, client)
 }
 }
 
 
 // GetClient returns a specific client
 // GetClient returns a specific client
 func (h *ClientHandler) GetClient(c *gin.Context) {
 func (h *ClientHandler) GetClient(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
 
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
-	client, err := h.store.GetClientByID(ctx, int(id))
+	client, err := h.store.GetClientByID(ctx, id)
 	if err != nil {
 	if err != nil {
 		models.RespondWithError(c, err)
 		models.RespondWithError(c, err)
 		return
 		return
@@ -105,13 +99,11 @@ func (h *ClientHandler) GetClient(c *gin.Context) {
 
 
 // UpdateClient updates a client
 // UpdateClient updates a client
 func (h *ClientHandler) UpdateClient(c *gin.Context) {
 func (h *ClientHandler) UpdateClient(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
 
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
@@ -122,7 +114,7 @@ func (h *ClientHandler) UpdateClient(c *gin.Context) {
 		return
 		return
 	}
 	}
 
 
-	updatedClient.ID = int(id)
+	updatedClient.ID = id
 
 
 	// Validate client data
 	// Validate client data
 	if updatedClient.Name == "" {
 	if updatedClient.Name == "" {
@@ -132,7 +124,7 @@ func (h *ClientHandler) UpdateClient(c *gin.Context) {
 		return
 		return
 	}
 	}
 
 
-	if err := h.store.UpdateClient(ctx, updatedClient); err != nil {
+	if err := h.store.UpdateClient(ctx, &updatedClient); err != nil {
 		models.RespondWithError(c, err)
 		models.RespondWithError(c, err)
 		return
 		return
 	}
 	}
@@ -142,17 +134,15 @@ func (h *ClientHandler) UpdateClient(c *gin.Context) {
 
 
 // DeleteClient deletes a client
 // DeleteClient deletes a client
 func (h *ClientHandler) DeleteClient(c *gin.Context) {
 func (h *ClientHandler) DeleteClient(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
 
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
-	if err := h.store.DeleteClient(ctx, int(id)); err != nil {
+	if err := h.store.DeleteClient(ctx, id); err != nil {
 		models.RespondWithError(c, err)
 		models.RespondWithError(c, err)
 		return
 		return
 	}
 	}
@@ -162,17 +152,15 @@ func (h *ClientHandler) DeleteClient(c *gin.Context) {
 
 
 // GetClientDeployments returns all deployments for a client
 // GetClientDeployments returns all deployments for a client
 func (h *ClientHandler) GetClientDeployments(c *gin.Context) {
 func (h *ClientHandler) GetClientDeployments(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
 
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
-	client, err := h.store.GetClientByID(ctx, int(id))
+	client, err := h.store.GetClientByID(ctx, id)
 	if err != nil {
 	if err != nil {
 		models.RespondWithError(c, err)
 		models.RespondWithError(c, err)
 		return
 		return

+ 24 - 31
handlers/components.go

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

+ 26 - 42
handlers/deployments.go

@@ -3,7 +3,6 @@ package handlers
 import (
 import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
-	"strconv"
 
 
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/models"
 	"git.linuxforward.com/byop/byop-engine/models"
@@ -63,7 +62,7 @@ func (h *DeploymentHandler) CreateDeployment(c *gin.Context) {
 
 
 	// Basic validation
 	// Basic validation
 	validationErrors := make(map[string]string)
 	validationErrors := make(map[string]string)
-	if deployment.AppId == 0 {
+	if deployment.AppID == 0 {
 		validationErrors["app_id"] = "App ID is required"
 		validationErrors["app_id"] = "App ID is required"
 	}
 	}
 	if deployment.Environment == "" {
 	if deployment.Environment == "" {
@@ -81,32 +80,29 @@ func (h *DeploymentHandler) CreateDeployment(c *gin.Context) {
 	}
 	}
 
 
 	// TODO: Add complex deployment logic with cloud providers
 	// TODO: Add complex deployment logic with cloud providers
-	id, err := h.store.CreateDeployment(ctx, deployment)
+	err := h.store.CreateDeployment(ctx, &deployment)
 	if err != nil {
 	if err != nil {
 		appErr := models.NewErrInternalServer("failed_create_deployment", fmt.Errorf("Failed to create deployment: %w", err))
 		appErr := models.NewErrInternalServer("failed_create_deployment", fmt.Errorf("Failed to create deployment: %w", err))
 		models.RespondWithError(c, appErr)
 		models.RespondWithError(c, appErr)
 		return
 		return
 	}
 	}
 
 
-	// Set the generated ID
-	deployment.ID = id
+	// GORM automatically sets the ID after creation
 
 
 	c.JSON(http.StatusCreated, deployment)
 	c.JSON(http.StatusCreated, deployment)
 }
 }
 
 
 // GetDeployment returns a specific deployment
 // GetDeployment returns a specific deployment
 func (h *DeploymentHandler) GetDeployment(c *gin.Context) {
 func (h *DeploymentHandler) GetDeployment(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
 
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
-	deployment, err := h.store.GetDeploymentByID(ctx, int(id))
+	deployment, err := h.store.GetDeploymentByID(ctx, id)
 	if err != nil {
 	if err != nil {
 		models.RespondWithError(c, err)
 		models.RespondWithError(c, err)
 		return
 		return
@@ -117,13 +113,11 @@ func (h *DeploymentHandler) GetDeployment(c *gin.Context) {
 
 
 // UpdateDeployment updates a deployment
 // UpdateDeployment updates a deployment
 func (h *DeploymentHandler) UpdateDeployment(c *gin.Context) {
 func (h *DeploymentHandler) UpdateDeployment(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
 
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
@@ -135,11 +129,11 @@ func (h *DeploymentHandler) UpdateDeployment(c *gin.Context) {
 	}
 	}
 
 
 	// Ensure the ID matches the URL parameter
 	// Ensure the ID matches the URL parameter
-	updatedDeployment.ID = int(id)
+	updatedDeployment.ID = id
 
 
 	// Basic validation for update
 	// Basic validation for update
 	validationErrors := make(map[string]string)
 	validationErrors := make(map[string]string)
-	if updatedDeployment.AppId == 0 {
+	if updatedDeployment.AppID == 0 {
 		validationErrors["app_id"] = "App ID is required"
 		validationErrors["app_id"] = "App ID is required"
 	}
 	}
 	if updatedDeployment.Environment == "" {
 	if updatedDeployment.Environment == "" {
@@ -161,17 +155,15 @@ func (h *DeploymentHandler) UpdateDeployment(c *gin.Context) {
 
 
 // DeleteDeployment deletes a deployment
 // DeleteDeployment deletes a deployment
 func (h *DeploymentHandler) DeleteDeployment(c *gin.Context) {
 func (h *DeploymentHandler) DeleteDeployment(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
 
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
-	if err := h.store.DeleteDeployment(ctx, int(id)); err != nil {
+	if err := h.store.DeleteDeployment(ctx, id); err != nil {
 		models.RespondWithError(c, err)
 		models.RespondWithError(c, err)
 		return
 		return
 	}
 	}
@@ -181,13 +173,11 @@ func (h *DeploymentHandler) DeleteDeployment(c *gin.Context) {
 
 
 // UpdateDeploymentStatus updates the status of a deployment
 // UpdateDeploymentStatus updates the status of a deployment
 func (h *DeploymentHandler) UpdateDeploymentStatus(c *gin.Context) {
 func (h *DeploymentHandler) UpdateDeploymentStatus(c *gin.Context) {
-	idStr := c.Param("id")
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
 
 
-	id, err := strconv.ParseInt(idStr, 10, 64)
+	id, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
@@ -202,7 +192,7 @@ func (h *DeploymentHandler) UpdateDeploymentStatus(c *gin.Context) {
 	}
 	}
 
 
 	// Get current deployment
 	// Get current deployment
-	deployment, err := h.store.GetDeploymentByID(ctx, int(id))
+	deployment, err := h.store.GetDeploymentByID(ctx, id)
 	if err != nil {
 	if err != nil {
 		models.RespondWithError(c, err)
 		models.RespondWithError(c, err)
 		return
 		return
@@ -223,17 +213,15 @@ func (h *DeploymentHandler) UpdateDeploymentStatus(c *gin.Context) {
 
 
 // GetDeploymentsByClient returns all deployments for a specific client
 // GetDeploymentsByClient returns all deployments for a specific client
 func (h *DeploymentHandler) GetDeploymentsByClient(c *gin.Context) {
 func (h *DeploymentHandler) GetDeploymentsByClient(c *gin.Context) {
-	clientIDStr := c.Param("clientId")
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
 
 
-	clientID, err := strconv.ParseInt(clientIDStr, 10, 64)
+	clientID, err := parseUintID(c, "clientId")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
-	deployments, err := h.store.GetDeploymentsByClientID(ctx, int(clientID))
+	deployments, err := h.store.GetDeploymentsByClientID(ctx, clientID)
 	if err != nil {
 	if err != nil {
 		if _, ok := err.(models.CustomError); !ok {
 		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))
 			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
 // GetDeploymentsByApp returns all deployments for a specific app
 func (h *DeploymentHandler) GetDeploymentsByApp(c *gin.Context) {
 func (h *DeploymentHandler) GetDeploymentsByApp(c *gin.Context) {
-	appIDStr := c.Param("appId")
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
 
 
-	appID, err := strconv.ParseInt(appIDStr, 10, 64)
+	appID, err := parseUintID(c, "appId")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
-	deployments, err := h.store.GetDeploymentsByAppID(ctx, int(appID))
+	deployments, err := h.store.GetDeploymentsByAppID(ctx, appID)
 	if err != nil {
 	if err != nil {
 		if _, ok := err.(models.CustomError); !ok {
 		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))
 			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)
 	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) {
 func (h *DeploymentHandler) GetDeploymentsByUser(c *gin.Context) {
-	userIDStr := c.Param("userId")
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
 
 
-	userID, err := strconv.ParseInt(userIDStr, 10, 64)
+	userID, err := parseUintID(c, "userId")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
-	deployments, err := h.store.GetDeploymentsByUserID(ctx, int(userID))
+	deployments, err := h.store.GetDeploymentsByUserID(ctx, userID)
 	if err != nil {
 	if err != nil {
 		if _, ok := err.(models.CustomError); !ok {
 		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))
 			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 (
 import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
-	"strconv"
 
 
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/models"
 	"git.linuxforward.com/byop/byop-engine/models"
@@ -28,7 +27,7 @@ func (h *PreviewHandler) CreatePreview(c *gin.Context) {
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
 
 
 	var requestBody struct {
 	var requestBody struct {
-		AppId int `json:"app_id" binding:"required"`
+		AppId uint `json:"app_id" binding:"required"`
 	}
 	}
 
 
 	if err := c.ShouldBindJSON(&requestBody); err != nil {
 	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.
 // GetPreview retrieves details of a specific preview session.
 func (h *PreviewHandler) GetPreview(c *gin.Context) {
 func (h *PreviewHandler) GetPreview(c *gin.Context) {
 	ctx := c.Request.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 {
 	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
 		return
 	}
 	}
 
 
-	preview, err := h.store.GetPreviewByID(ctx, previewIdInt)
+	preview, err := h.store.GetPreviewByID(ctx, previewId)
 	if err != nil {
 	if err != nil {
 		models.RespondWithError(c, err)
 		models.RespondWithError(c, err)
 		return
 		return

+ 26 - 32
handlers/tickets.go

@@ -3,7 +3,6 @@ package handlers
 import (
 import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
-	"strconv"
 	"time"
 	"time"
 
 
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/dbstore"
@@ -47,7 +46,7 @@ func (h *TicketHandler) ListTickets(c *gin.Context) {
 		return
 		return
 	}
 	}
 	if tickets == nil {
 	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)
 	c.JSON(http.StatusOK, tickets)
 }
 }
@@ -56,8 +55,8 @@ func (h *TicketHandler) ListTickets(c *gin.Context) {
 type CreateTicketInput struct {
 type CreateTicketInput struct {
 	Title       string `json:"title" validate:"required,min=3,max=255"`
 	Title       string `json:"title" validate:"required,min=3,max=255"`
 	Description string `json:"description" validate:"required,min=10"`
 	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"`
 	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
 // GetTicket returns a specific ticket
 func (h *TicketHandler) GetTicket(c *gin.Context) {
 func (h *TicketHandler) GetTicket(c *gin.Context) {
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	id, err := strconv.ParseInt(idStr, 10, 64)
+
+	id, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
-	ticket, err := h.Store.GetTicketByID(ctx, int(id))
+	ticket, err := h.Store.GetTicketByID(ctx, id)
 	if err != nil {
 	if err != nil {
 		if models.IsErrNotFound(err) {
 		if models.IsErrNotFound(err) {
 			appErr := models.NewErrNotFound("ticket_not_found", fmt.Errorf("Ticket with ID %d not found: %w", id, 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"`
 	Description *string `json:"description,omitempty" validate:"omitempty,min=10"`
 	Priority    *string `json:"priority,omitempty" validate:"omitempty,oneof=low medium high critical"`
 	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"`
 	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
 // UpdateTicket updates a ticket
 func (h *TicketHandler) UpdateTicket(c *gin.Context) {
 func (h *TicketHandler) UpdateTicket(c *gin.Context) {
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	id, err := strconv.ParseInt(idStr, 10, 64)
+
+	id, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
@@ -163,7 +160,7 @@ func (h *TicketHandler) UpdateTicket(c *gin.Context) {
 		return
 		return
 	}
 	}
 
 
-	ticket, err := h.Store.GetTicketByID(ctx, int(id))
+	ticket, err := h.Store.GetTicketByID(ctx, id)
 	if err != nil {
 	if err != nil {
 		if models.IsErrNotFound(err) {
 		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))
 			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
 // GetTicketComments returns comments for a ticket
 func (h *TicketHandler) GetTicketComments(c *gin.Context) {
 func (h *TicketHandler) GetTicketComments(c *gin.Context) {
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	ticketID, err := strconv.ParseInt(idStr, 10, 64)
+
+	ticketID, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
-	comments, err := h.Store.GetTicketComments(ctx, int(ticketID))
+	comments, err := h.Store.GetTicketComments(ctx, ticketID)
 	if err != nil {
 	if err != nil {
 		// If the error indicates the ticket itself was not found, that's a 404 for the ticket.
 		// 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.
 		// Otherwise, it's an internal error fetching comments.
@@ -240,7 +236,7 @@ func (h *TicketHandler) GetTicketComments(c *gin.Context) {
 		return
 		return
 	}
 	}
 	if comments == nil {
 	if comments == nil {
-		comments = []models.TicketComment{} // Return empty slice
+		comments = []*models.TicketComment{} // Return empty slice
 	}
 	}
 
 
 	c.JSON(http.StatusOK, comments)
 	c.JSON(http.StatusOK, comments)
@@ -255,11 +251,10 @@ type AddTicketCommentInput struct {
 // AddTicketComment adds a comment to a ticket
 // AddTicketComment adds a comment to a ticket
 func (h *TicketHandler) AddTicketComment(c *gin.Context) {
 func (h *TicketHandler) AddTicketComment(c *gin.Context) {
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	ticketID, err := strconv.ParseInt(idStr, 10, 64)
+
+	ticketID, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
@@ -278,7 +273,7 @@ func (h *TicketHandler) AddTicketComment(c *gin.Context) {
 	}
 	}
 
 
 	// Get authenticated user ID (placeholder - replace with actual auth logic)
 	// 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 {
 	// if !ok || authUserID == 0 {
 	// 	appErr := models.NewErrUnauthorized("user_not_authenticated_for_comment", fmt.Errorf("User must be authenticated to comment"))
 	// 	appErr := models.NewErrUnauthorized("user_not_authenticated_for_comment", fmt.Errorf("User must be authenticated to comment"))
 	// 	models.RespondWithError(c, appErr)
 	// 	models.RespondWithError(c, appErr)
@@ -286,7 +281,7 @@ func (h *TicketHandler) AddTicketComment(c *gin.Context) {
 	// }
 	// }
 
 
 	comment := models.TicketComment{
 	comment := models.TicketComment{
-		TicketID: int(ticketID),
+		TicketID: ticketID,
 		UserID:   authUserID, // Set from authenticated user
 		UserID:   authUserID, // Set from authenticated user
 		Content:  input.Content,
 		Content:  input.Content,
 	}
 	}
@@ -309,15 +304,14 @@ func (h *TicketHandler) AddTicketComment(c *gin.Context) {
 // ResolveTicket resolves a ticket
 // ResolveTicket resolves a ticket
 func (h *TicketHandler) ResolveTicket(c *gin.Context) {
 func (h *TicketHandler) ResolveTicket(c *gin.Context) {
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	id, err := strconv.ParseInt(idStr, 10, 64)
+
+	id, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
-	ticket, err := h.Store.GetTicketByID(ctx, int(id))
+	ticket, err := h.Store.GetTicketByID(ctx, id)
 	if err != nil {
 	if err != nil {
 		if models.IsErrNotFound(err) {
 		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))
 			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 (
 import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
-	"strconv"
 
 
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/models"
 	"git.linuxforward.com/byop/byop-engine/models"
@@ -88,7 +87,7 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
 		Active:   userActive,
 		Active:   userActive,
 	}
 	}
 
 
-	id, err := h.Store.CreateUser(ctx, user)
+	err = h.Store.CreateUser(ctx, &user)
 	if err != nil {
 	if err != nil {
 		if models.IsErrConflict(err) {
 		if models.IsErrConflict(err) {
 			models.RespondWithError(c, err)
 			models.RespondWithError(c, err)
@@ -99,7 +98,7 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
 		return
 		return
 	}
 	}
 
 
-	user.ID = id
+	// GORM automatically sets the ID after creation
 	// Clear the password before sending the response
 	// Clear the password before sending the response
 	createdUser := user
 	createdUser := user
 	createdUser.Password = ""
 	createdUser.Password = ""
@@ -110,11 +109,10 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
 // GetUser retrieves a user by ID
 // GetUser retrieves a user by ID
 func (h *UserHandler) GetUser(c *gin.Context) {
 func (h *UserHandler) GetUser(c *gin.Context) {
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	id, err := strconv.Atoi(idStr)
+
+	id, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
@@ -140,11 +138,10 @@ type UpdateUserInput struct {
 // UpdateUser updates an existing user
 // UpdateUser updates an existing user
 func (h *UserHandler) UpdateUser(c *gin.Context) {
 func (h *UserHandler) UpdateUser(c *gin.Context) {
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	id, err := strconv.Atoi(idStr)
+
+	id, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
@@ -214,11 +211,10 @@ func (h *UserHandler) UpdateUser(c *gin.Context) {
 // DeleteUser deletes a user by ID
 // DeleteUser deletes a user by ID
 func (h *UserHandler) DeleteUser(c *gin.Context) {
 func (h *UserHandler) DeleteUser(c *gin.Context) {
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	id, err := strconv.Atoi(idStr)
+
+	id, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 
@@ -251,11 +247,10 @@ func (h *UserHandler) ListUsers(c *gin.Context) {
 // GetUserDeployments returns all deployments for a specific user
 // GetUserDeployments returns all deployments for a specific user
 func (h *UserHandler) GetUserDeployments(c *gin.Context) {
 func (h *UserHandler) GetUserDeployments(c *gin.Context) {
 	ctx := c.Request.Context()
 	ctx := c.Request.Context()
-	idStr := c.Param("id")
-	userID, err := strconv.Atoi(idStr)
+
+	userID, err := parseUintID(c, "id")
 	if err != nil {
 	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
 		return
 	}
 	}
 
 

+ 15 - 12
models/build.go

@@ -17,18 +17,20 @@ const (
 
 
 // BuildRequest represents the information needed to initiate a build.
 // BuildRequest represents the information needed to initiate a build.
 type BuildRequest struct {
 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.
 // 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
 	RegistryPassword  string      `json:"registry_password,omitempty"` // Consider how to store this securely if at all long-term
 	BuildContext      string      `json:"build_context"`
 	BuildContext      string      `json:"build_context"`
 	Dockerfile        string      `json:"dockerfile"`
 	Dockerfile        string      `json:"dockerfile"`
+	Source            string      `json:"source,omitempty"` // "autodetect", "dockerfile", "docker-compose"
 	NoCache           bool        `json:"no_cache"`
 	NoCache           bool        `json:"no_cache"`
 	BuildArgs         string      `json:"build_args" gorm:"type:text"`                   // Stored as JSON string or similar
 	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
 	DockerfileContent string      `json:"dockerfile_content,omitempty" gorm:"type:text"` // Generated Dockerfile content

+ 95 - 90
models/common.go

@@ -1,6 +1,8 @@
 package models
 package models
 
 
-import "time"
+import (
+	"time"
+)
 
 
 // APIResponse represents a standard API response
 // APIResponse represents a standard API response
 type APIResponse struct {
 type APIResponse struct {
@@ -30,65 +32,68 @@ const (
 )
 )
 
 
 type App struct {
 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
 // Component represents a deployable component
 type Component struct {
 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"`
 	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
 // Deployment represents a deployment instance
 type Deployment struct {
 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
 // Provider represents a cloud provider
 type Provider struct {
 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.
 // TicketStatus defines the possible statuses for a ticket.
@@ -109,27 +114,27 @@ const (
 
 
 // Ticket represents a support ticket
 // Ticket represents a support ticket
 type Ticket struct {
 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
 	ResolvedAt  *time.Time `json:"resolved_at,omitempty"` // When the ticket was resolved
 }
 }
 
 
 // TicketComment represents a comment on a support ticket
 // TicketComment represents a comment on a support ticket
 type TicketComment struct {
 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
 // Role constants for User
@@ -141,25 +146,25 @@ const (
 
 
 // User represents a user in the system
 // User represents a user in the system
 type User struct {
 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
 // Client represents a client in the system
 type Client struct {
 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.
 // PreviewStatus defines the possible statuses for a preview.
@@ -172,16 +177,16 @@ const (
 )
 )
 
 
 type Preview struct {
 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"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"os" // Added import
 	"os" // Added import
+	"path/filepath"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
 	"git.linuxforward.com/byop/byop-engine/clients"
 	"git.linuxforward.com/byop/byop-engine/clients"
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/dbstore"
 	"git.linuxforward.com/byop/byop-engine/models"
 	"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"
 	"github.com/sirupsen/logrus"
 )
 )
 
 
@@ -112,6 +115,7 @@ func (s *Builder) QueueBuildJob(ctx context.Context, req models.BuildRequest) (*
 		RegistryPassword:  req.RegistryPassword, // Added
 		RegistryPassword:  req.RegistryPassword, // Added
 		BuildContext:      buildContext,
 		BuildContext:      buildContext,
 		Dockerfile:        dockerfilePath,
 		Dockerfile:        dockerfilePath,
+		Source:            req.Source,            // Added
 		DockerfileContent: req.DockerfileContent, // NEW: Generated Dockerfile content
 		DockerfileContent: req.DockerfileContent, // NEW: Generated Dockerfile content
 		NoCache:           req.NoCache,
 		NoCache:           req.NoCache,
 		BuildArgs:         buildArgsJSON,
 		BuildArgs:         buildArgsJSON,
@@ -162,19 +166,45 @@ func (s *Builder) processJob(ctx context.Context, jobID uint) {
 		return
 		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() {
 		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 {
 			} 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
 	// Debug: Check if DockerfileContent was retrieved from database
 	if job.DockerfileContent != "" {
 	if job.DockerfileContent != "" {
@@ -201,36 +231,79 @@ func (s *Builder) processJob(ctx context.Context, jobID uint) {
 		return
 		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 != "" {
 	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
 			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
 	// Finalize job with success status
 	s.finalizeJob(ctx, job.ID, job.ComponentID, models.BuildStatusSuccess, "")
 	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)
 	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.
 // 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) {
 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 {
 	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"
 		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)
 		s.entry.Errorf("Error updating component status for job ID %d: %v", jobID, updateErr)
 	} else {
 	} else {
 		s.entry.Infof("Updated component status for job ID %d to %s.", jobID, status)
 		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)
 			s.entry.Errorf("Error retrieving build job ID %d to update component image info: %v", jobID, err)
 		} else {
 		} else {
 			// Update component with the built image information
 			// 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)
 				s.entry.Errorf("Error updating component image info for component ID %d after successful build: %v", componentId, err)
 			} else {
 			} else {
 				s.entry.Infof("Successfully updated component ID %d with image tag %s and URI %s", componentId, job.ImageTag, job.FullImageURI)
 				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"
 		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)
 		s.entry.Errorf("Error updating component status for job ID %d: %v", jobID, updateErr)
 	} else {
 	} else {
 		s.entry.Infof("Updated component status for job ID %d to %s.", jobID, status)
 		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
 	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")
 	entry.Warn("LocalPreviewService initialized - this is for development/testing only, not for production use")
 
 
 	return &LocalPreviewService{
 	return &LocalPreviewService{
-		common: NewPreviewCommon(store, registryClient, registryURL, registryUser, registryPass),
+		common: NewPreviewCommon(store, registryURL, registryUser, registryPass),
 		entry:  entry,
 		entry:  entry,
 		config: cfg,
 		config: cfg,
 	}
 	}
@@ -57,7 +57,7 @@ func (lps *LocalPreviewService) Close(ctx context.Context) {
 }
 }
 
 
 // CreatePreview creates a local preview environment
 // 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
 	// Get app details
 	app, err := lps.common.GetStore().GetAppByID(ctx, appId)
 	app, err := lps.common.GetStore().GetAppByID(ctx, appId)
 	if err != nil {
 	if err != nil {
@@ -74,10 +74,10 @@ func (lps *LocalPreviewService) CreatePreview(ctx context.Context, appId int) (*
 	preview := models.Preview{
 	preview := models.Preview{
 		AppID:     app.ID,
 		AppID:     app.ID,
 		Status:    models.PreviewStatusBuilding,
 		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 err != nil {
 		if _, ok := err.(models.CustomError); !ok {
 		if _, ok := err.(models.CustomError); !ok {
 			return nil, models.NewErrInternalServer("failed to create preview record in db", err)
 			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
 		return nil, err
 	}
 	}
 
 
-	preview.ID = previewID
-
 	// Start async build and deploy locally
 	// Start async build and deploy locally
 	go lps.buildAndDeployPreview(context.Background(), preview, app)
 	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) {
 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")
 	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"
 	compose := "services:\n"
 
 
 	for i, imageName := range imageNames {
 	for i, imageName := range imageNames {
@@ -293,7 +372,7 @@ func (lps *LocalPreviewService) generatePreviewDockerCompose(ctx context.Context
 }
 }
 
 
 // DeletePreview deletes a local preview
 // 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)
 	preview, err := lps.common.GetStore().GetPreviewByAppID(ctx, appID)
 	if err != nil {
 	if err != nil {
 		if models.IsErrNotFound(err) {
 		if models.IsErrNotFound(err) {
@@ -322,7 +401,7 @@ func (lps *LocalPreviewService) DeletePreview(ctx context.Context, appID int) er
 }
 }
 
 
 // StopPreview stops a local preview
 // 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)
 	preview, err := lps.common.GetStore().GetPreviewByID(ctx, previewID)
 	if err != nil {
 	if err != nil {
 		if models.IsErrNotFound(err) {
 		if models.IsErrNotFound(err) {

+ 416 - 534
services/preview_common.go

@@ -5,7 +5,6 @@ import (
 	"bytes"
 	"bytes"
 	"context"
 	"context"
 	"crypto/rand"
 	"crypto/rand"
-	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
@@ -15,77 +14,44 @@ import (
 	"strings"
 	"strings"
 	"time"
 	"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/dbstore"
 	"git.linuxforward.com/byop/byop-engine/models"
 	"git.linuxforward.com/byop/byop-engine/models"
 	"github.com/sirupsen/logrus"
 	"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
 // PreviewService defines the interface for preview services
 type PreviewService interface {
 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
 // PreviewCommon contains shared functionality for preview services
 type PreviewCommon struct {
 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
 // 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{
 	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() {
 func (pc *PreviewCommon) Close() {
-	// Clean up BYOP preview images
-	pc.CleanupPreviewImages(context.Background())
-
 	// Clean up preview database state
 	// Clean up preview database state
 	pc.CleanupPreviewState(context.Background())
 	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
 // 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
 // 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
 	var buf bytes.Buffer
 	tw := tar.NewWriter(&buf)
 	tw := tar.NewWriter(&buf)
 	defer tw.Close()
 	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
 	// Common ignore patterns for Git repositories
 	ignorePatterns := []string{
 	ignorePatterns := []string{
 		".git",
 		".git",
@@ -192,13 +179,13 @@ func (pc *PreviewCommon) CreateBuildContext(ctx context.Context, contextDir stri
 		"documentation",
 		"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 {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
 		// Get relative path
 		// Get relative path
-		relPath, err := filepath.Rel(contextDir, file)
+		relPath, err := filepath.Rel(effectiveContextDir, file)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -279,208 +266,59 @@ func (pc *PreviewCommon) CreateBuildContext(ctx context.Context, contextDir stri
 	return io.NopCloser(&buf), nil
 	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) {
 func (pc *PreviewCommon) BuildComponentImages(ctx context.Context, components []models.Component) ([]string, string, error) {
 	var imageNames []string
 	var imageNames []string
 	var allLogs strings.Builder
 	var allLogs strings.Builder
 
 
 	for _, component := range components {
 	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
 		// Generate local image name for preview
 		imageName := fmt.Sprintf("byop-preview-%s:%d", component.Name, component.ID)
 		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 {
 		} 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")
 		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))
 		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
 			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 {
 		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))
 			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)
 			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
 	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
 // GetAppComponents retrieves components for an app
 func (pc *PreviewCommon) GetAppComponents(ctx context.Context, app *models.App) ([]models.Component, error) {
 func (pc *PreviewCommon) GetAppComponents(ctx context.Context, app *models.App) ([]models.Component, error) {
 	var components []models.Component
 	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)
 		component, err := pc.store.GetComponentByID(ctx, componentID)
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
@@ -671,39 +423,34 @@ func (pc *PreviewCommon) GetAppComponents(ctx context.Context, app *models.App)
 	return components, nil
 	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) {
 func (pc *PreviewCommon) CleanupPreviewImages(ctx context.Context) {
 	pc.entry.Info("Cleaning up BYOP preview images...")
 	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 {
 	if err != nil {
 		pc.entry.WithError(err).Error("Failed to list images for cleanup")
 		pc.entry.WithError(err).Error("Failed to list images for cleanup")
 		return
 		return
 	}
 	}
 
 
 	removedCount := 0
 	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
 			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 {
 	if err != nil {
 		pc.entry.WithError(err).Error("Failed to list containers for cleanup")
 		pc.entry.WithError(err).Error("Failed to list containers for cleanup")
 		return
 		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
 			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
 // 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
 	// Get app details
 	app, err := pc.store.GetAppByID(context.Background(), appID)
 	app, err := pc.store.GetAppByID(context.Background(), appID)
 	if err != nil {
 	if err != nil {
@@ -921,8 +566,8 @@ func (pc *PreviewCommon) GetPreviewImageNames(appID int) ([]string, error) {
 	return imageNames, nil
 	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)
 	imageNames, err := pc.GetPreviewImageNames(appID)
 	if err != nil {
 	if err != nil {
 		pc.entry.WithField("app_id", appID).WithError(err).Warn("Failed to get preview image names for cleanup")
 		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 {
 func (pc *PreviewCommon) cleanupLocalDockerImages(ctx context.Context, imageNames []string) error {
 	pc.entry.WithField("image_count", len(imageNames)).Info("Cleaning up specific Docker images locally")
 	pc.entry.WithField("image_count", len(imageNames)).Info("Cleaning up specific Docker images locally")
 
 
 	for _, imageName := range imageNames {
 	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
 			// 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)")
 			pc.entry.WithField("image_name", imageName).WithError(err).Warn("Failed to remove Docker image locally (this may be normal)")
 		} else {
 		} else {
@@ -1007,20 +650,259 @@ func (pc *PreviewCommon) executeSSHCommand(ctx context.Context, ipAddress, comma
 }
 }
 
 
 // Database helper methods
 // 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 {
 	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)
 		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 {
 	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)
 		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 {
 	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)
 		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
 // NewRemotePreviewService creates a new RemotePreviewService
 func NewRemotePreviewService(store *dbstore.SQLiteStore, ovhProvider cloud.Provider, cfg *config.Config, registryClient clients.RegistryClient, registryURL, registryUser, registryPass string) *RemotePreviewService {
 func NewRemotePreviewService(store *dbstore.SQLiteStore, ovhProvider cloud.Provider, cfg *config.Config, registryClient clients.RegistryClient, registryURL, registryUser, registryPass string) *RemotePreviewService {
 	return &RemotePreviewService{
 	return &RemotePreviewService{
-		common:      NewPreviewCommon(store, registryClient, registryURL, registryUser, registryPass),
+		common:      NewPreviewCommon(store, registryURL, registryUser, registryPass),
 		entry:       logrus.WithField("service", "RemotePreviewService"),
 		entry:       logrus.WithField("service", "RemotePreviewService"),
 		ovhProvider: ovhProvider,
 		ovhProvider: ovhProvider,
 		config:      cfg,
 		config:      cfg,
@@ -144,7 +144,7 @@ func (rps *RemotePreviewService) cleanupVPSResources(ctx context.Context, ipAddr
 }
 }
 
 
 // CreatePreview creates a remote preview environment on a VPS
 // 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
 	// Get app details
 	app, err := rps.common.GetStore().GetAppByID(ctx, appId)
 	app, err := rps.common.GetStore().GetAppByID(ctx, appId)
 	if err != nil {
 	if err != nil {
@@ -155,16 +155,14 @@ func (rps *RemotePreviewService) CreatePreview(ctx context.Context, appId int) (
 	preview := models.Preview{
 	preview := models.Preview{
 		AppID:     app.ID,
 		AppID:     app.ID,
 		Status:    "building",
 		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 {
 	if err != nil {
 		return nil, fmt.Errorf("failed to create preview record: %v", err)
 		return nil, fmt.Errorf("failed to create preview record: %v", err)
 	}
 	}
 
 
-	preview.ID = previewID
-
 	// Start async build and deploy to VPS
 	// Start async build and deploy to VPS
 	go rps.buildAndDeployPreview(ctx, preview, app)
 	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")
 	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
 	var logs strings.Builder
 
 
 	rps.entry.WithField("ip_address", ipAddress).WithField("app_name", app.Name).WithField("preview_id", previewID).Info("Starting deployment to VPS")
 	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
 	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")
 	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"
 	compose := "services:\n"
@@ -425,7 +423,7 @@ func (rps *RemotePreviewService) generatePreviewDockerCompose(imageNames []strin
 }
 }
 
 
 // DeletePreview deletes a remote preview
 // 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
 	// Get the preview to ensure it exists
 	preview, err := rps.common.GetStore().GetPreviewByAppID(ctx, appID)
 	preview, err := rps.common.GetStore().GetPreviewByAppID(ctx, appID)
 	if err != nil {
 	if err != nil {
@@ -481,7 +479,7 @@ func (rps *RemotePreviewService) DeletePreview(ctx context.Context, appID int) e
 }
 }
 
 
 // StopPreview stops a remote preview
 // 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)
 	preview, err := rps.common.GetStore().GetPreviewByID(ctx, previewID)
 	if err != nil {
 	if err != nil {
 		return err
 		return err