Software Recriado atravez do codex
This commit is contained in:
+429
-169
@@ -1,169 +1,429 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-br">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Notifier Master - Voltec</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background: #0b0b0b; color: #e0e0e0; font-family: 'Segoe UI', sans-serif; }
|
||||
.header-panel { background: #151515; padding: 20px; border-bottom: 2px solid #222; margin-bottom: 30px; }
|
||||
.node-group { background: #111; border: 1px solid #222; border-radius: 15px; padding: 20px; margin-bottom: 30px; }
|
||||
.node-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #333; margin-bottom: 20px; padding-bottom: 10px; }
|
||||
.node-title { border-left: 4px solid #d9534f; padding-left: 15px; color: #fff; font-weight: bold; margin: 0; }
|
||||
.node-status-badge { font-size: 0.7em; padding: 4px 10px; border-radius: 20px; text-transform: uppercase; font-weight: bold; }
|
||||
.card-sensor { background: #1a1a1a; border-radius: 12px; margin-bottom: 15px; padding: 15px; border-left: 5px solid #333; position: relative; }
|
||||
.badge-status { font-size: 0.75em; padding: 4px 10px; border-radius: 10px; font-weight: bold; }
|
||||
.edit-btn { position: absolute; top: 8px; right: 8px; cursor: pointer; opacity: 0.3; }
|
||||
.edit-btn:hover { opacity: 1; }
|
||||
.node-row { background: #1a1a1a; padding: 15px; border-radius: 10px; margin-bottom: 10px; border: 1px solid #333; }
|
||||
.loading-overlay { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.9); display:none; z-index:9999; justify-content:center; align-items:center; flex-direction:column; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="loader" class="loading-overlay">
|
||||
<div class="spinner-border text-danger"></div>
|
||||
<h5 class="mt-3 text-white">Varrendo central (1-600)...</h5>
|
||||
</div>
|
||||
|
||||
<div class="header-panel shadow-sm">
|
||||
<div class="container d-flex justify-content-between align-items-center">
|
||||
<h2 class="mb-0 text-danger fw-bold">🔥 Notifier Voltec</h2>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark btn-sm" onclick="abrirModalNodes()">⚙️ Config</button>
|
||||
<a href="/logout" class="btn btn-outline-danger btn-sm">Sair</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
{% if not licenciado %}
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-6 bg-dark p-5 rounded border border-danger text-center shadow-lg">
|
||||
<h2 class="text-danger">🔒 Sistema Bloqueado</h2>
|
||||
<p class="text-muted">ID: <code class="bg-black p-2 d-block my-2">{{ hwid }}</code></p>
|
||||
<textarea id="serial-in" class="form-control bg-black text-white mb-3" rows="3" placeholder="Serial"></textarea>
|
||||
<button class="btn btn-danger w-100" onclick="ativar()">ATIVAR</button>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 p-2 bg-dark rounded border border-secondary" style="gap: 15px; background-color: #151515 !important;">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="badge {% if dias_restantes < 15 %}bg-danger text-white{% else %}bg-secondary text-white{% endif %} p-2 shadow-sm">
|
||||
⏳ {{ dias_restantes }} dias restantes
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1 text-end text-truncate" style="max-width: 400px;">
|
||||
<span class="text-white fw-bold small" title="{{ lic_msg }}" style="letter-spacing: 0.5px; opacity: 0.9;">
|
||||
{{ lic_msg }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main-container"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="modalNodes" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content bg-dark border-secondary">
|
||||
<div class="modal-header border-secondary"><h5 class="modal-title">Config</h5><button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button></div>
|
||||
<div class="modal-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6"><label>Zabbix Server</label><input type="text" id="zab-server" class="form-control bg-black text-white border-secondary" value="{{config.zabbix_server}}"></div>
|
||||
<div class="col-md-6"><label>Hostname Zabbix</label><input type="text" id="zab-host" class="form-control bg-black text-white border-secondary" value="{{config.hostname_zabbix}}"></div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2"><h6>Nodes</h6><button class="btn btn-success btn-sm" onclick="addNodeRow()">+ Central</button></div>
|
||||
<div id="nodes-list"></div>
|
||||
</div>
|
||||
<div class="modal-footer border-secondary"><button class="btn btn-danger w-100" onclick="salvarTudo()">Salvar e Reiniciar</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
const modalNodes = new bootstrap.Modal(document.getElementById('modalNodes'));
|
||||
let nodesAtuais = {{ config.nodes | tojson }};
|
||||
|
||||
async function ativar() {
|
||||
const serial = document.getElementById('serial-in').value.trim();
|
||||
const res = await fetch('/api/license', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({serial}) });
|
||||
if(res.ok) { alert("Sucesso!"); location.reload(); } else { alert("Erro!"); }
|
||||
}
|
||||
|
||||
function abrirModalNodes() {
|
||||
const list = document.getElementById('nodes-list');
|
||||
list.innerHTML = '';
|
||||
nodesAtuais.forEach((node, idx) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'node-row';
|
||||
div.innerHTML = `
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-3"><input type="text" class="form-control form-control-sm n-nome" value="${node.nome}"></div>
|
||||
<div class="col-md-4"><input type="text" class="form-control form-control-sm n-ip" value="${node.ip}"></div>
|
||||
<div class="col-md-2"><input type="number" class="form-control form-control-sm n-unit" value="${node.unit}"></div>
|
||||
<div class="col-md-3 d-flex gap-1"><button class="btn btn-warning btn-sm flex-grow-1" onclick="scanIndividual(${idx})">Scan</button><button class="btn btn-danger btn-sm" onclick="this.closest('.node-row').remove()">❌</button></div>
|
||||
</div>`;
|
||||
list.appendChild(div);
|
||||
});
|
||||
modalNodes.show();
|
||||
}
|
||||
|
||||
function addNodeRow() {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'node-row';
|
||||
div.innerHTML = `<div class="row g-2 align-items-end"><div class="col-md-3"><input type="text" class="form-control form-control-sm n-nome" placeholder="Nome"></div><div class="col-md-4"><input type="text" class="form-control form-control-sm n-ip" placeholder="IP"></div><div class="col-md-2"><input type="number" class="form-control form-control-sm n-unit" value="3"></div><div class="col-md-3"><button class="btn btn-danger btn-sm w-100" onclick="this.closest('.node-row').remove()">❌</button></div></div>`;
|
||||
document.getElementById('nodes-list').appendChild(div);
|
||||
}
|
||||
|
||||
async function salvarTudo() {
|
||||
const nodes = [];
|
||||
document.querySelectorAll('.node-row').forEach(r => {
|
||||
const n = r.querySelector('.n-nome').value;
|
||||
const i = r.querySelector('.n-ip').value;
|
||||
const u = r.querySelector('.n-unit').value;
|
||||
if(n && i) nodes.push({ nome: n, ip: i, unit: parseInt(u) });
|
||||
});
|
||||
const res = await fetch('/api/nodes', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ zabbix_server: document.getElementById('zab-server').value, hostname_zabbix: document.getElementById('zab-host').value, nodes: nodes }) });
|
||||
if(res.ok) location.reload();
|
||||
}
|
||||
|
||||
async function scanIndividual(idx) {
|
||||
const rows = document.querySelectorAll('.node-row');
|
||||
const node = { nome: rows[idx].querySelector('.n-nome').value, ip: rows[idx].querySelector('.n-ip').value, unit: parseInt(rows[idx].querySelector('.n-unit').value) };
|
||||
modalNodes.hide();
|
||||
document.getElementById('loader').style.display = 'flex';
|
||||
await fetch('/api/rescan_node', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(node) });
|
||||
location.reload();
|
||||
}
|
||||
|
||||
async function renomear(key, atual) {
|
||||
const novo = prompt("Novo nome:", atual);
|
||||
if(novo && novo !== atual) { await fetch('/api/rename', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({addr: key, label: novo}) }); atualizar(); }
|
||||
}
|
||||
|
||||
async function atualizar() {
|
||||
try {
|
||||
const res = await fetch('/api/data');
|
||||
const data = await res.json();
|
||||
const main = document.getElementById('main-container');
|
||||
if(!main) return;
|
||||
const nodesMap = {};
|
||||
data.sensores.forEach(s => { if(!nodesMap[s.node]) nodesMap[s.node] = []; nodesMap[s.node].push(s); });
|
||||
main.innerHTML = '';
|
||||
for(const n in nodesMap) {
|
||||
const st = data.nodes_status[n] || "Offline";
|
||||
let h = `<div class="node-group"><div class="node-header"><h4 class="node-title">📍 ${n}</h4><span class="node-status-badge ${st==='Online'?'bg-success':'bg-danger'}">${st}</span></div><div class="row">`;
|
||||
nodesMap[n].forEach(s => {
|
||||
h += `<div class="col-md-3"><div class="card-sensor" style="border-left-color: ${s.color}"><span class="edit-btn" onclick="renomear('${s.key}', '${s.label}')">✏️</span><div class="fw-bold text-white small">${s.label}</div><div class="mt-2"><span class="badge-status" style="background: ${s.color}22; color: ${s.color}; border: 1px solid ${s.color}">${s.status_text}</span></div><div class="time-upd text-end">⏱ ${s.last}</div></div></div>`;
|
||||
});
|
||||
main.innerHTML += h + `</div></div>`;
|
||||
}
|
||||
} catch(e){}
|
||||
}
|
||||
setInterval(atualizar, 3000); atualizar();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>V-Fire Monitor</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0c0f12;
|
||||
--panel: #12171d;
|
||||
--panel-soft: #1a2129;
|
||||
--border: #2a333d;
|
||||
--text: #edf2f7;
|
||||
--muted: #94a3b8;
|
||||
--accent: #ff5a3d;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(255, 90, 61, 0.15), transparent 30%),
|
||||
linear-gradient(180deg, #091017 0%, var(--bg) 45%, #07090c 100%);
|
||||
color: var(--text);
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.app-header,
|
||||
.panel,
|
||||
.sensor-card,
|
||||
.node-row {
|
||||
background: rgba(18, 23, 29, 0.95);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.app-header {
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.panel {
|
||||
border-radius: 18px;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.node-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
margin: 0;
|
||||
padding-left: 0.9rem;
|
||||
border-left: 4px solid var(--accent);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sensor-card {
|
||||
height: 100%;
|
||||
border-radius: 14px;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
transition: transform 0.16s ease, border-color 0.16s ease;
|
||||
}
|
||||
|
||||
.sensor-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.sensor-label {
|
||||
padding-right: 2rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.edit-btn:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.badge-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.node-row {
|
||||
border-radius: 12px;
|
||||
padding: 0.9rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(7, 9, 12, 0.85);
|
||||
z-index: 9999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loader" class="loading-overlay">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-danger"></div>
|
||||
<p class="mt-3 mb-0 text-light">Executando varredura Modbus...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<header class="app-header border-bottom sticky-top">
|
||||
<div class="container py-3 d-flex flex-wrap justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<div class="text-uppercase small text-secondary">Voltec</div>
|
||||
<h1 class="h3 mb-0">V-Fire Monitor</h1>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-outline-light btn-sm" onclick="openConfigModal()">Configurar</button>
|
||||
<a href="/logout" class="btn btn-outline-danger btn-sm">Sair</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container py-4">
|
||||
<div id="alert-host"></div>
|
||||
|
||||
{% if not licenciado %}
|
||||
<section class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="panel text-center">
|
||||
<h2 class="h4 text-danger">Sistema bloqueado</h2>
|
||||
<p class="text-secondary mb-3">Ative a licenca deste equipamento para liberar o monitoramento.</p>
|
||||
<p class="text-secondary mb-2">Hardware ID</p>
|
||||
<code class="d-block p-3 rounded bg-dark text-light mb-3">{{ hwid }}</code>
|
||||
<textarea id="serial-in" class="form-control bg-dark text-light border-secondary mb-3" rows="4" placeholder="Cole o serial de licenca"></textarea>
|
||||
<button class="btn btn-danger w-100" onclick="activateLicense()">Ativar licenca</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="panel mb-4 d-flex flex-wrap justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<div class="text-secondary small">Licenca</div>
|
||||
<div class="fw-semibold">{{ lic_msg }}</div>
|
||||
</div>
|
||||
<span class="badge {% if dias_restantes < 15 %}bg-danger{% else %}bg-secondary{% endif %} fs-6">
|
||||
{{ dias_restantes }} dias restantes
|
||||
</span>
|
||||
</section>
|
||||
|
||||
<section id="main-container"></section>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
<div class="modal fade" id="modalNodes" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content bg-dark text-light border-secondary">
|
||||
<div class="modal-header border-secondary">
|
||||
<h2 class="modal-title fs-5">Configuracao</h2>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="zab-server" class="form-label">Zabbix Server</label>
|
||||
<input type="text" id="zab-server" class="form-control bg-black text-light border-secondary" value="{{ config.zabbix_server }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="zab-host" class="form-label">Hostname Zabbix</label>
|
||||
<input type="text" id="zab-host" class="form-control bg-black text-light border-secondary" value="{{ config.hostname_zabbix }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h3 class="h6 mb-0">Centrais</h3>
|
||||
<button class="btn btn-success btn-sm" onclick="addNodeRow()">Adicionar central</button>
|
||||
</div>
|
||||
<div id="nodes-list"></div>
|
||||
</div>
|
||||
<div class="modal-footer border-secondary">
|
||||
<button class="btn btn-danger w-100" onclick="saveConfig()">Salvar configuracao</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
const modalNodes = new bootstrap.Modal(document.getElementById("modalNodes"));
|
||||
let nodesAtuais = {{ config.nodes | tojson }};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function showAlert(message, type = "danger") {
|
||||
const host = document.getElementById("alert-host");
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
host.innerHTML = `
|
||||
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
|
||||
${escapeHtml(message)}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function apiFetch(url, options = {}) {
|
||||
const response = await fetch(url, options);
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(body.message || "Falha ao processar a requisicao.");
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
function renderNodeRow(node = { nome: "", ip: "", unit: 3 }) {
|
||||
const container = document.createElement("div");
|
||||
container.className = "node-row";
|
||||
container.innerHTML = `
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-secondary">Nome</label>
|
||||
<input type="text" class="form-control form-control-sm n-nome" value="${escapeHtml(node.nome)}" placeholder="Nome">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small text-secondary">IP</label>
|
||||
<input type="text" class="form-control form-control-sm n-ip" value="${escapeHtml(node.ip)}" placeholder="192.168.0.10">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small text-secondary">Unit</label>
|
||||
<input type="number" class="form-control form-control-sm n-unit" value="${Number(node.unit || 3)}" min="0" max="255">
|
||||
</div>
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button class="btn btn-warning btn-sm flex-grow-1 scan-btn" type="button">Scan</button>
|
||||
<button class="btn btn-outline-danger btn-sm remove-btn" type="button">Remover</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
container.querySelector(".remove-btn").addEventListener("click", () => container.remove());
|
||||
container.querySelector(".scan-btn").addEventListener("click", () => scanSingleNode(container));
|
||||
return container;
|
||||
}
|
||||
|
||||
function openConfigModal() {
|
||||
const list = document.getElementById("nodes-list");
|
||||
list.innerHTML = "";
|
||||
nodesAtuais.forEach((node) => list.appendChild(renderNodeRow(node)));
|
||||
modalNodes.show();
|
||||
}
|
||||
|
||||
function addNodeRow() {
|
||||
document.getElementById("nodes-list").appendChild(renderNodeRow());
|
||||
}
|
||||
|
||||
function collectNodes() {
|
||||
const nodes = [];
|
||||
document.querySelectorAll(".node-row").forEach((row) => {
|
||||
const nome = row.querySelector(".n-nome").value.trim();
|
||||
const ip = row.querySelector(".n-ip").value.trim();
|
||||
const unit = Number(row.querySelector(".n-unit").value);
|
||||
if (nome && ip) {
|
||||
nodes.push({ nome, ip, unit });
|
||||
}
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
async function activateLicense() {
|
||||
const serial = document.getElementById("serial-in").value.trim();
|
||||
try {
|
||||
await apiFetch("/api/license", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ serial })
|
||||
});
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
showAlert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
const payload = {
|
||||
zabbix_server: document.getElementById("zab-server").value.trim(),
|
||||
hostname_zabbix: document.getElementById("zab-host").value.trim(),
|
||||
nodes: collectNodes()
|
||||
};
|
||||
|
||||
try {
|
||||
await apiFetch("/api/nodes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
showAlert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function scanSingleNode(row) {
|
||||
const node = {
|
||||
nome: row.querySelector(".n-nome").value.trim(),
|
||||
ip: row.querySelector(".n-ip").value.trim(),
|
||||
unit: Number(row.querySelector(".n-unit").value)
|
||||
};
|
||||
|
||||
document.getElementById("loader").style.display = "flex";
|
||||
modalNodes.hide();
|
||||
|
||||
try {
|
||||
const data = await apiFetch("/api/rescan_node", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(node)
|
||||
});
|
||||
showAlert(`Scan concluido. ${data.count} pontos encontrados.`, "success");
|
||||
setTimeout(() => location.reload(), 500);
|
||||
} catch (error) {
|
||||
document.getElementById("loader").style.display = "none";
|
||||
showAlert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function renameDevice(key, currentLabel) {
|
||||
const nextLabel = prompt("Novo nome do dispositivo:", currentLabel);
|
||||
if (!nextLabel || nextLabel === currentLabel) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiFetch("/api/rename", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ addr: key, label: nextLabel })
|
||||
});
|
||||
await refreshData();
|
||||
} catch (error) {
|
||||
showAlert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function renderNodes(data) {
|
||||
const main = document.getElementById("main-container");
|
||||
if (!main) {
|
||||
return;
|
||||
}
|
||||
|
||||
const grouped = {};
|
||||
data.sensores.forEach((sensor) => {
|
||||
if (!grouped[sensor.node]) {
|
||||
grouped[sensor.node] = [];
|
||||
}
|
||||
grouped[sensor.node].push(sensor);
|
||||
});
|
||||
|
||||
const html = Object.entries(grouped).map(([nodeName, sensors]) => {
|
||||
const nodeStatus = data.nodes_status[nodeName] || "Offline";
|
||||
const statusClass = nodeStatus === "Online" ? "bg-success" : "bg-danger";
|
||||
|
||||
const sensorCards = sensors.map((sensor) => `
|
||||
<div class="col-md-4 col-xl-3">
|
||||
<article class="sensor-card" style="border-left: 5px solid ${escapeHtml(sensor.color)}">
|
||||
<button class="edit-btn" type="button" data-key="${escapeHtml(sensor.key)}" data-label="${escapeHtml(sensor.label)}">Editar</button>
|
||||
<div class="sensor-label fw-semibold">${escapeHtml(sensor.label)}</div>
|
||||
<div class="mt-3">
|
||||
<span class="badge-status" style="background: ${escapeHtml(sensor.color)}22; color: ${escapeHtml(sensor.color)}; border: 1px solid ${escapeHtml(sensor.color)};">
|
||||
${escapeHtml(sensor.status_text)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="small text-secondary mt-3 text-end">Atualizado as ${escapeHtml(sensor.last)}</div>
|
||||
</article>
|
||||
</div>`).join("");
|
||||
|
||||
return `
|
||||
<section class="panel node-group">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 gap-3">
|
||||
<h2 class="node-title">${escapeHtml(nodeName)}</h2>
|
||||
<span class="badge ${statusClass}">${escapeHtml(nodeStatus)}</span>
|
||||
</div>
|
||||
<div class="row g-3">${sensorCards}</div>
|
||||
</section>`;
|
||||
}).join("");
|
||||
|
||||
main.innerHTML = html || `
|
||||
<section class="panel text-center text-secondary">
|
||||
Nenhum dispositivo encontrado ainda. Execute um scan em uma central configurada.
|
||||
</section>`;
|
||||
|
||||
document.querySelectorAll(".edit-btn").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
renameDevice(button.dataset.key, button.dataset.label);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshData() {
|
||||
try {
|
||||
const data = await apiFetch("/api/data");
|
||||
renderNodes(data);
|
||||
} catch (error) {
|
||||
showAlert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
{% if licenciado %}
|
||||
setInterval(refreshData, 3000);
|
||||
refreshData();
|
||||
{% endif %}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+55
-22
@@ -1,22 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Login - Notifier</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background: #0f0f0f; color: white; display: flex; align-items: center; height: 100vh; justify-content: center; }
|
||||
.login-card { background: #1a1a1a; padding: 30px; border-radius: 15px; border: 1px solid #333; width: 320px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card shadow">
|
||||
<h4 class="text-center text-danger mb-4">Acesso Notifier</h4>
|
||||
<form method="POST">
|
||||
<div class="mb-3"><label>Usuário</label><input type="text" name="username" class="form-control bg-dark text-white border-secondary" required></div>
|
||||
<div class="mb-3"><label>Senha</label><input type="password" name="password" class="form-control bg-dark text-white border-secondary" required></div>
|
||||
<button type="submit" class="btn btn-danger w-100">Entrar</button>
|
||||
{% with messages = get_flashed_messages() %}{% if messages %}<div class="text-warning mt-2 small text-center">{{ messages[0] }}</div>{% endif %}{% endwith %}
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Login | V-Fire Monitor</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(255, 90, 61, 0.2), transparent 30%),
|
||||
linear-gradient(180deg, #0b1117 0%, #07090c 100%);
|
||||
color: #eef2f7;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: min(100%, 380px);
|
||||
padding: 2rem;
|
||||
border-radius: 18px;
|
||||
background: rgba(18, 23, 29, 0.96);
|
||||
border: 1px solid #2a333d;
|
||||
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<section class="login-card">
|
||||
<div class="text-uppercase small text-secondary mb-2">Voltec</div>
|
||||
<h1 class="h4 mb-4">Acesso ao V-Fire Monitor</h1>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="alert alert-warning py-2" role="alert">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" novalidate>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Usuario</label>
|
||||
<input id="username" type="text" name="username" class="form-control bg-dark text-light border-secondary" required autofocus>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label">Senha</label>
|
||||
<input id="password" type="password" name="password" class="form-control bg-dark text-light border-secondary" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger w-100">Entrar</button>
|
||||
</form>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user