Fix realtime build logs scrolling in deployment history
- Reload page when new deployment starts but no card exists in DOM - Only update logs content when it changes to avoid unnecessary reflows - Use double requestAnimationFrame for reliable scroll-to-bottom timing
This commit is contained in:
parent
2cbcd3d72a
commit
2b63219f66
826
static/js/app.js
826
static/js/app.js
@ -3,462 +3,474 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener("alpine:init", () => {
|
document.addEventListener("alpine:init", () => {
|
||||||
// ============================================
|
// ============================================
|
||||||
// Global Utilities Store
|
// Global Utilities Store
|
||||||
// ============================================
|
// ============================================
|
||||||
Alpine.store("utils", {
|
Alpine.store("utils", {
|
||||||
/**
|
/**
|
||||||
* Format a date string as relative time (e.g., "5 minutes ago")
|
* Format a date string as relative time (e.g., "5 minutes ago")
|
||||||
*/
|
*/
|
||||||
formatRelativeTime(dateStr) {
|
formatRelativeTime(dateStr) {
|
||||||
if (!dateStr) return "";
|
if (!dateStr) return "";
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffMs = now - date;
|
const diffMs = now - date;
|
||||||
const diffSec = Math.floor(diffMs / 1000);
|
const diffSec = Math.floor(diffMs / 1000);
|
||||||
const diffMin = Math.floor(diffSec / 60);
|
const diffMin = Math.floor(diffSec / 60);
|
||||||
const diffHour = Math.floor(diffMin / 60);
|
const diffHour = Math.floor(diffMin / 60);
|
||||||
const diffDay = Math.floor(diffHour / 24);
|
const diffDay = Math.floor(diffHour / 24);
|
||||||
|
|
||||||
if (diffSec < 60) return "just now";
|
if (diffSec < 60) return "just now";
|
||||||
if (diffMin < 60)
|
if (diffMin < 60)
|
||||||
return (
|
return diffMin + (diffMin === 1 ? " minute ago" : " minutes ago");
|
||||||
diffMin + (diffMin === 1 ? " minute ago" : " minutes ago")
|
if (diffHour < 24)
|
||||||
);
|
return diffHour + (diffHour === 1 ? " hour ago" : " hours ago");
|
||||||
if (diffHour < 24)
|
if (diffDay < 7)
|
||||||
return diffHour + (diffHour === 1 ? " hour ago" : " hours ago");
|
return diffDay + (diffDay === 1 ? " day ago" : " days ago");
|
||||||
if (diffDay < 7)
|
return date.toLocaleDateString();
|
||||||
return diffDay + (diffDay === 1 ? " day ago" : " days ago");
|
},
|
||||||
return date.toLocaleDateString();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the badge class for a given status
|
* Get the badge class for a given status
|
||||||
*/
|
*/
|
||||||
statusBadgeClass(status) {
|
statusBadgeClass(status) {
|
||||||
if (status === "running" || status === "success")
|
if (status === "running" || status === "success") return "badge-success";
|
||||||
return "badge-success";
|
if (status === "building" || status === "deploying")
|
||||||
if (status === "building" || status === "deploying")
|
return "badge-warning";
|
||||||
return "badge-warning";
|
if (status === "failed" || status === "error") return "badge-error";
|
||||||
if (status === "failed" || status === "error") return "badge-error";
|
return "badge-neutral";
|
||||||
return "badge-neutral";
|
},
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format status for display (capitalize first letter)
|
* Format status for display (capitalize first letter)
|
||||||
*/
|
*/
|
||||||
statusLabel(status) {
|
statusLabel(status) {
|
||||||
if (!status) return "";
|
if (!status) return "";
|
||||||
return status.charAt(0).toUpperCase() + status.slice(1);
|
return status.charAt(0).toUpperCase() + status.slice(1);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if status indicates active deployment
|
* Check if status indicates active deployment
|
||||||
*/
|
*/
|
||||||
isDeploying(status) {
|
isDeploying(status) {
|
||||||
return status === "building" || status === "deploying";
|
return status === "building" || status === "deploying";
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll an element to the bottom
|
* Scroll an element to the bottom
|
||||||
*/
|
*/
|
||||||
scrollToBottom(el) {
|
scrollToBottom(el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
// Use requestAnimationFrame for smoother scrolling after DOM update
|
// Use requestAnimationFrame for smoother scrolling after DOM update
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
el.scrollTop = el.scrollHeight;
|
el.scrollTop = el.scrollHeight;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy text to clipboard
|
* Copy text to clipboard
|
||||||
*/
|
*/
|
||||||
async copyToClipboard(text, button) {
|
async copyToClipboard(text, button) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Fallback for older browsers
|
// Fallback for older browsers
|
||||||
const textArea = document.createElement("textarea");
|
const textArea = document.createElement("textarea");
|
||||||
textArea.value = text;
|
textArea.value = text;
|
||||||
textArea.style.position = "fixed";
|
textArea.style.position = "fixed";
|
||||||
textArea.style.left = "-9999px";
|
textArea.style.left = "-9999px";
|
||||||
document.body.appendChild(textArea);
|
document.body.appendChild(textArea);
|
||||||
textArea.select();
|
textArea.select();
|
||||||
try {
|
try {
|
||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
document.body.removeChild(textArea);
|
document.body.removeChild(textArea);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.body.removeChild(textArea);
|
document.body.removeChild(textArea);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Copy Button Component
|
// Copy Button Component
|
||||||
// ============================================
|
// ============================================
|
||||||
Alpine.data("copyButton", (targetId) => ({
|
Alpine.data("copyButton", (targetId) => ({
|
||||||
copied: false,
|
copied: false,
|
||||||
async copy() {
|
async copy() {
|
||||||
const target = document.getElementById(targetId);
|
const target = document.getElementById(targetId);
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
const text = target.textContent || target.value;
|
const text = target.textContent || target.value;
|
||||||
const success = await Alpine.store("utils").copyToClipboard(text);
|
const success = await Alpine.store("utils").copyToClipboard(text);
|
||||||
if (success) {
|
if (success) {
|
||||||
this.copied = true;
|
this.copied = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.copied = false;
|
this.copied = false;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Confirm Action Component
|
// Confirm Action Component
|
||||||
// ============================================
|
// ============================================
|
||||||
Alpine.data("confirmAction", (message) => ({
|
Alpine.data("confirmAction", (message) => ({
|
||||||
confirm(event) {
|
confirm(event) {
|
||||||
if (!window.confirm(message)) {
|
if (!window.confirm(message)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Auto-dismiss Alert Component
|
// Auto-dismiss Alert Component
|
||||||
// ============================================
|
// ============================================
|
||||||
Alpine.data("autoDismiss", (delay = 5000) => ({
|
Alpine.data("autoDismiss", (delay = 5000) => ({
|
||||||
show: true,
|
show: true,
|
||||||
init() {
|
init() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.dismiss();
|
this.dismiss();
|
||||||
}, delay);
|
}, delay);
|
||||||
},
|
},
|
||||||
dismiss() {
|
dismiss() {
|
||||||
this.show = false;
|
this.show = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$el.remove();
|
this.$el.remove();
|
||||||
}, 300);
|
}, 300);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Relative Time Component
|
// Relative Time Component
|
||||||
// ============================================
|
// ============================================
|
||||||
Alpine.data("relativeTime", (isoTime) => ({
|
Alpine.data("relativeTime", (isoTime) => ({
|
||||||
display: "",
|
display: "",
|
||||||
init() {
|
init() {
|
||||||
this.update();
|
this.update();
|
||||||
// Update every minute
|
// Update every minute
|
||||||
setInterval(() => this.update(), 60000);
|
setInterval(() => this.update(), 60000);
|
||||||
},
|
},
|
||||||
update() {
|
update() {
|
||||||
this.display = Alpine.store("utils").formatRelativeTime(isoTime);
|
this.display = Alpine.store("utils").formatRelativeTime(isoTime);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// App Detail Page Component
|
// App Detail Page Component
|
||||||
// ============================================
|
// ============================================
|
||||||
Alpine.data("appDetail", (config) => ({
|
Alpine.data("appDetail", (config) => ({
|
||||||
appId: config.appId,
|
appId: config.appId,
|
||||||
currentDeploymentId: config.initialDeploymentId,
|
currentDeploymentId: config.initialDeploymentId,
|
||||||
appStatus: config.initialStatus || "unknown",
|
appStatus: config.initialStatus || "unknown",
|
||||||
containerLogs: "Loading container logs...",
|
containerLogs: "Loading container logs...",
|
||||||
containerStatus: "unknown",
|
containerStatus: "unknown",
|
||||||
buildLogs: config.initialDeploymentId
|
buildLogs: config.initialDeploymentId
|
||||||
? "Loading build logs..."
|
? "Loading build logs..."
|
||||||
: "No deployments yet",
|
: "No deployments yet",
|
||||||
buildStatus: config.initialBuildStatus || "unknown",
|
buildStatus: config.initialBuildStatus || "unknown",
|
||||||
showBuildLogs: !!config.initialDeploymentId,
|
showBuildLogs: !!config.initialDeploymentId,
|
||||||
deploying: false,
|
deploying: false,
|
||||||
deployments: [],
|
deployments: [],
|
||||||
|
|
||||||
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);
|
setInterval(() => this.fetchAll(), 1000);
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchAll() {
|
fetchAll() {
|
||||||
this.fetchAppStatus();
|
this.fetchAppStatus();
|
||||||
this.fetchContainerLogs();
|
this.fetchContainerLogs();
|
||||||
this.fetchBuildLogs();
|
this.fetchBuildLogs();
|
||||||
this.fetchRecentDeployments();
|
this.fetchRecentDeployments();
|
||||||
},
|
},
|
||||||
|
|
||||||
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();
|
||||||
this.appStatus = data.status;
|
this.appStatus = data.status;
|
||||||
this.deploying = Alpine.store("utils").isDeploying(data.status);
|
this.deploying = Alpine.store("utils").isDeploying(data.status);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
data.latestDeploymentID &&
|
data.latestDeploymentID &&
|
||||||
data.latestDeploymentID !== this.currentDeploymentId
|
data.latestDeploymentID !== this.currentDeploymentId
|
||||||
) {
|
) {
|
||||||
this.currentDeploymentId = data.latestDeploymentID;
|
this.currentDeploymentId = data.latestDeploymentID;
|
||||||
this.showBuildLogs = true;
|
this.showBuildLogs = true;
|
||||||
this.fetchBuildLogs();
|
this.fetchBuildLogs();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Status fetch error:", err);
|
console.error("Status fetch error:", err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchContainerLogs() {
|
async fetchContainerLogs() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/apps/${this.appId}/container-logs`);
|
const res = await fetch(`/apps/${this.appId}/container-logs`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
this.containerLogs = data.logs || "No logs available";
|
this.containerLogs = data.logs || "No logs available";
|
||||||
this.containerStatus = data.status;
|
this.containerStatus = data.status;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
Alpine.store("utils").scrollToBottom(
|
Alpine.store("utils").scrollToBottom(this.$refs.containerLogsWrapper);
|
||||||
this.$refs.containerLogsWrapper,
|
});
|
||||||
);
|
} catch (err) {
|
||||||
});
|
this.containerLogs = "Failed to fetch logs";
|
||||||
} catch (err) {
|
}
|
||||||
this.containerLogs = "Failed to fetch logs";
|
},
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchBuildLogs() {
|
async fetchBuildLogs() {
|
||||||
if (!this.currentDeploymentId) return;
|
if (!this.currentDeploymentId) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/apps/${this.appId}/deployments/${this.currentDeploymentId}/logs`,
|
`/apps/${this.appId}/deployments/${this.currentDeploymentId}/logs`,
|
||||||
);
|
);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
this.buildLogs = data.logs || "No build logs available";
|
this.buildLogs = data.logs || "No build logs available";
|
||||||
this.buildStatus = data.status;
|
this.buildStatus = data.status;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
Alpine.store("utils").scrollToBottom(
|
Alpine.store("utils").scrollToBottom(this.$refs.buildLogsWrapper);
|
||||||
this.$refs.buildLogsWrapper,
|
});
|
||||||
);
|
} catch (err) {
|
||||||
});
|
this.buildLogs = "Failed to fetch logs";
|
||||||
} catch (err) {
|
}
|
||||||
this.buildLogs = "Failed to fetch logs";
|
},
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchRecentDeployments() {
|
async fetchRecentDeployments() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(`/apps/${this.appId}/recent-deployments`);
|
||||||
`/apps/${this.appId}/recent-deployments`,
|
const data = await res.json();
|
||||||
);
|
this.deployments = data.deployments || [];
|
||||||
const data = await res.json();
|
} catch (err) {
|
||||||
this.deployments = data.deployments || [];
|
console.error("Deployments fetch error:", err);
|
||||||
} catch (err) {
|
}
|
||||||
console.error("Deployments fetch error:", err);
|
},
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
submitDeploy() {
|
submitDeploy() {
|
||||||
this.deploying = true;
|
this.deploying = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
get statusBadgeClass() {
|
get statusBadgeClass() {
|
||||||
return Alpine.store("utils").statusBadgeClass(this.appStatus);
|
return Alpine.store("utils").statusBadgeClass(this.appStatus);
|
||||||
},
|
},
|
||||||
|
|
||||||
get statusLabel() {
|
get statusLabel() {
|
||||||
return Alpine.store("utils").statusLabel(this.appStatus);
|
return Alpine.store("utils").statusLabel(this.appStatus);
|
||||||
},
|
},
|
||||||
|
|
||||||
get containerStatusBadgeClass() {
|
get containerStatusBadgeClass() {
|
||||||
return (
|
return (
|
||||||
Alpine.store("utils").statusBadgeClass(this.containerStatus) +
|
Alpine.store("utils").statusBadgeClass(this.containerStatus) +
|
||||||
" text-xs"
|
" text-xs"
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
get containerStatusLabel() {
|
get containerStatusLabel() {
|
||||||
return Alpine.store("utils").statusLabel(this.containerStatus);
|
return Alpine.store("utils").statusLabel(this.containerStatus);
|
||||||
},
|
},
|
||||||
|
|
||||||
get buildStatusBadgeClass() {
|
get buildStatusBadgeClass() {
|
||||||
return (
|
return (
|
||||||
Alpine.store("utils").statusBadgeClass(this.buildStatus) +
|
Alpine.store("utils").statusBadgeClass(this.buildStatus) + " text-xs"
|
||||||
" text-xs"
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
|
|
||||||
get buildStatusLabel() {
|
get buildStatusLabel() {
|
||||||
return Alpine.store("utils").statusLabel(this.buildStatus);
|
return Alpine.store("utils").statusLabel(this.buildStatus);
|
||||||
},
|
},
|
||||||
|
|
||||||
deploymentStatusClass(status) {
|
deploymentStatusClass(status) {
|
||||||
return Alpine.store("utils").statusBadgeClass(status);
|
return Alpine.store("utils").statusBadgeClass(status);
|
||||||
},
|
},
|
||||||
|
|
||||||
deploymentStatusLabel(status) {
|
deploymentStatusLabel(status) {
|
||||||
return Alpine.store("utils").statusLabel(status);
|
return Alpine.store("utils").statusLabel(status);
|
||||||
},
|
},
|
||||||
|
|
||||||
formatTime(isoTime) {
|
formatTime(isoTime) {
|
||||||
return Alpine.store("utils").formatRelativeTime(isoTime);
|
return Alpine.store("utils").formatRelativeTime(isoTime);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Deployments History Page Component
|
// Deployments History Page Component
|
||||||
// ============================================
|
// ============================================
|
||||||
Alpine.data("deploymentsPage", (config) => ({
|
Alpine.data("deploymentsPage", (config) => ({
|
||||||
appId: config.appId,
|
appId: config.appId,
|
||||||
currentDeploymentId: null,
|
currentDeploymentId: null,
|
||||||
isDeploying: false,
|
isDeploying: false,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// Check for in-progress deployments on page load
|
// Check for in-progress deployments on page load
|
||||||
const inProgressCard = document.querySelector(
|
const inProgressCard = document.querySelector(
|
||||||
'[data-status="building"], [data-status="deploying"]',
|
'[data-status="building"], [data-status="deploying"]',
|
||||||
);
|
);
|
||||||
if (inProgressCard) {
|
if (inProgressCard) {
|
||||||
this.currentDeploymentId = parseInt(
|
this.currentDeploymentId = parseInt(
|
||||||
inProgressCard.getAttribute("data-deployment-id"),
|
inProgressCard.getAttribute("data-deployment-id"),
|
||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
this.isDeploying = true;
|
this.isDeploying = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fetchAppStatus();
|
this.fetchAppStatus();
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
this.fetchAppStatus();
|
this.fetchAppStatus();
|
||||||
this.fetchDeploymentLogs();
|
this.fetchDeploymentLogs();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
|
|
||||||
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 deploying = Alpine.store("utils").isDeploying(
|
const deploying = Alpine.store("utils").isDeploying(data.status);
|
||||||
data.status,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
// Detect new deployment
|
||||||
data.latestDeploymentID &&
|
if (
|
||||||
data.latestDeploymentID !== this.currentDeploymentId
|
data.latestDeploymentID &&
|
||||||
) {
|
data.latestDeploymentID !== this.currentDeploymentId
|
||||||
this.currentDeploymentId = data.latestDeploymentID;
|
) {
|
||||||
if (deploying) {
|
// Check if we have a card for this deployment
|
||||||
this.isDeploying = true;
|
const hasCard = document.querySelector(
|
||||||
}
|
`[data-deployment-id="${data.latestDeploymentID}"]`,
|
||||||
}
|
);
|
||||||
|
|
||||||
// Reload page when deployment finishes
|
if (deploying && !hasCard) {
|
||||||
if (!deploying && this.isDeploying) {
|
// New deployment started but no card exists - reload to show it
|
||||||
this.isDeploying = false;
|
window.location.reload();
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Status fetch error:", err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchDeploymentLogs() {
|
return;
|
||||||
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
|
this.currentDeploymentId = data.latestDeploymentID;
|
||||||
const card = document.querySelector(
|
if (deploying) {
|
||||||
`[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)
|
|
||||||
logsContent.textContent = data.logs || "Loading...";
|
|
||||||
if (logsWrapper)
|
|
||||||
Alpine.store("utils").scrollToBottom(logsWrapper);
|
|
||||||
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;
|
this.isDeploying = true;
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
|
||||||
formatTime(isoTime) {
|
// Reload page when deployment finishes
|
||||||
return Alpine.store("utils").formatRelativeTime(isoTime);
|
if (!deploying && this.isDeploying) {
|
||||||
},
|
this.isDeploying = false;
|
||||||
}));
|
window.location.reload();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Status fetch error:", err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// ============================================
|
async fetchDeploymentLogs() {
|
||||||
// Dashboard Page - Relative Time Updates
|
if (!this.currentDeploymentId || !this.isDeploying) return;
|
||||||
// ============================================
|
try {
|
||||||
Alpine.data("dashboard", () => ({
|
const res = await fetch(
|
||||||
init() {
|
`/apps/${this.appId}/deployments/${this.currentDeploymentId}/logs`,
|
||||||
// Update relative times every minute
|
);
|
||||||
setInterval(() => {
|
const data = await res.json();
|
||||||
this.$el.querySelectorAll("[data-time]").forEach((el) => {
|
|
||||||
const time = el.getAttribute("data-time");
|
// Update the deployment card
|
||||||
if (time) {
|
const card = document.querySelector(
|
||||||
el.textContent =
|
`[data-deployment-id="${this.currentDeploymentId}"]`,
|
||||||
Alpine.store("utils").formatRelativeTime(time);
|
);
|
||||||
}
|
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;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}, 60000);
|
}
|
||||||
},
|
}
|
||||||
}));
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTime(isoTime) {
|
||||||
|
return Alpine.store("utils").formatRelativeTime(isoTime);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Dashboard Page - Relative Time Updates
|
||||||
|
// ============================================
|
||||||
|
Alpine.data("dashboard", () => ({
|
||||||
|
init() {
|
||||||
|
// Update relative times every minute
|
||||||
|
setInterval(() => {
|
||||||
|
this.$el.querySelectorAll("[data-time]").forEach((el) => {
|
||||||
|
const time = el.getAttribute("data-time");
|
||||||
|
if (time) {
|
||||||
|
el.textContent = Alpine.store("utils").formatRelativeTime(time);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 60000);
|
||||||
|
},
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Legacy support - expose utilities globally
|
// Legacy support - expose utilities globally
|
||||||
// ============================================
|
// ============================================
|
||||||
window.upaas = {
|
window.upaas = {
|
||||||
// These are kept for backwards compatibility but templates should use Alpine.js
|
// These are kept for backwards compatibility but templates should use Alpine.js
|
||||||
formatRelativeTime(dateStr) {
|
formatRelativeTime(dateStr) {
|
||||||
if (!dateStr) return "";
|
if (!dateStr) return "";
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffMs = now - date;
|
const diffMs = now - date;
|
||||||
const diffSec = Math.floor(diffMs / 1000);
|
const diffSec = Math.floor(diffMs / 1000);
|
||||||
const diffMin = Math.floor(diffSec / 60);
|
const diffMin = Math.floor(diffSec / 60);
|
||||||
const diffHour = Math.floor(diffMin / 60);
|
const diffHour = Math.floor(diffMin / 60);
|
||||||
const diffDay = Math.floor(diffHour / 24);
|
const diffDay = Math.floor(diffHour / 24);
|
||||||
|
|
||||||
if (diffSec < 60) return "just now";
|
if (diffSec < 60) return "just now";
|
||||||
if (diffMin < 60)
|
if (diffMin < 60)
|
||||||
return diffMin + (diffMin === 1 ? " minute ago" : " minutes ago");
|
return diffMin + (diffMin === 1 ? " minute ago" : " minutes ago");
|
||||||
if (diffHour < 24)
|
if (diffHour < 24)
|
||||||
return diffHour + (diffHour === 1 ? " hour ago" : " hours ago");
|
return diffHour + (diffHour === 1 ? " hour ago" : " hours ago");
|
||||||
if (diffDay < 7)
|
if (diffDay < 7)
|
||||||
return diffDay + (diffDay === 1 ? " day ago" : " days ago");
|
return diffDay + (diffDay === 1 ? " day ago" : " days ago");
|
||||||
return date.toLocaleDateString();
|
return date.toLocaleDateString();
|
||||||
},
|
},
|
||||||
// Placeholder functions - templates should migrate to Alpine.js
|
// Placeholder functions - templates should migrate to Alpine.js
|
||||||
initAppDetailPage() {},
|
initAppDetailPage() {},
|
||||||
initDeploymentsPage() {},
|
initDeploymentsPage() {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update relative times on page load for non-Alpine elements
|
// Update relative times on page load for non-Alpine elements
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
document.querySelectorAll(".relative-time[data-time]").forEach((el) => {
|
document.querySelectorAll(".relative-time[data-time]").forEach((el) => {
|
||||||
const time = el.getAttribute("data-time");
|
const time = el.getAttribute("data-time");
|
||||||
if (time) {
|
if (time) {
|
||||||
el.textContent = window.upaas.formatRelativeTime(time);
|
el.textContent = window.upaas.formatRelativeTime(time);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user