chore(tenants): conteudo de publico/ atualizado

This commit is contained in:
deploy
2026-09-26 15:53:24 +00:00
parent b55b9c871a
commit 711bee7fc1
10 changed files with 1410 additions and 20 deletions
+141 -2
View File
@@ -1981,6 +1981,38 @@ data:
.topo .acao:hover, .topo .sair:hover { border-color: var(--orange); color: var(--orange-600); }
.topo .acao:disabled { opacity: .6; cursor: wait; }
/* ── sino de alertas de infraestrutura ───────────────────────────────────── */
.sino-caixa { position: relative; }
.sino {
background: none; border: 1px solid var(--line); color: var(--navy);
border-radius: 10px; padding: 6px 10px; cursor: pointer; font-size: 16px;
line-height: 1; position: relative;
}
.sino:hover { border-color: var(--orange); color: var(--orange-600); }
.sino[aria-expanded="true"] { border-color: var(--orange); }
.sino-contagem {
position: absolute; top: -7px; right: -7px;
background: var(--perigo); color: var(--branco); border-radius: 999px;
font-size: 11px; font-weight: 700; padding: 1px 6px; line-height: 1.4; min-width: 14px; text-align: center;
}
.sino-contagem.aviso { background: var(--alerta); }
.sino-contagem.sem-dado { background: var(--muted); }
.sino-painel {
position: absolute; right: 0; top: calc(100% + 10px);
width: min(360px, calc(100vw - 32px)); max-height: 60vh; overflow: auto;
background: var(--branco); border: 1px solid var(--line); border-radius: 14px;
box-shadow: 0 14px 34px rgba(11,44,61,.16); padding: 14px; z-index: 20; text-align: left;
}
.sino-topo { font-weight: 700; margin-bottom: 10px; }
.sino-lista { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.sino-item { border-radius: 10px; padding: 10px 12px; font-size: 13px; }
.sino-item.critical { background: #fdecea; }
.sino-item.warning { background: #fdf3e6; }
.sino-item.info { background: var(--paper); }
.sino-item .nome { font-weight: 700; display: block; }
.sino-item .resumo { display: block; margin-top: 2px; }
.sino-item .quem { color: var(--muted); font-size: 12px; display: block; margin-top: 4px; }
.wrap { max-width: 1320px; margin: 0 auto; padding: 22px 28px 48px; }
/* ── cartoes ──────────────────────────────────────────────────────────────── */
@@ -2226,7 +2258,7 @@ data:
`Cache-Control: immutable` por um ano. Suba o numero a cada publicacao.
-->
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/painel.css?v=2.82">
<link rel="stylesheet" href="/publico/painel.css?v=2.83">
</head>
<body>
@@ -2234,6 +2266,18 @@ data:
<img class="logo" src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37">
<span class="titulo">Painel do dono</span>
<span id="apurado" class="apurado oculto"></span>
<div class="sino-caixa oculto" id="sinoCaixa">
<button type="button" id="sino" class="sino" aria-haspopup="true" aria-expanded="false" title="Alertas de infraestrutura">
<span aria-hidden="true">&#128276;</span>
<span id="sinoContagem" class="sino-contagem oculto"></span>
</button>
<div id="sinoPainel" class="sino-painel oculto" role="menu">
<div class="sino-topo">Alertas de infraestrutura</div>
<ul id="sinoLista" class="sino-lista"></ul>
<p id="sinoVazio" class="ajuda oculto">Nenhum alerta agora.</p>
<p id="sinoIndisponivel" class="ajuda oculto">Não consegui falar com o monitoramento agora.</p>
</div>
</div>
<button type="button" id="atualizar" class="acao oculto">Atualizar</button>
<button type="button" id="sair" class="sair oculto">Sair</button>
</header>
@@ -2388,7 +2432,7 @@ data:
</main>
<script src="/publico/painel.js?v=2.82"></script>
<script src="/publico/painel.js?v=2.83"></script>
</body>
</html>
@@ -2422,16 +2466,22 @@ data:
"use strict";
var API = "/api/plataforma/painel/visao";
var API_ALERTAS = "/api/plataforma/painel/alertas-infra";
var CHAVE = "atm.painel.token";
var INTERVALO_SINO_MS = 60000;
var $ = function (id) { return document.getElementById(id); };
var porta = $("porta"), conteudo = $("conteudo"), carregando = $("carregando");
var formEntrar = $("formEntrar"), campoToken = $("token"), erroEntrar = $("erroEntrar");
var botaoSair = $("sair"), botaoAtualizar = $("atualizar"), apurado = $("apurado");
var corpo = $("linhas"), vazio = $("vazio");
var sinoCaixa = $("sinoCaixa"), sino = $("sino"), sinoContagem = $("sinoContagem"),
sinoPainel = $("sinoPainel"), sinoLista = $("sinoLista"),
sinoVazio = $("sinoVazio"), sinoIndisponivel = $("sinoIndisponivel");
var visao = null;
var tokenAtual = null;
var sinoIntervalo = null;
/* ── token ──────────────────────────────────────────────────────────────── */
@@ -2448,6 +2498,83 @@ data:
});
}
/* ── sino de alertas de infraestrutura ─────────────────────────────────────
*
* Le o mesmo Alertmanager que ja manda e-mail — o sino nao inventa alerta
* nenhum, so mostra o que o e-mail ja avisou. Falha calada de proposito:
* se o servidor nao responder, o sino so fica "sem dado" — nao pode
* derrubar o painel inteiro por causa de um widget auxiliar.
*/
function buscarAlertasInfra(token) {
return fetch(API_ALERTAS, { headers: { "x-atm-admin-token": token } })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.catch(function () { return null; }); // null = "sem dado", diferente de "zero alertas"
}
function pesoSeveridade(s) { return s === "critical" ? 3 : s === "warning" ? 2 : 1; }
function pintarSino(dados) {
sinoLista.innerHTML = "";
sinoVazio.classList.add("oculto");
sinoIndisponivel.classList.add("oculto");
if (!dados || dados.indisponivel) {
sinoContagem.textContent = "!";
sinoContagem.className = "sino-contagem sem-dado";
sinoIndisponivel.classList.remove("oculto");
return;
}
var alertas = dados.alertas || [];
if (alertas.length === 0) {
sinoContagem.classList.add("oculto");
sinoVazio.classList.remove("oculto");
return;
}
sinoContagem.textContent = String(alertas.length);
sinoContagem.className = "sino-contagem" +
(alertas.some(function (a) { return a.severidade === "critical"; }) ? "" : " aviso");
sinoContagem.classList.remove("oculto");
alertas.forEach(function (a) {
var li = document.createElement("li");
var sev = pesoSeveridade(a.severidade) === 3 ? "critical" : pesoSeveridade(a.severidade) === 2 ? "warning" : "info";
li.className = "sino-item " + sev;
li.innerHTML = "<span class=\"nome\">" + esc(a.nome) + "</span>"
+ (a.resumo ? "<span class=\"resumo\">" + esc(a.resumo) + "</span>" : "")
+ (a.tenant ? "<span class=\"quem\">" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "</span>"
: (a.desde ? "<span class=\"quem\">" + ha(a.desde) + "</span>" : ""));
sinoLista.appendChild(li);
});
}
function atualizarSino(token) {
buscarAlertasInfra(token).then(pintarSino);
}
function ligarSino(token) {
sinoCaixa.classList.remove("oculto");
atualizarSino(token);
if (sinoIntervalo) clearInterval(sinoIntervalo);
sinoIntervalo = setInterval(function () { atualizarSino(token); }, INTERVALO_SINO_MS);
}
function desligarSino() {
sinoCaixa.classList.add("oculto");
fecharSino();
if (sinoIntervalo) { clearInterval(sinoIntervalo); sinoIntervalo = null; }
}
function abrirSino() {
sinoPainel.classList.remove("oculto");
sino.setAttribute("aria-expanded", "true");
}
function fecharSino() {
sinoPainel.classList.add("oculto");
sino.setAttribute("aria-expanded", "false");
}
/* ── formatacao ─────────────────────────────────────────────────────────── */
function esc(s) {
@@ -2958,6 +3085,7 @@ data:
tokenAtual = token;
pintar();
mostrar(conteudo);
ligarSino(token);
});
}
@@ -2992,6 +3120,17 @@ data:
location.reload();
});
sino.addEventListener("click", function () {
var abrindo = sinoPainel.classList.contains("oculto");
if (abrindo) { abrirSino(); if (tokenAtual) atualizarSino(tokenAtual); } else { fecharSino(); }
});
document.addEventListener("click", function (ev) {
if (!sinoPainel.classList.contains("oculto") && !sinoCaixa.contains(ev.target)) fecharSino();
});
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape") fecharSino();
});
botaoAtualizar.addEventListener("click", function () {
if (!tokenAtual) return;
botaoAtualizar.disabled = true;
+141 -2
View File
@@ -1981,6 +1981,38 @@ data:
.topo .acao:hover, .topo .sair:hover { border-color: var(--orange); color: var(--orange-600); }
.topo .acao:disabled { opacity: .6; cursor: wait; }
/* ── sino de alertas de infraestrutura ───────────────────────────────────── */
.sino-caixa { position: relative; }
.sino {
background: none; border: 1px solid var(--line); color: var(--navy);
border-radius: 10px; padding: 6px 10px; cursor: pointer; font-size: 16px;
line-height: 1; position: relative;
}
.sino:hover { border-color: var(--orange); color: var(--orange-600); }
.sino[aria-expanded="true"] { border-color: var(--orange); }
.sino-contagem {
position: absolute; top: -7px; right: -7px;
background: var(--perigo); color: var(--branco); border-radius: 999px;
font-size: 11px; font-weight: 700; padding: 1px 6px; line-height: 1.4; min-width: 14px; text-align: center;
}
.sino-contagem.aviso { background: var(--alerta); }
.sino-contagem.sem-dado { background: var(--muted); }
.sino-painel {
position: absolute; right: 0; top: calc(100% + 10px);
width: min(360px, calc(100vw - 32px)); max-height: 60vh; overflow: auto;
background: var(--branco); border: 1px solid var(--line); border-radius: 14px;
box-shadow: 0 14px 34px rgba(11,44,61,.16); padding: 14px; z-index: 20; text-align: left;
}
.sino-topo { font-weight: 700; margin-bottom: 10px; }
.sino-lista { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.sino-item { border-radius: 10px; padding: 10px 12px; font-size: 13px; }
.sino-item.critical { background: #fdecea; }
.sino-item.warning { background: #fdf3e6; }
.sino-item.info { background: var(--paper); }
.sino-item .nome { font-weight: 700; display: block; }
.sino-item .resumo { display: block; margin-top: 2px; }
.sino-item .quem { color: var(--muted); font-size: 12px; display: block; margin-top: 4px; }
.wrap { max-width: 1320px; margin: 0 auto; padding: 22px 28px 48px; }
/* ── cartoes ──────────────────────────────────────────────────────────────── */
@@ -2226,7 +2258,7 @@ data:
`Cache-Control: immutable` por um ano. Suba o numero a cada publicacao.
-->
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/painel.css?v=2.82">
<link rel="stylesheet" href="/publico/painel.css?v=2.83">
</head>
<body>
@@ -2234,6 +2266,18 @@ data:
<img class="logo" src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37">
<span class="titulo">Painel do dono</span>
<span id="apurado" class="apurado oculto"></span>
<div class="sino-caixa oculto" id="sinoCaixa">
<button type="button" id="sino" class="sino" aria-haspopup="true" aria-expanded="false" title="Alertas de infraestrutura">
<span aria-hidden="true">&#128276;</span>
<span id="sinoContagem" class="sino-contagem oculto"></span>
</button>
<div id="sinoPainel" class="sino-painel oculto" role="menu">
<div class="sino-topo">Alertas de infraestrutura</div>
<ul id="sinoLista" class="sino-lista"></ul>
<p id="sinoVazio" class="ajuda oculto">Nenhum alerta agora.</p>
<p id="sinoIndisponivel" class="ajuda oculto">Não consegui falar com o monitoramento agora.</p>
</div>
</div>
<button type="button" id="atualizar" class="acao oculto">Atualizar</button>
<button type="button" id="sair" class="sair oculto">Sair</button>
</header>
@@ -2388,7 +2432,7 @@ data:
</main>
<script src="/publico/painel.js?v=2.82"></script>
<script src="/publico/painel.js?v=2.83"></script>
</body>
</html>
@@ -2422,16 +2466,22 @@ data:
"use strict";
var API = "/api/plataforma/painel/visao";
var API_ALERTAS = "/api/plataforma/painel/alertas-infra";
var CHAVE = "atm.painel.token";
var INTERVALO_SINO_MS = 60000;
var $ = function (id) { return document.getElementById(id); };
var porta = $("porta"), conteudo = $("conteudo"), carregando = $("carregando");
var formEntrar = $("formEntrar"), campoToken = $("token"), erroEntrar = $("erroEntrar");
var botaoSair = $("sair"), botaoAtualizar = $("atualizar"), apurado = $("apurado");
var corpo = $("linhas"), vazio = $("vazio");
var sinoCaixa = $("sinoCaixa"), sino = $("sino"), sinoContagem = $("sinoContagem"),
sinoPainel = $("sinoPainel"), sinoLista = $("sinoLista"),
sinoVazio = $("sinoVazio"), sinoIndisponivel = $("sinoIndisponivel");
var visao = null;
var tokenAtual = null;
var sinoIntervalo = null;
/* ── token ──────────────────────────────────────────────────────────────── */
@@ -2448,6 +2498,83 @@ data:
});
}
/* ── sino de alertas de infraestrutura ─────────────────────────────────────
*
* Le o mesmo Alertmanager que ja manda e-mail — o sino nao inventa alerta
* nenhum, so mostra o que o e-mail ja avisou. Falha calada de proposito:
* se o servidor nao responder, o sino so fica "sem dado" — nao pode
* derrubar o painel inteiro por causa de um widget auxiliar.
*/
function buscarAlertasInfra(token) {
return fetch(API_ALERTAS, { headers: { "x-atm-admin-token": token } })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.catch(function () { return null; }); // null = "sem dado", diferente de "zero alertas"
}
function pesoSeveridade(s) { return s === "critical" ? 3 : s === "warning" ? 2 : 1; }
function pintarSino(dados) {
sinoLista.innerHTML = "";
sinoVazio.classList.add("oculto");
sinoIndisponivel.classList.add("oculto");
if (!dados || dados.indisponivel) {
sinoContagem.textContent = "!";
sinoContagem.className = "sino-contagem sem-dado";
sinoIndisponivel.classList.remove("oculto");
return;
}
var alertas = dados.alertas || [];
if (alertas.length === 0) {
sinoContagem.classList.add("oculto");
sinoVazio.classList.remove("oculto");
return;
}
sinoContagem.textContent = String(alertas.length);
sinoContagem.className = "sino-contagem" +
(alertas.some(function (a) { return a.severidade === "critical"; }) ? "" : " aviso");
sinoContagem.classList.remove("oculto");
alertas.forEach(function (a) {
var li = document.createElement("li");
var sev = pesoSeveridade(a.severidade) === 3 ? "critical" : pesoSeveridade(a.severidade) === 2 ? "warning" : "info";
li.className = "sino-item " + sev;
li.innerHTML = "<span class=\"nome\">" + esc(a.nome) + "</span>"
+ (a.resumo ? "<span class=\"resumo\">" + esc(a.resumo) + "</span>" : "")
+ (a.tenant ? "<span class=\"quem\">" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "</span>"
: (a.desde ? "<span class=\"quem\">" + ha(a.desde) + "</span>" : ""));
sinoLista.appendChild(li);
});
}
function atualizarSino(token) {
buscarAlertasInfra(token).then(pintarSino);
}
function ligarSino(token) {
sinoCaixa.classList.remove("oculto");
atualizarSino(token);
if (sinoIntervalo) clearInterval(sinoIntervalo);
sinoIntervalo = setInterval(function () { atualizarSino(token); }, INTERVALO_SINO_MS);
}
function desligarSino() {
sinoCaixa.classList.add("oculto");
fecharSino();
if (sinoIntervalo) { clearInterval(sinoIntervalo); sinoIntervalo = null; }
}
function abrirSino() {
sinoPainel.classList.remove("oculto");
sino.setAttribute("aria-expanded", "true");
}
function fecharSino() {
sinoPainel.classList.add("oculto");
sino.setAttribute("aria-expanded", "false");
}
/* ── formatacao ─────────────────────────────────────────────────────────── */
function esc(s) {
@@ -2958,6 +3085,7 @@ data:
tokenAtual = token;
pintar();
mostrar(conteudo);
ligarSino(token);
});
}
@@ -2992,6 +3120,17 @@ data:
location.reload();
});
sino.addEventListener("click", function () {
var abrindo = sinoPainel.classList.contains("oculto");
if (abrindo) { abrirSino(); if (tokenAtual) atualizarSino(tokenAtual); } else { fecharSino(); }
});
document.addEventListener("click", function (ev) {
if (!sinoPainel.classList.contains("oculto") && !sinoCaixa.contains(ev.target)) fecharSino();
});
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape") fecharSino();
});
botaoAtualizar.addEventListener("click", function () {
if (!tokenAtual) return;
botaoAtualizar.disabled = true;
+141 -2
View File
@@ -1981,6 +1981,38 @@ data:
.topo .acao:hover, .topo .sair:hover { border-color: var(--orange); color: var(--orange-600); }
.topo .acao:disabled { opacity: .6; cursor: wait; }
/* ── sino de alertas de infraestrutura ───────────────────────────────────── */
.sino-caixa { position: relative; }
.sino {
background: none; border: 1px solid var(--line); color: var(--navy);
border-radius: 10px; padding: 6px 10px; cursor: pointer; font-size: 16px;
line-height: 1; position: relative;
}
.sino:hover { border-color: var(--orange); color: var(--orange-600); }
.sino[aria-expanded="true"] { border-color: var(--orange); }
.sino-contagem {
position: absolute; top: -7px; right: -7px;
background: var(--perigo); color: var(--branco); border-radius: 999px;
font-size: 11px; font-weight: 700; padding: 1px 6px; line-height: 1.4; min-width: 14px; text-align: center;
}
.sino-contagem.aviso { background: var(--alerta); }
.sino-contagem.sem-dado { background: var(--muted); }
.sino-painel {
position: absolute; right: 0; top: calc(100% + 10px);
width: min(360px, calc(100vw - 32px)); max-height: 60vh; overflow: auto;
background: var(--branco); border: 1px solid var(--line); border-radius: 14px;
box-shadow: 0 14px 34px rgba(11,44,61,.16); padding: 14px; z-index: 20; text-align: left;
}
.sino-topo { font-weight: 700; margin-bottom: 10px; }
.sino-lista { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.sino-item { border-radius: 10px; padding: 10px 12px; font-size: 13px; }
.sino-item.critical { background: #fdecea; }
.sino-item.warning { background: #fdf3e6; }
.sino-item.info { background: var(--paper); }
.sino-item .nome { font-weight: 700; display: block; }
.sino-item .resumo { display: block; margin-top: 2px; }
.sino-item .quem { color: var(--muted); font-size: 12px; display: block; margin-top: 4px; }
.wrap { max-width: 1320px; margin: 0 auto; padding: 22px 28px 48px; }
/* ── cartoes ──────────────────────────────────────────────────────────────── */
@@ -2226,7 +2258,7 @@ data:
`Cache-Control: immutable` por um ano. Suba o numero a cada publicacao.
-->
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/painel.css?v=2.82">
<link rel="stylesheet" href="/publico/painel.css?v=2.83">
</head>
<body>
@@ -2234,6 +2266,18 @@ data:
<img class="logo" src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37">
<span class="titulo">Painel do dono</span>
<span id="apurado" class="apurado oculto"></span>
<div class="sino-caixa oculto" id="sinoCaixa">
<button type="button" id="sino" class="sino" aria-haspopup="true" aria-expanded="false" title="Alertas de infraestrutura">
<span aria-hidden="true">&#128276;</span>
<span id="sinoContagem" class="sino-contagem oculto"></span>
</button>
<div id="sinoPainel" class="sino-painel oculto" role="menu">
<div class="sino-topo">Alertas de infraestrutura</div>
<ul id="sinoLista" class="sino-lista"></ul>
<p id="sinoVazio" class="ajuda oculto">Nenhum alerta agora.</p>
<p id="sinoIndisponivel" class="ajuda oculto">Não consegui falar com o monitoramento agora.</p>
</div>
</div>
<button type="button" id="atualizar" class="acao oculto">Atualizar</button>
<button type="button" id="sair" class="sair oculto">Sair</button>
</header>
@@ -2388,7 +2432,7 @@ data:
</main>
<script src="/publico/painel.js?v=2.82"></script>
<script src="/publico/painel.js?v=2.83"></script>
</body>
</html>
@@ -2422,16 +2466,22 @@ data:
"use strict";
var API = "/api/plataforma/painel/visao";
var API_ALERTAS = "/api/plataforma/painel/alertas-infra";
var CHAVE = "atm.painel.token";
var INTERVALO_SINO_MS = 60000;
var $ = function (id) { return document.getElementById(id); };
var porta = $("porta"), conteudo = $("conteudo"), carregando = $("carregando");
var formEntrar = $("formEntrar"), campoToken = $("token"), erroEntrar = $("erroEntrar");
var botaoSair = $("sair"), botaoAtualizar = $("atualizar"), apurado = $("apurado");
var corpo = $("linhas"), vazio = $("vazio");
var sinoCaixa = $("sinoCaixa"), sino = $("sino"), sinoContagem = $("sinoContagem"),
sinoPainel = $("sinoPainel"), sinoLista = $("sinoLista"),
sinoVazio = $("sinoVazio"), sinoIndisponivel = $("sinoIndisponivel");
var visao = null;
var tokenAtual = null;
var sinoIntervalo = null;
/* ── token ──────────────────────────────────────────────────────────────── */
@@ -2448,6 +2498,83 @@ data:
});
}
/* ── sino de alertas de infraestrutura ─────────────────────────────────────
*
* Le o mesmo Alertmanager que ja manda e-mail — o sino nao inventa alerta
* nenhum, so mostra o que o e-mail ja avisou. Falha calada de proposito:
* se o servidor nao responder, o sino so fica "sem dado" — nao pode
* derrubar o painel inteiro por causa de um widget auxiliar.
*/
function buscarAlertasInfra(token) {
return fetch(API_ALERTAS, { headers: { "x-atm-admin-token": token } })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.catch(function () { return null; }); // null = "sem dado", diferente de "zero alertas"
}
function pesoSeveridade(s) { return s === "critical" ? 3 : s === "warning" ? 2 : 1; }
function pintarSino(dados) {
sinoLista.innerHTML = "";
sinoVazio.classList.add("oculto");
sinoIndisponivel.classList.add("oculto");
if (!dados || dados.indisponivel) {
sinoContagem.textContent = "!";
sinoContagem.className = "sino-contagem sem-dado";
sinoIndisponivel.classList.remove("oculto");
return;
}
var alertas = dados.alertas || [];
if (alertas.length === 0) {
sinoContagem.classList.add("oculto");
sinoVazio.classList.remove("oculto");
return;
}
sinoContagem.textContent = String(alertas.length);
sinoContagem.className = "sino-contagem" +
(alertas.some(function (a) { return a.severidade === "critical"; }) ? "" : " aviso");
sinoContagem.classList.remove("oculto");
alertas.forEach(function (a) {
var li = document.createElement("li");
var sev = pesoSeveridade(a.severidade) === 3 ? "critical" : pesoSeveridade(a.severidade) === 2 ? "warning" : "info";
li.className = "sino-item " + sev;
li.innerHTML = "<span class=\"nome\">" + esc(a.nome) + "</span>"
+ (a.resumo ? "<span class=\"resumo\">" + esc(a.resumo) + "</span>" : "")
+ (a.tenant ? "<span class=\"quem\">" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "</span>"
: (a.desde ? "<span class=\"quem\">" + ha(a.desde) + "</span>" : ""));
sinoLista.appendChild(li);
});
}
function atualizarSino(token) {
buscarAlertasInfra(token).then(pintarSino);
}
function ligarSino(token) {
sinoCaixa.classList.remove("oculto");
atualizarSino(token);
if (sinoIntervalo) clearInterval(sinoIntervalo);
sinoIntervalo = setInterval(function () { atualizarSino(token); }, INTERVALO_SINO_MS);
}
function desligarSino() {
sinoCaixa.classList.add("oculto");
fecharSino();
if (sinoIntervalo) { clearInterval(sinoIntervalo); sinoIntervalo = null; }
}
function abrirSino() {
sinoPainel.classList.remove("oculto");
sino.setAttribute("aria-expanded", "true");
}
function fecharSino() {
sinoPainel.classList.add("oculto");
sino.setAttribute("aria-expanded", "false");
}
/* ── formatacao ─────────────────────────────────────────────────────────── */
function esc(s) {
@@ -2958,6 +3085,7 @@ data:
tokenAtual = token;
pintar();
mostrar(conteudo);
ligarSino(token);
});
}
@@ -2992,6 +3120,17 @@ data:
location.reload();
});
sino.addEventListener("click", function () {
var abrindo = sinoPainel.classList.contains("oculto");
if (abrindo) { abrirSino(); if (tokenAtual) atualizarSino(tokenAtual); } else { fecharSino(); }
});
document.addEventListener("click", function (ev) {
if (!sinoPainel.classList.contains("oculto") && !sinoCaixa.contains(ev.target)) fecharSino();
});
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape") fecharSino();
});
botaoAtualizar.addEventListener("click", function () {
if (!tokenAtual) return;
botaoAtualizar.disabled = true;
@@ -1981,6 +1981,38 @@ data:
.topo .acao:hover, .topo .sair:hover { border-color: var(--orange); color: var(--orange-600); }
.topo .acao:disabled { opacity: .6; cursor: wait; }
/* ── sino de alertas de infraestrutura ───────────────────────────────────── */
.sino-caixa { position: relative; }
.sino {
background: none; border: 1px solid var(--line); color: var(--navy);
border-radius: 10px; padding: 6px 10px; cursor: pointer; font-size: 16px;
line-height: 1; position: relative;
}
.sino:hover { border-color: var(--orange); color: var(--orange-600); }
.sino[aria-expanded="true"] { border-color: var(--orange); }
.sino-contagem {
position: absolute; top: -7px; right: -7px;
background: var(--perigo); color: var(--branco); border-radius: 999px;
font-size: 11px; font-weight: 700; padding: 1px 6px; line-height: 1.4; min-width: 14px; text-align: center;
}
.sino-contagem.aviso { background: var(--alerta); }
.sino-contagem.sem-dado { background: var(--muted); }
.sino-painel {
position: absolute; right: 0; top: calc(100% + 10px);
width: min(360px, calc(100vw - 32px)); max-height: 60vh; overflow: auto;
background: var(--branco); border: 1px solid var(--line); border-radius: 14px;
box-shadow: 0 14px 34px rgba(11,44,61,.16); padding: 14px; z-index: 20; text-align: left;
}
.sino-topo { font-weight: 700; margin-bottom: 10px; }
.sino-lista { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.sino-item { border-radius: 10px; padding: 10px 12px; font-size: 13px; }
.sino-item.critical { background: #fdecea; }
.sino-item.warning { background: #fdf3e6; }
.sino-item.info { background: var(--paper); }
.sino-item .nome { font-weight: 700; display: block; }
.sino-item .resumo { display: block; margin-top: 2px; }
.sino-item .quem { color: var(--muted); font-size: 12px; display: block; margin-top: 4px; }
.wrap { max-width: 1320px; margin: 0 auto; padding: 22px 28px 48px; }
/* ── cartoes ──────────────────────────────────────────────────────────────── */
@@ -2226,7 +2258,7 @@ data:
`Cache-Control: immutable` por um ano. Suba o numero a cada publicacao.
-->
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/painel.css?v=2.82">
<link rel="stylesheet" href="/publico/painel.css?v=2.83">
</head>
<body>
@@ -2234,6 +2266,18 @@ data:
<img class="logo" src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37">
<span class="titulo">Painel do dono</span>
<span id="apurado" class="apurado oculto"></span>
<div class="sino-caixa oculto" id="sinoCaixa">
<button type="button" id="sino" class="sino" aria-haspopup="true" aria-expanded="false" title="Alertas de infraestrutura">
<span aria-hidden="true">&#128276;</span>
<span id="sinoContagem" class="sino-contagem oculto"></span>
</button>
<div id="sinoPainel" class="sino-painel oculto" role="menu">
<div class="sino-topo">Alertas de infraestrutura</div>
<ul id="sinoLista" class="sino-lista"></ul>
<p id="sinoVazio" class="ajuda oculto">Nenhum alerta agora.</p>
<p id="sinoIndisponivel" class="ajuda oculto">Não consegui falar com o monitoramento agora.</p>
</div>
</div>
<button type="button" id="atualizar" class="acao oculto">Atualizar</button>
<button type="button" id="sair" class="sair oculto">Sair</button>
</header>
@@ -2388,7 +2432,7 @@ data:
</main>
<script src="/publico/painel.js?v=2.82"></script>
<script src="/publico/painel.js?v=2.83"></script>
</body>
</html>
@@ -2422,16 +2466,22 @@ data:
"use strict";
var API = "/api/plataforma/painel/visao";
var API_ALERTAS = "/api/plataforma/painel/alertas-infra";
var CHAVE = "atm.painel.token";
var INTERVALO_SINO_MS = 60000;
var $ = function (id) { return document.getElementById(id); };
var porta = $("porta"), conteudo = $("conteudo"), carregando = $("carregando");
var formEntrar = $("formEntrar"), campoToken = $("token"), erroEntrar = $("erroEntrar");
var botaoSair = $("sair"), botaoAtualizar = $("atualizar"), apurado = $("apurado");
var corpo = $("linhas"), vazio = $("vazio");
var sinoCaixa = $("sinoCaixa"), sino = $("sino"), sinoContagem = $("sinoContagem"),
sinoPainel = $("sinoPainel"), sinoLista = $("sinoLista"),
sinoVazio = $("sinoVazio"), sinoIndisponivel = $("sinoIndisponivel");
var visao = null;
var tokenAtual = null;
var sinoIntervalo = null;
/* ── token ──────────────────────────────────────────────────────────────── */
@@ -2448,6 +2498,83 @@ data:
});
}
/* ── sino de alertas de infraestrutura ─────────────────────────────────────
*
* Le o mesmo Alertmanager que ja manda e-mail — o sino nao inventa alerta
* nenhum, so mostra o que o e-mail ja avisou. Falha calada de proposito:
* se o servidor nao responder, o sino so fica "sem dado" — nao pode
* derrubar o painel inteiro por causa de um widget auxiliar.
*/
function buscarAlertasInfra(token) {
return fetch(API_ALERTAS, { headers: { "x-atm-admin-token": token } })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.catch(function () { return null; }); // null = "sem dado", diferente de "zero alertas"
}
function pesoSeveridade(s) { return s === "critical" ? 3 : s === "warning" ? 2 : 1; }
function pintarSino(dados) {
sinoLista.innerHTML = "";
sinoVazio.classList.add("oculto");
sinoIndisponivel.classList.add("oculto");
if (!dados || dados.indisponivel) {
sinoContagem.textContent = "!";
sinoContagem.className = "sino-contagem sem-dado";
sinoIndisponivel.classList.remove("oculto");
return;
}
var alertas = dados.alertas || [];
if (alertas.length === 0) {
sinoContagem.classList.add("oculto");
sinoVazio.classList.remove("oculto");
return;
}
sinoContagem.textContent = String(alertas.length);
sinoContagem.className = "sino-contagem" +
(alertas.some(function (a) { return a.severidade === "critical"; }) ? "" : " aviso");
sinoContagem.classList.remove("oculto");
alertas.forEach(function (a) {
var li = document.createElement("li");
var sev = pesoSeveridade(a.severidade) === 3 ? "critical" : pesoSeveridade(a.severidade) === 2 ? "warning" : "info";
li.className = "sino-item " + sev;
li.innerHTML = "<span class=\"nome\">" + esc(a.nome) + "</span>"
+ (a.resumo ? "<span class=\"resumo\">" + esc(a.resumo) + "</span>" : "")
+ (a.tenant ? "<span class=\"quem\">" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "</span>"
: (a.desde ? "<span class=\"quem\">" + ha(a.desde) + "</span>" : ""));
sinoLista.appendChild(li);
});
}
function atualizarSino(token) {
buscarAlertasInfra(token).then(pintarSino);
}
function ligarSino(token) {
sinoCaixa.classList.remove("oculto");
atualizarSino(token);
if (sinoIntervalo) clearInterval(sinoIntervalo);
sinoIntervalo = setInterval(function () { atualizarSino(token); }, INTERVALO_SINO_MS);
}
function desligarSino() {
sinoCaixa.classList.add("oculto");
fecharSino();
if (sinoIntervalo) { clearInterval(sinoIntervalo); sinoIntervalo = null; }
}
function abrirSino() {
sinoPainel.classList.remove("oculto");
sino.setAttribute("aria-expanded", "true");
}
function fecharSino() {
sinoPainel.classList.add("oculto");
sino.setAttribute("aria-expanded", "false");
}
/* ── formatacao ─────────────────────────────────────────────────────────── */
function esc(s) {
@@ -2958,6 +3085,7 @@ data:
tokenAtual = token;
pintar();
mostrar(conteudo);
ligarSino(token);
});
}
@@ -2992,6 +3120,17 @@ data:
location.reload();
});
sino.addEventListener("click", function () {
var abrindo = sinoPainel.classList.contains("oculto");
if (abrindo) { abrirSino(); if (tokenAtual) atualizarSino(tokenAtual); } else { fecharSino(); }
});
document.addEventListener("click", function (ev) {
if (!sinoPainel.classList.contains("oculto") && !sinoCaixa.contains(ev.target)) fecharSino();
});
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape") fecharSino();
});
botaoAtualizar.addEventListener("click", function () {
if (!tokenAtual) return;
botaoAtualizar.disabled = true;
@@ -1981,6 +1981,38 @@ data:
.topo .acao:hover, .topo .sair:hover { border-color: var(--orange); color: var(--orange-600); }
.topo .acao:disabled { opacity: .6; cursor: wait; }
/* ── sino de alertas de infraestrutura ───────────────────────────────────── */
.sino-caixa { position: relative; }
.sino {
background: none; border: 1px solid var(--line); color: var(--navy);
border-radius: 10px; padding: 6px 10px; cursor: pointer; font-size: 16px;
line-height: 1; position: relative;
}
.sino:hover { border-color: var(--orange); color: var(--orange-600); }
.sino[aria-expanded="true"] { border-color: var(--orange); }
.sino-contagem {
position: absolute; top: -7px; right: -7px;
background: var(--perigo); color: var(--branco); border-radius: 999px;
font-size: 11px; font-weight: 700; padding: 1px 6px; line-height: 1.4; min-width: 14px; text-align: center;
}
.sino-contagem.aviso { background: var(--alerta); }
.sino-contagem.sem-dado { background: var(--muted); }
.sino-painel {
position: absolute; right: 0; top: calc(100% + 10px);
width: min(360px, calc(100vw - 32px)); max-height: 60vh; overflow: auto;
background: var(--branco); border: 1px solid var(--line); border-radius: 14px;
box-shadow: 0 14px 34px rgba(11,44,61,.16); padding: 14px; z-index: 20; text-align: left;
}
.sino-topo { font-weight: 700; margin-bottom: 10px; }
.sino-lista { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.sino-item { border-radius: 10px; padding: 10px 12px; font-size: 13px; }
.sino-item.critical { background: #fdecea; }
.sino-item.warning { background: #fdf3e6; }
.sino-item.info { background: var(--paper); }
.sino-item .nome { font-weight: 700; display: block; }
.sino-item .resumo { display: block; margin-top: 2px; }
.sino-item .quem { color: var(--muted); font-size: 12px; display: block; margin-top: 4px; }
.wrap { max-width: 1320px; margin: 0 auto; padding: 22px 28px 48px; }
/* ── cartoes ──────────────────────────────────────────────────────────────── */
@@ -2226,7 +2258,7 @@ data:
`Cache-Control: immutable` por um ano. Suba o numero a cada publicacao.
-->
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/painel.css?v=2.82">
<link rel="stylesheet" href="/publico/painel.css?v=2.83">
</head>
<body>
@@ -2234,6 +2266,18 @@ data:
<img class="logo" src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37">
<span class="titulo">Painel do dono</span>
<span id="apurado" class="apurado oculto"></span>
<div class="sino-caixa oculto" id="sinoCaixa">
<button type="button" id="sino" class="sino" aria-haspopup="true" aria-expanded="false" title="Alertas de infraestrutura">
<span aria-hidden="true">&#128276;</span>
<span id="sinoContagem" class="sino-contagem oculto"></span>
</button>
<div id="sinoPainel" class="sino-painel oculto" role="menu">
<div class="sino-topo">Alertas de infraestrutura</div>
<ul id="sinoLista" class="sino-lista"></ul>
<p id="sinoVazio" class="ajuda oculto">Nenhum alerta agora.</p>
<p id="sinoIndisponivel" class="ajuda oculto">Não consegui falar com o monitoramento agora.</p>
</div>
</div>
<button type="button" id="atualizar" class="acao oculto">Atualizar</button>
<button type="button" id="sair" class="sair oculto">Sair</button>
</header>
@@ -2388,7 +2432,7 @@ data:
</main>
<script src="/publico/painel.js?v=2.82"></script>
<script src="/publico/painel.js?v=2.83"></script>
</body>
</html>
@@ -2422,16 +2466,22 @@ data:
"use strict";
var API = "/api/plataforma/painel/visao";
var API_ALERTAS = "/api/plataforma/painel/alertas-infra";
var CHAVE = "atm.painel.token";
var INTERVALO_SINO_MS = 60000;
var $ = function (id) { return document.getElementById(id); };
var porta = $("porta"), conteudo = $("conteudo"), carregando = $("carregando");
var formEntrar = $("formEntrar"), campoToken = $("token"), erroEntrar = $("erroEntrar");
var botaoSair = $("sair"), botaoAtualizar = $("atualizar"), apurado = $("apurado");
var corpo = $("linhas"), vazio = $("vazio");
var sinoCaixa = $("sinoCaixa"), sino = $("sino"), sinoContagem = $("sinoContagem"),
sinoPainel = $("sinoPainel"), sinoLista = $("sinoLista"),
sinoVazio = $("sinoVazio"), sinoIndisponivel = $("sinoIndisponivel");
var visao = null;
var tokenAtual = null;
var sinoIntervalo = null;
/* ── token ──────────────────────────────────────────────────────────────── */
@@ -2448,6 +2498,83 @@ data:
});
}
/* ── sino de alertas de infraestrutura ─────────────────────────────────────
*
* Le o mesmo Alertmanager que ja manda e-mail — o sino nao inventa alerta
* nenhum, so mostra o que o e-mail ja avisou. Falha calada de proposito:
* se o servidor nao responder, o sino so fica "sem dado" — nao pode
* derrubar o painel inteiro por causa de um widget auxiliar.
*/
function buscarAlertasInfra(token) {
return fetch(API_ALERTAS, { headers: { "x-atm-admin-token": token } })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.catch(function () { return null; }); // null = "sem dado", diferente de "zero alertas"
}
function pesoSeveridade(s) { return s === "critical" ? 3 : s === "warning" ? 2 : 1; }
function pintarSino(dados) {
sinoLista.innerHTML = "";
sinoVazio.classList.add("oculto");
sinoIndisponivel.classList.add("oculto");
if (!dados || dados.indisponivel) {
sinoContagem.textContent = "!";
sinoContagem.className = "sino-contagem sem-dado";
sinoIndisponivel.classList.remove("oculto");
return;
}
var alertas = dados.alertas || [];
if (alertas.length === 0) {
sinoContagem.classList.add("oculto");
sinoVazio.classList.remove("oculto");
return;
}
sinoContagem.textContent = String(alertas.length);
sinoContagem.className = "sino-contagem" +
(alertas.some(function (a) { return a.severidade === "critical"; }) ? "" : " aviso");
sinoContagem.classList.remove("oculto");
alertas.forEach(function (a) {
var li = document.createElement("li");
var sev = pesoSeveridade(a.severidade) === 3 ? "critical" : pesoSeveridade(a.severidade) === 2 ? "warning" : "info";
li.className = "sino-item " + sev;
li.innerHTML = "<span class=\"nome\">" + esc(a.nome) + "</span>"
+ (a.resumo ? "<span class=\"resumo\">" + esc(a.resumo) + "</span>" : "")
+ (a.tenant ? "<span class=\"quem\">" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "</span>"
: (a.desde ? "<span class=\"quem\">" + ha(a.desde) + "</span>" : ""));
sinoLista.appendChild(li);
});
}
function atualizarSino(token) {
buscarAlertasInfra(token).then(pintarSino);
}
function ligarSino(token) {
sinoCaixa.classList.remove("oculto");
atualizarSino(token);
if (sinoIntervalo) clearInterval(sinoIntervalo);
sinoIntervalo = setInterval(function () { atualizarSino(token); }, INTERVALO_SINO_MS);
}
function desligarSino() {
sinoCaixa.classList.add("oculto");
fecharSino();
if (sinoIntervalo) { clearInterval(sinoIntervalo); sinoIntervalo = null; }
}
function abrirSino() {
sinoPainel.classList.remove("oculto");
sino.setAttribute("aria-expanded", "true");
}
function fecharSino() {
sinoPainel.classList.add("oculto");
sino.setAttribute("aria-expanded", "false");
}
/* ── formatacao ─────────────────────────────────────────────────────────── */
function esc(s) {
@@ -2958,6 +3085,7 @@ data:
tokenAtual = token;
pintar();
mostrar(conteudo);
ligarSino(token);
});
}
@@ -2992,6 +3120,17 @@ data:
location.reload();
});
sino.addEventListener("click", function () {
var abrindo = sinoPainel.classList.contains("oculto");
if (abrindo) { abrirSino(); if (tokenAtual) atualizarSino(tokenAtual); } else { fecharSino(); }
});
document.addEventListener("click", function (ev) {
if (!sinoPainel.classList.contains("oculto") && !sinoCaixa.contains(ev.target)) fecharSino();
});
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape") fecharSino();
});
botaoAtualizar.addEventListener("click", function () {
if (!tokenAtual) return;
botaoAtualizar.disabled = true;
+141 -2
View File
@@ -1981,6 +1981,38 @@ data:
.topo .acao:hover, .topo .sair:hover { border-color: var(--orange); color: var(--orange-600); }
.topo .acao:disabled { opacity: .6; cursor: wait; }
/* ── sino de alertas de infraestrutura ───────────────────────────────────── */
.sino-caixa { position: relative; }
.sino {
background: none; border: 1px solid var(--line); color: var(--navy);
border-radius: 10px; padding: 6px 10px; cursor: pointer; font-size: 16px;
line-height: 1; position: relative;
}
.sino:hover { border-color: var(--orange); color: var(--orange-600); }
.sino[aria-expanded="true"] { border-color: var(--orange); }
.sino-contagem {
position: absolute; top: -7px; right: -7px;
background: var(--perigo); color: var(--branco); border-radius: 999px;
font-size: 11px; font-weight: 700; padding: 1px 6px; line-height: 1.4; min-width: 14px; text-align: center;
}
.sino-contagem.aviso { background: var(--alerta); }
.sino-contagem.sem-dado { background: var(--muted); }
.sino-painel {
position: absolute; right: 0; top: calc(100% + 10px);
width: min(360px, calc(100vw - 32px)); max-height: 60vh; overflow: auto;
background: var(--branco); border: 1px solid var(--line); border-radius: 14px;
box-shadow: 0 14px 34px rgba(11,44,61,.16); padding: 14px; z-index: 20; text-align: left;
}
.sino-topo { font-weight: 700; margin-bottom: 10px; }
.sino-lista { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.sino-item { border-radius: 10px; padding: 10px 12px; font-size: 13px; }
.sino-item.critical { background: #fdecea; }
.sino-item.warning { background: #fdf3e6; }
.sino-item.info { background: var(--paper); }
.sino-item .nome { font-weight: 700; display: block; }
.sino-item .resumo { display: block; margin-top: 2px; }
.sino-item .quem { color: var(--muted); font-size: 12px; display: block; margin-top: 4px; }
.wrap { max-width: 1320px; margin: 0 auto; padding: 22px 28px 48px; }
/* ── cartoes ──────────────────────────────────────────────────────────────── */
@@ -2226,7 +2258,7 @@ data:
`Cache-Control: immutable` por um ano. Suba o numero a cada publicacao.
-->
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/painel.css?v=2.82">
<link rel="stylesheet" href="/publico/painel.css?v=2.83">
</head>
<body>
@@ -2234,6 +2266,18 @@ data:
<img class="logo" src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37">
<span class="titulo">Painel do dono</span>
<span id="apurado" class="apurado oculto"></span>
<div class="sino-caixa oculto" id="sinoCaixa">
<button type="button" id="sino" class="sino" aria-haspopup="true" aria-expanded="false" title="Alertas de infraestrutura">
<span aria-hidden="true">&#128276;</span>
<span id="sinoContagem" class="sino-contagem oculto"></span>
</button>
<div id="sinoPainel" class="sino-painel oculto" role="menu">
<div class="sino-topo">Alertas de infraestrutura</div>
<ul id="sinoLista" class="sino-lista"></ul>
<p id="sinoVazio" class="ajuda oculto">Nenhum alerta agora.</p>
<p id="sinoIndisponivel" class="ajuda oculto">Não consegui falar com o monitoramento agora.</p>
</div>
</div>
<button type="button" id="atualizar" class="acao oculto">Atualizar</button>
<button type="button" id="sair" class="sair oculto">Sair</button>
</header>
@@ -2388,7 +2432,7 @@ data:
</main>
<script src="/publico/painel.js?v=2.82"></script>
<script src="/publico/painel.js?v=2.83"></script>
</body>
</html>
@@ -2422,16 +2466,22 @@ data:
"use strict";
var API = "/api/plataforma/painel/visao";
var API_ALERTAS = "/api/plataforma/painel/alertas-infra";
var CHAVE = "atm.painel.token";
var INTERVALO_SINO_MS = 60000;
var $ = function (id) { return document.getElementById(id); };
var porta = $("porta"), conteudo = $("conteudo"), carregando = $("carregando");
var formEntrar = $("formEntrar"), campoToken = $("token"), erroEntrar = $("erroEntrar");
var botaoSair = $("sair"), botaoAtualizar = $("atualizar"), apurado = $("apurado");
var corpo = $("linhas"), vazio = $("vazio");
var sinoCaixa = $("sinoCaixa"), sino = $("sino"), sinoContagem = $("sinoContagem"),
sinoPainel = $("sinoPainel"), sinoLista = $("sinoLista"),
sinoVazio = $("sinoVazio"), sinoIndisponivel = $("sinoIndisponivel");
var visao = null;
var tokenAtual = null;
var sinoIntervalo = null;
/* ── token ──────────────────────────────────────────────────────────────── */
@@ -2448,6 +2498,83 @@ data:
});
}
/* ── sino de alertas de infraestrutura ─────────────────────────────────────
*
* Le o mesmo Alertmanager que ja manda e-mail — o sino nao inventa alerta
* nenhum, so mostra o que o e-mail ja avisou. Falha calada de proposito:
* se o servidor nao responder, o sino so fica "sem dado" — nao pode
* derrubar o painel inteiro por causa de um widget auxiliar.
*/
function buscarAlertasInfra(token) {
return fetch(API_ALERTAS, { headers: { "x-atm-admin-token": token } })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.catch(function () { return null; }); // null = "sem dado", diferente de "zero alertas"
}
function pesoSeveridade(s) { return s === "critical" ? 3 : s === "warning" ? 2 : 1; }
function pintarSino(dados) {
sinoLista.innerHTML = "";
sinoVazio.classList.add("oculto");
sinoIndisponivel.classList.add("oculto");
if (!dados || dados.indisponivel) {
sinoContagem.textContent = "!";
sinoContagem.className = "sino-contagem sem-dado";
sinoIndisponivel.classList.remove("oculto");
return;
}
var alertas = dados.alertas || [];
if (alertas.length === 0) {
sinoContagem.classList.add("oculto");
sinoVazio.classList.remove("oculto");
return;
}
sinoContagem.textContent = String(alertas.length);
sinoContagem.className = "sino-contagem" +
(alertas.some(function (a) { return a.severidade === "critical"; }) ? "" : " aviso");
sinoContagem.classList.remove("oculto");
alertas.forEach(function (a) {
var li = document.createElement("li");
var sev = pesoSeveridade(a.severidade) === 3 ? "critical" : pesoSeveridade(a.severidade) === 2 ? "warning" : "info";
li.className = "sino-item " + sev;
li.innerHTML = "<span class=\"nome\">" + esc(a.nome) + "</span>"
+ (a.resumo ? "<span class=\"resumo\">" + esc(a.resumo) + "</span>" : "")
+ (a.tenant ? "<span class=\"quem\">" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "</span>"
: (a.desde ? "<span class=\"quem\">" + ha(a.desde) + "</span>" : ""));
sinoLista.appendChild(li);
});
}
function atualizarSino(token) {
buscarAlertasInfra(token).then(pintarSino);
}
function ligarSino(token) {
sinoCaixa.classList.remove("oculto");
atualizarSino(token);
if (sinoIntervalo) clearInterval(sinoIntervalo);
sinoIntervalo = setInterval(function () { atualizarSino(token); }, INTERVALO_SINO_MS);
}
function desligarSino() {
sinoCaixa.classList.add("oculto");
fecharSino();
if (sinoIntervalo) { clearInterval(sinoIntervalo); sinoIntervalo = null; }
}
function abrirSino() {
sinoPainel.classList.remove("oculto");
sino.setAttribute("aria-expanded", "true");
}
function fecharSino() {
sinoPainel.classList.add("oculto");
sino.setAttribute("aria-expanded", "false");
}
/* ── formatacao ─────────────────────────────────────────────────────────── */
function esc(s) {
@@ -2958,6 +3085,7 @@ data:
tokenAtual = token;
pintar();
mostrar(conteudo);
ligarSino(token);
});
}
@@ -2992,6 +3120,17 @@ data:
location.reload();
});
sino.addEventListener("click", function () {
var abrindo = sinoPainel.classList.contains("oculto");
if (abrindo) { abrirSino(); if (tokenAtual) atualizarSino(tokenAtual); } else { fecharSino(); }
});
document.addEventListener("click", function (ev) {
if (!sinoPainel.classList.contains("oculto") && !sinoCaixa.contains(ev.target)) fecharSino();
});
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape") fecharSino();
});
botaoAtualizar.addEventListener("click", function () {
if (!tokenAtual) return;
botaoAtualizar.disabled = true;
+141 -2
View File
@@ -1981,6 +1981,38 @@ data:
.topo .acao:hover, .topo .sair:hover { border-color: var(--orange); color: var(--orange-600); }
.topo .acao:disabled { opacity: .6; cursor: wait; }
/* ── sino de alertas de infraestrutura ───────────────────────────────────── */
.sino-caixa { position: relative; }
.sino {
background: none; border: 1px solid var(--line); color: var(--navy);
border-radius: 10px; padding: 6px 10px; cursor: pointer; font-size: 16px;
line-height: 1; position: relative;
}
.sino:hover { border-color: var(--orange); color: var(--orange-600); }
.sino[aria-expanded="true"] { border-color: var(--orange); }
.sino-contagem {
position: absolute; top: -7px; right: -7px;
background: var(--perigo); color: var(--branco); border-radius: 999px;
font-size: 11px; font-weight: 700; padding: 1px 6px; line-height: 1.4; min-width: 14px; text-align: center;
}
.sino-contagem.aviso { background: var(--alerta); }
.sino-contagem.sem-dado { background: var(--muted); }
.sino-painel {
position: absolute; right: 0; top: calc(100% + 10px);
width: min(360px, calc(100vw - 32px)); max-height: 60vh; overflow: auto;
background: var(--branco); border: 1px solid var(--line); border-radius: 14px;
box-shadow: 0 14px 34px rgba(11,44,61,.16); padding: 14px; z-index: 20; text-align: left;
}
.sino-topo { font-weight: 700; margin-bottom: 10px; }
.sino-lista { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.sino-item { border-radius: 10px; padding: 10px 12px; font-size: 13px; }
.sino-item.critical { background: #fdecea; }
.sino-item.warning { background: #fdf3e6; }
.sino-item.info { background: var(--paper); }
.sino-item .nome { font-weight: 700; display: block; }
.sino-item .resumo { display: block; margin-top: 2px; }
.sino-item .quem { color: var(--muted); font-size: 12px; display: block; margin-top: 4px; }
.wrap { max-width: 1320px; margin: 0 auto; padding: 22px 28px 48px; }
/* ── cartoes ──────────────────────────────────────────────────────────────── */
@@ -2226,7 +2258,7 @@ data:
`Cache-Control: immutable` por um ano. Suba o numero a cada publicacao.
-->
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/painel.css?v=2.82">
<link rel="stylesheet" href="/publico/painel.css?v=2.83">
</head>
<body>
@@ -2234,6 +2266,18 @@ data:
<img class="logo" src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37">
<span class="titulo">Painel do dono</span>
<span id="apurado" class="apurado oculto"></span>
<div class="sino-caixa oculto" id="sinoCaixa">
<button type="button" id="sino" class="sino" aria-haspopup="true" aria-expanded="false" title="Alertas de infraestrutura">
<span aria-hidden="true">&#128276;</span>
<span id="sinoContagem" class="sino-contagem oculto"></span>
</button>
<div id="sinoPainel" class="sino-painel oculto" role="menu">
<div class="sino-topo">Alertas de infraestrutura</div>
<ul id="sinoLista" class="sino-lista"></ul>
<p id="sinoVazio" class="ajuda oculto">Nenhum alerta agora.</p>
<p id="sinoIndisponivel" class="ajuda oculto">Não consegui falar com o monitoramento agora.</p>
</div>
</div>
<button type="button" id="atualizar" class="acao oculto">Atualizar</button>
<button type="button" id="sair" class="sair oculto">Sair</button>
</header>
@@ -2388,7 +2432,7 @@ data:
</main>
<script src="/publico/painel.js?v=2.82"></script>
<script src="/publico/painel.js?v=2.83"></script>
</body>
</html>
@@ -2422,16 +2466,22 @@ data:
"use strict";
var API = "/api/plataforma/painel/visao";
var API_ALERTAS = "/api/plataforma/painel/alertas-infra";
var CHAVE = "atm.painel.token";
var INTERVALO_SINO_MS = 60000;
var $ = function (id) { return document.getElementById(id); };
var porta = $("porta"), conteudo = $("conteudo"), carregando = $("carregando");
var formEntrar = $("formEntrar"), campoToken = $("token"), erroEntrar = $("erroEntrar");
var botaoSair = $("sair"), botaoAtualizar = $("atualizar"), apurado = $("apurado");
var corpo = $("linhas"), vazio = $("vazio");
var sinoCaixa = $("sinoCaixa"), sino = $("sino"), sinoContagem = $("sinoContagem"),
sinoPainel = $("sinoPainel"), sinoLista = $("sinoLista"),
sinoVazio = $("sinoVazio"), sinoIndisponivel = $("sinoIndisponivel");
var visao = null;
var tokenAtual = null;
var sinoIntervalo = null;
/* ── token ──────────────────────────────────────────────────────────────── */
@@ -2448,6 +2498,83 @@ data:
});
}
/* ── sino de alertas de infraestrutura ─────────────────────────────────────
*
* Le o mesmo Alertmanager que ja manda e-mail — o sino nao inventa alerta
* nenhum, so mostra o que o e-mail ja avisou. Falha calada de proposito:
* se o servidor nao responder, o sino so fica "sem dado" — nao pode
* derrubar o painel inteiro por causa de um widget auxiliar.
*/
function buscarAlertasInfra(token) {
return fetch(API_ALERTAS, { headers: { "x-atm-admin-token": token } })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.catch(function () { return null; }); // null = "sem dado", diferente de "zero alertas"
}
function pesoSeveridade(s) { return s === "critical" ? 3 : s === "warning" ? 2 : 1; }
function pintarSino(dados) {
sinoLista.innerHTML = "";
sinoVazio.classList.add("oculto");
sinoIndisponivel.classList.add("oculto");
if (!dados || dados.indisponivel) {
sinoContagem.textContent = "!";
sinoContagem.className = "sino-contagem sem-dado";
sinoIndisponivel.classList.remove("oculto");
return;
}
var alertas = dados.alertas || [];
if (alertas.length === 0) {
sinoContagem.classList.add("oculto");
sinoVazio.classList.remove("oculto");
return;
}
sinoContagem.textContent = String(alertas.length);
sinoContagem.className = "sino-contagem" +
(alertas.some(function (a) { return a.severidade === "critical"; }) ? "" : " aviso");
sinoContagem.classList.remove("oculto");
alertas.forEach(function (a) {
var li = document.createElement("li");
var sev = pesoSeveridade(a.severidade) === 3 ? "critical" : pesoSeveridade(a.severidade) === 2 ? "warning" : "info";
li.className = "sino-item " + sev;
li.innerHTML = "<span class=\"nome\">" + esc(a.nome) + "</span>"
+ (a.resumo ? "<span class=\"resumo\">" + esc(a.resumo) + "</span>" : "")
+ (a.tenant ? "<span class=\"quem\">" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "</span>"
: (a.desde ? "<span class=\"quem\">" + ha(a.desde) + "</span>" : ""));
sinoLista.appendChild(li);
});
}
function atualizarSino(token) {
buscarAlertasInfra(token).then(pintarSino);
}
function ligarSino(token) {
sinoCaixa.classList.remove("oculto");
atualizarSino(token);
if (sinoIntervalo) clearInterval(sinoIntervalo);
sinoIntervalo = setInterval(function () { atualizarSino(token); }, INTERVALO_SINO_MS);
}
function desligarSino() {
sinoCaixa.classList.add("oculto");
fecharSino();
if (sinoIntervalo) { clearInterval(sinoIntervalo); sinoIntervalo = null; }
}
function abrirSino() {
sinoPainel.classList.remove("oculto");
sino.setAttribute("aria-expanded", "true");
}
function fecharSino() {
sinoPainel.classList.add("oculto");
sino.setAttribute("aria-expanded", "false");
}
/* ── formatacao ─────────────────────────────────────────────────────────── */
function esc(s) {
@@ -2958,6 +3085,7 @@ data:
tokenAtual = token;
pintar();
mostrar(conteudo);
ligarSino(token);
});
}
@@ -2992,6 +3120,17 @@ data:
location.reload();
});
sino.addEventListener("click", function () {
var abrindo = sinoPainel.classList.contains("oculto");
if (abrindo) { abrirSino(); if (tokenAtual) atualizarSino(tokenAtual); } else { fecharSino(); }
});
document.addEventListener("click", function (ev) {
if (!sinoPainel.classList.contains("oculto") && !sinoCaixa.contains(ev.target)) fecharSino();
});
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape") fecharSino();
});
botaoAtualizar.addEventListener("click", function () {
if (!tokenAtual) return;
botaoAtualizar.disabled = true;
+141 -2
View File
@@ -1981,6 +1981,38 @@ data:
.topo .acao:hover, .topo .sair:hover { border-color: var(--orange); color: var(--orange-600); }
.topo .acao:disabled { opacity: .6; cursor: wait; }
/* ── sino de alertas de infraestrutura ───────────────────────────────────── */
.sino-caixa { position: relative; }
.sino {
background: none; border: 1px solid var(--line); color: var(--navy);
border-radius: 10px; padding: 6px 10px; cursor: pointer; font-size: 16px;
line-height: 1; position: relative;
}
.sino:hover { border-color: var(--orange); color: var(--orange-600); }
.sino[aria-expanded="true"] { border-color: var(--orange); }
.sino-contagem {
position: absolute; top: -7px; right: -7px;
background: var(--perigo); color: var(--branco); border-radius: 999px;
font-size: 11px; font-weight: 700; padding: 1px 6px; line-height: 1.4; min-width: 14px; text-align: center;
}
.sino-contagem.aviso { background: var(--alerta); }
.sino-contagem.sem-dado { background: var(--muted); }
.sino-painel {
position: absolute; right: 0; top: calc(100% + 10px);
width: min(360px, calc(100vw - 32px)); max-height: 60vh; overflow: auto;
background: var(--branco); border: 1px solid var(--line); border-radius: 14px;
box-shadow: 0 14px 34px rgba(11,44,61,.16); padding: 14px; z-index: 20; text-align: left;
}
.sino-topo { font-weight: 700; margin-bottom: 10px; }
.sino-lista { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.sino-item { border-radius: 10px; padding: 10px 12px; font-size: 13px; }
.sino-item.critical { background: #fdecea; }
.sino-item.warning { background: #fdf3e6; }
.sino-item.info { background: var(--paper); }
.sino-item .nome { font-weight: 700; display: block; }
.sino-item .resumo { display: block; margin-top: 2px; }
.sino-item .quem { color: var(--muted); font-size: 12px; display: block; margin-top: 4px; }
.wrap { max-width: 1320px; margin: 0 auto; padding: 22px 28px 48px; }
/* ── cartoes ──────────────────────────────────────────────────────────────── */
@@ -2226,7 +2258,7 @@ data:
`Cache-Control: immutable` por um ano. Suba o numero a cada publicacao.
-->
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/painel.css?v=2.82">
<link rel="stylesheet" href="/publico/painel.css?v=2.83">
</head>
<body>
@@ -2234,6 +2266,18 @@ data:
<img class="logo" src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37">
<span class="titulo">Painel do dono</span>
<span id="apurado" class="apurado oculto"></span>
<div class="sino-caixa oculto" id="sinoCaixa">
<button type="button" id="sino" class="sino" aria-haspopup="true" aria-expanded="false" title="Alertas de infraestrutura">
<span aria-hidden="true">&#128276;</span>
<span id="sinoContagem" class="sino-contagem oculto"></span>
</button>
<div id="sinoPainel" class="sino-painel oculto" role="menu">
<div class="sino-topo">Alertas de infraestrutura</div>
<ul id="sinoLista" class="sino-lista"></ul>
<p id="sinoVazio" class="ajuda oculto">Nenhum alerta agora.</p>
<p id="sinoIndisponivel" class="ajuda oculto">Não consegui falar com o monitoramento agora.</p>
</div>
</div>
<button type="button" id="atualizar" class="acao oculto">Atualizar</button>
<button type="button" id="sair" class="sair oculto">Sair</button>
</header>
@@ -2388,7 +2432,7 @@ data:
</main>
<script src="/publico/painel.js?v=2.82"></script>
<script src="/publico/painel.js?v=2.83"></script>
</body>
</html>
@@ -2422,16 +2466,22 @@ data:
"use strict";
var API = "/api/plataforma/painel/visao";
var API_ALERTAS = "/api/plataforma/painel/alertas-infra";
var CHAVE = "atm.painel.token";
var INTERVALO_SINO_MS = 60000;
var $ = function (id) { return document.getElementById(id); };
var porta = $("porta"), conteudo = $("conteudo"), carregando = $("carregando");
var formEntrar = $("formEntrar"), campoToken = $("token"), erroEntrar = $("erroEntrar");
var botaoSair = $("sair"), botaoAtualizar = $("atualizar"), apurado = $("apurado");
var corpo = $("linhas"), vazio = $("vazio");
var sinoCaixa = $("sinoCaixa"), sino = $("sino"), sinoContagem = $("sinoContagem"),
sinoPainel = $("sinoPainel"), sinoLista = $("sinoLista"),
sinoVazio = $("sinoVazio"), sinoIndisponivel = $("sinoIndisponivel");
var visao = null;
var tokenAtual = null;
var sinoIntervalo = null;
/* ── token ──────────────────────────────────────────────────────────────── */
@@ -2448,6 +2498,83 @@ data:
});
}
/* ── sino de alertas de infraestrutura ─────────────────────────────────────
*
* Le o mesmo Alertmanager que ja manda e-mail — o sino nao inventa alerta
* nenhum, so mostra o que o e-mail ja avisou. Falha calada de proposito:
* se o servidor nao responder, o sino so fica "sem dado" — nao pode
* derrubar o painel inteiro por causa de um widget auxiliar.
*/
function buscarAlertasInfra(token) {
return fetch(API_ALERTAS, { headers: { "x-atm-admin-token": token } })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.catch(function () { return null; }); // null = "sem dado", diferente de "zero alertas"
}
function pesoSeveridade(s) { return s === "critical" ? 3 : s === "warning" ? 2 : 1; }
function pintarSino(dados) {
sinoLista.innerHTML = "";
sinoVazio.classList.add("oculto");
sinoIndisponivel.classList.add("oculto");
if (!dados || dados.indisponivel) {
sinoContagem.textContent = "!";
sinoContagem.className = "sino-contagem sem-dado";
sinoIndisponivel.classList.remove("oculto");
return;
}
var alertas = dados.alertas || [];
if (alertas.length === 0) {
sinoContagem.classList.add("oculto");
sinoVazio.classList.remove("oculto");
return;
}
sinoContagem.textContent = String(alertas.length);
sinoContagem.className = "sino-contagem" +
(alertas.some(function (a) { return a.severidade === "critical"; }) ? "" : " aviso");
sinoContagem.classList.remove("oculto");
alertas.forEach(function (a) {
var li = document.createElement("li");
var sev = pesoSeveridade(a.severidade) === 3 ? "critical" : pesoSeveridade(a.severidade) === 2 ? "warning" : "info";
li.className = "sino-item " + sev;
li.innerHTML = "<span class=\"nome\">" + esc(a.nome) + "</span>"
+ (a.resumo ? "<span class=\"resumo\">" + esc(a.resumo) + "</span>" : "")
+ (a.tenant ? "<span class=\"quem\">" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "</span>"
: (a.desde ? "<span class=\"quem\">" + ha(a.desde) + "</span>" : ""));
sinoLista.appendChild(li);
});
}
function atualizarSino(token) {
buscarAlertasInfra(token).then(pintarSino);
}
function ligarSino(token) {
sinoCaixa.classList.remove("oculto");
atualizarSino(token);
if (sinoIntervalo) clearInterval(sinoIntervalo);
sinoIntervalo = setInterval(function () { atualizarSino(token); }, INTERVALO_SINO_MS);
}
function desligarSino() {
sinoCaixa.classList.add("oculto");
fecharSino();
if (sinoIntervalo) { clearInterval(sinoIntervalo); sinoIntervalo = null; }
}
function abrirSino() {
sinoPainel.classList.remove("oculto");
sino.setAttribute("aria-expanded", "true");
}
function fecharSino() {
sinoPainel.classList.add("oculto");
sino.setAttribute("aria-expanded", "false");
}
/* ── formatacao ─────────────────────────────────────────────────────────── */
function esc(s) {
@@ -2958,6 +3085,7 @@ data:
tokenAtual = token;
pintar();
mostrar(conteudo);
ligarSino(token);
});
}
@@ -2992,6 +3120,17 @@ data:
location.reload();
});
sino.addEventListener("click", function () {
var abrindo = sinoPainel.classList.contains("oculto");
if (abrindo) { abrirSino(); if (tokenAtual) atualizarSino(tokenAtual); } else { fecharSino(); }
});
document.addEventListener("click", function (ev) {
if (!sinoPainel.classList.contains("oculto") && !sinoCaixa.contains(ev.target)) fecharSino();
});
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape") fecharSino();
});
botaoAtualizar.addEventListener("click", function () {
if (!tokenAtual) return;
botaoAtualizar.disabled = true;
+141 -2
View File
@@ -1981,6 +1981,38 @@ data:
.topo .acao:hover, .topo .sair:hover { border-color: var(--orange); color: var(--orange-600); }
.topo .acao:disabled { opacity: .6; cursor: wait; }
/* ── sino de alertas de infraestrutura ───────────────────────────────────── */
.sino-caixa { position: relative; }
.sino {
background: none; border: 1px solid var(--line); color: var(--navy);
border-radius: 10px; padding: 6px 10px; cursor: pointer; font-size: 16px;
line-height: 1; position: relative;
}
.sino:hover { border-color: var(--orange); color: var(--orange-600); }
.sino[aria-expanded="true"] { border-color: var(--orange); }
.sino-contagem {
position: absolute; top: -7px; right: -7px;
background: var(--perigo); color: var(--branco); border-radius: 999px;
font-size: 11px; font-weight: 700; padding: 1px 6px; line-height: 1.4; min-width: 14px; text-align: center;
}
.sino-contagem.aviso { background: var(--alerta); }
.sino-contagem.sem-dado { background: var(--muted); }
.sino-painel {
position: absolute; right: 0; top: calc(100% + 10px);
width: min(360px, calc(100vw - 32px)); max-height: 60vh; overflow: auto;
background: var(--branco); border: 1px solid var(--line); border-radius: 14px;
box-shadow: 0 14px 34px rgba(11,44,61,.16); padding: 14px; z-index: 20; text-align: left;
}
.sino-topo { font-weight: 700; margin-bottom: 10px; }
.sino-lista { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.sino-item { border-radius: 10px; padding: 10px 12px; font-size: 13px; }
.sino-item.critical { background: #fdecea; }
.sino-item.warning { background: #fdf3e6; }
.sino-item.info { background: var(--paper); }
.sino-item .nome { font-weight: 700; display: block; }
.sino-item .resumo { display: block; margin-top: 2px; }
.sino-item .quem { color: var(--muted); font-size: 12px; display: block; margin-top: 4px; }
.wrap { max-width: 1320px; margin: 0 auto; padding: 22px 28px 48px; }
/* ── cartoes ──────────────────────────────────────────────────────────────── */
@@ -2226,7 +2258,7 @@ data:
`Cache-Control: immutable` por um ano. Suba o numero a cada publicacao.
-->
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/painel.css?v=2.82">
<link rel="stylesheet" href="/publico/painel.css?v=2.83">
</head>
<body>
@@ -2234,6 +2266,18 @@ data:
<img class="logo" src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37">
<span class="titulo">Painel do dono</span>
<span id="apurado" class="apurado oculto"></span>
<div class="sino-caixa oculto" id="sinoCaixa">
<button type="button" id="sino" class="sino" aria-haspopup="true" aria-expanded="false" title="Alertas de infraestrutura">
<span aria-hidden="true">&#128276;</span>
<span id="sinoContagem" class="sino-contagem oculto"></span>
</button>
<div id="sinoPainel" class="sino-painel oculto" role="menu">
<div class="sino-topo">Alertas de infraestrutura</div>
<ul id="sinoLista" class="sino-lista"></ul>
<p id="sinoVazio" class="ajuda oculto">Nenhum alerta agora.</p>
<p id="sinoIndisponivel" class="ajuda oculto">Não consegui falar com o monitoramento agora.</p>
</div>
</div>
<button type="button" id="atualizar" class="acao oculto">Atualizar</button>
<button type="button" id="sair" class="sair oculto">Sair</button>
</header>
@@ -2388,7 +2432,7 @@ data:
</main>
<script src="/publico/painel.js?v=2.82"></script>
<script src="/publico/painel.js?v=2.83"></script>
</body>
</html>
@@ -2422,16 +2466,22 @@ data:
"use strict";
var API = "/api/plataforma/painel/visao";
var API_ALERTAS = "/api/plataforma/painel/alertas-infra";
var CHAVE = "atm.painel.token";
var INTERVALO_SINO_MS = 60000;
var $ = function (id) { return document.getElementById(id); };
var porta = $("porta"), conteudo = $("conteudo"), carregando = $("carregando");
var formEntrar = $("formEntrar"), campoToken = $("token"), erroEntrar = $("erroEntrar");
var botaoSair = $("sair"), botaoAtualizar = $("atualizar"), apurado = $("apurado");
var corpo = $("linhas"), vazio = $("vazio");
var sinoCaixa = $("sinoCaixa"), sino = $("sino"), sinoContagem = $("sinoContagem"),
sinoPainel = $("sinoPainel"), sinoLista = $("sinoLista"),
sinoVazio = $("sinoVazio"), sinoIndisponivel = $("sinoIndisponivel");
var visao = null;
var tokenAtual = null;
var sinoIntervalo = null;
/* ── token ──────────────────────────────────────────────────────────────── */
@@ -2448,6 +2498,83 @@ data:
});
}
/* ── sino de alertas de infraestrutura ─────────────────────────────────────
*
* Le o mesmo Alertmanager que ja manda e-mail — o sino nao inventa alerta
* nenhum, so mostra o que o e-mail ja avisou. Falha calada de proposito:
* se o servidor nao responder, o sino so fica "sem dado" — nao pode
* derrubar o painel inteiro por causa de um widget auxiliar.
*/
function buscarAlertasInfra(token) {
return fetch(API_ALERTAS, { headers: { "x-atm-admin-token": token } })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.catch(function () { return null; }); // null = "sem dado", diferente de "zero alertas"
}
function pesoSeveridade(s) { return s === "critical" ? 3 : s === "warning" ? 2 : 1; }
function pintarSino(dados) {
sinoLista.innerHTML = "";
sinoVazio.classList.add("oculto");
sinoIndisponivel.classList.add("oculto");
if (!dados || dados.indisponivel) {
sinoContagem.textContent = "!";
sinoContagem.className = "sino-contagem sem-dado";
sinoIndisponivel.classList.remove("oculto");
return;
}
var alertas = dados.alertas || [];
if (alertas.length === 0) {
sinoContagem.classList.add("oculto");
sinoVazio.classList.remove("oculto");
return;
}
sinoContagem.textContent = String(alertas.length);
sinoContagem.className = "sino-contagem" +
(alertas.some(function (a) { return a.severidade === "critical"; }) ? "" : " aviso");
sinoContagem.classList.remove("oculto");
alertas.forEach(function (a) {
var li = document.createElement("li");
var sev = pesoSeveridade(a.severidade) === 3 ? "critical" : pesoSeveridade(a.severidade) === 2 ? "warning" : "info";
li.className = "sino-item " + sev;
li.innerHTML = "<span class=\"nome\">" + esc(a.nome) + "</span>"
+ (a.resumo ? "<span class=\"resumo\">" + esc(a.resumo) + "</span>" : "")
+ (a.tenant ? "<span class=\"quem\">" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "</span>"
: (a.desde ? "<span class=\"quem\">" + ha(a.desde) + "</span>" : ""));
sinoLista.appendChild(li);
});
}
function atualizarSino(token) {
buscarAlertasInfra(token).then(pintarSino);
}
function ligarSino(token) {
sinoCaixa.classList.remove("oculto");
atualizarSino(token);
if (sinoIntervalo) clearInterval(sinoIntervalo);
sinoIntervalo = setInterval(function () { atualizarSino(token); }, INTERVALO_SINO_MS);
}
function desligarSino() {
sinoCaixa.classList.add("oculto");
fecharSino();
if (sinoIntervalo) { clearInterval(sinoIntervalo); sinoIntervalo = null; }
}
function abrirSino() {
sinoPainel.classList.remove("oculto");
sino.setAttribute("aria-expanded", "true");
}
function fecharSino() {
sinoPainel.classList.add("oculto");
sino.setAttribute("aria-expanded", "false");
}
/* ── formatacao ─────────────────────────────────────────────────────────── */
function esc(s) {
@@ -2958,6 +3085,7 @@ data:
tokenAtual = token;
pintar();
mostrar(conteudo);
ligarSino(token);
});
}
@@ -2992,6 +3120,17 @@ data:
location.reload();
});
sino.addEventListener("click", function () {
var abrindo = sinoPainel.classList.contains("oculto");
if (abrindo) { abrirSino(); if (tokenAtual) atualizarSino(tokenAtual); } else { fecharSino(); }
});
document.addEventListener("click", function (ev) {
if (!sinoPainel.classList.contains("oculto") && !sinoCaixa.contains(ev.target)) fecharSino();
});
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape") fecharSino();
});
botaoAtualizar.addEventListener("click", function () {
if (!tokenAtual) return;
botaoAtualizar.disabled = true;
+141 -2
View File
@@ -1981,6 +1981,38 @@ data:
.topo .acao:hover, .topo .sair:hover { border-color: var(--orange); color: var(--orange-600); }
.topo .acao:disabled { opacity: .6; cursor: wait; }
/* ── sino de alertas de infraestrutura ───────────────────────────────────── */
.sino-caixa { position: relative; }
.sino {
background: none; border: 1px solid var(--line); color: var(--navy);
border-radius: 10px; padding: 6px 10px; cursor: pointer; font-size: 16px;
line-height: 1; position: relative;
}
.sino:hover { border-color: var(--orange); color: var(--orange-600); }
.sino[aria-expanded="true"] { border-color: var(--orange); }
.sino-contagem {
position: absolute; top: -7px; right: -7px;
background: var(--perigo); color: var(--branco); border-radius: 999px;
font-size: 11px; font-weight: 700; padding: 1px 6px; line-height: 1.4; min-width: 14px; text-align: center;
}
.sino-contagem.aviso { background: var(--alerta); }
.sino-contagem.sem-dado { background: var(--muted); }
.sino-painel {
position: absolute; right: 0; top: calc(100% + 10px);
width: min(360px, calc(100vw - 32px)); max-height: 60vh; overflow: auto;
background: var(--branco); border: 1px solid var(--line); border-radius: 14px;
box-shadow: 0 14px 34px rgba(11,44,61,.16); padding: 14px; z-index: 20; text-align: left;
}
.sino-topo { font-weight: 700; margin-bottom: 10px; }
.sino-lista { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.sino-item { border-radius: 10px; padding: 10px 12px; font-size: 13px; }
.sino-item.critical { background: #fdecea; }
.sino-item.warning { background: #fdf3e6; }
.sino-item.info { background: var(--paper); }
.sino-item .nome { font-weight: 700; display: block; }
.sino-item .resumo { display: block; margin-top: 2px; }
.sino-item .quem { color: var(--muted); font-size: 12px; display: block; margin-top: 4px; }
.wrap { max-width: 1320px; margin: 0 auto; padding: 22px 28px 48px; }
/* ── cartoes ──────────────────────────────────────────────────────────────── */
@@ -2226,7 +2258,7 @@ data:
`Cache-Control: immutable` por um ano. Suba o numero a cada publicacao.
-->
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/painel.css?v=2.82">
<link rel="stylesheet" href="/publico/painel.css?v=2.83">
</head>
<body>
@@ -2234,6 +2266,18 @@ data:
<img class="logo" src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37">
<span class="titulo">Painel do dono</span>
<span id="apurado" class="apurado oculto"></span>
<div class="sino-caixa oculto" id="sinoCaixa">
<button type="button" id="sino" class="sino" aria-haspopup="true" aria-expanded="false" title="Alertas de infraestrutura">
<span aria-hidden="true">&#128276;</span>
<span id="sinoContagem" class="sino-contagem oculto"></span>
</button>
<div id="sinoPainel" class="sino-painel oculto" role="menu">
<div class="sino-topo">Alertas de infraestrutura</div>
<ul id="sinoLista" class="sino-lista"></ul>
<p id="sinoVazio" class="ajuda oculto">Nenhum alerta agora.</p>
<p id="sinoIndisponivel" class="ajuda oculto">Não consegui falar com o monitoramento agora.</p>
</div>
</div>
<button type="button" id="atualizar" class="acao oculto">Atualizar</button>
<button type="button" id="sair" class="sair oculto">Sair</button>
</header>
@@ -2388,7 +2432,7 @@ data:
</main>
<script src="/publico/painel.js?v=2.82"></script>
<script src="/publico/painel.js?v=2.83"></script>
</body>
</html>
@@ -2422,16 +2466,22 @@ data:
"use strict";
var API = "/api/plataforma/painel/visao";
var API_ALERTAS = "/api/plataforma/painel/alertas-infra";
var CHAVE = "atm.painel.token";
var INTERVALO_SINO_MS = 60000;
var $ = function (id) { return document.getElementById(id); };
var porta = $("porta"), conteudo = $("conteudo"), carregando = $("carregando");
var formEntrar = $("formEntrar"), campoToken = $("token"), erroEntrar = $("erroEntrar");
var botaoSair = $("sair"), botaoAtualizar = $("atualizar"), apurado = $("apurado");
var corpo = $("linhas"), vazio = $("vazio");
var sinoCaixa = $("sinoCaixa"), sino = $("sino"), sinoContagem = $("sinoContagem"),
sinoPainel = $("sinoPainel"), sinoLista = $("sinoLista"),
sinoVazio = $("sinoVazio"), sinoIndisponivel = $("sinoIndisponivel");
var visao = null;
var tokenAtual = null;
var sinoIntervalo = null;
/* ── token ──────────────────────────────────────────────────────────────── */
@@ -2448,6 +2498,83 @@ data:
});
}
/* ── sino de alertas de infraestrutura ─────────────────────────────────────
*
* Le o mesmo Alertmanager que ja manda e-mail — o sino nao inventa alerta
* nenhum, so mostra o que o e-mail ja avisou. Falha calada de proposito:
* se o servidor nao responder, o sino so fica "sem dado" — nao pode
* derrubar o painel inteiro por causa de um widget auxiliar.
*/
function buscarAlertasInfra(token) {
return fetch(API_ALERTAS, { headers: { "x-atm-admin-token": token } })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.catch(function () { return null; }); // null = "sem dado", diferente de "zero alertas"
}
function pesoSeveridade(s) { return s === "critical" ? 3 : s === "warning" ? 2 : 1; }
function pintarSino(dados) {
sinoLista.innerHTML = "";
sinoVazio.classList.add("oculto");
sinoIndisponivel.classList.add("oculto");
if (!dados || dados.indisponivel) {
sinoContagem.textContent = "!";
sinoContagem.className = "sino-contagem sem-dado";
sinoIndisponivel.classList.remove("oculto");
return;
}
var alertas = dados.alertas || [];
if (alertas.length === 0) {
sinoContagem.classList.add("oculto");
sinoVazio.classList.remove("oculto");
return;
}
sinoContagem.textContent = String(alertas.length);
sinoContagem.className = "sino-contagem" +
(alertas.some(function (a) { return a.severidade === "critical"; }) ? "" : " aviso");
sinoContagem.classList.remove("oculto");
alertas.forEach(function (a) {
var li = document.createElement("li");
var sev = pesoSeveridade(a.severidade) === 3 ? "critical" : pesoSeveridade(a.severidade) === 2 ? "warning" : "info";
li.className = "sino-item " + sev;
li.innerHTML = "<span class=\"nome\">" + esc(a.nome) + "</span>"
+ (a.resumo ? "<span class=\"resumo\">" + esc(a.resumo) + "</span>" : "")
+ (a.tenant ? "<span class=\"quem\">" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "</span>"
: (a.desde ? "<span class=\"quem\">" + ha(a.desde) + "</span>" : ""));
sinoLista.appendChild(li);
});
}
function atualizarSino(token) {
buscarAlertasInfra(token).then(pintarSino);
}
function ligarSino(token) {
sinoCaixa.classList.remove("oculto");
atualizarSino(token);
if (sinoIntervalo) clearInterval(sinoIntervalo);
sinoIntervalo = setInterval(function () { atualizarSino(token); }, INTERVALO_SINO_MS);
}
function desligarSino() {
sinoCaixa.classList.add("oculto");
fecharSino();
if (sinoIntervalo) { clearInterval(sinoIntervalo); sinoIntervalo = null; }
}
function abrirSino() {
sinoPainel.classList.remove("oculto");
sino.setAttribute("aria-expanded", "true");
}
function fecharSino() {
sinoPainel.classList.add("oculto");
sino.setAttribute("aria-expanded", "false");
}
/* ── formatacao ─────────────────────────────────────────────────────────── */
function esc(s) {
@@ -2958,6 +3085,7 @@ data:
tokenAtual = token;
pintar();
mostrar(conteudo);
ligarSino(token);
});
}
@@ -2992,6 +3120,17 @@ data:
location.reload();
});
sino.addEventListener("click", function () {
var abrindo = sinoPainel.classList.contains("oculto");
if (abrindo) { abrirSino(); if (tokenAtual) atualizarSino(tokenAtual); } else { fecharSino(); }
});
document.addEventListener("click", function (ev) {
if (!sinoPainel.classList.contains("oculto") && !sinoCaixa.contains(ev.target)) fecharSino();
});
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape") fecharSino();
});
botaoAtualizar.addEventListener("click", function () {
if (!tokenAtual) return;
botaoAtualizar.disabled = true;