diff --git a/moldes/consolidado/95-frontend-spa.yaml b/moldes/consolidado/95-frontend-spa.yaml index 83e9c6f..3e91509 100644 --- a/moldes/consolidado/95-frontend-spa.yaml +++ b/moldes/consolidado/95-frontend-spa.yaml @@ -149,6 +149,9 @@ spec: - { key: "minha-conta.css", path: "minha-conta.css" } - { key: "minha-conta.html", path: "minha-conta.html" } - { key: "minha-conta.js", path: "minha-conta.js" } + - { key: "painel-c4.css", path: "painel-c4.css" } + - { key: "painel-c4.js", path: "painel-c4.js" } + - { key: "painel-markdown.js", path: "painel-markdown.js" } - { key: "painel.css", path: "painel.css" } - { key: "painel.html", path: "painel.html" } - { key: "painel.js", path: "painel.js" } diff --git a/moldes/consolidado/96-publico-configmap.yaml b/moldes/consolidado/96-publico-configmap.yaml index c9edb1e..02bf9b5 100644 --- a/moldes/consolidado/96-publico-configmap.yaml +++ b/moldes/consolidado/96-publico-configmap.yaml @@ -3157,6 +3157,1867 @@ data: comecar(); })(); + painel-c4.css: | + /* gerado de governanca/assets/c4.js (ATM_C4.ESTILO) por governanca/gerar.py - nao edite */ + .c4-f-pessoa{fill:var(--c4-pessoa, #08427B);}.c4-t-pessoa{fill:var(--c4-texto-caixa, #FFFFFF);} + .c4-f-sistema{fill:var(--c4-sistema, #1168BD);}.c4-t-sistema{fill:var(--c4-texto-caixa, #FFFFFF);} + .c4-f-sistema-externo{fill:var(--c4-externo, #6B6B6B);}.c4-t-sistema-externo{fill:var(--c4-texto-caixa, #FFFFFF);} + .c4-f-container{fill:var(--c4-container, #438DD5);}.c4-t-container{fill:var(--c4-texto-caixa, #FFFFFF);} + .c4-f-container-externo{fill:var(--c4-externo, #6B6B6B);}.c4-t-container-externo{fill:var(--c4-texto-caixa, #FFFFFF);} + .c4-f-componente{fill:var(--c4-componente, #85BBF0);}.c4-t-componente{fill:var(--c4-texto-componente, #0B2C3D);} + .c4-f-banco{fill:var(--c4-banco, #438DD5);}.c4-t-banco{fill:var(--c4-texto-caixa, #FFFFFF);} + .c4-f-fila{fill:var(--c4-container, #438DD5);}.c4-t-fila{fill:var(--c4-texto-caixa, #FFFFFF);} + .c4-f-no-de-infra{fill:var(--c4-infra, #2F4F5F);}.c4-t-no-de-infra{fill:var(--c4-texto-caixa, #FFFFFF);} + .c4-borda,.c4-linha{stroke:var(--c4-linha,var(--mudo,#5C6870));}.c4-seta-ponta{fill:var(--c4-linha,var(--mudo,#5C6870));} + .c4-txt{fill:var(--c4-texto,var(--texto,#1B2A33));}.c4-fundo-rotulo{fill:var(--c4-fundo,var(--superficie,#FFFFFF));} + figure[data-c4]{margin:1.5rem 0;} + .c4-titulo{font-weight:700;margin:0 0 .5rem;color:var(--titulo,inherit);} + .c4-titulo span{font-weight:400;color:var(--mudo,#5C6870);font-size:.85em;margin-left:.4rem;} + .c4-palco{overflow:auto;border:1px solid var(--linha,#E7E5E1);border-radius:var(--raio,10px);background:var(--c4-fundo,var(--superficie,#FFF));} + .c4-palco[data-tamanho="ajustado"] .c4-svg{width:100%;height:auto;} + .c4-palco .c4-svg{display:block;} + .c4-no{cursor:pointer;outline:none;} + .c4-no:focus-visible > rect,.c4-no:focus-visible > path,.c4-no:focus-visible > circle,.c4-no[data-c4-ativo] > rect,.c4-no[data-c4-ativo] > path{stroke:var(--foco,#2563EB);stroke-width:3;} + .c4-barra{display:flex;flex-wrap:wrap;gap:.5rem 1rem;align-items:center;margin:.5rem 0;} + .c4-barra button,.c4-fechar{font:inherit;font-size:.85rem;padding:.2rem .6rem;border:1px solid var(--linha-forte,#D6D2CB);border-radius:6px;background:var(--superficie,#FFF);color:var(--texto,#1B2A33);cursor:pointer;} + .c4-dica{font-size:.85rem;color:var(--mudo,#5C6870);} + .c4-legenda{display:flex;flex-wrap:wrap;gap:.25rem 1rem;list-style:none;padding:0;margin:.25rem 0 .5rem;font-size:.85rem;color:var(--texto,#1B2A33);} + .c4-legenda li{display:flex;align-items:center;gap:.35rem;} + .c4-detalhe{position:relative;border:1px solid var(--linha-forte,#D6D2CB);border-left:4px solid var(--c4-container,#438DD5);border-radius:6px;padding:.6rem 2.5rem .6rem .9rem;margin:.5rem 0;background:var(--superficie,#FFF);font-size:.92rem;} + .c4-detalhe p{margin:.25rem 0;}.c4-detalhe ul{margin:.2rem 0 .4rem 1.1rem;padding:0;} + .c4-detalhe-titulo span{color:var(--mudo,#5C6870);font-size:.85em;} + .c4-detalhe-sub{font-weight:600;margin-top:.5rem !important;} + .c4-fechar{position:absolute;top:.4rem;right:.4rem;line-height:1;} + .c4-tabela{margin:.5rem 0;font-size:.88rem;} + .c4-tabela summary{cursor:pointer;color:var(--link,#A24C08);} + .c4-tabela table{border-collapse:collapse;width:100%;margin:.5rem 0;} + .c4-tabela caption{text-align:left;font-weight:600;padding:.25rem 0;} + .c4-tabela th,.c4-tabela td{border:1px solid var(--linha,#E7E5E1);padding:.3rem .45rem;vertical-align:top;text-align:left;} + .c4-tabela code{font-size:.85em;word-break:break-all;} + .c4-erro{border:1px dashed var(--linha-forte,#D6D2CB);border-radius:6px;padding:.75rem 1rem;color:var(--mudo,#5C6870);} + @media print{.c4-barra,.c4-detalhe,.c4-fechar{display:none !important;}.c4-palco{overflow:visible;border:0;}.c4-palco .c4-svg{width:100% !important;height:auto !important;}figure[data-c4]{break-inside:avoid-page;}.c4-tabela summary{display:none;}} + + painel-c4.js: | + /* + * c4.js — desenha as vistas do modelo C4 (governanca/c4/modelo.json) em SVG. + * + * Sem dependência. Uso: + * window.ATM_C4.desenhar(raiz, modelo) + * procura `figure[data-c4]` dentro de `raiz` (o valor do atributo é o id da vista), + * troca o conteúdo da figura por: título, diagrama SVG (com /<desc>), + * botão de tamanho, legenda, painel de detalhe (clique ou Enter numa caixa) e uma + * tabela textual equivalente (elementos e relações), útil para impressão e leitores + * de tela. Vista inexistente vira uma mensagem dentro da figura. + * + * Cores por variáveis CSS, com padrão: --c4-pessoa, --c4-sistema, --c4-container, + * --c4-componente, --c4-externo, --c4-banco, --c4-infra, --c4-texto, --c4-linha, + * --c4-fundo, --c4-texto-caixa. + * + * O layout é por camadas: pessoas no topo; quem só chama o foco logo abaixo; o + * que está dentro do foco no meio (quem chama acima de quem é chamado); externos, + * bancos de fora e o resto embaixo. Cada linha é ordenada pela média da posição + * dos vizinhos (heurística do baricentro) para cruzar menos setas. + * + * As funções puras (prepararVista, calcularLayout, gerarSvg, quebrarTexto, + * validarModelo) ficam expostas para os testes (node governanca/assets/c4.teste.js). + */ + (function (global) { + 'use strict'; + + var ROTULO_TIPO = { + 'pessoa': 'Pessoa', + 'sistema': 'Sistema', + 'sistema-externo': 'Sistema externo', + 'container': 'Container', + 'container-externo': 'Container externo', + 'componente': 'Componente', + 'banco': 'Banco de dados', + 'fila': 'Fila', + 'no-de-infra': 'Nó de infraestrutura' + }; + var TIPOS = Object.keys(ROTULO_TIPO); + var EXTERNO = { 'sistema-externo': true, 'container-externo': true }; + var NIVEIS = { contexto: 'Contexto', containers: 'Containers', componentes: 'Componentes', implantacao: 'Implantação' }; + + // cor padrão (preenchimento, texto) de cada tipo. O desenho usa classe + atributo de + // apresentação (fill/stroke), nunca style="": assim funciona sob CSP style-src 'self' + // (o painel do dono). As variáveis CSS (--c4-*) entram pela folha ESTILO, quando ela + // puder ser aplicada; sem ela, valem estas cores. + var COR = { + 'pessoa': ['#08427B', '#FFFFFF'], + 'sistema': ['#1168BD', '#FFFFFF'], + 'sistema-externo': ['#6B6B6B', '#FFFFFF'], + 'container': ['#438DD5', '#FFFFFF'], + 'container-externo': ['#6B6B6B', '#FFFFFF'], + 'componente': ['#85BBF0', '#0B2C3D'], + 'banco': ['#438DD5', '#FFFFFF'], + 'fila': ['#438DD5', '#FFFFFF'], + 'no-de-infra': ['#2F4F5F', '#FFFFFF'] + }; + var VAR_TIPO = { + 'pessoa': '--c4-pessoa', 'sistema': '--c4-sistema', 'sistema-externo': '--c4-externo', 'container': '--c4-container', + 'container-externo': '--c4-externo', 'componente': '--c4-componente', 'banco': '--c4-banco', 'fila': '--c4-container', + 'no-de-infra': '--c4-infra' + }; + function pinta(tipo) { var k = COR[tipo] ? tipo : 'container'; return ' class="c4-f-' + k + ' c4-borda" fill="' + COR[k][0] + '" stroke="#5C6870" stroke-width="1"'; } + function letra(tipo) { var k = COR[tipo] ? tipo : 'container'; return ' class="c4-t-' + k + '" fill="' + COR[k][1] + '"'; } + var LINHA = ' class="c4-linha" stroke="#5C6870"'; + var TEXTO = ' class="c4-txt" fill="#1B2A33"'; + var FUNDO = ' class="c4-fundo-rotulo" fill="#FFFFFF" fill-opacity=".93"'; + + var CFG = { + largura: 216, // largura de cada caixa + folgaX: 64, // espaço horizontal entre caixas + folgaY: 132, // espaço vertical entre linhas (é onde ficam os rótulos) + margem: 28, + maxPorLinha: 6, + cabeca: 30, // altura da "cabeça" da pessoa + linhaNome: 16, + linhaTexto: 14, + charsNome: 24, + charsTipo: 32, + charsDesc: 34, + linhasDesc: 4, + charsRotulo: 28, + linhasRotulo: 3, + fronteira: 22, // folga da fronteira em volta do foco + tituloFronteira: 22, + larguraAjustavel: 1240 + }; + + // ───────────────────────── utilidades ───────────────────────── + + function esc(s) { + return String(s == null ? '' : s) + .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + .replace(/"/g, '"').replace(/'/g, '''); + } + + /** Quebra `texto` em linhas de até `max` caracteres (no máximo `maxLinhas`; o resto vira "…"). */ + function quebrarTexto(texto, max, maxLinhas) { + var palavras = String(texto == null ? '' : texto).replace(/\s+/g, ' ').trim().split(' '); + var linhas = []; + var atual = ''; + for (var i = 0; i < palavras.length; i++) { + var p = palavras[i]; + if (!p) continue; + while (p.length > max) { // palavra maior que a linha: corta + if (atual) { linhas.push(atual); atual = ''; } + linhas.push(p.slice(0, max - 1) + '-'); + p = p.slice(max - 1); + } + if (!atual) atual = p; + else if ((atual + ' ' + p).length <= max) atual += ' ' + p; + else { linhas.push(atual); atual = p; } + } + if (atual) linhas.push(atual); + if (maxLinhas && linhas.length > maxLinhas) { + linhas = linhas.slice(0, maxLinhas); + var ult = linhas[maxLinhas - 1]; + if (ult.length >= max) ult = ult.slice(0, max - 1); + linhas[maxLinhas - 1] = ult.replace(/[\s,.;:-]+$/, '') + '…'; + } + return linhas; + } + + function indexar(modelo) { + var porId = {}; + (modelo && modelo.elementos || []).forEach(function (e) { porId[e.id] = e; }); + return porId; + } + + function ancestrais(porId, id) { + var lista = []; + var visto = {}; + var e = porId[id]; + while (e && e.pai && !visto[e.pai]) { + visto[e.pai] = true; + lista.push(e.pai); + e = porId[e.pai]; + } + return lista; + } + + // ───────────────────────── validação estrutural ───────────────────────── + + /** Confere a forma do modelo. Devolve a lista de erros (vazia = válido). */ + function validarModelo(modelo) { + var erros = []; + if (!modelo || typeof modelo !== 'object') return ['modelo ausente']; + if (modelo.versao !== 1) erros.push('versao deve ser 1'); + var els = Array.isArray(modelo.elementos) ? modelo.elementos : (erros.push('elementos deve ser lista'), []); + var rels = Array.isArray(modelo.relacoes) ? modelo.relacoes : (erros.push('relacoes deve ser lista'), []); + var vistas = Array.isArray(modelo.vistas) ? modelo.vistas : (erros.push('vistas deve ser lista'), []); + var ids = {}; + els.forEach(function (e, i) { + var onde = 'elemento ' + (e && e.id || '#' + i); + if (!e || typeof e.id !== 'string' || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(e.id)) erros.push(onde + ': id fora do padrão kebab'); + else if (ids[e.id]) erros.push(onde + ': id repetido'); + else ids[e.id] = e; + if (TIPOS.indexOf(e && e.tipo) < 0) erros.push(onde + ': tipo desconhecido "' + (e && e.tipo) + '"'); + if (!e || !e.nome) erros.push(onde + ': sem nome'); + if (!e || !e.descricao) erros.push(onde + ': sem descrição'); + if (!e || !Array.isArray(e.fontes) || !e.fontes.length || e.fontes.some(function (f) { return typeof f !== 'string' || !f.trim(); })) { + erros.push(onde + ': sem fontes'); + } + }); + els.forEach(function (e) { + if (e && e.pai) { + if (!ids[e.pai]) erros.push('elemento ' + e.id + ': pai inexistente "' + e.pai + '"'); + else if (e.pai === e.id || ancestrais(ids, e.id).indexOf(e.id) >= 0) erros.push('elemento ' + e.id + ': ciclo de pai'); + } + }); + rels.forEach(function (r, i) { + var onde = 'relação #' + i + ' (' + (r && r.de) + ' → ' + (r && r.para) + ')'; + if (!r || !ids[r.de]) erros.push(onde + ': "de" inexistente'); + if (!r || !ids[r.para]) erros.push(onde + ': "para" inexistente'); + if (r && r.de === r.para) erros.push(onde + ': relação consigo mesmo'); + if (!r || !r.descricao) erros.push(onde + ': sem descrição'); + if (!r || !Array.isArray(r.fontes) || !r.fontes.length || r.fontes.some(function (f) { return typeof f !== 'string' || !f.trim(); })) { + erros.push(onde + ': sem fontes'); + } + }); + var idsVista = {}; + vistas.forEach(function (v, i) { + var onde = 'vista ' + (v && v.id || '#' + i); + if (!v || !v.id) erros.push(onde + ': sem id'); + else if (idsVista[v.id]) erros.push(onde + ': id repetido'); + else idsVista[v.id] = true; + if (!v || !NIVEIS[v.nivel]) erros.push(onde + ': nível desconhecido "' + (v && v.nivel) + '"'); + if (!v || !v.titulo) erros.push(onde + ': sem título'); + if (v && v.foco && !ids[v.foco]) erros.push(onde + ': foco inexistente "' + v.foco + '"'); + if (v && (v.nivel === 'containers' || v.nivel === 'componentes') && !v.foco) erros.push(onde + ': nível ' + v.nivel + ' exige foco'); + if (!v || !Array.isArray(v.incluir) || !v.incluir.length) erros.push(onde + ': incluir vazio'); + else { + var vistos = {}; + v.incluir.forEach(function (id) { + if (!ids[id]) erros.push(onde + ': inclui id inexistente "' + id + '"'); + if (vistos[id]) erros.push(onde + ': inclui "' + id + '" duas vezes'); + vistos[id] = true; + }); + } + }); + return erros; + } + + // ───────────────────────── preparação da vista ───────────────────────── + + /** + * Monta o que uma vista desenha: os elementos incluídos e as relações entre eles. + * Relação entre elementos mais finos (ex.: componente → componente) "sobe" para o + * ancestral incluído na vista; se já houver relação direta entre o mesmo par, só + * as diretas são usadas. + */ + function prepararVista(modelo, vistaId) { + var vistas = (modelo && modelo.vistas) || []; + var vista = null; + for (var i = 0; i < vistas.length; i++) if (vistas[i].id === vistaId) vista = vistas[i]; + if (!vista) return { erro: 'A vista "' + vistaId + '" não existe no modelo C4.' }; + var porId = indexar(modelo); + var avisos = []; + var incluidos = {}; + var elementos = []; + vista.incluir.forEach(function (id) { + if (!porId[id]) { avisos.push('id inexistente: ' + id); return; } + if (id === vista.foco || incluidos[id]) return; // o foco vira fronteira, não caixa + incluidos[id] = true; + elementos.push(porId[id]); + }); + var foco = vista.foco ? porId[vista.foco] || null : null; + + function mapear(id) { + if (incluidos[id]) return id; + var anc = ancestrais(porId, id); + for (var k = 0; k < anc.length; k++) if (incluidos[anc[k]]) return anc[k]; + return null; + } + var grupos = {}; + var ordem = []; + (modelo.relacoes || []).forEach(function (r) { + var a = mapear(r.de); + var b = mapear(r.para); + if (!a || !b || a === b) return; + // as duas pontas subiram (ex.: componente de um container → componente de outro): é relação + // interna de dois níveis abaixo; no nível da vista ela vira ruído, e fica só na vista de componentes + if (a !== r.de && b !== r.para) return; + var chave = a + '\u0000' + b; + if (!grupos[chave]) { grupos[chave] = { de: a, para: b, diretas: [], derivadas: [] }; ordem.push(chave); } + (a === r.de && b === r.para ? grupos[chave].diretas : grupos[chave].derivadas).push(r); + }); + var relacoes = ordem.map(function (chave) { + var g = grupos[chave]; + var usar = g.diretas.length ? g.diretas : g.derivadas; + var techs = []; + usar.forEach(function (r) { if (r.tecnologia && techs.indexOf(r.tecnologia) < 0) techs.push(r.tecnologia); }); + var descricao = usar[0].descricao + (usar.length > 1 ? ' (+' + (usar.length - 1) + ')' : ''); + return { + de: g.de, para: g.para, descricao: descricao, + tecnologia: techs.slice(0, 2).join(', ') + (techs.length > 2 ? '…' : ''), + derivada: !g.diretas.length, + origem: usar + }; + }); + + var interno = {}; + elementos.forEach(function (e) { + if (foco) interno[e.id] = ancestrais(porId, e.id).indexOf(foco.id) >= 0; + else interno[e.id] = !EXTERNO[e.tipo] && e.tipo !== 'pessoa'; + }); + // com foco, só entram as relações que tocam algo de dentro (as de fora entre si são de outra vista) + if (foco) relacoes = relacoes.filter(function (rel) { return interno[rel.de] || interno[rel.para]; }); + return { vista: vista, foco: foco, elementos: elementos, relacoes: relacoes, interno: interno, avisos: avisos, porId: porId }; + } + + // ───────────────────────── layout ───────────────────────── + + function textoTipo(e) { + var t = ROTULO_TIPO[e.tipo] || e.tipo; + return '[' + t + (e.tecnologia ? ': ' + e.tecnologia : '') + ']'; + } + + function medirCaixa(e, cfg) { + var nome = quebrarTexto(e.nome, cfg.charsNome, 2); + var tipo = quebrarTexto(textoTipo(e), cfg.charsTipo, 2); + var desc = quebrarTexto(e.descricao, cfg.charsDesc, cfg.linhasDesc); + var h = 14 + nome.length * cfg.linhaNome + 4 + tipo.length * cfg.linhaTexto + 6 + desc.length * cfg.linhaTexto + 10; + if (e.tipo === 'banco') h += 14; + return { nome: nome, tipo: tipo, desc: desc, h: h }; + } + + /** + * Calcula posições de caixas, fronteira, setas e rótulos. Não usa DOM: a largura + * do texto é estimada pelo número de caracteres (as linhas já vêm quebradas). + */ + function calcularLayout(prep, opcoes) { + var cfg = {}; + Object.keys(CFG).forEach(function (k) { cfg[k] = CFG[k]; }); + Object.keys(opcoes || {}).forEach(function (k) { cfg[k] = opcoes[k]; }); + + var els = prep.elementos; + var porIdV = {}; + els.forEach(function (e, i) { porIdV[e.id] = { e: e, ordem: i }; }); + var saem = {}, entram = {}; + els.forEach(function (e) { saem[e.id] = []; entram[e.id] = []; }); + prep.relacoes.forEach(function (r) { saem[r.de].push(r.para); entram[r.para].push(r.de); }); + + // 1. grupos verticais + var grupo = {}; + els.forEach(function (e) { + if (e.tipo === 'pessoa') grupo[e.id] = 'topo'; + else if (prep.interno[e.id]) grupo[e.id] = 'meio'; + else if (EXTERNO[e.tipo]) grupo[e.id] = 'base'; + else { + var paraDentro = saem[e.id].some(function (d) { return prep.interno[d]; }); + var deDentro = entram[e.id].some(function (o) { return prep.interno[o]; }); + grupo[e.id] = paraDentro && !deDentro ? 'entrada' : 'base'; + } + }); + + // 2. camadas do meio: caminho mais longo, ignorando arestas de retorno (ciclos) + var meio = els.filter(function (e) { return grupo[e.id] === 'meio'; }).map(function (e) { return e.id; }); + var arestasMeio = {}; + meio.forEach(function (id) { arestasMeio[id] = saem[id].filter(function (d) { return grupo[d] === 'meio'; }); }); + var estado = {}, retorno = {}; + function dfs(u) { + estado[u] = 1; + arestasMeio[u].forEach(function (v) { + if (estado[v] === 1) retorno[u + '>' + v] = true; + else if (!estado[v]) dfs(v); + }); + estado[u] = 2; + } + // começa por quem recebe de fora do meio (ou não recebe de ninguém do meio) + var inicio = meio.filter(function (id) { return !entram[id].some(function (o) { return grupo[o] === 'meio'; }); }); + inicio.concat(meio).forEach(function (id) { if (!estado[id]) dfs(id); }); + var camada = {}; + meio.forEach(function (id) { camada[id] = 0; }); + for (var it = 0; it < meio.length; it++) { + var mudou = false; + meio.forEach(function (u) { + arestasMeio[u].forEach(function (v) { + if (retorno[u + '>' + v]) return; + if (camada[v] < camada[u] + 1) { camada[v] = camada[u] + 1; mudou = true; } + }); + }); + if (!mudou) break; + } + // bancos e filas sem saída descem para a última camada do meio + var maxCamada = 0; + meio.forEach(function (id) { if (camada[id] > maxCamada) maxCamada = camada[id]; }); + meio.forEach(function (id) { + var t = porIdV[id].e.tipo; + if ((t === 'banco' || t === 'fila') && !arestasMeio[id].length) camada[id] = Math.max(camada[id], maxCamada); + }); + + // 3. linhas (cada linha com no máximo cfg.maxPorLinha caixas) + function quebrarEmLinhas(ids, rotulo) { + var linhas = []; + // linhas equilibradas: 13 caixas com máximo 6 viram 5 + 4 + 4, e não 6 + 6 + 1 + var n = Math.max(1, Math.ceil(ids.length / cfg.maxPorLinha)); + var por = Math.ceil(ids.length / n); + for (var i = 0; i < ids.length; i += por) linhas.push({ grupo: rotulo, ids: ids.slice(i, i + por) }); + return linhas; + } + function doGrupo(g) { return els.filter(function (e) { return grupo[e.id] === g; }).map(function (e) { return e.id; }); } + var linhas = []; + linhas = linhas.concat(quebrarEmLinhas(doGrupo('topo'), 'topo')); + linhas = linhas.concat(quebrarEmLinhas(doGrupo('entrada'), 'entrada')); + var camadasMeio = []; + meio.forEach(function (id) { (camadasMeio[camada[id]] = camadasMeio[camada[id]] || []).push(id); }); + camadasMeio.forEach(function (ids) { if (ids && ids.length) linhas = linhas.concat(quebrarEmLinhas(ids, 'meio')); }); + linhas = linhas.concat(quebrarEmLinhas(doGrupo('base'), 'base')); + + // 4. ordem dentro de cada linha: baricentro dos vizinhos (algumas passadas) + var vizinhos = {}; + els.forEach(function (e) { vizinhos[e.id] = saem[e.id].concat(entram[e.id]); }); + var x = {}; + function posicionar(l) { + var w = l.ids.length * cfg.largura + (l.ids.length - 1) * cfg.folgaX; + l.ids.forEach(function (id, i) { x[id] = -w / 2 + i * (cfg.largura + cfg.folgaX) + cfg.largura / 2; }); + } + linhas.forEach(posicionar); + function passada(lista) { + lista.forEach(function (l) { + var chave = {}; + l.ids.forEach(function (id, i) { + var vs = vizinhos[id].filter(function (v) { return l.ids.indexOf(v) < 0; }); + chave[id] = vs.length ? vs.reduce(function (s, v) { return s + x[v]; }, 0) / vs.length : x[id]; + chave[id] += i * 1e-6; // estável + }); + l.ids.sort(function (a, b) { return chave[a] - chave[b]; }); + posicionar(l); + }); + } + for (var p = 0; p < 4; p++) { passada(linhas); passada(linhas.slice().reverse()); } + + // 5. alturas das caixas + var medida = {}; + els.forEach(function (e) { medida[e.id] = medirCaixa(e, cfg); }); + var larguraMax = 0; + linhas.forEach(function (l) { + var w = l.ids.length * cfg.largura + (l.ids.length - 1) * cfg.folgaX; + if (w > larguraMax) larguraMax = w; + }); + var temFronteira = !!prep.foco && linhas.some(function (l) { return l.grupo === 'meio'; }); + var extraLado = temFronteira ? cfg.fronteira + 8 : 0; + var largura = larguraMax + 2 * cfg.margem + 2 * extraLado; + var centro = largura / 2; + var linhaDe = {}; + linhas.forEach(function (l, i) { l.ids.forEach(function (id) { linhaDe[id] = i; }); }); + + // 6. posiciona as linhas com as folgas dadas (uma por intervalo entre linhas) + function montar(folgas) { + var y = cfg.margem; + var nos = {}; + var faixas = []; // [topo, base] de cada linha (inclui a cabeça das pessoas) + linhas.forEach(function (l, i) { + if (i > 0) { + y += folgas[i - 1]; + var ant = linhas[i - 1].grupo; + if (temFronteira && l.grupo === 'meio' && ant !== 'meio') y += cfg.fronteira + cfg.tituloFronteira; + if (temFronteira && l.grupo !== 'meio' && ant === 'meio') y += cfg.fronteira; + } + var cabeca = l.ids.some(function (id) { return porIdV[id].e.tipo === 'pessoa'; }) ? cfg.cabeca : 0; + var h = 0; + l.ids.forEach(function (id) { if (medida[id].h > h) h = medida[id].h; }); + l.ids.forEach(function (id) { + var e = porIdV[id].e; + var m = medida[id]; + var c = e.tipo === 'pessoa' ? cfg.cabeca : 0; + nos[id] = { + id: id, tipo: e.tipo, x: centro + x[id] - cfg.largura / 2, y: y + cabeca, w: cfg.largura, h: h, + cabeca: c, nome: m.nome, linhaTipo: m.tipo, desc: m.desc, grupo: l.grupo, linha: i + }; + }); + faixas.push([y, y + cabeca + h]); + y += cabeca + h; + }); + return { nos: nos, faixas: faixas, altura: y + cfg.margem }; + } + + function retDe(n) { return { x: n.x, y: n.y - n.cabeca, w: n.w, h: n.h + n.cabeca }; } + + // 7. rotas: porta no lado de cima/baixo de cada caixa, distribuída pela posição + // da outra ponta; entre linhas não vizinhas a seta desce pelos corredores + // entre as caixas das linhas do meio (não atravessa caixa). + function rotear(geo) { + var nos = geo.nos, faixas = geo.faixas; + var lados = {}; // id|lado -> [{rel, outroX}] + function lado(id, qual, item) { (lados[id + '|' + qual] = lados[id + '|' + qual] || []).push(item); } + var rotas = prep.relacoes.map(function (r) { + var a = nos[r.de], b = nos[r.para]; + var rota = { r: r, a: a, b: b }; + if (a.linha < b.linha) { rota.tipo = 'desce'; lado(r.de, 'baixo', { rota: rota, ponta: 'a', outro: b }); lado(r.para, 'cima', { rota: rota, ponta: 'b', outro: a }); } + else if (a.linha > b.linha) { rota.tipo = 'sobe'; lado(r.de, 'cima', { rota: rota, ponta: 'a', outro: b }); lado(r.para, 'baixo', { rota: rota, ponta: 'b', outro: a }); } + else { rota.tipo = 'mesma'; lado(r.de, 'baixo', { rota: rota, ponta: 'a', outro: b }); lado(r.para, 'baixo', { rota: rota, ponta: 'b', outro: a }); } + return rota; + }); + Object.keys(lados).forEach(function (k) { + var lista = lados[k]; + var id = k.split('|')[0], qual = k.split('|')[1]; + var n = nos[id]; + lista.sort(function (p, q) { return (p.outro.x - q.outro.x) || (p.outro.linha - q.outro.linha); }); + var util = n.w - 24; + lista.forEach(function (it, i) { + var px = n.x + 12 + util * (i + 1) / (lista.length + 1); + var py = qual === 'baixo' ? n.y + n.h : n.y - n.cabeca; + if (qual === 'cima' && n.tipo === 'pessoa') py = n.y; // entra pelo corpo, não pela cabeça + it.rota[it.ponta === 'a' ? 'pa' : 'pb'] = { x: px, y: py }; + }); + }); + var usoCorredor = {}; + function corredor(iLinha, xDesejado) { + var ids = linhas[iLinha].ids.slice().sort(function (p, q) { return nos[p].x - nos[q].x; }); + var opcoes = []; + opcoes.push({ k: 0, x: nos[ids[0]].x - cfg.folgaX / 2, larg: cfg.folgaX }); + for (var i = 0; i + 1 < ids.length; i++) { + var esq = nos[ids[i]].x + cfg.largura, dir = nos[ids[i + 1]].x; + opcoes.push({ k: i + 1, x: (esq + dir) / 2, larg: dir - esq }); + } + opcoes.push({ k: ids.length, x: nos[ids[ids.length - 1]].x + cfg.largura + cfg.folgaX / 2, larg: cfg.folgaX }); + var melhor = opcoes[0]; + opcoes.forEach(function (o) { if (Math.abs(o.x - xDesejado) < Math.abs(melhor.x - xDesejado)) melhor = o; }); + var chave = iLinha + ':' + melhor.k; + var u = usoCorredor[chave] = (usoCorredor[chave] || 0) + 1; + var passo = Math.min(7, (melhor.larg / 2 - 6) / 4); + var desloc = u === 1 ? 0 : (u % 2 ? -1 : 1) * Math.ceil((u - 1) / 2) * passo; + return melhor.x + desloc; + } + rotas.forEach(function (rota) { + var a = rota.a, b = rota.b; + var pts = [], folgaDoRotulo = []; // índices de segmentos que estão numa folga entre linhas + if (rota.tipo === 'mesma') { + var yb = faixas[a.linha][1] + 22 + (Math.abs(a.x - b.x) / (cfg.largura + cfg.folgaX)) * 6; + pts = [rota.pa, { x: rota.pa.x, y: yb }, { x: rota.pb.x, y: yb }, { x: rota.pb.x, y: rota.pb.y + 3 }]; + folgaDoRotulo = [1]; + } else { + var desce = rota.tipo === 'desce'; + var ini = rota.pa, fim = rota.pb; + pts.push({ x: ini.x, y: ini.y }); + var la = a.linha, lb = b.linha; + var passoL = desce ? 1 : -1; + var y0 = ini.y, y1 = fim.y; + for (var L = la + passoL; L !== lb; L += passoL) { + var topo = faixas[L][0], base = faixas[L][1]; + var yEntrada = desce ? topo - 6 : base + 6, ySaida = desce ? base + 6 : topo - 6; + var t = (yEntrada - y0) / ((y1 - y0) || 1); + var xd = ini.x + (fim.x - ini.x) * t; + var xc = corredor(L, xd); + pts.push({ x: xc, y: yEntrada }); + pts.push({ x: xc, y: ySaida }); + } + pts.push({ x: fim.x, y: desce ? fim.y - 3 : fim.y + 3 }); + for (var s = 0; s + 1 < pts.length; s += 2) folgaDoRotulo.push(s); + } + rota.pts = pts; + rota.folgas = folgaDoRotulo; + // folga (intervalo entre linhas) onde o rótulo prefere ficar: a da ponta de origem + if (rota.tipo === 'desce' || rota.tipo === 'mesma') rota.intervalo = a.linha; + else if (rota.tipo === 'sobe') rota.intervalo = a.linha - 1; + }); + return rotas; + } + + function medirRotulo(r) { + var linhasR = quebrarTexto(r.descricao, cfg.charsRotulo, cfg.linhasRotulo); + var tec = r.tecnologia ? quebrarTexto('[' + r.tecnologia + ']', cfg.charsRotulo + 4, 1) : []; + var maior = 0; + linhasR.concat(tec).forEach(function (s) { if (s.length > maior) maior = s.length; }); + return { linhas: linhasR, tec: tec, w: maior * 6.1 + 10, h: (linhasR.length + tec.length) * 13 + 6 }; + } + + // 1ª passada: folgas padrão, conta quantos rótulos caem em cada intervalo + var folgas = linhas.map(function () { return cfg.folgaY; }); + var geo = montar(folgas); + var rotas = rotear(geo); + var porIntervalo = {}; + rotas.forEach(function (rt) { + if (rt.intervalo === null || rt.intervalo < 0) return; + var m = medirRotulo(rt.r); + var p = porIntervalo[rt.intervalo] = porIntervalo[rt.intervalo] || { area: 0, n: 0 }; + p.area += (m.w + 8) * (m.h + 6); + p.n++; + }); + var mudou = false; + Object.keys(porIntervalo).forEach(function (k) { + // área livre de um intervalo ≈ largura útil × altura; deixa folga de 2,4× para posicionar sem colisão + var precisa = Math.ceil(2.4 * porIntervalo[k].area / (largura - 2 * cfg.margem)) + 36; + if (precisa > folgas[k]) { folgas[k] = Math.min(precisa, 520); mudou = true; } + }); + if (mudou) { geo = montar(folgas); rotas = rotear(geo); } + var nos = geo.nos; + var altura = geo.altura; + + // 8. fronteira do foco + var fronteira = null; + if (temFronteira) { + var x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity; + Object.keys(nos).forEach(function (id) { + var n = nos[id]; + if (n.grupo !== 'meio') return; + x0 = Math.min(x0, n.x); y0 = Math.min(y0, n.y - n.cabeca); + x1 = Math.max(x1, n.x + n.w); y1 = Math.max(y1, n.y + n.h); + }); + fronteira = { + x: x0 - cfg.fronteira, y: y0 - cfg.fronteira - cfg.tituloFronteira, + w: x1 - x0 + 2 * cfg.fronteira, h: y1 - y0 + 2 * cfg.fronteira + cfg.tituloFronteira, + titulo: prep.foco.nome, tipo: textoTipo(prep.foco) + }; + } + + // 9. rótulos: no segmento que está numa folga entre linhas, onde não haja caixa nem outro rótulo + var caixas = Object.keys(nos).map(function (id) { return retDe(nos[id]); }); + if (fronteira) caixas.push({ x: fronteira.x, y: fronteira.y, w: Math.min(fronteira.w, 40 + (fronteira.titulo.length + fronteira.tipo.length) * 7), h: cfg.tituloFronteira + 4 }); + var rotulos = []; + function sobreposicao(a, b) { + var w = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x); + var h = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y); + return w > 0 && h > 0 ? w * h : 0; + } + // os rótulos das rotas mais longas primeiro têm menos opções; ordena pelos de menos opções + var ordemRotulo = rotas.slice().sort(function (p, q) { return p.folgas.length - q.folgas.length; }); + ordemRotulo.forEach(function (rt) { + var m = medirRotulo(rt.r); + var fracoes = [0.5, 0.36, 0.64, 0.24, 0.76, 0.14, 0.86]; + var desvios = [0, -1, 1]; + var melhor = null, melhorCusto = Infinity; + rt.folgas.forEach(function (s, ordemSeg) { + var p1 = rt.pts[s], p2 = rt.pts[s + 1]; + fracoes.forEach(function (t, ordemT) { + desvios.forEach(function (d) { + var px = p1.x + (p2.x - p1.x) * t + d * (m.w / 2 + 6); + var py = p1.y + (p2.y - p1.y) * t; + var ret = { x: px - m.w / 2, y: py - m.h / 2, w: m.w, h: m.h }; + var custo = ordemSeg * 2 + ordemT * 0.5 + Math.abs(d) * 4; + caixas.forEach(function (k) { custo += sobreposicao(ret, { x: k.x - 3, y: k.y - 3, w: k.w + 6, h: k.h + 6 }) * 10; }); + rotulos.forEach(function (k) { custo += sobreposicao(ret, { x: k.x - 2, y: k.y - 2, w: k.w + 4, h: k.h + 4 }) * 3; }); + if (ret.x < 0 || ret.x + ret.w > largura) custo += 500; + if (custo < melhorCusto) { melhorCusto = custo; melhor = ret; } + }); + }); + }); + melhor.apertado = melhorCusto >= 40; + rotulos.push(melhor); + rt.rotulo = melhor; + rt.medida = m; + }); + var arestas = rotas.map(function (rt) { + return { + de: rt.r.de, para: rt.r.para, pontos: rt.pts, + x1: rt.pts[0].x, y1: rt.pts[0].y, x2: rt.pts[rt.pts.length - 1].x, y2: rt.pts[rt.pts.length - 1].y, + rotulo: rt.rotulo, linhas: rt.medida.linhas, tecnologia: rt.medida.tec, derivada: rt.r.derivada + }; + }); + + // 10. o rótulo pode passar da borda: aumenta a área + var minX = 0, minY = 0, maxX = largura, maxY = altura; + rotulos.forEach(function (k) { + minX = Math.min(minX, k.x - 4); minY = Math.min(minY, k.y - 4); + maxX = Math.max(maxX, k.x + k.w + 4); maxY = Math.max(maxY, k.y + k.h + 4); + }); + arestas.forEach(function (a) { + a.pontos.forEach(function (p) { + minX = Math.min(minX, p.x - 12); maxX = Math.max(maxX, p.x + 12); + minY = Math.min(minY, p.y - 12); maxY = Math.max(maxY, p.y + 12); + }); + }); + return { minX: minX, minY: minY, largura: maxX - minX, altura: maxY - minY, nos: nos, arestas: arestas, fronteira: fronteira, linhas: linhas }; + } + + // ───────────────────────── SVG ───────────────────────── + + function textoLinhas(linhas, x, y, passo, atributos) { + return linhas.map(function (l, i) { + return '<text x="' + x.toFixed(1) + '" y="' + (y + i * passo).toFixed(1) + '"' + atributos + '>' + esc(l) + '</text>'; + }).join(''); + } + + function desenharNo(n, e, cfg) { + var s = ''; + var rotuloAcessivel = e.nome + ', ' + (ROTULO_TIPO[e.tipo] || e.tipo) + (e.tecnologia ? ' (' + e.tecnologia + ')' : '') + '. ' + e.descricao; + s += '<g class="c4-no c4-' + esc(n.tipo) + '" data-c4-id="' + esc(n.id) + '" tabindex="0" role="button" aria-label="' + esc(rotuloAcessivel) + '">'; + s += '<title>' + esc(e.nome + ' — ' + e.descricao) + ''; + var x = n.x, y = n.y, w = n.w, h = n.h; + var forma = pinta(n.tipo); + if (n.tipo === 'pessoa') { + s += ''; + s += ''; + } else if (n.tipo === 'banco') { + var ry = 9; + s += ''; + s += ''; + y += 12; + } else if (n.tipo === 'fila') { + s += ''; + } else if (n.tipo === 'no-de-infra') { + s += ''; + s += ''; + } else { + s += ''; + } + var cx = x + w / 2; + var ty = y + 14 + 11; + s += textoLinhas(n.nome, cx, ty, cfg.linhaNome, ' text-anchor="middle" font-weight="700" font-size="13"' + letra(n.tipo)); + ty += n.nome.length * cfg.linhaNome + 2; + s += textoLinhas(n.linhaTipo, cx, ty, cfg.linhaTexto, ' text-anchor="middle" font-size="10.5" opacity=".92"' + letra(n.tipo)); + ty += n.linhaTipo.length * cfg.linhaTexto + 6; + s += textoLinhas(n.desc, cx, ty, cfg.linhaTexto, ' text-anchor="middle" font-size="11"' + letra(n.tipo)); + s += ''; + return s; + } + + /** Gera o SVG da vista (string). `idBase` torna únicos os ids internos (marcador, title, desc). */ + function gerarSvg(prep, lay, idBase, opcoes) { + var cfg = {}; + Object.keys(CFG).forEach(function (k) { cfg[k] = CFG[k]; }); + Object.keys(opcoes || {}).forEach(function (k) { cfg[k] = opcoes[k]; }); + idBase = idBase || 'c4'; + var v = prep.vista; + var nomes = prep.elementos.map(function (e) { return e.nome; }); + var desc = (NIVEIS[v.nivel] || v.nivel) + '. ' + prep.elementos.length + ' elementos e ' + prep.relacoes.length + + ' relações' + (prep.foco ? ', dentro de ' + prep.foco.nome : '') + ': ' + nomes.join(', ') + + '. A tabela logo abaixo do diagrama traz o mesmo conteúdo em texto.'; + var s = ''; + s += '' + esc(v.titulo) + ''; + s += '' + esc(desc) + ''; + s += '' + + ''; + if (lay.fronteira) { + var f = lay.fronteira; + s += ''; + s += '' + esc(f.titulo) + + ' ' + esc(f.tipo) + ''; + } + s += ''; + lay.arestas.forEach(function (a) { + var d = a.pontos.map(function (p, k) { return (k ? 'L' : 'M') + p.x.toFixed(1) + ',' + p.y.toFixed(1); }).join(' '); + s += ''; + }); + s += ''; + prep.elementos.forEach(function (e) { s += desenharNo(lay.nos[e.id], e, cfg); }); + s += ''; + lay.arestas.forEach(function (a) { + var r = a.rotulo; + s += ''; + var cx = r.x + r.w / 2; + s += textoLinhas(a.linhas, cx, r.y + 13, 13, ' text-anchor="middle" font-size="10.5"' + TEXTO); + s += textoLinhas(a.tecnologia, cx, r.y + 13 + a.linhas.length * 13, 13, ' text-anchor="middle" font-size="10" font-style="italic" opacity=".8"' + TEXTO); + s += ''; + }); + s += ''; + return s; + } + + // ───────────────────────── HTML em volta ───────────────────────── + + function linkDaFonte(f) { + // caminho do nova-atm: relativo a governanca/index.html; outro repositório (repo@branch:caminho): só texto + if (/^[A-Za-z0-9_.-]+@[^:]+:/.test(f) || /^https?:/i.test(f)) return '' + esc(f) + ''; + return '' + esc(f) + ''; + } + + function legendaHtml(prep) { + var tipos = {}; + prep.elementos.forEach(function (e) { tipos[e.tipo] = true; }); + if (prep.foco) tipos.__fronteira = true; + var itens = TIPOS.filter(function (t) { return tipos[t]; }).map(function (t) { + var forma = t === 'pessoa' ? '' + esc(ROTULO_TIPO[t]) + ''; + }); + if (tipos.__fronteira) itens.push('
  • Fronteira de ' + esc(prep.foco.nome) + '
  • '); + itens.push('
  • Relação (descrição e [tecnologia])
  • '); + if (prep.relacoes.some(function (r) { return r.derivada; })) { + itens.push('
  • Relação resumida (vem de partes internas)
  • '); + } + return '
      ' + itens.join('') + '
    '; + } + + function tabelaHtml(prep) { + var porId = prep.porId; + var s = '
    Tabela equivalente: ' + prep.elementos.length + ' elementos e ' + prep.relacoes.length + ' relações'; + s += ''; + prep.elementos.forEach(function (e) { + s += ''; + }); + s += '
    Elementos — ' + esc(prep.vista.titulo) + '
    ElementoTipoTecnologiaDescriçãoFontes
    ' + esc(e.nome) + '' + esc(ROTULO_TIPO[e.tipo] || e.tipo) + (prep.interno[e.id] && prep.foco ? ' (dentro de ' + esc(prep.foco.nome) + ')' : '') + '' + esc(e.tecnologia || '—') + '' + esc(e.descricao) + '' + + e.fontes.map(linkDaFonte).join('
    ') + '
    '; + s += ''; + prep.relacoes.forEach(function (r) { + var fontes = []; + r.origem.forEach(function (o) { o.fontes.forEach(function (f) { if (fontes.indexOf(f) < 0) fontes.push(f); }); }); + var descs = r.origem.map(function (o) { + var detalhe = (o.de !== r.de || o.para !== r.para) ? ' (' + (porId[o.de] || {}).nome + ' → ' + (porId[o.para] || {}).nome + ')' : ''; + return esc(o.descricao + detalhe); + }); + var techs = []; + r.origem.forEach(function (o) { if (o.tecnologia && techs.indexOf(o.tecnologia) < 0) techs.push(o.tecnologia); }); + s += ''; + }); + s += '
    Relações — ' + esc(prep.vista.titulo) + '
    DeParaDescriçãoTecnologiaFontes
    ' + esc(porId[r.de].nome) + '' + esc(porId[r.para].nome) + '' + descs.join('
    ') + '
    ' + esc(techs.join(', ') || '—') + '' + fontes.map(linkDaFonte).join('
    ') + '
    '; + return s; + } + + function detalheHtml(e, prep) { + var porId = prep.porId; + var s = ''; + s += '

    ' + esc(e.nome) + ' ' + esc(textoTipo(e)) + '

    '; + s += '

    ' + esc(e.descricao) + '

    '; + if (e.pai && porId[e.pai]) s += '

    Dentro de: ' + esc(porId[e.pai].nome) + '

    '; + var rel = prep.relacoes.filter(function (r) { return r.de === e.id || r.para === e.id; }); + if (rel.length) { + s += '

    Relações nesta vista

      '; + rel.forEach(function (r) { + s += '
    • ' + (r.de === e.id ? '→ ' + esc(porId[r.para].nome) : '← ' + esc(porId[r.de].nome)) + ': ' + esc(r.descricao) + (r.tecnologia ? ' [' + esc(r.tecnologia) + ']' : '') + '
    • '; + }); + s += '
    '; + } + s += '

    Fontes

      ' + e.fontes.map(function (f) { return '
    • ' + linkDaFonte(f) + '
    • '; }).join('') + '
    '; + return s; + } + + var ESTILO = [ + Object.keys(COR).map(function (t) { + var txt = t === 'componente' ? 'var(--c4-texto-componente, ' + COR[t][1] + ')' : 'var(--c4-texto-caixa, ' + COR[t][1] + ')'; + return '.c4-f-' + t + '{fill:var(' + VAR_TIPO[t] + ', ' + COR[t][0] + ');}.c4-t-' + t + '{fill:' + txt + ';}'; + }).join('\n'), + '.c4-borda,.c4-linha{stroke:var(--c4-linha,var(--mudo,#5C6870));}.c4-seta-ponta{fill:var(--c4-linha,var(--mudo,#5C6870));}', + '.c4-txt{fill:var(--c4-texto,var(--texto,#1B2A33));}.c4-fundo-rotulo{fill:var(--c4-fundo,var(--superficie,#FFFFFF));}', + 'figure[data-c4]{margin:1.5rem 0;}', + '.c4-titulo{font-weight:700;margin:0 0 .5rem;color:var(--titulo,inherit);}', + '.c4-titulo span{font-weight:400;color:var(--mudo,#5C6870);font-size:.85em;margin-left:.4rem;}', + '.c4-palco{overflow:auto;border:1px solid var(--linha,#E7E5E1);border-radius:var(--raio,10px);background:var(--c4-fundo,var(--superficie,#FFF));}', + '.c4-palco[data-tamanho="ajustado"] .c4-svg{width:100%;height:auto;}', + '.c4-palco .c4-svg{display:block;}', + '.c4-no{cursor:pointer;outline:none;}', + '.c4-no:focus-visible > rect,.c4-no:focus-visible > path,.c4-no:focus-visible > circle,.c4-no[data-c4-ativo] > rect,.c4-no[data-c4-ativo] > path{stroke:var(--foco,#2563EB);stroke-width:3;}', + '.c4-barra{display:flex;flex-wrap:wrap;gap:.5rem 1rem;align-items:center;margin:.5rem 0;}', + '.c4-barra button,.c4-fechar{font:inherit;font-size:.85rem;padding:.2rem .6rem;border:1px solid var(--linha-forte,#D6D2CB);border-radius:6px;background:var(--superficie,#FFF);color:var(--texto,#1B2A33);cursor:pointer;}', + '.c4-dica{font-size:.85rem;color:var(--mudo,#5C6870);}', + '.c4-legenda{display:flex;flex-wrap:wrap;gap:.25rem 1rem;list-style:none;padding:0;margin:.25rem 0 .5rem;font-size:.85rem;color:var(--texto,#1B2A33);}', + '.c4-legenda li{display:flex;align-items:center;gap:.35rem;}', + '.c4-detalhe{position:relative;border:1px solid var(--linha-forte,#D6D2CB);border-left:4px solid var(--c4-container,#438DD5);border-radius:6px;padding:.6rem 2.5rem .6rem .9rem;margin:.5rem 0;background:var(--superficie,#FFF);font-size:.92rem;}', + '.c4-detalhe p{margin:.25rem 0;}.c4-detalhe ul{margin:.2rem 0 .4rem 1.1rem;padding:0;}', + '.c4-detalhe-titulo span{color:var(--mudo,#5C6870);font-size:.85em;}', + '.c4-detalhe-sub{font-weight:600;margin-top:.5rem !important;}', + '.c4-fechar{position:absolute;top:.4rem;right:.4rem;line-height:1;}', + '.c4-tabela{margin:.5rem 0;font-size:.88rem;}', + '.c4-tabela summary{cursor:pointer;color:var(--link,#A24C08);}', + '.c4-tabela table{border-collapse:collapse;width:100%;margin:.5rem 0;}', + '.c4-tabela caption{text-align:left;font-weight:600;padding:.25rem 0;}', + '.c4-tabela th,.c4-tabela td{border:1px solid var(--linha,#E7E5E1);padding:.3rem .45rem;vertical-align:top;text-align:left;}', + '.c4-tabela code{font-size:.85em;word-break:break-all;}', + '.c4-erro{border:1px dashed var(--linha-forte,#D6D2CB);border-radius:6px;padding:.75rem 1rem;color:var(--mudo,#5C6870);}', + '@media print{.c4-barra,.c4-detalhe,.c4-fechar{display:none !important;}.c4-palco{overflow:visible;border:0;}.c4-palco .c4-svg{width:100% !important;height:auto !important;}figure[data-c4]{break-inside:avoid-page;}.c4-tabela summary{display:none;}}' + ].join('\n'); + + function injetarEstilo(doc) { + if (!doc || !doc.head || !doc.createElement) return; + if (doc.getElementById && doc.getElementById('atm-c4-estilo')) return; + var st = doc.createElement('style'); + st.id = 'atm-c4-estilo'; + st.textContent = ESTILO; + doc.head.appendChild(st); + } + + var contador = 0; + var impressaoLigada = false; + + function ligarImpressao() { + if (impressaoLigada || typeof global.addEventListener !== 'function' || !global.document) return; + impressaoLigada = true; + var abertas = []; + global.addEventListener('beforeprint', function () { + abertas = []; + var ds = global.document.querySelectorAll('details.c4-tabela'); + for (var i = 0; i < ds.length; i++) if (!ds[i].open) { ds[i].open = true; abertas.push(ds[i]); } + }); + global.addEventListener('afterprint', function () { + abertas.forEach(function (d) { d.open = false; }); + abertas = []; + }); + } + + function acharNo(alvo, fig) { + var n = alvo; + while (n && n !== fig) { + if (n.getAttribute && n.getAttribute('data-c4-id')) return n; + if (n.getAttribute && n.getAttribute('data-c4-fechar') !== null && n.getAttribute('data-c4-fechar') !== undefined) return n; + n = n.parentNode; + } + return null; + } + + function ouvir(fig) { + if (fig.__c4Ouvindo || typeof fig.addEventListener !== 'function') return; + fig.__c4Ouvindo = true; + function abrir(no) { + var estado = fig.__c4; + if (!estado) return; + var painel = fig.querySelector('.c4-detalhe'); + if (!painel) return; + if (no.getAttribute('data-c4-fechar') !== null && !no.getAttribute('data-c4-id')) { + painel.hidden = true; + painel.innerHTML = ''; + var ativo = fig.querySelector('[data-c4-ativo]'); + if (ativo) { ativo.removeAttribute('data-c4-ativo'); if (ativo.focus) ativo.focus(); } + return; + } + var id = no.getAttribute('data-c4-id'); + var e = estado.prep.porId[id]; + if (!e) return; + var antes = fig.querySelectorAll('[data-c4-ativo]'); + for (var i = 0; i < antes.length; i++) antes[i].removeAttribute('data-c4-ativo'); + no.setAttribute('data-c4-ativo', ''); + painel.innerHTML = detalheHtml(e, estado.prep); + painel.hidden = false; + } + fig.addEventListener('click', function (ev) { + var alvo = ev.target; + if (alvo && alvo.getAttribute && alvo.getAttribute('data-c4-tamanho') !== null && alvo.getAttribute('data-c4-tamanho') !== undefined) { + var palco = fig.querySelector('.c4-palco'); + var real = palco.getAttribute('data-tamanho') === 'ajustado'; + palco.setAttribute('data-tamanho', real ? 'real' : 'ajustado'); + alvo.textContent = real ? 'Ajustar à largura' : 'Tamanho real'; + alvo.setAttribute('aria-pressed', real ? 'true' : 'false'); + return; + } + var no = acharNo(alvo, fig); + if (no) abrir(no); + }); + fig.addEventListener('keydown', function (ev) { + if (ev.key !== 'Enter' && ev.key !== ' ') return; + var no = acharNo(ev.target, fig); + if (no && no.getAttribute('data-c4-id')) { if (ev.preventDefault) ev.preventDefault(); abrir(no); } + }); + } + + /** Renderiza a vista de uma figura. Devolve true se desenhou. */ + function desenharFigura(fig, modelo, opcoes) { + var vistaId = String(fig.getAttribute('data-c4') || '').trim(); + if (!modelo || !Array.isArray(modelo.vistas)) { + fig.innerHTML = '

    Modelo C4 não carregado: não foi possível desenhar a vista "' + esc(vistaId) + '".

    '; + fig.setAttribute('data-c4-estado', 'erro'); + return false; + } + var prep = prepararVista(modelo, vistaId); + if (prep.erro) { + fig.innerHTML = '

    ' + esc(prep.erro) + ' Vistas disponíveis: ' + esc(modelo.vistas.map(function (v) { return v.id; }).join(', ')) + '.

    '; + fig.setAttribute('data-c4-estado', 'erro'); + return false; + } + var lay = calcularLayout(prep, opcoes); + contador += 1; + var idBase = 'c4-' + vistaId.replace(/[^a-z0-9-]/gi, '') + '-' + contador; + // ajusta à largura quando o texto ainda fica legível (escala >= 62%); senão, tamanho real com rolagem + var disponivel = fig.clientWidth || 0; + var ajustado = disponivel ? disponivel >= lay.largura * 0.62 : lay.largura <= CFG.larguraAjustavel; + var html = ''; + html += '
    ' + esc(prep.vista.titulo) + 'C4 · ' + esc(NIVEIS[prep.vista.nivel] || prep.vista.nivel) + '
    '; + html += '
    ' + + 'Clique (ou Enter) numa caixa para ver a descrição e as fontes.
    '; + html += '
    ' + gerarSvg(prep, lay, idBase, opcoes) + '
    '; + html += legendaHtml(prep); + html += ''; + html += tabelaHtml(prep); + fig.innerHTML = html; + fig.setAttribute('data-c4-estado', 'ok'); + fig.__c4 = { prep: prep, layout: lay }; + ouvir(fig); + return true; + } + + /** Acha `figure[data-c4]` em `raiz` e desenha cada vista. Devolve quantas desenhou. */ + function desenhar(raiz, modelo, opcoes) { + raiz = raiz || global.document; + if (!raiz || typeof raiz.querySelectorAll !== 'function') return 0; + injetarEstilo(raiz.ownerDocument || global.document); + ligarImpressao(); + var figs = raiz.querySelectorAll('figure[data-c4]'); + var n = 0; + for (var i = 0; i < figs.length; i++) if (desenharFigura(figs[i], modelo, opcoes)) n++; + return n; + } + + var api = { + desenhar: desenhar, + prepararVista: prepararVista, + calcularLayout: calcularLayout, + gerarSvg: gerarSvg, + tabelaHtml: tabelaHtml, + quebrarTexto: quebrarTexto, + validarModelo: validarModelo, + ESTILO: ESTILO, // folha do diagrama; sob CSP sem estilo inline, sirva-a como .css com id="atm-c4-estilo" (aí não é injetada) + TIPOS: TIPOS.slice() + }; + global.ATM_C4 = api; + if (typeof module !== 'undefined' && module.exports) module.exports = api; + })(typeof window !== 'undefined' ? window : globalThis); + + painel-markdown.js: | + /* =========================================================================== + markdown.js — renderizador Markdown do visualizador de governança. + + Sem dependências. Funciona no navegador (window.ATMMarkdown) e no Node + (module.exports), para os testes em markdown.teste.js. + + SEGURANÇA + Todo texto do conteúdo é escapado. HTML cru escrito no Markdown NÃO é + interpretado: aparece como texto. As únicas exceções são
    (comum em + célula de tabela), que vira quebra de linha, e comentários , que + são descartados. Links e imagens só aceitam http, https, mailto, âncora e + caminho relativo; qualquer outro esquema (javascript:, data:, ...) vira "#". + + SUPORTE + Títulos ATX (#) e setext (=== / ---), parágrafos, ênfase (* _ ** __ ~~), + código inline e em bloco (``` ~~~ e recuo de 4 espaços), listas ordenadas + e não ordenadas aninhadas (inclusive de tarefa: - [ ] / - [x]), tabelas + GFM com alinhamento, citações aninhadas, links inline, de referência e + automáticos ( e URL solta), imagens, regras horizontais e + quebras de linha (dois espaços ou barra invertida no fim da linha). + Bloco cercado ```c4 vira
    , + que o c4.js desenha a partir do modelo (ATM_GOV.c4). + + CÓPIA NO PAINEL DO DONO + frontend/publico/painel-markdown.js é cópia byte a byte deste arquivo + (`python governanca/gerar.py --copiar-para-o-painel`); o gerar.py avisa + quando as duas divergem. + + USO + ATMMarkdown.render(md, { + resolverLink: function (href) { return hrefNovo ou null }, + resolverImagem: function (src) { return srcNovo ou null }, + prefixoId: '', // prefixo dos ids dos títulos + alinhamentoPorClasse: false // true: alinhamento de tabela por classe, sem style= + }) + -> { html: '...', titulos: [{ nivel, texto, id }] } + =========================================================================== */ + (function (raiz, fabrica) { + var api = fabrica(); + if (typeof module === 'object' && module.exports) module.exports = api; + else raiz.ATMMarkdown = api; + })(typeof self !== 'undefined' ? self : this, function () { + 'use strict'; + + /* ---------------------------------------------------------------- util */ + + function escapar(s) { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + var PONTUACAO = /[!-\/:-@\[-`{-~¡-¿‐-‧‰-⁞]/; + var ESPACO = /\s/; + + function ehBranca(linha) { return /^\s*$/.test(linha); } + + function recuo(linha) { + var m = /^ */.exec(linha); + return m[0].length; + } + + /* Slug no estilo do GitHub: minúsculas, mantém letras acentuadas, troca + espaço por hífen e descarta o resto da pontuação. Assim uma âncora + escrita à mão pelo autor ("#visão-geral") bate com o id gerado. */ + function slug(texto) { + return String(texto) + .trim() + .toLowerCase() + .replace(/[^\p{L}\p{N}\p{M}_\- ]/gu, '') + .replace(/ /g, '-'); + } + + function semAcento(s) { + return String(s).normalize('NFD').replace(/[̀-ͯ]/g, ''); + } + + var ESQUEMA = /^([a-zA-Z][a-zA-Z0-9+.\-]*):/; + + /* Devolve um href seguro, ou "#" se o esquema não for permitido. */ + function urlSegura(url) { + var u = String(url).trim(); + // Remove caracteres de controle e espaços que navegadores ignoram dentro + // do esquema ("java\tscript:"). + var limpo = u.replace(/[\u0000- \u007F]/g, ''); + var m = ESQUEMA.exec(limpo); + if (m) { + var esq = m[1].toLowerCase(); + if (esq !== 'http' && esq !== 'https' && esq !== 'mailto') return '#'; + } + return u; + } + + function textoPuro(html) { + return html + .replace(/<[^>]*>/g, '') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/&/g, '&'); + } + + /* -------------------------------------------------------------- inline */ + + /* O texto vira uma lista de nós: texto cru, HTML já pronto (código, link) + e delimitadores de ênfase. A ênfase é resolvida depois, numa versão + simplificada do algoritmo do CommonMark ("process emphasis"). */ + + function Inline(ctx) { this.ctx = ctx; } + + Inline.prototype.render = function (texto) { + var nos = this.tokenizar(texto); + this.enfase(nos); + return this.serializar(nos); + }; + + Inline.prototype.tokenizar = function (s) { + var nos = []; + var buf = ''; + var i = 0; + var n = s.length; + var self = this; + + function despejar() { + if (buf) { nos.push({ t: 'txt', v: buf }); buf = ''; } + } + function html(v) { despejar(); nos.push({ t: 'html', v: v }); } + + while (i < n) { + var c = s.charAt(i); + + // Escape com barra invertida. + if (c === '\\') { + var prox = s.charAt(i + 1); + if (prox === '\n') { html('
    \n'); i += 2; continue; } + if (prox && PONTUACAO.test(prox) && prox.charCodeAt(0) < 128) { + buf += prox; i += 2; continue; + } + buf += c; i++; continue; + } + + // Código inline. + if (c === '`') { + var ini = i; + while (i < n && s.charAt(i) === '`') i++; + var marca = s.slice(ini, i); + var fim = acharCrases(s, i, marca.length); + if (fim < 0) { buf += marca; continue; } + var cod = s.slice(i, fim).replace(/\n/g, ' '); + if (/^ .*[^ ].* $/.test(cod) || /^ [^ ] $/.test(cod)) cod = cod.slice(1, -1); + html('' + escapar(cod) + ''); + i = fim + marca.length; + continue; + } + + // Quebra dura: dois ou mais espaços antes da quebra de linha. + if (c === '\n') { + var mb = / {2,}$/.exec(buf); + if (mb) { buf = buf.slice(0, mb.index); html('
    \n'); } + else { buf = buf.replace(/ +$/, ''); buf += '\n'; } + i++; + while (s.charAt(i) === ' ') i++; + continue; + } + + // Comentário HTML: descartado. + if (c === '<' && s.substr(i, 4) === '', i + 4); + if (fc >= 0) { i = fc + 3; continue; } + } + + //
    permitido. + if (c === '<') { + var mbr = /^/i.exec(s.slice(i)); + if (mbr) { html('
    '); i += mbr[0].length; continue; } + var mal = /^<((?:https?|mailto):[^\s<>]*)>/i.exec(s.slice(i)); + if (mal) { html(self.link(mal[1], escapar(mal[1]), '')); i += mal[0].length; continue; } + var mem = /^<([A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,})>/.exec(s.slice(i)); + if (mem) { html(self.link('mailto:' + mem[1], escapar(mem[1]), '')); i += mem[0].length; continue; } + } + + // Imagem e link. + if (c === '!' && s.charAt(i + 1) === '[') { + var img = self.lerLink(s, i + 1); + if (img) { + html(self.imagem(img.dest, textoPuro(self.render(img.texto)), img.titulo)); + i = img.fim; continue; + } + } + // Link dentro de link não existe em HTML: o "[" interno fica como texto. + if (c === '[' && !self.ctx.dentroDeLink) { + var lk = self.lerLink(s, i); + if (lk) { + self.ctx.dentroDeLink = true; + var dentro = self.render(lk.texto); + self.ctx.dentroDeLink = false; + html(self.link(lk.dest, dentro, lk.titulo)); + i = lk.fim; continue; + } + } + + // URL solta (extensão GFM). + if ((c === 'h' || c === 'H') && !self.ctx.dentroDeLink) { + var antes = i > 0 ? s.charAt(i - 1) : ' '; + if (!/[A-Za-z0-9]/.test(antes)) { + var mu = /^https?:\/\/[^\s<]+/i.exec(s.slice(i)); + if (mu) { + var url = aparaUrl(mu[0]); + html(self.link(url, escapar(url), '')); + i += url.length; continue; + } + } + } + + // Delimitadores de ênfase. + if (c === '*' || c === '_' || c === '~') { + var j = i; + while (j < n && s.charAt(j) === c) j++; + var qtd = j - i; + if (c === '~' && qtd !== 2) { buf += s.slice(i, j); i = j; continue; } + var ant = i > 0 ? s.charAt(i - 1) : ' '; + var dep = j < n ? s.charAt(j) : ' '; + var antEsp = ESPACO.test(ant), depEsp = ESPACO.test(dep); + var antPont = PONTUACAO.test(ant), depPont = PONTUACAO.test(dep); + var esq = !depEsp && (!depPont || antEsp || antPont); + var dir = !antEsp && (!antPont || depEsp || depPont); + var abre, fecha; + if (c === '_') { + abre = esq && (!dir || antPont); + fecha = dir && (!esq || depPont); + } else { + abre = esq; fecha = dir; + } + despejar(); + nos.push({ t: 'del', c: c, n: qtd, orig: qtd, abre: abre, fecha: fecha }); + i = j; + continue; + } + + buf += c; + i++; + } + despejar(); + return nos; + }; + + function acharCrases(s, de, tam) { + var i = de; + while (i < s.length) { + var p = s.indexOf('`', i); + if (p < 0) return -1; + var j = p; + while (j < s.length && s.charAt(j) === '`') j++; + if (j - p === tam) return p; + i = j; + } + return -1; + } + + function aparaUrl(url) { + // Pontuação final não faz parte da URL; ")" só fica se estiver casado. + for (;;) { + var ult = url.charAt(url.length - 1); + if (/[.,:;!?*_~'"]/.test(ult)) { url = url.slice(0, -1); continue; } + if (ult === ')') { + var abre = (url.match(/\(/g) || []).length; + var fecha = (url.match(/\)/g) || []).length; + if (fecha > abre) { url = url.slice(0, -1); continue; } + } + return url; + } + } + + /* Lê [texto](destino "título") ou [texto][ref] / [texto][] / [texto] + a partir de s[i] === '['. Devolve null se não for link. */ + Inline.prototype.lerLink = function (s, i) { + var n = s.length; + var prof = 0; + var j = i; + for (; j < n; j++) { + var c = s.charAt(j); + if (c === '\\') { j++; continue; } + if (c === '`') { + var k = j; while (k < n && s.charAt(k) === '`') k++; + var f = acharCrases(s, k, k - j); + if (f >= 0) { j = f + (k - j) - 1; continue; } + j = k - 1; continue; + } + if (c === '[') prof++; + else if (c === ']') { prof--; if (prof === 0) break; } + } + if (j >= n) return null; + var texto = s.slice(i + 1, j); + var p = j + 1; + + if (s.charAt(p) === '(') { + var q = p + 1; + while (q < n && /[ \t\n]/.test(s.charAt(q))) q++; + var dest = ''; + if (s.charAt(q) === '<') { + var fimA = s.indexOf('>', q); + if (fimA < 0) return null; + dest = s.slice(q + 1, fimA); + q = fimA + 1; + } else { + var par = 0; + var d0 = q; + while (q < n) { + var ch = s.charAt(q); + if (ch === '\\') { q += 2; continue; } + if (/\s/.test(ch)) break; + if (ch === '(') par++; + else if (ch === ')') { if (par === 0) break; par--; } + q++; + } + dest = s.slice(d0, q); + } + while (q < n && /[ \t\n]/.test(s.charAt(q))) q++; + var titulo = ''; + var ab = s.charAt(q); + if (ab === '"' || ab === "'" || ab === '(') { + var fe = ab === '(' ? ')' : ab; + var ft = s.indexOf(fe, q + 1); + if (ft < 0) return null; + titulo = s.slice(q + 1, ft); + q = ft + 1; + while (q < n && /[ \t\n]/.test(s.charAt(q))) q++; + } + if (s.charAt(q) !== ')') return null; + return { texto: texto, dest: dest.replace(/\\([!-\/:-@\[-`{-~])/g, '$1'), titulo: titulo, fim: q + 1 }; + } + + // Referência. + var refs = this.ctx.refs; + var rotulo = null; + var fim = p; + if (s.charAt(p) === '[') { + var fr = s.indexOf(']', p + 1); + if (fr >= 0) { + rotulo = s.slice(p + 1, fr) || texto; + fim = fr + 1; + } + } + if (rotulo === null) rotulo = texto; + var def = refs[normRotulo(rotulo)]; + if (!def) return null; + return { texto: texto, dest: def.dest, titulo: def.titulo, fim: fim }; + }; + + function normRotulo(r) { return String(r).trim().replace(/\s+/g, ' ').toLowerCase(); } + + Inline.prototype.link = function (dest, htmlTexto, titulo) { + var href = dest; + var externo = /^(https?:)?\/\//i.test(dest); + if (this.ctx.resolverLink && !externo && !/^mailto:/i.test(dest)) { + var r = this.ctx.resolverLink(dest); + if (r) href = r; + } + href = urlSegura(href); + var attrs = ' href="' + escapar(href) + '"'; + if (titulo) attrs += ' title="' + escapar(titulo) + '"'; + if (externo) attrs += ' class="externo" target="_blank" rel="noopener noreferrer"'; + return '' + htmlTexto + ''; + }; + + Inline.prototype.imagem = function (src, alt, titulo) { + var s = src; + if (this.ctx.resolverImagem && !/^(https?:)?\/\//i.test(src)) { + var r = this.ctx.resolverImagem(src); + if (r) s = r; + } + s = urlSegura(s); + if (/^mailto:/i.test(s)) s = '#'; + var attrs = ' src="' + escapar(s) + '" alt="' + escapar(alt) + '" loading="lazy"'; + if (titulo) attrs += ' title="' + escapar(titulo) + '"'; + return ''; + }; + + Inline.prototype.enfase = function (nos) { + var k = 0; + while (k < nos.length) { + var fe = nos[k]; + if (fe.t !== 'del' || !fe.fecha || fe.n === 0) { k++; continue; } + // Procura um abridor compatível para trás. + var a = k - 1; + var achou = -1; + for (; a >= 0; a--) { + var ab = nos[a]; + if (ab.t !== 'del' || ab.c !== fe.c || !ab.abre || ab.n === 0) continue; + if (fe.c === '~' && ab.n !== fe.n) continue; + // Regra do múltiplo de 3 (CommonMark). + if ((ab.fecha || fe.abre) && (ab.orig + fe.orig) % 3 === 0 && + !(ab.orig % 3 === 0 && fe.orig % 3 === 0)) continue; + achou = a; break; + } + if (achou < 0) { + if (!fe.abre) fe.fecha = false; // não fecha nada; vira texto + k++; + continue; + } + var abr = nos[achou]; + var usa = fe.c === '~' ? 2 : (abr.n >= 2 && fe.n >= 2 ? 2 : 1); + var tag = fe.c === '~' ? 'del' : (usa === 2 ? 'strong' : 'em'); + var meio = nos.slice(achou + 1, k); + var htmlMeio = this.serializar(meio); + abr.n -= usa; + fe.n -= usa; + var novo = { t: 'html', v: '<' + tag + '>' + htmlMeio + '' }; + nos.splice(achou + 1, k - achou - 1, novo); + k = achou + 2; // posição do fechador + if (abr.n === 0) { nos.splice(achou, 1); k--; } + if (fe.n === 0) { nos.splice(k, 1); } + } + }; + + Inline.prototype.serializar = function (nos) { + var out = ''; + for (var i = 0; i < nos.length; i++) { + var no = nos[i]; + if (no.t === 'txt') out += escapar(no.v); + else if (no.t === 'html') out += no.v; + else if (no.t === 'del') out += escapar(new Array(no.n + 1).join(no.c)); + } + return out; + }; + + /* -------------------------------------------------------------- blocos */ + + var RX = { + cerca: /^( {0,3})(`{3,}|~{3,})[ \t]*([^\s`]*)[^`]*$/, + cercaTil: /^( {0,3})(~{3,})[ \t]*(\S*).*$/, + atx: /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/, + hr: /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/, + citacao: /^ {0,3}> ?/, + setext1: /^ {0,3}=+[ \t]*$/, + setext2: /^ {0,3}-+[ \t]*$/, + comentario: /^ {0,3}') < 0) i++; + i++; + continue; + } + + // Título ATX. + var mh = RX.atx.exec(l); + if (mh) { + out.push({ t: 'titulo', nivel: mh[1].length, texto: (mh[2] || '').trim(), ini: ini, fim: i + 1 }); + i++; + continue; + } + + // Regra horizontal. + if (RX.hr.test(l)) { + out.push({ t: 'hr', ini: ini, fim: i + 1 }); + i++; + continue; + } + + // Citação. + if (RX.citacao.test(l)) { + var cit = []; + while (i < n) { + var lc = linhas[i]; + if (RX.citacao.test(lc)) { cit.push(lc.replace(RX.citacao, '')); i++; continue; } + // Continuação preguiçosa de parágrafo. + if (!ehBranca(lc) && cit.length && !ehBranca(cit[cit.length - 1]) && !interrompe(linhas, i)) { + cit.push(lc); i++; continue; + } + break; + } + out.push({ t: 'citacao', filhos: this.blocos(cit), ini: ini, fim: i }); + continue; + } + + // Lista. + var mk = marcador(l); + if (mk) { + var lista = this.lista(linhas, i); + out.push(lista.bloco); + i = lista.prox; + continue; + } + + // Código por recuo. + if (recuo(l) >= 4) { + var cod = []; + while (i < n && (recuo(linhas[i]) >= 4 || ehBranca(linhas[i]))) { + cod.push(linhas[i].slice(Math.min(4, recuo(linhas[i])))); + i++; + } + while (cod.length && ehBranca(cod[cod.length - 1])) { cod.pop(); } + // Não consome as linhas em branco do fim. + i = ini + cod.length; + out.push({ t: 'codigo', lang: '', texto: cod.join('\n'), ini: ini, fim: i }); + continue; + } + + // Tabela. + if (ehInicioTabela(linhas, i)) { + var cab = celulas(linhas[i]); + var alin = celulas(linhas[i + 1]).map(function (d) { + var e = d.charAt(0) === ':', r = d.charAt(d.length - 1) === ':'; + return e && r ? 'center' : r ? 'right' : e ? 'left' : ''; + }); + i += 2; + var linhasT = []; + while (i < n && !ehBranca(linhas[i]) && linhas[i].indexOf('|') >= 0 && + !abreCerca(linhas[i]) && !RX.citacao.test(linhas[i])) { + var cs = celulas(linhas[i]); + while (cs.length < cab.length) cs.push(''); + linhasT.push(cs.slice(0, cab.length)); + i++; + } + out.push({ t: 'tabela', cab: cab, alin: alin, linhas: linhasT, ini: ini, fim: i }); + continue; + } + + // Parágrafo (ou título setext). + var par = [l.replace(/^\s+/, '')]; + i++; + var setext = 0; + while (i < n && !ehBranca(linhas[i])) { + if (RX.setext1.test(linhas[i])) { setext = 1; i++; break; } + if (RX.setext2.test(linhas[i])) { setext = 2; i++; break; } + if (interrompe(linhas, i)) break; + par.push(linhas[i].replace(/^\s+/, '')); + i++; + } + // Espaços no fim do parágrafo não viram quebra de linha. + var txt = par.join('\n').replace(/\s+$/, ''); + if (setext) out.push({ t: 'titulo', nivel: setext, texto: txt.replace(/\n/g, ' '), ini: ini, fim: i }); + else out.push({ t: 'par', texto: txt, ini: ini, fim: i }); + } + return out; + }; + + Analisador.prototype.lista = function (linhas, i) { + var n = linhas.length; + var primeiro = marcador(linhas[i]); + var itens = []; + var solta = false; + var ini = i; + + while (i < n) { + var mk = marcador(linhas[i]); + if (!mk || mk.ordenada !== primeiro.ordenada || mk.sinal !== primeiro.sinal) break; + if (RX.hr.test(linhas[i])) break; + var larg = mk.largura; + var conteudo = [mk.conteudo]; + i++; + while (i < n) { + var l = linhas[i]; + if (ehBranca(l)) { conteudo.push(''); i++; continue; } + var r = recuo(l); + if (r >= larg) { conteudo.push(l.slice(larg)); i++; continue; } + var anteriorBranca = ehBranca(conteudo[conteudo.length - 1]); + // Tolerância: sublista recuada menos que o exigido (ex.: 2 espaços + // sob "1."), comum em texto escrito à mão. + var sub = marcador(l); + if (sub && r > mk.recuo) { larg = r; conteudo.push(l.slice(r)); i++; continue; } + if (anteriorBranca) break; + if (!interrompe(linhas, i) && !sub) { conteudo.push(l); i++; continue; } + break; + } + // Linhas em branco no fim do item. + var brancas = 0; + while (conteudo.length > 1 && ehBranca(conteudo[conteudo.length - 1])) { conteudo.pop(); brancas++; } + var proxMk = i < n ? marcador(linhas[i]) : null; + var continua = proxMk && proxMk.ordenada === primeiro.ordenada && proxMk.sinal === primeiro.sinal && !RX.hr.test(linhas[i]); + if (brancas && continua) solta = true; + if (!continua) i -= brancas; + + // Item de tarefa. + var tarefa = null; + var mt = /^\[([ xX])\][ \t]+/.exec(conteudo[0]); + if (mt) { tarefa = mt[1] !== ' '; conteudo[0] = conteudo[0].slice(mt[0].length); } + + var filhos = this.blocos(conteudo); + for (var k = 1; k < filhos.length; k++) { + if (filhos[k].ini > filhos[k - 1].fim) solta = true; + } + itens.push({ filhos: filhos, tarefa: tarefa }); + if (!continua) break; + } + return { + bloco: { t: 'lista', ordenada: primeiro.ordenada, inicio: primeiro.inicio, itens: itens, solta: solta, ini: ini, fim: i }, + prox: i + }; + }; + + /* ------------------------------------------------------------ geração */ + + function Gerador(ctx) { + this.ctx = ctx; + this.inline = new Inline(ctx); + } + + Gerador.prototype.blocos = function (blocos, justa) { + var out = ''; + for (var i = 0; i < blocos.length; i++) out += this.bloco(blocos[i], justa); + return out; + }; + + Gerador.prototype.bloco = function (b, justa) { + var ctx = this.ctx; + switch (b.t) { + case 'par': + var h = this.inline.render(b.texto); + return justa ? h + '\n' : '

    ' + h + '

    \n'; + case 'titulo': + var conteudo = this.inline.render(b.texto); + var texto = textoPuro(conteudo).trim(); + var base = slug(texto) || 'secao'; + var id = base; + if (ctx.slugs[base] !== undefined) { + ctx.slugs[base]++; + id = base + '-' + ctx.slugs[base]; + } else { + ctx.slugs[base] = 0; + } + var idFinal = ctx.prefixoId + id; + ctx.titulos.push({ nivel: b.nivel, texto: texto, id: idFinal, ancora: id }); + return '' + conteudo + '\n'; + case 'hr': + return '
    \n'; + case 'codigo': + // ```c4 -> lugar de um diagrama C4, desenhado depois por + // c4.js (window.ATM_C4.desenhar). O id só aceita [a-z0-9-]; sem id + // válido, o bloco continua sendo código comum. + if (String(b.lang || '').toLowerCase() === 'c4') { + var vista = String(b.resto || '').toLowerCase().replace(/[^a-z0-9-]/g, ''); + if (vista) { + return '
    Diagrama C4: ' + vista + + '
    \n'; + } + } + var lang = String(b.lang || '').replace(/[^\w+#.\-]/g, ''); + var cls = lang ? ' class="language-' + escapar(lang) + '"' : ''; + var dl = lang ? ' data-lang="' + escapar(lang) + '"' : ''; + return '' + escapar(b.texto) + (b.texto ? '\n' : '') + '\n'; + case 'citacao': + return '
    \n' + this.blocos(b.filhos, false) + '
    \n'; + case 'lista': + var tag = b.ordenada ? 'ol' : 'ul'; + var attrs = b.ordenada && b.inicio !== 1 ? ' start="' + b.inicio + '"' : ''; + var temTarefa = b.itens.some(function (it) { return it.tarefa !== null; }); + if (temTarefa) attrs += ' class="tarefas"'; + var out = '<' + tag + attrs + '>\n'; + for (var i = 0; i < b.itens.length; i++) { + var it = b.itens[i]; + var caixa = ''; + if (it.tarefa !== null) { + caixa = ' '; + } + var corpo = this.blocos(it.filhos, !b.solta).replace(/\n$/, ''); + out += '
  • ' + caixa + corpo + '
  • \n'; + } + return out + '\n'; + case 'tabela': + var self = this; + function celula(t, v, k) { + // Com `alinhamentoPorClasse` (o painel do dono: a CSP recusa style=), + // o alinhamento vira a classe `alinha-`. + var a = !b.alin[k] ? '' : ctx.alinhamentoPorClasse + ? ' class="alinha-' + b.alin[k] + '"' : ' style="text-align:' + b.alin[k] + '"'; + var sc = t === 'th' ? ' scope="col"' : ''; + return '<' + t + sc + a + '>' + self.inline.render(v) + ''; + } + var html = '
    \n\n\n'; + html += b.cab.map(function (v, k) { return celula('th', v, k); }).join(''); + html += '\n\n'; + if (b.linhas.length) { + html += '\n'; + for (var r = 0; r < b.linhas.length; r++) { + html += '' + b.linhas[r].map(function (v, k) { return celula('td', v, k); }).join('') + '\n'; + } + html += '\n'; + } + return html + '
    \n
    \n'; + } + return ''; + }; + + /* --------------------------------------------------------------- API */ + + function preparar(md) { + var texto = String(md == null ? '' : md) + .replace(/^/, '') + .replace(/\r\n?/g, '\n') + .replace(/\u0000/g, '�'); + var linhas = texto.split('\n').map(function (l) { + // Tabulação no recuo vira 4 espaços. + return l.replace(/^\t+/, function (t) { return new Array(t.length * 4 + 1).join(' '); }); + }); + // Definições de referência ([rótulo]: url "título"), fora de código. + var refs = {}; + var restantes = []; + var cerca = null; + for (var i = 0; i < linhas.length; i++) { + var l = linhas[i]; + if (cerca) { + if (new RegExp('^ {0,3}' + cerca.charAt(0) + '{' + cerca.length + ',}[ \\t]*$').test(l)) cerca = null; + restantes.push(l); + continue; + } + var c = abreCerca(l); + if (c) { cerca = c.marca; restantes.push(l); continue; } + var m = RX.ref.exec(l); + if (m && !/^\^/.test(m[1])) { + var k = normRotulo(m[1]); + if (!refs[k]) refs[k] = { dest: m[2], titulo: m[3] || m[4] || m[5] || '' }; + continue; + } + restantes.push(l); + } + return { linhas: restantes, refs: refs }; + } + + function render(md, opcoes) { + opcoes = opcoes || {}; + var prep = preparar(md); + var ctx = { + refs: prep.refs, + resolverLink: opcoes.resolverLink || null, + resolverImagem: opcoes.resolverImagem || null, + prefixoId: opcoes.prefixoId || '', + alinhamentoPorClasse: !!opcoes.alinhamentoPorClasse, + slugs: {}, + titulos: [], + dentroDeLink: false + }; + var blocos = new Analisador(ctx).blocos(prep.linhas); + var html = new Gerador(ctx).blocos(blocos, false); + return { html: html, titulos: ctx.titulos }; + } + + return { + render: render, + escapar: escapar, + slug: slug, + semAcento: semAcento, + urlSegura: urlSegura, + textoPuro: textoPuro + }; + }); + painel.css: | /* O painel do dono, na identidade do escolinha: papel, azul-marinho e laranja, * titulos em Poppins, e as cores de plano iguais as do painel do comprador. @@ -3497,6 +5358,153 @@ data: .catalogo-acoes { display: flex; gap: 14px; align-items: center; margin: 4px 0 24px; } .catalogo-acoes .erro { color: var(--perigo); } + /* ── informacoes: o Documento de Governanca dentro do painel ─────────────── */ + .sr-only { + position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; + overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; + } + .topo .info { display: inline-flex; align-items: center; gap: 6px; } + .topo .info svg { flex: none; } + .topo .info[aria-expanded="true"] { border-color: var(--orange); color: var(--orange-600); background: var(--orange-claro); } + .topo .acao:focus-visible, .topo .sair:focus-visible, .sino:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; } + + /* O topo e fixo: ao abrir, a cabeca da documentacao para logo abaixo dele. */ + .documentacao { scroll-margin-top: 84px; } + .doc-cabeca { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 20px; } + .doc-cabeca-texto { flex: 1 1 260px; min-width: 0; } + .doc-cabeca h1 { margin: 0; font-size: 22px; } + .doc-cabeca h1:focus { outline: none; } + .doc-cabeca .secao { margin: 0 0 2px; } + .doc-busca { flex: 1 1 260px; max-width: 380px; } + .doc-busca input { + width: 100%; font: inherit; font-size: 14px; padding: 9px 12px; border: 1px solid var(--line); + border-radius: 10px; background: var(--branco); color: var(--navy); + } + .doc-busca input:focus { outline: 2px solid var(--orange); border-color: transparent; } + .doc-acoes { display: flex; gap: 8px; flex-wrap: wrap; } + .doc-acoes .btn { margin-top: 0; font-size: 14px; padding: 8px 14px; } + .doc-estado { color: var(--muted); } + .doc-estado.erro { color: var(--perigo); margin-top: 0; font-size: 15px; } + + .doc-layout { display: grid; grid-template-columns: 260px minmax(0, 1fr) 210px; gap: 18px; align-items: start; } + .doc-layout .cartao { margin-bottom: 0; } + .doc-menu { position: sticky; top: 76px; max-height: calc(100vh - 96px); overflow-y: auto; padding: 16px 12px; } + .doc-arvore, .doc-arvore ul { list-style: none; margin: 0; padding: 0; } + .doc-arvore a { + display: block; padding: 5px 10px; border-radius: 8px; color: var(--navy); + text-decoration: none; font-size: 13.5px; line-height: 1.35; + } + .doc-arvore a:hover { background: var(--paper); color: var(--orange-600); } + .doc-arvore a[aria-current="page"] { background: var(--orange-claro); font-weight: 700; box-shadow: inset 3px 0 0 var(--orange); } + .doc-arvore-secao { margin-top: 12px; } + .doc-arvore-nome { display: block; padding: 0 10px 4px; font-family: 'Poppins', 'Barlow', system-ui, sans-serif; font-weight: 600; font-size: 13px; } + .doc-arvore-secao .ajuda { padding: 0 10px; } + .doc-num { font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--orange-600); } + + .doc-principal { min-width: 0; } + .doc-trilha ol { display: flex; flex-wrap: wrap; list-style: none; margin: 0 0 10px; padding: 0; font-size: 13px; color: var(--muted); } + .doc-trilha li + li::before { content: "/"; margin: 0 8px; color: var(--line); } + .doc-trilha a { color: var(--muted); text-decoration: none; } + .doc-trilha a:hover { color: var(--orange-600); text-decoration: underline; } + .doc-trilha [aria-current] { color: var(--navy); font-weight: 600; } + + .doc-sumario { position: sticky; top: 76px; max-height: calc(100vh - 96px); overflow-y: auto; font-size: 13px; padding-left: 14px; border-left: 1px solid var(--line); } + .doc-sumario h2 { font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 500; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); margin: 4px 0 8px; } + .doc-sumario ol { list-style: none; margin: 0; padding: 0; } + .doc-sumario a { display: block; padding: 3px 0; color: var(--muted); text-decoration: none; line-height: 1.35; } + .doc-sumario .nivel-3 a { padding-left: 12px; font-size: 12.5px; } + .doc-sumario a:hover { color: var(--orange-600); } + + .doc-paginacao { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 14px; } + .doc-paginacao a { + display: flex; flex-direction: column; gap: 2px; padding: 12px 16px; border: 1px solid var(--line); + border-radius: 14px; background: var(--branco); color: var(--navy); text-decoration: none; + } + .doc-paginacao a:hover { border-color: var(--orange); } + .doc-paginacao .doc-prox { grid-column: 2; text-align: right; } + .doc-cartoes { list-style: none; padding: 0; margin: 18px 0 0; display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); } + .doc-cartoes .bloco { border-top-color: var(--orange); } + .doc-cartoes a, .doc-cartoes b { font-family: 'Poppins', 'Barlow', system-ui, sans-serif; font-weight: 600; color: var(--navy); text-decoration: none; } + .doc-cartoes a:hover { color: var(--orange-600); text-decoration: underline; } + .doc-resultados { list-style: none; padding: 0; } + .doc-texto .doc-cartoes, .doc-texto .doc-resultados { padding-left: 0; } + .doc-resultados li { padding: 12px 0; border-bottom: 1px solid var(--line); } + .doc-resultados a { font-weight: 700; } + + /* O texto renderizado do Markdown. */ + .doc-texto { overflow-wrap: break-word; line-height: 1.65; } + .doc-texto:focus { outline: none; } + .doc-texto > .secao:first-child { margin-top: 0; } + .doc-texto h1 { font-size: 26px; margin: 0 0 16px; line-height: 1.25; } + .doc-texto h2 { font-size: 20px; margin: 1.8em 0 .6em; padding-bottom: .3em; border-bottom: 1px solid var(--line); } + .doc-texto h3 { font-size: 16.5px; margin: 1.5em 0 .5em; } + .doc-texto h4, .doc-texto h5, .doc-texto h6 { font-size: 15px; margin: 1.3em 0 .4em; } + .doc-texto h1:focus, .doc-texto h2:focus, .doc-texto h3:focus { outline: none; } + .doc-texto h2, .doc-texto h3 { scroll-margin-top: 80px; } + .doc-texto ul, .doc-texto ol { padding-left: 1.4em; } + .doc-texto ul li::marker { color: var(--orange); } + .doc-texto ul.tarefas { list-style: none; padding-left: .2em; } + .doc-texto blockquote { margin: 0 0 1em; padding: .6em 1em; border-left: 4px solid var(--orange); background: var(--orange-claro); border-radius: 0 10px 10px 0; } + .doc-texto code { font-family: 'IBM Plex Mono', ui-monospace, Consolas, monospace; font-size: .86em; background: var(--paper); border: 1px solid var(--line); border-radius: 6px; padding: .1em .35em; } + .doc-texto pre { overflow-x: auto; padding: 14px 16px; background: var(--paper); border: 1px solid var(--line); border-radius: 10px; line-height: 1.5; } + .doc-texto pre code { background: none; border: 0; padding: 0; white-space: pre; } + .doc-texto .tabela { overflow-x: auto; border: 1px solid var(--line); border-radius: 10px; margin: 0 0 1em; } + .doc-texto table { width: 100%; border-collapse: collapse; font-size: 14px; line-height: 1.45; } + .doc-texto th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); background: #fbfaf8; border-bottom: 1px solid var(--line); padding: 8px 10px; } + .doc-texto td { padding: 9px 10px; border-bottom: 1px solid var(--line); vertical-align: top; } + .doc-texto tbody tr:last-child td { border-bottom: 0; } + .doc-texto .alinha-left { text-align: left; } + .doc-texto .alinha-center { text-align: center; } + .doc-texto .alinha-right { text-align: right; } + .doc-texto hr { border: 0; border-top: 1px solid var(--line); margin: 2em 0; } + .doc-texto a.externo::after { content: "\2197"; font-size: .8em; margin-left: 2px; } + .doc-arquivo { font-family: 'IBM Plex Mono', monospace; font-size: .9em; border-bottom: 1px dotted var(--muted); cursor: help; } + .doc-origem { margin-top: 2em; padding-top: 1em; border-top: 1px dashed var(--line); } + + /* Diagrama C4 (o desenho vem do painel-c4.js; sem ele, fica so a legenda). + O SVG le as cores destas variaveis. */ + .c4 { + --c4-pessoa: var(--navy); --c4-sistema: var(--azul); --c4-externo: #8A94A0; + --c4-conteiner: #2F7BA3; --c4-componente: #5FA3C7; --c4-texto: var(--branco); + --c4-linha: var(--muted); --c4-fundo: var(--branco); --c4-borda: var(--line); --c4-destaque: var(--orange); + margin: 0 0 1.2em; padding: 14px; background: var(--c4-fundo); border: 1px solid var(--c4-borda); border-radius: 14px; overflow-x: auto; + } + .c4 svg { display: block; max-width: 100%; height: auto; margin: 0 auto; } + .c4 figcaption { font-size: 12.5px; color: var(--muted); margin-top: 8px; text-align: center; } + + @media (max-width: 1180px) { + .doc-layout { grid-template-columns: 240px minmax(0, 1fr); } + .doc-sumario { display: none; } + } + @media (max-width: 760px) { + .topo .info { margin-left: auto; } + .topo { gap: 8px; } + .topo .logo { height: 26px; } + .topo .acao, .topo .sair { padding: 7px 10px; } + .documentacao { scroll-margin-top: 120px; } + .topo .info:not(.oculto) ~ .acao { margin-left: 0; } + .topo .info span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } + .doc-layout { grid-template-columns: 1fr; } + .doc-menu { position: static; max-height: 40vh; } + .doc-busca { max-width: none; } + .doc-paginacao { grid-template-columns: 1fr; } + .doc-paginacao .doc-prox { grid-column: 1; } + } + + /* "Imprimir pagina": so o texto aberto. */ + @media print { + body.doc-aberta { background: #fff; } + body.doc-aberta .topo, body.doc-aberta .doc-cabeca, body.doc-aberta .doc-estado, body.doc-aberta .doc-menu, + body.doc-aberta .doc-sumario, body.doc-aberta .doc-trilha, body.doc-aberta .doc-paginacao, + body.doc-aberta #conteudo, body.doc-aberta .ficha { display: none !important; } + body.doc-aberta .wrap { max-width: none; padding: 0; } + body.doc-aberta .doc-layout { display: block; } + body.doc-aberta .doc-texto { border: 0; box-shadow: none; padding: 0; } + .doc-texto pre, .doc-texto pre code { white-space: pre-wrap; } + .doc-texto h2, .doc-texto h3 { break-after: avoid; } + .doc-texto pre, .doc-texto tr, .c4 { break-inside: avoid; } + } + painel.html: | @@ -3524,9 +5532,17 @@ data: O `?v=` NOS ARQUIVOS NAO E ENFEITE: o nginx serve js e css com `Cache-Control: immutable` por um ano. Suba o numero a cada publicacao. + + "INFORMACOES" (o Documento de Governanca, 07/10/2026): o CONTEUDO nao mora + aqui. Ele tem detalhe de infraestrutura e vem da captacao, com o mesmo login + do painel (GET /api/plataforma/painel/documentacao), so quando o dono clica. + Daqui so saem o renderizador (`painel-markdown.js`, copia de + governanca/assets/markdown.js) e o desenho C4 (`painel-c4.js`, carregado na + primeira abertura; sem ele o diagrama fica so com a legenda). --> - + + @@ -3546,6 +5562,11 @@ data:

    Não consegui falar com o monitoramento agora.

    + @@ -3772,6 +5793,39 @@ data: + +
    +
    +
    +

    Informações

    +

    Documento de Governança

    +

    +
    + +
    + + +
    +
    +

    +
    + +
    + +
    + +
    + +
    +
    + + @@ -3772,6 +5793,39 @@ data: + +
    +
    +
    +

    Informações

    +

    Documento de Governança

    +

    +
    + +
    + + +
    +
    +

    +
    + +
    + +
    + +
    + +
    +
    +