/** * upaas - App Detail Page Component * * Handles the single-app view: status polling, container logs, * build logs, and recent deployments list. */ document.addEventListener("alpine:init", () => { Alpine.data("appDetail", (config) => ({ appId: config.appId, currentDeploymentId: config.initialDeploymentId, appStatus: config.initialStatus || "unknown", containerLogs: "Loading container logs...", containerStatus: "unknown", buildLogs: config.initialDeploymentId ? "Loading build logs..." : "No deployments yet", buildStatus: config.initialBuildStatus || "unknown", showBuildLogs: !!config.initialDeploymentId, deploying: false, deployments: [], // Track whether user wants auto-scroll (per log pane) _containerAutoScroll: true, _buildAutoScroll: true, _pollTimer: null, init() { this.deploying = Alpine.store("utils").isDeploying(this.appStatus); this.fetchAll(); this._schedulePoll(); // Set up scroll listeners after DOM is ready this.$nextTick(() => { this._initScrollTracking(this.$refs.containerLogsWrapper, '_containerAutoScroll'); this._initScrollTracking(this.$refs.buildLogsWrapper, '_buildAutoScroll'); }); }, _schedulePoll() { if (this._pollTimer) clearTimeout(this._pollTimer); const interval = Alpine.store("utils").isDeploying(this.appStatus) ? 1000 : 10000; this._pollTimer = setTimeout(() => { this.fetchAll(); this._schedulePoll(); }, interval); }, _initScrollTracking(el, flag) { if (!el) return; el.addEventListener('scroll', () => { this[flag] = Alpine.store("utils").isScrolledToBottom(el); }, { passive: true }); }, fetchAll() { this.fetchAppStatus(); // Only fetch logs when the respective pane is visible if (this.$refs.containerLogsWrapper && this._isElementVisible(this.$refs.containerLogsWrapper)) { this.fetchContainerLogs(); } if (this.showBuildLogs && this.$refs.buildLogsWrapper && this._isElementVisible(this.$refs.buildLogsWrapper)) { this.fetchBuildLogs(); } this.fetchRecentDeployments(); }, _isElementVisible(el) { if (!el) return false; // Check if element is in viewport (roughly) const rect = el.getBoundingClientRect(); return rect.bottom > 0 && rect.top < window.innerHeight; }, async fetchAppStatus() { try { const res = await fetch(`/apps/${this.appId}/status`); const data = await res.json(); const wasDeploying = this.deploying; this.appStatus = data.status; this.deploying = Alpine.store("utils").isDeploying(data.status); // Re-schedule polling when deployment state changes if (this.deploying !== wasDeploying) { this._schedulePoll(); } if ( data.latestDeploymentID && data.latestDeploymentID !== this.currentDeploymentId ) { this.currentDeploymentId = data.latestDeploymentID; this.showBuildLogs = true; this.fetchBuildLogs(); } } catch (err) { console.error("Status fetch error:", err); } }, async fetchContainerLogs() { try { const res = await fetch(`/apps/${this.appId}/container-logs`); const data = await res.json(); const newLogs = data.logs || "No logs available"; const changed = newLogs !== this.containerLogs; this.containerLogs = newLogs; this.containerStatus = data.status; if (changed && this._containerAutoScroll) { this.$nextTick(() => { Alpine.store("utils").scrollToBottom(this.$refs.containerLogsWrapper); }); } } catch (err) { this.containerLogs = "Failed to fetch logs"; } }, async fetchBuildLogs() { if (!this.currentDeploymentId) return; try { const res = await fetch( `/apps/${this.appId}/deployments/${this.currentDeploymentId}/logs`, ); const data = await res.json(); const newLogs = data.logs || "No build logs available"; const changed = newLogs !== this.buildLogs; this.buildLogs = newLogs; this.buildStatus = data.status; if (changed && this._buildAutoScroll) { this.$nextTick(() => { Alpine.store("utils").scrollToBottom(this.$refs.buildLogsWrapper); }); } } catch (err) { this.buildLogs = "Failed to fetch logs"; } }, async fetchRecentDeployments() { try { const res = await fetch(`/apps/${this.appId}/recent-deployments`); const data = await res.json(); this.deployments = data.deployments || []; } catch (err) { console.error("Deployments fetch error:", err); } }, submitDeploy() { this.deploying = true; }, get statusBadgeClass() { return Alpine.store("utils").statusBadgeClass(this.appStatus); }, get statusLabel() { return Alpine.store("utils").statusLabel(this.appStatus); }, get containerStatusBadgeClass() { return ( Alpine.store("utils").statusBadgeClass(this.containerStatus) + " text-xs" ); }, get containerStatusLabel() { return Alpine.store("utils").statusLabel(this.containerStatus); }, get buildStatusBadgeClass() { return ( Alpine.store("utils").statusBadgeClass(this.buildStatus) + " text-xs" ); }, get buildStatusLabel() { return Alpine.store("utils").statusLabel(this.buildStatus); }, deploymentStatusClass(status) { return Alpine.store("utils").statusBadgeClass(status); }, deploymentStatusLabel(status) { return Alpine.store("utils").statusLabel(status); }, formatTime(isoTime) { return Alpine.store("utils").formatRelativeTime(isoTime); }, })); });