Add deployment improvements and UI enhancements
- Clone specific commit SHA from webhook instead of just branch HEAD - Log webhook payload in deployment logs - Add build/deploy timing to ntfy and Slack notifications - Implement container rollback on deploy failure - Remove old container only after successful deployment - Show relative times in deployment history (hover for full date) - Update port mappings UI with labeled text inputs - Add footer with version info, license, and repo link - Format deploy key comment as upaas_DATE_appname
This commit is contained in:
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -27,7 +28,7 @@ func (h *Handlers) HandleAppNew() http.HandlerFunc {
|
||||
tmpl := templates.GetParsed()
|
||||
|
||||
return func(writer http.ResponseWriter, _ *http.Request) {
|
||||
data := map[string]any{}
|
||||
data := h.addGlobals(map[string]any{})
|
||||
|
||||
err := tmpl.ExecuteTemplate(writer, "app_new.html", data)
|
||||
if err != nil {
|
||||
@@ -127,22 +128,28 @@ func (h *Handlers) HandleAppDetail() http.HandlerFunc {
|
||||
recentDeploymentsLimit,
|
||||
)
|
||||
|
||||
host := request.Host
|
||||
webhookURL := "https://" + host + "/webhook/" + application.WebhookSecret
|
||||
deployKey := formatDeployKey(application.SSHPublicKey, application.CreatedAt, host)
|
||||
|
||||
data := map[string]any{
|
||||
"App": application,
|
||||
"EnvVars": envVars,
|
||||
"Labels": labels,
|
||||
"Volumes": volumes,
|
||||
"Ports": ports,
|
||||
"Deployments": deployments,
|
||||
"WebhookURL": webhookURL,
|
||||
"DeployKey": deployKey,
|
||||
"Success": request.URL.Query().Get("success"),
|
||||
// Get latest deployment for build logs pane
|
||||
var latestDeployment *models.Deployment
|
||||
if len(deployments) > 0 {
|
||||
latestDeployment = deployments[0]
|
||||
}
|
||||
|
||||
webhookURL := "https://" + request.Host + "/webhook/" + application.WebhookSecret
|
||||
deployKey := formatDeployKey(application.SSHPublicKey, application.CreatedAt, application.Name)
|
||||
|
||||
data := h.addGlobals(map[string]any{
|
||||
"App": application,
|
||||
"EnvVars": envVars,
|
||||
"Labels": labels,
|
||||
"Volumes": volumes,
|
||||
"Ports": ports,
|
||||
"Deployments": deployments,
|
||||
"LatestDeployment": latestDeployment,
|
||||
"WebhookURL": webhookURL,
|
||||
"DeployKey": deployKey,
|
||||
"Success": request.URL.Query().Get("success"),
|
||||
})
|
||||
|
||||
err := tmpl.ExecuteTemplate(writer, "app_detail.html", data)
|
||||
if err != nil {
|
||||
h.log.Error("template execution failed", "error", err)
|
||||
@@ -172,9 +179,9 @@ func (h *Handlers) HandleAppEdit() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
data := h.addGlobals(map[string]any{
|
||||
"App": application,
|
||||
}
|
||||
})
|
||||
|
||||
err := tmpl.ExecuteTemplate(writer, "app_edit.html", data)
|
||||
if err != nil {
|
||||
@@ -325,10 +332,10 @@ func (h *Handlers) HandleAppDeployments() http.HandlerFunc {
|
||||
deploymentsHistoryLimit,
|
||||
)
|
||||
|
||||
data := map[string]any{
|
||||
data := h.addGlobals(map[string]any{
|
||||
"App": application,
|
||||
"Deployments": deployments,
|
||||
}
|
||||
})
|
||||
|
||||
err := tmpl.ExecuteTemplate(writer, "deployments.html", data)
|
||||
if err != nil {
|
||||
@@ -388,6 +395,148 @@ func (h *Handlers) HandleAppLogs() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// HandleDeploymentLogsAPI returns JSON with deployment logs.
|
||||
func (h *Handlers) HandleDeploymentLogsAPI() http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
appID := chi.URLParam(request, "id")
|
||||
deploymentIDStr := chi.URLParam(request, "deploymentID")
|
||||
|
||||
application, findErr := models.FindApp(request.Context(), h.db, appID)
|
||||
if findErr != nil || application == nil {
|
||||
http.NotFound(writer, request)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
deploymentID, parseErr := strconv.ParseInt(deploymentIDStr, 10, 64)
|
||||
if parseErr != nil {
|
||||
http.NotFound(writer, request)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
deployment, deployErr := models.FindDeployment(request.Context(), h.db, deploymentID)
|
||||
if deployErr != nil || deployment == nil || deployment.AppID != appID {
|
||||
http.NotFound(writer, request)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
|
||||
logs := ""
|
||||
if deployment.Logs.Valid {
|
||||
logs = deployment.Logs.String
|
||||
}
|
||||
|
||||
response := map[string]any{
|
||||
"logs": logs,
|
||||
"status": deployment.Status,
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(writer).Encode(response)
|
||||
}
|
||||
}
|
||||
|
||||
// containerLogsAPITail is the default number of log lines for the container logs API.
|
||||
const containerLogsAPITail = "100"
|
||||
|
||||
// HandleContainerLogsAPI returns JSON with container logs.
|
||||
func (h *Handlers) HandleContainerLogsAPI() http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
appID := chi.URLParam(request, "id")
|
||||
|
||||
application, findErr := models.FindApp(request.Context(), h.db, appID)
|
||||
if findErr != nil || application == nil {
|
||||
http.NotFound(writer, request)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
|
||||
containerInfo, containerErr := h.docker.FindContainerByAppID(request.Context(), appID)
|
||||
if containerErr != nil || containerInfo == nil {
|
||||
response := map[string]any{
|
||||
"logs": "No container running\n",
|
||||
"status": "stopped",
|
||||
}
|
||||
_ = json.NewEncoder(writer).Encode(response)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logs, logsErr := h.docker.ContainerLogs(
|
||||
request.Context(),
|
||||
containerInfo.ID,
|
||||
containerLogsAPITail,
|
||||
)
|
||||
if logsErr != nil {
|
||||
h.log.Error("failed to get container logs",
|
||||
"error", logsErr,
|
||||
"app", application.Name,
|
||||
"container", containerInfo.ID,
|
||||
)
|
||||
|
||||
response := map[string]any{
|
||||
"logs": "Failed to fetch container logs\n",
|
||||
"status": "error",
|
||||
}
|
||||
_ = json.NewEncoder(writer).Encode(response)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
status := "stopped"
|
||||
if containerInfo.Running {
|
||||
status = "running"
|
||||
}
|
||||
|
||||
response := map[string]any{
|
||||
"logs": logs,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(writer).Encode(response)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleAppStatusAPI returns JSON with app status and latest deployment info.
|
||||
func (h *Handlers) HandleAppStatusAPI() http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
appID := chi.URLParam(request, "id")
|
||||
|
||||
application, findErr := models.FindApp(request.Context(), h.db, appID)
|
||||
if findErr != nil || application == nil {
|
||||
http.NotFound(writer, request)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Get latest deployment
|
||||
deployments, _ := application.GetDeployments(request.Context(), 1)
|
||||
|
||||
var latestDeploymentID int64
|
||||
|
||||
var latestDeploymentStatus string
|
||||
|
||||
if len(deployments) > 0 {
|
||||
latestDeploymentID = deployments[0].ID
|
||||
latestDeploymentStatus = string(deployments[0].Status)
|
||||
}
|
||||
|
||||
response := map[string]any{
|
||||
"status": string(application.Status),
|
||||
"latestDeploymentID": latestDeploymentID,
|
||||
"latestDeploymentStatus": latestDeploymentStatus,
|
||||
}
|
||||
|
||||
_ = json.NewEncoder(writer).Encode(response)
|
||||
}
|
||||
}
|
||||
|
||||
// containerAction represents a container operation type.
|
||||
type containerAction string
|
||||
|
||||
@@ -778,8 +927,8 @@ func (h *Handlers) HandlePortDelete() http.HandlerFunc {
|
||||
}
|
||||
|
||||
// formatDeployKey formats an SSH public key with a descriptive comment.
|
||||
// Format: ssh-ed25519 AAAA... upaas-2025-01-15-example.com.
|
||||
func formatDeployKey(pubKey string, createdAt time.Time, host string) string {
|
||||
// Format: ssh-ed25519 AAAA... upaas_2025-01-15_myapp
|
||||
func formatDeployKey(pubKey string, createdAt time.Time, appName string) string {
|
||||
const minKeyParts = 2
|
||||
|
||||
parts := strings.Fields(pubKey)
|
||||
@@ -787,7 +936,7 @@ func formatDeployKey(pubKey string, createdAt time.Time, host string) string {
|
||||
return pubKey
|
||||
}
|
||||
|
||||
comment := "upaas-" + createdAt.Format("2006-01-02") + "-" + host
|
||||
comment := "upaas_" + createdAt.Format("2006-01-02") + "_" + appName
|
||||
|
||||
return parts[0] + " " + parts[1] + " " + comment
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user