/** * 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 = '