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
This commit is contained in:
user 2026-02-15 22:00:10 -08:00
parent e9bf63d18b
commit 3a18221eea

View File

@ -185,11 +185,12 @@ document.addEventListener("alpine:init", () => {
// Track whether user wants auto-scroll (per log pane) // Track whether user wants auto-scroll (per log pane)
_containerAutoScroll: true, _containerAutoScroll: true,
_buildAutoScroll: true, _buildAutoScroll: true,
_pollTimer: null,
init() { init() {
this.deploying = Alpine.store("utils").isDeploying(this.appStatus); this.deploying = Alpine.store("utils").isDeploying(this.appStatus);
this.fetchAll(); this.fetchAll();
setInterval(() => this.fetchAll(), 1000); this._schedulePoll();
// Set up scroll listeners after DOM is ready // Set up scroll listeners after DOM is ready
this.$nextTick(() => { 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) { _initScrollTracking(el, flag) {
if (!el) return; if (!el) return;
el.addEventListener('scroll', () => { el.addEventListener('scroll', () => {
@ -207,18 +217,36 @@ document.addEventListener("alpine:init", () => {
fetchAll() { fetchAll() {
this.fetchAppStatus(); this.fetchAppStatus();
this.fetchContainerLogs(); // Only fetch logs when the respective pane is visible
this.fetchBuildLogs(); 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(); 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() { async fetchAppStatus() {
try { try {
const res = await fetch(`/apps/${this.appId}/status`); const res = await fetch(`/apps/${this.appId}/status`);
const data = await res.json(); const data = await res.json();
const wasDeploying = this.deploying;
this.appStatus = data.status; this.appStatus = data.status;
this.deploying = Alpine.store("utils").isDeploying(data.status); this.deploying = Alpine.store("utils").isDeploying(data.status);
// Re-schedule polling when deployment state changes
if (this.deploying !== wasDeploying) {
this._schedulePoll();
}
if ( if (
data.latestDeploymentID && data.latestDeploymentID &&
data.latestDeploymentID !== this.currentDeploymentId data.latestDeploymentID !== this.currentDeploymentId
@ -429,7 +457,18 @@ document.addEventListener("alpine:init", () => {
} }
this.fetchAppStatus(); 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() { async fetchAppStatus() {
@ -464,6 +503,7 @@ document.addEventListener("alpine:init", () => {
// Update deploying state based on latest deployment status // Update deploying state based on latest deployment status
if (deploying && !this.isDeploying) { if (deploying && !this.isDeploying) {
this.isDeploying = true; this.isDeploying = true;
this._scheduleStatusPoll(); // Switch to fast polling
} else if (!deploying && this.isDeploying) { } else if (!deploying && this.isDeploying) {
// Deployment finished - reload to show final state // Deployment finished - reload to show final state
this.isDeploying = false; this.isDeploying = false;