From 3a18221eea90213dbacd8472847174a7c3ce1721 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 15 Feb 2026 22:00:10 -0800 Subject: [PATCH] perf: adaptive polling intervals for frontend (closes #43) - appDetail: poll every 1s during active deployments, 10s when idle - deploymentsPage: same adaptive polling for status checks - Skip fetching container/build logs when panes are not visible - Use setTimeout chains instead of setInterval for dynamic intervals --- static/js/app.js | 48 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index cd567a9..4829867 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -185,11 +185,12 @@ document.addEventListener("alpine:init", () => { // 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(); - setInterval(() => this.fetchAll(), 1000); + this._schedulePoll(); // Set up scroll listeners after DOM is ready this.$nextTick(() => { @@ -198,6 +199,15 @@ document.addEventListener("alpine:init", () => { }); }, + _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', () => { @@ -207,18 +217,36 @@ document.addEventListener("alpine:init", () => { fetchAll() { this.fetchAppStatus(); - this.fetchContainerLogs(); - this.fetchBuildLogs(); + // 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 @@ -429,7 +457,18 @@ document.addEventListener("alpine:init", () => { } this.fetchAppStatus(); - setInterval(() => this.fetchAppStatus(), 1000); + 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() { @@ -464,6 +503,7 @@ document.addEventListener("alpine:init", () => { // 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;