Conn2Flow
Navegação das docs

SDD

Decision Log (Archive: DEC-081 to DEC-090)

Mapeamento no Pai de Widgets Múltiplos, Image Picker, Filtro/Agrupamento de Módulos e Backups Página×Layout (req-079 / BATCH-079). Lote de 8 melhorias no Live Editor + Dashboard…

DEC-081 - 2026-07-10 - accepted

Mapeamento no Pai de Widgets Múltiplos, Image Picker, Filtro/Agrupamento de Módulos e Backups Página×Layout (req-079 / BATCH-079). Lote de 8 melhorias no Live Editor + Dashboard Site Toolbar. Decisões desta rodada:

  1. Mapeamento de widget no elemento PAI (item 1): quando a expansão viva de um widget produz múltiplos elementos-raiz que são o único conteúdo estrutural do contêiner pai (ex.: um menu de <a> dentro de um <nav>), mapTree marca o próprio liveParent (novo data-c2f-widget-parent="1" + data-c2f-widget-id/-root/-marker/-widget-type/-slug) em vez do 1º irmão — a borda/label cobre o menu inteiro e os links filhos ficam inertes. reconstructOriginal: quando o nó marcado tem data-c2f-widget-parent, substitui o innerHTML do pai pelo marcador cru decodificado (preserva a tag externa <nav>/<ul> + atributos), em vez de trocar o nó. Guardas: só entra em modo-pai se K===1, há >1 elementos-raiz, o widget é o único conteúdo estrutural do pai, e o pai não é a raiz editável (#c2f-page-content/#c2f-layout-root/#c2f-raw-content/body) — caso contrário mantém a marcação por-elemento (BATCH-078 r2).

  2. Alinhamento por cauda + agrupamento de widgets consecutivos (item 7): como o output público remove os comentários <!-- widgets#… --> (a expansão viva não tem âncora de widget), dois widgets idênticos consecutivos faziam a busca de âncora afterSig casar dentro do próprio 1º widget e fundir os dois. Solução: collectWidgetGroup agrupa widgets consecutivos no cru (separados só por nós não-estruturais); a âncora após o grupo casa por assinatura E pela contagem de nós estruturais restantes até o fim do contêiner (remNodes × remLive — tail alignment); a faixa viva do grupo é dividida igualmente entre os K widgets (idênticos renderizam a mesma quantidade), cada um recebendo seu próprio data-c2f-widget-id. Divergência do intake: como a assinatura do próximo cru é 'W' (widget) e não existe no vivo, o grupo-com-divisão substitui a busca literal structSig===afterSig prescrita, que não casaria — a contagem de cauda é aplicada na âncora estática após o grupo.

  3. Image picker autônomo no modal do live editor (item 3): o modal fallback (ensureFallbackModal) ganha o botão _html-editor-imagepick-btn ao lado do #element-src. Como o live editor roda na própria janela hospedeira (sem o par iframe-preview/interface.js do admin), o motor monta um picker autônomo (openLiveImagePicker): overlay com <iframe> → raiz + admin-arquivos/?paginaIframe=sim; a seleção (postada pelo iframe a window.parent = a janela hospedeira) é capturada por um listener próprio (data.moduloId==='admin-arquivos', JSON.parse(decodeURI(data.data)), filtro image/*) e preenche o #element-src. A raiz chega pela nova opção raiz do construtor de HtmlEditorClass (passada por dashboard.toolbar.js); sem raiz, cai no window.prompt (retrocompat).

  4. Ícones Fomantic restantes → SVG inline (item 2): novo helper svgIcon(name) (mapa nome-Fomantic→path, stroke="currentColor", 24×24) substitui as <i class="… icon"> remanescentes em html-editor.js — accordion do styler (dropdown), bgimage (folder open/trash) e os botões kind:'icon' do styler (align *, ban, underline, strikethrough, arrows alternate *). Independem da fonte Fomantic (ausente na página pública).

  5. Modal de edição redimensionável (item 4): a caixa interna do modal fallback recebe resize:both;overflow:auto + min/max-width/height e width explícita (arrasto pelo canto inferior direito), com header/footer flex:0 0 auto e corpo flex:1 1 auto.

  6. Filtro + agrupamento por categoria no menu de módulos (itens 5+6): dashboard_site_toolbar_menu reusa a fonte de gestor_pagina_menu — agrupa os módulos permitidos por modulos_grupos (ordenados por ordemMenu), com cabeçalhos .c2f-group-header não-clicáveis (caixa-alta atenuada) + itens .c2f-menu-item; grupos sem categoria caem em "Outros". Campo #c2f-modules-filter ("Filtre os módulos...") no topo do dropdown. dashboard.iframe-toolbar.js: listener input filtra .c2f-menu-item (case-insensitive, por textContent) e oculta cabeçalhos de grupos sem itens visíveis, reajustando a altura do iframe (pushHeight).

  7. Backups divididos Página × Layout com restauração independente (item 8): dashboard_ajax_site_toolbar_backups retorna page_backups (módulo dono, id_paginas) + layout_backups (módulo admin-layouts, id_layouts do layout vinculado). dashboard_ajax_site_toolbar_backup_get aceita type=page|layout: layout → extrai o body-inner do layouts.html do backup, renderiza as caixas legadas (.c2f-dyn-box, compatíveis com reconstructOriginal) e injeta um slot #c2f-page-content vazio no lugar de @[[pagina#corpo]]@. Frontend: painel de 2 colunas; backup de página → só #c2f-page-content.innerHTML; backup de layout → restoreLayoutBackup injeta o body-inner no #c2f-layout-root preservando o #c2f-page-content vivo (destaca e re-encaixa no slot).

  8. Cache-bust: live carrega interface/html-editor.js?v=c2f3 → ?v=c2f4; admin bumpa biblioteca-html-editor 1.4.0 → 1.4.1.

Validação: node --check (3 JS) OK; php -l (dashboard.php, html-editor.php) OK; testes integrados via vitest em tests/Unit/JS/dashboard.toolbar.test.js (happy-dom, arquivo real via hook de teste) rodando 18 checks (mapeamento no pai + reconstrução por innerHTML; 2 widgets idênticos consecutivos separados; âncora estática por cauda preservada) aprovados; composer test 76/76 (287 assertions, 4 skipped) sem regressão; npm run test (vitest) 6/6 total aprovados. Runtime (deploy Update => Core + homologação visual) pendente com o operador. Nenhum git commit/git push executado.

Adendo — Marcadores de widget preservados no modo edição (2026-07-10, pós-teste do Chefe). A homologação mostrou que a heurística de alinhamento DOM-vivo × cru (itens 1 e 7 acima) ainda fragmentava widgets em casos reais: no header do [REDACTED], um <nav> com dois menu-header-padrao idênticos consecutivos (duplicação real no banco) fazia a divisão por assinatura/cauda marcar um <a> como raiz em vez do <nav>. Decisão (proposta do Engenheiro Chefe Humano): em vez de adivinhar a fronteira do widget, preservá-la explicitamente.

  1. gestor_pagina_widgets mantém os comentários no modo edição (conn2flow/gestor.php): quando gestor_dashboard_toolbar_ativo() é verdadeiro (editor logado), a substituição do bloco de widget passa de render para <!-- widgets#SIG < -->render<!-- widgets#SIG > --> — os comentários de abertura/fechamento são preservados no DOM vivo (só o mockup interno vira o render). Para o visitante normal, os marcadores continuam removidos (comportamento original). Aplicado nas duas formas (wrapper por comentário e inline @[[widgets#…]]@).

  2. mapTree delimita o widget pelos PRÓPRIOS comentários vivos (dashboard.toolbar.js): eliminados collectWidgetGroup, o tail alignment, a divisão igualitária e a função structSig. O branch de widget casa o comentário de abertura vivo pela mesma assinatura a partir de li (o que separa naturalmente widgets duplicados/consecutivos — cada um tem seu par de comentários), marca os nós entre open/close e remove os comentários do DOM vivo. Modo-pai (item 1) determinístico: se não há nenhum elemento/texto/outro-widget FORA da faixa [open,close] no contêiner (otherContent === false), há múltiplos elementos-raiz e o pai não é a raiz editável → marca o pai (data-c2f-widget-parent); senão marca os elementos-raiz (1º recebe marcador/label). O markerHtml (com o mockup do designer) continua vindo do cru, preservado no save. reconstructOriginal inalterado.

  3. Efeito: um menu que é o único conteúdo de um <nav>/<ul> marca o <nav> inteiro (borda/label no contêiner); dois widgets idênticos consecutivos viram widgets separados e independentes; nós estáticos irmãos nunca são absorvidos. Sem :nth-child/:first-child afetados (comentários HTML não são elementos).

Validação do adendo: node --check (dashboard.toolbar.js) OK; php -l (gestor.php, dashboard.php) OK; testes vitest em dashboard.toolbar.test.js reescritos para marcadores no DOM vivo passando 20/20 checks (widget único → pai; 2 widgets consecutivos no mesmo <nav> separados; widget + rodapé estático preservado); composer test 76/76 sem regressão. Cache-bust: biblioteca-html-editor 1.4.1→1.4.2 (bump do operador/linter). Runtime (deploy + homologação visual) pendente com o operador.

Adendo 2 — Widget de múltiplos elementos = bloco ATÔMICO (2026-07-10, pós-teste do Chefe / entregue junto do BATCH-080). Após deploy, o header do [REDACTED] (<nav> com 2 menu-header-padrao idênticos — caso REAL, N instâncias idênticas é válido) mapeava os 2 widgets corretamente, mas cada <a> (com data-c2f-widget-id) ficava selecionável isolado e o outline/label só cobria o 1º link. Como há 2 widgets no mesmo <nav>, não dá para usar modo-pai. Correções no html-editor.js (sem wrapper — preserva :nth-child/flex do site):

  1. resolveEditable → raiz do grupo: qualquer elemento com data-c2f-widget-id resolve para o elemento com data-c2f-widget-root do mesmo grupo. Clicar em qualquer link seleciona o widget inteiro (um átomo); links individuais deixam de ser selecionáveis.

  2. Overlay abrange a união do grupo: novos helpers elementRect/unionRect — positionOverlay e updateSelectionUI usam a união dos bounding boxes de todos os [data-c2f-widget-id="gid"], então hover/seleção/toolbar cobrem o widget inteiro.

  3. CSS: outline em [data-c2f-widget-id] (todos os elementos do grupo mostram a extensão) + [data-c2f-widget-root]::before mantém o label único. Validado por tests/Unit/JS/html-editor.live.test.js (resolveEditable de qualquer link → raiz). Cache-bust junto do BATCH-080 (?v=c2f5, 1.4.3).

DEC-082 - 2026-07-10 - accepted

Integração de Modelos de Sessão e Assistente IA no Live Editor (req-080 / BATCH-080). Porta os dois recursos do editor clássico para a barra flutuante do Live Editor, 100% vanilla (sem Fomantic) e autônomos na página pública. Decisões desta rodada:

  1. Recursos gated ao Live Editor por this.raiz: createToolbar só injeta os botões .he-tb-templates e .he-tb-ai quando this.raiz está presente (contexto do live editor, passado por dashboard.toolbar.js). O editor visual do admin (sem raiz) fica inalterado — evita duplicar/colidir com a UI Fomantic clássica (que vive nos painéis laterais, não na floating toolbar). As chamadas AJAX vão para rotas site-toolbar-* do dashboard.php, derivadas de this.raiz + 'dashboard/'.

  2. Backend reusa o clássico, sem duplicar lógica: dashboard.php ganha 5 rotas — site-toolbar-templates-load (→ html_editor_ajax_templates_load), site-toolbar-ia-init (→ novo ia_editor_dados), site-toolbar-ia-prompt (→ ia_ajax_prompts), site-toolbar-ia-mode (→ ia_ajax_modos), site-toolbar-ia-request (→ html_editor_ajax_ia_requests) — todas com gestor_acesso('editar','admin-paginas') + gestor_incluir_biblioteca. Novo ia_editor_dados($alvo) em ia.php devolve prompts/modos/conexões/modelos como arrays JSON-friendly (espelha a coleta de ia_renderizar_prompt sem gerar HTML Fomantic). html_editor_ajax_templates_load passou a aceitar params[busca] (LIKE por nome) — retrocompatível (o editor clássico não envia busca).

  3. Painéis vanilla blindados: #c2f-tpl-panel e #c2f-ai-panel são overlays injetados no body com CSS !important (cor/fundo/fonte) contra herança do site, mesmo padrão do floating toolbar. Modelos: busca com debounce, select de framework, relação de inserção (Substituir/Antes/Depois), grid de cards paginado ("Carregar mais"); insertTemplate insere o HTML no DOM vivo e injeta o CSS numa <style id="c2f-templates-css">. IA: abas Prompt/Modo/Config (show/hide vanilla), selects populados por site-toolbar-ia-init, textarea de instrução, decodificação de entidades via textarea-trick (as rotas ia_ajax_prompts/modos e modo_padrao retornam htmlentities); submitAi envia outerHTML do elemento + mode/prompt/server_id/model, applyAiResult troca o elemento pelo HTML gerado e injeta o CSS.

  4. Background image picker autônomo (item 4): requestBackgroundImage no live (this.raiz) usa openLiveImagePicker com imagePickerTarget='background'; bindLiveImagePicker roteia a seleção — 'background' → applyBackgroundImage(url), senão preenche #element-src. Remove a dependência do window.prompt/html_editor.imagepick (ausentes no live). .he-bgimage-clear segue em clearBackgroundImage.

  5. Fix BATCH-079 entregue nesta rodada — widget de múltiplos elementos é bloco ATÔMICO: ver o Adendo 2 da DEC-081 (resolveEditable → raiz do grupo, overlay da união, outline em todos os elementos). Sem wrapper (preserva CSS/flex do site).

  6. Fix crítico — desgatear o render/AJAX do dashboard-site-toolbar (pós-teste do Chefe): após deploy, o menu de módulos sumiu e o botão "Editar Página" não respondia. Causa: os switches de dashboard_start() estavam embrulhados em if(gestor_dashboard_toolbar_ativo()){…}, mas esse gate é FALSE na própria página do toolbar (guard anti-recursão opcao=='dashboard-site-toolbar') — então dashboard_site_toolbar() não rodava, deixando #page_id#/#advanced_edit_url#/<!-- menu --> literais e não injetando o dashboard.iframe-toolbar.js (por isso o clique no edit não fazia nada). Correção: reverter os dois switches (AJAX e não-AJAX) para UNGATED, como no BATCH-075 — a segurança já está DENTRO de cada handler AJAX (gestor_acesso('editar','admin-paginas')) e na permissão da página; o gate serve só para a INJEÇÃO do toolbar na página hospedeira, nunca para o render do toolbar em si.

  7. Botões Modelos/IA movidos da floating toolbar para a EDITBAR (pós-teste do Chefe — divergência do intake §1): o req-080 pedia os botões na #html-editor-floating-toolbar (per-elemento, aparece só na seleção). Na homologação ficaram pouco descobríveis. Decisão (orientação do Chefe): mover para a barra de edição #c2f-toolbar-editbar (ao lado de "Adicionar"/"Backups"), sempre visível no modo edição. Botões #c2f-templates-btn/#c2f-ai-btn nos 2 HTMLs → dashboard.iframe-toolbar.js posta c2f-toolbar:edit-templates/edit-ai → dashboard.toolbar.js chama c2fEditor.openTemplatesPanel()/openAiPanel(). Removidos os botões .he-tb-templates/.he-tb-ai da floating toolbar do html-editor.js (e o gate this.raiz deles). Os HTMLs vivem no banco → exigem Update => Core para sincronizar.

  8. Fix — painéis Modelos/IA em isEditorOwned (mesma classe do BATCH-078 correção 2): como os painéis (#c2f-tpl-panel/#c2f-ai-panel) e o overlay do image-picker (#c2f-he-imagepick-overlay) são injetados no document.body (fora do contentRoot), o listener de clique em capture do html-editor.js (que usa elementsFromPoint) atravessava o modal e selecionava o elemento da página atrás (dentro do #c2f-layout-root), matando os cliques dentro do painel (stopPropagation). Adicionados os 3 seletores a isEditorOwned → early-return no clique/hover, os cliques ficam no painel. Regra geral (reforço): TODA UI do live editor criada no document.body PRECISA entrar em isEditorOwned. Cache-bust ?v=c2f7 + biblioteca-html-editor 1.4.6.

Validação: node --check (html-editor.js, dashboard.toolbar.js) OK; php -l (dashboard.php, ia.php, html-editor.php) OK; npm run test (vitest) 11/11 — novo tests/Unit/JS/html-editor.live.test.js 5/5 (botões Modelos/IA presentes só no live, painéis abrem, resolveEditable atômico, bg picker roteia p/ background); composer test 76/76 (287 assertions, 4 skipped) sem regressão. Cache-bust interface/html-editor.js?v=c2f4→?v=c2f5 + biblioteca-html-editor 1.4.2→1.4.3. Runtime (deploy Update => Core + homologação visual dos painéis, geração por IA real e picker de fundo) pendente com o operador. Nenhum git commit/git push executado.

DEC-083 - 2026-07-11 - accepted

CodeMirror no Assistente IA, Correção do Save Intermitente, Dropdowns da Toolbar e Painel "+" em Duas Colunas (req-081 / BATCH-081). Seis metas de refinamento do Live Editor / Dashboard Site Toolbar (recursos do motor gated ao live por this.raiz):

  1. CodeMirror (markdown) + caixa redimensionável no Assistente IA (§1): initAiCodeMirror instancia CodeMirror sobre #c2f-ai-instruction e #c2f-ai-mode-text (modo markdown, tema tomorrow-night-bright, dedup por .CodeMirror irmão) apenas quando window.CodeMirror existe — no Live Editor ele é carregado por dashboard.toolbar.js (que passou a incluir mode/markdown/markdown.js + mode/javascript/javascript.js); sem CodeMirror, degrada para os textareas. Os acessos passaram por helpers aiGetInstruction/aiSetInstruction/aiGetMode/aiSetMode (CodeMirror getValue/setValue com fallback ao .value), usados por submitAi, loadAiInit e os handlers de troca de prompt/modo. CSS .c2f-he-live-box { resize:both; overflow:auto; min-*; } torna as caixas de Modelos e IA redimensionáveis pelo canto inferior direito.

  2. Causa-raiz do "Erro ao salvar a página." intermitente (§2): o alert vinha do .catch() do saveEdit — ou seja, r.json() falhava por corpo não-JSON, sem erro de rede (HTTP 200) e sem log do PHP (com display_errors=on/log_errors=off, warnings/notices vão ao corpo da resposta, não ao log). O gestor.php ecoava json_encode($_GESTOR['ajax-json']) sem buffer de saída (não há ob_start global no bootstrap), então qualquer saída espúria emitida durante a execução do módulo/gestor_pagina_widgets_ajax prefixava o JSON e o quebrava — intermitente porque depende dos dados salvos. Correção (raiz, beneficia TODOS os endpoints AJAX-json): envolver a execução do módulo + widgets num ob_start() e descartar o buffer (ob_end_clean até o nível pré-buffer) imediatamente antes do echo json_encode(..., JSON_INVALID_UTF8_SUBSTITUTE); a flag também evita json_encode retornar false com UTF-8 inválido (que geraria corpo vazio). Defesa em profundidade no cliente (dashboard.toolbar.js): (a) deselectAll() do editor antes do getCleanHtml; (b) guarda de prontidão — abortar o save com aviso se o editor ainda não instanciou (corrige a corrida "Editar → Salvar imediatamente"); (c) parse tolerante da resposta (lê como texto, recorta {...}) e, em falha real, mostra um trecho do corpo para diagnóstico em vez do alerta opaco. Novo deselectAll() no html-editor.js (sai do modo inserção, limpa seleção/hover/contornos/wrap-menu).

  3. Dropdowns de Usuário e Página na toolbar (§3): os botões expostos "Editar Página"/"Editar no Painel" foram reorganizados em dois dropdowns (clique) na barra direita dos 2 HTMLs (pt-br/en). Usuário: ícone + @[[usuario#nome]]@ (variável de template resolvida em gestor.php); menu com ícone maior, nome, Perfil (@[[pagina#url-raiz]]@perfil-usuario/) e Sair (@[[pagina#url-raiz]]@signout/). Página: ícone + "Página"; menu com título (#page_title#), Editar Página (c2f-toolbar-edit preservado), Editar Avançado (c2f-toolbar-edit-advanced preservado), Criar Nova Página (#new_page_url#) e Clonar Página (#clone_page_url#) — publisher-pages vs admin-paginas resolvido em dashboard_site_toolbar() (#page_title# de paginas.nome). O toggle vive em dashboard.iframe-toolbar.js (fecha ao clicar fora / ao abrir o outro / ao entrar na edição) e o iframe cresce para caber o menu aberto (iframeHeight considera openDropdown).

  4. CRUD de prompts no Assistente IA (§4): botões ai-prompt-new/edit/del/clear no painel; aiPromptNew/Edit/Del/Clear POSTam params[...] para 3 rotas novas do dashboard.php — site-toolbar-ia-prompt-new/-edit/-del — que reusam ia_ajax_prompt_novo/_edit/_del (target paginas). O <select> de prompts é atualizado após cada operação. "Limpar" só zera a instrução.

  5. Código Customizado no painel "+" (§5): item "Código Customizado" na coluna de elementos → c2fEditor.openCustomCodePanel() (modal .c2f-he-live-box com CodeMirror htmlmixed); insertCustomHtml(html) insere o bloco no DOM vivo após a seleção (ou ao fim do contentRoot), estilo c2f-add-el. #c2f-custom-panel entra em isEditorOwned.

  6. Painel "+" em duas colunas, grupos, paginação e autocomplete (§6): painel reestruturado — Elementos (esq.) × Widgets (dir.), Widgets em subcolunas c2f-add-widget-groups × c2f-add-widget-items; clicar num grupo carrega seus itens; input de busca no topo (debounce 300ms → AJAX cross-grupo) com autocomplete; botão "Carregar mais". Backend: dashboard_ajax_site_toolbar_widgets_list passou a delegar ao novo html_editor_widgets_buscar() (paginação pagina/limite, busca name/id LIKE, cross-grupo quando module vazio, anti-injection nos nomes de tabela/coluna), resposta {status, data:{items:[{id,nome,module}], tem_mais}}. O limite vem da opção do módulo site_toolbar.widgets_por_pagina (padrão 10) no dashboard.json. O editor clássico segue usando html_editor_ajax_widgets_list (shape antigo, inalterado).

Cache-bust: interface/html-editor.js?v=c2f7→?v=c2f8, biblioteca-html-editor 1.4.6→1.4.7, dashboard.json versao 1.0.8→1.0.9 (busta dashboard.iframe-toolbar.js). Validação: php -l (gestor.php/dashboard.php/html-editor.php/ia.php) OK; node --check (html-editor.js/dashboard.toolbar.js/dashboard.iframe-toolbar.js) OK; dashboard.json válido; npm run test 14/14 (novos casos: CRUD de prompts + aiPromptClear, deselectAll, openCustomCodePanel+insertCustomHtml; stub de CodeMirror em setup.js estendido com setValue/getValue); composer test 76/76 (287 assertions, 4 skipped) sem regressão. Runtime (deploy Update => Core + homologação visual dos dropdowns, CodeMirror/CRUD do assistente, código customizado, painel "+" e do save intermitente) pendente com o operador. Nenhum git commit/git push executado.

Adendo DEC-083 (2026-07-11, follow-up pós-homologação do Chefe)

Correções após teste runtime do Chefe (mesma rodada BATCH-081):

  1. Causa-raiz REAL do save "302 → home" (confirmada por backtrace; substitui as hipóteses de buffer E de request_order): instrumentando gestor_redirecionar no mirror, a cadeia foi dashboard_ajax_site_toolbar_save → interface_historico_incluir() → interface_modulo_variavel_valor() → gestor_redirecionar_raiz(). Sem id_numerico_manual/sem_id, interface_historico_incluir resolve o id numérico via interface_modulo_variavel_valor, que exige $_GESTOR['modulo-registro-id'] (contexto do CRUD admin) — ausente no AJAX do site toolbar → redirect 302 text/html (o gestor_redirecionar não checa ajax), seguido para a home. Intermitente porque o histórico só roda quando conteúdo/layout muda. Correção real (mínima): passar id_numerico_manual ($pagina['id_paginas'] / $layout['id_layouts']) nas 2 chamadas de interface_historico_incluir (página em dashboard_ajax_site_toolbar_save, layout em dashboard_site_toolbar_salvar_layout). Hipóteses descartadas: (a) NÃO era warning poluindo o JSON (log PHP vazio; blindagem ob_start em gestor.php revertida); (b) NÃO era request_order sem "P" (php -i → request_order=no value → fallback variables_order=EGPCS inclui POST). O roteamento por query string + array_merge($_REQUEST,$_POST) (saveEdit/submitAi/aiPromptCrud/dashboard_start) ficou como defensivo (não era necessário), com comentários honestos apontando o id_numerico_manual como fix real. Ver memória de execução.

  2. Dropdowns de Usuário/Página no HOVER: passaram a abrir no hover (group + group-hover:block), igual ao c2f-toolbar-menu, em vez de só no clique. dashboard.iframe-toolbar.js só ajusta a altura do iframe (mouseenter/mouseleave → openDropdown).

  3. Resize-follow dos campos do modal + fix do loop: ResizeObserver em .c2f-he-modal-box faz element-text (altura) e o CodeMirror (setSize) acompanharem o arrasto do canto; idem para os CodeMirror dos painéis .c2f-he-live-box (IA/Código Customizado). O loop de feedback do observer (oscilação de altura ao abrir) foi corrigido dando altura inicial definida (height:70vh) às caixas + guarda anti-reentrância no callback.

Cache-bust adicional: interface/html-editor.js?v=c2f8→?v=c2f9. Validação: php -l (gestor/dashboard) OK, node --check (3 JS) OK, npm run test 14/14. Deploy fonte→mirror + homologação runtime pendentes com o operador (o site roda do espelho dev-environment/data/sites/localhost/[REDACTED]/, não da fonte gestor/).

DEC-084 - 2026-07-13 - accepted

Carregamento de Widgets, Seleção de Modelos, Restauração de Backups e Isolamento Multi-usuário no Live Editor (req-082 / BATCH-082). Quatro correções de homologação; todo recurso novo do motor permanece gated ao Live Editor (this.raiz) e as chamadas seguem nas rotas site-toolbar-* do dashboard.php, reusando a lib html-editor.php.

  1. Ponte de renderização de widget (§1): o motor (html-editor.js), ao inserir um widget, já postava a string JSON c2f-he:widget-render para window.parent (na página hospedeira top, window.parent === window), mas não havia receptor no Live Editor → o widget ficava preso em "Carregando widget…". Adicionados: rota site-toolbar-widget-render → dashboard_ajax_site_toolbar_widget_render() (checa gestor_acesso('editar','admin-paginas'), inclui a lib e delega a html_editor_ajax_widget_render(), que já resolvia modulo->func({...}) em modo page-load), e um listener dedicado no dashboard.toolbar.js que só trata mensagens string (o listener principal trata os objetos c2f-toolbar:* do iframe): faz POST na rota (params[signature]) e devolve c2f-he:widget-rendered (string JSON) para a própria window → o motor chama applyWidgetRender. O handler filtra action==='c2f-he:widget-render' (não entra em laço com a própria resposta) e retorna a promise do fetch (testabilidade).

  2. Seleção automática do modelo inserido (§2): insertTemplate passou a rastrear o primeiro nó de elemento (nodeType===1) inserido nas três relações (replace/before/after) e o seleciona ao final (selectElement). Antes só replace selecionava — os modos before/after deixavam o modelo sem seleção, exigindo clique manual.

  3. Restauração de backups marker-based com re-mapeamento (§3): ao restaurar backup, os widgets vinham como .c2f-widget-box (caixa "morta", não interativa) e as anotações se perdiam. Decisões: (a) dashboard_site_toolbar_boxes_widgets passa a embrulhar cada widget nos marcadores de comentário vivos <!-- widgets#SIG < -->{render}<!-- widgets#SIG > --> (formato que o mapTree do frontend delimita/anota), em vez da div .c2f-widget-box — muda também o output do motor legado de caixas no render, que não é consumido pelo frontend (usa content_raw/layout_raw), sem regressão; (b) novo helper dashboard_site_toolbar_resolver_variaveis($html) resolve @[[var]]@ para o valor puro (texto e atributo, sem .c2f-var-box), espelhando a página viva, para o mapTree re-anotar as variáveis a partir do raw (usar .c2f-var-box no html causaria duplicação de variável ao re-mapear); (c) dashboard_ajax_site_toolbar_backup_get devolve também raw (HTML cru salvo, marcadores intactos; para layout, html/raw alinham o slot vazio #c2f-page-content); (d) no frontend, restorePageBackup(html,raw)/restoreLayoutBackup(html,raw) chamam deselectAll(), injetam html no DOM vivo e raw no container oculto de mapeamento (#c2f-raw-content para o conteúdo; backup inteiro para o layout), re-rodam mapTree() e chamam restoreChrome(). Divergência do intake (robustez, não a prosa): o varMap não é zerado — o intake pedia varMap={}, mas ao restaurar layout o conteúdo vivo (slot vazio no raw) não é reprocessado e teria seu mapeamento apagado, "queimando" variáveis no save; preservar o varMap re-anota só a subárvore restaurada e deixa entradas antigas como lixo inofensivo (o reconstructOriginal ignora IDs órfãos). Limitação conhecida: widget salvo na forma inline @[[widgets#...]]@ (legado pré-BATCH-079) não é re-delimitado no restore — o formato dominante (comentário) funciona.

  4. Isolamento multi-usuário na Editbar via hook core↔projeto (§4): a regra de isolamento é específica de projeto e NÃO pode viver no core. Seguindo o sistema de hooks do Conn2Flow (gestor/bibliotecas/hooks.php, estilo WordPress: hook_apply_filters($namespace,$evento,$value,...$args)), o core apenas expõe um ponto de extensão: o helper dashboard_site_toolbar_verificar_permissao_pagina($pagina) retorna hook_apply_filters('dashboard','site-toolbar.permissao-pagina', true, $pagina) (default true — o core não restringe; guard function_exists('hook_apply_filters')). As 4 rotas (site-toolbar-render/-save/-backups/-backup-get) chamam o helper; os selects passam a trazer id_usuarios/publisher_id, e backup-get recebe page_id do contexto (propagado pelo cliente a partir do openBackupPanel) para resolver a página dona. O projeto (Conn2Flow Site) consome o filtro: novo handler multiusuario_dashboard_site_toolbar_permissao_pagina_filter($permitido,$pagina) em gestor/project/hooks/controllers/multiusuario.hooks.php (registrado em project/hooks/hooks.json → controllers.dashboard + filters.dashboard["site-toolbar.permissao-pagina"]), que aplica o mesmo isolamento dos hooks admin-paginas/publisher-pages: acesso completo (acesso-completo-a-paginas/admin-paginas ou acesso-completo-a-paginas-publisher/publisher-pages, conforme publisher_id) opera qualquer página; usuário restrito só a própria (id_usuarios = gestor_usuario()['id_usuarios']). Correção de rodada: a 1ª implementação colocou multiusuario_* diretamente no dashboard.php (core) — acoplamento indevido; movida para o repo do projeto via hook, mantendo o core agnóstico e reutilizável por outros projetos.

Cache-bust: interface/html-editor.js?v=c2f9→?v=c2f10, biblioteca-html-editor 1.4.9→1.4.10. Validação: php -l (dashboard.php/html-editor.php) OK; node --check (html-editor.js/dashboard.toolbar.js) OK; npx vitest run 19/19 (à época; ver DEC-085 para o total atual) (novos: insertTemplate seleciona o 1º elemento em replace/before/after e ignora texto; ponte widget-render→AJAX→widget-rendered + guarda de vazios; restorePageBackup re-anota o widget no pai consumindo os comentários); composer test 76/76 (287 assertions, 4 skipped) sem regressão. Runtime (deploy Update => Core + homologação visual dos 4 itens + teste de bloqueio multi-usuário em projeto com a lib) pendente com o operador. Nenhum git commit/git push executado.

DEC-085 - 2026-07-13 - accepted

Correções de Homologação do Live Editor (BATCH-083). Três correções pós-BATCH-082, demandadas diretamente pelo Engenheiro Chefe durante a homologação. Tudo no Dashboard Site Toolbar / Live Editor.

  1. Hover dos dropdowns robusto (hover-intent): os 3 dropdowns da toolbar (c2f-toolbar-menu/c2f-page-dropdown/c2f-user-dropdown) viviam DENTRO do iframe de 30px e a caixa (absolute top-full) só ficava visível por CSS (group-hover:block). Ao abrir, o iframe precisa crescer (via postMessage assíncrono ao host) para não recortar a caixa; até crescer, o mouse ao descer do trigger para a caixa "caía" fora do iframe e o :hover se perdia → a caixa sumia (corrida agravada pós-resize; reload "consertava" por ganhar a corrida). Decisão: hover-intent unificado (registerHoverDropdown) — a caixa é forçada visível por JS (classe c2f-dropdown-force-open, display:block!important no <style> dos 2 HTMLs) antes de medir a altura (offsetHeight confiável, independe do timing do :hover), e o fechamento é adiado 220ms (cancelável) — o mouse alcança a caixa enquanto o iframe cresce e, ao entrar nela (mouseenter do próprio group, que a contém), o timer é cancelado. Substitui os handlers antigos (menuOpen + mouseenter/mouseleave por dropdown). Novo window.resize no iframe reajusta a altura se há dropdown aberto.

  2. Preview de dispositivo fiel via iframe (screenPagina): mudar só a largura do #c2f-layout-root (elemento) NÃO dispara media queries (@media), unidades vw/vh nem window.resize — todas usam a viewport, não o elemento. Decisão (escolha do Chefe entre iframe-fiel × largura-de-elemento): iframe fiel. setEditScreen renderiza o conteúdo dentro de um IFRAME (#c2f-device-preview) cuja largura é a do device → a viewport do iframe passa a ser a largura do device e o CSS responsivo/JS de resize respondem de verdade. O srcdoc reúne o CSS do site (<link rel=stylesheet>/<style> do <head> + <base href>), sem <script> (evita reexecutar JS/reinjetar a toolbar no iframe). O conteúdo vem de getCleanHtml() (fallback innerHTML), refletindo as edições atuais. Desktop (100%) volta à edição in-place; nos modos device o editor é desabilitado (preview fiel, não editável) — trade-off aceito pelo Chefe. Alternar Tablet↔Mobile só troca a largura do iframe (media queries re-avaliam sem recarregar).

  3. Normalização de variáveis/widgets digitados no save: no template a notação é [[var]] (openText/closeText) e no banco é @[[var]]@ (open/close, com cerco @). Variáveis vindas das caixas já são reconstruídas com o cerco (via data-c2f-marker); as que o usuário digita à mão no editor ([[pagina#url-raiz]], [[widgets#slug]]) ficavam sem o cerco e não renderizavam. Decisão: passe final no reconstructOriginal que normaliza toda [[x]]→@[[x]]@ com a regex /@?\[\[([\s\S]+?)\]\]@?/g — idempotente (@[[x]]@→@[[x]]@, sem duplicar o cerco) e cobrindo variáveis e widgets inline. Não afeta os comentários de widget (<!-- widgets#… -->, sem [[).

Cache-bust: dashboard.json versao 1.0.9→1.0.10 (busta dashboard.toolbar.js e dashboard.iframe-toolbar.js). Validação: node --check (dashboard.iframe-toolbar.js/dashboard.toolbar.js) OK; dashboard.json JSON válido; npx vitest run 20/20 (novo caso: normaliza [[x]]→@[[x]]@, idempotente, cobre widget inline). Sem mudança de PHP neste lote. Runtime (deploy Update => Core + homologação visual dos 3 itens) pendente com o operador. Nenhum git commit/git push executado.

DEC-086 - 2026-07-13 - accepted

Restauração de Backup Server-Side e Preview de Dispositivo com JS do Site (BATCH-085). Duas correções pós-BATCH-083, demandadas diretamente pelo Engenheiro Chefe.

  1. Restauração de backup SERVER-SIDE (substitui a injeção client-side do BATCH-082 §3): injetar o HTML do backup no cliente (restorePageBackup/restoreLayoutBackup+mapTree) quebrava o DOM/scripts ao trocar de backup. Decisão (fluxo proposto pelo Chefe): (a) o front (restoreBackup) deixa de injetar e passa a sinalizar o backup à rota site-toolbar-backup-restore (dashboard_ajax_site_toolbar_backup_restore grava gestor_sessao_variavel('site-toolbar-backup-restore', {id,type,caminho}) após validar permissão/propriedade) e dá location.reload(); (b) no reload, gestor_site_toolbar_backup_aplicar(&$paginas) (core, chamada logo após o hook roteador.paginas) — para usuário logado em page load normal — lê a sessão, confere o caminho, consome a sinalização (uma vez), busca backup_campos.valor e substitui o HTML da PÁGINA ($paginas[0]['html'] + override $_GESTOR['site-toolbar-backup-page-html'] para cobrir dev-env, que lê do arquivo) ou do LAYOUT ($_GESTOR['site-toolbar-backup-layout-html'], aplicado onde gestor_layout monta), setando o flag JS gestor.siteToolbarBackupRestaurado; (c) o backup é renderizado pelo pipeline normal do gestor (robusto); (d) o dashboard.iframe-toolbar.js lê window.parent.gestor.siteToolbarBackupRestaurado (same-origin) no init e chama setEdit(true) → reentra em edição já com o backup. Removidas as funções client-side restorePageBackup/restoreLayoutBackup/remapAfterRestore e o teste correspondente. A lógica ficou no roteador do CORE (não em hook) porque o dashboard é módulo do core (acoplamento core↔core aceitável) e evita a dependência de deploy da tabela hooks; ainda assim, guardada por early-returns (não-ajax + logado) para não onerar visitantes.

  2. Preview de dispositivo com JS do site (corrige o BATCH-083 §2): o srcdoc incluía só o CSS do <head>, sem os <script> do site → interações JS (menu hambúrguer) não funcionavam no preview. Decisão: enterDevicePreview passa a carregar a PRÓPRIA página no iframe (iframe.src = location + '?c2f-device-preview=1') — layout + CSS + JS reais, media queries/vw/vh fiéis; gestor_dashboard_toolbar_ativo() (core) reconhece c2f-device-preview e não injeta a toolbar/editor (evita a recursão do iframe da barra). Trade-off: o preview reflete a versão salva (recarrega do servidor); as edições não salvas ficam preservadas no DOM real do host (só ocultado) e reaparecem ao voltar ao Desktop. Removidos collectSiteCss/buildPreviewDoc (srcdoc). Segue preview não-editável (edição no Desktop) — editar no device exigiria reescopar o editor para dentro do iframe (não feito).

Cache-bust: dashboard.json versao 1.0.10→1.0.11. Validação: php -l (gestor.php/dashboard.php) OK; node --check (dashboard.toolbar.js/dashboard.iframe-toolbar.js) OK; dashboard.json válido; npx vitest run 19/19 (removido o teste de restorePageBackup, agora obsoleto); composer test 76/76 (287 assertions, 4 skipped) sem regressão. Runtime (deploy Update => Core + homologação: restauração de backup página/layout com reentrada em edição, isolamento multi-usuário, e preview device com hambúrguer) pendente com o operador. Nenhum git commit/git push executado.

DEC-089 - 2026-07-15 - accepted

Criação dos Módulos/Widgets forms-search (Formulários de Busca) e pages-index (Páginas Índice) (req-088 / BATCH-088). Dois módulos clonados de bases consolidadas e adaptados. Decisões desta rodada:

  1. pages-index consulta DIRETAMENTE a tabela paginas (não publisher_pages): o widget (reescrito) substitui o INNER JOIN paginas × publisher_pages do publisher-index por uma consulta direta a paginas filtrando páginas públicas e ativas (status='A' AND language='$language' AND tipo='pagina' AND sem_permissao=1). Removidos os conceitos de publisher_id, curadoria manual (rule/selected_items/Sortable) e tipos de campo de imagem — não se aplicam a uma listagem de todas as páginas. Ordenação por title_asc/desc (nome) e date_asc/desc (data_modificacao). O CRUD admin foi enxugado (sem dropdown de publicador nem seção de curadoria); os campos de item viraram fixos (campos-load AJAX).

  2. Variáveis de item fixas [[item#title/summary/url/date]]: title=paginas.nome; summary=strip_tags(paginas.html) normalizado e truncado em ~200 caracteres (com reticências); url=url-raiz+caminho; date=data_modificacao formatada (conveniência além das 3 obrigatórias do intake). O variable_mapping do schema continua permitindo aliases.

  3. Busca por GET no forms-search, com registro no destino: o widget (herdando a dinamicidade de campos do forms) renderiza <form method="get"> cujo action é a página alvo (fields_schema.form_action, configurável no CRUD via #form_action, default busca/) e garante um campo name="search" na saída (forms_search_widget_garantir_campo_search converte o 1º input text quando o template herdado não tem). NÃO usa o controlador de submissão AJAX/reCAPTCHA do forms (esse é para POST) — o submit é navegação nativa do navegador. Analytics: quem grava em forms_search_submissions é o widget pages-index (ponto de captura garantido do destino), sempre que processa uma busca — no page load com ?search= e no AJAX de nova busca (página 1, termo não-vazio; paginação não infla o log). A função registradora forms_search_registrar_busca() vive no forms-search (dono da tabela) e é invocada pelo pages-index com guard function_exists + require_once sob demanda (desacoplamento: sem o forms-search, a listagem segue funcionando). Confirmado com o Engenheiro Chefe.

  4. Clonagem por script com placeholders (forms → forms-search): como forms é ambíguo — vira forms_search (tabela/funções, underscore) OU forms-search (módulo/target/arquivos, hífen) — a substituição usou tokens temporários (@@FSPFX@@/@@FSIDNUM@@) e ordem id_forms→forms_→\bforms\b→correção de tabela→restauração, evitando reentrância (forms_search_search). O clone publisher-index → pages-index foi trivial (sem sobreposição entre publisher_index e publisher-index), preservando publisher_id/publisher_pages/publisher intactos para reescrita posterior.

  5. Página semente pública em busca/ (não pages-index/): o intake pedia caminho pages-index/, mas esse slug é a rota administrativa (page type=system option=listar) do módulo; declarar uma página pública com o mesmo caminho colidiria no compilador (índice de caminho único por idioma → órfão). Adotado busca/ (type=pagina, without_permission=1, layout pagina-simples-tailwindcss), com o widget apontando para o índice grupo_slug="padrao". O action default do forms-search acompanha (busca/). Precedente "seguir o código/robustez, não a prosa do intake". Passo runtime: criar o registro pages_index id='padrao' para a semente renderizar.

  6. Sem tocar o core html-editor.php: pages-index e forms-search caem no default do switch de alvo do html-editor (como o publisher-index já faz) — o editor de código funciona sem a aba de variáveis visual. Evita alterar o core e minimiza risco; o render público resolve as variáveis normalmente.

  7. Migração e registro: 20260715120000_create_forms_search_and_pages_index_tables.php cria forms_search (espelha forms), forms_search_submissions (espelha forms_submissions, log) e pages_index (espelha publisher_index sem publisher_id), todas com índice único [id, language] e down() reversível. Os módulos foram registrados em ModulosData.json (pt+en) e UsuariosPerfisModulosData.json; os demais *Data.json derivados são populados pelo pipeline a partir dos manifests. Cobertura: novo PagesIndexFormsSearchWidgetTest (14 testes, funções puras dos dois widgets). Validação: php -l 5/5, node --check 4/4, JSON 4/4, composer test 90/90 (314 assertions, 4 skipped). Deploy/migração/criação do índice padrao e homologação runtime pendentes com o operador.

DEC-090 - 2026-07-17 - accepted

Transição do Gerenciador de Arquivos para Árvore Física no Disco e CRUD Completo de Diretórios no Admin-Arquivos (req-090 / BATCH-090). Decisões desta rodada:

  1. Fonte de verdade = disco, não a tabela arquivos: o módulo admin-arquivos passa a listar/gravar/excluir varrendo fisicamente a árvore de $_GESTOR['contents-path'] (scandir, filemtime, filesize). Nenhum registro individual de arquivo é lido/gravado em arquivos no novo fluxo. A tabela arquivos (e a legada arquivos_categorias) permanecem intactas apenas para retrocompatibilidade de registros antigos consultados pelo Image Picker.

  2. Identificador do arquivo = CAMINHO RELATIVO (com hash MD5 para indexação): a associação arquivo↔categoria usa o caminho relativo à raiz de conteúdos. Foi criada a tabela nova arquivos_disco_categorias (caminho VARCHAR(1024) + caminho_hash CHAR(32)=MD5(caminho), indexado + id_categorias, índice único [caminho_hash,id_categorias]) — migração 20260717120000. Optou-se por tabela NOVA (não reusar arquivos_categorias, que referencia id_arquivos) para não perturbar o legado. O filtro por categoria cruza as associações do banco com os arquivos físicos da pasta consultada.

  3. Helpers PUROS de segurança na biblioteca bibliotecas/arquivo.php (independentes do framework, testáveis): arquivo_nome_sanitizar (remove : * ? " < > | / \, controles e componentes de diretório via split determinístico em /+\), arquivo_extensao_perigosa (blocklist de executáveis — php/phtml/phar/py/sh/exe/.htaccess… inclusive dupla extensão a.php.jpg/a.jpg.php), arquivo_caminho_relativo_seguro (rejeita .., caminho absoluto POSIX/Windows e byte nulo; higieniza cada segmento), arquivo_caminho_resolver (junta base+relativo e confirma via realpath que o alvo não escapa da base), arquivo_mini_caminho_relativo e arquivo_tipo_por_extensao. Toda rota do backend (listar/upload/miniaturas/excluir/pasta-criar/renomear/categorias) valida a entrada por esses helpers antes de tocar o disco. Diretriz mandatória do Engenheiro Chefe atendida (path traversal, uploads perigosos, sanitização de nomes).

  4. Miniaturas físicas lazy em <pasta>/mini/<nome> via AJAX em lotes: ao listar, o backend identifica imagens sem miniatura (thumbsMissing); o frontend exibe a barra Processando miniaturas... e dispara lotes (ajaxOpcao=miniaturas, padrão 5/lote) que geram as minis com SimpleImage e devolvem a URL com cache-bust por ?t=filemtime (diretriz do Chefe). Upload de imagem também gera a mini imediatamente (best-effort). A subpasta mini/ é ocultada da listagem.

  5. Renderização client-side a partir de JSON (não server-side de componente): para suportar os 4 modos de visualização (grande/médio/pequeno/listagem) alternáveis sem round-trip, o backend ajaxOpcao=listar devolve JSON (pastas + arquivos + breadcrumb + paginação + thumbsMissing) e o admin-arquivos.js renderiza os itens por template, aplicando o modo via classe no container (persistido em localStorage). Diverge do padrão legado (componente arquivos-lista server-side, agora não usado no fluxo novo) por ser a escolha correta para os 4 modos + miniaturas incrementais + seleção em lote. Paginação de leitura física aplicada aos arquivos (padrão 60/página; pastas na 1ª página) — diretriz do Chefe.

  6. CRUD de diretórios sem limite de profundidade: rotas pasta-criar (herda permissão do pai), renomear (arquivo/pasta; move as associações de categoria por prefixo; preserva extensão e rebloqueia extensão perigosa) e excluir (lote; pasta não-vazia exige recursivo=true, confirmado no frontend; limpa mini e associações). Seleção em lote via checkboxes + exclusão em massa.

  7. Retrocompatibilidade do Image Picker sem alterar cada consumidor: o picker (iframe admin-arquivos/?paginaIframe=sim) passa a devolver o caminho relativo como identificador (data.id = caminho). Na renderização de campos imagepick (interface.php), quando o identificador salvo não é numérico ele é tratado como caminho físico; sem registro no banco, os metadados vêm do arquivo em disco (filemtime/basename/mime_content_type), preferindo a miniatura física (mini/…?t=…). Identificadores numéricos legados continuam resolvidos pela tabela arquivos. O interface.js não precisou mudar (já encaminhava id e caminho). Extensões perigosas também são bloqueadas no cliente (defesa em profundidade; autoridade é o servidor).

Validação: php -l 5/5, node --check OK, JSON válido, novo AdminArquivosSegurancaTest 16/16 (53 assertions), composer test 106/106 (367 assertions, 4 skipped). Deploy (Update => Core + migração 20260717120000) e homologação runtime (4 modos, drag&drop, miniaturas, CRUD de pastas, picker retrocompatível) pendentes com o operador.

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