# 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:
-
O 403 era da reescrita do Apache, não do PHP nem da codificação da URL: medido com
curlno container (extensão sem espaço → 404 normal; qualquer extensão com%20→ 403) e confirmado nolocalhost-error.log:AH10411: Rewritten query string contains control characters or spaces. O.htaccesspassa 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.htaccessdo instalador e nos sites locais criados recentemente; apenas o mirror local antigo (arquivo emdev-environment/data/, git-ignored) estava defasado. Nenhuma mudança de.htaccessversionado foi necessária. -
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 emfile_existse nas associações de categoria do BATCH-090) — codificar o identificador quebraria a resolução no servidor; só a URL de exibição é codificada. -
Streaming de mídia é requisito, não refinamento: o servidor de estáticos respondia com
readfile()cru —Transfer-Encoding: chunked, semContent-Length, semAccept-Rangese ignorando oRangepedido. 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 comContent-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 guardSDD_NO_AUTORUNpara ser incluído em teste sem disparar o envio. -
charsetsó 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 (fallbackapplication/octet-stream). -
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 dissobuildMediaMarkup()nunca emiteheightpara áudio eapplyEmbedSize()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:
-
src=""é pior que ausência desrc: 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 osrcdo Google Viewer (que apontaria para um viewer comurl=vazia). -
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 nosrc=""— e o caso quebrado ainda traziaposition:relative; z-index:1com espaço após o;, assinatura dostyleExtrareconstruído porembedReadConfig(join('; ')), provando que ele passara pelo "Aplicar" do modal enquanto o correto viera apenas dobuildElement. A troca de section relatada era coincidência. -
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:
-
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. -
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. -
A listagem passou a aceitar
?dir=: antes o PHP fixavadirInicial => ''na listagem. Agora expõedirExplicito/dirIniciala partir de$_REQUEST['dir'], saneado porarquivo_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. -
Fluxo do picker fica fechado: navegar até a pasta → "Adicionar" (com
?dir=, explícito) → enviar → voltar pela seta (semdir) → 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 emadmin_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:
-
Comportamento portado da Editbar, adaptado à estrutura do menu: o
c2f-modules-filteresconde 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 > .itemque já contém o cabeçalho e seus links —, então basta ocultar o bloco quando nenhuma.iteminterno 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. -
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-sistemajá é versionado por idioma (resources/pt-breresources/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. -
O asset acompanha o HTML do menu, e não a fila de assets:
gestor_pagina_menu()é chamada porgestor_pagina_variaveis()(gestor.php:619), que roda DEPOIS degestor_pagina_extra_head_e_javascript(). Nesse ponto o marcador de JS da página já foi substituído, entãogestor_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 emgestor_pagina_extra_head_e_javascriptcom guarda de usuário logado. -
Regex de acentuação montado por código: o range de marcas combinantes é construído com
String.fromCharCode(0x300)/0x36fem 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). -
API pública para teste:
window.gestorMenuFiltro.aplicar(termo, escopo)einiciar()(idempotente, marcada por atributo no campo), mesmo padrão dewindow.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)
-
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.jse a busca de modelos emhtml-editor-interface.js), todos usando o mesmo helper com o range montado porString.fromCharCode. Lição registrada: ao portar comportamento, avaliar a base antes de copiá-la — o padrão existente pode ser o defeito. -
Botão 'x' de limpar padronizado:
#modelos-search-inputera 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 porhiddendo Fomantic; o clique limpa, refiltra e devolve o foco ao campo. Os textos (title) ficam nos componentes por idioma. -
Salto de rolagem no 'Carregar Mais' era o
hide()da lista:modelosCarregarescondia#modelos-cardsenquanto 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 ohide()à 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/restaurarscrollTop, 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:
-
Geração de Client Token OAuth: criação da função
paypal_gerar_client_token($params)na biblioteca backendgestor/bibliotecas/paypal.php, requisitandoPOST /v1/identity/generate-tokencom suporte acustomer_idopcional para autorização PCI-DSS SAQ-A no navegador. -
Suporte nativo a
payment_sourceem Pedidos e Assinaturas: atualização depaypal_criar_pedidoepaypal_criar_assinaturapara aceitar a estruturapayment_source(cartões tokenizadospayment_source.carde tokenspayment_source.token). -
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. -
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 decomponents,vaultedata-client-token; renderização e estilização dos Hosted/Card Fields com feedback de validação em tempo real; e métodopaypalCardFieldsSubmitpara tokenização síncrona/assíncrona no submit do formulário.
Adendo DEC-100 — alinhamento com os contratos oficiais durante a implementação
-
data-client-tokené atributo, não query string: o loader mantémclient-id,components,intent,vaulte demais opções públicas na URL, mas grava o client token emdata-client-tokenna tag<script>, como exige o SDK. Colocar o segredo na URL aumentaria sua exposição em histórico, telemetria e logs de proxy. -
Card Fields aprova ordem; não se inventa nonce: no SDK atual,
CardFields.submit()dispara o fluxo ligado acreateOrder/onApprove. O retorno normalizado depaypalCardFieldsSubmitcontémorder_ide somente incluipayment_sourcequando o SDK ou um callback da integração realmente o fornece. O helper PHP aceitaorder_idpara captura; assim os dados PCI permanecem nos iframes PayPal. Payloads já tokenizados continuam aceitos viapayment_source.card/payment_source.token. -
Nesting correto em assinaturas: Orders recebe
payment_sourceno topo; Subscriptions recebe emsubscriber.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. -
Idempotência no pagamento direto: pedidos com
payment_sourceenviamPayPal-Request-Id(aceito por$params['request_id']ou gerado localmente) e tratam respostas 200/201.intentpassou a aceitarCAPTUREeAUTHORIZE, preservandoCAPTUREcomo padrão. -
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 emwindow.conn2flowPaypale 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:
-
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. -
Recalcular a lista visivel a cada tecla: a navegacao consulta
.c2f-menu-item/a.itemno estado atual e ignora os elementos comdisplay:none; assim nunca entra em resultado removido pelo termo digitado e nao precisa sincronizar estado adicional com o listener deinput. -
Limites sem ciclo:
ArrowDownno ultimo resultado mantem o foco;ArrowUpno 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. -
Markup preservado:
dashboard.phpe os componentes pt-br/en nao precisam detabindex, 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:
-
Promocao unificada dos 10 itens do backlog (
BL-001aBL-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. -
CSPRNG obrigatorio em tokens e sessoes: substituicao de
md5(uniqid(rand(), true))emd5(uniqid(mt_rand(), true))porrandom_bytes()/random_intcom entropia minima de 128 bits centralizados emseguranca_token_aleatorio(). -
Contencao estrita de Path Traversal por
realpath: validacao porrealpathestr_starts_withcontra os diretoriaos autorizados (assets/,contents/,modulos/) com rejeicao imediata de..e bytes nulos. -
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. -
Hardening de API e OAuth2: CORS parametrizado por allow-list, eliminacao de tokens em query string (
?token=), validacao do HMACpubIDValidationna verificacao de access tokens e rotacao FIFO de sessoes limite. -
Arquivamento rastreavel do backlog: os rascunhos
BL-001aBL-010foram movidos parasdd/backlog/archive/e anotados comoPROMOTEDemsdd/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:
-
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ó aodocument.body. Todos os listeners registrados na criação continuam válidos,renderStyler/renderBreadcrumb/renderChildrenseguem intactos e não existe risco de duas cópias divergirem. O custo é uma regra emupdateSelectionUI: o que está encaixado não é medido nem posicionado. -
Os painéis SOBREPÕEM o conteúdo; não empurram a página: a alternativa (aplicar
margin-leftao<html>, como o Live Editor já faz commargin-top) mudaria a largura de renderização do conteúdo editado — percentuais,100vwe 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. -
O offset do topo vem do
margin-topdo<html>, não da altura do iframe da barra: o iframe#c2f-site-toolbarCRESCE quando um dropdown abre (ele apenas sobrepõe), então sua altura não serve de referência. Omargin-topquedashboard.toolbar.jsaplica é exatamente o offset PERSISTENTE (30px + 44px da editbar aberta). No editor clássico não há barra e o offset é 0. -
Estado persistido, padrão desligado: as opções vivem em
localStorage['c2f-he-view-options']com os dois valores emfalsepor 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. -
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 chamac2fEditor.setViewOption()direto (e lêgetViewOption()ao abrir, evitando um estado paralelo). No editor clássico o motor está dentro do iframe: a janela pai postac2f-he:view-option. Como o iframe usasrcdoc, ele herda a origem — o painel do pai lê o mesmolocalStoragepara marcar os checkboxes. -
Separação Tailwind × classes do projeto é HEURÍSTICA e organizacional:
isTailwindClasstrabalha 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. -
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.
-
"Substituir" limpa a seleção ANTES de remover o alvo:
afterDomMutationchamaupdateSelectionUI, que trabalha sobreselectedElement; 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 porremapPastedIds— colar/substituir na mesma página do original não pode duplicardata-c2f-widget-id— e o novo objeto é selecionado ao final. -
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 viaelementsFromPoint),extractUserHtmle o seletor de fallback do salvamento emhtml-editor-interface.js. Sem isso, no editor clássico (ondecontentRooté o própriobody) 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)
-
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. -
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.
-
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. -
Clique no iframe da Editbar precisa ser ANUNCIADO ao host: os painéis do [REDACTED] por um
mousedownnodocumentda 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 publicarc2f-toolbar:ui-dismissem captura, e o [REDACTED] o fechamento emdismissHostPanels(). -
A regra é "clicou fora, fechou" — em qualquer superfície (decisão do Chefe na rodada 2): o
ui-dismissfecha TODA a UI flutuante, não só a do host.dismissHostPanels()delega ao motor porc2fEditor.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. -
Ler
isModalActiveANTES 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.dismissFloatingUicaptura 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)
-
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 oon('blur'), registrado apenas quando a build expõe eventos.applyInlineCssignora valor idêntico aostyleatual, senão cada troca de foco empilharia um passo de undo vazio. -
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-statussó 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 descontaalturaAposElemento(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 porclientHeight − (scrollHeight − altura dos editores).scrollHeightnunca é menor queclientHeight, então, com o conteúdo cabendo na caixa, a expressão colapsa em "altura atual do editor − folga": cada disparo doResizeObserverencolhia 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. -
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
setSizeinicial até o usuário arrastar o canto. A lógica virou o métodosyncLiveBoxCodeMirrors(panel), chamado pelo observador, pela troca de abas e pela abertura dos painéis de IA e Código Customizado. -
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 aodisable(). 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:
-
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 comLocation: _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-Cookiena 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()egestor_usuario_perfil()apenas LEEM o cookie e passaram ao modo silencioso. -
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. -
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 é200com título,robots: noindexe as tags OpenGraph, corpo vazio, e o módulo da página nem chega a ser incluído (a interceptação acontece antes degestor_permissao()). Visitante humano deslogado segue para/signin/com o retorno preservado, sem mudança. -
Bloqueio de rastreamento REMOVE o bloco, não deixa de adicioná-lo: o snippet de GTM/Meta Pixel quase sempre vem do
html_extra_headda 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órioconn2flow(varredura emgestor/,conn2flow-sitee[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 ogestor.rastreamentoBloqueado, que transformafbq/dataLayer/gtagem no-op antes de o snippet do projeto rodar — sem sobrescrever coletor já carregado. -
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ãodispatchEvent) que não aciona listeners nativos; a ação padrão dele éHTMLFormElement.prototype.submit(), que também não dispara eventosubmitalgum. Odocument.addEventListener('submit', …, true)do BATCH-107 — única proteção até aqui — nunca era acionado nesse caminho, e o campo_csrf_tokenjamais era anexado. A correção cobre os TRÊS caminhos: captura nativa (usuário), handler delegado do jQuery (trigger) e envelope deHTMLFormElement.prototype.submit(rede final), este instalado uma única vez e marcado por__c2fCsrfpara o asset carregado duas vezes não empilhar envelopes. -
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 oglobal.js.csrfToken()passou a procurar também emwindow.gestor.csrfTokenewindow.parent.gestor.csrfToken, e orenderWidgetsenvia 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). -
_csrf_tokencontinua 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. -
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. -
moduloUrl()como fonte única da URL do módulo no editor:gestor.moduloCaminhojá chega do backend com a barra final (rtrim($caminho,'/').'/'emgestor.php), então o+ '/'espalhado pelo arquivo produziaadmin-paginas/editar//. A normalização foi centralizada nos três pontos do editor (ajaxDefault, render-vars e orenderWidgetsdosrcdoc). 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. -
OpenGraph nasce no core com fallback, e some quando a página já tem o seu:
og:imagevazio 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 nohtml_extra_head, o core não injeta o seu — duasog:titlefazem 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-imagedoconfig.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:
-
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 — oimagepickdainterface.phpjá resolve miniatura, nome e tipo pelos metadados do arquivo em disco quando não existe linha emarquivos. Guardar o id numérico criaria uma dependência que o próprio gerenciador não garante mais. -
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ãoisset. -
A aba de SEO só existe quando o chamador a pede:
html_editor_componente()recebeseoe, sem ele, remove menu e conteúdo pormodelo_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. -
Nenhum código novo de seletor de imagem no editor clássico: o
imagepickdeinterface_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. -
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. -
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 oui-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. -
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/SimpleXMLreescreveriam 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>. -
Elegibilidade reaproveita a lista de rotas de sistema do BATCH-109:
sitemap_pagina_elegivel()chamagestor_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. -
Falha de sitemap nunca derruba o CRUD: é artefato derivado e regenerável. Erro de escrita vai para
log_discoe a gravação da página segue. O mesmo vale para a exceção capturada no callback. -
Status e exclusão entram por
callbackFunction, não por alteração dainterface: as duas operações são executadas pela biblioteca compartilhada, que já dispara o callback depois de gravar e antes de redirecionar. Alterarinterface_status_finalizar/interface_excluir_finalizarafetaria todos os módulos do sistema por causa de um requisito de um só. -
publisher-pagesficou de fora, com motivo: as publicações gravam na MESMA tabelapaginas(vinculadas porpublisher_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:
-
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 (404e500são exatamente o que se quer ver no relatório). Decisão do Engenheiro Chefe: os coletores continuam acessando tudo. -
A evidência veio de medição, não de leitura de código:
curlcontra 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. Ocurlaborta 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. -
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, chamagestor_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. -
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 degestor_cookie_verificacao(), nogestor/gestor.php(que executagestor_start()no fim do arquivo e não é carregável em teste), isso era impossível. São 5 casos cobrindo o laço. -
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. -
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=pagecomsem_permissao, ou seja, nada impedia a indexação.noindex, nofollowno<head>eX-Robots-Tagno cabeçalho tiram o que já entrou e evitam que uma reincidência volte a poluir relatório e busca. -
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.
-
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. -
Rename honesto:
gestor_pagina_sistema_sem_rastreamento()virougestor_pagina_rota_sistema(). Sem o bloqueio, o nome antigo descrevia um comportamento que deixou de existir. A função sobrevive porque ositemap_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:
-
assets/em vez da raiz, porque a raiz dependia de configuração externa: na raiz ositemap.xmlsó era servido pela regraRewriteCond %{SCRIPT_FILENAME} !-fdo.htaccess— que varia por instalação e some em servidor com roteador mais restritivo. Emassets/quem entrega é oarquivo-estatico.php, que é parte do core e vale em toda instalação. Achado importante: odefault:do switch de extensão já resolve qualquer extensão desconhecida contraassets-path, exmljá estava na tabela de Content-Type — nenhuma linha do controlador ou do.htaccessprecisou mudar. Só a função de caminho. -
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í asitemap_caminho_nao_indexavel(), que barra por lista exata + dois sufixos (-confirmation,/success) + prefixoadmin-. As três rotas nomeadas no intake continuam entrando. -
interface_modulo_variavel_valor()era o defeito do 301, não a chamada: essa helper chamagestor_redirecionar_raiz()quando não encontra o registro. Num CRUD isso é umexitno 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 porid_hostsque não corresponde a estes módulos. A troca por umbanco_select_namedireto 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 empaginas_301— A → B → A → B gerava duas linhas idênticas. -
A correção do "event bleed-through" não era CSS: o intake descreve
pointer-events,z-indexestopPropagation, e todos ajudam — mas a causa-raiz erac2f-page-config-panelec2f-page-config-pickernão estarem emisEditorOwned(), omissão minha no BATCH-110. O motor decide o que é UI porelementsFromPoint+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. -
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). -
descriptionekeywordssã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_descricaoda própria página →config.php. -
keywordssó é 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>. -
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 nocaminho—, então não havia risco de perda de dado nem necessidade de 301 ali. O que faltava era o<lastmod>: ela atualizadata_modificacaoe 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)
-
html-editor.jstem 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=c2fNNescrita emdashboard.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: alterarhtml-editor.jsexige bumpar os DOIS. -
Asset de módulo incluído sem
versaoherda a versão do SISTEMA:dashboard.toolbar.jsera injetado porgestor_pagina_javascript_incluir()sem a chaveversao, 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. -
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 — usarbiblioteca-html-editor, porque a Editbar e o motor mudam JUNTOS: mexer na barra quase sempre implica mexer no editor. Assimgestor/bibliotecas/html-editor.phpvira 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 JSgestor.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. -
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:
-
Página
user_modifiednã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 seucss_precompiledchegam mesmo quando o HTML autoral é preservado pelo sincronizador. -
Um componente pode ser um catálogo funcional de templates, mas não um catálogo de classes:
busca-clinica-runtime-fragmentsconté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. -
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.
-
[REDACTED]-systempermanece coberto até a última família ser migrada: o asset compartilhado concentra dezenas de renderizadores de seis telas. Removertailwind_sourcesapós migrar apenas uma família produziria uma falsa conclusão e novas perdas visuais; a remoção será o último passo. -
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):
-
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;
.hiddenposterior anuloulg:flexdo layout na Busca Clínica. -
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 antigosystem-output.cssglobal. -
Migração obrigatoriamente opt-in: o runtime só elimina sidecars isolados quando o módulo define
tailwind-page-bundlee existe umpage-precompiled. Rotas não inventariadas continuam no contrato anterior; ativar globalmente esconderia dependências ainda não declaradas. -
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 ecss-fimmantêm sua precedência e seu propósito. -
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_precompiledno fallback sem bundle; omitir o campo foi a causa da imagem clínica semmax-h-56.
DEC-110 - 2026-08-14 - accepted
Contrato de camadas para bundles Tailwind canônicos (req-115 / BATCH-115):
-
Bundle que substitui layout precisa substituir também suas camadas globais: compilar somente utilities é correto para sidecar isolado, mas incorreto para
page-precompiledque elimina o layout da resposta. O bundle deve carregar tema, base e Preflight do input central. -
tailwind_sourcesnã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 emtailwind_bundle: true, restrita a páginas e exigindo fontes adicionais declaradas. -
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.
-
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 portype,module,languageeid; o compilador traduz para arquivo apenas durante o build. O runtime continua usandogestor_componente(),gestor_layout()e tabelas do banco. -
tailwind_sourcesfica 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 portailwind_dependencies.
DEC-111 - 2026-08-17 - accepted
Captura do css_compiled e política de delta contra a cascata real (req-117 / BATCH-117):
-
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). -
Todo
<style>que o sistema emite declara seu papel:data-c2f-tailwind-rolepara o que o editor injeta,data-tailwind-rolepara os pré-compilados offline edata-c2f-css-rolepara as filas de CSS autoral ecss_compiledda página pública. Folha sem papel declarado é candidata a ser confundida com a saída do compilador — e ocss_compiledanterior é o caso mais perigoso, porque contém@layer utilitiese congelaria a captura no valor já gravado. -
O
css_compiledgrava 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áginasobredo photon: 65% de redução. -
Camada de fundação é decidida por CAMADA; utilities, por regra:
themeebasenã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-buttonno Preflight do 4.3.0). Como ocss_compiledentra 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. -
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.
-
HTML do painel de Código é aplicado por ação explícita, CSS ao vivo: reescrever
#c2f-page-contentrecria 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. -
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):
-
Descarte silencioso é defeito de diagnóstico, não só de estilo: em modo bundle, sidecar
resource-precompileddescartado 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. -
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. -
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
displaysob 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. -
Papel de pré-compilado é contrato de ORDEM, não anotação de diagnóstico: os includes de
gestor_layout()sem papel caíam no defaultresource-precompilede 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. -
Heurística de nomenclatura se calibra contra o inventário real: a regra de "página de confirmação" cobria
…/successenquanto 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. -
Artefato derivado que mudou de lugar tem de apagar o antigo: o
sitemap.xmlna raiz sobreviveu à mudança paraassets/e, em instalação onde a regra!-fresolva antes do roteador, seria servido para sempre. A remoção só acontece sob trava de autoria — arquivo de terceiro é preservado. -
robots.txte sitemap derivam da MESMA lista de rotas: onoindexsó é visto depois do rastreio; orobots.txté o único ponto que barra antes. Alimentá-lo por outra fonte abriria a chance de liberar o que o sitemap esconde. -
Dedup precisa do escopo inteiro da chave:
paginas_301deduplicava porcaminho, mas a chave real é (caminho,id_paginas) — semid_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. -
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
@themedo layout tem apenas os tokens do LAYOUT. Descartá-lo inteiro deixa as utilities novas da página comvar()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:
-
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,tailwindcssehibrido. 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. -
Variante de componente por sufixo, com o legado intocado. Cada componente da interface ganha um irmão
<id>-tailwind, escolhido em runtime. Em modohibridoa variante não é aplicada — com o Fomantic na página, o componente legado é o único com estilo garantido. Osswitchque decidem quais variáveis cada componente recebe continuam escritos sobre o id canônico: a variante muda o HTML, nunca o contrato de dados. -
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.jsinterpreta 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. -
A folha de ÍCONES do Fomantic fica; o framework sai. A coluna
modulos.iconeguarda 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 carregadist/components/icon.min.csse nada mais — o CSS de layout continua 100% Tailwind. -
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 emitedisplaysob 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. -
O modificador
!do Tailwind v4 fica nos controles de formulário. No modohibrido,.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.phpfoi 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.