/** * upaas - Frontend JavaScript utilities * Vanilla JS, no dependencies */ (function () { "use strict"; /** * Copy text to clipboard * @param {string} text - Text to copy * @param {HTMLElement} button - Button element to update feedback */ function copyToClipboard(text, button) { const originalText = button.textContent; const originalTitle = button.getAttribute("title"); navigator.clipboard .writeText(text) .then(function () { // Success feedback button.textContent = "Copied!"; button.classList.add("text-success-500"); setTimeout(function () { button.textContent = originalText; button.classList.remove("text-success-500"); if (originalTitle) { button.setAttribute("title", originalTitle); } }, 2000); }) .catch(function (err) { // Fallback for older browsers console.error("Failed to copy:", err); // Try fallback method var 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"); button.textContent = "Copied!"; setTimeout(function () { button.textContent = originalText; }, 2000); } catch (e) { button.textContent = "Failed"; setTimeout(function () { button.textContent = originalText; }, 2000); } document.body.removeChild(textArea); }); } /** * Initialize copy buttons * Looks for elements with data-copy attribute */ function initCopyButtons() { var copyButtons = document.querySelectorAll("[data-copy]"); copyButtons.forEach(function (button) { button.addEventListener("click", function (e) { e.preventDefault(); var text = button.getAttribute("data-copy"); copyToClipboard(text, button); }); }); // Also handle buttons that copy content from a sibling element var copyTargetButtons = document.querySelectorAll("[data-copy-target]"); copyTargetButtons.forEach(function (button) { button.addEventListener("click", function (e) { e.preventDefault(); var targetId = button.getAttribute("data-copy-target"); var target = document.getElementById(targetId); if (target) { var text = target.textContent || target.value; copyToClipboard(text, button); } }); }); } /** * Confirm destructive actions * Looks for forms with data-confirm attribute */ function initConfirmations() { var confirmForms = document.querySelectorAll("form[data-confirm]"); confirmForms.forEach(function (form) { form.addEventListener("submit", function (e) { var message = form.getAttribute("data-confirm"); if (!confirm(message)) { e.preventDefault(); } }); }); // Also handle buttons with data-confirm var confirmButtons = document.querySelectorAll("button[data-confirm]"); confirmButtons.forEach(function (button) { button.addEventListener("click", function (e) { var message = button.getAttribute("data-confirm"); if (!confirm(message)) { e.preventDefault(); } }); }); } /** * Toggle visibility of elements * Looks for buttons with data-toggle attribute */ function initToggles() { var toggleButtons = document.querySelectorAll("[data-toggle]"); toggleButtons.forEach(function (button) { button.addEventListener("click", function (e) { e.preventDefault(); var targetId = button.getAttribute("data-toggle"); var target = document.getElementById(targetId); if (target) { target.classList.toggle("hidden"); // Update button text if data-toggle-text is provided var toggleText = button.getAttribute("data-toggle-text"); if (toggleText) { var currentText = button.textContent; button.textContent = toggleText; button.setAttribute("data-toggle-text", currentText); } } }); }); } /** * Auto-dismiss alerts after a delay * Looks for elements with data-auto-dismiss attribute */ function initAutoDismiss() { var dismissElements = document.querySelectorAll("[data-auto-dismiss]"); dismissElements.forEach(function (element) { var delay = parseInt(element.getAttribute("data-auto-dismiss"), 10) || 5000; setTimeout(function () { element.style.transition = "opacity 0.3s ease-out"; element.style.opacity = "0"; setTimeout(function () { element.remove(); }, 300); }, delay); }); } /** * Manual dismiss for alerts * Looks for buttons with data-dismiss attribute */ function initDismissButtons() { var dismissButtons = document.querySelectorAll("[data-dismiss]"); dismissButtons.forEach(function (button) { button.addEventListener("click", function (e) { e.preventDefault(); var targetId = button.getAttribute("data-dismiss"); var target = targetId ? document.getElementById(targetId) : button.closest(".alert"); if (target) { target.style.transition = "opacity 0.3s ease-out"; target.style.opacity = "0"; setTimeout(function () { target.remove(); }, 300); } }); }); } /** * Initialize all features when DOM is ready */ function init() { initCopyButtons(); initConfirmations(); initToggles(); initAutoDismiss(); initDismissButtons(); } // Run on DOM ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } // ============================================ // Deployment & Status Utilities // ============================================ /** * Format a date string as relative time (e.g., "5 minutes ago") */ function formatRelativeTime(dateStr) { var date = new Date(dateStr); var now = new Date(); var diffMs = now - date; var diffSec = Math.floor(diffMs / 1000); var diffMin = Math.floor(diffSec / 60); var diffHour = Math.floor(diffMin / 60); var 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(); } /** * Update all elements with class 'relative-time' to show relative times */ function updateRelativeTimes() { document.querySelectorAll(".relative-time").forEach(function (el) { var time = el.getAttribute("data-time"); if (time) { el.textContent = formatRelativeTime(time); } }); } /** * Get the badge class for a given status */ function 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) */ function statusLabel(status) { if (!status) return ""; return status.charAt(0).toUpperCase() + status.slice(1); } /** * Update a status badge element */ function updateStatusBadge(el, status, extraClasses) { if (!el) return; el.className = statusBadgeClass(status) + (extraClasses ? " " + extraClasses : ""); el.textContent = statusLabel(status); } /** * Scroll an element to the bottom */ function scrollToBottom(el) { if (el) { el.scrollTop = el.scrollHeight; } } /** * Disable a deploy button immediately */ function disableDeployButton(btn, btnText) { if (!btn) return; btn.disabled = true; btn.classList.add("opacity-50", "cursor-not-allowed"); if (btnText) { btnText.textContent = "Deploying..."; } else { btn.textContent = "Deploying..."; } } /** * Enable a deploy button */ function enableDeployButton(btn, btnText) { if (!btn) return; btn.disabled = false; btn.classList.remove("opacity-50", "cursor-not-allowed"); if (btnText) { btnText.textContent = "Deploy Now"; } else { btn.textContent = "Deploy Now"; } } /** * Update deploy button based on status */ function updateDeployButton(btn, btnText, status) { var isDeploying = status === "building" || status === "deploying"; if (isDeploying) { disableDeployButton(btn, btnText); } else { enableDeployButton(btn, btnText); } } // ============================================ // App Detail Page // ============================================ /** * Initialize app detail page polling and updates * @param {object} config - Configuration object * @param {string} config.appId - App ID * @param {number|null} config.initialDeploymentId - Initial deployment ID or null */ function initAppDetailPage(config) { var appId = config.appId; var currentDeploymentId = config.initialDeploymentId; var containerLogsEl = document.getElementById("container-logs"); var containerLogsWrapper = document.getElementById( "container-logs-wrapper", ); var containerStatusEl = document.getElementById("container-status"); var buildLogsEl = document.getElementById("build-logs"); var buildLogsWrapper = document.getElementById("build-logs-wrapper"); var buildStatusEl = document.getElementById("build-status"); var buildLogsSection = document.getElementById("build-logs-section"); var appStatusEl = document.getElementById("app-status"); var deployBtn = document.getElementById("deploy-btn"); var deployBtnText = document.getElementById("deploy-btn-text"); // Disable deploy button immediately on form submit var deployForm = document.getElementById("deploy-form"); if (deployForm && deployBtn) { deployForm.addEventListener("submit", function () { disableDeployButton(deployBtn, deployBtnText); }); } function fetchAppStatus() { fetch("/apps/" + appId + "/status") .then(function (r) { return r.json(); }) .then(function (data) { updateStatusBadge(appStatusEl, data.status); updateDeployButton(deployBtn, deployBtnText, data.status); if ( data.latestDeploymentID && data.latestDeploymentID !== currentDeploymentId ) { currentDeploymentId = data.latestDeploymentID; if (buildLogsSection) { buildLogsSection.style.display = ""; } fetchBuildLogs(); } }) .catch(function (err) { console.error("Status fetch error:", err); }); } function fetchContainerLogs() { fetch("/apps/" + appId + "/container-logs") .then(function (r) { return r.json(); }) .then(function (data) { if (containerLogsEl) containerLogsEl.textContent = data.logs || "No logs available"; updateStatusBadge( containerStatusEl, data.status, "text-xs", ); scrollToBottom(containerLogsWrapper); }) .catch(function (err) { if (containerLogsEl) containerLogsEl.textContent = "Failed to fetch logs"; }); } function fetchBuildLogs() { if (!currentDeploymentId || !buildLogsEl) return; fetch( "/apps/" + appId + "/deployments/" + currentDeploymentId + "/logs", ) .then(function (r) { return r.json(); }) .then(function (data) { buildLogsEl.textContent = data.logs || "No build logs available"; updateStatusBadge(buildStatusEl, data.status, "text-xs"); scrollToBottom(buildLogsWrapper); }) .catch(function (err) { buildLogsEl.textContent = "Failed to fetch logs"; }); } function fetchRecentDeployments() { fetch("/apps/" + appId + "/recent-deployments") .then(function (r) { return r.json(); }) .then(function (data) { var tbody = document.getElementById("deployments-tbody"); if (!tbody || !data.deployments) return; if (data.deployments.length === 0) { tbody.innerHTML = 'No deployments yet.'; return; } var html = ""; for (var i = 0; i < data.deployments.length; i++) { var d = data.deployments[i]; html += ""; html += '' + d.finishedAtLabel + ""; html += '' + (d.duration || "-") + ""; html += '' + statusLabel(d.status) + ""; html += '' + d.shortCommit + ""; html += ""; } tbody.innerHTML = html; updateRelativeTimes(); }) .catch(function (err) { console.error("Deployments fetch error:", err); }); } // Initial fetch and polling fetchAppStatus(); fetchContainerLogs(); fetchBuildLogs(); fetchRecentDeployments(); setInterval(fetchAppStatus, 1000); setInterval(fetchContainerLogs, 1000); setInterval(fetchBuildLogs, 1000); setInterval(fetchRecentDeployments, 1000); } // ============================================ // Deployments History Page // ============================================ /** * Initialize deployments page polling and updates * @param {object} config - Configuration object * @param {string} config.appId - App ID */ function initDeploymentsPage(config) { var appId = config.appId; var deployBtn = document.getElementById("deploy-btn"); var deployBtnEmpty = document.getElementById("deploy-btn-empty"); var liveLogsSection = document.getElementById("live-logs-section"); var liveLogsEl = document.getElementById("live-logs"); var liveLogsWrapper = document.getElementById("live-logs-wrapper"); var liveStatusEl = document.getElementById("live-status"); var currentDeploymentId = null; var isDeploying = false; // Disable deploy buttons immediately on submit var deployForm = document.getElementById("deploy-form"); var deployFormEmpty = document.getElementById("deploy-form-empty"); if (deployForm && deployBtn) { deployForm.addEventListener("submit", function () { disableDeployButton(deployBtn, null); isDeploying = true; if (liveLogsSection) { liveLogsSection.style.display = ""; if (liveLogsEl) liveLogsEl.textContent = "Starting deployment..."; updateStatusBadge(liveStatusEl, "building", "text-xs"); } }); } if (deployFormEmpty && deployBtnEmpty) { deployFormEmpty.addEventListener("submit", function () { disableDeployButton(deployBtnEmpty, null); }); } function fetchAppStatus() { fetch("/apps/" + appId + "/status") .then(function (r) { return r.json(); }) .then(function (data) { var status = data.status; var deploying = status === "building" || status === "deploying"; updateDeployButton(deployBtn, null, status); updateDeployButton(deployBtnEmpty, null, status); if ( data.latestDeploymentID && data.latestDeploymentID !== currentDeploymentId ) { currentDeploymentId = data.latestDeploymentID; if (deploying && liveLogsSection) { liveLogsSection.style.display = ""; isDeploying = true; } } if (!deploying && isDeploying) { isDeploying = false; window.location.reload(); } }) .catch(function (err) { console.error("Status fetch error:", err); }); } function fetchLiveLogs() { if (!currentDeploymentId || !isDeploying) return; fetch( "/apps/" + appId + "/deployments/" + currentDeploymentId + "/logs", ) .then(function (r) { return r.json(); }) .then(function (data) { if (liveLogsEl) liveLogsEl.textContent = data.logs || "Waiting for logs..."; updateStatusBadge(liveStatusEl, data.status, "text-xs"); scrollToBottom(liveLogsWrapper); // Update matching deployment card if present var card = document.querySelector( '[data-deployment-id="' + currentDeploymentId + '"]', ); if (card) { var logsContent = card.querySelector(".logs-content"); var logsWrapper = card.querySelector(".logs-wrapper"); var statusBadge = card.querySelector(".deployment-status"); if (logsContent) logsContent.textContent = data.logs || "Loading..."; if (logsWrapper) scrollToBottom(logsWrapper); if (statusBadge) updateStatusBadge(statusBadge, data.status); } }) .catch(function (err) { console.error("Logs fetch error:", err); }); } // Check for in-progress deployments on page load var inProgressCards = document.querySelectorAll( '[data-status="building"], [data-status="deploying"]', ); if (inProgressCards.length > 0) { var card = inProgressCards[0]; currentDeploymentId = parseInt( card.getAttribute("data-deployment-id"), 10, ); isDeploying = true; if (liveLogsSection) liveLogsSection.style.display = ""; } // Initial fetch and polling fetchAppStatus(); fetchLiveLogs(); setInterval(fetchAppStatus, 1000); setInterval(fetchLiveLogs, 1000); } // Initialize relative times on all pages if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", function () { updateRelativeTimes(); }); } else { updateRelativeTimes(); } // Update relative times every minute setInterval(updateRelativeTimes, 60000); // Expose API globally window.upaas = { copyToClipboard: copyToClipboard, formatRelativeTime: formatRelativeTime, updateRelativeTimes: updateRelativeTimes, statusBadgeClass: statusBadgeClass, statusLabel: statusLabel, updateStatusBadge: updateStatusBadge, scrollToBottom: scrollToBottom, initAppDetailPage: initAppDetailPage, initDeploymentsPage: initDeploymentsPage, }; })();