"use strict";
// Armazenamento e chaves
const STORAGE = {
UNITS: "telseg_units",
CREDS: "telseg_passwords",
VAULT: "telseg_vault",
THEME: "telseg_theme",
NOTES: "telseg_notes",
AUTH: "telseg_auth",
};
const $ = (sel, el = document) => el.querySelector(sel);
const $$ = (sel, el = document) => Array.from(el.querySelectorAll(sel));
// Detecção de API
const state = { useApi: false, units: [], creds: [], vault: null, notes: [], auth: null };
async function probeApi() {
try {
const r = await fetch('/api/health', { cache: 'no-store' });
state.useApi = r.ok; return state.useApi;
} catch { state.useApi = false; return false; }
}
function updateStorageInfo() {
const el = document.getElementById('storageInfo');
if (!el) return;
el.textContent = state.useApi
? 'Servidor • Dados persistem no banco (Postgres) do container.'
: 'Local • Seus dados ficam somente no seu navegador.';
}
// ---- Auth (API) ----
function getToken() { try { return JSON.parse(localStorage.getItem(STORAGE.AUTH) || 'null')?.token || null; } catch { return null; } }
function setToken(token) { localStorage.setItem(STORAGE.AUTH, JSON.stringify({ token })); }
async function apiFetch(path, opts = {}) {
const o = { ...opts, headers: { ...(opts.headers||{}), 'Content-Type': 'application/json' } };
const token = getToken();
if (token) o.headers['Authorization'] = `Bearer ${token}`;
const r = await fetch(path, o);
if (r.status === 401) { showLogin(); throw new Error('unauthorized'); }
return r;
}
function canMaster() { return !!state.auth?.user && state.auth.user.role === 'master'; }
function can(flag) { if (canMaster()) return true; return !!state.auth?.user && !!state.auth.user.permissions?.[flag]; }
function applyPermissionsUI() {
if (!state.useApi || !state.auth?.user) return;
// tabs
const tabs = [
{ id: 'unidades', read: 'units_read' },
{ id: 'senhas', read: 'creds_read' },
{ id: 'notas', read: 'notes_read' },
];
tabs.forEach(t => {
const btn = document.querySelector(`.tab[data-tab="${t.id}"]`);
const view = document.getElementById(`view-${t.id}`);
const visible = can(t.read);
if (btn) btn.style.display = visible ? '' : 'none';
if (view) view.style.display = visible ? '' : 'none';
});
// users tab only master
const userTab = document.querySelector('#tab-users');
const userView = document.getElementById('view-usuarios');
if (userTab) userTab.style.display = canMaster() ? '' : 'none';
if (userView) userView.style.display = canMaster() ? '' : 'none';
// action buttons
document.getElementById('addUnidadeBtn').style.display = can('units_write') ? '' : 'none';
document.getElementById('addCredBtn').style.display = can('creds_write') ? '' : 'none';
document.getElementById('addNoteBtn').style.display = can('notes_write') ? '' : 'none';
}
function showLogin() { document.getElementById('authGate')?.classList.remove('hidden'); }
function hideLogin() { document.getElementById('authGate')?.classList.add('hidden'); }
// Tema (claro/escuro)
function loadTheme() {
const t = localStorage.getItem(STORAGE.THEME) || "dark";
document.documentElement.setAttribute("data-theme", t);
}
function toggleTheme() {
const cur = document.documentElement.getAttribute("data-theme") || "dark";
const next = cur === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", next);
localStorage.setItem(STORAGE.THEME, next);
}
// Utilidades
function uid() {
// ID simples e estável suficiente para uso local
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
function saveJSON(key, value) { localStorage.setItem(key, JSON.stringify(value)); }
function loadJSON(key, fallback) {
try { const v = JSON.parse(localStorage.getItem(key) || ""); return v ?? fallback; }
catch { return fallback; }
}
function todayStr() { return new Date().toISOString().slice(0, 10); }
function copyText(text) {
navigator.clipboard?.writeText(text).catch(() => {
const ta = document.createElement("textarea");
ta.value = text; document.body.appendChild(ta); ta.select();
document.execCommand("copy"); document.body.removeChild(ta);
});
}
// Navegação por abas
function initTabs() {
$$(".tab").forEach(btn => {
btn.addEventListener("click", () => {
$$(".tab").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
const tab = btn.dataset.tab;
$$(".view").forEach(v => v.classList.remove("active"));
$(`#view-${tab}`).classList.add("active");
if (tab === 'usuarios') renderUsers();
});
});
}
// ------- Unidades -------
async function refreshUnits() {
if (state.useApi) {
const r = await apiFetch('/api/units');
state.units = r.ok ? await r.json() : [];
} else {
state.units = loadJSON(STORAGE.UNITS, []);
}
return state.units;
}
function loadUnits() { return state.units || []; }
function saveUnits(list) { saveJSON(STORAGE.UNITS, list); state.units = list; }
function parseOCs(str) {
return (str || "")
.split(/[\n,]/g)
.map(s => s.trim())
.filter(Boolean);
}
async function renderUnits(filter = "") {
if (state.useApi) await refreshUnits();
const listEl = $("#unidadesList");
const items = loadUnits();
const f = filter.trim().toLowerCase();
const filtered = !f ? items : items.filter(u => {
return (
u.nome.toLowerCase().includes(f) ||
(u.ip || "").toLowerCase().includes(f) ||
(u.ocs || []).some(x => x.toLowerCase().includes(f))
);
});
// Ordena por nome: números primeiro, depois letras (ordem alfabética/natural)
filtered.sort((a, b) => {
const an = (a.nome || '').trim();
const bn = (b.nome || '').trim();
const aNum = /^[0-9]/.test(an);
const bNum = /^[0-9]/.test(bn);
if (aNum !== bNum) return aNum ? -1 : 1; // números primeiro
return an.localeCompare(bn, 'pt-BR', { numeric: true, sensitivity: 'base' });
});
const canWrite = !state.useApi || can('units_write');
listEl.innerHTML = filtered.map(u => {
const ocs = (u.ocs || []).map(x => `OC ${escapeHTML(x)}`).join(" ");
return `
${escapeHTML(u.nome)}
Criado em ${formatDate(u.dataCriacao)}${u.ip ? ` • IP ${escapeHTML(u.ip)}` : ""}
${ocs ? `${ocs}
` : ""}
${u.link ? `` : ""}
${u.link ? `` : ""}
${u.ip ? `` : ""}
${canWrite ? `` : ''}
${canWrite ? `` : ''}
`;
}).join("");
// Complementa UI com login/senha
filtered.forEach(u => {
const card = listEl.querySelector(`.card[data-id="${u.id}"]`);
if (!card) return;
const meta = card.querySelector('.meta');
if (u.login && meta && !meta.textContent.includes('Login')) {
meta.innerHTML = `${meta.innerHTML} • Login ${escapeHTML(u.login)}`;
}
const actions = card.querySelector('.actions');
if (actions) {
if (u.login && !actions.querySelector('[data-act="copylogin"]')) {
const b = document.createElement('button');
b.className = 'btn small';
b.setAttribute('data-act', 'copylogin');
b.textContent = 'Copiar Login';
actions.insertBefore(b, actions.querySelector('[data-act="edit"]') || null);
}
if (u.senha && !actions.querySelector('[data-act="copypass"]')) {
const b2 = document.createElement('button');
b2.className = 'btn small';
b2.setAttribute('data-act', 'copypass');
b2.textContent = 'Copiar Senha';
actions.insertBefore(b2, actions.querySelector('[data-act="edit"]') || null);
}
}
});
// Delegação de eventos nos cartões
listEl.onclick = async (ev) => {
const btn = ev.target.closest("[data-act]");
if (!btn) return;
const card = ev.target.closest(".card");
const id = card?.dataset.id;
if (!id) return;
const items2 = loadUnits();
const idx = items2.findIndex(x => x.id === id);
const u = items2[idx];
if (!u) return;
const act = btn.dataset.act;
if (act === "open" && u.link) {
window.open(u.link, "_blank", "noopener");
} else if (act === "copyip" && u.ip) {
copyText(u.ip);
} else if (act === "copylogin" && u.login) {
copyText(u.login);
} else if (act === "copypass" && u.senha) {
copyText(u.senha);
} else if (act === "edit") {
openUnidadeModal(u);
} else if (act === "del") {
if (confirm(`Excluir unidade "${u.nome}"?`)) {
if (state.useApi) {
await apiFetch(`/api/units/${encodeURIComponent(u.id)}`, { method: 'DELETE' });
await refreshUnits();
} else {
items2.splice(idx, 1); saveUnits(items2);
}
renderUnits($("#searchUnidades").value);
}
}
};
}
function openUnidadeModal(u) {
$("#unidadeId").value = u?.id || "";
$("#unidadeNome").value = u?.nome || "";
$("#unidadeData").value = u?.dataCriacao || todayStr();
$("#unidadeOCs").value = (u?.ocs || []).join(", ");
$("#unidadeLink").value = u?.link || "";
$("#unidadeIP").value = u?.ip || "";
$("#unidadeLogin").value = u?.login || "";
$("#unidadeSenha").value = ""; // nunca preencher senha existente
showModal("unidadeModal");
}
// ------- Modal Helpers -------
function showModal(id) { const el = document.getElementById(id); el?.classList.remove("hidden"); }
function hideModal(id) { const el = document.getElementById(id); el?.classList.add("hidden"); }
// Escapar HTML básico
function escapeHTML(s) { return (s ?? "").replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">","\"":"""}[c])); }
function escapeAttr(s) { return escapeHTML(s).replace(/'/g, "'"); }
function formatDate(s) { try { const [y,m,d] = (s||"").split("-"); return d && m && y ? `${d}/${m}/${y}` : s; } catch { return s; } }
// ------- Cofre de Senhas (WebCrypto) -------
async function refreshCreds() {
if (state.useApi) {
const r = await apiFetch('/api/creds');
state.creds = r.ok ? await r.json() : [];
} else {
state.creds = loadJSON(STORAGE.CREDS, []);
}
return state.creds;
}
function loadCreds() { return state.creds || []; }
function saveCreds(list) { saveJSON(STORAGE.CREDS, list); state.creds = list; }
async function refreshVault() {
if (state.useApi) {
const r = await apiFetch('/api/vault');
state.vault = r.ok ? await r.json() : null;
} else {
state.vault = loadJSON(STORAGE.VAULT, null);
}
return state.vault;
}
function getVault() { return state.vault; }
function setVault(v) { saveJSON(STORAGE.VAULT, v); state.vault = v; }
// ------- Notas -------
async function refreshNotes() {
if (state.useApi) {
const r = await apiFetch('/api/notes');
state.notes = r.ok ? await r.json() : [];
} else {
state.notes = loadJSON(STORAGE.NOTES, []);
}
return state.notes;
}
function loadNotes() { return state.notes || []; }
function saveNotes(list) { saveJSON(STORAGE.NOTES, list); state.notes = list; }
function openNoteModal(n) {
$("#noteId").value = n?.id || "";
$("#noteTitle").value = n?.title || "";
$("#noteBody").value = n?.body || "";
$("#noteType").value = n?.data?.type || 'texto';
editingSteps = (n?.data?.steps || []).map(s => ({ id: s.id || uid(), text: s.text || '' }));
toggleStepsBlock();
renderSteps();
showModal("noteModal");
}
async function renderNotes(filter = "") {
if (state.useApi) await refreshNotes();
const listText = document.getElementById('notesTextList');
const listSteps = document.getElementById('notesStepsList');
const items = loadNotes();
const f = filter.trim().toLowerCase();
const filtered = !f ? items : items.filter(n => (
(n.title || "").toLowerCase().includes(f) || (n.body || "").toLowerCase().includes(f)
));
filtered.sort((a,b) => (b.updatedAt||0)-(a.updatedAt||0));
const renderCard = (n) => {
const preview = escapeHTML((n.body || '').split('\n').slice(0,3).join(' '));
const when = n.updatedAt ? new Date(n.updatedAt).toLocaleString() : '';
return `
${escapeHTML(n.title || 'Sem título')}
${n.data?.type === 'passos' ? 'Passo a passo' : ''} ${when ? `• Atualizado: ${when}` : ''}
${n.data?.type === 'passos' ? `${(n.data.steps||[]).slice(0,3).map((s,i)=>`${i+1}. ${escapeHTML(s.text||'')}`).join('
')}
` : (preview ? `${preview}
` : '')}
`;
};
const texts = filtered.filter(n => (n.data?.type || 'texto') !== 'passos');
const steps = filtered.filter(n => n.data?.type === 'passos');
if (listText) listText.innerHTML = texts.map(renderCard).join("");
if (listSteps) listSteps.innerHTML = steps.map(renderCard).join("");
const attach = (el) => {
if (!el) return;
el.onclick = (ev) => {
const card = ev.target.closest('.card');
if (!card) return;
const id = card.dataset.id;
const n = loadNotes().find(x => x.id === id);
if (n) openNoteView(n);
};
};
attach(listText);
attach(listSteps);
}
// --- Editor de passos (modal Nota)
let editingSteps = [];
function toggleStepsBlock() {
const type = $("#noteType").value;
const block = $("#stepsBlock");
if (type === 'passos') block.classList.remove('hidden'); else block.classList.add('hidden');
}
function renderSteps() {
const el = $("#stepsList");
el.innerHTML = editingSteps.map((s, idx) => `
${escapeHTML(s.text)}
`).join("");
}
// Visualização de nota (popup)
function openNoteView(n) {
const body = document.getElementById('noteViewBody');
const title = document.getElementById('noteViewTitle');
title.textContent = n.title || 'Sem título';
const when = n.updatedAt ? new Date(n.updatedAt).toLocaleString() : '';
const chip = n.data?.type === 'passos' ? 'Passo a passo' : 'Texto';
const sections = [];
if (n.data?.type === 'passos' && (n.data.steps || []).length) {
const items = (n.data.steps || []).map(s => `${escapeHTML(s.text || '')}`).join('');
sections.push(`Passos
${items}
`);
}
if ((n.body || '').trim()) {
sections.push(`Conteúdo
${escapeHTML(n.body)}
`);
}
if (!sections.length) {
sections.push('Sem conteúdo
');
}
body.innerHTML = `${chip} ${when ? `• Atualizado: ${when}` : ''}
${sections.join('')}
`;
// Ações
document.getElementById('noteViewEdit').onclick = () => { hideModal('noteViewModal'); openNoteModal(n); };
document.getElementById('noteViewDelete').onclick = async () => {
if (!confirm('Excluir esta nota?')) return;
if (state.useApi) {
await apiFetch(`/api/notes/${encodeURIComponent(n.id)}`, { method: 'DELETE' });
await refreshNotes();
} else {
const list = loadNotes().filter(x => x.id !== n.id); saveNotes(list);
}
hideModal('noteViewModal');
renderNotes($("#searchNotes").value);
};
showModal('noteViewModal');
}
const textEnc = new TextEncoder();
const textDec = new TextDecoder();
function ab2b64(ab) {
const bytes = new Uint8Array(ab);
let bin = ""; for (let i=0; i {
return (
c.nome.toLowerCase().includes(f) ||
(c.usuario || "").toLowerCase().includes(f) ||
(c.url || "").toLowerCase().includes(f)
);
});
filtered.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
const canWrite = !state.useApi || can('creds_write');
listEl.innerHTML = filtered.map(c => {
const preview = escapeHTML((c.notas || '').split('\n').slice(0,2).join(' '));
const when = c.updatedAt ? new Date(c.updatedAt).toLocaleString() : '';
return `
${escapeHTML(c.nome)}
${preview ? `${preview}
` : ''}
`;
}).join("");
listEl.onclick = async (ev) => {
const btn = ev.target.closest("[data-act]");
const card = ev.target.closest('.card');
if (!card) return;
const id = card.dataset.id;
const items2 = loadCreds();
const idx = items2.findIndex(x => x.id === id);
const c = items2[idx];
if (!c) return;
if (btn) {
// Mantido por compatibilidade caso algum botão exista
if (!vaultKey) { alert("Cofre bloqueado."); return; }
const act = btn.dataset.act;
if (act === "open" && c.url) {
window.open(c.url, "_blank", "noopener");
} else if (act === "copyuser" && c.usuario) {
copyText(c.usuario);
} else if (act === "copypass") {
try { const pass = await decryptString(vaultKey, c.passwordEnc); copyText(pass); } catch { alert("Falha ao descriptografar."); }
} else if (act === "reveal") {
try { const pass = await decryptString(vaultKey, c.passwordEnc); alert(`Senha: ${pass}`); } catch { alert("Falha ao descriptografar."); }
} else if (act === "edit") {
openCredModal(c);
} else if (act === "del") {
if (confirm(`Excluir credencial "${c.nome}"?`)) {
if (state.useApi) {
await apiFetch(`/api/creds/${encodeURIComponent(c.id)}`, { method: 'DELETE' });
await refreshCreds();
} else {
items2.splice(idx,1); saveCreds(items2);
}
renderCreds($("#searchCreds").value);
}
}
} else {
// Clique no card abre visualização
if (!vaultKey) { alert("Cofre bloqueado."); return; }
openCredView(c, canWrite);
}
};
}
function openCredModal(c) {
$("#credId").value = c?.id || "";
$("#credNome").value = c?.nome || "";
$("#credUsuario").value = c?.usuario || "";
$("#credSenha").value = ""; // nunca preencher
$("#credUrl").value = c?.url || "";
$("#credNotas").value = c?.notas || "";
showModal("credModal");
}
// Visualização de credencial (popup)
function openCredView(c, canWrite) {
const body = document.getElementById('credViewBody');
const title = document.getElementById('credViewTitle');
title.textContent = c.nome || 'Credencial';
const when = c.updatedAt ? new Date(c.updatedAt).toLocaleString() : '';
const sections = [];
const access = [];
if (c.usuario) access.push(`Usuário: ${escapeHTML(c.usuario)}
`);
if (c.url) access.push(``);
if (access.length) sections.push(`Acesso
${access.join('')}
`);
if ((c.notas || '').trim()) sections.push(`Notas
${escapeHTML(c.notas)}
`);
if (!sections.length) sections.push('Sem detalhes
');
body.innerHTML = `${when ? `Atualizado: ${when}` : ''}
${sections.join('')}
`;
// Ações
const btnOpen = document.getElementById('credViewOpen');
const btnCopyUser = document.getElementById('credViewCopyUser');
const btnCopyPass = document.getElementById('credViewCopyPass');
const btnReveal = document.getElementById('credViewReveal');
const btnEdit = document.getElementById('credViewEdit');
const btnDelete = document.getElementById('credViewDelete');
btnOpen.style.display = c.url ? '' : 'none';
btnCopyUser.style.display = c.usuario ? '' : 'none';
btnEdit.style.display = canWrite ? '' : 'none';
btnDelete.style.display = canWrite ? '' : 'none';
btnOpen.onclick = () => { if (c.url) window.open(c.url, '_blank', 'noopener'); };
btnCopyUser.onclick = () => { if (c.usuario) copyText(c.usuario); };
btnCopyPass.onclick = async () => {
try { const pass = await decryptString(vaultKey, c.passwordEnc); copyText(pass); } catch { alert('Falha ao descriptografar.'); }
};
btnReveal.onclick = async () => {
try { const pass = await decryptString(vaultKey, c.passwordEnc); alert(`Senha: ${pass}`); } catch { alert('Falha ao descriptografar.'); }
};
btnEdit.onclick = () => { hideModal('credViewModal'); openCredModal(c); };
btnDelete.onclick = async () => {
if (!confirm(`Excluir credencial "${c.nome}"?`)) return;
const items2 = loadCreds();
const idx = items2.findIndex(x => x.id === c.id);
if (state.useApi) {
await apiFetch(`/api/creds/${encodeURIComponent(c.id)}`, { method: 'DELETE' });
await refreshCreds();
} else if (idx >= 0) {
items2.splice(idx,1); saveCreds(items2);
}
hideModal('credViewModal');
renderCreds($("#searchCreds").value);
};
showModal('credViewModal');
}
// ------- Exportar / Importar -------
function exportAll() {
const payload = {
version: 1,
exportedAt: new Date().toISOString(),
theme: document.documentElement.getAttribute("data-theme"),
units: loadUnits(),
vault: getVault(),
creds: loadCreds(),
notes: loadNotes(),
};
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
const a = document.createElement("a");
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth()+1).padStart(2,"0");
const day = String(d.getDate()).padStart(2,"0");
a.download = `telseg_dashboard_backup_${y}${m}${day}.json`;
a.href = URL.createObjectURL(blob);
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 3000);
}
function importAll(file) {
const reader = new FileReader();
reader.onload = () => {
try {
const data = JSON.parse(String(reader.result || "{}"));
if (!confirm("Importar backup e substituir dados atuais?")) return;
if (data.theme) { localStorage.setItem(STORAGE.THEME, data.theme); document.documentElement.setAttribute("data-theme", data.theme); }
if (Array.isArray(data.units)) saveUnits(data.units);
if (Array.isArray(data.creds)) saveCreds(data.creds);
if (data.vault && data.vault.salt && data.vault.verification) setVault(data.vault);
if (Array.isArray(data.notes)) saveNotes(data.notes);
alert("Importação concluída.");
renderUnits($("#searchUnidades").value);
renderNotes($("#searchNotes").value);
lockVault(); // força rechecagem do cofre
} catch (e) {
alert("Arquivo inválido.");
}
};
reader.readAsText(file);
}
// ------- Inicialização -------
document.addEventListener("DOMContentLoaded", async () => {
loadTheme();
await probeApi();
// Auth flow when API is enabled
if (state.useApi) {
// try existing token
const token = getToken();
if (token) {
try {
const r = await apiFetch('/api/auth/me');
const data = await r.json();
state.auth = data; hideLogin();
applyPermissionsUI();
if (canMaster()) await renderUsers();
} catch { showLogin(); }
} else {
showLogin();
}
}
if (!state.useApi || state.auth?.user) {
await Promise.all([refreshUnits(), refreshCreds(), refreshVault(), refreshNotes()]);
}
updateStorageInfo();
applyPermissionsUI();
$("#themeToggle").addEventListener("click", toggleTheme);
initTabs();
// Unidades
$("#addUnidadeBtn").addEventListener("click", () => openUnidadeModal());
$("#searchUnidades").addEventListener("input", ev => renderUnits(ev.target.value));
$("#unidadeForm").addEventListener("submit", async (ev) => {
ev.preventDefault();
const id = $("#unidadeId").value || uid();
const listBefore = loadUnits();
const idxBefore = listBefore.findIndex(x => x.id === id);
const existing = idxBefore >= 0 ? listBefore[idxBefore] : null;
const item = {
id,
nome: $("#unidadeNome").value.trim(),
dataCriacao: $("#unidadeData").value || todayStr(),
ocs: parseOCs($("#unidadeOCs").value),
link: $("#unidadeLink").value.trim(),
ip: $("#unidadeIP").value.trim(),
login: $("#unidadeLogin").value.trim(),
senha: $("#unidadeSenha").value ? $("#unidadeSenha").value : (existing?.senha || ''),
};
if (!item.nome) { alert("Informe o nome do cliente."); return; }
if (state.useApi) {
const exists = idxBefore >= 0;
await apiFetch(exists ? `/api/units/${encodeURIComponent(id)}` : '/api/units', {
method: exists ? 'PUT' : 'POST',
body: JSON.stringify(item)
});
await refreshUnits();
} else {
const list = listBefore.slice();
if (idxBefore >= 0) list[idxBefore] = item; else list.push(item);
saveUnits(list);
}
$("#unidadeSenha").value = "";
hideModal("unidadeModal");
renderUnits($("#searchUnidades").value);
});
// Fechar modais
document.body.addEventListener("click", (ev) => {
const btn = ev.target.closest("[data-close-modal]");
if (btn) hideModal(btn.getAttribute("data-close-modal"));
});
// Senhas/Cofre
renderVaultGate();
$("#lockVaultBtn").addEventListener("click", lockVault);
$("#searchCreds").addEventListener("input", ev => renderCreds(ev.target.value));
$("#addCredBtn").addEventListener("click", () => {
if (!vaultKey) { alert("Desbloqueie o cofre primeiro."); return; }
openCredModal();
});
// Notas
$("#addNoteBtn").addEventListener("click", () => openNoteModal());
$("#searchNotes").addEventListener("input", ev => renderNotes(ev.target.value));
$("#noteType").addEventListener("change", toggleStepsBlock);
$("#addStepBtn").addEventListener("click", () => {
const input = $("#newStepText");
const text = input.value.trim();
if (!text) return;
editingSteps.push({ id: uid(), text });
input.value = "";
renderSteps();
});
$("#stepsList").addEventListener("click", (ev) => {
const del = ev.target.closest('[data-del]');
const row = ev.target.closest('.step-card');
if (del && row) {
const idx = Number(row.dataset.idx);
editingSteps.splice(idx,1);
renderSteps();
}
});
$("#stepsList").addEventListener("blur", (ev) => {
const row = ev.target.closest('.step-card');
if (!row) return;
const idx = Number(row.dataset.idx);
const textEl = row.querySelector('.text');
if (textEl) editingSteps[idx].text = textEl.textContent.trim();
}, true);
$("#noteForm").addEventListener("submit", async (ev) => {
ev.preventDefault();
const id = $("#noteId").value || uid();
const note = {
id,
title: $("#noteTitle").value.trim(),
body: $("#noteBody").value,
updatedAt: Date.now(),
data: $("#noteType").value === 'passos' ? { type: 'passos', steps: editingSteps.slice() } : { type: 'texto' }
};
if (state.useApi) {
const exists = loadNotes().some(n => n.id === id);
await apiFetch(exists ? `/api/notes/${encodeURIComponent(id)}` : '/api/notes', {
method: exists ? 'PUT' : 'POST',
body: JSON.stringify(note)
});
await refreshNotes();
} else {
const list = loadNotes();
const idx = list.findIndex(n => n.id === id);
if (idx>=0) list[idx] = note; else list.push(note);
saveNotes(list);
}
hideModal('noteModal');
renderNotes($("#searchNotes").value);
});
$("#setVaultForm").addEventListener("submit", async (ev) => {
ev.preventDefault();
const p1 = $("#vaultPass1").value;
const p2 = $("#vaultPass2").value;
if (!p1 || p1 !== p2) { alert("Senhas não conferem."); return; }
const salt = ab2b64(crypto.getRandomValues(new Uint8Array(16)).buffer);
const key = await deriveKey(p1, salt, VAULT_ITERS);
const verification = await encryptString(key, "ok");
const meta = { salt, verification, iterations: VAULT_ITERS, v: 1 };
if (state.useApi) {
await apiFetch('/api/vault', { method: 'PUT', body: JSON.stringify(meta) });
await refreshVault();
} else {
setVault(meta);
}
alert("Cofre configurado. Guarde sua senha mestra!");
$("#vaultPass1").value = ""; $("#vaultPass2").value = "";
unlockVault(p1);
});
$("#unlockVaultForm").addEventListener("submit", async (ev) => {
ev.preventDefault();
const p = $("#vaultUnlockPass").value;
await unlockVault(p);
});
$("#credForm").addEventListener("submit", async (ev) => {
ev.preventDefault();
if (!vaultKey) { alert("Cofre bloqueado."); return; }
const id = $("#credId").value || uid();
const list = loadCreds();
const idx = list.findIndex(x => x.id === id);
const existing = idx >= 0 ? list[idx] : null;
const passwordPlain = $("#credSenha").value; // pode estar vazio ao editar
let passwordEnc = existing?.passwordEnc;
if (passwordPlain) passwordEnc = await encryptString(vaultKey, passwordPlain);
const item = {
id,
nome: $("#credNome").value.trim(),
usuario: $("#credUsuario").value.trim(),
url: $("#credUrl").value.trim(),
notas: $("#credNotas").value.trim(),
passwordEnc,
updatedAt: Date.now(),
};
if (!item.nome) { alert("Informe o nome."); return; }
if (!item.passwordEnc) { alert("Informe a senha."); return; }
if (state.useApi) {
const exists = idx >= 0;
await apiFetch(exists ? `/api/creds/${encodeURIComponent(id)}` : '/api/creds', { method: exists ? 'PUT' : 'POST', body: JSON.stringify(item) });
await refreshCreds();
} else {
if (idx >= 0) list[idx] = item; else list.push(item);
saveCreds(list);
}
hideModal("credModal");
renderCreds($("#searchCreds").value);
});
// Exportar / Importar
$("#exportBtn").addEventListener("click", async () => {
if (state.useApi) {
const r = await apiFetch('/api/export');
const payload = await r.json();
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
const a = document.createElement("a");
a.download = `telseg_dashboard_backup_api_${new Date().toISOString().slice(0,10)}.json`;
a.href = URL.createObjectURL(blob); a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 3000);
} else {
exportAll();
}
});
$("#importFile").addEventListener("change", async (ev) => {
const f = ev.target.files?.[0]; if (!f) return;
if (state.useApi) {
const text = await f.text();
const data = JSON.parse(text);
if (!confirm("Importar backup e substituir dados atuais no servidor?")) return;
await apiFetch('/api/import', { method: 'POST', body: JSON.stringify(data) });
await Promise.all([refreshUnits(), refreshCreds(), refreshVault(), refreshNotes()]);
lockVault();
renderUnits($("#searchUnidades").value); // re-render
renderNotes($("#searchNotes").value);
} else {
importAll(f);
}
ev.target.value = ""; // permite importar o mesmo arquivo novamente
});
// Login form
const loginForm = document.getElementById('loginForm');
if (loginForm) {
loginForm.addEventListener('submit', async (ev) => {
ev.preventDefault();
const username = document.getElementById('loginUser').value.trim();
const password = document.getElementById('loginPass').value;
try {
const r = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) });
if (!r.ok) throw new Error();
const data = await r.json();
setToken(data.token);
state.auth = { user: data.user };
hideLogin();
applyPermissionsUI();
await Promise.all([refreshUnits(), refreshCreds(), refreshVault(), refreshNotes()]);
if (canMaster()) await renderUsers();
renderUnits(document.getElementById('searchUnidades').value);
renderNotes(document.getElementById('searchNotes').value);
} catch {
alert('Login inválido');
}
});
}
// Render inicial
if (!state.useApi || state.auth?.user) {
renderUnits("");
renderNotes("");
}
});
// ---- Usuários (somente Master) ----
async function renderUsers() {
if (!state.useApi || !canMaster()) return;
try {
const r = await apiFetch('/api/users');
const users = await r.json();
const list = document.getElementById('usersList');
list.innerHTML = users.map(u => {
const p = u.permissions || {};
return `
${escapeHTML(u.username)} ${u.role === 'master' ? 'Master' : ''}
`;
}).join('');
// Handlers
list.onchange = async (ev) => {
const card = ev.target.closest('.card'); if (!card) return;
const id = card.dataset.id;
if (ev.target.matches('[data-perm]')) {
// Collect card perms
const inputs = Array.from(card.querySelectorAll('[data-perm]'));
const body = Object.fromEntries(inputs.map(i => [i.getAttribute('data-perm'), i.checked]));
await apiFetch(`/api/users/${encodeURIComponent(id)}/permissions`, { method: 'PUT', body: JSON.stringify(body) });
} else if (ev.target.matches('[data-role]')) {
const role = ev.target.value;
await apiFetch(`/api/users/${encodeURIComponent(id)}`, { method: 'PUT', body: JSON.stringify({ role }) });
await renderUsers();
}
};
list.onclick = async (ev) => {
const card = ev.target.closest('.card'); if (!card) return;
const id = card.dataset.id;
const actBtn = ev.target.closest('[data-act]'); if (!actBtn) return;
if (actBtn.dataset.act === 'reset') {
const pass = prompt('Nova senha:'); if (!pass) return;
await apiFetch(`/api/users/${encodeURIComponent(id)}`, { method: 'PUT', body: JSON.stringify({ password: pass }) });
alert('Senha atualizada');
} else if (actBtn.dataset.act === 'del') {
if (!confirm('Excluir este usuário?')) return;
await apiFetch(`/api/users/${encodeURIComponent(id)}`, { method: 'DELETE' });
await renderUsers();
}
};
} catch (e) {
console.error('renderUsers', e);
}
}
// Create user form
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('userForm');
if (!form) return;
form.addEventListener('submit', async (ev) => {
ev.preventDefault();
const username = document.getElementById('newUserName').value.trim();
const password = document.getElementById('newUserPass').value;
const role = document.getElementById('newUserRole').value;
const body = {
username, password, role,
permissions: {
units_read: document.getElementById('p_units_read').checked,
units_write: document.getElementById('p_units_write').checked,
creds_read: document.getElementById('p_creds_read').checked,
creds_write: document.getElementById('p_creds_write').checked,
notes_read: document.getElementById('p_notes_read').checked,
notes_write: document.getElementById('p_notes_write').checked,
}
};
try {
await apiFetch('/api/users', { method: 'POST', body: JSON.stringify(body) });
form.reset();
await renderUsers();
alert('Usuário criado');
} catch { alert('Falha ao criar usuário'); }
});
});