/** * upaas - Deployment Components * * Deployment card (individual deployment log viewer) and * deployments history page (list of all deployments). */ 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, _autoScroll: true, init() { // Read initial logs from script tag (avoids escaping issues) const initialLogsEl = this.$el.querySelector(".initial-logs"); this.logs = initialLogsEl?.dataset.logs || "Loading..."; // Set up scroll tracking this.$nextTick(() => { const wrapper = this.$refs.logsWrapper; if (wrapper) { wrapper.addEventListener('scroll', () => { this._autoScroll = Alpine.store("utils").isScrolledToBottom(wrapper); }, { passive: true }); } }); // 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(); const newLogs = data.logs || "No logs available"; const logsChanged = newLogs !== this.logs; this.logs = newLogs; this.status = data.status; // Scroll to bottom only when content changes AND user hasn't scrolled up if (logsChanged && this._autoScroll) { 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 // ============================================ Alpine.data("deploymentsPage", (config) => ({ appId: config.appId, currentDeploymentId: null, isDeploying: false, init() { // Check for in-progress deployments on page load const inProgressCard = document.querySelector( '[data-status="building"], [data-status="deploying"]', ); if (inProgressCard) { this.currentDeploymentId = parseInt( inProgressCard.getAttribute("data-deployment-id"), 10, ); this.isDeploying = true; } this.fetchAppStatus(); this._scheduleStatusPoll(); }, _statusPollTimer: null, _scheduleStatusPoll() { if (this._statusPollTimer) clearTimeout(this._statusPollTimer); const interval = this.isDeploying ? 1000 : 10000; this._statusPollTimer = setTimeout(() => { this.fetchAppStatus(); this._scheduleStatusPoll(); }, interval); }, async fetchAppStatus() { try { const res = await fetch(`/apps/${this.appId}/status`); const data = await res.json(); // Use deployment status, not app status - it's more reliable during transitions const deploying = Alpine.store("utils").isDeploying( data.latestDeploymentStatus, ); // Detect new deployment if ( data.latestDeploymentID && data.latestDeploymentID !== this.currentDeploymentId ) { // Check if we have a card for this deployment const hasCard = document.querySelector( `[data-deployment-id="${data.latestDeploymentID}"]`, ); if (deploying && !hasCard) { // New deployment started but no card exists - reload to show it window.location.reload(); return; } this.currentDeploymentId = data.latestDeploymentID; } // Update deploying state based on latest deployment status if (deploying && !this.isDeploying) { this.isDeploying = true; this._scheduleStatusPoll(); // Switch to fast polling } else if (!deploying && this.isDeploying) { // Deployment finished - reload to show final state this.isDeploying = false; window.location.reload(); } } catch (err) { console.error("Status fetch error:", err); } }, submitDeploy() { this.isDeploying = true; }, formatTime(isoTime) { return Alpine.store("utils").formatRelativeTime(isoTime); }, })); });