/* 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 ``;
}
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