diff --git a/internal/docker/client.go b/internal/docker/client.go index 711962e..5fc5806 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -554,16 +554,20 @@ func (c *Client) createGitContainer( // Build the git command based on whether we have a specific commit SHA var cmd []string + var entrypoint []string + if cfg.commitSHA != "" { // Clone without depth limit so we can checkout any commit, then checkout specific SHA - // Using sh -c to run multiple commands + // Using sh -c to run multiple commands - need to clear entrypoint script := fmt.Sprintf( "git clone --branch %s %s /repo && cd /repo && git checkout %s", cfg.branch, cfg.repoURL, cfg.commitSHA, ) + entrypoint = []string{} cmd = []string{"sh", "-c", script} } else { - // Shallow clone of branch HEAD + // Shallow clone of branch HEAD - use default git entrypoint + entrypoint = nil cmd = []string{"clone", "--depth", "1", "--branch", cfg.branch, cfg.repoURL, "/repo"} } @@ -571,7 +575,7 @@ func (c *Client) createGitContainer( resp, err := c.docker.ContainerCreate(ctx, &container.Config{ Image: gitImage, - Entrypoint: []string{}, // Clear entrypoint when using sh -c + Entrypoint: entrypoint, Cmd: cmd, Env: []string{"GIT_SSH_COMMAND=" + gitSSHCmd}, WorkingDir: "/", diff --git a/internal/handlers/app.go b/internal/handlers/app.go index 9ca117f..42a0aee 100644 --- a/internal/handlers/app.go +++ b/internal/handlers/app.go @@ -537,6 +537,49 @@ func (h *Handlers) HandleAppStatusAPI() http.HandlerFunc { } } +// HandleRecentDeploymentsAPI returns JSON with recent deployments. +func (h *Handlers) HandleRecentDeploymentsAPI() 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 + } + + deployments, deployErr := application.GetDeployments( + request.Context(), + recentDeploymentsLimit, + ) + if deployErr != nil { + h.log.Error("failed to get deployments", "error", deployErr, "app", appID) + + deployments = []*models.Deployment{} + } + + // Build response with formatted data + deploymentsData := make([]map[string]any, 0, len(deployments)) + + for _, d := range deployments { + deploymentsData = append(deploymentsData, map[string]any{ + "id": d.ID, + "status": string(d.Status), + "duration": d.Duration(), + "shortCommit": d.ShortCommit(), + "finishedAtISO": d.FinishedAtISO(), + "finishedAtLabel": d.FinishedAtFormatted(), + }) + } + + writer.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(writer).Encode(map[string]any{ + "deployments": deploymentsData, + }) + } +} + // containerAction represents a container operation type. type containerAction string diff --git a/internal/handlers/dashboard.go b/internal/handlers/dashboard.go index e2204c0..76fd64f 100644 --- a/internal/handlers/dashboard.go +++ b/internal/handlers/dashboard.go @@ -2,17 +2,29 @@ package handlers import ( "net/http" + "time" "git.eeqj.de/sneak/upaas/internal/models" "git.eeqj.de/sneak/upaas/templates" ) +// AppStats holds deployment statistics for an app. +type AppStats struct { + App *models.App + LastDeployTime *time.Time + LastDeployISO string + LastDeployLabel string + DeployCount int +} + // HandleDashboard returns the dashboard handler. func (h *Handlers) HandleDashboard() http.HandlerFunc { tmpl := templates.GetParsed() return func(writer http.ResponseWriter, request *http.Request) { - apps, fetchErr := models.AllApps(request.Context(), h.db) + ctx := request.Context() + + apps, fetchErr := models.AllApps(ctx, h.db) if fetchErr != nil { h.log.Error("failed to fetch apps", "error", fetchErr) http.Error(writer, "Internal Server Error", http.StatusInternalServerError) @@ -20,8 +32,41 @@ func (h *Handlers) HandleDashboard() http.HandlerFunc { return } + // Fetch stats for each app + appStats := make([]*AppStats, 0, len(apps)) + + for _, app := range apps { + stats := &AppStats{App: app} + + // Get deploy count + count, countErr := models.CountDeploymentsByAppID(ctx, h.db, app.ID) + if countErr != nil { + h.log.Error("failed to count deployments", "error", countErr, "app", app.ID) + } else { + stats.DeployCount = count + } + + // Get latest deployment + latest, latestErr := models.LatestDeploymentForApp(ctx, h.db, app.ID) + if latestErr != nil { + h.log.Error("failed to get latest deployment", "error", latestErr, "app", app.ID) + } else if latest != nil { + if latest.FinishedAt.Valid { + stats.LastDeployTime = &latest.FinishedAt.Time + stats.LastDeployISO = latest.FinishedAt.Time.Format(time.RFC3339) + stats.LastDeployLabel = latest.FinishedAt.Time.Format("2006-01-02 15:04:05") + } else { + stats.LastDeployTime = &latest.StartedAt + stats.LastDeployISO = latest.StartedAt.Format(time.RFC3339) + stats.LastDeployLabel = latest.StartedAt.Format("2006-01-02 15:04:05") + } + } + + appStats = append(appStats, stats) + } + data := h.addGlobals(map[string]any{ - "Apps": apps, + "AppStats": appStats, }) execErr := tmpl.ExecuteTemplate(writer, "dashboard.html", data) diff --git a/internal/models/deployment.go b/internal/models/deployment.go index 855563c..e0af1ec 100644 --- a/internal/models/deployment.go +++ b/internal/models/deployment.go @@ -302,3 +302,24 @@ func LatestDeploymentForApp( return deploy, nil } + +// CountDeploymentsByAppID returns the total number of deployments for an app. +func CountDeploymentsByAppID( + ctx context.Context, + deployDB *database.Database, + appID string, +) (int, error) { + var count int + + row := deployDB.QueryRow(ctx, + "SELECT COUNT(*) FROM deployments WHERE app_id = ?", + appID, + ) + + err := row.Scan(&count) + if err != nil { + return 0, fmt.Errorf("counting deployments: %w", err) + } + + return count, nil +} diff --git a/internal/server/routes.go b/internal/server/routes.go index b8fb039..196a4b3 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -69,6 +69,7 @@ func (s *Server) SetupRoutes() { r.Get("/apps/{id}/logs", s.handlers.HandleAppLogs()) r.Get("/apps/{id}/container-logs", s.handlers.HandleContainerLogsAPI()) r.Get("/apps/{id}/status", s.handlers.HandleAppStatusAPI()) + r.Get("/apps/{id}/recent-deployments", s.handlers.HandleRecentDeploymentsAPI()) r.Post("/apps/{id}/restart", s.handlers.HandleAppRestart()) r.Post("/apps/{id}/stop", s.handlers.HandleAppStop()) r.Post("/apps/{id}/start", s.handlers.HandleAppStart()) diff --git a/static/js/app.js b/static/js/app.js index e347369..cdcab0b 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -3,213 +3,665 @@ * Vanilla JS, no dependencies */ -(function() { - 'use strict'; +(function () { + "use strict"; - /** - * Copy text to clipboard - * @param {string} text - Text to copy - * @param {HTMLElement} button - Button element to update feedback - */ - function copyToClipboard(text, button) { - const originalText = button.textContent; - const originalTitle = button.getAttribute('title'); + /** + * Copy text to clipboard + * @param {string} text - Text to copy + * @param {HTMLElement} button - Button element to update feedback + */ + function copyToClipboard(text, button) { + const originalText = button.textContent; + const originalTitle = button.getAttribute("title"); - navigator.clipboard.writeText(text).then(function() { - // Success feedback - button.textContent = 'Copied!'; - button.classList.add('text-success-500'); + navigator.clipboard + .writeText(text) + .then(function () { + // Success feedback + button.textContent = "Copied!"; + button.classList.add("text-success-500"); - setTimeout(function() { - button.textContent = originalText; - button.classList.remove('text-success-500'); - if (originalTitle) { - button.setAttribute('title', originalTitle); + setTimeout(function () { + button.textContent = originalText; + button.classList.remove("text-success-500"); + if (originalTitle) { + button.setAttribute("title", originalTitle); + } + }, 2000); + }) + .catch(function (err) { + // Fallback for older browsers + console.error("Failed to copy:", err); + + // Try fallback method + var textArea = document.createElement("textarea"); + textArea.value = text; + textArea.style.position = "fixed"; + textArea.style.left = "-9999px"; + document.body.appendChild(textArea); + textArea.select(); + + try { + document.execCommand("copy"); + button.textContent = "Copied!"; + setTimeout(function () { + button.textContent = originalText; + }, 2000); + } catch (e) { + button.textContent = "Failed"; + setTimeout(function () { + button.textContent = originalText; + }, 2000); + } + + document.body.removeChild(textArea); + }); + } + + /** + * Initialize copy buttons + * Looks for elements with data-copy attribute + */ + function initCopyButtons() { + var copyButtons = document.querySelectorAll("[data-copy]"); + + copyButtons.forEach(function (button) { + button.addEventListener("click", function (e) { + e.preventDefault(); + var text = button.getAttribute("data-copy"); + copyToClipboard(text, button); + }); + }); + + // Also handle buttons that copy content from a sibling element + var copyTargetButtons = document.querySelectorAll("[data-copy-target]"); + + copyTargetButtons.forEach(function (button) { + button.addEventListener("click", function (e) { + e.preventDefault(); + var targetId = button.getAttribute("data-copy-target"); + var target = document.getElementById(targetId); + if (target) { + var text = target.textContent || target.value; + copyToClipboard(text, button); + } + }); + }); + } + + /** + * Confirm destructive actions + * Looks for forms with data-confirm attribute + */ + function initConfirmations() { + var confirmForms = document.querySelectorAll("form[data-confirm]"); + + confirmForms.forEach(function (form) { + form.addEventListener("submit", function (e) { + var message = form.getAttribute("data-confirm"); + if (!confirm(message)) { + e.preventDefault(); + } + }); + }); + + // Also handle buttons with data-confirm + var confirmButtons = document.querySelectorAll("button[data-confirm]"); + + confirmButtons.forEach(function (button) { + button.addEventListener("click", function (e) { + var message = button.getAttribute("data-confirm"); + if (!confirm(message)) { + e.preventDefault(); + } + }); + }); + } + + /** + * Toggle visibility of elements + * Looks for buttons with data-toggle attribute + */ + function initToggles() { + var toggleButtons = document.querySelectorAll("[data-toggle]"); + + toggleButtons.forEach(function (button) { + button.addEventListener("click", function (e) { + e.preventDefault(); + var targetId = button.getAttribute("data-toggle"); + var target = document.getElementById(targetId); + if (target) { + target.classList.toggle("hidden"); + + // Update button text if data-toggle-text is provided + var toggleText = button.getAttribute("data-toggle-text"); + if (toggleText) { + var currentText = button.textContent; + button.textContent = toggleText; + button.setAttribute("data-toggle-text", currentText); + } + } + }); + }); + } + + /** + * Auto-dismiss alerts after a delay + * Looks for elements with data-auto-dismiss attribute + */ + function initAutoDismiss() { + var dismissElements = document.querySelectorAll("[data-auto-dismiss]"); + + dismissElements.forEach(function (element) { + var delay = + parseInt(element.getAttribute("data-auto-dismiss"), 10) || 5000; + + setTimeout(function () { + element.style.transition = "opacity 0.3s ease-out"; + element.style.opacity = "0"; + + setTimeout(function () { + element.remove(); + }, 300); + }, delay); + }); + } + + /** + * Manual dismiss for alerts + * Looks for buttons with data-dismiss attribute + */ + function initDismissButtons() { + var dismissButtons = document.querySelectorAll("[data-dismiss]"); + + dismissButtons.forEach(function (button) { + button.addEventListener("click", function (e) { + e.preventDefault(); + var targetId = button.getAttribute("data-dismiss"); + var target = targetId + ? document.getElementById(targetId) + : button.closest(".alert"); + + if (target) { + target.style.transition = "opacity 0.3s ease-out"; + target.style.opacity = "0"; + + setTimeout(function () { + target.remove(); + }, 300); + } + }); + }); + } + + /** + * Initialize all features when DOM is ready + */ + function init() { + initCopyButtons(); + initConfirmations(); + initToggles(); + initAutoDismiss(); + initDismissButtons(); + } + + // Run on DOM ready + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } + + // ============================================ + // Deployment & Status Utilities + // ============================================ + + /** + * Format a date string as relative time (e.g., "5 minutes ago") + */ + function formatRelativeTime(dateStr) { + var date = new Date(dateStr); + var now = new Date(); + var diffMs = now - date; + var diffSec = Math.floor(diffMs / 1000); + var diffMin = Math.floor(diffSec / 60); + var diffHour = Math.floor(diffMin / 60); + var diffDay = Math.floor(diffHour / 24); + + if (diffSec < 60) return "just now"; + if (diffMin < 60) + return diffMin + (diffMin === 1 ? " minute ago" : " minutes ago"); + if (diffHour < 24) + return diffHour + (diffHour === 1 ? " hour ago" : " hours ago"); + if (diffDay < 7) + return diffDay + (diffDay === 1 ? " day ago" : " days ago"); + return date.toLocaleDateString(); + } + + /** + * Update all elements with class 'relative-time' to show relative times + */ + function updateRelativeTimes() { + document.querySelectorAll(".relative-time").forEach(function (el) { + var time = el.getAttribute("data-time"); + if (time) { + el.textContent = formatRelativeTime(time); + } + }); + } + + /** + * Get the badge class for a given status + */ + function statusBadgeClass(status) { + if (status === "running" || status === "success") + return "badge-success"; + if (status === "building" || status === "deploying") + return "badge-warning"; + if (status === "failed" || status === "error") return "badge-error"; + return "badge-neutral"; + } + + /** + * Format status for display (capitalize first letter) + */ + function statusLabel(status) { + if (!status) return ""; + return status.charAt(0).toUpperCase() + status.slice(1); + } + + /** + * Update a status badge element + */ + function updateStatusBadge(el, status, extraClasses) { + if (!el) return; + el.className = + statusBadgeClass(status) + (extraClasses ? " " + extraClasses : ""); + el.textContent = statusLabel(status); + } + + /** + * Scroll an element to the bottom + */ + function scrollToBottom(el) { + if (el) { + el.scrollTop = el.scrollHeight; } - }, 2000); - }).catch(function(err) { - // Fallback for older browsers - console.error('Failed to copy:', err); + } - // Try fallback method - var textArea = document.createElement('textarea'); - textArea.value = text; - textArea.style.position = 'fixed'; - textArea.style.left = '-9999px'; - document.body.appendChild(textArea); - textArea.select(); - - try { - document.execCommand('copy'); - button.textContent = 'Copied!'; - setTimeout(function() { - button.textContent = originalText; - }, 2000); - } catch (e) { - button.textContent = 'Failed'; - setTimeout(function() { - button.textContent = originalText; - }, 2000); - } - - document.body.removeChild(textArea); - }); - } - - /** - * Initialize copy buttons - * Looks for elements with data-copy attribute - */ - function initCopyButtons() { - var copyButtons = document.querySelectorAll('[data-copy]'); - - copyButtons.forEach(function(button) { - button.addEventListener('click', function(e) { - e.preventDefault(); - var text = button.getAttribute('data-copy'); - copyToClipboard(text, button); - }); - }); - - // Also handle buttons that copy content from a sibling element - var copyTargetButtons = document.querySelectorAll('[data-copy-target]'); - - copyTargetButtons.forEach(function(button) { - button.addEventListener('click', function(e) { - e.preventDefault(); - var targetId = button.getAttribute('data-copy-target'); - var target = document.getElementById(targetId); - if (target) { - var text = target.textContent || target.value; - copyToClipboard(text, button); + /** + * Disable a deploy button immediately + */ + function disableDeployButton(btn, btnText) { + if (!btn) return; + btn.disabled = true; + btn.classList.add("opacity-50", "cursor-not-allowed"); + if (btnText) { + btnText.textContent = "Deploying..."; + } else { + btn.textContent = "Deploying..."; } - }); - }); - } + } - /** - * Confirm destructive actions - * Looks for forms with data-confirm attribute - */ - function initConfirmations() { - var confirmForms = document.querySelectorAll('form[data-confirm]'); - - confirmForms.forEach(function(form) { - form.addEventListener('submit', function(e) { - var message = form.getAttribute('data-confirm'); - if (!confirm(message)) { - e.preventDefault(); + /** + * Enable a deploy button + */ + function enableDeployButton(btn, btnText) { + if (!btn) return; + btn.disabled = false; + btn.classList.remove("opacity-50", "cursor-not-allowed"); + if (btnText) { + btnText.textContent = "Deploy Now"; + } else { + btn.textContent = "Deploy Now"; } - }); - }); + } - // Also handle buttons with data-confirm - var confirmButtons = document.querySelectorAll('button[data-confirm]'); - - confirmButtons.forEach(function(button) { - button.addEventListener('click', function(e) { - var message = button.getAttribute('data-confirm'); - if (!confirm(message)) { - e.preventDefault(); + /** + * Update deploy button based on status + */ + function updateDeployButton(btn, btnText, status) { + var isDeploying = status === "building" || status === "deploying"; + if (isDeploying) { + disableDeployButton(btn, btnText); + } else { + enableDeployButton(btn, btnText); } - }); - }); - } + } - /** - * Toggle visibility of elements - * Looks for buttons with data-toggle attribute - */ - function initToggles() { - var toggleButtons = document.querySelectorAll('[data-toggle]'); + // ============================================ + // App Detail Page + // ============================================ - toggleButtons.forEach(function(button) { - button.addEventListener('click', function(e) { - e.preventDefault(); - var targetId = button.getAttribute('data-toggle'); - var target = document.getElementById(targetId); - if (target) { - target.classList.toggle('hidden'); + /** + * Initialize app detail page polling and updates + * @param {object} config - Configuration object + * @param {string} config.appId - App ID + * @param {number|null} config.initialDeploymentId - Initial deployment ID or null + */ + function initAppDetailPage(config) { + var appId = config.appId; + var currentDeploymentId = config.initialDeploymentId; - // Update button text if data-toggle-text is provided - var toggleText = button.getAttribute('data-toggle-text'); - if (toggleText) { - var currentText = button.textContent; - button.textContent = toggleText; - button.setAttribute('data-toggle-text', currentText); - } + var containerLogsEl = document.getElementById("container-logs"); + var containerLogsWrapper = document.getElementById( + "container-logs-wrapper", + ); + var containerStatusEl = document.getElementById("container-status"); + var buildLogsEl = document.getElementById("build-logs"); + var buildLogsWrapper = document.getElementById("build-logs-wrapper"); + var buildStatusEl = document.getElementById("build-status"); + var buildLogsSection = document.getElementById("build-logs-section"); + var appStatusEl = document.getElementById("app-status"); + var deployBtn = document.getElementById("deploy-btn"); + var deployBtnText = document.getElementById("deploy-btn-text"); + + // Disable deploy button immediately on form submit + var deployForm = document.getElementById("deploy-form"); + if (deployForm && deployBtn) { + deployForm.addEventListener("submit", function () { + disableDeployButton(deployBtn, deployBtnText); + }); } - }); - }); - } - /** - * Auto-dismiss alerts after a delay - * Looks for elements with data-auto-dismiss attribute - */ - function initAutoDismiss() { - var dismissElements = document.querySelectorAll('[data-auto-dismiss]'); + function fetchAppStatus() { + fetch("/apps/" + appId + "/status") + .then(function (r) { + return r.json(); + }) + .then(function (data) { + updateStatusBadge(appStatusEl, data.status); + updateDeployButton(deployBtn, deployBtnText, data.status); - dismissElements.forEach(function(element) { - var delay = parseInt(element.getAttribute('data-auto-dismiss'), 10) || 5000; - - setTimeout(function() { - element.style.transition = 'opacity 0.3s ease-out'; - element.style.opacity = '0'; - - setTimeout(function() { - element.remove(); - }, 300); - }, delay); - }); - } - - /** - * Manual dismiss for alerts - * Looks for buttons with data-dismiss attribute - */ - function initDismissButtons() { - var dismissButtons = document.querySelectorAll('[data-dismiss]'); - - dismissButtons.forEach(function(button) { - button.addEventListener('click', function(e) { - e.preventDefault(); - var targetId = button.getAttribute('data-dismiss'); - var target = targetId ? document.getElementById(targetId) : button.closest('.alert'); - - if (target) { - target.style.transition = 'opacity 0.3s ease-out'; - target.style.opacity = '0'; - - setTimeout(function() { - target.remove(); - }, 300); + if ( + data.latestDeploymentID && + data.latestDeploymentID !== currentDeploymentId + ) { + currentDeploymentId = data.latestDeploymentID; + if (buildLogsSection) { + buildLogsSection.style.display = ""; + } + fetchBuildLogs(); + } + }) + .catch(function (err) { + console.error("Status fetch error:", err); + }); } - }); - }); - } - /** - * Initialize all features when DOM is ready - */ - function init() { - initCopyButtons(); - initConfirmations(); - initToggles(); - initAutoDismiss(); - initDismissButtons(); - } + function fetchContainerLogs() { + fetch("/apps/" + appId + "/container-logs") + .then(function (r) { + return r.json(); + }) + .then(function (data) { + if (containerLogsEl) + containerLogsEl.textContent = + data.logs || "No logs available"; + updateStatusBadge( + containerStatusEl, + data.status, + "text-xs", + ); + scrollToBottom(containerLogsWrapper); + }) + .catch(function (err) { + if (containerLogsEl) + containerLogsEl.textContent = "Failed to fetch logs"; + }); + } - // Run on DOM ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); - } else { - init(); - } + function fetchBuildLogs() { + if (!currentDeploymentId || !buildLogsEl) return; - // Expose copyToClipboard globally for inline onclick handlers if needed - window.upaas = { - copyToClipboard: copyToClipboard - }; + fetch( + "/apps/" + + appId + + "/deployments/" + + currentDeploymentId + + "/logs", + ) + .then(function (r) { + return r.json(); + }) + .then(function (data) { + buildLogsEl.textContent = + data.logs || "No build logs available"; + updateStatusBadge(buildStatusEl, data.status, "text-xs"); + scrollToBottom(buildLogsWrapper); + }) + .catch(function (err) { + buildLogsEl.textContent = "Failed to fetch logs"; + }); + } + function fetchRecentDeployments() { + fetch("/apps/" + appId + "/recent-deployments") + .then(function (r) { + return r.json(); + }) + .then(function (data) { + var tbody = document.getElementById("deployments-tbody"); + if (!tbody || !data.deployments) return; + + if (data.deployments.length === 0) { + tbody.innerHTML = + '
| Repository | Branch | Status | +Last Deploy | +Deploys | Actions | - {{range .Apps}} + {{range .AppStats}}|||
|---|---|---|---|---|---|---|---|---|
| - - {{.Name}} + + {{.App.Name}} | -{{.RepoURL}} | -{{.Branch}} | +{{.App.RepoURL}} | +{{.App.Branch}} | - {{if eq .Status "running"}} + {{if eq .App.Status "running"}} Running - {{else if eq .Status "building"}} + {{else if eq .App.Status "building"}} Building - {{else if eq .Status "error"}} + {{else if eq .App.Status "error"}} Error - {{else if eq .Status "stopped"}} + {{else if eq .App.Status "stopped"}} Stopped {{else}} - {{.Status}} + {{.App.Status}} {{end}} | ++ {{if .LastDeployTime}} + + {{.LastDeployLabel}} + + {{else}} + - + {{end}} + | +{{.DeployCount}} |
diff --git a/templates/deployments.html b/templates/deployments.html
index 77467f6..00d3416 100644
--- a/templates/deployments.html
+++ b/templates/deployments.html
@@ -17,38 +17,39 @@
Deployment History-
+
+
+
+
+ {{if .Deployments}}
{{range .Deployments}}
-
- {{else}}
-
+
{{end}}
-
- {{.StartedAt.Format "2006-01-02 15:04:05"}}
+ {{.StartedAt.Format "2006-01-02 15:04:05"}}
{{if .FinishedAt.Valid}}
- -
- {{.FinishedAt.Time.Format "15:04:05"}}
+ •
+ {{.Duration}}
{{end}}
- {{if eq .Status "success"}}
- Success
- {{else if eq .Status "failed"}}
- Failed
- {{else if eq .Status "building"}}
- Building
- {{else if eq .Status "deploying"}}
- Deploying
- {{else}}
- {{.Status}}
- {{end}}
+ {{if eq .Status "success"}}Success{{else if eq .Status "failed"}}Failed{{else if eq .Status "building"}}Building{{else if eq .Status "deploying"}}Deploying{{else}}{{.Status}}{{end}}
Commit:
- {{.CommitSHA.String}}
+ {{.ShortCommit}}
{{end}}
@@ -75,35 +76,43 @@
{{end}}
+ {{if or .Logs.Valid (eq .Status "building") (eq .Status "deploying")}}
+ --{{.Logs.String}}
+
+
{{if .Logs.Valid}}{{.Logs.String}}{{else}}Loading...{{end}}
+
-
- No deployments yet-Deploy your application to see the deployment history here. -
-
+ {{else}}
+
- {{end}}
+
+
{{end}}
+
+ {{end}}
+
No deployments yet+Deploy your application to see the deployment history here. +
+
+
|