Conn2Flow
Navegação das docs

SDD

decisions-096-113

# Decision Log — Archive (DEC-096 a DEC-113)

# Decision Log — Archive (DEC-096 a DEC-113)

Decisões arquivadas DEC-096 a DEC-113 do conn2flow. Para o log ativo, ver DECISION-LOG.md.


DEC-096 - 2026-07-30 - accepted

Mídia embutida: 403 em arquivos com espaço, streaming por HTTP Range e dimensionamento do áudio (homologação do BATCH-097 / BATCH-100). Decisões desta rodada:

  1. O 403 era da reescrita do Apache, não do PHP nem da codificação da URL: medido com curl no container (extensão sem espaço → 404 normal; qualquer extensão com %20 → 403) e confirmado no localhost-error.log: AH10411: Rewritten query string contains control characters or spaces. O .htaccess passa o caminho para a query (index.php?_gestor-caminho=$1) e o mod_rewrite entrega o valor DECODIFICADO; desde o Apache 2.4.53 isso é recusado. Consequência importante: codificar a URL no retorno do picker não resolveria — o navegador já enviava %20 (visível no console do Chefe) e o 403 ocorria assim mesmo. A flag [B] (re-escapa backreferences ao montar a query) é a correção, e ela já é o padrão do projeto: está no .htaccess do instalador e nos sites locais criados recentemente; apenas o mirror local antigo (arquivo em dev-environment/data/, git-ignored) estava defasado. Nenhuma mudança de .htaccess versionado foi necessária.

  2. Corrigir na ORIGEM, como o Chefe pediu, sem depender do .htaccess: (a) arquivo_nome_sanitizar() passou a trocar espaço por hífen na ENTRADA (upload e renomeação) — arquivos vindos de gravador de tela e do WhatsApp trazem espaço por padrão, e agora nascem com nome URL-safe em todo o sistema; (b) urlDeArquivo() no motor codifica a URL devolvida pelo gerenciador (encodeURI(decodeURI(...)), idempotente) nos três pontos que a montam. O caminho continua cru como IDENTIFICADOR (é ele que o backend usa em file_exists e nas associações de categoria do BATCH-090) — codificar o identificador quebraria a resolução no servidor; só a URL de exibição é codificada.

  3. Streaming de mídia é requisito, não refinamento: o servidor de estáticos respondia com readfile() cru — Transfer-Encoding: chunked, sem Content-Length, sem Accept-Ranges e ignorando o Range pedido. Assim não há seek na linha do tempo e o Safari/iOS não reproduz <video>/<audio>. Agora há Accept-Ranges: bytes, Content-Length, resposta 206 com Content-Range (envio em blocos de 8 KB), 416 para faixa inválida e limpeza dos buffers antes do envio (binário íntegro). A lógica de parsing ficou numa função PURA (arquivo_estatico_range) coberta por teste, e o controlador ganhou o guard SDD_NO_AUTORUN para ser incluído em teste sem disparar o envio.

  4. charset só em formatos de texto: Content-Type: video/mp4; charset=UTF-8 é inválido e atrapalha players. A tabela agora aplica charset apenas a js/css/svg/json/html/txt/xml; binários usam o mime puro (fallback application/octet-stream).

  5. Altura padrão por TIPO de mídia: o modal usava 600px como altura padrão para qualquer embed sem altura própria, o que esticava o player de <audio> (que tem altura intrínseca de ~54px). embedDefaultSize() passa a devolver áudio sem altura, vídeo 360px, iframe/embed 400px e documento 600px; além disso buildMediaMarkup() nunca emite height para áudio e applyEmbedSize() redimensiona áudio só na largura — a altura do player nativo é preservada mesmo arrastando as alças.

Validação: verificação HTTP real no container (200 com Accept-Ranges/Content-Length, 206 com Content-Range, 416, CSS mantendo charset), php -l 3/3, node --check OK, composer test 165/165 (novo ArquivoEstaticoRangeTest 6/6), npx vitest run 66/66. Cache-bust: biblioteca-html-editor 1.5.3, motor ?v=c2f14. Nenhum git commit/git push executado.

DEC-097 - 2026-07-30 - accepted

Embed sem arquivo escolhido não emite atributo de fonte vazio (homologação / BATCH-101). Decisões desta rodada:

  1. src="" é pior que ausência de src: um atributo de fonte vazio resolve para a URL da PRÓPRIA página — o navegador baixa o HTML, tenta decodificá-lo como mídia, falha e (no Chrome) colapsa o player, além da requisição inútil. Como o fluxo do painel "+" cria o embed SEM fonte por design (o modal abre logo depois para o usuário escolher o arquivo), o caminho "aplicar sem escolher" era comum. Todos os geradores passaram a omitir o atributo quando a fonte está vazia: src (mídia/iframe), data (<object>), data-pdf-src (PDF.js) e o src do Google Viewer (que apontaria para um viewer com url= vazia).

  2. O diagnóstico veio da comparação dos dois HTMLs, não de suposição: o Chefe enviou um caso quebrado e um correto. Os invólucros eram idênticos (display: block, mesma estrutura), o que descartou contexto de flex/grid. A diferença estava no src="" — e o caso quebrado ainda trazia position:relative; z-index:1 com espaço após o ;, assinatura do styleExtra reconstruído por embedReadConfig (join('; ')), provando que ele passara pelo "Aplicar" do modal enquanto o correto viera apenas do buildElement. A troca de section relatada era coincidência.

  3. Hipótese descartada foi REVERTIDA, não mantida por precaução: durante a investigação aplicou-se width:100% ao invólucro de largura fluida, para o caso de ele cair num contexto shrink-to-fit. Sem evidência de que isso ocorria — e com risco de comprimir irmãos em containers flex — a mudança foi desfeita. Registrar a hipótese aqui evita que alguém a reintroduza sem medir.

Cache-bust: biblioteca-html-editor 1.5.4, motor ?v=c2f15. Validação: node --check OK, npx vitest run 68/68, composer test 165/165. Nenhum git commit/git push executado.

DEC-098 - 2026-07-30 - accepted

Memória da última pasta no modo picker do admin-arquivos (demanda direta do Engenheiro Chefe / BATCH-102). Decisões desta rodada:

  1. O cache de pasta passa a valer no picker: o BATCH-090 introduziu o cache (localStorage['adminArquivosDir']) excluindo explicitamente o modo iframe nas duas pontas — não lia no boot nem gravava ao navegar. A premissa era que o seletor deveria começar onde o chamador mandasse; na prática nenhum chamador informa pasta, então o efeito real era abrir sempre na raiz e obrigar o usuário a refazer o caminho a cada seleção. Como é a MESMA árvore de arquivos, o cache passa a ser lido e gravado também no picker.

  2. Precedência explícita em vez de exclusão por modo: a regra deixou de ser "picker não usa cache" e passou a ser ?dir= explícito > cache > raiz. Isso preserva a intenção original (abrir numa pasta específica quando alguém pedir) sem sacrificar a usabilidade no caso comum. A tela de envio já usava exatamente esse contrato (cfg.dirExplicito ? cfg.dirInicial : cache); a listagem apenas foi alinhada a ele.

  3. A listagem passou a aceitar ?dir=: antes o PHP fixava dirInicial => '' na listagem. Agora expõe dirExplicito/dirInicial a partir de $_REQUEST['dir'], saneado por arquivo_caminho_relativo_seguro (helper de segurança do BATCH-090) — o que habilita, de graça, abrir o seletor direto numa pasta quando algum ponto de uso quiser isso no futuro.

  4. Fluxo do picker fica fechado: navegar até a pasta → "Adicionar" (com ?dir=, explícito) → enviar → voltar pela seta (sem dir) → o cache devolve a mesma pasta, onde o arquivo recém-enviado está. Combina com o BATCH-099, que liberou o envio dentro do iframe. Pasta apagada continua caindo na raiz pelo self-heal já existente em admin_arquivos_ler_pasta.

Cache-bust: módulo admin-arquivos 1.1.6→1.1.7. Validação: php -l OK, node --check OK, JSON válido, composer test 165/165. O comportamento é estado de front + renderização condicional, sem cobertura unitária viável — verificação manual registrada no checklist. Nenhum git commit/git push executado.

DEC-099 - 2026-07-30 - accepted

Filtro de módulos no menu principal do gestor (demanda direta do Engenheiro Chefe / BATCH-103). Decisões desta rodada:

  1. Comportamento portado da Editbar, adaptado à estrutura do menu: o c2f-modules-filter esconde itens e depois varre irmãos para ocultar cabeçalhos de grupo órfãos. No menu do gestor a estrutura é mais simples — cada grupo é um .ui.list > .item que já contém o cabeçalho e seus links —, então basta ocultar o bloco quando nenhum a.item interno estiver visível. Itens fixos (Dashboard e Sair) também são filtrados, decisão do Chefe, para o comportamento ficar uniforme com a Editbar.

  2. Texto do campo vem do COMPONENTE, não do PHP: a Editbar decide o rótulo no PHP (condicional de idioma) porque monta o menu em código. Aqui o componente menu-principal-sistema já é versionado por idioma (resources/pt-br e resources/en), então placeholder e aviso de vazio ficam no HTML de cada idioma — sem condicional de idioma no core e sem depender de registro novo na tabela de variáveis.

  3. O asset acompanha o HTML do menu, e não a fila de assets: gestor_pagina_menu() é chamada por gestor_pagina_variaveis() (gestor.php:619), que roda DEPOIS de gestor_pagina_extra_head_e_javascript(). Nesse ponto o marcador de JS da página já foi substituído, então gestor_pagina_css_incluir()/gestor_pagina_javascript_incluir() apenas empilhariam a tag num array que ninguém mais lê — o script nunca chegaria à página. A tag <script src> com cache-bust de versão é concatenada ao HTML retornado pelo menu: carrega de fato, mantém o custo restrito a quem vê o menu e dispensa um gate novo no pipeline. Alternativa registrada, caso se prefira a fila: incluir em gestor_pagina_extra_head_e_javascript com guarda de usuário logado.

  4. Regex de acentuação montado por código: o range de marcas combinantes é construído com String.fromCharCode(0x300)/0x36f em vez de escrito literalmente. Mantém o fonte 100% ASCII e imune a um editor que normalize o arquivo (NFC juntaria as marcas ao colchete anterior, corrompendo o padrão silenciosamente).

  5. API pública para teste: window.gestorMenuFiltro.aplicar(termo, escopo) e iniciar() (idempotente, marcada por atributo no campo), mesmo padrão de window.conn2flowPdfViewerInit — permite cobrir a lógica com Vitest sem simular digitação em toda checagem.

Validação: php -l OK, node --check OK, npx vitest run 76/76 (novo admin-menu-filtro.test.js 8/8), composer test 165/165. Requer deploy Update => Core: o componente do menu é lido do banco. Nenhum git commit/git push executado.

Adendo DEC-099 (rodada 2 — consistência das buscas, mesma sessão)

  1. O bug estava na BASE, não só no port: ao portar o filtro da Editbar para o menu do painel, a versão nova ganhou normalização de acentuação — e o Chefe notou a inconsistência: 'pa' encontrava 'Páginas' no menu novo, mas não na Editbar. A comparação crua falha porque o segundo caractere do texto é acentuado. Corrigidos os dois pontos remanescentes (dashboard.iframe-toolbar.js e a busca de modelos em html-editor-interface.js), todos usando o mesmo helper com o range montado por String.fromCharCode. Lição registrada: ao portar comportamento, avaliar a base antes de copiá-la — o padrão existente pode ser o defeito.

  2. Botão 'x' de limpar padronizado: #modelos-search-input era o único campo de busca do sistema sem o atalho de limpar. Adotado o par lupa/x do gerenciador de arquivos (.c2f-search-icon/.c2f-search-clear → .modelos-search-icon/.modelos-search-clear), alternado por hidden do Fomantic; o clique limpa, refiltra e devolve o foco ao campo. Os textos (title) ficam nos componentes por idioma.

  3. Salto de rolagem no 'Carregar Mais' era o hide() da lista: modelosCarregar escondia #modelos-cards enquanto o AJAX rodava. Com a lista fora do fluxo a página encolhe e o navegador reposiciona a rolagem no topo — daí o salto ao paginar. A correção condiciona o hide() à PRIMEIRA página (única em que a lista é realmente substituída); na paginação o indicador de carregamento aparece abaixo dos cards, sem deslocar o que o usuário lê. Preferiu-se isso a salvar/restaurar scrollTop, que trataria o sintoma e dependeria de descobrir o contêiner rolável correto.

DEC-100 - 2026-07-31 - accepted

Extensão da biblioteca PayPal (PHP) e asset JS para Checkout Transparente e Tokenização (req-098 / BATCH-104). Decisões desta rodada:

  1. Geração de Client Token OAuth: criação da função paypal_gerar_client_token($params) na biblioteca backend gestor/bibliotecas/paypal.php, requisitando POST /v1/identity/generate-token com suporte a customer_id opcional para autorização PCI-DSS SAQ-A no navegador.

  2. Suporte nativo a payment_source em Pedidos e Assinaturas: atualização de paypal_criar_pedido e paypal_criar_assinatura para aceitar a estrutura payment_source (cartões tokenizados payment_source.card e tokens payment_source.token).

  3. Helper de Processamento Transparente: inclusão de paypal_processar_pagamento_transparente($params) para orquestrar a validação e cobrança/assinatura via backend de forma padronizada.

  4. Asset JS de Frontend (gestor/assets/paypal/paypal.js): criação da biblioteca utilitária no frontend composta por: carregamento dinâmico do SDK oficial com parâmetros de components, vault e data-client-token; renderização e estilização dos Hosted/Card Fields com feedback de validação em tempo real; e método paypalCardFieldsSubmit para tokenização síncrona/assíncrona no submit do formulário.

Adendo DEC-100 — alinhamento com os contratos oficiais durante a implementação

  1. data-client-token é atributo, não query string: o loader mantém client-id, components, intent, vault e demais opções públicas na URL, mas grava o client token em data-client-token na tag <script>, como exige o SDK. Colocar o segredo na URL aumentaria sua exposição em histórico, telemetria e logs de proxy.

  2. Card Fields aprova ordem; não se inventa nonce: no SDK atual, CardFields.submit() dispara o fluxo ligado a createOrder/onApprove. O retorno normalizado de paypalCardFieldsSubmit contém order_id e somente inclui payment_source quando o SDK ou um callback da integração realmente o fornece. O helper PHP aceita order_id para captura; assim os dados PCI permanecem nos iframes PayPal. Payloads já tokenizados continuam aceitos via payment_source.card/payment_source.token.

  3. Nesting correto em assinaturas: Orders recebe payment_source no topo; Subscriptions recebe em subscriber.payment_source, conforme o schema da API v1. O parâmetro público da função continua sendo $params['payment_source'], sem exigir que o chamador conheça o nesting do endpoint.

  4. Idempotência no pagamento direto: pedidos com payment_source enviam PayPal-Request-Id (aceito por $params['request_id'] ou gerado localmente) e tratam respostas 200/201. intent passou a aceitar CAPTURE e AUTHORIZE, preservando CAPTURE como padrão.

  5. Compatibilidade de frontend: Card Fields é a integração preferida; Hosted Fields legado é fallback explícito quando o SDK carregado não oferece CardFields. A API pública também fica exposta em window.conn2flowPaypal e nos três nomes globais solicitados.

Validação: php -l e node --check OK; Vitest 90/90 (7 casos PayPal novos); PHPUnit 172/172 (7 casos PayPal novos, 4 testes preexistentes pulados). A homologação com credenciais Sandbox/Live permanece operacional, pois a suíte não deve chamar APIs financeiras externas. Nenhum git commit/git push executado.

DEC-101 - 2026-08-03 - accepted

Navegacao por teclado nos filtros de modulos da Editbar e do menu principal do gestor (demanda direta do Engenheiro Chefe / BATCH-105). Decisoes desta rodada:

  1. Mover o foco real, sem manter um indice visual paralelo: os resultados ja sao links. Focar o link visivel produz navegacao acessivel, feedback de foco do navegador/framework e mantem Enter como ativacao nativa, sem duplicar click() em JavaScript.

  2. Recalcular a lista visivel a cada tecla: a navegacao consulta .c2f-menu-item/a.item no estado atual e ignora os elementos com display:none; assim nunca entra em resultado removido pelo termo digitado e nao precisa sincronizar estado adicional com o listener de input.

  3. Limites sem ciclo: ArrowDown no ultimo resultado mantem o foco; ArrowUp no primeiro volta ao input. O retorno explicito ao filtro atende a edicao imediata do termo e evita um salto inesperado do ultimo para o primeiro.

  4. Markup preservado: dashboard.php e os componentes pt-br/en nao precisam de tabindex, roles ou texto novo, pois os itens acionaveis ja sao anchors. O delta fica restrito aos dois controladores JavaScript, aos testes e ao cache-bust do dashboard.

Validacao: node --check nos dois assets e parse do JSON OK; testes focados 16/16; Vitest completo 93/93; git diff --check OK. Nenhum git commit/git push executado.

DEC-102 - 2026-08-06 - accepted

Promocao e consolidacao das recomendacoes de seguranca e arquitetura do backlog para a requisicao humana req-107.md (BATCH-107). Decisões desta rodada:

  1. Promocao unificada dos 10 itens do backlog (BL-001 a BL-010): em vez de dispersar as correcoes de seguranca em pequenos lotes fragmentados, consolida-se uma requisicao estruturada (req-107.md) cobrindo instalador, CSPRNG, path traversal, CSRF, API, OAuth2, cabeçalhos HTTP, prepared statements e saneamento de codigo.

  2. CSPRNG obrigatorio em tokens e sessoes: substituicao de md5(uniqid(rand(), true)) e md5(uniqid(mt_rand(), true)) por random_bytes() / random_int com entropia minima de 128 bits centralizados em seguranca_token_aleatorio().

  3. Contencao estrita de Path Traversal por realpath: validacao por realpath e str_starts_with contra os diretoriaos autorizados (assets/, contents/, modulos/) com rejeicao imediata de .. e bytes nulos.

  4. Validacao CSRF obrigatoria em acoes de estado por cookie: ativacao de gestor_csrf_validar() nos formulaios e requisições AJAX autenticados por cookie no painel, mantendo isencao documentada apenas para endpoints M2M/HMAC.

  5. Hardening de API e OAuth2: CORS parametrizado por allow-list, eliminacao de tokens em query string (?token=), validacao do HMAC pubIDValidation na verificacao de access tokens e rotacao FIFO de sessoes limite.

  6. Arquivamento rastreavel do backlog: os rascunhos BL-001 a BL-010 foram movidos para sdd/backlog/archive/ e anotados como PROMOTED em sdd/backlog/BACKLOG-INDEX.md.

DEC-103 - 2026-08-06 - accepted

Painéis fixos opcionais do editor visual, expansão da caixa de estilização e ação "Substituir" (req-106 / BATCH-106). Decisões desta rodada:

  1. Encaixe (dock) do MESMO nó, em vez de painéis novos: a Sidebar de CSS e a Barra de Navegação não reimplementam styler, breadcrumb e lista de filhos — elas os RECEBEM. Ligar um toggle move o elemento existente (appendChild) para dentro do painel fixo e acrescenta uma classe de estado (he-styler-docked / he-nav-docked) que anula o posicionamento absoluto e a moldura própria; desligar devolve o nó ao document.body. Todos os listeners registrados na criação continuam válidos, renderStyler/renderBreadcrumb/renderChildren seguem intactos e não existe risco de duas cópias divergirem. O custo é uma regra em updateSelectionUI: o que está encaixado não é medido nem posicionado.

  2. Os painéis SOBREPÕEM o conteúdo; não empurram a página: a alternativa (aplicar margin-left ao <html>, como o Live Editor já faz com margin-top) mudaria a largura de renderização do conteúdo editado — percentuais, 100vw e layouts fluidos passariam a ser desenhados numa caixa diferente da que o visitante vê, além de gerar rolagem horizontal. Como o editor edita o DOM real, preferiu-se a camada sobreposta (padrão de inspetor), com o toggle disponível para liberar a borda esquerda quando o usuário precisar. "Sem sobreposições" do intake foi lido como o que é: a sidebar encaixa ABAIXO da barra de navegação, que por sua vez encaixa abaixo da Editbar.

  3. O offset do topo vem do margin-top do <html>, não da altura do iframe da barra: o iframe #c2f-site-toolbar CRESCE quando um dropdown abre (ele apenas sobrepõe), então sua altura não serve de referência. O margin-top que dashboard.toolbar.js aplica é exatamente o offset PERSISTENTE (30px + 44px da editbar aberta). No editor clássico não há barra e o offset é 0.

  4. Estado persistido, padrão desligado: as opções vivem em localStorage['c2f-he-view-options'] com os dois valores em false por padrão, no mesmo modelo da área de transferência do BATCH-098. Sem persistência o usuário teria de religar a sidebar a cada página; com ela, a exigência do intake ("desativado por padrão") vale na primeira experiência e a preferência é respeitada depois. Storage indisponível cai silenciosamente no padrão desligado.

  5. O painel de opções conversa com o motor pelo canal de cada contexto: no Live Editor o motor roda na MESMA página do dashboard.toolbar.js, então o toggle chama c2fEditor.setViewOption() direto (e lê getViewOption() ao abrir, evitando um estado paralelo). No editor clássico o motor está dentro do iframe: a janela pai posta c2f-he:view-option. Como o iframe usa srcdoc, ele herda a origem — o painel do pai lê o mesmo localStorage para marcar os checkboxes.

  6. Separação Tailwind × classes do projeto é HEURÍSTICA e organizacional: isTailwindClass trabalha sobre o utilitário sem variante, aceita a sintaxe de colchetes e casa contra um conjunto de prefixos/palavras. Uma classe do projeto que colida com um prefixo (text-destaque) aparece no bloco do Tailwind — o que muda é só o agrupamento visual, nunca o que é aplicado ao elemento. Preferiu-se isso a exigir um dicionário completo do Tailwind embarcado no motor.

  7. Valores manuais e CSS inline entram no accordion existente, não numa área exclusiva da sidebar: assim o painel flutuante (sidebar desligada) ganha as mesmas capacidades sem inflar — as seções nascem fechadas — e não é preciso manter dois modos de renderização do styler.

  8. "Substituir" limpa a seleção ANTES de remover o alvo: afterDomMutation chama updateSelectionUI, que trabalha sobre selectedElement; apagar o nó antes de zerar a seleção deixaria a UI apontando para um elemento fora do documento. O bloco entra na POSIÇÃO do original (não ao lado, como o "Colar"), passa por remapPastedIds — colar/substituir na mesma página do original não pode duplicar data-c2f-widget-id — e o novo objeto é selecionado ao final.

  9. Os três ids novos entram no contrato de UI do editor: isEditorOwned (para o clique não vazar para o conteúdo atrás via elementsFromPoint), extractUserHtml e o seletor de fallback do salvamento em html-editor-interface.js. Sem isso, no editor clássico (onde contentRoot é o próprio body) os painéis seriam persistidos junto com a página.

Validação: node --check 5/5, php -l OK, JSON OK, npx vitest run 118/118 (novo html-editor-view-options.test.js 21/21), composer test 172/172. Requer deploy Update => Core: os dois botões vêm de página/componente lidos do banco. Nenhum git commit/ git push executado.

Adendo DEC-103 (rodada 2 — homologação do Chefe, mesma data)

  1. O modo flutuante dos três painéis foi APOSENTADO, não mantido como alternativa: a decisão do Chefe na homologação é que a usabilidade antiga (breadcrumb, filhos e styler perseguindo o elemento) não se justifica agora que existem lugares fixos para eles. Com o painel desligado, o conteúdo continua sendo RENDERIZADO (a barra fixa o consome assim que ligada) mas não é exibido. O empilhamento flutuante permanece no arquivo, inativo — desabilitar custa uma linha e preserva a possibilidade de retomada; remover exigiria reescrever updateSelectionUI. A barra flutuante de AÇÕES (html-editor-floating-toolbar) não faz parte do legado aposentado: ela precisa estar junto do elemento.

  2. Ancoragem alternável em vez de painel arrastável: o problema real relatado é o painel cobrir o objeto que se quer editar. Duas posições opostas por painel (esquerda/direita, topo/base) resolvem isso com um clique, sem introduzir arraste, redimensionamento e persistência de coordenadas — que exigiriam tratar viewport pequena, colisão entre os painéis e reencaixe. O botão vive no cabeçalho, junto do fechar, porque é lá que o usuário já olha para identificar o painel.

  3. O ✕ do cabeçalho é o MESMO caminho do toggle: setViewOption(painel, false) — não um "ocultar temporário" paralelo. Assim o painel de Opções de Exibição continua sendo a fonte única do estado (ao reabrir, o toggle aparece desmarcado) e a escolha é persistida como qualquer outra.

  4. Clique no iframe da Editbar precisa ser ANUNCIADO ao host: os painéis do [REDACTED] por um mousedown no document da página hospedeira, e os do motor por um clique no próprio backdrop. Nenhum dos dois acontece quando o clique cai dentro do iframe da barra — daí os painéis ficarem abertos ao usar outro botão da Editbar. A barra passa a publicar c2f-toolbar:ui-dismiss em captura, e o [REDACTED] o fechamento em dismissHostPanels().

  5. A regra é "clicou fora, fechou" — em qualquer superfície (decisão do Chefe na rodada 2): o ui-dismiss fecha TODA a UI flutuante, não só a do host. dismissHostPanels() delega ao motor por c2fEditor.dismissFloatingUi(), que fecha exatamente o conjunto que já fechava por backdrop ao clicar na área editável — Modelos, IA, Código Customizado, modal de embed, seletor de arquivos e modal de edição. Chegou-se a preservar os painéis do motor (receio de perder texto digitado no Assistente de IA), mas eles já sumiam ao clicar na página; manter a exceção só na barra criaria duas regras para o mesmo gesto. Clique DENTRO de um painel continua sem efeito: o aviso nasce apenas de cliques na barra e, na página, cada backdrop/closest() segue responsável.

  6. Ler isModalActive ANTES de fechar os demais: closeEmbedModal() zera a flag incondicionalmente (mesmo com o modal de embed fechado). Encadear os fechamentos sem cuidado fazia o modal de EDIÇÃO deixar de ser fechado — o teste do guard pegou isso. dismissFloatingUi captura o estado no início e só fecha embed/picker quando eles estão de fato abertos.

Validação da rodada 2: node --check 3/3, php -l OK, JSON OK, npx vitest run 130/130, composer test 181/181. Cache-bust: biblioteca-html-editor 1.5.8, dashboard.json 1.0.17, motor ?v=c2f17. Nenhum git commit/git push executado.

Adendo DEC-103 (rodada 3 — homologação do Chefe, mesma data)

  1. CodeMirror no CSS inline com altura própria: a diretriz de Chefia para editores CodeMirror em abas/containers extras fixa setSize('100%', 800). Aqui o editor vive numa sidebar de 240px de largura, ao lado de outros blocos roláveis — 800px de altura tornaria o painel inutilizável. Herdam-se o tema (tomorrow-night-bright) e a indentação (4); o modo é css (é uma declaração de estilo, não HTML) e a altura fica em 110px. A criação é sob demanda, só com a sidebar ligada, e a degradação graciosa para <textarea> é mantida — inclusive o on('blur'), registrado apenas quando a build expõe eventos. applyInlineCss ignora valor idêntico ao style atual, senão cada troca de foco empilharia um passo de undo vazio.

  2. O resize-follow precisa descontar o que vem DEPOIS do editor — sem perder o dinamismo: a conta original (fundo do corpo − topo do editor) dava ao CodeMirror todo o espaço restante, empurrando para fora da área visível o que vinha depois dele no corpo — daí o #c2f-ai-status só aparecer rolando a caixa. A correção mantém a base (fundo do corpo, que cresce junto com a caixa quando o usuário arrasta o canto) e desconta alturaAposElemento(editor, corpo): a soma dos irmãos posteriores do editor e dos seus ancestrais até o corpo, com margens. Vale para o status de hoje e para qualquer bloco futuro. Tentativa descartada, registrada para não ser reintroduzida: calcular o livre por clientHeight − (scrollHeight − altura dos editores). scrollHeight nunca é menor que clientHeight, então, com o conteúdo cabendo na caixa, a expressão colapsa em "altura atual do editor − folga": cada disparo do ResizeObserver encolhia o editor até o mínimo e matava o acompanhamento do mouse — justamente o comportamento do BATCH-081 que a rodada deveria preservar. Como a conta adotada não depende da altura do editor, não há realimentação e a guarda anti-loop por igualdade exata continua suficiente.

  3. Ajuste de altura não pode viver só no ResizeObserver: ele reage a mudanças da CAIXA, então o editor de uma aba recém-exibida ficava com a altura fixa do setSize inicial até o usuário arrastar o canto. A lógica virou o método syncLiveBoxCodeMirrors(panel), chamado pelo observador, pela troca de abas e pela abertura dos painéis de IA e Código Customizado.

  4. Preview de dispositivo não é sair da edição: enterDevicePreview() desabilita o editor para que o preview seja fiel, e a rodada 1 amarrou a visibilidade dos painéis fixos ao disable(). O resultado é que trocar desktop/tablet/mobile fazia a Sidebar e a Barra sumirem, sugerindo que a funcionalidade tinha ido embora. disable() passou a aceitar { manterPaineis: true }, usado só nesse caminho — preferido a criar um método novo, porque mantém um único ponto de desligamento do editor e deixa a exceção explícita em quem a pede.

Validação da rodada 3: node --check 2/2, php -l OK, JSON OK, npx vitest run 137/137, composer test 181/181. Cache-bust: biblioteca-html-editor 1.5.9, dashboard.json 1.0.18, motor ?v=c2f18. Nenhum git commit/git push executado.

DEC-104 - 2026-08-13 - accepted

Isenção de crawlers, verificação silenciosa de cookie, OpenGraph no core e auditoria de CSRF em formulários submetidos por código (req-109 / BATCH-109). Decisões desta rodada:

  1. A verificação de cookie deixou de ser um pedágio universal: gestor_cookie_verificacao() emitia o cookie e, no MESMO request, interrompia a resposta com Location: _gestor-cookie-verify/. O round-trip é legítimo para PROVAR que o navegador aceita cookies antes de autenticar — mas era cobrado de toda requisição, inclusive GET de página pública. A função passou a ter três desfechos explícitos: crawler (nada acontece), padrão (Set-Cookie na própria resposta, sem 302) e $exigirSessao = true (comportamento original). Só gestor_permissao() e os fluxos de signin/signup pedem o modo forte; gestor_permissao_token_processar() e gestor_usuario_perfil() apenas LEEM o cookie e passaram ao modo silencioso.

  2. Detecção de crawler por token, não por lista fechada de User-Agents: os agentes do WhatsApp, Meta, Twitter/X e buscadores carregam versão, URL de contato e sufixos que mudam sem aviso. Comparar substring em caixa baixa contra 29 tokens conhecidos é estável; uma lista de UA completos quebraria na primeira atualização deles. O resultado é memoizado por request (gestor_requisicao_crawler()), porque a checagem aparece em quatro pontos do bootstrap.

  3. Crawler em página protegida recebe <head> e nada mais: as duas alternativas óbvias eram ruins — redirecionar para /signin/ faz o preview do link exibir a tela de login, e servir a página seria vazamento de conteúdo privado. A resposta é 200 com título, robots: noindex e as tags OpenGraph, corpo vazio, e o módulo da página nem chega a ser incluído (a interceptação acontece antes de gestor_permissao()). Visitante humano deslogado segue para /signin/ com o retorno preservado, sem mudança.

  4. Bloqueio de rastreamento REMOVE o bloco, não deixa de adicioná-lo: o snippet de GTM/Meta Pixel quase sempre vem do html_extra_head da página ou do layout, ou seja, do BANCO — o core não tem como "não incluir" o que não foi ele que enfileirou. gestor_rastreamento_remover() varre as filas de head/JS no último ponto antes da serialização e apaga os blocos por assinatura. Registro honesto de limite: não existe nenhuma linha de GTM/Meta Pixel no repositório conn2flow (varredura em gestor/, conn2flow-site e [REDACTED]), então a deduplicação do ID do Pixel pedida no intake não tem onde ser feita no core; o que o core entrega é o mecanismo genérico mais o gestor.rastreamentoBloqueado, que transforma fbq/dataLayer/gtag em no-op antes de o snippet do projeto rodar — sem sobrescrever coletor já carregado.

  5. A causa-raiz do 403 no salvamento do Editor Visual não era a barra dupla: $.formSubmitNormal() chama $(form).submit(), e o jQuery percorre uma propagação SIMULADA (dispatch próprio, não dispatchEvent) que não aciona listeners nativos; a ação padrão dele é HTMLFormElement.prototype.submit(), que também não dispara evento submit algum. O document.addEventListener('submit', …, true) do BATCH-107 — única proteção até aqui — nunca era acionado nesse caminho, e o campo _csrf_token jamais era anexado. A correção cobre os TRÊS caminhos: captura nativa (usuário), handler delegado do jQuery (trigger) e envelope de HTMLFormElement.prototype.submit (rede final), este instalado uma única vez e marcado por __c2fCsrf para o asset carregado duas vezes não empilhar envelopes.

  6. O token precisa ser buscado fora do <head> quando o contexto é srcdoc: o iframe do preview herda a origem, mas não o <head> da página hospedeira — não tem a <meta name="csrf-token"> nem o global.js. csrfToken() passou a procurar também em window.gestor.csrfToken e window.parent.gestor.csrfToken, e o renderWidgets envia o token no cabeçalho e no corpo (seguranca_csrf_token_requisicao() aceita os dois; o cabeçalho sobrevive a proxy que reescreva o corpo).

  7. _csrf_token continua sendo o nome do campo: o intake pede _gestor-csrf-token, mas o contrato do backend nasceu no BATCH-107 e lê _csrf_token/X-CSRF-Token. Um segundo nome exigiria mudar o validador e reprocessar todos os formulários já existentes, sem ganho de segurança.

  8. Falha de CSRF em navegação normal deixou de ser JSON cru: o usuário do Editor Visual via {"status":"error","message":"Token CSRF inválido ou ausente."} em tela cheia e concluía que perdera o trabalho. gestor_csrf_resposta_invalida() preserva o JSON para clientes AJAX (contrato intacto) e devolve uma página "Sessão expirada" com botão de voltar para navegação — o conteúdo do formulário fica preservado pelo histórico. No front, o editor ainda BLOQUEIA o envio quando não há token, avisando antes de disparar um POST condenado.

  9. moduloUrl() como fonte única da URL do módulo no editor: gestor.moduloCaminho já chega do backend com a barra final (rtrim($caminho,'/').'/' em gestor.php), então o + '/' espalhado pelo arquivo produzia admin-paginas/editar//. A normalização foi centralizada nos três pontos do editor (ajaxDefault, render-vars e o renderWidgets do srcdoc). O MESMO padrão existe em vários módulos (admin-ia, admin-plugins, interface.js…), mas eles não fazem parte deste slice — trocar todos seria refactor amplo sem mudança normativa, e o roteador tolera a barra extra.

  10. OpenGraph nasce no core com fallback, e some quando a página já tem o seu: og:image vazio faz o WhatsApp exibir o card SEM imagem em vez de recorrer ao padrão do site, então tag sem valor simplesmente não é emitida. E se a página/layout já traz OpenGraph gravado à mão no html_extra_head, o core não injeta o seu — duas og:title fazem o scraper escolher arbitrariamente. Os valores vêm de $_GESTOR['pagina#og'], um ponto de extensão que o CRUD do req-110 vai preencher sem tocar nesta camada; até lá, o fallback é nome da página + site-name/site-description/site-og-image do config.php.

Validação: php -l 6/6, node --check 2/2, bash -n 2/2, git diff --check OK, composer test 200/200 (novo CrawlersOpenGraphTest 15/15), npx vitest run 171/171 (novos global-csrf.test.js 14/14 e html-editor-csrf-url.test.js 14/14). Deploy Update => Core e homologação runtime pendentes com o operador. Nenhum git commit/git push executado.

DEC-105 - 2026-08-13 - accepted

Metadados de compartilhamento por página, aba de SEO no Editor HTML, painel de Configurações na Editbar e sitemap.xml incremental (req-110 / BATCH-110). Decisões desta rodada:

  1. O que se grava é o CAMINHO do arquivo, não o id_arquivos: desde o BATCH-090 a árvore de arquivos é física e o registro no banco é opcional — o imagepick da interface.php já resolve miniatura, nome e tipo pelos metadados do arquivo em disco quando não existe linha em arquivos. Guardar o id numérico criaria uma dependência que o próprio gerenciador não garante mais.

  2. Chave ausente ≠ chave vazia: gestor_pagina_og_do_registro() devolve SOMENTE as colunas preenchidas. Se devolvesse ['title' => ''] para uma página sem título social, gestor_open_graph_dados() (BATCH-109) veria a chave presente e ela venceria o nome da página — o fallback deixaria de funcionar exatamente nas páginas que mais dependem dele. Do outro lado, na EDIÇÃO o valor vazio precisa ser gravado (o usuário está removendo o título social de propósito), então a comparação usa o valor do request sempre presente, e não isset.

  3. A aba de SEO só existe quando o chamador a pede: html_editor_componente() recebe seo e, sem ele, remove menu e conteúdo por modelo_tag_del — o mesmo mecanismo já usado para os blocos do publisher. Assim layouts, componentes e todos os demais alvos continuam com exatamente o conjunto de abas de antes, sem condicional espalhada pelo JS.

  4. Nenhum código novo de seletor de imagem no editor clássico: o imagepick de interface_formulario_campos() substitui <span>#imagepick-featured-image#</span> DEPOIS que o componente do editor já foi inserido em $_GESTOR['pagina']. Bastou declarar o campo no módulo e deixar o placeholder no componente — o modal, o iframe do gerenciador e o preview vieram de graça.

  5. O painel da Editbar vive na página HOSPEDEIRA, não no iframe da barra: é a mesma razão dos painéis do BATCH-106, agravada aqui — o seletor de arquivos precisa cobrir o conteúdo; dentro do iframe da Editbar ele ficaria confinado à altura da barra. A barra posta a posição do botão e o page_id; o [REDACTED], carrega e salva.

  6. dismissHostPanels() ganhou uma exceção deliberada: com o seletor de arquivos aberto, o painel de configurações NÃO é fechado. O clique que escolhe o arquivo acontece dentro do iframe do gerenciador e propagaria o ui-dismiss; fechar ali apagaria o título e a descrição já digitados. É a única exceção à regra "clicou na barra, fechou tudo" do DEC-103 §14, e ela é temporária — dura só enquanto o overlay está aberto.

  7. O sitemap é atualizado INCREMENTALMENTE, por manipulação de string: o CRUD de páginas é chamado uma vez por edição; regenerar o arquivo inteiro a cada salvamento significaria uma varredura da tabela por clique, visível no tempo de resposta do painel num site grande. O upsert remove o bloco <url> da URL alvo e reinsere antes de </urlset>, preservando byte a byte tudo que não mudou — inclusive entradas que o operador tenha acrescentado à mão. DOM/SimpleXML reescreveriam o documento inteiro e destruiriam essas edições. Geração completa continua existindo para dois casos: arquivo ausente (primeira execução) e arquivo corrompido/sem <urlset>.

  8. Elegibilidade reaproveita a lista de rotas de sistema do BATCH-109: sitemap_pagina_elegivel() chama gestor_pagina_sistema_sem_rastreamento() em vez de manter uma segunda lista. São a mesma pergunta feita duas vezes ("isto é página de conteúdo?"), e duas listas divergiriam.

  9. Falha de sitemap nunca derruba o CRUD: é artefato derivado e regenerável. Erro de escrita vai para log_disco e a gravação da página segue. O mesmo vale para a exceção capturada no callback.

  10. Status e exclusão entram por callbackFunction, não por alteração da interface: as duas operações são executadas pela biblioteca compartilhada, que já dispara o callback depois de gravar e antes de redirecionar. Alterar interface_status_finalizar/interface_excluir_finalizar afetaria todos os módulos do sistema por causa de um requisito de um só.

  11. publisher-pages ficou de fora, com motivo: as publicações gravam na MESMA tabela paginas (vinculadas por publisher_id), então as colunas novas já valem para elas e o roteador já as lê; o painel da Editbar edita qualquer página, inclusive as de publicação. O que falta é só a aba de SEO no formulário daquele módulo — um slice pequeno e isolado, que merece batch próprio em vez de misturar duas superfícies de CRUD na mesma validação.

Validação: php -l 10/10, node --check 4/4, JSON OK, git diff --check OK, composer test 223/223 (novo SitemapTest 19/19 + 4 casos em CrawlersOpenGraphTest), npx vitest run 181/181 (novo dashboard.page-config.test.js 10/10). Migração Phinx e deploy Update => Core pendentes com o operador — a migração precisa vir ANTES do deploy, senão o SELECT do roteador falha. Nenhum git commit/git push executado.

DEC-106 - 2026-08-13 - accepted

Reversão do bloqueio de analytics e fim do laço de verificação de cookie (CR-001 / BATCH-111). Decisões desta rodada:

  1. O requisito estava errado, não a implementação: o req-109 §3/§4 pedia bloquear analytics em páginas de sistema, partindo da leitura de que a cookies-is-mandatory/ aparecia nos relatórios porque o coletor rodava nela. É o contrário: o coletor não escolheu ir lá — todo cliente sem cookie era empurrado para lá. Bots de analytics são stateless por definição. Bloquear o script trata o sintoma no lugar errado e ainda joga fora sinal legítimo (404 e 500 são exatamente o que se quer ver no relatório). Decisão do Engenheiro Chefe: os coletores continuam acessando tudo.

  2. A evidência veio de medição, não de leitura de código: curl contra os dois domínios de produção mostrou navegador real chegando a 200 em 2 saltos e cliente stateless — incluindo o Googlebot — em laço infinito. O curl aborta por excesso de redirects. Registrar isto importa porque a conclusão inverteu o diagnóstico do intake, e inverter requisito exige prova, não opinião.

  3. A causa do laço é a página explicar o problema estando dentro dele: cookies-is-mandatory/ é uma página como qualquer outra e, ao ser renderizada, chama gestor_cookie_verificacao() de novo. A trava adotada é a mais simples que fecha o ciclo: rota de sistema nunca redireciona — emite o cookie e devolve a página. Preferida a alternativas com marcador de tentativa na URL, que exigiriam carregar estado por query string justamente no fluxo de quem não carrega estado.

  4. A decisão virou função pura de propósito: gestor_cookie_verificacao_desfecho() existe para ser testável. A diretriz de blindagem de bugs da Chefia pede que defeito identificado nasça com teste que falhe se ele voltar; enquanto a regra vivia dentro de gestor_cookie_verificacao(), no gestor/gestor.php (que executa gestor_start() no fim do arquivo e não é carregável em teste), isso era impossível. São 5 casos cobrindo o laço.

  5. O round-trip saiu de gestor_permissao(): a chamada que o BATCH-109 acrescentou ali era redundante. Quem chega numa página protegida sem sessão é mandado para /signin/ duas linhas abaixo, e é o /signin/ que faz a prova; quem chega com sessão já tem o cookie de autenticação e a função retornaria sem efeito. O único resultado prático era um salto a mais antes do login.

  6. Rota de sistema sai do índice: enquanto o laço existiu, cookies-is-mandatory/ foi entregue a buscador e a coletor no lugar da página pedida — e ela é tipo=page com sem_permissao, ou seja, nada impedia a indexação. noindex, nofollow no <head> e X-Robots-Tag no cabeçalho tiram o que já entrou e evitam que uma reincidência volte a poluir relatório e busca.

  7. A detecção de robô deixou de ser peça crítica — e essa é a resposta ao "correr atrás do rabo do cachorro". Com o laço fechado, cliente sem cookie recebe a página pública seja ele reconhecido ou não. Lista de tokens é, por natureza, sempre desatualizada; o que muda o jogo é a correção estrutural, não o cadastro. Uma tela de configuração sem a correção só mudaria o lugar onde a caçada é anotada.

  8. Mesmo assim a lista continua existindo, em duas camadas: a detecção ainda serve ao OUTRO uso, entregar só o <head> com OpenGraph em página protegida (req-109 §10). Por isso o baseline fica embutido e sempre ativo — desligar tudo por padrão faria o preview de link de página protegida voltar a exibir a tela de login, regressão silenciosa do que o BATCH-109 consertou. O baseline é composto de identificadores estáveis (WhatsApp, facebookexternalhit, Googlebot não mudam de nome há anos) e foi ampliado de 29 para 50 tokens com os bots de anúncio e auditoria que faltavam. A lista extra configurável, desligada por padrão, atende o caso levantado pelo Chefe: site com fluxo alto de um robô específico, resolvido sem release do core.

  9. Rename honesto: gestor_pagina_sistema_sem_rastreamento() virou gestor_pagina_rota_sistema(). Sem o bloqueio, o nome antigo descrevia um comportamento que deixou de existir. A função sobrevive porque o sitemap_pagina_elegivel() (BATCH-110) a usa para excluir rotas utilitárias do sitemap — duas perguntas diferentes sobre a mesma lista, e uma lista só.

Validação: php -l OK, node --check OK, JSON OK, git diff --check OK, composer test 229/229 (CrawlersOpenGraphTest 15→25), npx vitest run 178/178 (os 3 casos do bloqueio saíram junto com o código). Deploy Update => Core pendente e prioritário: é ele que tira os sites do laço. Nenhum git commit/git push executado.

Adendo ao DEC-104 (item 4)

O item 4 do DEC-104 — bloqueio de rastreamento em páginas de sistema — está REVERTIDO por este DEC-106 e pelo CR-001. O raciocínio original ("o snippet vem do banco, então filtre a fila em vez de não adicionar") era tecnicamente correto, mas resolvia um problema que não existia: o analytics não era a causa do sintoma, era mais uma vítima do laço de redirecionamento.

DEC-107 - 2026-08-14 - accepted

Sitemap em assets, correção do 301, aba SEO no publisher-pages, isolamento do painel da Editbar e meta tags de SEO (req-112 / BATCH-112). Decisões desta rodada:

  1. assets/ em vez da raiz, porque a raiz dependia de configuração externa: na raiz o sitemap.xml só era servido pela regra RewriteCond %{SCRIPT_FILENAME} !-f do .htaccess — que varia por instalação e some em servidor com roteador mais restritivo. Em assets/ quem entrega é o arquivo-estatico.php, que é parte do core e vale em toda instalação. Achado importante: o default: do switch de extensão já resolve qualquer extensão desconhecida contra assets-path, e xml já estava na tabela de Content-Type — nenhuma linha do controlador ou do .htaccess precisou mudar. Só a função de caminho.

  2. Abrir o sitemap para tipo='sistema' exigiu uma segunda peneira: o intake pede que páginas públicas de sistema entrem, citando /signin/, /signup/ e /recover-password/. Um levantamento das páginas públicas do core mostrou que a maioria delas NÃO é conteúdo: oauth-callback, oauth-authenticate, social-login, signin-2fa, validate-user, email-confirmation, forms-submissions-process, pagina-de-impressao, dashboard-site-toolbar, além das telas de confirmação. Indexar isso gasta orçamento de rastreio e leva o visitante a tela sem contexto — callback então só faz sentido dentro de um fluxo com parâmetros. Daí a sitemap_caminho_nao_indexavel(), que barra por lista exata + dois sufixos (-confirmation, /success) + prefixo admin-. As três rotas nomeadas no intake continuam entrando.

  3. interface_modulo_variavel_valor() era o defeito do 301, não a chamada: essa helper chama gestor_redirecionar_raiz() quando não encontra o registro. Num CRUD isso é um exit no meio da gravação — o 301 nunca era inserido, e o usuário era jogado para a raiz sem entender por quê. Ela ainda aplica um filtro por id_hosts que não corresponde a estes módulos. A troca por um banco_select_name direto elimina os dois problemas e permite tratar a ausência do id como o que ela é: um caso a registrar em log, não a derrubar a requisição. Aproveitada a passagem para impedir duplicata em paginas_301 — A → B → A → B gerava duas linhas idênticas.

  4. A correção do "event bleed-through" não era CSS: o intake descreve pointer-events, z-index e stopPropagation, e todos ajudam — mas a causa-raiz era c2f-page-config-panel e c2f-page-config-picker não estarem em isEditorOwned(), omissão minha no BATCH-110. O motor decide o que é UI por elementsFromPoint + isEditorOwned; fora dessa lista, o painel é tratado como conteúdo, o hover realça o elemento atrás e o primeiro clique vira seleção. A memória de execução do BATCH-106 já registrava a regra — "todo elemento novo de UI precisa entrar em três lugares" — e ela não foi seguida na entrega anterior.

  5. A barreira de eventos fica na BOLHA, nunca na captura: stopPropagation() num listener de captura no painel impediria o evento de chegar ao próprio botão dentro dele — quebrando exatamente o que o intake pede para consertar. Cheguei a escrever em captura e corrigi antes de validar; há teste cobrindo os dois lados (evento não vaza para o documento E o botão responde).

  6. description e keywords são campos separados do OpenGraph, não os mesmos: buscador lê description, rede social lê og:description, e um site pode querer textos diferentes para cada público (um objetivo, outro persuasivo). O fallback em cascata evita retrabalho para quem não quer essa distinção: metadado próprio → og_descricao da própria página → config.php.

  7. keywords só é emitida quando preenchida: nenhum buscador relevante a usa para ranquear há anos. Mantê-la é atender ao intake e a quem tem processo interno que a exige; emiti-la vazia seria só ruído no <head>.

  8. A Editbar é a terceira superfície de gravação e precisava do mesmo gatilho (levantado pelo Chefe durante a rodada): verifiquei que dashboard_ajax_site_toolbar_save() grava apenas os campos de conteúdo — não sobrescreve os metadados de SEO nem toca no caminho —, então não havia risco de perda de dado nem necessidade de 301 ali. O que faltava era o <lastmod>: ela atualiza data_modificacao e não sincronizava o sitemap, justamente na superfície de edição mais usada.

Validação: php -l 10/10, node --check 2/2, JSON OK, git diff --check OK, composer test 241/241, npx vitest run 181/181. Migração Phinx precisa rodar ANTES do deploy Update => Core. Nenhum git commit/git push executado.

Adendo DEC-107 (rodada 2 — homologação do Chefe, mesma data)

  1. html-editor.js tem DOIS cache-busts independentes, e eu tratei só um: o editor clássico é servido com ?v={biblioteca-html-editor.versao}; o Live Editor carrega o MESMO arquivo físico com uma string FIXA ?v=c2fNN escrita em dashboard.toolbar.js. Bumpar a versão da biblioteca — que foi o que o Chefe tentou — não alcança o Live Editor. A correção do M5 estava no arquivo desde o início e simplesmente nunca era baixada. Regra que fica: alterar html-editor.js exige bumpar os DOIS.

  2. Asset de módulo incluído sem versao herda a versão do SISTEMA: dashboard.toolbar.js era injetado por gestor_pagina_javascript_incluir() sem a chave versao, e a helper cai em $_GESTOR['versao'] — que só muda a cada release. Na prática, nenhuma alteração nesse arquivo chegava ao navegador entre releases, mesmo com deploy feito. Vale conferir o mesmo padrão em outros assets de módulo incluídos por array.

  3. Cache-bust do editor visual unificado na BIBLIOTECA (decisão do Chefe): a primeira correção usou a versão do módulo dashboard. O Chefe apontou o desenho melhor — usar biblioteca-html-editor, porque a Editbar e o motor mudam JUNTOS: mexer na barra quase sempre implica mexer no editor. Assim gestor/bibliotecas/html-editor.php vira o cache-bust ÚNICO dos três consumidores (motor no clássico, toolbar.js, e o motor carregado por dentro da Editbar via a variável JS gestor.htmlEditorVersao). A string fixa foi ELIMINADA: número escrito à mão, num arquivo diferente do que se está editando, é exatamente o que falhou nesta rodada.

  4. Sintoma que não muda após correção é hipótese sobre ENTREGA, não sobre lógica: a segunda rodada foi resolvida sem tocar na regra — só descobrindo por que o arquivo não chegava. Antes de reescrever lógica que parece correta, confirmar no DevTools qual URL (e qual ?v=) o navegador está de fato pedindo.

DEC-108 - 2026-08-14 - accepted

Extração incremental dos bridges privados do BATCH-115:

  1. Página user_modified não pode ser pré-requisito para receber templates novos: os fragmentos da busca clínica são anexados pelo controlador depois da montagem da página. O recurso e seu css_precompiled chegam mesmo quando o HTML autoral é preservado pelo sincronizador.

  2. Um componente pode ser um catálogo funcional de templates, mas não um catálogo de classes: busca-clinica-runtime-fragments contém os sete DOMs realmente clonados pela aplicação. Isso é diferente do bridge antigo, cujo HTML era só comentário e existia para fazer o compilador ler o JavaScript.

  3. Fragmento PHP selecionado no servidor deve carregar só seu próprio sidecar: os três estados do preço do checkout viraram componentes distintos. Um plano gratuito não precisa baixar as utilities exclusivas do card sob medida ou pago.

  4. [REDACTED]-system permanece coberto até a última família ser migrada: o asset compartilhado concentra dezenas de renderizadores de seis telas. Remover tailwind_sources após migrar apenas uma família produziria uma falsa conclusão e novas perdas visuais; a remoção será o último passo.

  5. Escala de navegador não deve virar regra de tema: o aparente aumento local era zoom de 110%. O projeto permanece com o padrão de 16px e deve ser homologado com zoom do navegador em 100%.

DEC-109 - 2026-08-14 - accepted

Bundle Tailwind canônico e opt-in por página (req-115 / BATCH-115):

  1. Sidecar correto isoladamente não implica cascata correta em conjunto: cada compilação possui apenas um subconjunto das utilities. Ao concatená-las, a ordem entre bundles passa a valer mais que a ordem canônica do Tailwind; .hidden posterior anulou lg:flex do layout na Busca Clínica.

  2. A unidade de entrega visual é a rota, não a soma cega dos recursos: o artefato continua armazenado por recurso, mas uma rota migrada compila layout, página e dependências declaradas em um único page-precompiled. Isso preserva o benefício de CSS pequeno sem voltar ao antigo system-output.css global.

  3. Migração obrigatoriamente opt-in: o runtime só elimina sidecars isolados quando o módulo define tailwind-page-bundle e existe um page-precompiled. Rotas não inventariadas continuam no contrato anterior; ativar globalmente esconderia dependências ainda não declaradas.

  4. CSS autoral e CSS do editor não são descartados: a seleção afeta somente a faixa css-precompiled. css, css_compiled, CSS de projeto e css-fim mantêm sua precedência e seu propósito.

  5. Template escolhido dinamicamente é dependência da página: todos os templates elegíveis precisam entrar nas fontes do bundle. O PHP também deve carregar templates.css_precompiled no fallback sem bundle; omitir o campo foi a causa da imagem clínica sem max-h-56.

DEC-110 - 2026-08-14 - accepted

Contrato de camadas para bundles Tailwind canônicos (req-115 / BATCH-115):

  1. Bundle que substitui layout precisa substituir também suas camadas globais: compilar somente utilities é correto para sidecar isolado, mas incorreto para page-precompiled que elimina o layout da resposta. O bundle deve carregar tema, base e Preflight do input central.

  2. tailwind_sources não implica bundle: a propriedade também serve a pontes temporárias de PHP/JavaScript. A intenção de substituir a faixa pré-compilada inteira fica explícita em tailwind_bundle: true, restrita a páginas e exigindo fontes adicionais declaradas.

  3. A Editbar é uma rota independente: seu iframe tem página, layout e componentes próprios; portanto recebe seu próprio bundle canônico. Não deve herdar nem depender do bundle da página hospedeira.

  4. Dependência entre recursos é semântica, não um caminho de resources/: produção remove as árvores físicas. Componentes, layouts e templates entram no metadado por type, module, language e id; o compilador traduz para arquivo apenas durante o build. O runtime continua usando gestor_componente(), gestor_layout() e tabelas do banco.

  5. tailwind_sources fica reservado a fonte bruta transitória: PHP/JavaScript ainda não extraído pode permanecer como fonte auditada. Assim que o HTML vira recurso do Gestor, a ponte por caminho deve ser substituída por tailwind_dependencies.

DEC-111 - 2026-08-17 - accepted

Captura do css_compiled e política de delta contra a cascata real (req-117 / BATCH-117):

  1. A folha do Tailwind Browser é reconhecida pelo FORMATO, nunca pela posição: ela é criada vazia (document.head.append) e só recebe conteúdo quando o build assíncrono termina. "A última <style> do <head> com regras" escolhia qualquer folha injetada em runtime — o motor injeta 4 delas no mesmo documento. A assinatura válida são as camadas nomeadas da v4 (theme/base/components/utilities/properties).

  2. Todo <style> que o sistema emite declara seu papel: data-c2f-tailwind-role para o que o editor injeta, data-tailwind-role para os pré-compilados offline e data-c2f-css-role para as filas de CSS autoral e css_compiled da página pública. Folha sem papel declarado é candidata a ser confundida com a saída do compilador — e o css_compiled anterior é o caso mais perigoso, porque contém @layer utilities e congelaria a captura no valor já gravado.

  3. O css_compiled grava o DELTA contra o que a página recebe no runtime, não uma quantidade fixa: o baseline do editor é a cascata (layout + dependências + recurso), e não apenas o pré-compilado do próprio recurso. Página sob layout pré-compilado grava só as utilities novas; página sem cascata offline grava o output completo e continua autossuficiente. A decisão é do dado, sem flag. Medido na página sobre do photon: 65% de redução.

  4. Camada de fundação é decidida por CAMADA; utilities, por regra: theme e base não dependem do conteúdo — o compilador as emite inteiras a partir do contrato — e o filtro por assinatura não as segura, porque build offline e runtime do browser divergem em detalhes de seletor (::file-selector-button no Preflight do 4.3.0). Como o css_compiled entra depois do pré-compilado na cascata, a versão do editor venceria a do build em produção. Divergência entre as duas versões é assunto de recompilar o bundle, não de remendar página a página.

  5. Captura incompleta nunca sobrescreve o valor gravado: esgotada a janela de polling, o editor clássico preserva o conteúdo do CodeMirror e a Editbar omite o campo do POST. Gravar o resultado parcial é exatamente o defeito que fez páginas inteiras perderem as utilities.

  6. HTML do painel de Código é aplicado por ação explícita, CSS ao vivo: reescrever #c2f-page-content recria os nós e derruba as anotações do mapeamento in-place (data-c2f-variable, .c2f-dyn-box). Ao vivo, a cada tecla, isso destruiria a edição em curso.

  7. Guarda de build avisa por padrão e só quebra com --tailwind-strict: os defeitos cobertos (F1/F4c) são silenciosos e antigos; transformá-los em erro de imediato pararia builds que hoje passam. Heurística de detecção é calibrada contra o inventário real — um aviso que soa em 176 recursos é um aviso que ninguém lê.

DEC-112 - 2026-08-17 - accepted

Fechamento dos findings do review de 2026-08-15 (BATCH-118):

  1. Descarte silencioso é defeito de diagnóstico, não só de estilo: em modo bundle, sidecar resource-precompiled descartado passa a ser logado. Layout e dependências continuam saindo em silêncio porque o descarte deles é intencional; o bucket ANÔNIMO é o único que denuncia recurso escolhido em runtime, que nasceu fora do bundle declarado em build-time.

  2. Catálogo de recurso selecionável em runtime e bundle canônico são incompatíveis sem declaração explícita: enquanto a dependência não puder ser declarada por target, toda rota com bundle que escolha template por consulta ao banco precisa listar os ids elegíveis.

  3. O compilador avisa o que só ele consegue ver: ele conhece o CSS que gerou para cada layout e a declaração de cada página. Layout que emite display sob variante responsiva com concorrente incondicional, servindo página sem bundle, é conflito de ordem em potencial — e o sintoma (menu invertido) leva a suspeitar do markup, que está correto.

  4. Papel de pré-compilado é contrato de ORDEM, não anotação de diagnóstico: os includes de gestor_layout() sem papel caíam no default resource-precompiled e iam para o fim da cascata, depois das dependências e da página. CSS de layout carrega theme, base e Preflight: a ordem invertida é um defeito visual, não uma imprecisão de rótulo.

  5. Heurística de nomenclatura se calibra contra o inventário real: a regra de "página de confirmação" cobria …/success enquanto as páginas se chamavam …-success. Sufixo com hífen, underscore e barra; e etapa de fluxo (payment) só é barrada em caminho composto, porque na raiz pode ser conteúdo legítimo.

  6. Artefato derivado que mudou de lugar tem de apagar o antigo: o sitemap.xml na raiz sobreviveu à mudança para assets/ e, em instalação onde a regra !-f resolva antes do roteador, seria servido para sempre. A remoção só acontece sob trava de autoria — arquivo de terceiro é preservado.

  7. robots.txt e sitemap derivam da MESMA lista de rotas: o noindex só é visto depois do rastreio; o robots.txt é o único ponto que barra antes. Alimentá-lo por outra fonte abriria a chance de liberar o que o sitemap esconde.

  8. Dedup precisa do escopo inteiro da chave: paginas_301 deduplicava por caminho, mas a chave real é (caminho, id_paginas) — sem id_paginas, o registro de um idioma bloqueava o do outro e caminho reciclado continuava apontando para a página anterior. Na leitura, o desempate é pelo mais recente que resolva no idioma corrente.

  9. Camada de tema se filtra por DECLARAÇÃO, nunca por camada inteira (revisa o item 4 da DEC-111): o Tailwind v4 só emite a variável que vê usada, então o @theme do layout tem apenas os tokens do LAYOUT. Descartá-lo inteiro deixa as utilities novas da página com var() indefinida — 13 de 21 elementos mudaram de aparência ao salvar. Só @layer base (Preflight), que não depende do conteúdo, pode ser decidido por camada.

DEC-113 - 2026-08-18 - accepted

Título: Base administrativa Tailwind — resolução por layout + página, variantes de componente e runtime próprio da interface (req-118 / BATCH-119)

Contexto: o intake pedia o painel de perfil modernizado; o Chefe ampliou o escopo para um layout administrativo nativo em Tailwind e decidiu, na mesma sessão, que ele não carregaria o Fomantic — a linha 3.0 do sistema será 100% Tailwind, e a adaptação começa por aqui.

O bloqueio não era o CSS: era o runtime. interface.js chama $.fn.modal do Fomantic em 16 pontos sem nenhuma guarda, e entre eles está o modal de Área Restrita, que é a trava de credenciais do próprio perfil. Além disso, gestor_pagina_css() e gestor_pagina_extra_head_e_javascript() usavam a mesma condição para o CSS e o JS do Fomantic — um layout tailwindcss perde os dois de uma vez.

Decisão:

  1. A resolução de framework sai de layout E página, nunca de um lado só. Uma página final é layout + página, e as duas carregam a coluna framework_css. gestor_framework_css_resolver() é pura e devolve três modos: fomantic-ui, tailwindcss e hibrido. As regras são as que o core já aplicava; o ganho é haver um lugar só onde a decisão mora, e um teste que a falsifica.

  2. Variante de componente por sufixo, com o legado intocado. Cada componente da interface ganha um irmão <id>-tailwind, escolhido em runtime. Em modo hibrido a variante não é aplicada — com o Fomantic na página, o componente legado é o único com estilo garantido. Os switch que decidem quais variáveis cada componente recebe continuam escritos sobre o id canônico: a variante muda o HTML, nunca o contrato de dados.

  3. O contrato de validação é portável; o runtime é que muda. interface_formulario_validacao() emite regras no formato do validador do Fomantic, e o inventário inteiro do core usa apenas 6 tipos (notEmpty, minLength[n], maxLength[n], email, match[campo], regExp[/…/]). interface-tailwind.js interpreta esse mesmo dicionário — nenhuma função PHP de validação precisou mudar. Tipo desconhecido é ignorado em vez de reprovar: o legado tem regras que este runtime ainda não precisa, e travar formulário válido por desconhecimento seria pior que o defeito.

  4. A folha de ÍCONES do Fomantic fica; o framework sai. A coluna modulos.icone guarda nomes de ícone Fomantic para todos os módulos instalados. Trocar essa fonte exigiria reescrever o cadastro inteiro e quebraria plugins de terceiros. O layout novo carrega dist/components/icon.min.css e nada mais — o CSS de layout continua 100% Tailwind.

  5. Bundle canônico no painel administrativo não é preferência, é o F3 do review de 2026-08-15. Sem tailwind_bundle, o compilador acusou que o layout emite display sob variante responsiva (lg:block, lg:hidden, sm:inline) e a concatenação de sidecars pode inverter desktop/mobile. Com o bundle o aviso desaparece e a ordem global das utilities fica preservada.

  6. O modificador ! do Tailwind v4 fica nos controles de formulário. No modo hibrido, .ui.form input[type="text"] tem especificidade (0,3,1) contra (0,1,0) de .px-3 — ordem na cascata não resolve. Em Tailwind puro ele é redundante (o Preflight já normaliza), mas inofensivo, e é o que sustenta a página quando ela é servida por um layout Fomantic.

Consequências:

  • O painel administrativo passa a ter duas bases coexistindo, com fronteira explícita e testada. Os módulos ainda não migrados continuam exatamente como estavam, no layout legado.

  • interface.php foi adaptada apenas nos caminhos exercitados por este lote (edição, componentes de modal, runtime e autorização provisória), conforme instrução do Chefe. Listagem, inclusão, visualização e configuração seguem no componente legado — servidas pelo layout legado, sem regressão. Migrá-las é o caminho natural dos próximos lotes.

  • A dívida "decisão de arquitetura sobre Tailwind no painel administrativo", registrada no review de 2026-08-15, está fechada.


O Futuro é Híbrido: Humano + IA.

O Conn2Flow é a ferramenta. Se você precisa do Arquiteto para desenhar a estratégia da sua empresa usando essa tecnologia...

Contrate Consultoria Especializada