/* 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)}
Ú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();
});