/* ui.js — Utilitários de interface: formatação, toasts, modal de confirmação, ícones SVG e pequenos helpers de DOM. Sem dependências. */ const UI = (() => { // -------------------------------------------------------------- formatação function bytes(n) { if (!n && n !== 0) return "—"; const u = ["B", "KB", "MB", "GB", "TB"]; let i = 0; while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } return `${n.toFixed(i === 0 ? 0 : 1)} ${u[i]}`; } function uptime(sec) { sec = Math.floor(sec || 0); const d = Math.floor(sec / 86400); const h = Math.floor((sec % 86400) / 3600); const m = Math.floor((sec % 3600) / 60); const parts = []; if (d) parts.push(`${d}d`); if (h) parts.push(`${h}h`); parts.push(`${m}min`); return parts.join(" "); } function escapeHtml(s) { return String(s == null ? "" : s) .replace(/&/g, "&").replace(//g, ">") .replace(/"/g, """); } // -------------------------------------------------------------- estado function stateBadge(state, healthy) { if (state === "active") { const cls = healthy === false ? "warn" : "ok"; const label = healthy === false ? "Iniciando" : "ativo"; return `${label}`; } if (state === "failed") return `falhou`; if (state === "activating") return `iniciando`; if (state === "inactive") return `parado`; return `${escapeHtml(state || "—")}`; } // -------------------------------------------------------------- toasts function toast(message, kind = "") { const box = document.getElementById("toasts"); const el = document.createElement("div"); el.className = `toast ${kind}`; el.textContent = message; box.appendChild(el); setTimeout(() => { el.style.transition = "opacity .25s, transform .25s"; el.style.opacity = "0"; el.style.transform = "translateY(6px)"; setTimeout(() => el.remove(), 260); }, 3600); } // -------------------------------------------------------------- modal confirm // opts: { title, text, confirmLabel, danger, requireText } // resolve(true) se confirmado; resolve(false) se cancelado. function confirm(opts) { return new Promise((resolve) => { const bg = document.getElementById("modal-bg"); const ico = document.getElementById("modal-ico"); const okBtn = document.getElementById("modal-ok"); const cancelBtn = document.getElementById("modal-cancel"); const field = document.getElementById("modal-confirm-field"); const input = document.getElementById("modal-confirm-input"); const label = document.getElementById("modal-confirm-label"); document.getElementById("modal-title").textContent = opts.title || "Confirmar ação"; document.getElementById("modal-text").textContent = opts.text || ""; okBtn.textContent = opts.confirmLabel || "Confirmar"; okBtn.className = "btn " + (opts.danger ? "danger" : "primary"); ico.className = "m-ico " + (opts.danger ? "danger" : "warn"); if (opts.requireText) { field.style.display = "block"; label.textContent = `Digite "${opts.requireText}" para confirmar:`; input.value = ""; okBtn.disabled = true; input.oninput = () => { okBtn.disabled = input.value.trim() !== opts.requireText; }; } else { field.style.display = "none"; okBtn.disabled = false; } const close = (val) => { bg.classList.remove("show"); okBtn.onclick = cancelBtn.onclick = null; document.removeEventListener("keydown", onKey); resolve(val); }; const onKey = (e) => { if (e.key === "Escape") close(false); }; okBtn.onclick = () => close(true); cancelBtn.onclick = () => close(false); bg.onclick = (e) => { if (e.target === bg) close(false); }; document.addEventListener("keydown", onKey); bg.classList.add("show"); if (opts.requireText) setTimeout(() => input.focus(), 50); }); } // -------------------------------------------------------------- ícones const icons = { overview: '', services: '', remote: '', docs: '', admin: '', profiles: '', plus: '', back: '', copy: '', trash: '', key: '', edit: '', cpu: '', gpu: '', mem: '', disk: '', clock: '', bolt: '', fan: '', link: '', benchmarks: '', reports: '', menu: '', users: '', logs: '', eye: '', }; function permBadge(label, active) { return `${escapeHtml(label)}`; } // Badge para o status de um job de benchmark. function benchBadge(status) { const map = { queued: ["na fila", "idle"], running: ["em execução", "warn"], done: ["concluído", "ok"], canceled: ["cancelado", "idle"], error: ["erro", "bad"], }; const [label, cls] = map[status] || [status || "—", "idle"]; return `${label}`; } function icon(name, size = 18) { return `${icons[name] || ""}`; } function barClass(pct) { if (pct >= 90) return "danger"; if (pct >= 75) return "warn"; return "ok"; } // Duração legível a partir de segundos (ex.: 75 -> "1m15s"). function duration(sec) { if (sec == null) return "—"; sec = Math.round(sec); if (sec < 60) return `${sec}s`; const m = Math.floor(sec / 60), s = sec % 60; if (m < 60) return `${m}m${String(s).padStart(2, "0")}s`; const h = Math.floor(m / 60); return `${h}h${String(m % 60).padStart(2, "0")}m`; } // Data/hora ISO -> "AAAA-MM-DD HH:MM" (UTC, compacto). function dt(iso) { if (!iso) return "—"; return String(iso).replace("T", " ").replace(/(\+00:00|Z)$/, "").slice(0, 16); } const _SVC_LABELS = { gpu_chat: "Chat GPU", cpu_chat: "Chat CPU", agent_cpu: "Chat CPU+GPU", comfyui: "T2I ComfyUI", xtts: "Voice clone xtts", chatterbox_toolkit: "Chatterbox Toolkit" }; function svcLabel(id) { return _SVC_LABELS[id] || id || "—"; } // Copia texto para a área de transferência. navigator.clipboard só existe em // contexto seguro (HTTPS ou localhost); no painel via http://:8090 o // Safari/Chrome não o expõem, então caímos para um