/** * upaas - Frontend JavaScript with Alpine.js */ document.addEventListener("alpine:init", () => { // ============================================ // Global Utilities Store // ============================================ Alpine.store("utils", { /** * Format a date string as relative time (e.g., "5 minutes ago") */ formatRelativeTime(dateStr) { if (!dateStr) return ""; const date = new Date(dateStr); const now = new Date(); const diffMs = now - date; const diffSec = Math.floor(diffMs / 1000); const diffMin = Math.floor(diffSec / 60); const diffHour = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHour / 24); if (diffSec < 60) return "just now"; if (diffMin < 60) return ( diffMin + (diffMin === 1 ? " minute ago" : " minutes ago") ); if (diffHour < 24) return diffHour + (diffHour === 1 ? " hour ago" : " hours ago"); if (diffDay < 7) return diffDay + (diffDay === 1 ? " day ago" : " days ago"); return date.toLocaleDateString(); }, /** * Get the badge class for a given status */ statusBadgeClass(status) { if (status === "running" || status === "success") return "badge-success"; if (status === "building" || status === "deploying") return "badge-warning"; if (status === "failed" || status === "error") return "badge-error"; return "badge-neutral"; }, /** * Format status for display (capitalize first letter) */ statusLabel(status) { if (!status) return ""; return status.charAt(0).toUpperCase() + status.slice(1); }, /** * Check if status indicates active deployment */ isDeploying(status) { return status === "building" || status === "deploying"; }, /** * Scroll an element to the bottom */ scrollToBottom(el) { if (el) { // Use requestAnimationFrame for smoother scrolling after DOM update requestAnimationFrame(() => { el.scrollTop = el.scrollHeight; }); } }, /** * Copy text to clipboard */ async copyToClipboard(text, button) { try { await navigator.clipboard.writeText(text); return true; } catch (err) { // Fallback for older browsers const textArea = document.createElement("textarea"); textArea.value = text; textArea.style.position = "fixed"; textArea.style.left = "-9999px"; document.body.appendChild(textArea); textArea.select(); try { document.execCommand("copy"); document.body.removeChild(textArea); return true; } catch (e) { document.body.removeChild(textArea); return false; } } }, }); // ============================================ // Copy Button Component // ============================================ Alpine.data("copyButton", (targetId) => ({ copied: false, async copy() { const target = document.getElementById(targetId); if (!target) return; const text = target.textContent || target.value; const success = await Alpine.store("utils").copyToClipboard(text); if (success) { this.copied = true; setTimeout(() => { this.copied = false; }, 2000); } }, })); // ============================================ // Confirm Action Component // ============================================ Alpine.data("confirmAction", (message) => ({ confirm(event) { if (!window.confirm(message)) { event.preventDefault(); } }, })); // ============================================ // Auto-dismiss Alert Component // ============================================ Alpine.data("autoDismiss", (delay = 5000) => ({ show: true, init() { setTimeout(() => { this.dismiss(); }, delay); }, dismiss() { this.show = false; setTimeout(() => { this.$el.remove(); }, 300); }, })); // ============================================ // Relative Time Component // ============================================ Alpine.data("relativeTime", (isoTime) => ({ display: "", init() { this.update(); // Update every minute setInterval(() => this.update(), 60000); }, update() { this.display = Alpine.store("utils").formatRelativeTime(isoTime); }, })); // ============================================ // App Detail Page Component // ============================================ 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: [], init() { this.deploying = Alpine.store("utils").isDeploying(this.appStatus); this.fetchAll(); setInterval(() => this.fetchAll(), 1000); }, fetchAll() { this.fetchAppStatus(); this.fetchContainerLogs(); this.fetchBuildLogs(); this.fetchRecentDeployments(); }, async fetchAppStatus() { try { const res = await fetch(`/apps/${this.appId}/status`); const data = await res.json(); this.appStatus = data.status; this.deploying = Alpine.store("utils").isDeploying(data.status); 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(); this.containerLogs = data.logs || "No logs available"; this.containerStatus = data.status; 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(); this.buildLogs = data.logs || "No build logs available"; this.buildStatus = data.status; 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); }, })); // ============================================ // Deployments History Page Component // ============================================ Alpine.data("deploymentsPage", (config) => ({ appId: config.appId, currentDeploymentId: null, isDeploying: false, init() { // Check for in-progress deployments on page load const inProgressCard = document.querySelector( '[data-status="building"], [data-status="deploying"]', ); if (inProgressCard) { this.currentDeploymentId = parseInt( inProgressCard.getAttribute("data-deployment-id"), 10, ); this.isDeploying = true; } this.fetchAppStatus(); setInterval(() => { this.fetchAppStatus(); this.fetchDeploymentLogs(); }, 1000); }, async fetchAppStatus() { try { const res = await fetch(`/apps/${this.appId}/status`); const data = await res.json(); const deploying = Alpine.store("utils").isDeploying( data.status, ); if ( data.latestDeploymentID && data.latestDeploymentID !== this.currentDeploymentId ) { this.currentDeploymentId = data.latestDeploymentID; if (deploying) { this.isDeploying = true; } } // Reload page when deployment finishes if (!deploying && this.isDeploying) { this.isDeploying = false; window.location.reload(); } } catch (err) { console.error("Status fetch error:", err); } }, 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) 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; }, 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 // ============================================ window.upaas = { // These are kept for backwards compatibility but templates should use Alpine.js formatRelativeTime(dateStr) { if (!dateStr) return ""; const date = new Date(dateStr); const now = new Date(); const diffMs = now - date; const diffSec = Math.floor(diffMs / 1000); const diffMin = Math.floor(diffSec / 60); const diffHour = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHour / 24); if (diffSec < 60) return "just now"; if (diffMin < 60) return diffMin + (diffMin === 1 ? " minute ago" : " minutes ago"); if (diffHour < 24) return diffHour + (diffHour === 1 ? " hour ago" : " hours ago"); if (diffDay < 7) return diffDay + (diffDay === 1 ? " day ago" : " days ago"); return date.toLocaleDateString(); }, // Placeholder functions - templates should migrate to Alpine.js initAppDetailPage() {}, initDeploymentsPage() {}, }; // Update relative times on page load for non-Alpine elements document.addEventListener("DOMContentLoaded", () => { document.querySelectorAll(".relative-time[data-time]").forEach((el) => { const time = el.getAttribute("data-time"); if (time) { el.textContent = window.upaas.formatRelativeTime(time); } }); });