diff --git a/moldes/consolidado/96-publico-configmap.yaml b/moldes/consolidado/96-publico-configmap.yaml index 0d32bd7..a2053ce 100644 --- a/moldes/consolidado/96-publico-configmap.yaml +++ b/moldes/consolidado/96-publico-configmap.yaml @@ -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. --> - + @@ -2234,6 +2266,18 @@ data: Painel do dono +
+ + +
@@ -2388,7 +2432,7 @@ data: - + @@ -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 = "" + esc(a.nome) + "" + + (a.resumo ? "" + esc(a.resumo) + "" : "") + + (a.tenant ? "" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "" + : (a.desde ? "" + ha(a.desde) + "" : "")); + 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; diff --git a/tenants/acme/96-publico-configmap.yaml b/tenants/acme/96-publico-configmap.yaml index 9c3124c..dcc8e75 100644 --- a/tenants/acme/96-publico-configmap.yaml +++ b/tenants/acme/96-publico-configmap.yaml @@ -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. --> - + @@ -2234,6 +2266,18 @@ data: Painel do dono +
+ + +
@@ -2388,7 +2432,7 @@ data: - + @@ -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 = "" + esc(a.nome) + "" + + (a.resumo ? "" + esc(a.resumo) + "" : "") + + (a.tenant ? "" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "" + : (a.desde ? "" + ha(a.desde) + "" : "")); + 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; diff --git a/tenants/demo/96-publico-configmap.yaml b/tenants/demo/96-publico-configmap.yaml index 49bc690..7519d70 100644 --- a/tenants/demo/96-publico-configmap.yaml +++ b/tenants/demo/96-publico-configmap.yaml @@ -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. --> - + @@ -2234,6 +2266,18 @@ data: Painel do dono +
+ + +
@@ -2388,7 +2432,7 @@ data: - + @@ -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 = "" + esc(a.nome) + "" + + (a.resumo ? "" + esc(a.resumo) + "" : "") + + (a.tenant ? "" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "" + : (a.desde ? "" + ha(a.desde) + "" : "")); + 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; diff --git a/tenants/escolinha-de-teste-2/96-publico-configmap.yaml b/tenants/escolinha-de-teste-2/96-publico-configmap.yaml index e8e5048..48d2fbf 100644 --- a/tenants/escolinha-de-teste-2/96-publico-configmap.yaml +++ b/tenants/escolinha-de-teste-2/96-publico-configmap.yaml @@ -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. --> - + @@ -2234,6 +2266,18 @@ data: Painel do dono +
+ + +
@@ -2388,7 +2432,7 @@ data: - + @@ -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 = "" + esc(a.nome) + "" + + (a.resumo ? "" + esc(a.resumo) + "" : "") + + (a.tenant ? "" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "" + : (a.desde ? "" + ha(a.desde) + "" : "")); + 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; diff --git a/tenants/escolinha-de-teste/96-publico-configmap.yaml b/tenants/escolinha-de-teste/96-publico-configmap.yaml index c97b25d..2dc072b 100644 --- a/tenants/escolinha-de-teste/96-publico-configmap.yaml +++ b/tenants/escolinha-de-teste/96-publico-configmap.yaml @@ -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. --> - + @@ -2234,6 +2266,18 @@ data: Painel do dono +
+ + +
@@ -2388,7 +2432,7 @@ data: - + @@ -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 = "" + esc(a.nome) + "" + + (a.resumo ? "" + esc(a.resumo) + "" : "") + + (a.tenant ? "" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "" + : (a.desde ? "" + ha(a.desde) + "" : "")); + 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; diff --git a/tenants/escolinha/96-publico-configmap.yaml b/tenants/escolinha/96-publico-configmap.yaml index 61e061e..d8fd56d 100644 --- a/tenants/escolinha/96-publico-configmap.yaml +++ b/tenants/escolinha/96-publico-configmap.yaml @@ -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. --> - + @@ -2234,6 +2266,18 @@ data: Painel do dono +
+ + +
@@ -2388,7 +2432,7 @@ data: - + @@ -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 = "" + esc(a.nome) + "" + + (a.resumo ? "" + esc(a.resumo) + "" : "") + + (a.tenant ? "" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "" + : (a.desde ? "" + ha(a.desde) + "" : "")); + 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; diff --git a/tenants/piloto/96-publico-configmap.yaml b/tenants/piloto/96-publico-configmap.yaml index dd20505..92b66a7 100644 --- a/tenants/piloto/96-publico-configmap.yaml +++ b/tenants/piloto/96-publico-configmap.yaml @@ -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. --> - + @@ -2234,6 +2266,18 @@ data: Painel do dono +
+ + +
@@ -2388,7 +2432,7 @@ data: - + @@ -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 = "" + esc(a.nome) + "" + + (a.resumo ? "" + esc(a.resumo) + "" : "") + + (a.tenant ? "" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "" + : (a.desde ? "" + ha(a.desde) + "" : "")); + 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; diff --git a/tenants/plataforma/96-publico-configmap.yaml b/tenants/plataforma/96-publico-configmap.yaml index 91fb030..87663df 100644 --- a/tenants/plataforma/96-publico-configmap.yaml +++ b/tenants/plataforma/96-publico-configmap.yaml @@ -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. --> - + @@ -2234,6 +2266,18 @@ data: Painel do dono +
+ + +
@@ -2388,7 +2432,7 @@ data: - + @@ -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 = "" + esc(a.nome) + "" + + (a.resumo ? "" + esc(a.resumo) + "" : "") + + (a.tenant ? "" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "" + : (a.desde ? "" + ha(a.desde) + "" : "")); + 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; diff --git a/tenants/teste5/96-publico-configmap.yaml b/tenants/teste5/96-publico-configmap.yaml index b8612b9..be25591 100644 --- a/tenants/teste5/96-publico-configmap.yaml +++ b/tenants/teste5/96-publico-configmap.yaml @@ -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. --> - + @@ -2234,6 +2266,18 @@ data: Painel do dono +
+ + +
@@ -2388,7 +2432,7 @@ data: - + @@ -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 = "" + esc(a.nome) + "" + + (a.resumo ? "" + esc(a.resumo) + "" : "") + + (a.tenant ? "" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "" + : (a.desde ? "" + ha(a.desde) + "" : "")); + 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; diff --git a/tenants/zico-0/96-publico-configmap.yaml b/tenants/zico-0/96-publico-configmap.yaml index 88fa2a9..85690d2 100644 --- a/tenants/zico-0/96-publico-configmap.yaml +++ b/tenants/zico-0/96-publico-configmap.yaml @@ -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. --> - + @@ -2234,6 +2266,18 @@ data: Painel do dono +
+ + +
@@ -2388,7 +2432,7 @@ data: - + @@ -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 = "" + esc(a.nome) + "" + + (a.resumo ? "" + esc(a.resumo) + "" : "") + + (a.tenant ? "" + esc(a.tenant) + (a.desde ? " — " + ha(a.desde) : "") + "" + : (a.desde ? "" + ha(a.desde) + "" : "")); + 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;