From 07161bebf9f8d00d400ba95f00570d0182949f72 Mon Sep 17 00:00:00 2001 From: coderbuzz Date: Thu, 25 Jun 2026 08:12:18 +0000 Subject: [PATCH] feat: add Start/Stop functionality for apps - Service: Stop() runs docker compose stop, Start() runs docker compose start - API routes: POST /api/apps/{id}/stop and /api/apps/{id}/start - Web routes: POST /apps/{id}/stop and /apps/{id}/start - Detail page: Start button when stopped, Stop button when running - Logs now accessible for stopped apps too --- internal/app/handler.go | 44 +++++++++++++++++ internal/app/service.go | 60 ++++++++++++++++++++++++ internal/http/router.go | 8 ++++ internal/http/templates/apps_detail.html | 12 ++++- 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/internal/app/handler.go b/internal/app/handler.go index 64b93c2..4ac0641 100644 --- a/internal/app/handler.go +++ b/internal/app/handler.go @@ -135,6 +135,28 @@ func (h *Handler) Rollback(w http.ResponseWriter, r *http.Request) { jsonResponse(w, http.StatusAccepted, map[string]string{"message": "rollback started"}) } +func (h *Handler) Stop(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + + if err := h.service.Stop(id); err != nil { + jsonResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + jsonResponse(w, http.StatusOK, map[string]string{"message": "stopped"}) +} + +func (h *Handler) Start(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + + if err := h.service.Start(id); err != nil { + jsonResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + jsonResponse(w, http.StatusOK, map[string]string{"message": "started"}) +} + func (h *Handler) ListSecrets(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) secrets, err := h.service.ListSecrets(id) @@ -542,6 +564,28 @@ func (h *WebHandler) AppRollbackWeb(w http.ResponseWriter, r *http.Request, rend http.Redirect(w, r, "/apps/"+strconv.FormatInt(id, 10), http.StatusSeeOther) } +func (h *WebHandler) AppStopWeb(w http.ResponseWriter, r *http.Request, render RenderFunc) { + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + + if err := h.service.Stop(id); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/apps/"+strconv.FormatInt(id, 10), http.StatusSeeOther) +} + +func (h *WebHandler) AppStartWeb(w http.ResponseWriter, r *http.Request, render RenderFunc) { + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + + if err := h.service.Start(id); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/apps/"+strconv.FormatInt(id, 10), http.StatusSeeOther) +} + func saveFormSecrets(r *http.Request, svc *Service, appID int64) { keys := r.Form["secret_key"] vals := r.Form["secret_val"] diff --git a/internal/app/service.go b/internal/app/service.go index ee6886d..ad4cee6 100644 --- a/internal/app/service.go +++ b/internal/app/service.go @@ -342,6 +342,66 @@ func (s *Service) Redeploy(id int64) { s.deployWithCommit(id, "") } +func (s *Service) Stop(id int64) error { + app, err := s.repo.Get(id) + if err != nil || app == nil { + return fmt.Errorf("app not found") + } + + svr, err := s.serverRepo.Get(app.ServerID) + if err != nil || svr == nil { + return fmt.Errorf("server not found") + } + + client, err := ssh.Connect(svr.Host, svr.Port, svr.User, svr.SSHKey) + if err != nil { + return fmt.Errorf("SSH connect: %w", err) + } + defer client.Close() + + dc := dockerComposeCmd(client) + composePath := fmt.Sprintf("/opt/dockify/apps/%s/docker-compose.yml", app.Name) + log.Printf("Stopping %q on %s...", app.Name, svr.Name) + + if out, err := client.Exec(fmt.Sprintf("%s -f %s stop 2>&1", dc, composePath)); err != nil { + return fmt.Errorf("compose stop: %w\n%s", err, out) + } + + s.repo.UpdateStatus(id, StatusStopped) + log.Printf("App %q stopped", app.Name) + return nil +} + +func (s *Service) Start(id int64) error { + app, err := s.repo.Get(id) + if err != nil || app == nil { + return fmt.Errorf("app not found") + } + + svr, err := s.serverRepo.Get(app.ServerID) + if err != nil || svr == nil { + return fmt.Errorf("server not found") + } + + client, err := ssh.Connect(svr.Host, svr.Port, svr.User, svr.SSHKey) + if err != nil { + return fmt.Errorf("SSH connect: %w", err) + } + defer client.Close() + + dc := dockerComposeCmd(client) + composePath := fmt.Sprintf("/opt/dockify/apps/%s/docker-compose.yml", app.Name) + log.Printf("Starting %q on %s...", app.Name, svr.Name) + + if out, err := client.Exec(fmt.Sprintf("%s -f %s start 2>&1", dc, composePath)); err != nil { + return fmt.Errorf("compose start: %w\n%s", err, out) + } + + s.repo.UpdateStatus(id, StatusRunning) + log.Printf("App %q started", app.Name) + return nil +} + func (s *Service) DashboardStats() *DashboardStats { apps, _ := s.repo.List() servers, _ := s.serverRepo.List() diff --git a/internal/http/router.go b/internal/http/router.go index 13c47b4..aebd734 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -101,6 +101,8 @@ func NewRouter(svc *server.Service, appSvc *app.Service, render RenderFunc, serv r.Delete("/{id}", appAPIHandler.Delete) r.Post("/{id}/redeploy", appAPIHandler.Redeploy) r.Post("/{id}/rollback", appAPIHandler.Rollback) + r.Post("/{id}/stop", appAPIHandler.Stop) + r.Post("/{id}/start", appAPIHandler.Start) r.Get("/{id}/deployments", appAPIHandler.ListDeployments) r.Get("/{id}/logs", appAPIHandler.Logs) r.Get("/{id}/secrets", appAPIHandler.ListSecrets) @@ -138,6 +140,12 @@ func NewRouter(svc *server.Service, appSvc *app.Service, render RenderFunc, serv r.Post("/{id}/rollback", func(w http.ResponseWriter, r *http.Request) { appWebHandler.AppRollbackWeb(w, r, render) }) + r.Post("/{id}/stop", func(w http.ResponseWriter, r *http.Request) { + appWebHandler.AppStopWeb(w, r, render) + }) + r.Post("/{id}/start", func(w http.ResponseWriter, r *http.Request) { + appWebHandler.AppStartWeb(w, r, render) + }) r.Get("/{id}/edit", func(w http.ResponseWriter, r *http.Request) { appWebHandler.AppEditPage(w, r, render) }) diff --git a/internal/http/templates/apps_detail.html b/internal/http/templates/apps_detail.html index 3e74e0c..2fbec62 100644 --- a/internal/http/templates/apps_detail.html +++ b/internal/http/templates/apps_detail.html @@ -86,6 +86,16 @@

Config Files

+ {{if eq .App.Status "running"}} +
+ +
+ {{end}} + {{if eq .App.Status "stopped"}} +
+ +
+ {{end}} Edit
@@ -126,7 +136,7 @@

Deployment History

{{end}} - {{if eq .App.Status "running"}} + {{if or (eq .App.Status "running") (eq .App.Status "stopped")}}

Logs