/* Tokentin Dashboard - Frontend simples (S3 + CloudFront) Autenticação: senha do overlay vira token do header x-dashboard-token (somente sessionStorage). */ const STORAGE_KEY = "tokentin_dashboard_cfg_v1"; const SESSION_TOKEN_KEY = "tokentin_dashboard_token_v1"; function $(id) { return document.getElementById(id); } function normalizeBaseUrl(v) { const s = String(v || "").trim().replace(/\/+$/, ""); return s; } function parseDeviceIds(raw) { const txt = String(raw || ""); const ids = txt .split(",") .map((x) => x.trim()) .filter(Boolean); return Array.from(new Set(ids)).slice(0, 50); } function setMsg(el, msg) { if (!el) return; el.textContent = msg || ""; } function setSessionUI() { const pill = $("sessionStatus"); const has = Boolean(sessionStorage.getItem(SESSION_TOKEN_KEY)); if (!pill) return; if (has) { pill.textContent = "Autenticado"; pill.classList.add("ok"); } else { pill.textContent = "Não autenticado"; pill.classList.remove("ok"); } } function showOverlay() { const o = $("loginOverlay"); if (o) { o.classList.add("show"); o.setAttribute("aria-hidden", "false"); } } function hideOverlay() { const o = $("loginOverlay"); if (o) { o.classList.remove("show"); o.setAttribute("aria-hidden", "true"); } } function getTokenOrNull() { return sessionStorage.getItem(SESSION_TOKEN_KEY); } async function apiFetch(path, { method = "GET", body = null } = {}) { const base = normalizeBaseUrl($("apiBase").value); const token = getTokenOrNull(); if (!base) throw new Error("API Base URL vazio."); if (!token) throw new Error("Não autenticado. Faça login."); const url = `${base}${path}`; const headers = { "x-dashboard-token": token, }; if (body) headers["content-type"] = "application/json"; const resp = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : undefined, }); const text = await resp.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch (_) { data = text; } if (!resp.ok) { const msg = typeof data === "string" ? data : JSON.stringify(data); throw new Error(`HTTP ${resp.status}: ${msg}`); } return data; } function renderDevicesSummary(summary) { const root = $("devices"); if (!root) return; root.innerHTML = ""; const items = (summary && summary.items) || []; if (!items.length) { root.innerHTML = `
Nenhum dado encontrado.
`; return; } for (const it of items) { const deviceId = it.deviceId || ""; const total = it.total || 0; const paid = it.paid || 0; const pending = it.pending || 0; const expired = it.expired || 0; const lastAt = it.lastAt || ""; const el = document.createElement("div"); el.className = "device"; el.innerHTML = `

${escapeHtml(deviceId)}

${total}
total
${paid}
paid
${pending}
pending
${expired}
expired

Último: ${escapeHtml(lastAt)}

`; el.querySelector("button")?.addEventListener("click", () => { $("selectedDevice").value = deviceId; $("btnFetchCharges").click(); window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }); }); root.appendChild(el); } } function escapeHtml(s) { return String(s || "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function escapeAttr(s) { return escapeHtml(s).replaceAll(" ", ""); } let nextCursor = null; let prevCursors = []; async function fetchSummary() { const cfgMsg = $("cfgMsg"); setMsg(cfgMsg, ""); const minutes = Number($("minutes").value || 1440); const deviceIds = parseDeviceIds($("deviceIds").value); const params = new URLSearchParams(); params.set("minutes", String(minutes)); if (deviceIds.length) params.set("deviceIds", deviceIds.join(",")); const data = await apiFetch(`/dashboard/summary?${params.toString()}`); renderDevicesSummary(data); } function renderCharges(data) { const body = $("chargesBody"); const msg = $("chargesMsg"); if (!body) return; const items = (data && data.items) || []; nextCursor = data && data.nextCursor ? data.nextCursor : null; body.innerHTML = ""; if (!items.length) { setMsg(msg, "Nenhuma cobrança encontrada."); } else { setMsg(msg, ""); } for (const it of items) { const tr = document.createElement("tr"); tr.innerHTML = ` ${escapeHtml(it.createdAt)} ${escapeHtml(it.status)} ${escapeHtml(it.slot)} ${escapeHtml(it.amountCents)} ${escapeHtml(it.paidAt || "")} ${escapeHtml(it.txid)} `; body.appendChild(tr); } $("btnNext").disabled = !nextCursor; $("btnPrev").disabled = prevCursors.length === 0; } async function fetchCharges({ cursor = null } = {}) { const msg = $("chargesMsg"); setMsg(msg, ""); const deviceId = String($("selectedDevice").value || "").trim(); const limit = Number($("limit").value || 20); if (!deviceId) { setMsg(msg, "Informe o Device ID selecionado."); return; } const params = new URLSearchParams(); params.set("deviceId", deviceId); params.set("limit", String(limit)); if (cursor) params.set("cursor", cursor); const data = await apiFetch(`/dashboard/charges?${params.toString()}`); renderCharges(data); } function loadCfgToForm(cfg) { $("apiBase").value = cfg.apiBase || ""; $("deviceIds").value = (cfg.deviceIds || []).join(", "); $("minutes").value = String(cfg.minutes || 1440); $("selectedDevice").value = cfg.selectedDevice || ""; $("limit").value = String(cfg.limit || 20); } function getCfgFromForm() { return { apiBase: normalizeBaseUrl($("apiBase").value), deviceIds: parseDeviceIds($("deviceIds").value), minutes: Number($("minutes").value || 1440), selectedDevice: String($("selectedDevice").value || "").trim(), limit: Number($("limit").value || 20), }; } function saveCfg() { const cfg = getCfgFromForm(); localStorage.setItem(STORAGE_KEY, JSON.stringify(cfg)); setMsg($("cfgMsg"), "Configuração salva."); } function loadCfg() { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) { setMsg($("cfgMsg"), "Nenhuma configuração salva ainda."); return; } const cfg = JSON.parse(raw); loadCfgToForm(cfg); setMsg($("cfgMsg"), "Configuração carregada."); } function ensureLoginOnLoad() { setSessionUI(); const raw = localStorage.getItem(STORAGE_KEY); if (raw) { try { const cfg = JSON.parse(raw); loadCfgToForm(cfg); } catch (_) {} } else { // defaults prd (pode ajustar manualmente) if (!$("apiBase").value) $("apiBase").value = "https://g731yg8vih.execute-api.us-east-1.amazonaws.com/prd"; if (!$("minutes").value) $("minutes").value = "1440"; if (!$("limit").value) $("limit").value = "20"; } const token = getTokenOrNull(); if (!token) { // prepara overlay com os mesmos valores $("loginApiBase").value = $("apiBase").value; $("loginDeviceIds").value = $("deviceIds").value; $("loginPass").value = ""; setMsg($("loginMsg"), ""); showOverlay(); } } function doLogout() { sessionStorage.removeItem(SESSION_TOKEN_KEY); setSessionUI(); // limpa mensagens e dados visuais setMsg($("cfgMsg"), ""); setMsg($("chargesMsg"), ""); $("devices").innerHTML = ""; $("chargesBody").innerHTML = ""; nextCursor = null; prevCursors = []; $("btnNext").disabled = true; $("btnPrev").disabled = true; // reabre overlay $("loginApiBase").value = $("apiBase").value; $("loginDeviceIds").value = $("deviceIds").value; $("loginPass").value = ""; setMsg($("loginMsg"), ""); showOverlay(); } async function doLogin() { const pass = String($("loginPass").value || ""); const apiBase = normalizeBaseUrl($("loginApiBase").value); const devs = $("loginDeviceIds").value; if (!pass.trim()) { setMsg($("loginMsg"), "Informe a senha."); return; } if (!apiBase) { setMsg($("loginMsg"), "Informe a API Base URL."); return; } // aplica no form principal $("apiBase").value = apiBase; $("deviceIds").value = devs; // armazena somente na sessão sessionStorage.setItem(SESSION_TOKEN_KEY, pass.trim()); setSessionUI(); hideOverlay(); // tenta um fetch rápido para validar token try { await fetchSummary(); } catch (e) { // se falhar, volta pro overlay e limpa token sessionStorage.removeItem(SESSION_TOKEN_KEY); setSessionUI(); setMsg($("loginMsg"), `Falha no login: ${String(e && e.message ? e.message : e)}`); showOverlay(); } } function wire() { $("btnSave")?.addEventListener("click", saveCfg); $("btnLoad")?.addEventListener("click", loadCfg); $("btnFetchDevices")?.addEventListener("click", async () => { try { await fetchSummary(); } catch (e) { setMsg($("cfgMsg"), String(e && e.message ? e.message : e)); } }); $("btnFetchCharges")?.addEventListener("click", async () => { try { prevCursors = []; await fetchCharges(); } catch (e) { setMsg($("chargesMsg"), String(e && e.message ? e.message : e)); } }); $("btnNext")?.addEventListener("click", async () => { try { if (!nextCursor) return; prevCursors.push(nextCursor); await fetchCharges({ cursor: nextCursor }); } catch (e) { setMsg($("chargesMsg"), String(e && e.message ? e.message : e)); } }); $("btnPrev")?.addEventListener("click", async () => { try { if (prevCursors.length === 0) return; // remove o cursor atual e volta para o anterior prevCursors.pop(); const back = prevCursors.length ? prevCursors[prevCursors.length - 1] : null; await fetchCharges({ cursor: back }); } catch (e) { setMsg($("chargesMsg"), String(e && e.message ? e.message : e)); } }); $("btnClear")?.addEventListener("click", () => { $("chargesBody").innerHTML = ""; setMsg($("chargesMsg"), ""); nextCursor = null; prevCursors = []; $("btnNext").disabled = true; $("btnPrev").disabled = true; }); $("btnReload")?.addEventListener("click", () => window.location.reload()); $("btnLogout")?.addEventListener("click", doLogout); $("btnLogin")?.addEventListener("click", doLogin); $("loginPass")?.addEventListener("keydown", (e) => { if (e.key === "Enter") doLogin(); }); // bloqueia clique fora para não fechar sem querer $("loginOverlay")?.addEventListener("click", (e) => { if (e.target && e.target.id === "loginOverlay") { // não fecha } }); } document.addEventListener("DOMContentLoaded", () => { wire(); ensureLoginOnLoad(); });