Fix build logs overflow by using Alpine.js reactive bindings

Replaced manual DOM manipulation with a deploymentCard Alpine component
that uses x-text bindings and x-ref for proper scrolling, matching the
working container logs implementation.
This commit is contained in:
2026-01-08 10:21:16 -08:00
parent aaa55fd153
commit ee4afbde80
2 changed files with 77 additions and 56 deletions

View File

@@ -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;
},