diff --git a/static/js/app.js b/static/js/app.js index c9721f3..04becc9 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -295,6 +295,71 @@ document.addEventListener("alpine:init", () => { }, })); + // ============================================ + // Deployment Card Component (for individual deployment cards) + // ============================================ + Alpine.data("deploymentCard", (config) => ({ + appId: config.appId, + deploymentId: config.deploymentId, + logs: "", + status: config.status || "", + pollInterval: null, + + init() { + // Read initial logs from script tag (avoids escaping issues) + const initialLogsEl = this.$el.querySelector(".initial-logs"); + this.logs = initialLogsEl?.textContent || "Loading..."; + + // Only poll if deployment is in progress + if (Alpine.store("utils").isDeploying(this.status)) { + this.fetchLogs(); + this.pollInterval = setInterval(() => this.fetchLogs(), 1000); + } + }, + + destroy() { + if (this.pollInterval) { + clearInterval(this.pollInterval); + } + }, + + async fetchLogs() { + try { + const res = await fetch( + `/apps/${this.appId}/deployments/${this.deploymentId}/logs`, + ); + const data = await res.json(); + this.logs = data.logs || "No logs available"; + this.status = data.status; + + // Scroll to bottom after update + this.$nextTick(() => { + Alpine.store("utils").scrollToBottom(this.$refs.logsWrapper); + }); + + // Stop polling if deployment is done + if (!Alpine.store("utils").isDeploying(data.status)) { + if (this.pollInterval) { + clearInterval(this.pollInterval); + this.pollInterval = null; + } + // Reload page to show final state with duration etc + window.location.reload(); + } + } catch (err) { + console.error("Logs fetch error:", err); + } + }, + + get statusBadgeClass() { + return Alpine.store("utils").statusBadgeClass(this.status); + }, + + get statusLabel() { + return Alpine.store("utils").statusLabel(this.status); + }, + })); + // ============================================ // Deployments History Page Component // ============================================ @@ -317,10 +382,7 @@ document.addEventListener("alpine:init", () => { } this.fetchAppStatus(); - setInterval(() => { - this.fetchAppStatus(); - this.fetchDeploymentLogs(); - }, 1000); + setInterval(() => this.fetchAppStatus(), 1000); }, async fetchAppStatus() { @@ -365,53 +427,6 @@ document.addEventListener("alpine:init", () => { } }, - async fetchDeploymentLogs() { - if (!this.currentDeploymentId || !this.isDeploying) return; - try { - const res = await fetch( - `/apps/${this.appId}/deployments/${this.currentDeploymentId}/logs`, - ); - const data = await res.json(); - - // Update the deployment card - const card = document.querySelector( - `[data-deployment-id="${this.currentDeploymentId}"]`, - ); - if (card) { - const logsContent = card.querySelector(".logs-content"); - const logsWrapper = card.querySelector(".logs-wrapper"); - const statusBadge = card.querySelector(".deployment-status"); - - if (logsContent) { - const newLogs = data.logs || "Loading..."; - // Only update if content changed to avoid unnecessary reflows - if (logsContent.textContent !== newLogs) { - logsContent.textContent = newLogs; - // Scroll after content update - use double rAF to ensure DOM is ready - if (logsWrapper) { - requestAnimationFrame(() => { - requestAnimationFrame(() => { - logsWrapper.scrollTop = logsWrapper.scrollHeight; - }); - }); - } - } - } - - if (statusBadge) { - statusBadge.className = - "deployment-status " + - Alpine.store("utils").statusBadgeClass(data.status); - statusBadge.textContent = Alpine.store("utils").statusLabel( - data.status, - ); - } - } - } catch (err) { - console.error("Logs fetch error:", err); - } - }, - submitDeploy() { this.isDeploying = true; }, diff --git a/templates/deployments.html b/templates/deployments.html index 4b8295f..3f83911 100644 --- a/templates/deployments.html +++ b/templates/deployments.html @@ -27,7 +27,12 @@