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:
-
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>),mapTreemarca o próprioliveParent(novodata-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 temdata-c2f-widget-parent, substitui oinnerHTMLdo pai pelo marcador cru decodificado (preserva a tag externa<nav>/<ul>+ atributos), em vez de trocar o nó. Guardas: só entra em modo-pai seK===1, há>1elementos-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). -
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 âncoraafterSigcasar dentro do próprio 1º widget e fundir os dois. Solução:collectWidgetGroupagrupa 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ópriodata-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 literalstructSig===afterSigprescrita, que não casaria — a contagem de cauda é aplicada na âncora estática após o grupo. -
Image picker autônomo no modal do live editor (item 3): o modal fallback (
ensureFallbackModal) ganha o botão_html-editor-imagepick-btnao 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 awindow.parent= a janela hospedeira) é capturada por um listener próprio (data.moduloId==='admin-arquivos',JSON.parse(decodeURI(data.data)), filtroimage/*) e preenche o#element-src. Araizchega pela nova opçãoraizdo construtor deHtmlEditorClass(passada pordashboard.toolbar.js); semraiz, cai nowindow.prompt(retrocompat). -
Í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 emhtml-editor.js— accordion do styler (dropdown), bgimage (folder open/trash) e os botõeskind:'icon'do styler (align *,ban,underline,strikethrough,arrows alternate *). Independem da fonte Fomantic (ausente na página pública). -
Modal de edição redimensionável (item 4): a caixa interna do modal fallback recebe
resize:both;overflow:auto+min/max-width/heightewidthexplícita (arrasto pelo canto inferior direito), com header/footerflex:0 0 autoe corpoflex:1 1 auto. -
Filtro + agrupamento por categoria no menu de módulos (itens 5+6):
dashboard_site_toolbar_menureusa a fonte degestor_pagina_menu— agrupa os módulos permitidos pormodulos_grupos(ordenados porordemMenu), com cabeçalhos.c2f-group-headernã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: listenerinputfiltra.c2f-menu-item(case-insensitive, portextContent) e oculta cabeçalhos de grupos sem itens visíveis, reajustando a altura do iframe (pushHeight). -
Backups divididos Página × Layout com restauração independente (item 8):
dashboard_ajax_site_toolbar_backupsretornapage_backups(módulo dono,id_paginas) +layout_backups(móduloadmin-layouts,id_layoutsdo layout vinculado).dashboard_ajax_site_toolbar_backup_getaceitatype=page|layout: layout → extrai o body-inner dolayouts.htmldo backup, renderiza as caixas legadas (.c2f-dyn-box, compatíveis comreconstructOriginal) e injeta um slot#c2f-page-contentvazio no lugar de@[[pagina#corpo]]@. Frontend: painel de 2 colunas; backup de página → só#c2f-page-content.innerHTML; backup de layout →restoreLayoutBackupinjeta o body-inner no#c2f-layout-rootpreservando o#c2f-page-contentvivo (destaca e re-encaixa no slot). -
Cache-bust: live carrega
interface/html-editor.js?v=c2f3→?v=c2f4; admin bumpabiblioteca-html-editor1.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.
-
gestor_pagina_widgetsmantém os comentários no modo edição (conn2flow/gestor.php): quandogestor_dashboard_toolbar_ativo()é verdadeiro (editor logado), a substituição do bloco de widget passa derenderpara<!-- 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#…]]@). -
mapTreedelimita o widget pelos PRÓPRIOS comentários vivos (dashboard.toolbar.js): eliminadoscollectWidgetGroup, o tail alignment, a divisão igualitária e a funçãostructSig. O branch de widget casa o comentário de abertura vivo pela mesma assinatura a partir deli(o que separa naturalmente widgets duplicados/consecutivos — cada um tem seu par de comentários), marca os nós entreopen/closee 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). OmarkerHtml(com o mockup do designer) continua vindo do cru, preservado no save.reconstructOriginalinalterado. -
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-childafetados (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):
-
resolveEditable→ raiz do grupo: qualquer elemento comdata-c2f-widget-idresolve para o elemento comdata-c2f-widget-rootdo mesmo grupo. Clicar em qualquer link seleciona o widget inteiro (um átomo); links individuais deixam de ser selecionáveis. -
Overlay abrange a união do grupo: novos helpers
elementRect/unionRect—positionOverlayeupdateSelectionUIusam a união dos bounding boxes de todos os[data-c2f-widget-id="gid"], então hover/seleção/toolbar cobrem o widget inteiro. -
CSS: outline em
[data-c2f-widget-id](todos os elementos do grupo mostram a extensão) +[data-c2f-widget-root]::beforemantém o label único. Validado portests/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:
-
Recursos gated ao Live Editor por
this.raiz:createToolbarsó injeta os botões.he-tb-templatese.he-tb-aiquandothis.raizestá presente (contexto do live editor, passado pordashboard.toolbar.js). O editor visual do admin (semraiz) 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 rotassite-toolbar-*dodashboard.php, derivadas dethis.raiz + 'dashboard/'. -
Backend reusa o clássico, sem duplicar lógica:
dashboard.phpganha 5 rotas —site-toolbar-templates-load(→html_editor_ajax_templates_load),site-toolbar-ia-init(→ novoia_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 comgestor_acesso('editar','admin-paginas')+gestor_incluir_biblioteca. Novoia_editor_dados($alvo)emia.phpdevolve prompts/modos/conexões/modelos como arrays JSON-friendly (espelha a coleta deia_renderizar_promptsem gerar HTML Fomantic).html_editor_ajax_templates_loadpassou a aceitarparams[busca](LIKE por nome) — retrocompatível (o editor clássico não enviabusca). -
Painéis vanilla blindados:
#c2f-tpl-panele#c2f-ai-panelsã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");insertTemplateinsere 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 porsite-toolbar-ia-init, textarea de instrução, decodificação de entidades via textarea-trick (as rotasia_ajax_prompts/modosemodo_padraoretornamhtmlentities);submitAienviaouterHTMLdo elemento +mode/prompt/server_id/model,applyAiResulttroca o elemento pelo HTML gerado e injeta o CSS. -
Background image picker autônomo (item 4):
requestBackgroundImageno live (this.raiz) usaopenLiveImagePickercomimagePickerTarget='background';bindLiveImagePickerroteia a seleção —'background'→applyBackgroundImage(url), senão preenche#element-src. Remove a dependência dowindow.prompt/html_editor.imagepick(ausentes no live)..he-bgimage-clearsegue emclearBackgroundImage. -
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).
-
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 dedashboard_start()estavam embrulhados emif(gestor_dashboard_toolbar_ativo()){…}, mas esse gate é FALSE na própria página do toolbar (guard anti-recursãoopcao=='dashboard-site-toolbar') — entãodashboard_site_toolbar()não rodava, deixando#page_id#/#advanced_edit_url#/<!-- menu -->literais e não injetando odashboard.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. -
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-btnnos 2 HTMLs →dashboard.iframe-toolbar.jspostac2f-toolbar:edit-templates/edit-ai→dashboard.toolbar.jschamac2fEditor.openTemplatesPanel()/openAiPanel(). Removidos os botões.he-tb-templates/.he-tb-aida floating toolbar dohtml-editor.js(e o gatethis.raizdeles). Os HTMLs vivem no banco → exigemUpdate => Corepara sincronizar. -
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 nodocument.body(fora docontentRoot), o listener de clique em capture dohtml-editor.js(que usaelementsFromPoint) 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 aisEditorOwned→ early-return no clique/hover, os cliques ficam no painel. Regra geral (reforço): TODA UI do live editor criada nodocument.bodyPRECISA entrar emisEditorOwned. Cache-bust?v=c2f7+biblioteca-html-editor1.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):
-
CodeMirror (markdown) + caixa redimensionável no Assistente IA (§1):
initAiCodeMirrorinstancia CodeMirror sobre#c2f-ai-instructione#c2f-ai-mode-text(modomarkdown, tematomorrow-night-bright, dedup por.CodeMirrorirmão) apenas quandowindow.CodeMirrorexiste — no Live Editor ele é carregado pordashboard.toolbar.js(que passou a incluirmode/markdown/markdown.js+mode/javascript/javascript.js); sem CodeMirror, degrada para os textareas. Os acessos passaram por helpersaiGetInstruction/aiSetInstruction/aiGetMode/aiSetMode(CodeMirrorgetValue/setValuecom fallback ao.value), usados porsubmitAi,loadAiInite 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. -
Causa-raiz do "Erro ao salvar a página." intermitente (§2): o alert vinha do
.catch()dosaveEdit— ou seja,r.json()falhava por corpo não-JSON, sem erro de rede (HTTP 200) e sem log do PHP (comdisplay_errors=on/log_errors=off, warnings/notices vão ao corpo da resposta, não ao log). Ogestor.phpecoavajson_encode($_GESTOR['ajax-json'])sem buffer de saída (não háob_startglobal no bootstrap), então qualquer saída espúria emitida durante a execução do módulo/gestor_pagina_widgets_ajaxprefixava 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 numob_start()e descartar o buffer (ob_end_cleanaté o nível pré-buffer) imediatamente antes doecho json_encode(..., JSON_INVALID_UTF8_SUBSTITUTE); a flag também evitajson_encoderetornarfalsecom UTF-8 inválido (que geraria corpo vazio). Defesa em profundidade no cliente (dashboard.toolbar.js): (a)deselectAll()do editor antes dogetCleanHtml; (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. NovodeselectAll()nohtml-editor.js(sai do modo inserção, limpa seleção/hover/contornos/wrap-menu). -
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 emgestor.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-editpreservado), Editar Avançado (c2f-toolbar-edit-advancedpreservado), Criar Nova Página (#new_page_url#) e Clonar Página (#clone_page_url#) — publisher-pages vs admin-paginas resolvido emdashboard_site_toolbar()(#page_title#depaginas.nome). O toggle vive emdashboard.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 (iframeHeightconsideraopenDropdown). -
CRUD de prompts no Assistente IA (§4): botões
ai-prompt-new/edit/del/clearno painel;aiPromptNew/Edit/Del/ClearPOSTamparams[...]para 3 rotas novas dodashboard.php—site-toolbar-ia-prompt-new/-edit/-del— que reusamia_ajax_prompt_novo/_edit/_del(targetpaginas). O<select>de prompts é atualizado após cada operação. "Limpar" só zera a instrução. -
Código Customizado no painel "+" (§5): item "Código Customizado" na coluna de elementos →
c2fEditor.openCustomCodePanel()(modal.c2f-he-live-boxcom CodeMirrorhtmlmixed);insertCustomHtml(html)insere o bloco no DOM vivo após a seleção (ou ao fim docontentRoot), estiloc2f-add-el.#c2f-custom-panelentra emisEditorOwned. -
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_listpassou a delegar ao novohtml_editor_widgets_buscar()(paginaçãopagina/limite, buscaname/id LIKE, cross-grupo quandomodulevazio, anti-injection nos nomes de tabela/coluna), resposta{status, data:{items:[{id,nome,module}], tem_mais}}. O limite vem da opção do módulosite_toolbar.widgets_por_pagina(padrão 10) nodashboard.json. O editor clássico segue usandohtml_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):
-
Causa-raiz REAL do save "302 → home" (confirmada por backtrace; substitui as hipóteses de buffer E de request_order): instrumentando
gestor_redirecionarno mirror, a cadeia foidashboard_ajax_site_toolbar_save→interface_historico_incluir()→interface_modulo_variavel_valor()→gestor_redirecionar_raiz(). Semid_numerico_manual/sem_id,interface_historico_incluirresolve o id numérico viainterface_modulo_variavel_valor, que exige$_GESTOR['modulo-registro-id'](contexto do CRUD admin) — ausente no AJAX do site toolbar → redirect 302text/html(ogestor_redirecionarnão checaajax), seguido para a home. Intermitente porque o histórico só roda quando conteúdo/layout muda. Correção real (mínima): passarid_numerico_manual($pagina['id_paginas']/$layout['id_layouts']) nas 2 chamadas deinterface_historico_incluir(página emdashboard_ajax_site_toolbar_save, layout emdashboard_site_toolbar_salvar_layout). Hipóteses descartadas: (a) NÃO era warning poluindo o JSON (log PHP vazio; blindagemob_startemgestor.phprevertida); (b) NÃO erarequest_ordersem "P" (php -i→request_order=no value→ fallbackvariables_order=EGPCSinclui 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. -
Dropdowns de Usuário/Página no HOVER: passaram a abrir no hover (
group+group-hover:block), igual aoc2f-toolbar-menu, em vez de só no clique.dashboard.iframe-toolbar.jssó ajusta a altura do iframe (mouseenter/mouseleave →openDropdown). -
Resize-follow dos campos do modal + fix do loop:
ResizeObserverem.c2f-he-modal-boxfazelement-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.
-
Ponte de renderização de widget (§1): o motor (
html-editor.js), ao inserir um widget, já postava a string JSONc2f-he:widget-renderparawindow.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: rotasite-toolbar-widget-render→dashboard_ajax_site_toolbar_widget_render()(checagestor_acesso('editar','admin-paginas'), inclui a lib e delega ahtml_editor_ajax_widget_render(), que já resolviamodulo->func({...})em modo page-load), e um listener dedicado nodashboard.toolbar.jsque só trata mensagens string (o listener principal trata os objetosc2f-toolbar:*do iframe): faz POST na rota (params[signature]) e devolvec2f-he:widget-rendered(string JSON) para a própriawindow→ o motor chamaapplyWidgetRender. O handler filtraaction==='c2f-he:widget-render'(não entra em laço com a própria resposta) e retorna a promise do fetch (testabilidade). -
Seleção automática do modelo inserido (§2):
insertTemplatepassou 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óreplaceselecionava — os modosbefore/afterdeixavam o modelo sem seleção, exigindo clique manual. -
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_widgetspassa a embrulhar cada widget nos marcadores de comentário vivos<!-- widgets#SIG < -->{render}<!-- widgets#SIG > -->(formato que omapTreedo frontend delimita/anota), em vez da div.c2f-widget-box— muda também o output do motor legado de caixas norender, que não é consumido pelo frontend (usacontent_raw/layout_raw), sem regressão; (b) novo helperdashboard_site_toolbar_resolver_variaveis($html)resolve@[[var]]@para o valor puro (texto e atributo, sem.c2f-var-box), espelhando a página viva, para omapTreere-anotar as variáveis a partir doraw(usar.c2f-var-boxnohtmlcausaria duplicação de variável ao re-mapear); (c)dashboard_ajax_site_toolbar_backup_getdevolve tambémraw(HTML cru salvo, marcadores intactos; para layout,html/rawalinham o slot vazio#c2f-page-content); (d) no frontend,restorePageBackup(html,raw)/restoreLayoutBackup(html,raw)chamamdeselectAll(), injetamhtmlno DOM vivo erawno container oculto de mapeamento (#c2f-raw-contentpara o conteúdo; backup inteiro para o layout), re-rodammapTree()e chamamrestoreChrome(). Divergência do intake (robustez, não a prosa): ovarMapnão é zerado — o intake pediavarMap={}, 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 ovarMapre-anota só a subárvore restaurada e deixa entradas antigas como lixo inofensivo (oreconstructOriginalignora 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. -
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 helperdashboard_site_toolbar_verificar_permissao_pagina($pagina)retornahook_apply_filters('dashboard','site-toolbar.permissao-pagina', true, $pagina)(defaulttrue— o core não restringe; guardfunction_exists('hook_apply_filters')). As 4 rotas (site-toolbar-render/-save/-backups/-backup-get) chamam o helper; os selects passam a trazerid_usuarios/publisher_id, ebackup-getrecebepage_iddo contexto (propagado pelo cliente a partir doopenBackupPanel) para resolver a página dona. O projeto (Conn2Flow Site) consome o filtro: novo handlermultiusuario_dashboard_site_toolbar_permissao_pagina_filter($permitido,$pagina)emgestor/project/hooks/controllers/multiusuario.hooks.php(registrado emproject/hooks/hooks.json→controllers.dashboard+filters.dashboard["site-toolbar.permissao-pagina"]), que aplica o mesmo isolamento dos hooksadmin-paginas/publisher-pages: acesso completo (acesso-completo-a-paginas/admin-paginasouacesso-completo-a-paginas-publisher/publisher-pages, conformepublisher_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 colocoumultiusuario_*diretamente nodashboard.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.
-
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:hoverse 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 (classec2f-dropdown-force-open,display:block!importantno<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ópriogroup, que a contém), o timer é cancelado. Substitui os handlers antigos (menuOpen+ mouseenter/mouseleave por dropdown). Novowindow.resizeno iframe reajusta a altura se há dropdown aberto. -
Preview de dispositivo fiel via iframe (
screenPagina): mudar só a largura do#c2f-layout-root(elemento) NÃO dispara media queries (@media), unidadesvw/vhnemwindow.resize— todas usam a viewport, não o elemento. Decisão (escolha do Chefe entre iframe-fiel × largura-de-elemento): iframe fiel.setEditScreenrenderiza 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. Osrcdocreú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 degetCleanHtml()(fallbackinnerHTML), 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). -
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 (viadata-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 noreconstructOriginalque 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.
-
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 à rotasite-toolbar-backup-restore(dashboard_ajax_site_toolbar_backup_restoregravagestor_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 hookroteador.paginas) — para usuário logado em page load normal — lê a sessão, confere ocaminho, consome a sinalização (uma vez), buscabackup_campos.valore 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 ondegestor_layoutmonta), setando o flag JSgestor.siteToolbarBackupRestaurado; (c) o backup é renderizado pelo pipeline normal do gestor (robusto); (d) odashboard.iframe-toolbar.jslêwindow.parent.gestor.siteToolbarBackupRestaurado(same-origin) no init e chamasetEdit(true)→ reentra em edição já com o backup. Removidas as funções client-siderestorePageBackup/restoreLayoutBackup/remapAfterRestoree o teste correspondente. A lógica ficou no roteador do CORE (não em hook) porque odashboardé módulo do core (acoplamento core↔core aceitável) e evita a dependência de deploy da tabelahooks; ainda assim, guardada por early-returns (não-ajax + logado) para não onerar visitantes. -
Preview de dispositivo com JS do site (corrige o BATCH-083 §2): o
srcdocincluía só o CSS do<head>, sem os<script>do site → interações JS (menu hambúrguer) não funcionavam no preview. Decisão:enterDevicePreviewpassa 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) reconhecec2f-device-previewe 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. RemovidoscollectSiteCss/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:
-
pages-indexconsulta DIRETAMENTE a tabelapaginas(nãopublisher_pages): o widget (reescrito) substitui o INNER JOINpaginas × publisher_pagesdo publisher-index por uma consulta direta apaginasfiltrando páginas públicas e ativas (status='A' AND language='$language' AND tipo='pagina' AND sem_permissao=1). Removidos os conceitos depublisher_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 portitle_asc/desc(nome) edate_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-loadAJAX). -
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_modificacaoformatada (conveniência além das 3 obrigatórias do intake). Ovariable_mappingdo schema continua permitindo aliases. -
Busca por GET no
forms-search, com registro no destino: o widget (herdando a dinamicidade de campos do forms) renderiza<form method="get">cujoactioné a página alvo (fields_schema.form_action, configurável no CRUD via#form_action, defaultbusca/) e garante um camponame="search"na saída (forms_search_widget_garantir_campo_searchconverte 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 emforms_search_submissionsé o widgetpages-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 registradoraforms_search_registrar_busca()vive noforms-search(dono da tabela) e é invocada pelopages-indexcom guardfunction_exists+require_oncesob demanda (desacoplamento: sem o forms-search, a listagem segue funcionando). Confirmado com o Engenheiro Chefe. -
Clonagem por script com placeholders (forms → forms-search): como
formsé ambíguo — viraforms_search(tabela/funções, underscore) OUforms-search(módulo/target/arquivos, hífen) — a substituição usou tokens temporários (@@FSPFX@@/@@FSIDNUM@@) e ordemid_forms→forms_→\bforms\b→correção de tabela→restauração, evitando reentrância (forms_search_search). O clonepublisher-index → pages-indexfoi trivial (sem sobreposição entrepublisher_indexepublisher-index), preservandopublisher_id/publisher_pages/publisherintactos para reescrita posterior. -
Página semente pública em
busca/(nãopages-index/): o intake pedia caminhopages-index/, mas esse slug é a rota administrativa (pagetype=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). Adotadobusca/(type=pagina,without_permission=1, layoutpagina-simples-tailwindcss), com o widget apontando para o índicegrupo_slug="padrao". Oactiondefault doforms-searchacompanha (busca/). Precedente "seguir o código/robustez, não a prosa do intake". Passo runtime: criar o registropages_indexid='padrao'para a semente renderizar. -
Sem tocar o core
html-editor.php:pages-indexeforms-searchcaem nodefaultdo switch de alvo do html-editor (como opublisher-indexjá 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. -
Migração e registro:
20260715120000_create_forms_search_and_pages_index_tables.phpcriaforms_search(espelhaforms),forms_search_submissions(espelhaforms_submissions, log) epages_index(espelhapublisher_indexsempublisher_id), todas com índice único[id, language]edown()reversível. Os módulos foram registrados emModulosData.json(pt+en) eUsuariosPerfisModulosData.json; os demais*Data.jsonderivados são populados pelo pipeline a partir dos manifests. Cobertura: novoPagesIndexFormsSearchWidgetTest(14 testes, funções puras dos dois widgets). Validação:php -l5/5,node --check4/4, JSON 4/4,composer test90/90 (314 assertions, 4 skipped). Deploy/migração/criação do índicepadraoe 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:
-
Fonte de verdade = disco, não a tabela
arquivos: o móduloadmin-arquivospassa a listar/gravar/excluir varrendo fisicamente a árvore de$_GESTOR['contents-path'](scandir,filemtime,filesize). Nenhum registro individual de arquivo é lido/gravado emarquivosno novo fluxo. A tabelaarquivos(e a legadaarquivos_categorias) permanecem intactas apenas para retrocompatibilidade de registros antigos consultados pelo Image Picker. -
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(caminhoVARCHAR(1024) +caminho_hashCHAR(32)=MD5(caminho), indexado +id_categorias, índice único[caminho_hash,id_categorias]) — migração20260717120000. Optou-se por tabela NOVA (não reusararquivos_categorias, que referenciaid_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. -
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ãoa.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_relativoearquivo_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). -
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 barraProcessando miniaturas...e dispara lotes (ajaxOpcao=miniaturas, padrão 5/lote) que geram as minis comSimpleImagee devolvem a URL com cache-bust por?t=filemtime(diretriz do Chefe). Upload de imagem também gera a mini imediatamente (best-effort). A subpastamini/é ocultada da listagem. -
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=listardevolve JSON (pastas + arquivos + breadcrumb + paginação + thumbsMissing) e oadmin-arquivos.jsrenderiza os itens por template, aplicando o modo via classe no container (persistido emlocalStorage). Diverge do padrão legado (componentearquivos-listaserver-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. -
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) eexcluir(lote; pasta não-vazia exigerecursivo=true, confirmado no frontend; limpa mini e associações). Seleção em lote via checkboxes + exclusão em massa. -
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 camposimagepick(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 tabelaarquivos. Ointerface.jsnão precisou mudar (já encaminhavaidecaminho). 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.