Merge remote-tracking branch 'origin/main'

# Conflicts:
#	.claude-plugin/marketplace.json
#	.gitignore
#	acidaos/skills/devops/SKILL.md
#	acidaos/skills/docs/SKILL.md
#	acidaos/skills/rust-dev/SKILL.md
#	acidaos/skills/spoke-dev/SKILL.md
#	automacao/agents/automation-lead.md
#	automacao/agents/mcp-protocol-developer.md
#	automacao/agents/n8n-automation-expert.md
#	automacao/skills/moloni-api/SKILL.md
#	automacao/skills/n8n/SKILL.md
#	core-tools/skills/_core/SKILL.md
#	core-tools/skills/agent-context-injector/SKILL.md
#	core-tools/skills/component-generator/SKILL.md
#	core-tools/skills/db-maintenance-manager/SKILL.md
#	core-tools/skills/infrastructure-manager/SKILL.md
#	core-tools/skills/metrics/SKILL.md
#	core-tools/skills/plugin-curator/SKILL.md
#	core-tools/skills/plugin-manager/SKILL.md
#	core-tools/skills/plugin-manager/profiles.json
#	core-tools/skills/quality-validator/SKILL.md
#	core-tools/skills/relationship-manager/SKILL.md
#	crm-ops/.claude-plugin/plugin.json
#	crm-ops/CHANGELOG.md
#	crm-ops/agents/budget-proposal-specialist.md
#	crm-ops/agents/lead-approach-strategist.md
#	crm-ops/agents/lead-qualifier.md
#	crm-ops/agents/sales-manager.md
#	crm-ops/agents/support-specialist.md
#	crm-ops/skills/auto-expense/SKILL.md
#	crm-ops/skills/billing-check/SKILL.md
#	crm-ops/skills/crm-admin/SKILL.md
#	crm-ops/skills/crm/SKILL.md
#	crm-ops/skills/desk/SKILL.md
#	crm-ops/skills/estrategia-marca/SKILL.md
#	crm-ops/skills/estrategia-marca/references/metodologia-acida.md
#	crm-ops/skills/estrategia-marca/references/template-documento.md
#	crm-ops/skills/expense/SKILL.md
#	crm-ops/skills/invoice/SKILL.md
#	crm-ops/skills/orcamento/SKILL-light.md
#	crm-ops/skills/orcamento/SKILL.md
#	crm-ops/skills/proposal-deck/SKILL.md
#	crm-ops/skills/proposta-visual/SKILL.md
#	crm-ops/skills/ticket-manage/SKILL.md
#	crm-ops/skills/ticket-triage/SKILL.md
#	crm-ops/skills/ticket/SKILL.md
#	design-media/agents/design-critic.md
#	design-media/agents/design-generator.md
#	design-media/agents/design-lead.md
#	design-media/agents/design-prompt-architect.md
#	design-media/agents/design-researcher.md
#	design-media/agents/ui-designer.md
#	design-media/agents/video-production-specialist.md
#	design-media/agents/web-designer.md
#	design-media/skills/brand-guidelines/SKILL.md
#	design-media/skills/brand-guidelines/references/descomplicar-theme.md
#	design-media/skills/cinematic-site/SKILL.md
#	design-media/skills/clone-style/SKILL.md
#	design-media/skills/design-b2b/SKILL.md
#	design-media/skills/design-ecommerce/SKILL.md
#	design-media/skills/design-saude/SKILL.md
#	design-media/skills/design-solar/SKILL.md
#	design-media/skills/design/SKILL.md
#	design-media/skills/design/references/premium-proposals.md
#	dev-tools/agents/database-design-specialist.md
#	dev-tools/agents/dev-helper.md
#	dev-tools/agents/development-lead.md
#	dev-tools/agents/javascript-fullstack-specialist.md
#	dev-tools/agents/nextjs-specialist.md
#	dev-tools/agents/php-fullstack-engineer.md
#	dev-tools/skills/benchmark/SKILL.md
#	dev-tools/skills/canary/SKILL.md
#	dev-tools/skills/chrome/SKILL.md
#	dev-tools/skills/db-design/SKILL.md
#	dev-tools/skills/dep-audit/SKILL.md
#	dev-tools/skills/diagrama/SKILL.md
#	dev-tools/skills/docx/SKILL-light.md
#	dev-tools/skills/docx/SKILL.md
#	dev-tools/skills/docx/scripts/__init__.py
#	dev-tools/skills/docx/scripts/office/pack.py
#	dev-tools/skills/docx/scripts/office/unpack.py
#	dev-tools/skills/docx/scripts/office/validate.py
#	dev-tools/skills/nextjs/SKILL.md
#	dev-tools/skills/pdf/SKILL-light.md
#	dev-tools/skills/pdf/SKILL.md
#	dev-tools/skills/php-dev/SKILL.md
#	dev-tools/skills/prompt-refine/SKILL.md
#	dev-tools/skills/react-patterns/SKILL.md
#	dev-tools/skills/skill-creator/SKILL.md
#	dev-tools/skills/skill-creator/references/descomplicar-standards.md
#	dev-tools/skills/webapp-testing/SKILL.md
#	dev-tools/skills/webapp-testing/scripts/with_server.py
#	dev-tools/skills/xlsx/SKILL.md
#	dev-tools/skills/xlsx/scripts/office/pack.py
#	dev-tools/skills/xlsx/scripts/office/unpack.py
#	dev-tools/skills/xlsx/scripts/office/validate.py
#	dev-tools/skills/youtube-extract/SKILL.md
#	gestao/.claude-plugin/plugin.json
#	gestao/agents/analytics-insights-agent.md
#	gestao/agents/compliance-auditor.md
#	gestao/agents/project-manager.md
#	gestao/agents/reflect-agent.md
#	gestao/agents/software-project-planner.md
#	gestao/agents/time-management-agent.md
#	gestao/agents/wiki-knowledge-agent.md
#	gestao/skills/aiktop-tasks/SKILL.md
#	gestao/skills/archive/SKILL.md
#	gestao/skills/calendar-manager/SKILL.md
#	gestao/skills/cleanup-downloads/SKILL.md
#	gestao/skills/clip-agent/SKILL.md
#	gestao/skills/clip-health/SKILL.md
#	gestao/skills/clip-instructions/SKILL.md
#	gestao/skills/clip-issue/SKILL.md
#	gestao/skills/clip-org/SKILL.md
#	gestao/skills/clip-routine/SKILL.md
#	gestao/skills/clip-skill/SKILL.md
#	gestao/skills/clip-vision/SKILL.md
#	gestao/skills/clip/SKILL.md
#	gestao/skills/daily-digest/SKILL.md
#	gestao/skills/deep-research/SKILL.md
#	gestao/skills/delegate/SKILL.md
#	gestao/skills/doc-coauthoring/SKILL.md
#	gestao/skills/dual-review/SKILL.md
#	gestao/skills/hub-compile/SKILL.md
#	gestao/skills/hub-search/SKILL.md
#	gestao/skills/imap-triage/SKILL.md
#	gestao/skills/index-update/SKILL.md
#	gestao/skills/knowledge/SKILL.md
#	gestao/skills/knowledge/docs/workflow.md
#	gestao/skills/knowledge/references/routing-guide.md
#	gestao/skills/mindmap/SKILL.md
#	gestao/skills/monday-briefing/SKILL.md
#	gestao/skills/notebooklm/SKILL.md
#	gestao/skills/office-hours/SKILL.md
#	gestao/skills/onboarding/SKILL.md
#	gestao/skills/proc-creator/SKILL.md
#	gestao/skills/reflect/SKILL.md
#	gestao/skills/report-templates/SKILL.md
#	gestao/skills/report/SKILL.md
#	gestao/skills/research-pipeline/SKILL.md
#	gestao/skills/review-corrections/SKILL.md
#	gestao/skills/spec-coauthor/SKILL.md
#	gestao/skills/task-state/SKILL.md
#	gestao/skills/tasks-overview/SKILL.md
#	gestao/skills/time/SKILL.md
#	gestao/skills/today/SETUP_MCPS.md
#	gestao/skills/today/SKILL.md
#	gestao/skills/today/prompts/execute.md
#	gestao/skills/today/templates/daily-report.md
#	gestao/skills/validate-component/SKILL.md
#	gestao/skills/worklog/SKILL.md
#	infraestrutura/.claude-plugin/plugin.json
#	infraestrutura/agents/backup-specialist.md
#	infraestrutura/agents/cwp-server-manager.md
#	infraestrutura/agents/easypanel-specialist.md
#	infraestrutura/agents/infrastructure-orchestrator.md
#	infraestrutura/agents/proxmox-specialist.md
#	infraestrutura/agents/security-compliance-specialist.md
#	infraestrutura/skills/authentik/SKILL.md
#	infraestrutura/skills/backup-strategies/SKILL.md
#	infraestrutura/skills/backup-strategies/references/scripts.md
#	infraestrutura/skills/cwp/SKILL.md
#	infraestrutura/skills/easypanel-api/SKILL.md
#	infraestrutura/skills/easypanel-cicd/SKILL.md
#	infraestrutura/skills/easypanel-deploy/SKILL.md
#	infraestrutura/skills/easypanel-init/SKILL.md
#	infraestrutura/skills/easypanel-monitor/SKILL.md
#	infraestrutura/skills/easypanel-rollback/SKILL.md
#	infraestrutura/skills/easypanel-troubleshoot/SKILL.md
#	infraestrutura/skills/easypanel-validate/SKILL.md
#	infraestrutura/skills/gateway-check/SKILL.md
#	infraestrutura/skills/infra-check/SKILL.md
#	infraestrutura/skills/mcp-dev/SKILL.md
#	infraestrutura/skills/mcp-dev/references/best-practices.md
#	infraestrutura/skills/mcp-dev/references/evaluation-guide.md
#	infraestrutura/skills/mcp-dev/references/templates.md
#	infraestrutura/skills/pbs-config/SKILL.md
#	infraestrutura/skills/proxmox-cluster/SKILL.md
#	infraestrutura/skills/proxmox-ha/SKILL.md
#	infraestrutura/skills/proxmox-setup/SKILL.md
#	infraestrutura/skills/security-audit/SKILL.md
#	infraestrutura/skills/security-check/SKILL.md
#	infraestrutura/skills/server-health/SKILL.md
#	infraestrutura/skills/vm-migration/SKILL.md
#	marketing/.claude-plugin/plugin.json
#	marketing/CHANGELOG.md
#	marketing/agents/content-manager.md
#	marketing/agents/copywriter.md
#	marketing/agents/marketing-planning-expert.md
#	marketing/agents/ppc-specialist.md
#	marketing/agents/seo-specialist.md
#	marketing/agents/social-media-manager.md
#	marketing/agents/video-production-specialist.md
#	marketing/agents/youtube-specialist.md
#	marketing/skills/ads/SKILL.md
#	marketing/skills/content-marketing-pt/SKILL.md
#	marketing/skills/lead-approach/SKILL.md
#	marketing/skills/marketing-strategy/SKILL.md
#	marketing/skills/seo-audit/SKILL.md
#	marketing/skills/seo-audit/references/ferramentas-api.md
#	marketing/skills/seo-audit/references/template-relatorio-auditoria.md
#	marketing/skills/seo-content-optimization/SKILL.md
#	marketing/skills/seo-content-optimization/references/structured-data.md
#	marketing/skills/seo-report/SKILL.md
#	marketing/skills/seo-report/references/implementacao-tecnica.md
#	marketing/skills/seo-report/references/template-relatorio.md
#	marketing/skills/social-media/SKILL.md
#	marketing/skills/trend-signal/SKILL.md
#	marketing/skills/video/SKILL.md
#	marketing/skills/video/rules/3d.md
#	marketing/skills/video/rules/animations.md
#	marketing/skills/video/rules/assets.md
#	marketing/skills/video/rules/audio.md
#	marketing/skills/video/rules/calculate-metadata.md
#	marketing/skills/video/rules/can-decode.md
#	marketing/skills/video/rules/charts.md
#	marketing/skills/video/rules/compositions.md
#	marketing/skills/video/rules/display-captions.md
#	marketing/skills/video/rules/extract-frames.md
#	marketing/skills/video/rules/fonts.md
#	marketing/skills/video/rules/get-audio-duration.md
#	marketing/skills/video/rules/get-video-dimensions.md
#	marketing/skills/video/rules/get-video-duration.md
#	marketing/skills/video/rules/gifs.md
#	marketing/skills/video/rules/images.md
#	marketing/skills/video/rules/import-srt-captions.md
#	marketing/skills/video/rules/lottie.md
#	marketing/skills/video/rules/maps.md
#	marketing/skills/video/rules/measuring-dom-nodes.md
#	marketing/skills/video/rules/measuring-text.md
#	marketing/skills/video/rules/parameters.md
#	marketing/skills/video/rules/sequencing.md
#	marketing/skills/video/rules/tailwind.md
#	marketing/skills/video/rules/text-animations.md
#	marketing/skills/video/rules/timing.md
#	marketing/skills/video/rules/transcribe-captions.md
#	marketing/skills/video/rules/transitions.md
#	marketing/skills/video/rules/trimming.md
#	marketing/skills/video/rules/videos.md
#	marketing/skills/youtube/SKILL.md
#	metabase/agents/metabase-analyst.md
#	metabase/skills/dashboard/SKILL.md
#	metabase/skills/metabase/SKILL.md
#	metabase/skills/query/SKILL.md
#	negocio/agents/ecommerce-architect.md
#	negocio/agents/finance-manager.md
#	negocio/agents/research-analyst.md
#	negocio/agents/saas-architect.md
#	negocio/skills/ecommerce/SKILL.md
#	negocio/skills/interview/SKILL.md
#	negocio/skills/product/SKILL.md
#	negocio/skills/research/SKILL.md
#	negocio/skills/saas/SKILL.md
#	perfex-dev/agents/perfex-crm-module-developer.md
#	perfex-dev/skills/perfex-controllers/SKILL.md
#	perfex-dev/skills/perfex-database/SKILL.md
#	perfex-dev/skills/perfex-forms/SKILL.md
#	perfex-dev/skills/perfex-hooks/SKILL.md
#	perfex-dev/skills/perfex-menus/SKILL.md
#	perfex-dev/skills/perfex-migrations/SKILL.md
#	perfex-dev/skills/perfex-module-basics/SKILL.md
#	perfex-dev/skills/perfex-permissions/SKILL.md
#	perfex-dev/skills/perfex-security/SKILL.md
#	perfex-dev/skills/perfex-views/SKILL.md
#	project-manager/.claude-plugin/plugin.json
#	project-manager/agents/scrum-master.md
#	project-manager/agents/spec-writer.md
#	project-manager/hooks/spec-gate.sh
#	project-manager/skills/brainstorm/SKILL.md
#	project-manager/skills/discover/SKILL.md
#	project-manager/skills/scope/SKILL.md
#	project-manager/skills/spec/SKILL.md
#	project-manager/skills/sprint/SKILL.md
#	project-manager/skills/status/SKILL.md
#	wordpress/.claude-plugin/plugin.json
#	wordpress/agents/elementor-specialist.md
#	wordpress/agents/woocommerce-specialist.md
#	wordpress/agents/wordpress-performance-specialist.md
#	wordpress/agents/wordpress-plugin-developer.md
#	wordpress/skills/branda-menu/SKILL.md
#	wordpress/skills/crocoblock/SKILL.md
#	wordpress/skills/elementor/SKILL.md
#	wordpress/skills/jetengine/SKILL.md
#	wordpress/skills/rank-math/SKILL.md
#	wordpress/skills/rank-math/references/commands.md
#	wordpress/skills/seo-post/SKILL.md
#	wordpress/skills/woocommerce-cli/SKILL.md
#	wordpress/skills/woocommerce/SKILL.md
#	wordpress/skills/wp-cli/SKILL.md
#	wordpress/skills/wp-cli/references/commands.md
#	wordpress/skills/wp-cli/scripts/wp-auto.sh
#	wordpress/skills/wp-dev/SKILL.md
#	wordpress/skills/wp-hardening/SKILL.md
#	wordpress/skills/wp-performance/SKILL.md
#	wordpress/skills/wp-translate/SKILL.md
#	wordpress/skills/wp-update/SKILL.md
This commit is contained in:
2026-10-28 16:11:16 +00:00
377 changed files with 36766 additions and 1824 deletions
+28 -3
View File
@@ -1,12 +1,37 @@
{
"name": "wordpress",
"description": "WordPress development, maintenance and optimization - plugins, themes, WooCommerce, Elementor, Crocoblock. Backed by NotebookLM notebooks.",
"version": "1.0.0",
"description": "WordPress development, maintenance and optimization - plugins, themes, WooCommerce, Elementor, Crocoblock, EMCP Tools MCP (page building, content ops, security/performance audit), Elementor Pro/ElementsKit/PowerPack widget catalogs. Backed by NotebookLM notebooks.",
"version": "2.0.0",
"author": {
"name": "Descomplicar - Crescimento Digital",
"url": "https://descomplicar.pt"
},
"homepage": "https://git.descomplicar.pt/ealmeida/descomplicar-plugins",
"license": "MIT",
"keywords": ["wordpress", "woocommerce", "elementor", "crocoblock", "development", "performance"]
"keywords": [
"wordpress",
"woocommerce",
"elementor",
"crocoblock",
"development",
"performance",
"licensing",
"emcp-tools",
"elementskit",
"powerpack",
"cloudflare",
"social-media",
"booking",
"fluentcrm",
"seo",
"podcast",
"kivicare",
"clinic",
"translate",
"ifthenpay",
"sitekit",
"complianz",
"tablepress",
"gdpr"
]
}
+43 -4
View File
@@ -9,6 +9,8 @@ model: sonnet
tools: Read, Write, Edit, Bash, Glob, Grep, ToolSearch
# Dependencies
# Nota: MCPs como desk-crm-v3, moloni, easypanel, lighthouse, authentik, spaceship e youtube
# estão desligados por omissão — pedir activação via /mcp (regra 57) antes de os usar.
primary_mcps:
- desk-crm-v3
- ssh-unified
@@ -37,7 +39,6 @@ compliance:
created: '2025-01-13'
updated: '2026-02-04'
author: Descomplicar®
layer: wiki
---
# Elementor Specialist Descomplicar
@@ -170,13 +171,51 @@ Especialista em page building com Elementor Pro, entregando websites visuais de
- **n8n** - Workflows automation
- **filesystem** - Ficheiros locais
- **magic** - AI-powered UI component generation (tipo v0.dev)
- **mem0** - Memória longo prazo
- **mem0** - Mem0 (Tier 2, `mcp__mem0__*`) + IJFW (Tier 1, automático via hooks)
- **cwp** - CentOS Web Panel
- **puppeteer** - Browser automation
### All Available (32 total)
moloni, google-analytics, google-workspace, imap, outline-api, youtube-research, youtube-uploader, wikijs, gsc, lighthouse, mcp-time, mcp-mermaid, mcp-echarts, powerpoint, penpot, pixabay, pexels, tavily, elevenlabs, vimeo, design-systems, replicate
moloni, google-analytics, google-workspace, imap, outline-api, youtube-research, youtube-uploader, gsc, lighthouse, mcp-time, mcp-mermaid, mcp-echarts, powerpoint, penpot, pixabay, pexels, tavily, elevenlabs, vimeo, design-systems, replicate
**Discovery:** Use ToolSearch to find specific tools.
**Example:** `ToolSearch("ssh upload")` finds SSH upload tools.
## Your Available
## Your Available Skills
### Primary Skills (Your Domain)
✓ **/wp-dev** - Desenvolvimento WordPress especializado - plugins, temas, WooCommerce. Usar para
- Invoke: `/wp-dev`
✓ **/php-dev** - Desenvolvimento PHP fullstack - Laravel, Symfony, APIs RESTful, arquitectura bac
- Invoke: `/php-dev`
✓ **/db-design** - Design de bases de dados - schema, optimização queries, MySQL/PostgreSQL, arquit
- Invoke: `/db-design`
✓ **/mcp-dev** - Desenvolvimento de servidores MCP - criar, configurar, testar e documentar MCPs
- Invoke: `/mcp-dev`
### Recommended for dev
- **/react-patterns** - Padrões React modernos - Hooks, Server Components, State Man
- **/nextjs** - Desenvolvimento Next.js moderno com App Router, Server Compo
- **/elementor** - Desenvolvimento avançado com Elementor Pro e Crocoblock - wi
- **/woocommerce** - Desenvolvimento e optimização de lojas WooCommerce - checkou
- **/crm-admin** - Administração e desenvolvimento Perfex CRM - gestão dados, m
- **/video** - Criar vídeos programáticos com Remotion e React. Gera intros
- **/doc-sync** - Sincronização automática de documentação Stack/Vault. Garant
- **/delegate** - Delegar tarefas dev para outros chats Claude com workflow co
- **/time** - Time tracking integrado com Desk CRM. Inicia e para timers e
### Core Skills (All Agents)
- **/reflect** - Auto-reflexão e melhoria contínua do sistema. Analisa sessõe
- **/worklog** - Registo automático de trabalho - tarefas, problemas, soluçõe
- **/_core** - Padrões fundamentais Descomplicar® - Sacred Rules, Excellenc
- **/knowledge** - Gestão unificada de conhecimento - pesquisa inteligente com
- **/desk** - Integração com Desk CRM via ficheiro .desk-project. Auto-det
### All Available (53 total)
/billing-check, /crm-ops, /ecommerce, /lead-approach, /orcamento, /saas, /content-marketing-pt, /remotion-video, /seo-content-optimization, /social-media, /ui-ux-pro-max-repo, /brand-voice-generator, /frontend-design, /pptx-generator, /ui-ux-pro-max, /backup-strategies, /security-audit, /server-health, /wp-performance, /wp-update, /second-brain-repo, /ads, /marketing-strategy, /product, /skill-creator, /sop-creator, /calendar-manager, /interview, /today, /research, /youtube, /seo-audit, /seo-report, /archive, /metrics, /sdk
**Discovery:** Use the Skill tool to invoke skills.
**Example:** `Skill("skill-name")` invokes the skill.
+46 -3
View File
@@ -10,6 +10,8 @@ model: sonnet
tools: Read, Write, Edit, Bash, Glob, Grep, ToolSearch
# Dependencies
# Nota: MCPs como desk-crm-v3, moloni, easypanel, lighthouse, authentik, spaceship e youtube
# estão desligados por omissão — pedir activação via /mcp (regra 57) antes de os usar.
primary_mcps:
- desk-crm-v3
- ssh-unified
@@ -40,7 +42,6 @@ compliance:
created: '2025-01-13'
updated: '2026-02-04'
author: Descomplicar®
layer: wiki
---
# WooCommerce Specialist Descomplicar
@@ -143,9 +144,51 @@ mcp__notebooklm__notebook_query notebook_id:"226e384e-d4bc-48f4-bb82-7927360436c
- **filesystem** - Ficheiros locais
- **magic** - AI-powered UI component generation (tipo v0.dev)
- **** - Knowledge base AI
- **mem0** - Memória longo prazo
- **mem0** - Mem0 (Tier 2, `mcp__mem0__*`) + IJFW (Tier 1, automático via hooks)
- **cwp** - CentOS Web Panel
- **puppeteer** - Browser automation
### All Available (32 total)
moloni, google-analytics, google-workspace, imap, outline-api, youtube-research, youtube-uploader, wikijs, gsc, lighthouse, mcp-time, mcp-mermaid,
moloni, google-analytics, google-workspace, imap, outline-api, youtube-research, youtube-uploader, gsc, lighthouse, mcp-time, mcp-mermaid, mcp-echarts, powerpoint, penpot, pixabay, pexels, tavily, elevenlabs, vimeo, design-systems, replicate
**Discovery:** Use ToolSearch to find specific tools.
**Example:** `ToolSearch("ssh upload")` finds SSH upload tools.
## Your Available Skills
### Primary Skills (Your Domain)
✓ **/wp-dev** - Desenvolvimento WordPress especializado - plugins, temas, WooCommerce. Usar para
- Invoke: `/wp-dev`
✓ **/php-dev** - Desenvolvimento PHP fullstack - Laravel, Symfony, APIs RESTful, arquitectura bac
- Invoke: `/php-dev`
✓ **/db-design** - Design de bases de dados - schema, optimização queries, MySQL/PostgreSQL, arquit
- Invoke: `/db-design`
✓ **/mcp-dev** - Desenvolvimento de servidores MCP - criar, configurar, testar e documentar MCPs
- Invoke: `/mcp-dev`
### Recommended for dev
- **/react-patterns** - Padrões React modernos - Hooks, Server Components, State Man
- **/nextjs** - Desenvolvimento Next.js moderno com App Router, Server Compo
- **/elementor** - Desenvolvimento avançado com Elementor Pro e Crocoblock - wi
- **/woocommerce** - Desenvolvimento e optimização de lojas WooCommerce - checkou
- **/crm-admin** - Administração e desenvolvimento Perfex CRM - gestão dados, m
- **/video** - Criar vídeos programáticos com Remotion e React. Gera intros
- **/doc-sync** - Sincronização automática de documentação Stack/Vault. Garant
- **/delegate** - Delegar tarefas dev para outros chats Claude com workflow co
- **/time** - Time tracking integrado com Desk CRM. Inicia e para timers e
### Core Skills (All Agents)
- **/reflect** - Auto-reflexão e melhoria contínua do sistema. Analisa sessõe
- **/worklog** - Registo automático de trabalho - tarefas, problemas, soluçõe
- **/_core** - Padrões fundamentais Descomplicar® - Sacred Rules, Excellenc
- **/knowledge** - Gestão unificada de conhecimento - pesquisa inteligente com
- **/desk** - Integração com Desk CRM via ficheiro .desk-project. Auto-det
### All Available (53 total)
/billing-check, /crm-ops, /ecommerce, /lead-approach, /orcamento, /saas, /content-marketing-pt, /remotion-video, /seo-content-optimization, /social-media, /ui-ux-pro-max-repo, /brand-voice-generator, /frontend-design, /pptx-generator, /ui-ux-pro-max, /backup-strategies, /security-audit, /server-health, /wp-performance, /wp-update, /second-brain-repo, /ads, /marketing-strategy, /product, /skill-creator, /sop-creator, /calendar-manager, /interview, /today, /research, /youtube, /seo-audit, /seo-report, /archive, /metrics, /sdk
**Discovery:** Use the Skill tool to invoke skills.
**Example:** `Skill("skill-name")` invokes the skill.
@@ -11,6 +11,8 @@ model: sonnet
tools: Read, Write, Edit, Bash, Glob, Grep, ToolSearch
# Dependencies
# Nota: MCPs como desk-crm-v3, moloni, easypanel, lighthouse, authentik, spaceship e youtube
# estão desligados por omissão — pedir activação via /mcp (regra 57) antes de os usar.
primary_mcps:
- desk-crm-v3
- ssh-unified
@@ -39,7 +41,6 @@ compliance:
created: '2025-01-13'
updated: '2026-02-04'
author: Descomplicar®
layer: wiki
---
# WordPress Performance Specialist Descomplicar
@@ -133,4 +134,98 @@ Especialista em optimizacao de performance WordPress responsavel por maximizar v
- desk-crm-v3: Registar auditorias e optimizacoes
## Ferramentas
- **Aud
- **Audit**: PageSpeed Insights, GTmetrix, Lighthouse
- **Profiling**: Query Monitor, P3 Plugin Profiler
- **Caching**: WP Rocket, Redis, Cloudflare
- **DB**: WP-CLI, phpMyAdmin, Adminer
## Targets Performance
| Metrica | Target | Critico |
|---------|--------|---------|
| LCP | <2.5s | <4s |
| INP | <200ms | <500ms |
| CLS | <0.1 | <0.25 |
| TTFB | <600ms | <1.8s |
| PageSpeed Score | >90 | >50 |
```
## Colaboracao
- Reports to: Development Lead ou System Development Agent
- Colabora com: WordPress Plugin Developer, CWP Manager, Database Specialist
## Your Available MCPs
### Primary MCPs (Your Domain)
✓ **desk-crm-v3** (business)
- Clientes, projectos, facturas, time tracking
- Usage: `mcp__desk-crm-v3__*`
✓ **ssh-unified** (infra)
- SSH, SFTP, servidor management
- Usage: `mcp__ssh-unified__*`
### Recommended for infra
- **cwp** - CentOS Web Panel
- **filesystem** - Ficheiros locais
- **lighthouse** - Performance audits
- **mcp-time** - Hora actual, conversão fusos horários
- **puppeteer** - Browser automation
### All Available (32 total)
moloni, context7, gitea, n8n, google-analytics, google-workspace, imap, outline-api, youtube-research, youtube-uploader, gsc, mem0, mcp-mermaid, mcp-echarts, powerpoint, penpot, pixabay, pexels, tavily, elevenlabs, magic, vimeo, design-systems, replicate
**Discovery:** Use ToolSearch to find specific tools.
**Example:** `ToolSearch("ssh upload")` finds SSH upload tools.
## Your Available Skills
### Primary Skills (Your Domain)
✓ **/server-health** - Diagnóstico completo de servidor - CPU, RAM, MySQL, Web, SSL, Segurança. Usar qu
- Invoke: `/server-health`
✓ **/wp-performance** - Auditoria de performance WordPress - cache, GZIP, imagens, plugins, base de dado
- Invoke: `/wp-performance`
✓ **/wp-update** - Actualização e verificação de todos os sites WordPress do servidor CWP
- Invoke: `/wp-update`
### Recommended for infra
- **/backup-strategies** - Estratégias de backup e recovery - RTO, RPO, disaster recove
- **/security-audit** - Auditoria de segurança e compliance - OWASP Top 10, GDPR, an
- **/archive** - Arquivamento automatico de ficheiros concluidos - move fiche
- **/delegate** - Delegar tarefas dev para outros chats Claude com workflow co
### Core Skills (All Agents)
- **/reflect** - Auto-reflexão e melhoria contínua do sistema. Analisa sessõe
- **/worklog** - Registo automático de trabalho - tarefas, problemas, soluçõe
- **/_core** - Padrões fundamentais Descomplicar® - Sacred Rules, Excellenc
- **/knowledge** - Gestão unificada de conhecimento - pesquisa inteligente com
- **/desk** - Integração com Desk CRM via ficheiro .desk-project. Auto-det
### All Available (53 total)
/billing-check, /crm-ops, /ecommerce, /lead-approach, /orcamento, /saas, /content-marketing-pt, /remotion-video, /seo-content-optimization, /social-media, /video, /ui-ux-pro-max-repo, /brand-voice-generator, /frontend-design, /pptx-generator, /ui-ux-pro-max, /crm-admin, /db-design, /elementor, /mcp-dev, /nextjs, /php-dev, /react-patterns, /woocommerce, /wp-dev, /second-brain-repo, /ads, /doc-sync, /marketing-strategy, /product, /skill-creator, /sop-creator, /calendar-manager, /interview, /time, /today, /research, /youtube, /seo-audit, /seo-report, /metrics, /sdk
**Discovery:** Use the Skill tool to invoke skills.
**Example:** `Skill("skill-name")` invokes the skill.
## Your Team & Responsibilities
You are part of **2 SDKs** (TaskForce teams):
### TaskForce Claude Agents
**Purpose:** NULL
**Your responsibilities in this TaskForce:**
### TaskForce WordPress
**Purpose:** NULL
**Your responsibilities in this TaskForce:**
- **Desenvolvimento, manutencao e optimizacao WordPress - plugins, temas, WooCommerce, Elementor, Crocoblock, performance e actualizacoes.**: NULL
**Collaboration:**
- Work with other agents in your TaskForce teams
- Share knowledge and context across team members
- Leverage team-specific skills and MCPs
- Contribute to team goals and deliverables
+29 -2
View File
@@ -18,6 +18,8 @@ user_invocable: true
tier: 1
# Dependencies
# Nota: MCPs como desk-crm-v3, moloni, easypanel, lighthouse, authentik, spaceship e youtube
# estão desligados por omissão — pedir activação via /mcp (regra 57) antes de os usar.
primary_mcps:
- desk-crm-v3 # Business operations
- gitea # Version control
@@ -27,7 +29,7 @@ recommended_mcps:
- n8n # Workflow automation
- filesystem # Local file operations
- magic # UI component generation
- mem0 # Long-term memory
- mem0 # Memória longo prazo (Tier 2) via Mem0
- cwp # Server management
- puppeteer # Browser automation
@@ -80,7 +82,6 @@ collaborations:
created: 2026-02-04
last_updated: 2026-02-04
quality_score: 85
layer: wiki
---
# WordPress Plugin Developer Descomplicar
@@ -139,3 +140,29 @@ Developer WordPress senior responsavel por criar plugins personalizados, temas e
1. CPT: register_post_type() com labels, supports, capabilities
2. Taxonomy: register_taxonomy() se necessario
3. Meta boxes: add_meta_box() ou ACF/Pods
4. Save: Hook save_post com nonce verification
5. Display: Custom template em theme ou override
6. REST API: Expor CPT via 'show_in_rest' => true
### Workflow 3: Security Hardening
1. Nonces: wp_nonce_field() em forms, wp_verify_nonce() ao processar
2. Sanitization: sanitize_text_field(), sanitize_email(), wp_kses()
3. Validation: is_email(), absint(), verificar user capabilities
4. Escape output: esc_html(), esc_attr(), esc_url()
5. Database: $wpdb->prepare() para queries custom
6. Permissions: current_user_can() antes de accoes privilegiadas
### Workflow 4: WooCommerce Extension
1. Hooks: woocommerce_init, woocommerce_loaded
2. Templates: Override via woocommerce/ folder em theme
3. Emails: Extend WC_Email class para custom emails
4. Payment gateway: Extend WC_Payment_Gateway
5. Shipping method: Extend WC_Shipping_Method
6. Testing: WooCommerce unit test suite
## WordPress APIs
- **Options API**: get_option(), update_option()
- **Settings API**: add_settings_section(), register_setting()
- **Shortcode API**: add_shortcode()
- **Widget API**: WP_Widget class
- **REST API**: register_rest_route()
@@ -0,0 +1,651 @@
---
name: app-for-cloudflare
description: Gestão da zona Cloudflare a partir do wp-admin via App for Cloudflare® (`app-for-cf`, Digital Point) — SSL/TLS, Speed, Caching, Network, Scrape Shield, Bot Management, Super Bot Fight Mode, WAF real via Managed Ruleset, HSTS, Certificate Transparency, Leaked Credential Checks, Turnstile CAPTCHA, R2 media storage, Zero Trust Access, Page/Cache Rules, DMARC, Web Analytics, ferramentas de diagnóstico (request trace/IP/domain/WHOIS), purge de cache, guest page caching a nível de edge. Usar quando "app for cloudflare", "cloudflare wordpress", "zona cloudflare", "waf cloudflare", "bot management", "super bot fight mode", "hsts cloudflare", "rocket loader", "certificate transparency", "leaked credential checks", "bots de ia cloudflare", "ai_bots_protection", "is_robots_txt_managed", "managed ruleset", "purge cache cloudflare", "turnstile", "cloudflare r2", "cloudflare access", "page rules cloudflare", "cache rules cloudflare", "dmarc cloudflare", "cfPageCachingSeconds", "cfTurnstile", "app_for_cf", ou qualquer alteração a definições de zona Cloudflare/config do plugin num site do bundle Descomplicar.
---
# /app-for-cloudflare — Gestão de zona Cloudflare via App for Cloudflare®
Plugin `app-for-cf` (Digital Point), versão `1.10.0` (`APP_FOR_CLOUDFLARE_VERSION`),
instalado nos sites do bundle Descomplicar®.
Não é um simples proxy visual — a maior parte das definições de "boas
práticas" que o plugin lista **não vivem em `wp_options`**: são lidas e
escritas directamente na **Cloudflare API v4**, usando um token scoped
guardado numa única opção serializada. Confundir as duas camadas é o erro
mais comum ao operar este plugin.
**Fonte:** `CONFIG-Plugins-Referencia.md` §1 (App for Cloudflare®) e
`BUNDLE-Excelencia-WP.md` §2.2 e §2.4 (GEO/bots de IA) — mapeamento inicial
feito em `emanuelalmeida.pt` (16-08-2026), zona `574b243ebabd8ea2b8d89e68921f52cc`,
plano **Free Website**. Expandido na mesma sessão com leitura completa do
código-fonte (11.309 linhas em 61 ficheiros PHP,
`wp-content/plugins/app-for-cf/`): `Repository/Cloudflare.php` (2178
linhas — `getSettingsToManage()`, `setEasyMode()`, `updateSettings()`),
`Setup.php` (defaults de `wp_options`), `Admin/Template/Settings.php` (766
linhas — UI completa das 6 tabs), `Admin/Base/Admin.php` (estrutura das 14
páginas de menu), `Base/Pub.php` (657 linhas — guest page caching, purge,
preload), `Helper/Api.php` (gate Free/Pro), `Cron/Jobs.php` e
`Cli/PurgeCache.php` (automação de purge), e todos os 14 templates de
página em `Admin/Template/`.
---
## 0. As duas camadas de config — não confundir
| Camada | Onde vive | Como ler/escrever | Exemplos |
|---|---|---|---|
| **Config do plugin** (não é Cloudflare) | `wp_options.app_for_cf`, array PHP serializado, 18 chaves (ver §8) | `wp option get/patch` | `cfZoneId`, `cfAccountId`, `cloudflareAuth.token`, `cfPageCachingSeconds`, `cloudflarePreload`, `cfPurgeCacheOnAdminBar`, `cfTurnstile`, `cfProxy`, `cfR2Bucket` |
| **Config de zona Cloudflare** (as ~52 "boas práticas" + Bot Management + WAF, ver §1) | **Na Cloudflare, não no WordPress** | API v4 (`https://api.cloudflare.com/client/v4`), bearer token = `cloudflareAuth.token` da opção acima | `ssl`, `security_header` (HSTS), `waf`, `bot_management`, `rocket_loader`, `cache_level`, `hotlink_protection`, etc. |
Confirmado por leitura completa do código: **não existe** namespace WP REST
próprio exposto pelo plugin (`app-for-cf` não aparece em `rest_api_init` em
nenhum dos 61 ficheiros PHP) — todas as chamadas de configuração de zona
são directas à Cloudflare API v4 a partir do backend PHP do plugin
(`Api/Cloudflare.php`), usando o token guardado localmente. Para
automatizar fora do wp-admin, replicar essa mesma chamada directa
(`curl`/PHP) em vez de procurar um endpoint local que não existe.
A opção `cloudflareAuth` suporta **dois modos de autenticação** (ver
`Setup::defaults()`): `type: 'token'` (API token scoped, o único usado nos
sites do bundle) ou o modo legado `email` + `api_key` (Global API Key —
não usar, é o modo antigo e menos seguro da Cloudflare, mantido só por
compatibilidade retroativa no plugin).
### Ler a config do plugin (wp-cli)
```bash
PATH=/home/USER/public_html
# Opção completa (JSON)
wp option get app_for_cf --format=json --allow-root --path=$PATH
# Uma sub-chave
wp option pluck app_for_cf cfZoneId --allow-root --path=$PATH
wp option pluck app_for_cf cfPageCachingSeconds --allow-root --path=$PATH
# Editar uma sub-chave sem apagar as restantes (SEMPRE patch, nunca update, em opção serializada)
wp option patch update app_for_cf cfPageCachingSeconds 21600 --allow-root --path=$PATH
wp option patch update app_for_cf cloudflarePreload 1 --allow-root --path=$PATH
```
`cfProxy`, `cfR2Bucket`, `cloudflareAuth`, `cfTurnstile` são sub-arrays —
`wp option patch` só edita um nível; para sub-arrays aninhados usar
`wp eval` com `update_option()` sobre o array completo lido primeiro.
### Extrair o token para chamar a Cloudflare API directamente
```bash
TOKEN=$(wp eval 'echo maybe_unserialize(get_option("app_for_cf"))["cloudflareAuth"]["token"];' --allow-root --path=$PATH)
ZONE=$(wp eval 'echo maybe_unserialize(get_option("app_for_cf"))["cfZoneId"];' --allow-root --path=$PATH)
curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/settings" | jq .
```
---
## 1. As definições de zona (por categoria) — lista completa do código
Fonte: `Repository/Cloudflare.php::getSettingsToManage()` (linhas 88-721),
a definição autoritativa usada pelo próprio plugin para desenhar a página
de Settings e para `updateSettings()`. **Achado desta expansão:** o número
"44" da auditoria original referia-se à contagem informal mostrada na UI
(que agrupa alguns campos); a contagem real de entradas no array
`getSettingsToManage()` é **52**. Lidas em bloco via `GET
/zones/{zone}/settings` (a maioria) + endpoints individuais para os campos
com `override_endpoint` (`security_header`, `certificate_transparency`,
`leaked_credential_checks`, `bot_management`, `speed_brain`, `fonts`,
`content_converter`, `origin_max_http_version`, `h2_prioritization`,
`settings/nel`, `flags` para `crawlhints`, `argo/tiered_caching`).
Escritas via `PATCH /zones/{zone}/settings/{setting_id}` com `{"value":
...}`, excepto onde a definição tem `overwrite_write_method` diferente
(`PUT` para tudo em `bot_management`, `POST` para
`leaked-credential-checks`/`ct/alerting`/`crawlhints`).
| Categoria | Definições |
|---|---|
| **Topo (fora de secção)** | `development_mode` (bool, bom = `off`), `security_level` (select: `off`/`essentially_off`/`low`/`medium`/`high`/`under_attack`, bom = `essentially_off`) — mostradas no topo da página Settings, não numa tab |
| **SSL/TLS** | `ssl` (modo de encriptação: `off`/`flexible`/`full`/`strict`/`origin_pull`), `always_use_https`, `security_header` (HSTS — ver §3), `min_tls_version` (`1.0`-`1.3`), `opportunistic_encryption`, `tls_1_3` (radio: `off`/`on`/`zrt` — zero round trip), `automatic_https_rewrites`, `ech` (Encrypted Client Hello), `certificate_transparency` (ver §4, `beta`), `tls_client_auth` |
| **Segurança** | `waf` (toggle legado, **deprecated** — ver §2), `bot_fight_mode` (Bot Fight Mode básico — ver §5a), `ai_bots_protection`, `crawler_protection` (ambos geridos via `bot_management` PUT — ver §5a), `bot_likely_automated`, `bot_definitely_automated`, `bot_verified_bots`, `bot_static_resource_protection`, `bot_optimize_wordpress`, `bot_enable_js` (Super Bot Fight Mode — ver §5a), `leaked_credential_checks` (ver §4), `challenge_ttl` (select 5min-12meses, bom = 30min), `browser_check`, `replace_insecure_js` |
| **Speed** | `polish` (select: `off`/`lossless`/`lossy`), `webp`, `speed_brain` (`beta`, substitui Rocket Loader/Mirage antigos), `fonts` (`beta`, Cloudflare Fonts — serve Google Fonts a partir do edge), `early_hints`, `rocket_loader` (deprecated para segurança — ver §2), `content_converter` (`beta`), `http2`, `origin_max_http_version` (int, bom = `2`), `http3`, `h2_prioritization`, `0rtt` (TLS 1.3 zero round trip) |
| **Caching** | `cache_level` (radio: `basic`/`simplified`/`aggressive`, bom = `aggressive`), `browser_cache_ttl` (select 0-12meses, `0` = respeitar headers de origem), `crawlhints` (`beta`, indica a crawlers quando o conteúdo mudou), `always_online`, `tiered_caching` (Argo Tiered Cache) |
| **Network** | `ipv6`, `websockets`, `pseudo_ipv4` (select: `off`/`add_header`/`overwrite_header`), `ip_geolocation`, `max_upload` (select 100-500MB), `nel` (Network Error Logging, `value.enabled` aninhado), `opportunistic_onion` |
| **Scrape Shield** | `email_obfuscation`, `hotlink_protection` |
```bash
# Ler uma definição individual
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/settings/rocket_loader" | jq .
# Escrever uma definição individual (formato standard "value")
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/settings/rocket_loader" \
-d '{"value":"off"}' | jq .
```
`webp`, `content_converter`, `speed_brain`, `fonts`, `crawlhints`,
`tiered_caching` e outros podem estar **plan-locked** (não editáveis no
plano Free Website) — a API devolve o valor actual mas ignora tentativas
de escrita; não é bug, é limite de plano. A UI do plugin marca isto
lendo `editable` do resultado da API (ver `override_result_editable_if_has`
no código) e desactiva o `<select>`/checkbox correspondente.
### EasyConfig — o que o botão "Easy config" aplica em bloco
`Repository\Cloudflare::setEasyMode()` (linha 723) aplica de uma vez, via
`POST /zones/{zone}/settings` (bulk) + 5 chamadas individuais:
```
0rtt=on, browser_cache_ttl=0, cache_level=aggressive, early_hints=on,
http3=on, ip_geolocation=on, ipv6=on, min_tls_version=1.2,
opportunistic_encryption=on, opportunistic_onion=on, pseudo_ipv4=off,
rocket_loader=off, tls_1_3=zrt, websockets=on,
security_level=essentially_off (fora do WordPress; 'medium' fora do XF)
+ settings/nel → {"value":{"enabled":false}}
+ settings/origin_max_http_version → {"value":"2"}
+ settings/speed_brain → {"value":"on"}
+ settings/fonts → {"value":"on"}
+ argo/tiered_caching → {"value":"on"}
```
Não toca em SSL/TLS (excepto `min_tls_version`/`opportunistic_encryption`),
Bot Management, WAF, HSTS nem Scrape Shield — o EasyConfig é só Speed +
Caching + Network básico. Preferir aplicar definição a definição (§7 desta
skill) para manter controlo e rasto de decisão, como já recomendado no
achado original.
---
## 2. WAF real — o toggle `waf` está morto, usar Managed Ruleset
O toggle legado `waf` está **deprecated para zonas novas**: `PATCH
/zones/{id}/settings/waf` devolve `1027 WAF is deprecated for this zone`
independentemente do valor enviado. O mecanismo real de protecção é a fase
de ruleset `http_request_firewall_managed`, que **pode estar completamente
vazia** (zero regras, zero protecção) mesmo num site "protegido pelo
Cloudflare" — foi o estado encontrado em `emanuelalmeida.pt` antes da
correcção.
```bash
# 1. Confirmar se a fase managed já tem entrypoint com regras
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/rulesets/phases/http_request_firewall_managed/entrypoint" | jq .
# Vazio/erro "could not find entrypoint" = zero protecção real, apesar do que o toggle `waf` sugere
# 2. Implantar o Cloudflare Managed Free Ruleset na fase managed
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/rulesets/phases/http_request_firewall_managed/entrypoint" \
-d '{
"rules": [
{
"action": "execute",
"action_parameters": {
"id": "77454fe2d30c4220b5701f6fdfb893ba"
},
"expression": "true",
"description": "Cloudflare Managed Free Ruleset"
}
]
}' | jq .
```
`77454fe2d30c4220b5701f6fdfb893ba` é o ID do **Cloudflare Managed Free
Ruleset** — disponível em qualquer plano, incluindo Free Website. Testar
sempre com um pedido real (POST/AJAX contra o site) depois de implantar,
não confiar só no `200 OK` do PUT.
A fase `http_request_firewall_custom` (regras próprias, não geridas) fica
tipicamente vazia (`10003 could not find entrypoint`) num site sem regras
custom — não é erro, é "zero regras próprias, só o Managed Ruleset". O
plugin (versão Pro) gere regras custom através da página **Firewall**
(§10) — "User agents" e "IP addresses" são interfaces amigáveis para criar
regras Custom Rules na fase `http_request_firewall_custom` sem precisar de
escrever expressões Cloudflare manualmente.
**Rocket Loader não substitui isto.** `rocket_loader` é uma definição de
**Speed** (adia/reordena `<script>` no browser), não de segurança — o
próprio plugin marca-o `not_good_alert` (recomenda desligado). Testado
nesta sessão como hipótese de causa de CLS elevado num site: desligar não
resolveu o CLS, mas manter desligado continua a ser a recomendação do
plugin por outras razões (risco de quebra em sites com jQuery/Elementor).
`speed_brain` é o sucessor recomendado pela Cloudflare (substitui Rocket
Loader e Mirage), mas está marcado `beta` no código do plugin.
---
## 3. HSTS (`security_header`)
```bash
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/settings/security_header" \
-d '{
"value": {
"strict_transport_security": {
"enabled": true,
"max_age": 15552000,
"include_subdomains": true,
"preload": false,
"nosniff": true
}
}
}' | jq .
```
**Gotcha real desta sessão:** se a origem (nginx) já envia o seu próprio
header `Strict-Transport-Security`, **o HSTS do Cloudflare não o sobrepõe**
— é comportamento documentado, não bug. Confirmar com `curl -sI` que o
`cf-cache-status` do pedido é `MISS`/`DYNAMIC` (não é cache stale) e que o
`max-age` devolvido é o da origem, não o configurado no Cloudflare. Ajustar
o `max_age` a partir da origem (nginx) quando o objectivo for realmente
mudar o valor visto pelo browser.
`preload: false` é deliberado por omissão — entrar na lista de preload dos
browsers é quase irreversível (remoção demora meses/anos a propagar). Só
activar depois de HSTS estável em produção há semanas sem incidentes.
O "bom" (`good`) definido no código do plugin para este campo é
`max_age: 31536000` (12 meses), `include_subdomains: 1`, `preload: 1`,
`nosniff: ''` (vazio, i.e. não activa X-Content-Type-Options via este
mecanismo) — mais agressivo do que o valor sugerido na correcção original
desta skill (6 meses). Escolher em função do risco de subdomínios não
cobertos por HTTPS antes de ir para 12 meses + preload.
---
## 4. Certificate Transparency e Leaked Credential Checks
Endpoints próprios, fora do bulk `settings`:
```bash
# Certificate Transparency (alertas quando um certificado é emitido para o domínio)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/ct/alerting" \
-d '{"enabled": true}' | jq .
# Leaked Credential Checks (avisa se credenciais usadas no site apareceram em leaks conhecidos)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/leaked-credential-checks" \
-d '{"enabled": true}' | jq .
```
Ambos vinham `off` por omissão num site com anos de conta Cloudflare — não
assumir que "conta antiga = tudo activo por defeito"; a Cloudflare tem
adicionado features de segurança ao longo do tempo sem as retroactivar em
zonas já existentes.
---
## 5. Bot Management (endpoint `/bot_management`) — 7 campos + `is_robots_txt_managed`
Endpoint próprio: `GET`/`PUT /zones/{zone}/bot_management` (não faz parte
do bulk `settings`, e **não aceita `PATCH`**). Estes campos foram
verificados via chamada directa à API (não são geridos pela UI do plugin
através de `getSettingsToManage()`, ao contrário dos campos do §5a) — o
plugin não expõe `content_bots_protection`, `ai_training`, `ai_search`,
`ai_user` nem `is_robots_txt_managed` na sua própria página de Settings,
mas estes são campos reais e válidos do endpoint `bot_management` da
Cloudflare, geríveis por chamada directa à API.
| Campo | Padrão óptimo Descomplicar® | O que faz mal se ligado |
|---|---|---|
| `ai_bots_protection` | `disabled` | Bloqueia crawlers de IA (GPTBot, ClaudeBot, etc.) — visto `block` activo em 2 sites do bundle |
| `content_bots_protection` | `disabled` | Idem, granularidade diferente |
| `crawler_protection` | `disabled` | Bloqueia crawlers genéricos |
| `ai_training` | `disabled` | Bloqueia bots de treino de modelos |
| `ai_search` | `disabled` | Bloqueia bots de motores de busca IA |
| `ai_user` | `disabled` | Bloqueia bots accionados por utilizador de IA (ex. ChatGPT a navegar em nome do utilizador) |
| `fight_mode` | `false` | Desafia (challenge JS) tráfego automatizado por heurística — apanha crawlers legítimos de IA que não resolvem JS challenges, visto a devolver "Just a moment..." em vez de conteúdo real |
**Achado real desta sessão (GEO):** com `is_robots_txt_managed: true`, o
Cloudflare **injecta directivas `Disallow: /`** no `robots.txt` servido
para estes user-agents, por cima do ficheiro físico do site — mesmo que o
ficheiro físico já permita esses bots explicitamente. Parsers que fundem
grupos do mesmo user-agent aplicam o `Disallow` na mesma, tornando a
permissão do ficheiro físico letra morta.
```bash
# Ler estado actual
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/bot_management" | jq .
# Corrigir — PUT, NUNCA PATCH (PATCH devolve sempre 10405 Method not allowed,
# independentemente das permissões do token)
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/bot_management" \
-d '{
"ai_bots_protection": "disabled",
"content_bots_protection": "disabled",
"crawler_protection": "disabled",
"ai_training": "disabled",
"ai_search": "disabled",
"ai_user": "disabled",
"fight_mode": false,
"is_robots_txt_managed": false
}' | jq .
```
Depois de corrigir, **purgar cache** (Cloudflare + cache local WP, ex. WP
Fastest Cache) — `robots.txt` costuma ter `cache-control:
max-age=315360000`, a correcção não aparece sem purge. Confirmar ao vivo
com `curl -sI` que `cf-cache-status` não é `HIT` e o corpo já não tem
`Disallow: /` para os bots visados.
### 5a. Super Bot Fight Mode — campos geridos pela própria UI do plugin
Ao contrário do §5 (chamada directa, campos fora da UI), estes 8 campos
**são** geridos por `getSettingsToManage()` e aparecem na tab "Security"
da página Settings do plugin. Todos escrevem no mesmo endpoint `PUT
/bot_management`, mas com nomes de campo JSON diferentes do que o `id` da
definição no plugin:
| ID no plugin | Campo real na API `bot_management` | Bom (`good`) |
|---|---|---|
| `bot_fight_mode` | `fight_mode` | `false` |
| `ai_bots_protection` | `ai_bots_protection` | `block` *(nota: o `good` do código é `block`, oposto do padrão Descomplicar do §5 — ver decisão abaixo)* |
| `crawler_protection` | `crawler_protection` | `enabled` *(idem — oposto do padrão Descomplicar)* |
| `bot_likely_automated` | `sbfm_likely_automated` (select `allow`/`block`/`managed_challenge`) | `allow` |
| `bot_definitely_automated` | `sbfm_definitely_automated` (select `allow`/`block`/`managed_challenge`) | `allow` |
| `bot_verified_bots` | `sbfm_verified_bots` (select `allow`/`block`) | `allow` |
| `bot_static_resource_protection` | `sbfm_static_resource_protection` (bool) | `false` |
| `bot_optimize_wordpress` | `optimize_wordpress` (bool) | `false` |
| `bot_enable_js` | `enable_js` (bool) — comentário no código: "a API diz que é editável, mas só é possível de facto em plano Pro ou superior" | `false` |
**Contradição intencional a documentar:** o `good` que o código do plugin
define para `ai_bots_protection`/`crawler_protection` é o valor "block"/
"enabled" (i.e. o próprio autor do plugin recomenda bloquear bots de IA
por omissão). A decisão Descomplicar® (documentada no §5 e no gotcha da
tabela §7) é o oposto — **nunca bloquear bots de IA em nenhum site do
bundle**, por ser GEO-crítico (visibilidade em AI Overviews/ChatGPT/
Perplexity). Ao corrigir zonas, ignorar deliberadamente o "good" do plugin
para estes 2 campos específicos e aplicar sempre `disabled`.
Todas as escritas neste grupo enviam `unset_on_write: ['using_latest_model']`
— o código remove explicitamente esse campo do payload antes de fazer
`PUT`, porque `using_latest_model` é campo só de leitura devolvido pela API
(indica se a Cloudflare está a usar o modelo de detecção de bots mais
recente) e a API rejeita o `PUT` se ele for reenviado.
---
## 6. Scopes de token — lista completa (21 permissões, do código de setup)
`Admin/Template/MultisiteSettings.php` e `Admin/Template/Settings.php`
embutem a lista completa e oficial de scopes recomendados pelo autor do
plugin (usada para construir o link de criação de token pré-preenchido no
dashboard Cloudflare) — muito mais granular do que os 6 grupos informais
documentados na versão anterior desta skill:
| Scope Cloudflare | Nível de acesso | Necessário para |
|---|---|---|
| `Account.Access: Apps and Policies` | Edit | Zero Trust Access (página Access, Pro) |
| `Account.Access: Organizations, Identity Providers, and Groups` | Read | Zero Trust Access — resolver grupos/IdPs nas políticas |
| `Account.Account Analytics` | Read | Dados agregados de conta (não usado nesta skill) |
| `Account.Account Settings` | Read + Edit | Detalhes/definições de conta Cloudflare |
| `Account.Allow Request Tracer` | Read | Página "HTTP request trace" (§10) |
| `Account.Billing` | Read | Info de plano/facturação Cloudflare mostrada no plugin |
| `Account.Intel` | Read | Páginas "IP address details"/"Domain details"/"WHOIS" (Cloudflare Intel API, §10) |
| `Account.Turnstile` | Edit | Criar/gerir sitekeys Turnstile CAPTCHA (§8, `cfTurnstile`) |
| `Account.Workers R2 Storage` | Edit | R2 media storage (página R2, Pro) |
| `Account.Workers Scripts` | Edit | Worker proxy (`cfWorkersSubdomain`/`cfProxy`, §8) |
| `User.API Tokens` | Read | O plugin lê as próprias permissões do token para colorir a checklist de setup a verde |
| `Zone.Analytics` | Read | Web Analytics / dashboards de tráfego |
| `Zone.Bot Management` | Edit | Bot Management + Super Bot Fight Mode (§5, §5a) — **separado** do Zone Settings, falta frequentemente no token gerado por omissão |
| `Zone.Cache Purge` | Purge | Purge de cache (página "Purge cache", botão admin bar) |
| `Zone.Cache Rules` | Edit | Cache Rules (página Rules, Pro) |
| `Zone.Firewall Services` | Edit | Firewall Rules, regras de User-Agent/IP (página Firewall, Pro) |
| `Zone.Page Rules` | Edit | Page Rules legadas (página Rules, Pro) |
| `Zone.SSL and Certificates` | Edit | Certificate Transparency, TLS client auth |
| `Zone.Zone` | Edit | Operações de gestão da própria zona |
| `Zone.Zone Settings` | Edit | As ~52 definições de zona do §1 (SSL/TLS, Speed, Caching, Network, Scrape Shield) |
| `Zone.Zone WAF` | Edit | WAF/Managed Ruleset (§2) |
`Zone.DNS` **não está na lista oficial do plugin** — o plugin não gere DNS
(DMARC/SPF/DKIM são lidos como registos públicos via `dig`, ver §7/§10, não
via API Cloudflare autenticada).
**Sintoma de scope em falta:** a API devolve `403` ou o próprio plugin
mostra um erro de permissão específico à operação, não um erro genérico de
autenticação — o token pode estar válido e ainda assim faltar-lhe
exactamente o scope daquela chamada. Testado nesta sessão: token com `Zone
SSL/Settings Edit` funcionava para as definições de zona mas falhava em
Bot Management até se alargar explicitamente o scope no dashboard
Cloudflare (`My Profile → API Tokens → editar token`).
---
## 7. Gotchas e erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| `PATCH /zones/{id}/settings/waf` → `1027 WAF is deprecated for this zone` | Toggle legado morto para zonas novas | Implantar Managed Ruleset na fase `http_request_firewall_managed` (§2) |
| `PATCH /zones/{id}/bot_management` → `10405 Method not allowed` | Endpoint só aceita `PUT` | Usar `PUT` sempre, mesmo para alterar um único campo (envia o objecto completo) |
| Correcção de `robots.txt`/HSTS não aparece no browser | Header/ficheiro servido a partir de cache (Cloudflare e/ou plugin de cache WP) | Purgar Cloudflare + cache WP local; confirmar `cf-cache-status` não é `HIT` antes de validar |
| HSTS configurado no Cloudflare mas browser continua a ver `max-age` diferente | Origem (nginx) já envia o seu próprio header `Strict-Transport-Security` — Cloudflare **não sobrepõe** HSTS que a origem já envia | Ajustar na origem, ou aceitar que o Cloudflare é só fallback para origens sem HSTS próprio |
| Regra de firewall/Transform Rule com operador `matches` (regex) falha ou não fica disponível na UI | `matches` exige plano **Business** (WAF Advanced) | Reescrever a expressão com `in`, `eq` ou `contains` — disponíveis em qualquer plano, incluindo Free |
| `wp option update app_for_cf '...'` apaga sub-chaves que não estavam no valor enviado | `wp option update` substitui a opção inteira; `app_for_cf` é um array serializado | Usar `wp option patch update app_for_cf <chave> <valor>` para editar uma sub-chave sem tocar nas restantes |
| R2 (media storage) devolve `10042 Please enable R2 through the Cloudflare Dashboard` | R2 não está activado a nível de **conta** (não é definição de zona nem do plugin) | Activar manualmente no dashboard Cloudflare da conta antes de qualquer chamada à API de R2 |
| `ai_bots_protection`/`crawler_protection` a `block`/`enabled` num site do bundle | Divergência do padrão óptimo — visto em `carstuff.pt` e `solarfvengenharia.com` nesta sessão; **o próprio `good` do código do plugin recomenda `block`** (§5a), por isso corrigir manualmente sempre que se aplicar EasyConfig ou se seguir uma recomendação genérica do plugin | Corrigir para `disabled` via `PUT /bot_management` (§5) — decisão explícita: nunca bloquear bots de IA em nenhum site Descomplicar |
| Página "Public page caching"/"Purge cache"/analytics parecem não fazer nada de imediato | `handleHeaders()` só define `Cache-Control: max-age=0,s-maxage=X` — depende do Cloudflare respeitar esse header (`cache_level`/`browser_cache_ttl` compatíveis) e de não haver cookies de sessão a bloquear o cache (ver §9) | Confirmar `browser_cache_ttl` = "Respect existing headers" (`0`) e `cache_level` ≠ `basic`; testar em aba anónima sem cookies `wp-*`/`wordpress_*`/`comment_*`/`woocommerce_*` |
| Botões "Firewall", "Access", "Rules" (criar regra), "R2", "Copy from...", "License key" aparecem desactivados/a cinzento ou com classe `pro` | Funcionalidade gated para a versão **Premium/Pro** do plugin (`Helper\Api::check()` — ver §11) | Confirmar licença activa (`cfLicenseKey`) ou aceitar a limitação no plano Free |
| `wp option get app_for_cf` mostra `cfTurnstile:["0"]` em vez do array completo com `siteKey`/`onLogin`/etc. | Turnstile nunca foi activado neste site — o checkbox "Use Turnstile CAPTCHA" desmarcado grava só `[0]` (o `<input type="hidden" value="0">` do formulário), não o array completo de `Setup::defaults()` | Normal em sites sem Turnstile configurado; a estrutura completa (13 sub-chaves, ver §8) só aparece depois de gerar uma sitekey e gravar o formulário |
---
## 8. Config do plugin (`wp_options.app_for_cf`) — as 18 chaves em detalhe
Fonte: `Setup::defaults()` (schema oficial de instalação) cruzado com a
leitura ao vivo (`wp option get app_for_cf`) em `emanuelalmeida.pt`
(16-08-2026) — as 18 chaves batem certo entre defaults e produção.
```json
{"cfWorkersSubdomain":"","cfProxy":{"image":"0","url":"0"},"cfR2Bucket":{"media":""},"cfAccountId":"a8b00ad3227d729a146143a6a85a7522","cfZoneId":"574b243ebabd8ea2b8d89e68921f52cc","cfTokenId":"6b6d24fe8d324112ea4cfc328bf16f6c","cfZone":"emanuelalmeida.pt","cfPageCachingSeconds":"21600","cloudflareAuth":{"token":"cfut_..."},"cloudflareFirewallExpireDays":"7","cloudflareBlockIpsSpamClean":"0","cfExternalDataUrl":"","cloudflarePreload":"1","cfImagesTransform":"0","cfLicenseKey":"","cfPurgeCacheOnAdminBar":"1","cfTurnstile":["0"],"LockSettingsUserId":"1"}
```
| Chave | Tipo/Default | O que faz |
|---|---|---|
| `cfWorkersSubdomain` | string, `''` | Subdomínio `*.workers.dev` da conta Cloudflare, usado quando o Worker proxy (R2/imagens) está activo |
| `cfProxy.image` / `cfProxy.url` | `0`/`1` cada | Toggle interno para saber se URLs de imagem/media já estão a ser reescritas para servir via Worker/R2 (usado por `filterWpGetAttachmentUrl()`/`filterWpCalculateImageSrcset()` em `Base/Pub.php`) |
| `cfR2Bucket.media` | string, `''` | Nome do bucket R2 escolhido/criado para armazenar media da biblioteca WordPress (feature Pro, página R2) |
| `cfAccountId` | string | ID da conta Cloudflare — resolvido automaticamente por `getZoneId()` a partir do hostname na primeira chamada, cacheado aqui |
| `cfZoneId` | string | ID da zona Cloudflare — idem, cacheado para evitar `listZones()` a cada pedido |
| `cfTokenId` | string | ID do token API (não o valor secreto) — mostrado na UI como referência ("Token ID: ...") para o admin confirmar qual token está activo |
| `cfZone` | string | Nome do domínio da zona (ex. `emanuelalmeida.pt`), cacheado junto com `cfAccountId`/`cfZoneId` |
| `cfPageCachingSeconds` | int (segundos), `''` (vazio = desligado) | Tempo de `s-maxage` aplicado ao HTML para guests (§9) — `0`/vazio desliga o guest page caching |
| `cloudflareAuth.type` | `'token'` ou `'email'` | Modo de autenticação — `token` é o único usado/recomendado; `email`+`api_key` é o modo legado Global API Key |
| `cloudflareAuth.token` | string secreta | Bearer token da Cloudflare API, scoped (ver permissões no §6) |
| `cloudflareAuth.email` / `cloudflareAuth.api_key` | string, `''` | Só usados se `type = 'email'` (não usar) |
| `cloudflareFirewallExpireDays` | int, default `7` (1-90) | Dias até uma regra de firewall **criada automaticamente pelo plugin** (ex. bloqueio de IP de spammer, feature Pro) expirar sozinha |
| `cloudflareBlockIpsSpamClean` | `0`/`1`, default `1` | Se `1`, bloqueia automaticamente (feature Pro) o IP de um utilizador cujo comentário foi marcado como spam pela fila de moderação do WordPress |
| `cfExternalDataUrl` | string URL, `''` | URL pública onde o media fica acessível quando R2 está activo — normalmente auto-preenchido ao activar R2, não editar manualmente salvo troubleshooting |
| `cloudflarePreload` | `0`/`1`, default `1` | Se `1`, o plugin usa hooks `script_loader_tag`/`style_loader_tag`/`wp_footer` para emitir headers HTTP `Link: <url>;rel=preload` (até 10 recursos) — funciona em conjunto com a definição de zona `early_hints` |
| `cfImagesTransform` | `0`/`1`, default `0` | Se `1`, activa **Cloudflare Images Transform** para blocos de imagem/media (serve AVIF/WebP a browsers modernos, formatos mais antigos a browsers antigos, qualidade reduzida em ligações muito lentas) — requer activação também a nível de zona no dashboard Cloudflare |
| `cfLicenseKey` | string, `''` | Chave de licença da versão Premium/Pro do plugin — sem isto, `Helper\Api::check()` devolve sempre `false` (ver §11) |
| `cfPurgeCacheOnAdminBar` | `0`/`1`, default `0` | Se `1`, mostra um botão de purge de cache com um clique na admin bar do WordPress |
| `cfTurnstile` | array com 13 sub-chaves quando activo: `siteKey`, `secretKey`, `onRegister`, `onLogin`, `onPassword`, `onComment`, `onContactForm7`, `onHtmlForms`, `onMetForm`, `onWPForms`, `onWooCommerceRegister`, `onWooCommerceLogin`, `onWooCommercePassword` | CAPTCHA Turnstile aplicado por contexto — cada `onX` é um toggle independente que decide se o widget Turnstile aparece nesse formulário específico (registo/login/reset password/comentários WP nativos, WooCommerce, e os plugins de formulário Contact Form 7/HTML Forms/MetForm/WPForms se estiverem instalados) |
| `LockSettingsUserId` | `0` ou ID de utilizador WP | Se preenchido com um ID de utilizador, **só essa conta WordPress** pode alterar as definições Cloudflare a partir do wp-admin — risco: apagar essa conta ou trocar de utilizador tranca o acesso às definições |
`network_exclude` (visto em código de multisite, `Settings.php` linha 290)
é uma chave adicional só relevante em instalações multisite — indica que
um site individual optou explicitamente por **não** herdar o token
API de rede.
---
## 9. Guest page caching (HTML a nível de edge) — como funciona de facto
Página "Public page caching" (§10) e a chave `cfPageCachingSeconds` (§8)
não usam Page Rules nem Cache Rules Cloudflare — usam um mecanismo mais
simples e mais portável: o hook `wp_headers` do WordPress
(`Base\Pub::handleHeaders()`, `Base/Pub.php` linha 276) define
```
Cache-Control: max-age=0,s-maxage=<cfPageCachingSeconds>
```
em toda resposta HTML elegível. `max-age=0` diz ao browser para não
cachear localmente; `s-maxage=<N>` diz a caches partilhadas (o edge da
Cloudflare) para cachear durante N segundos — desde que a definição de
zona `browser_cache_ttl` esteja em `0` ("Respect Existing Headers", o
padrão recomendado no §1) e `cache_level` não seja `basic`.
**Critérios de elegibilidade** (`pageCachingCriteria()`, filtráveis via
`app_for_cf_guest_page_cacheable`):
- Utilizador **não** tem sessão WordPress activa (`is_user_logged_in()`
falso)
- Nenhum cookie do browser começa por `wp-`, `wordpress_`, `comment_`
ou `woocommerce_` (senão a página é tratada como dinâmica/pessoal e o
header de cache não é aplicado)
- Pedido não é para `/wp-login.php`
- Content-Type da resposta começa por `text/html`
- Página não é 404
- `WP_DEBUG` não está activo
Por omissão o valor sugerido na UI é 6 horas (`21600` segundos, a mesma
opção `default` usada em `CachingGuestPage.php`); opções disponíveis vão
de 5 minutos a 24 horas, filtráveis via `app_for_cf_cache_times`, e o
limite máximo é derivado da vida do nonce do WordPress
(`DAY_IN_SECONDS - 3600`, tipicamente 23h).
```bash
# Ler/alterar o tempo de guest page caching
wp option pluck app_for_cf cfPageCachingSeconds --allow-root --path=$PATH
wp option patch update app_for_cf cfPageCachingSeconds 21600 --allow-root --path=$PATH # 6h
wp option patch update app_for_cf cfPageCachingSeconds '' --allow-root --path=$PATH # desligar
```
Como não é uma Page/Cache Rule, **não aparece no dashboard Cloudflare**
como regra — só é visível inspeccionando o header `Cache-Control` da
resposta HTTP (`curl -sI https://SITE/`) e o `cf-cache-status`.
---
## 10. As 14 páginas de admin — o que cada uma faz
Estrutura de menu confirmada em `Admin/Base/Admin.php`. O item de topo
(`Cloudflare`, ícone `dashicons-cloud`) mostra um badge `!` quando não há
token configurado.
| Slug (`page=`) | Título no menu | O que faz |
|---|---|---|
| `app-for-cf_caching` | **Public page caching** | Toggle + selector de tempo para o guest page caching do §9 (`cfPageCachingSeconds`) |
| `app-for-cf_firewall` | **Firewall** | 3 tabelas: **Firewall Rules** (link "Create rule" abre o dashboard Cloudflare — Pro), **User agents** (regras de bloqueio por user-agent geridas inline — Pro) e **IP addresses** (bloqueio manual por IP — Pro); todas suportam bulk enable/disable/delete via `bulkActionNotice()` |
| `app-for-cf_access` | **Access** | Lista de Zero Trust Access Apps + Groups da conta, com delete em massa (feature **Pro** — cria políticas de acesso, ex. proteger `/wp-admin` com SSO) |
| `app-for-cf_rules` | **Rules** | 2 tabelas: **Page Rules** legadas (link "Create page rule" abre dashboard — Pro) e **Cache Rules** modernas (link "Create cache rule" — Pro) |
| `app-for-cf_r2` | **R2 (media storage)** | Explica e liga a biblioteca de media do WordPress a um bucket Cloudflare R2 (10GB grátis, depois $0.015/GB); requer token válido **e** licença Pro |
| `app-for-cf_cache` | **Purge cache** | Botão único "Purge Cloudflare cache..." — chama `purgeCache()` sem filtro (`Purge Everything`) |
| `options-general.php?page=app-for-cf` | **Settings** (link para `wp-admin/options-general.php?page=app-for-cf`) | A página principal com as 6 tabs de definições de zona (Setup/SSL-TLS/Security/Speed/Caching/Network+Scrape Shield) — ver §1; badge "Missing API token" se `cloudflareAuth.token` vazio |
| `app-for-cf_analytics` | **Web analytics** | Toggle único para injectar o beacon JavaScript do Cloudflare Web Analytics (privacy-first, sem cookies) em todas as páginas; link directo para o dashboard de analytics quando activo |
| `app-for-cf_dmarc` | **DMARC management** | Gráfico (Chart.js) de volume de email pass/fail por semana/mês + tabela de fontes de envio detectadas via os relatórios DMARC que a Cloudflare recebe em nome do domínio; link "View in Cloudflare" para o dashboard |
| `app-for-cf_request-trace` | **HTTP request trace** | Formulário para simular um pedido (URL, método, protocolo, bot score, país, threat score, skip challenge) e ver que produtos/regras Cloudflare seriam aplicados — requer scope `Account.Allow Request Tracer` |
| `app-for-cf_ip-details` | **IP address details** | Consulta a Cloudflare Intel API para um IP: PTR, ASN/organização, tipo, risk types — requer `Account.Intel:Read` |
| `app-for-cf_domain-details` | **Domain details** | Idem para domínios: tipo, IPs de resolução, categorias de conteúdo |
| `app-for-cf_whois` | **WHOIS** | Consulta WHOIS via Cloudflare Intel: datas de criação/actualização, registrant, registrar, nameservers |
| (sem entrada de menu própria) **Copy Settings** | acedida a partir de um botão na página Settings | Copia todas as definições de zona de uma zona diferente da mesma conta Cloudflare para a zona actual — **irreversível**, exige confirmação explícita ("I understand...") antes de activar o botão de submissão |
Páginas exclusivas de **multisite network admin** (`settings.php?page=`):
| Slug | Título | O que faz |
|---|---|---|
| `app-for-cf_multisite-settings` | **Cloudflare** (em Network Admin → Settings) | Define um token API partilhado por toda a rede (`app_for_cf_network`), com a mesma checklist de 21 permissões do §6; cada site individual pode optar por usar o token de rede ou o seu próprio |
| `app-for-cf_multisite-r2` | **R2 (media storage)** (Network Admin) | Um único bucket R2 partilhado por todos os sites da rede, com o caminho de cada ficheiro prefixado por `{siteId}/` (ex. `cdn.site.com/50/2023/10/image.png`) — feature Pro, licenciada ao hostname do site principal da rede |
---
## 11. Free vs Pro — gate `Helper\Api::check()`
`Helper\Api::$version` / `Helper\Api::check($force = false)` (`Helper/Api.php`)
determina se a licença Premium está activa através de um **transient**
(`acf_int`) com 6 horas de validade (`21600` segundos) — não é lido de
`cfLicenseKey` directamente a cada pedido, é cacheado. `Setup::install()`
chama `Api::check(true)` (forçado) na activação do plugin.
Funcionalidades bloqueadas/degradadas no plano Free (confirmado nos
templates admin, condicional `Helper\Api::$version` ou classe CSS `pro`):
- Botão "Create rule"/"Create page rule"/"Create cache rule"/"Create user
agent rule"/"Create IP address rule" (Firewall, Rules) — sem eles, as
tabelas continuam a **listar** regras já criadas manualmente no
dashboard Cloudflare, só não permitem criar a partir do wp-admin
- Página R2 (media storage) — bloqueada mesmo com token válido
- Página Access (Zero Trust) — lista mas não gere
- "Copy from..." (Copy Settings) — botão só aparece se
`Helper\WordPress::hasOwnApiToken()` **e** implicitamente exige Pro
para ser útil em produção
- Campo "License key" só aparece se `Helper\Api::$version` já tiver
alguma licença associada (mostra o campo para trocar/renovar)
- "Block spammer IPs"/"Days that firewall rules last"/"External data
URL" (`cloudflareBlockIpsSpamClean`, `cloudflareFirewallExpireDays`,
`cfExternalDataUrl`) aparecem com classe CSS `pro` e `disabled` quando
`(int)$cloudflareAppInternal` é falso — os campos ficam visíveis mas
não editáveis
Todas as definições de zona do §1 (SSL/TLS, Speed, Caching, Network,
Scrape Shield, Bot Management/SBFM), o WAF via Managed Ruleset (§2), HSTS
(§3), Certificate Transparency/Leaked Credential Checks (§4), guest page
caching (§9), purge de cache e Turnstile CAPTCHA (§8) **funcionam no plano
Free do plugin** — não são gated, só dependem do plano Cloudflare
subjacente (Free Website vs Pro/Business), que é uma dimensão diferente.
---
## 12. Automação — WP-CLI e Cron
```bash
# Purgar cache Cloudflare (Purge Everything) via linha de comandos
wp app-for-cf purge-cache --allow-root --path=$PATH
```
Implementado em `Cli/PurgeCache.php` (comando `app-for-cf purge-cache`,
`@when after_wp_load`) — chama `Repository\Cloudflare::purgeCache()` sem
argumentos (equivalente ao botão da página "Purge cache", §10).
`Cron\Jobs::purgeCache(array $urls)` é o mecanismo interno usado por
`Base\Pub::purgeCacheByPostIds()` quando um post muda de estado
(publicado/editado) ou um comentário muda de estado — faz **chunking em
lotes de 30 URLs** por chamada à API de purge (limite da Cloudflare por
pedido), parando o loop no primeiro chunk que falhar em vez de continuar a
tentar os restantes.
---
## 13. Áreas do plugin sem config a auditar/activar (confirmado nesta sessão, `emanuelalmeida.pt`)
| Área | Estado |
|---|---|
| Cache Rules / Page Rules | 0 regras — `GET /zones/{id}/pagerules` → `count:0` |
| Access (Zero Trust) | Não configurado, fora de âmbito nesta sessão — funcionalidade real do plugin, ver §10 |
| Turnstile CAPTCHA | Site key existe mas desligada no plugin (`cfTurnstile: ["0"]`) — infra pronta se decidido substituir reCAPTCHA; ver estrutura completa das 13 sub-chaves no §8 |
| Workers proxy (`cfProxy`) | `{image: off, url: off}` |
| R2 (media storage) | `cfR2Bucket.media` vazio, `cfExternalDataUrl` vazio — nunca activado neste site; requer licença Pro (§11) |
| DMARC/SPF/DKIM (`Dmarc.php`) | **Auditar via DNS público directo** (`dig TXT`), nunca precisa de token Cloudflare — são registos públicos por desenho; a página DMARC do plugin (§10) é sobre os relatórios agregados que a Cloudflare recebe, não sobre editar os próprios registos DNS |
| Web Analytics (beacon JS) | Confirmar por ausência/presença de `cloudflareinsights`/`beacon.min.js` no HTML servido, não por option — a página Web Analytics (§10) tem toggle próprio fora de `wp_options` (via API, não confirmado nesta sessão se lê de zona ou de conta) |
| EasyConfig (`setEasyMode()`) | Aplica tudo em bloco (lista exacta no §1) — preferir aplicar definição a definição para manter controlo e rasto de decisão |
| Licença Pro (`cfLicenseKey`) | Vazia — plugin a correr em modo Free; ver §11 para o que fica indisponível |
---
## Fonte
`CONFIG-Plugins-Referencia.md` §1 (App for Cloudflare®) — achados originais
de HSTS/WAF/DMARC, credenciais de sessão. `BUNDLE-Excelencia-WP.md` §2.2
(Performance) e §2.4 (GEO — bots de IA, achado completo do
`is_robots_txt_managed` e dos 7 campos de Bot Management via API directa).
Ambos em `Hub/04-Stack/02.04-Sistemas/71.Seguranca/`, piloto
`emanuelalmeida.pt`, sessão 16-08-2026.
**Expansão desta sessão (16-08-2026):** leitura completa do código-fonte
do plugin `app-for-cf` v1.10.0 via SSH (`server`, 61 ficheiros PHP,
11.309 linhas) — não apenas os ficheiros tocados na auditoria pontual
original. Ficheiros lidos na íntegra: `app-for-cf.php` (bootstrap),
`Setup.php` (schema de `wp_options`), `Repository/Cloudflare.php` (2178
linhas — lista completa de 52 definições de zona + `setEasyMode()` +
lógica de leitura/escrita), `Admin/Template/Settings.php` (766 linhas —
UI completa, scopes de token, Turnstile), `Admin/Base/Admin.php` (menu,
14 páginas), `Helper/Api.php` (gate Free/Pro), `Cron/Jobs.php`,
`Cli/PurgeCache.php`, e os 14 templates individuais em `Admin/Template/`
(`Firewall.php`, `R2.php`, `Access.php`, `Rules.php`, `Analytics.php`,
`Dmarc.php`, `RequestTrace.php`, `IpDetails.php`, `DomainDetails.php`,
`Whois.php`, `CachingGuestPage.php`, `Caching.php`, `Cache.php`,
`CopySettings.php`, `MultisiteR2.php`, `MultisiteSettings.php`) e trechos
relevantes de `Base/Pub.php` (657 linhas — guest page caching, preload,
purge). Confirmado ao vivo com `wp option get app_for_cf --format=json`
em `emanuelalmeida.pt` que as 18 chaves de `wp_options` batem certo com
`Setup::defaults()`. Nenhuma escrita feita em produção durante esta
expansão — só leitura de código-fonte e `wp option get`.
+56 -2
View File
@@ -1,7 +1,6 @@
---
name: branda-menu
description: Organizacao do menu admin WordPress via Branda (branda-white-labeling) em seccoes Descomplicar standard. Computa Branda IDs a partir de slugs WP, constroi menu custom e guarda programaticamente via WP-CLI.
layer: wiki
---
# /branda-menu - Organizar Menu Admin WordPress
@@ -130,4 +129,59 @@ Verificar visualmente no browser.
```bash
mcp__ssh-unified__ssh_execute server:"server" command:"rm -f /tmp/branda-menu-SITE.php"
mcp__ssh-unified__ssh_e
mcp__ssh-unified__ssh_execute server:"server" command:"wp eval '
delete_option(\"_branda_menu_capture\");
delete_option(\"_branda_menu_captured\");
echo \"Limpo\";
' --allow-root --path=/home/USER/SITE"
mcp__ssh-unified__ssh_execute server:"server" command:"rm -f /home/USER/SITE/wp-content/mu-plugins/capture-menu.php 2>/dev/null; echo ok"
```
---
## Troubleshooting
| Problema | Causa | Solucao |
|----------|-------|---------|
| Menu nao muda apos guardar | Cache ou modulo inactivo | `wp cache flush` + verificar `admin/menu.php` activo |
| Itens nativos fora das seccoes | Item nao incluido no config | Identificar slug, computar Branda ID, adicionar ao config |
| Separadores nativos aparecem | Slugs diferentes dos standard | Verificar slugs via captura, adicionar como hidden |
| Items custom em duplicado | Existe como custom e native | Remover custom link, manter native |
**CSS fallback para separadores:**
```css
#adminmenu li.wp-menu-separator { display: none !important; }
```
---
## Checklist
- [ ] Branda activo e modulo `admin/menu.php` activo
- [ ] Menu nativo extraido (todos os itens identificados)
- [ ] Todos os itens nativos incluidos no config (visiveis ou hidden)
- [ ] 9 seccoes criadas na ordem standard
- [ ] CPTs do projecto na seccao Conteudo
- [ ] Verificado visualmente no browser
- [ ] Script temporario e capturas removidos
- [ ] Permissoes corrigidas: `chown -R USER:USER /home/USER/SITE/`
---
## References
- `references/mapeamento-slugs.md` - Mapeamento completo slug -> seccao e tabela de Branda IDs
- `references/script-template.md` - Script PHP template + adaptacoes por tipo de site
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
+109 -2
View File
@@ -1,7 +1,6 @@
---
name: crocoblock
description: Desenvolvimento com o ecossistema Crocoblock e todos os JetPlugins para Elementor. Cobre selecção, licenciamento, instalação, configuração e integração entre JetEngine, JetSmartFilters, JetWooBuilder, JetElements, JetPopup, JetFormBuilder e mais.
layer: wiki
---
# /crocoblock — Ecossistema Crocoblock + JetPlugins
@@ -127,4 +126,112 @@ mcp__dify-kb__dify_kb_retrieve_segments({
})
```
## Troubleshooting
## Troubleshooting Comum
### Plugin não activa após upload
```bash
# Verificar se Elementor está activo (dependência)
$WP plugin list --search=elementor --fields=name,status
# Verificar erros PHP
$WP eval 'error_reporting(E_ALL); ini_set("display_errors", 1);' --allow-root --path=$PATH
# Limpar transients após activar
$WP transient delete --all --allow-root --path=$PATH
```
### Conflito entre versões
```
Regra: Manter todos os JetPlugins na mesma major version
Verificar: account.crocoblock.com > Changelogs para breaking changes
```
### Listings não actualizam após filtrar
```bash
# Regenerar CSS Elementor
$WP elementor flush-css --regenerate --allow-root --path=$PATH
# Limpar cache de objecto
$WP cache flush --allow-root --path=$PATH
```
## Padrões de Uso por Tipo de Site
### Site Corporativo / Portfolio
```
JetEngine (CPT: Projecto, Serviço, Testemunho)
+ JetSmartFilters (filtrar por categoria/tag)
+ JetElements (timeline, animações)
```
### E-commerce Avançado
```
WooCommerce + JetEngine (meta boxes extra)
+ JetWooBuilder (templates produto, categoria, cart)
+ JetCompareWishlist
+ JetSmartFilters (filtros loja)
```
### Directório / Listagem de Negócios
```
JetEngine (CPT: Negócio, com Relations para Categoria/Localização)
+ JetSmartFilters (filtros AJAX por localização, categoria, preço)
+ JetSearch (pesquisa instantânea)
+ JetEngine Maps (mapa de listagens)
```
### Portal com Accounts de Utilizador
```
JetEngine (CPT, Profile Builder)
+ JetFormBuilder (submissão de conteúdo por utilizador)
+ JetEngine Relations (conteúdo associado ao utilizador)
```
### Site com Reservas
```
JetBooking (calendário + disponibilidade)
+ JetFormBuilder (form de reserva)
+ JetEngine (meta boxes de propriedade/serviço)
```
## JetEngine MCP Server (v3.8.0+)
A partir do JetEngine 3.8.0, existe integração nativa com agentes IA via **MCP Server**:
```
WP Admin > JetEngine > AI Command Center > Enable MCP Server
→ Claude pode inspecionar schema completo (CPTs, CCTs, Relações, Queries)
→ e criar/modificar estruturas via linguagem natural
```
Usar `references/automation.md` para detalhes sobre Formless Actions, JetSmartFilters Indexer e pipelines de automação.
## Referências Adicionais
- **`references/plugins.md`** — Referência detalhada de cada plugin (widgets, configurações, casos de uso avançados)
- **`references/patterns.md`** — Padrões de integração avançados (multi-relation, filtros aninhados, profile builder completo)
- **`references/automation.md`** — MCP Server nativo, JetFormBuilder Formless Actions, JetSmartFilters Indexer, Kits export/import, cache e riscos críticos
---
**Versão**: 1.1.0 | **Autor**: Descomplicar® | **Data**: 18-02-2026
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
@@ -0,0 +1,80 @@
---
name: element-pack-widgets
description: Catálogo completo e verificado dos widgets Element Pack Pro (BdThemes) para Elementor — 168 widgets core (`element_pack_active_modules`) + 95 widgets de integração com plugins de terceiros, com o widgetType real de cada um extraído do código-fonte (`get_name()` em `modules/<slug>/widgets/*.php`) — não estão no catálogo curado do EMCP Tools (list-widgets), por isso precisam de get-widget-schema com full:true. Usar quando "element pack", "bdt-widget", "advanced heading elementor", "mega menu elementor", "custom carousel elementor", "brand grid", "crypto currency widget".
layer: wiki
---
# /element-pack-widgets — Catálogo de Widgets Element Pack Pro (BdThemes)
Element Pack Pro (plugin `bdthemes-element-pack`, activo em **descomplicar.pt**, v9.8.12) regista
os próprios widgets Elementor **fora** do catálogo curado do EMCP Tools — `list-widgets` não os
lista e `get-widget-schema` sem `full:true` devolve `"Not in the curated catalog"`.
Catálogo extraído directamente de `get_name()` em cada `modules/<slug>/widgets/*.php`, num site em
produção — **não adivinhado por convenção de nomes**, o que importa porque a convenção
`bdt-<slug-do-módulo>` tem 11 excepções reais confirmadas (ver `references/catalogo.md`). Catálogo
completo (168 core + 95 integração) em `references/catalogo.md`.
## Como usar (workflow)
1. Confirmar que `bdthemes-element-pack` está activo: `list-plugins`, procurar o slug (ou
`wp option get element_pack_active_modules --format=json` via SSH para ver módulos on/off).
2. `get-widget-schema({ widget_type: "bdt-<slug>", full: true })` — **sempre `full:true`**, nunca
confiar no catálogo curado. Devolve o schema de controlos real e vivo do widget (responsivo,
condições de exibição, efeitos).
3. Adicionar com `add-free-widget({ post_id, parent_id, widget_type, settings })` num container
**legacy** (`add-container`) — mesma mecânica de `elementskit-widgets`/`powerpack-widgets`, sem
equivalente atómico v4.
4. **Nunca assumir o `widgetType` pela pasta do módulo** — confirmado no código-fonte, 11 excepções
reais: `we-forms` → `bdt-we-form` (singular), `gravity-forms` → `bdt-gravity-form`,
`ninja-forms` → `bdt-ninja-form`, `edd-download-history` → `bdt-easy-digital-download-history`,
`edd-profile-editor` → `bdt-easy-digital-profile-editor`, `edd-purchase-history` →
`bdt-easy-digital-purchase-history`, `events-calendar-carousel/grid/list` → `bdt-event-*` (sem
"calendar"), `fooevents-calendar` → `fooevents-calendar` (sem prefixo `bdt-`). E **um caso não
resolvido**: `lightbox` resolve para `widgetType: "lightbox"` no código, mas `get-widget-schema`
devolve 0 propriedades para `bdt-lightbox` e uma resposta anormalmente grande (1624 propriedades
— provável fallback do EMCP, não o schema real) para `lightbox`; tratar como **não confirmado**
até verificar directamente numa página de teste antes de usar em produção.
5. Testado ponta-a-ponta em produção (19-08-2026): página draft → `add-container` →
`add-free-widget` com `bdt-advanced-heading` → `get-page-structure`/`get-element-settings`
confirmaram persistência → limpeza da página de teste.
## Widgets mais usados (ver `references/catalogo.md` para os 168+95 completos)
| `widgetType` | Título | Uso |
|---|---|---|
| `bdt-advanced-heading` | Advanced Heading | título com sub-heading, split text, efeitos de fundo |
| `bdt-custom-carousel` | Custom Carousel | carrossel genérico de slides imagem/vídeo/template |
| `bdt-mega-menu` | Mega Menu | menu mega multi-coluna, requer `element_pack_other_settings.mega-menu = on` |
| `bdt-brand-grid` / `bdt-brand-carousel` | Brand Grid/Carousel | logótipos de marcas em grelha/carrossel |
| `bdt-price-table` | Price Table | tabela de preços |
| `bdt-flip-box` | Flip Box | cartão com flip ao hover |
| `bdt-testimonial-carousel` | Testimonial Carousel | testemunhos em carrossel |
| `bdt-countdown` | Countdown | temporizador de contagem decrescente |
| `bdt-crypto-currency-ticker` | Crypto Ticker | ticker de criptomoedas em tempo real |
| `bdt-post-grid` | Post Grid | grelha de posts com query control |
| `bdt-timeline` | Timeline | linha do tempo vertical/horizontal |
| `bdt-table-of-content` | Table of Content | índice automático da página |
| `bdt-instagram` | Instagram Feed | feed do Instagram |
| `bdt-svg-maps` | SVG Maps | mapas SVG interactivos por região |
| `bdt-review-card` | Review Card | cartão de avaliação/review |
## Notas
- 168 widgets são **core**, controlados por `element_pack_active_modules` (todos `on` neste site
excepto `cookie-consent`, verificado 19-08-2026) — ver `mcp-element-pack` para ligar/desligar via
MCP dedicado.
- 95 widgets são **de integração**: só funcionam com o plugin de terceiros correspondente activo
(ACF, bbPress, BuddyPress, Charitable, Easy Digital Downloads, The Events Calendar, Tutor LMS,
WooCommerce, Give, LearnPress, vários plugins de formulários). Confirmar o plugin dependente antes
de inserir — `add-free-widget` não valida isto, pode inserir um widget "órfão" que não renderiza.
- `mega-menu` é widget **e** módulo de outros_settings — o widget só produz saída com
`element_pack_other_settings['mega-menu'] = 'on'` (hoje `off` em produção); confirmar/activar via
`mcp-element-pack` antes de o usar.
## Skills relacionadas
- `mcp-element-pack` — MCP dedicado para gerir licença/módulos/mega-menu/white-label/custom-code
do Element Pack Pro em descomplicar.pt via WP-CLI/SSH (não cobre inserção de widgets).
- `emcp-page-building` — mecânica de coexistência atómico/legacy, sequência de construção.
- `elementskit-widgets` / `powerpack-widgets` — catálogos dos addons concorrentes, mesmo padrão de
descoberta (`full:true` obrigatório, fora do catálogo curado).
@@ -0,0 +1,338 @@
# Catálogo completo — Element Pack Pro (BdThemes) widgets
Extraído do código-fonte real (`get_name()` em cada `modules/<slug>/widgets/*.php`) em descomplicar.pt, Element Pack Pro 9.8.12, 19-08-2026. 168 widgets core (gerados por `element_pack_active_modules`, todos activos neste site) + 95 widgets de integração com plugins de terceiros (activos independentemente do toggle de módulos core).
## Widgets core (168) — controlados por `element_pack_active_modules`
| Módulo | `widgetType` |
|---|---|
| `accordion` | `bdt-accordion` |
| `advanced-button` | `bdt-advanced-button` |
| `advanced-calculator` | `bdt-advanced-calculator` |
| `advanced-counter` | `bdt-advanced-counter` |
| `advanced-divider` | `bdt-advanced-divider` |
| `advanced-gmap` | `bdt-advanced-gmap` |
| `advanced-heading` | `bdt-advanced-heading` |
| `advanced-icon-box` | `bdt-advanced-icon-box` |
| `advanced-image-gallery` | `bdt-advanced-image-gallery` |
| `advanced-progress-bar` | `bdt-advanced-progress-bar` |
| `age-gate` | `bdt-age-gate` |
| `air-pollution` | `bdt-air-pollution` |
| `animated-card` | `bdt-animated-card` |
| `animated-heading` | `bdt-animated-heading` |
| `animated-link` | `bdt-animated-link` |
| `audio-player` | `bdt-audio-player` |
| `barcode` | `bdt-barcode` |
| `brand-carousel` | `bdt-brand-carousel` |
| `brand-grid` | `bdt-brand-grid` |
| `breadcrumbs` | `bdt-breadcrumbs` |
| `business-hours` | `bdt-business-hours` |
| `calendly` | `bdt-calendly` |
| `call-out` | `bdt-call-out` |
| `carousel` | `bdt-carousel` |
| `changelog` | `bdt-changelog` |
| `chart` | `bdt-chart` |
| `circle-info` | `bdt-circle-info` |
| `circle-menu` | `bdt-circle-menu` |
| `comment` | `bdt-comment` |
| `comparison-list` | `bdt-comparison-list` |
| `contact-form` | `bdt-contact-form` |
| `content-switcher` | `bdt-content-switcher` |
| `cookie-consent` | `bdt-cookie-consent` |
| `countdown` | `bdt-countdown` |
| `coupon-code` | `bdt-coupon-code` |
| `creative-button` | `bdt-creative-button` |
| `crypto-currency-card` | `bdt-crypto-currency-card` |
| `crypto-currency-carousel` | `bdt-crypto-currency-carousel` |
| `crypto-currency-chart` | `bdt-crypto-currency-chart` |
| `crypto-currency-chart-carousel` | `bdt-crypto-currency-chart-carousel` |
| `crypto-currency-grid` | `bdt-crypto-currency-grid` |
| `crypto-currency-list` | `bdt-crypto-currency-list` |
| `crypto-currency-table` | `bdt-crypto-currency-table` |
| `crypto-currency-ticker` | `bdt-crypto-currency-ticker` |
| `custom-carousel` | `bdt-custom-carousel` |
| `custom-gallery` | `bdt-custom-gallery` |
| `dark-mode` | `bdt-dark-mode` |
| `device-slider` | `bdt-device-slider` |
| `document-viewer` | `bdt-document-viewer` |
| `dropbar` | `bdt-dropbar` |
| `dual-button` | `bdt-dual-button` |
| `dynamic-carousel` | `bdt-dynamic-carousel` |
| `dynamic-grid` | `bdt-dynamic-grid` |
| `facebook-feed` | `bdt-facebook-feed` |
| `facebook-feed-carousel` | `bdt-facebook-feed-carousel` |
| `fancy-card` | `bdt-fancy-card` |
| `fancy-icons` | `bdt-fancy-icons` |
| `fancy-list` | `bdt-fancy-list` |
| `fancy-slider` | `bdt-fancy-slider` |
| `fancy-tabs` | `bdt-fancy-tabs` |
| `featured-box` | `bdt-featured-box` |
| `flip-box` | `bdt-flip-box` |
| `floating-knowledgebase` | `bdt-floating-knowledgebase` |
| `google-reviews` | `bdt-google-reviews` |
| `helpdesk` | `bdt-helpdesk` |
| `honeycombs` | `bdt-honeycombs` |
| `horizontal-scroller` | `bdt-horizontal-scroller` |
| `hover-box` | `bdt-hover-box` |
| `hover-video` | `bdt-hover-video` |
| `icon-mobile-menu` | `bdt-icon-mobile-menu` |
| `iconnav` | `bdt-iconnav` |
| `iframe` | `bdt-iframe` |
| `image-accordion` | `bdt-image-accordion` |
| `image-compare` | `bdt-image-compare` |
| `image-expand` | `bdt-image-expand` |
| `image-magnifier` | `bdt-image-magnifier` |
| `image-stack` | `bdt-image-stack` |
| `instagram` | `bdt-instagram` |
| `interactive-card` | `bdt-interactive-card` |
| `interactive-tabs` | `bdt-interactive-tabs` |
| `lightbox` | `lightbox` ⚠️ excepção de nome |
| `logo-carousel` | `bdt-logo-carousel` |
| `logo-grid` | `bdt-logo-grid` |
| `lottie-icon-box` | `bdt-lottie-icon-box` |
| `lottie-image` | `bdt-lottie-image` |
| `mailchimp` | `bdt-mailchimp` |
| `marker` | `bdt-marker` |
| `marquee` | `bdt-marquee` |
| `mega-menu` | `bdt-mega-menu` |
| `member` | `bdt-member` |
| `modal` | `bdt-modal` |
| `navbar` | `bdt-navbar` |
| `news-ticker` | `bdt-news-ticker` |
| `notification` | `bdt-notification` |
| `offcanvas` | `bdt-offcanvas` |
| `open-street-map` | `bdt-open-street-map` |
| `panel-slider` | `bdt-panel-slider` |
| `post-block` | `bdt-post-block` |
| `post-block-modern` | `bdt-post-block-modern` |
| `post-card` | `bdt-post-card` |
| `post-comments` | `bdt-post-comments` |
| `post-content` | `bdt-post-content` |
| `post-featured-image` | `bdt-post-featured-image` |
| `post-gallery` | `bdt-post-gallery` |
| `post-grid` | `bdt-post-grid` |
| `post-grid-tab` | `bdt-post-grid-tab` |
| `post-info` | `bdt-post-info` |
| `post-list` | `bdt-post-list` |
| `post-slider` | `bdt-post-slider` |
| `post-title` | `bdt-post-title` |
| `price-list` | `bdt-price-list` |
| `price-table` | `bdt-price-table` |
| `product-carousel` | `bdt-product-carousel` |
| `product-grid` | `bdt-product-grid` |
| `profile-card` | `bdt-profile-card` |
| `progress-pie` | `bdt-progress-pie` |
| `protected-content` | `bdt-protected-content` |
| `qrcode` | `bdt-qrcode` |
| `reading-progress` | `bdt-reading-progress` |
| `reading-timer` | `bdt-reading-timer` |
| `remote-arrows` | `bdt-remote-arrows` |
| `remote-fraction` | `bdt-remote-fraction` |
| `remote-pagination` | `bdt-remote-pagination` |
| `remote-thumbs` | `bdt-remote-thumbs` |
| `review-card` | `bdt-review-card` |
| `review-card-carousel` | `bdt-review-card-carousel` |
| `review-card-grid` | `bdt-review-card-grid` |
| `scroll-button` | `bdt-scroll-button` |
| `scroll-image` | `bdt-scroll-image` |
| `scrollnav` | `bdt-scrollnav` |
| `search` | `bdt-search` |
| `single-post` | `bdt-single-post` |
| `slider` | `bdt-slider` |
| `slideshow` | `bdt-slideshow` |
| `slinky-vertical-menu` | `bdt-slinky-vertical-menu` |
| `social-proof` | `bdt-social-proof` |
| `social-share` | `bdt-social-share` |
| `source-code` | `bdt-source-code` |
| `stacker` | `bdt-stacker` |
| `static-carousel` | `bdt-static-carousel` |
| `static-grid-tab` | `bdt-static-grid-tab` |
| `step-flow` | `bdt-step-flow` |
| `sub-menu` | `bdt-sub-menu` |
| `svg-blob` | `bdt-svg-blob` |
| `svg-image` | `bdt-svg-image` |
| `svg-maps` | `bdt-svg-maps` |
| `switcher` | `bdt-switcher` |
| `table` | `bdt-table` |
| `table-of-content` | `bdt-table-of-content` |
| `tabs` | `bdt-tabs` |
| `tags-cloud` | `bdt-tags-cloud` |
| `threesixty-product-viewer` | `bdt-threesixty-product-viewer` |
| `thumb-gallery` | `bdt-thumb-gallery` |
| `time-zone` | `bdt-time-zone` |
| `timeline` | `bdt-timeline` |
| `toggle` | `bdt-toggle` |
| `total-count` | `bdt-total-count` |
| `trailer-box` | `bdt-trailer-box` |
| `twitter-carousel` | `bdt-twitter-carousel` |
| `twitter-grid` | `bdt-twitter-grid` |
| `twitter-slider` | `bdt-twitter-slider` |
| `user-login` | `bdt-user-login` |
| `user-register` | `bdt-user-register` |
| `vertical-menu` | `bdt-vertical-menu` |
| `video-gallery` | `bdt-video-gallery` |
| `video-player` | `bdt-video-player` |
| `weather` | `bdt-weather` |
| `webhook-form` | `bdt-webhook-form` |
## Widgets de integração (95) — plugin de terceiros tem de estar activo
### ACF (Advanced Custom Fields) (5)
| Módulo | `widgetType` |
|---|---|
| `acf-accordion` | `bdt-acf-accordion` |
| `acf-gallery` | `bdt-acf-gallery` |
| `acf-list` | `bdt-acf-list` |
| `acf-slider` | `bdt-acf-slider` |
| `acf-tabs` | `bdt-acf-tabs` |
### BuddyPress (3)
| Módulo | `widgetType` |
|---|---|
| `buddypress-friends` | `bdt-buddypress-friends` |
| `buddypress-group` | `bdt-buddypress-group` |
| `buddypress-member` | `bdt-buddypress-member` |
### Charitable (8)
| Módulo | `widgetType` |
|---|---|
| `charitable-campaigns` | `bdt-charitable-campaigns` |
| `charitable-donation-form` | `bdt-charitable-donation-form` |
| `charitable-donations` | `bdt-charitable-donations` |
| `charitable-donors` | `bdt-charitable-donors` |
| `charitable-login` | `bdt-charitable-login` |
| `charitable-profile` | `bdt-charitable-profile` |
| `charitable-registration` | `bdt-charitable-registration` |
| `charitable-stat` | `bdt-charitable-stat` |
### Easy Digital Downloads (15)
| Módulo | `widgetType` |
|---|---|
| `edd-cart` | `bdt-edd-cart` |
| `edd-category-carousel` | `bdt-edd-category-carousel` |
| `edd-category-grid` | `bdt-edd-category-grid` |
| `edd-checkout` | `bdt-edd-checkout` |
| `edd-download-history` | `bdt-easy-digital-download-history` ⚠️ excepção de nome |
| `edd-login` | `bdt-edd-login` |
| `edd-mini-cart` | `bdt-edd-mini-cart` |
| `edd-product` | `bdt-edd-product` |
| `edd-product-carousel` | `bdt-edd-product-carousel` |
| `edd-product-review-carousel` | `bdt-edd-product-review-carousel` |
| `edd-product-reviews` | `bdt-edd-product-reviews` |
| `edd-profile-editor` | `bdt-easy-digital-profile-editor` ⚠️ excepção de nome |
| `edd-purchase-history` | `bdt-easy-digital-purchase-history` ⚠️ excepção de nome |
| `edd-register` | `bdt-edd-register` |
| `edd-tabs` | `bdt-edd-tabs` |
### FooEvents (1)
| Módulo | `widgetType` |
|---|---|
| `fooevents-calendar` | `fooevents-calendar` ⚠️ excepção de nome |
### Formulários (vários plugins) (6)
| Módulo | `widgetType` |
|---|---|
| `contact-form-7` | `bdt-contact-form-7` |
| `gravity-forms` | `bdt-gravity-form` ⚠️ excepção de nome |
| `ninja-forms` | `bdt-ninja-form` ⚠️ excepção de nome |
| `quform` | `bdt-quform` |
| `we-forms` | `bdt-we-form` ⚠️ excepção de nome |
| `wp-forms` | `bdt-wp-forms` |
### Outros / a confirmar dependência (30)
| Módulo | `widgetType` |
|---|---|
| `download-monitor` | `bdt-download-monitor` |
| `everest-forms` | `bdt-everest-forms` |
| `faq` | `bdt-faq` |
| `fluent-forms` | `bdt-fluent-forms` |
| `fooevents-calendar-carousel` | `bdt-fooevents-calendar-carousel` |
| `fooevents-calendar-events` | `bdt-fooevents-calendar-events` |
| `formidable-forms` | `bdt-formidable-forms` |
| `forminator-forms` | `bdt-forminator-forms` |
| `give-donation-history` | `bdt-give-donation-history` |
| `give-donor-wall` | `bdt-give-donor-wall` |
| `give-form` | `bdt-give-form` |
| `give-form-grid` | `bdt-give-form-grid` |
| `give-goal` | `bdt-give-goal` |
| `give-login` | `bdt-give-login` |
| `give-profile-editor` | `bdt-give-profile-editor` |
| `give-receipt` | `bdt-give-receipt` |
| `give-register` | `bdt-give-register` |
| `give-totals` | `bdt-give-totals` |
| `instagram-feed` | `bdt-instagram-feed` |
| `layer-slider` | `bdt-layer-slider` |
| `learnpress-carousel` | `bdt-learnpress-carousel` |
| `learnpress-grid` | `bdt-learnpress-grid` |
| `mailchimp-for-wp` | `bdt-mailchimp-for-wp` |
| `portfolio-carousel` | `bdt-portfolio-carousel` |
| `portfolio-gallery` | `bdt-portfolio-gallery` |
| `portfolio-list` | `bdt-portfolio-list` |
| `testimonial-carousel` | `bdt-testimonial-carousel` |
| `testimonial-grid` | `bdt-testimonial-grid` |
| `testimonial-slider` | `bdt-testimonial-slider` |
| `the-newsletter` | `bdt-the-newsletter` |
### Slider Revolution (1)
| Módulo | `widgetType` |
|---|---|
| `revolution-slider` | `bdt-revolution-slider` |
### Tabelas (WP Data Tables / TablePress) (2)
| Módulo | `widgetType` |
|---|---|
| `tablepress` | `bdt-tablepress` |
| `wpdatatable` | `bdt-wpdatatable` |
### The Events Calendar (3)
| Módulo | `widgetType` |
|---|---|
| `events-calendar-carousel` | `bdt-event-carousel` ⚠️ excepção de nome |
| `events-calendar-grid` | `bdt-event-grid` ⚠️ excepção de nome |
| `events-calendar-list` | `bdt-event-list` ⚠️ excepção de nome |
### Tutor LMS (2)
| Módulo | `widgetType` |
|---|---|
| `tutor-lms-course-carousel` | `bdt-tutor-lms-course-carousel` |
| `tutor-lms-course-grid` | `bdt-tutor-lms-course-grid` |
### WooCommerce (7)
| Módulo | `widgetType` |
|---|---|
| `wc-add-to-cart` | `bdt-wc-add-to-cart` |
| `wc-carousel` | `bdt-wc-carousel` |
| `wc-categories` | `bdt-wc-categories` |
| `wc-elements` | `bdt-wc-elements` |
| `wc-mini-cart` | `bdt-wc-mini-cart` |
| `wc-products` | `bdt-wc-products` |
| `wc-slider` | `bdt-wc-slider` |
### bbPress (12)
| Módulo | `widgetType` |
|---|---|
| `bbpress-forum-form` | `bdt-bbpress-forum-form` |
| `bbpress-forum-index` | `bdt-bbpress-forum-index` |
| `bbpress-reply-form` | `bdt-bbpress-reply-form` |
| `bbpress-single-forum` | `bdt-bbpress-single-forum` |
| `bbpress-single-reply` | `bdt-bbpress-single-reply` |
| `bbpress-single-tag` | `bdt-bbpress-single-tag` |
| `bbpress-single-topic` | `bdt-bbpress-single-topic` |
| `bbpress-single-view` | `bdt-bbpress-single-view` |
| `bbpress-stats` | `bdt-bbpress-stats` |
| `bbpress-topic-form` | `bdt-bbpress-topic-form` |
| `bbpress-topic-index` | `bdt-bbpress-topic-index` |
| `bbpress-topic-tags` | `bdt-bbpress-topic-tags` |
@@ -0,0 +1,70 @@
---
name: elementor-pro-widgets
description: Catálogo completo e verificado dos widgets nativos Elementor Pro (30 gerais + 5 WooCommerce) e como os descobrir/adicionar via EMCP Tools. Usar quando "widgets elementor pro", "que widgets pro existem", "loop grid", "nested tabs", "form elementor pro", "table of contents elementor", "portfolio elementor", "reviews carousel elementor".
layer: wiki
---
# /elementor-pro-widgets — Catálogo de Widgets Nativos Elementor Pro
Catálogo obtido ao vivo via `list-widgets(tier:"pro")` num site Elementor Pro 4.0.0 (EMCP Tools), cruzado com a documentação oficial. Widget types testados como legacy (`add-free-widget`/`add-pro-widget` + `widget_type`), não atómicos — Elementor Pro ainda não migrou o catálogo completo para elementos atómicos 4.0.
## Como descobrir e adicionar (workflow)
1. `list-widgets({ tier: "pro" })` — confirma quais dos 30+5 abaixo estão realmente disponíveis nesta ligação (Elementor Pro tem de estar activo).
2. `get-widget-schema({ widget_type })` — parâmetros curados (obrigatórios, defaults, enums). **Campo é `widget_type`, não `type`.**
3. `add-pro-widget({ post_id, parent_id, widget_type, settings })` num container legacy (`add-container`), ou `add-free-widget` se a tool `add-pro-widget` estiver desligada nesse site (verificar com `list-widgets` primeiro — `tier:"pro"` a devolver resultados não implica que `add-pro-widget` esteja montada; é frequentemente a primeira tool fechada num deny-list de produção).
4. Ver skill `emcp-page-building` para as regras de coexistência atómico/legacy e para `batch-update`/`update-element` genérico.
## Catálogo — widgets gerais (30)
| `widget_type` | Título | Uso |
|---|---|---|
| `form` | Form | Formulários de contacto/captação com tipos de campo configuráveis, acções de submissão |
| `posts` | Posts Grid | Grelha pesquisável de posts/pages com contagem de colunas e paginação |
| `countdown` | Countdown | Temporizador com modo data-limite ou evergreen, labels e mensagem de expiração custom |
| `price-table` | Price Table | Tabela de plano de preços com moeda, preço promocional, lista de features, ribbon, CTA |
| `flip-box` | Flip Box | Cartão de dois lados que vira ao hover, cada lado com gráfico/título/descrição |
| `animated-headline` | Animated Headline | Título com efeito de destaque ou texto rotativo |
| `call-to-action` | Call to Action | Bloco CTA com título, descrição, botão e gráfico/ribbon opcional |
| `slides` | Slides | Slider hero full-width com heading/descrição/botão/background por slide |
| `testimonial-carousel` | Testimonial Carousel | Carrossel rotativo de testemunhos com skins, layouts e navegação |
| `price-list` | Price List | Lista tipo menu com título, preço, descrição, imagem e separador |
| `gallery` | Gallery | Galeria de imagens avançada com layouts grid/justified/masonry e filtros |
| `share-buttons` | Share Buttons | Botões de partilha social para a página actual com skins e formas |
| `table-of-contents` | Table of Contents | Índice auto-gerado a partir dos headings da página, com marcação de secção activa |
| `blockquote` | Blockquote | Citação estilizada com texto, atribuição de autor, opção de tweet |
| `lottie` | Lottie Animation | Embed de animação Lottie JSON com triggers, loop, velocidade, renderer |
| `hotspot` | Hotspot | Imagem interactiva com pontos clicáveis/hover configuráveis |
| `nav-menu` | Navigation Menu | Menu de navegação WordPress com layouts, ponteiros de hover, breakpoint de dropdown |
| `loop-grid` | Loop Grid | Posts/pages/CPTs em grelha renderizada por um loop template |
| `loop-carousel` | Loop Carousel | Posts em carrossel renderizado por um loop template, responsivo |
| `media-carousel` | Media Carousel | Carrossel de imagens/vídeos com skins carousel/slideshow/coverflow |
| `nested-tabs` | Nested Tabs | Tabs modernas onde cada conteúdo de tab é um container populável livremente |
| `nested-accordion` | Nested Accordion | Acordeão moderno onde cada item é um container populável livremente |
| `portfolio` | Portfolio | Grelha filtrável de posts/CPTs para portfólio/trabalho, com modal |
| `author-box` | Author Box | Avatar, nome, bio do autor do post e link para os posts dele |
| `login` | Login | Formulário de login frontend com labels, remember-me, link de password perdida |
| `code-highlight` | Code Highlight | Bloco de código com syntax highlight, linguagem, tema, números de linha |
| `reviews` | Reviews | Carrossel de reviews/testemunhos com slides por vista, autoplay, loop |
| `off-canvas` | Off-Canvas | Painel off-canvas deslizante com posição, largura, animação de entrada |
| `progress-tracker` | Progress Tracker | Indicador de progresso de scroll (horizontal ou circular) |
| `search` | Search | Input de pesquisa do site com dropdown de resultados ao vivo opcional e tabs por post-type |
## Catálogo — widgets WooCommerce (5)
| `widget_type` | Título | Uso |
|---|---|---|
| `woocommerce-products` | WooCommerce Products | Grelha de produtos WooCommerce com colunas/linhas, ordenação |
| `wc-add-to-cart` | WooCommerce Add to Cart | Botão de adicionar ao carrinho para um produto específico, com quantidade |
| `woocommerce-cart` | WooCommerce Cart | Renderiza a página completa do carrinho WooCommerce |
| `woocommerce-checkout-page` | WooCommerce Checkout | Renderiza a página completa de checkout WooCommerce |
| `woocommerce-menu-cart` | WooCommerce Menu Cart | Ícone de mini-carrinho para o menu/header com indicador de itens |
## Notas
- Este catálogo é o que `list-widgets(tier:"pro")` devolveu ao vivo — pode variar ligeiramente por versão do Elementor Pro; correr sempre `list-widgets` primeiro no site alvo em vez de confiar cegamente nesta tabela para um `widget_type` exacto.
- `add-pro-widget` está frequentemente no deny-list de sites de produção (ver skill `emcp-tools` para o mecanismo). Se estiver desligada, reportar ao utilizador em vez de tentar contornar via `add-free-widget` com um `widget_type` Pro — normalmente falha da mesma forma (widget não registado para essa chamada).
## Skills relacionadas
- `emcp-page-building` — sequência de construção de página, coexistência atómico/legacy, `batch-update`.
- `elementskit-widgets` / `powerpack-widgets` — catálogos de addons de terceiros (não aparecem em `list-widgets`, exigem `get-widget-schema(..., full:true)`).
@@ -0,0 +1,161 @@
---
name: elementor-v4-visual-editor
description: Guia do editor visual Elementor Pro V4 (painel General/Style/Interactions, Class Manager, Variables Manager, breakpoints, animações nativas) para quem constrói/ajusta layouts na interface gráfica, distinto de emcp-page-building (que cobre a mesma versão via chamadas MCP/API). Usar quando "elementor v4 painel", "flexbox widget elementor", "class manager elementor", "variables manager elementor", "dark mode elementor", "interactions tab", "breakpoints elementor v4", "posiciona elementos no elementor", "porque é que o gap não aparece elementor".
layer: wiki
---
# /elementor-v4-visual-editor — Editor Visual Elementor Pro V4
Fonte: adaptado de [jainshwetank/elementor-pro-designer-skill](https://github.com/jainshwetank/elementor-pro-designer-skill) (MIT), traduzido e cruzado com os achados ao vivo de `emcp-page-building`. Termos de campos do painel mantidos em inglês (é a língua real da interface Elementor, mesmo em sites PT-PT) — só a prosa é PT-PT.
**Distinção de âmbito:** esta skill descreve o que aparece no **painel visual** (o que um humano vê e clica no editor Elementor, ou o que descreves a alguém a operar o editor). `emcp-page-building` descreve a **camada MCP/API** (chamadas `add-flexbox`/`update-element`/settings JSON) para o mesmo Elementor 4.x. Os nomes de campo do painel abaixo mapeiam directamente para chaves de `settings` das tools MCP — ex. painel "Layout > Direction: Column" ≈ `settings.flex_direction: "column"` em `add-flexbox`; ver `emcp-page-building` para a forma exacta de cada chave verificada ao vivo.
Referências detalhadas:
- **references/painel.md** — referência completa de tabs/widgets/campos, Variables Manager, Class Manager.
- **references/padroes.md** — padrões de layout reutilizáveis com instruções de painel completas.
- **references/troubleshooting.md** — problemas comuns e correcções.
## V4 vs V3 — o que mudou
V4 é uma alteração disruptiva do V3 em várias áreas. Nunca sugerir padrões V3 num site V4.
| Área | V3 | V4 |
|---|---|---|
| Widget de layout primário | Container | **Flexbox** (atómico) |
| Widget de layout secundário | Inner Container | **Div block** (atómico) |
| Tabs do painel | Content / Style / Advanced | **General / Style / Interactions** — sem tab Advanced |
| Sistema de classes | Limitado | **Class Manager** — classes globais reutilizáveis com 4 estados |
| Design tokens | Não nativo | **Variables Manager** — CSS custom properties |
| Interactions | Básico | Nativo por elemento, empilhável, disparado por scroll |
| Tipografia | Widget Text Editor | **Paragraph** (atómico) substitui Text Editor |
**Crítico:** se te apanhares a escrever "tab Advanced" ou "widget Container" — pára. São V3. Em V4: tab Style, widget Flexbox, tab Interactions.
## Perguntas a fazer primeiro
1. **Há um theme switcher dark/light?** Se sim → todas as cores têm de usar variáveis CSS (valores fixos no painel não respondem ao toggle). Se não → cores directas no painel são aceitáveis.
2. **Já existe um sistema de classes/variáveis, ou vamos construir um?** Se existente → pedir nomes de variáveis e convenções de classe antes de criar novas. Se de raiz → ver `references/painel.md` para convenções de nomeação recomendadas.
## Hierarquia de decisão
Antes de sugerir QUALQUER implementação, seguir esta ordem:
1. Uma classe existente resolve? Empilhar (`t-h2 + is-accent`).
2. Uma definição do tab Style resolve? Usar como valor LOCAL.
3. Uma variável CSS resolve? Referenciar via variable picker (ícone ƒ).
4. Precisa de uma classe global nova? Criar no Class Manager.
5. Precisa de uma variável nova? Criar no Variables Manager.
6. Precisa mesmo de Custom Code? Parar e repensar — quase sempre há solução panel-first.
7. Se Custom Code for mesmo inevitável: criar um **snippet nomeado separado**. Nunca acrescentar ao snippet de Design Tokens.
Classe global vs local: propriedade reutilizada em vários elementos → classe global. Uso único → local.
## Erros comuns de IA no Elementor V4
- **❌ Referenciar o tab Advanced** — não existe em V4. Propriedades vivem em General/Style/Interactions. O campo Custom CSS existe no fundo do tab Style mas deve ficar sempre vazio.
- **❌ Sugerir CSS inline/custom por widget** — classes primeiro, definições de painel depois. Custom CSS por widget contorna o sistema de classes.
- **❌ Confundir Flexbox com Container** — Container é o widget V3. Em V4 usar Flexbox (Atomic Elements).
- **❌ Hardcodar cores hex com theme switcher activo** — têm de ser variáveis CSS, senão não respondem ao dark mode.
- **❌ Usar alturas fixas para layout** — usar `Flex child > Flex Size: custom (grow: 1)` para preencher espaço disponível.
- **❌ Usar margens nos filhos para espaçamento uniforme** — usar Gap no Flexbox pai; margens só para espaçamento irregular específico.
- **❌ Sugerir `position: absolute` para problemas de layout** — quase sempre resolvível com Flex child settings; absolute é último recurso.
- **❌ Assumir que Laptop é breakpoint por omissão** — V4 vem com 3: Desktop, Tablet (1024px), Phone (767px). Laptop (1366px) é breakpoint custom, não vem por omissão.
- **❌ Assumir que todos os widgets começam sem padding** — alguns widgets V4 têm padding por omissão de 10px; definir explicitamente a 0 se não for pretendido.
## Formato de instrução (para guiar um humano no editor)
Dar sempre instruções pela ordem dos tabs: General → Style → Interactions. Nunca descrever CSS — descrever nomes de campo do painel e valores.
```
WIDGET: Flexbox
NAME: "Hero Content" (nome a escrever no campo de label do painel Structure)
GENERAL TAB:
HTML Tag: Section
ID: hero-content [só se necessário para JS ou anchor links]
STYLE TAB:
Classes: l-stack ← CLASSE GLOBAL
Layout:
Display: Flex ← vem da classe l-stack
Direction: Column ← vem da classe l-stack
Justify: Center ← LOCAL
Align: Center ← LOCAL
Gap, Row: var(--spacing_md) ← LOCAL, usar o variable picker (ƒ)
Spacing:
Padding Top/Bottom: var(--spacing_xl) ← LOCAL
Size:
Height: 90vh ← LOCAL
INTERACTIONS TAB:
1: Page load / Fade In / 750ms / delay 0ms
```
Legenda: `← CLASSE GLOBAL` (definida pela classe, não sobrepor localmente) · `← LOCAL` (definido directamente para este elemento) · `← NOVA CLASSE [nome]` (criar no Class Manager primeiro) · "usar variable picker" (clicar no ícone ƒ, nunca escrever hex à mão).
## Arquitectura de camadas
```
Page
└── Flexbox "Section Wrap" ← mais externo, sempre Flexbox
├── Flexbox "Section Content" ← container de layout primário
│ ├── Flexbox "Group A"
│ │ ├── Heading "..."
│ │ └── Paragraph "..."
│ └── Flexbox "Group B"
│ └── Image "..."
└── Div block "Transition" ← decorativo, não estrutural
```
Regras: secção mais externa = sempre Flexbox (nunca Div block) · limite de nesting = 4 níveis · distribuição de espaço = Flex child > Flex Size (grow), nunca alturas fixas · espaçamento uniforme = gaps no Flexbox pai · espaçamento irregular = margem só no filho específico · renomear sempre cada layer no painel Structure com nomes descritivos ("Hero Wrap", não "Flexbox 3").
## Sistema responsivo
Breakpoints por omissão V4: Desktop (base), Tablet (1024px), Phone (767px). Laptop (1366px) é recomendado como breakpoint custom (Elementor → Site Settings → Layout), não vem activo por omissão.
Desktop é sempre a base — definir tudo em Desktop primeiro, depois mudar o selector de breakpoint no cabeçalho do painel para overrides em tamanhos menores. Overrides propagam para baixo (um override em Tablet aplica-se a Phone salvo override próprio).
Variáveis CSS não mudam por breakpoint — para dimensionamento responsivo sem overrides de breakpoint, usar `clamp()` no Variables Manager:
```css
--spacing_section: clamp(1rem, 3vw, 1.5rem);
--font_h1: clamp(2rem, 5vw, 4rem);
```
Overrides standard em Phone (quando não se usa clamp): tamanhos de heading −20% do valor desktop · padding reduzido para variável de spacing menor · flex direction Row→Column para empilhar · gaps reduzidos.
## Dark mode
Só implementar se o projecto tiver um theme switcher light/dark. A abordagem V4 usa um atributo `[data-theme="dark"]` no `<html>`, alternado por JavaScript, com overrides de variáveis CSS no snippet Design Tokens: 1) valores light em `:root`; 2) overrides dark em `[data-theme="dark"]`; 3) todas as cores do painel via variáveis (trocam automaticamente); 4) botão de toggle com atributo custom `data-theme-toggle` (tab General → Attributes).
## Interactions
Tab 3 em todos os widgets V4. Múltiplas animações podem empilhar-se por elemento.
| Campo | Opções |
|---|---|
| Trigger | Page load / Scroll into view |
| Effect | Fade / Slide / Scale |
| Type | In / Out |
| Direction | Up / Down / Left / Right |
| Duration | 0–1500ms (passos fixos) |
| Delay | 0–1500ms (passos fixos) |
Elementos acima da dobra (visíveis sem scroll) → trigger **Page load**. Elementos abaixo da dobra → trigger **Scroll into view**.
## Checklist de qualidade
- [ ] Nomes de layer no painel Structure são descritivos
- [ ] Nenhum Custom CSS em nenhum widget
- [ ] Hierarquia de decisão seguida (classe → painel → variável → código)
- [ ] Máximo 4 níveis de nesting
- [ ] Se houver theme switcher: todas as cores usam variáveis, zero hex fixo
- [ ] Responsivo verificado em Desktop, Tablet, Phone (+ Laptop se adicionado)
- [ ] Interactions: Page load acima da dobra, Scroll into view abaixo
- [ ] Espaçamento usa gaps nos pais, não margens nos filhos
- [ ] Elementos com espaçamento inesperado: verificar padding 10px por omissão
## Skills relacionadas
- `emcp-page-building` — a mesma versão Elementor 4.x via chamadas MCP/API (`add-flexbox`, `update-element`, settings JSON) em vez do painel visual.
- `elementor-pro-widgets` / `elementskit-widgets` / `powerpack-widgets` — catálogos de widgets disponíveis para colocar dentro destes layouts.
@@ -0,0 +1,131 @@
# Padrões de Layout — Elementor V4
Fonte: adaptado de jainshwetank/elementor-pro-designer-skill (MIT). Sequências de instrução de painel testadas em produção — copiar e adaptar.
## Padrão: secção full-height com elemento pinado ao fundo
Caso de uso: heros, ecrãs de abertura com indicador de scroll pinado no fundo.
```
Flexbox "Section Wrap"
CLASSES: [a tua classe de secção]
STYLE > Layout > Direction: Column
STYLE > Layout > Align items: center
STYLE > Size > Height: 100vh ou 90vh ← LOCAL
├── Flexbox "Content"
│ STYLE > Layout > Justify: center
│ STYLE > Layout > Align: center
│ STYLE > Flex child > Flex Size: custom (grow 1) ← LOCAL
│ └── [Headings, CTAs, etc.]
│
└── [Indicador de scroll ou elemento de fundo]
STYLE > Spacing > Margin bottom: [variável de spacing] ← LOCAL
```
Chave: usar `Flex child > Flex Size: grow 1` na área de conteúdo, não uma altura fixa. O elemento de fundo pina naturalmente por ser o último flex child.
## Padrão: layout de duas colunas
Caso de uso: imagem + texto, métrica + descrição, qualquer arranjo lado a lado.
```
Flexbox "Row"
STYLE > Layout > Direction: Row
STYLE > Layout > Align items: center
STYLE > Layout > Gap, Column: [variável de spacing] ← LOCAL
├── Flexbox "Left Column"
│ STYLE > Flex child > Flex Size: custom (grow 1, shrink 1, basis 0%) ← LOCAL
│
└── Flexbox "Right Column"
STYLE > Flex child > Flex Size: custom (grow 1, shrink 1, basis 0%) ← LOCAL
```
Para colunas desiguais (ex. 60/40): basis a `60%` e `40%` respectivamente. No breakpoint Phone: mudar Direction para Column para empilhar.
## Padrão: imagem em moldura
Caso de uso: screenshots, mockups, qualquer imagem que precise de dimensionamento contido.
```
Flexbox "Frame"
STYLE > Size > Width: [px ou % específico] ← LOCAL
STYLE > Size > Aspect Ratio: [conforme o conteúdo — 16/10, 4/3, etc.] ← LOCAL
STYLE > Border > Radius: 4px ← LOCAL
STYLE > Size > Overflow: hidden ← LOCAL
└── Image
STYLE > Size > Width: 100%, Height: 100% ← LOCAL
STYLE > Size > Object fit: cover ← LOCAL
```
Importante: o aspect ratio deve servir o conteúdo. Não forçar enquadramento paisagem em conteúdo retrato (ex. screenshots de telemóvel, UI de app alta) — deixar o conteúdo ditar a forma da moldura.
## Padrão: cabeçalho de passo/secção
Caso de uso: cabeçalhos de secção numerados que comunicam hierarquia.
```
Flexbox "Step Header"
STYLE > Layout > Direction: Column
STYLE > Layout > Gap, Row: [variável de spacing pequena] ← LOCAL
├── Heading "Number" (ex. "01")
│ CLASSES: [a tua classe display/muted]
│ STYLE > Typography > Size: 72px ← LOCAL
│ STYLE > Typography > Weight: 200 ← LOCAL
│ STYLE > Effects > Opacity: 6% ← LOCAL
│
├── Heading "Label" (ex. "Step 1")
│ CLASSES: [a tua classe overline/label]
│
└── Heading "Title" (ex. "O padrão")
CLASSES: [a tua classe h2]
```
O número fantasma (opacity 6%) cria profundidade sem competir com o título.
## Padrão: fundos de secção alternados
Caso de uso: ritmo visual entre secções de conteúdo.
```
Secção A: sem background (herda o background do body)
Secção B: STYLE > Background > Color: [variável de bg secundária] ← LOCAL
Secção C: sem background (herda)
```
Nunca alternar com cores fixas — usar sempre variáveis para o dark mode funcionar.
## Padrão: secção centrada com largura de texto limitada
Caso de uso: secções de texto longo onde o comprimento de linha precisa de limite para legibilidade.
```
Flexbox "Section"
STYLE > Layout > Direction: Column
STYLE > Layout > Align items: center
STYLE > Spacing > Padding [T/B]: [variável de spacing grande]
└── Flexbox "Content"
STYLE > Size > Max width: 640px ← LOCAL
STYLE > Layout > Direction: Column
STYLE > Layout > Gap, Row: [variável de spacing média]
└── [Parágrafos, headings]
```
Aplicar max-width no wrapper de conteúdo interno, não na secção — a secção mantém-se full-width.
## Padrão: separador horizontal
Caso de uso: separação visual dentro de uma secção (não entre secções).
```
Div block "Divider"
STYLE > Size > Width: 100%, Height: 1px ← LOCAL
STYLE > Background > Color: [variável border/muted] ← LOCAL
STYLE > Spacing > Margin [T/B]: [variável de spacing] ← LOCAL
```
Usar um Div block, não o widget Divider — mais controlo, sistema de classes consistente.
@@ -0,0 +1,145 @@
# Referência de Painel — Elementor V4
Fonte: adaptado de jainshwetank/elementor-pro-designer-skill (MIT). Termos de campo mantidos em inglês (interface real do editor).
## Painel de widget: 3 tabs
Todo widget em V4 tem exactamente três tabs: **General**, **Style**, **Interactions**.
### Tab 1: General
- **HTML Tag** — Div / Section / Header / Footer / Main / Article / Nav / Aside
- **Link** — botão (+) para adicionar
- **ID** — campo de texto (anchor links ou hooks JS; deixar vazio caso contrário)
- **Attributes** — botão (+) para atributos `data-*` custom (ex. `data-theme-toggle`)
- **Display Conditions** — só Elementor Pro; mostrar/esconder por role, URL, data, etc.
### Tab 2: Style
Propriedades listadas de cima a baixo pela ordem real no painel.
**Classes** — topo do tab Style. Escrever nome de classe para aplicar do Class Manager. Várias classes empilham (separadas por espaço). Badge "local" = override de instância. Badge de classe = propriedade vinda da classe global.
**Layout**
- Display: `Block` / `Flex` (omissão) / `In-blk` / `None` / `Inline-flex`
- Direction (só Flex): `Row` (omissão) / `Column` / `Row reversed` / `Column reversed`
- Justify content: flex-start / center / flex-end / space-between / space-around / space-evenly
- Align items: flex-start / center / flex-end / stretch
- Gap: Column gap (px) + Row gap (px), toggle de link para sincronizar
- Wrap: nowrap / wrap / wrap-reverse
Sub-secção Flex child (visível quando o elemento está dentro de um pai Flex):
- Align self: auto / flex-start / center / flex-end / stretch
- Order: inteiro
- Flex Size: Fill space (grow 1) / Fit content / Fixed / Custom (grow + shrink + basis)
**Spacing**
- Margin: T/R/B/L, toggle de link, unidades px/%/vw/vh/em/rem
- Padding: T/R/B/L, toggle de link, mesmas unidades
- Nota: alguns widgets V4 têm **padding por omissão de 10px** — verificar sempre e definir a 0 se não pretendido.
**Size**
- Width, Height, Min width, Min height, Max width, Max height (px/%/vw/vh/em/rem)
- Overflow: visible / hidden / auto / scroll
- (Show more): Aspect Ratio, Object fit (cover/contain/fill/none)
**Position**
- Position: default / relative / absolute / fixed / sticky
- Inset (top/right/bottom/left) aparece quando não-default
- Z-index, Anchor offset (para sticky)
**Typography**
- Font family — picker; usar variable picker (ƒ) para variável de fonte
- Font weight: 100–900
- Font size + unidade — variable picker (ƒ) para variável de tamanho ou valor clamp
- Text align: left/center/right/justify
- Text color — picker; variable picker (ƒ) para variável de cor
- (Show more): Line height, Letter spacing, Word spacing, Text decoration, Text transform, Direction (LTR/RTL), Font style, Text stroke
**Background**
- Color — picker; variable picker (ƒ)
- Overlay (+): gradiente ou imagem
- Clipping: text / content-box / border-box / padding-box
**Border**
- Width: T/R/B/L ou todos, toggle de link
- Color — picker; variable picker (ƒ)
- Type: none/solid/dashed/dotted/double/groove/ridge
- Radius: TL/TR/BR/BL, toggle de link
**Effects**
- Blend mode, Opacity (0–100%)
- Box shadow (+): cor, X, Y, blur, spread, toggle inset
- Transform (+): rotate, scale, translate, skew
- Transitions (+): propriedade, duração, timing, delay
- Filters (+): blur, brightness, contrast, grayscale, hue-rotate, invert, saturate, sepia
- Backdrop filters (+): mesmas opções, aplicadas ao fundo atrás do elemento
**Custom CSS** — editor de código no fundo do tab Style. **Deixar sempre vazio.** Usar classes e definições de painel em vez disso.
### Tab 3: Interactions
Botão (+) no topo. Várias interactions podem empilhar-se num elemento.
- **Trigger**: Page load / Scroll into view
- **Effect**: Fade / Slide / Scale
- **Type**: In / Out
- **Direction**: Up / Down / Left / Right
- **Duration**: 0/100/200/300/400/500/750/1000/1250/1500 ms
- **Delay**: mesma escala
- **Preview**: botão ▶
- **Delete**: ✕
## Widgets atómicos (V4 — usar estes)
| Widget | Propósito | Substitui (V3) |
|---|---|---|
| Flexbox | Container de layout com flex | Container |
| Div block | Bloco genérico não-flex | Inner Container |
| Heading | `<h1>`–`<h6>` | Heading (Basic) |
| Paragraph | Texto corpo/rich text | Text Editor (Basic) |
| Image | Imagem com object-fit | Image (Basic) |
| Button | Botão CTA | Button (Basic) |
| SVG | SVG inline | — |
| Divider | Linha `<hr>` | Divider (Basic) |
| YouTube | Vídeo embed | Video (Basic) |
| Tabs | Conteúdo em tabs | — |
**Correspondência com `emcp-page-building`:** estes 10 widgets atómicos mapeiam directamente para as tools MCP `add-flexbox`/`add-div-block`/`add-atomic-heading`/`add-atomic-paragraph`/`add-atomic-image`/`add-atomic-button`/`add-atomic-svg`/`add-atomic-divider`/`add-atomic-youtube` — confirmado ao vivo nessa skill. "Tabs" não tem tool de conveniência própria confirmada; usar `add-atomic-widget` genérico.
## Widgets legacy (V3 — evitar em construções V4 novas)
Container, Inner Container, Text Editor, Spacer, Google Maps. Continuam a existir por compatibilidade. Não usar em construções V4 novas — mas ver `emcp-page-building` para os casos em que um widget legacy é mesmo necessário (terceiros sem versão atómica: ElementsKit, PowerPack, etc.).
## Variables Manager
**Acesso:** ao editar um campo de cor/tamanho no tab Style, clicar no ícone ƒ (variable picker), depois **+** para criar uma variável nova. Acesso directo: ícone ƒ → engrenagem (⚙) abre o Variables Manager completo.
Variáveis são CSS custom properties. O Elementor guarda-as com prefixo `--`, logo uma variável `color_accent` referencia-se em código como `var(--color_accent)`.
**Convenção de nomes** (underscores como separador):
```
color_accent → cor de destaque da marca
color_primary → cor primária de texto/marca
color_bg-primary → cores de fundo/texto/borda
spacing_md → padding, margin, gap
font_heading → família de fonte
size_section-pad → spacing composto para secções
```
**`clamp()` para variáveis responsivas** — definir valores fluidos no Variables Manager para escalar sem overrides por breakpoint:
```css
clamp(2rem, 5vw, 4rem) /* tamanho de fonte que escala de 2rem (mobile) a 4rem (desktop) */
clamp(1rem, 3vw, 2rem) /* spacing que escala de 1rem a 2rem */
```
Escrever a expressão clamp directamente como valor da variável. Aplicar a tamanhos de fonte, padding de secção e valores de gap resolve a maior parte da responsividade num único sítio.
## Class Manager
**Acesso:** tab Style → campo Classes → escrever nome de classe → link "Manage Classes", ou via Elementor site settings.
**O que uma classe guarda:** múltiplas propriedades, cada uma podendo referenciar uma variável. Exemplo: uma classe `t-body` pode definir font-family (via `var(--font_body)`), font-size (via `var(--size_body)`), line-height (1.55) e color (via `var(--color_text-primary)`) — tudo numa classe.
**Quatro estados por classe:** 1. Normal (omissão) · 2. Hover (mouse por cima) · 3. Focus (foco de teclado) · 4. Active (pressionado). Definir cada estado conforme necessário; Hover é o mais usado além de Normal; deixar vazio se não houver mudança nesse estado.
**Badge de classe vs local:** propriedade da classe aplicada e não sobreposta → badge de classe. Override de uma propriedade da classe para um elemento específico → badge "local" (não altera a classe em si, é override de instância).
@@ -0,0 +1,108 @@
# Troubleshooting — Elementor V4
Fonte: adaptado de jainshwetank/elementor-pro-designer-skill (MIT). Sintomas, causas e correcções para problemas comuns do editor visual V4.
## Problemas de layout
**Elementos não empilham verticalmente dentro de um Flexbox**
- Verificar Direction — tem de estar ↓ (column), não → (row).
- Se uma classe está a definir a direction, sobrepor localmente ou rever a classe.
**Flex child não preenche a altura restante**
- Definir Flex child > Flex Size: Custom → Grow: 1, Shrink: 0, Basis: 0%.
- O pai precisa de altura definida (vh, px, ou ele próprio flex) para o grow funcionar.
- Nunca usar `height: 100%` num flex child — usar grow.
**Duas colunas a empilhar verticalmente em desktop**
- Direction do Flexbox pai tem de ser → (row), não ↓ (column).
- Se uma classe força column, sobrepor localmente no breakpoint desktop.
**Gap não aparece entre elementos**
- Gap só funciona em pais Flexbox. Se o pai é um Div block (não-flex), gap não tem efeito.
- Solução: mudar o pai para Flexbox, ou usar Spacing > Margin nos filhos individuais.
**Elemento a fazer overflow do pai**
- Definir no pai: Size > Overflow: hidden.
- Verificar se uma width/min-width fixa no filho excede o pai.
**Elemento sticky não fica sticky**
- Position tem de estar definido como Sticky no próprio elemento.
- O pai não pode ter `overflow: hidden` — isso quebra o sticky.
- Anchor offset controla a que distância do topo o elemento gruda.
## Problemas de espaçamento
**Padding não aplica simetricamente**
- Verificar se o toggle de link (ícone de corrente) está activo — se não, T/R/B/L são independentes.
- Verificar se uma classe está a definir padding conflituoso — o badge "local" aparece se for override.
**Margem entre elementos é irregular**
- Se usares margin nos filhos, os gaps compõem-se entre alguns elementos.
- Solução: remover margens dos filhos, usar Gap no Flexbox pai. Margem no filho só para espaçamento irregular pontual.
## Problemas de tipografia
**Tamanho de fonte não muda**
- Uma classe pode estar a definir o tamanho e estás a editar um override local que não tem efeito.
- Verificar se o campo Typography > Font size mostra badge de classe ou badge "local".
- Se badge de classe: mudar a classe no Class Manager, ou sobrepor localmente.
**Cor do texto não corresponde à variável**
- Confirmar que o campo de cor mostra o nome da variável, não um hex.
- Se mostrar hex: abrir o color picker, seleccionar o ícone de variável (ƒ), escolher a variável.
**Line height demasiado apertado ou solto**
- Line height está escondido em Style > Typography > Show more.
- Texto corpo: 1.5–1.6 é standard. Headings: 1.1–1.2 é standard.
## Problemas de dark mode
**Dark mode não aplica**
- Verificar que o snippet Design Tokens contém overrides `[data-theme="dark"]`.
- Verificar que o botão de toggle tem o atributo `data-theme-toggle` no tab General → Attributes.
- Verificar que o snippet Dark Mode Toggle está activo e na posição footer.
- Abrir DevTools e confirmar que o elemento `html` tem o atributo `data-theme="dark"` depois de clicar no toggle.
**Alguns elementos não mudam no dark mode**
- Só elementos a usar variáveis CSS mudam. Cores hex fixas não mudam.
- Localizar o elemento, verificar cada campo de cor — substituir hex por variável apropriada.
**Flash do tema errado ao carregar a página (FOUC)**
- O snippet Dark Mode Toggle lê o localStorage ao carregar, mas o snippet FOUC pode atrasar a visibilidade.
- Garantir que o atributo `data-theme` é aplicado antes de `visibility: visible` disparar.
- Solução: mover a leitura do localStorage para um `<script>` inline no `<head>` (antes do snippet FOUC disparar).
## Problemas de interactions
**Animação não dispara**
- Trigger tem de ser "Scroll into view" para reveals por scroll.
- Testar fazendo scroll do elemento para fora e depois de volta.
- Se a página carregou com o elemento já visível, "Scroll into view" nunca dispara — usar "Page load" para elementos acima da dobra.
**Animação a disparar cedo demais / elemento visível antes da animação**
- A direction "Out" (Fade Out, Slide Out) é para animações de saída. Provavelmente queres "In".
- Se elementos estão visíveis antes do scroll, verificar se uma animação anterior os deixou num estado visível.
**Várias animações no mesmo elemento em conflito**
- Verificar o tab Interactions — empilha. Se houver uma animação residual de testes, apagar (botão ✕).
- Duas "Fade In" no mesmo elemento disparam ambas e podem entrar em conflito.
**Interaction não aparece no tab Interactions**
- O tab Interactions é o Tab 3 (não Style, não General).
- Só disponível em elementos Elementor V4 — não em widgets legacy V3.
## Problemas de Custom Code
**Snippet Custom Code não aplica**
- Verificar se o snippet está Active (toggle na lista de snippets).
- Verificar a Location: `<head>` para CSS, footer para JS.
- Verificar erros de sintaxe — um `;` em falta parte o snippet inteiro.
**Variável CSS não resolve**
- Nomes de variável em Custom Code usam prefixo `--`: `var(--color_accent)`.
- Confirmar que a variável está definida em `:root` no snippet Design Tokens.
- O Elementor acrescenta `--` automaticamente às variáveis do Variables Manager, mas snippets Custom Code têm de usar a sintaxe completa `var(--nome)`.
**Snippet Design Tokens a crescer demasiado**
- Só variáveis de cor e mapeamentos classe-para-cor pertencem aqui.
- Se estiveres a acrescentar regras de layout, animações ou estilos de componente — mover para um snippet novo e nomeado.
+95 -2
View File
@@ -1,7 +1,6 @@
---
name: elementor
description: Desenvolvimento avançado com Elementor Pro e ecossistema Crocoblock. Cria widgets custom, configura Theme Builder, automatiza deploys, manipula _elementor_data via MySQL, REST API, Dynamic Tags e cache CSS.
layer: wiki
---
# /elementor - Elementor Development
@@ -228,4 +227,98 @@ Evitar: Section > Column (legacy, mais pesado)
### 5. Minimizar Widgets
```
Regra: M
Regra: Máximo 50 widgets por página
Usar: CSS custom em vez de widgets simples
```
## Breakpoints
```
Desktop: > 1024px
Tablet: 768-1024px
Mobile: < 768px
Configuração:
Elementor > Settings > Advanced > Breakpoints
```
## Troubleshooting
### Widget não aparece / CSS desactualizado
```bash
# CWP — SEMPRE com prefixo PHP completo
PHP="/opt/alt/php-fpm83/usr/bin/php"
WP="$PHP /usr/local/bin/wp --allow-root --path=/home/USER/public_html"
# Regenerar CSS (após qualquer alteração programática)
$WP elementor flush-css --regenerate
# Substituição segura de URLs (NUNCA usar wp search-replace directamente)
$WP elementor replace-urls https://antigo.com https://novo.com
# Sincronizar BD após update do plugin
$WP elementor update db
```
### Erro após update
```bash
# Rollback Elementor
Elementor > Tools > Version Control > Rollback
```
### Performance lenta
```
1. Desactivar Google Fonts desnecessárias
2. Activar cache CSS minificado
3. Usar Flexbox Container
4. Lazy load imagens
5. Limitar widgets por página
```
## Automação Programática (WP-CLI / MySQL / REST API)
Para deploy automatizado, pipelines IA, migração em massa e manipulação programática do Elementor sem GUI, consultar:
**`references/automation.md`** — Referência completa com:
- Pipeline de deploy CWP (kit import + replace-urls + flush-css)
- Estrutura `_elementor_data` JSON e queries MySQL com `JSON_REPLACE`
- REST API custom endpoint (com `wp_slash`, cache invalidation)
- PHP Dynamic Tags para conteúdo runtime (requer Pro)
- Geração de Kit ZIP em Python
- Rebranding global via `elementor_active_kit`
- Regras críticas (NUNCA `wp search-replace` em `_elementor_data`)
- MCP Elementor (Ultimate Elementor MCP, ~60 tools)
## Datasets Dify
| Dataset | ID | Prioridade |
|---------|----|-----------:|
| **Elementor** | `9c77d3e2-4d88-4a43-abff-d4e681267cc7` | 1 |
| **Crocoblock** | `bdf85c26-1824-4021-92d1-be20501b35ac` | 1 |
| **Crocooblock** | `139cdf67-afce-46ec-9ccd-2a06040e5b9d` | 1 |
| **Wordpress** | `9da0b2b9-5051-4b99-b9f6-20bf35067092` | 2 |
---
## Referências Adicionais
- **`references/automation.md`** — Automação programática completa (WP-CLI pipelines, MySQL, REST API, PHP Hooks, Kits, CSS)
---
**Versão**: 1.2.0 | **Autor**: Descomplicar® | **Actualizado**: 18-02-2026
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
@@ -0,0 +1,73 @@
---
name: elementskit-widgets
description: Catálogo completo e verificado dos 42 widgets ElementsKit Lite (Wpmet) para Elementor, com o widgetType real de cada um extraído do código-fonte — não estão no catálogo curado do EMCP Tools (list-widgets), por isso precisam de get-widget-schema com full:true. Usar quando "elementskit", "elements kit", "widget ekit", "elementskit-button", "que widgets elementskit existem", "nav menu elementskit".
layer: wiki
---
# /elementskit-widgets — Catálogo de Widgets ElementsKit Lite
ElementsKit Lite (Wpmet, plugin `elementskit-lite`) regista os próprios widgets Elementor **fora** do catálogo curado do EMCP Tools — `list-widgets` não os lista. Este catálogo foi extraído directamente do código-fonte (`wp-content/plugins/elementskit-lite/widgets/*/`, ficheiros `*-handler.php`, método `get_name()`) num site em produção com ElementsKit Lite 4.0.1, não adivinhado por convenção de nomes.
## Como usar (workflow)
1. Confirmar que `elementskit-lite` está activo: `list-plugins` e procurar o slug — **atenção:** o site pode ter também `elementskit` (versão Pro) instalado mas **inactivo**; confirmar qual dos dois está `active:true` antes de assumir que widgets Pro do ElementsKit existem.
2. `get-widget-schema({ widget_type: "elementskit-<slug>", full: true })` — estes widgets **não estão no catálogo curado**, por isso a chamada sem `full:true` devolve `{"error": "Not in the curated catalog. Retry with full:true..."}`. `full:true` devolve o schema de controlos bruto e funciona.
3. Adicionar com `add-free-widget({ post_id, parent_id, widget_type, settings })` num container **legacy** (`add-container`) — confirmado ao vivo que widgets ElementsKit são legacy, não têm equivalente atómico. Ver skill `emcp-page-building` para a mecânica de coexistência atómico/legacy.
4. **Nunca assumir o `widgetType` por convenção `elementskit-<pasta>`** — a tabela abaixo tem duas excepções confirmadas (`nav-menu` e `tab`); verificar sempre com `get-widget-schema(full:true)` antes de construir em produção.
## Catálogo (42 widgets, `widgetType` real verificado no código-fonte)
| `widgetType` | Título | Categoria/nota |
|---|---|---|
| `elementskit-accordion` | Accordion | geral |
| `elementskit-back-to-top` | Back to Top | geral |
| `elementskit-blog-posts` | Blog Posts | posts |
| `elementskit-business-hours` | Business Hours | marketing |
| `elementskit-button` | Button | geral — visto ao vivo em uso real |
| `elementskit-category-list` | Category List | geral |
| `elementskit-client-logo` | Client Logo | marketing |
| `elementskit-contact-form7` | Contact Form 7 | integração — exige plugin CF7 activo |
| `elementskit-countdown-timer` | Countdown Timer | marketing |
| `elementskit-drop-caps` | Drop Caps | geral |
| `elementskit-dual-button` | Dual Button | geral |
| `elementskit-faq` | FAQ | geral |
| `elementskit-fluent-forms` | Fluent Forms | integração — exige Fluent Forms activo |
| `elementskit-funfact` | Funfact | criativo |
| `elementskit-header-info` | Header Info | geral |
| `elementskit-header-offcanvas` | Header Offcanvas | geral |
| `elementskit-header-search` | Header Search | geral |
| `elementskit-heading` | Heading | geral |
| `elementskit-icon-box` | Icon Box | geral |
| `elementskit-icon-hover` | Icon Hover | geral |
| `elementskit-image-accordion` | Image Accordion | geral |
| `elementskit-image-box` | Image Box | geral |
| `elementskit-image-comparison` | Image Comparison | geral |
| `elementskit-lottie` | Lottie | animação |
| `elementskit-mail-chimp` | Mail Chimp | integração — exige Mailchimp configurado |
| **`ekit-nav-menu`** | ElementsKit Nav Menu | geral — **excepção: prefixo `ekit-`, não `elementskit-`** |
| `elementskit-ninja-forms` | Ninja Forms | integração — exige Ninja Forms activo |
| `elementskit-page-list` | Page List | geral |
| `elementskit-piechart` | Pie Chart | geral |
| `elementskit-post-grid` | Post Grid | posts |
| `elementskit-post-list` | Post List | posts |
| `elementskit-post-tab` | Post Tab | posts |
| `elementskit-pricing` | Pricing Table | marketing |
| `elementskit-progressbar` | Progress Bar | geral |
| `elementskit-social-media` | Social | geral — nota: pasta chama-se `social`, widgetType é `elementskit-social-media` |
| `elementskit-social-share` | Social Share | geral |
| **`elementskit-simple-tab`** | Tab | geral — **excepção: pasta chama-se `tab`, widgetType é `elementskit-simple-tab`** |
| `elementskit-tablepress` | TablePress | integração — exige plugin TablePress activo |
| `elementskit-team` | Team | geral |
| `elementskit-testimonial` | Testimonial | geral — visto ao vivo em uso real |
| `elementskit-video` | Video | geral |
| `elementskit-we-forms` | WeForms | integração — exige weForms activo |
| `elementskit-wp-forms` | WP Forms | integração — exige WPForms activo, visto ao vivo em uso real |
## Definições transversais injectadas pelo ElementsKit em QUALQUER widget
Confirmado ao vivo em `get-element-settings` de um widget `heading` legacy comum (não um widget ElementsKit): a presença do ElementsKit Lite injecta a definição `ekit_cursor_text_label` ("Elementskit Cursor") em widgets genéricos via uma extensão global de cursor. Não é exclusivo dos 42 widgets acima — pode aparecer em qualquer widget legacy no site.
## Skills relacionadas
- `emcp-page-building` — mecânica de coexistência atómico/legacy, sequência de construção.
- `elementor-pro-widgets` — catálogo nativo Elementor Pro (curado, no `list-widgets`).
- `powerpack-widgets` — catálogo do addon concorrente PowerPack Elements (também fora do catálogo curado).
@@ -0,0 +1,56 @@
---
name: emcp-content-ops
description: Gestão de conteúdo WordPress (posts/pages/CPTs, taxonomias, media, menus, redirects, blocos Gutenberg, pesquisa de conteúdo) via qualquer ligação MCP EMCP Tools — distinto de construção de páginas Elementor (ver emcp-page-building) ou auditoria de segurança/performance (ver emcp-site-audit). Usar quando "gerir conteúdo wordpress via mcp", "emcp posts", "upload media mcp", "menus wordpress mcp", "redirects mcp", "blocos gutenberg mcp".
layer: wiki
---
# /emcp-content-ops — Operações de Conteúdo WordPress via EMCP Tools
Aplica-se a qualquer ligação MCP EMCP Tools. Confirma sempre o site alvo com `core/get-site-info` — nunca assumir pelo nome da ligação.
## Posts / Pages / CPTs
- `list-post-types` primeiro num site desconhecido — os CPTs variam muito (visto ao vivo: `e-landing-page`, `podcast`, `elementor_library`, `elementskit_content`/`elementskit_template` além dos standard `post`/`page`).
- `create-post({ post_type, title, content, status, slug, author, date, parent, terms, meta, featured_image })` escreve `post_content` (HTML clássico ou markup de blocos Gutenberg) — **não** dados Elementor. Para uma página construída em Elementor usar `create-page` (ver `emcp-page-building`); `create-post` explicitamente não toca em dados Elementor.
- `get-post`/`list-posts`/`update-post`/`delete-post`/`set-post-terms`. `is_elementor` em `get-post`/`list-posts` diz qual editor é dono de determinada página — nunca editar o conteúdo de uma página Elementor via `update-post`.
- `delete-post` vai para o Lixo por omissão (reversível); `force:true` para eliminação permanente. Devolve também um `redirect_suggestion` a lembrar de chamar `create-redirect` para o URL agora morto — agir sobre isso em conteúdo publicado, ignorar em rascunhos/conteúdo de teste.
- `set-post-terms({ mode: replace|append|remove, create_missing })` — **`create_missing:true` deixa um termo de taxonomia permanente** se não houver tool de eliminação de termos montada na ligação (comummente o caso). Confirmar nomes de termos antes de usar em conteúdo descartável.
## Media
- `list-media`/`get-media` para a biblioteca existente (pesquisa por título/alt/legenda/descrição) antes de recorrer a fotos de stock — reutilizar o que já existe.
- `upload-media({ filename, data: base64, alt, title, caption, description })` — bytes vindos do **cliente**, não de um URL. `sideload-image(url)` — o **servidor** vai buscar um URL que consegue alcançar. Casos de uso diferentes, não confundir.
- `search-images`/`add-stock-image` (Unsplash/Pexels/Pixabay) exigem uma chave API do provedor configurada em EMCP Tools → Connection nesse site específico — **verificar que está configurada antes de planear um workflow em torno disso**; um site sem nenhuma configurada devolve um erro simples "sem chave API", não é uma falha da tool em si.
- `delete-media` está comummente no deny-list (destrutivo, o WordPress ignora o Lixo para media por omissão). Se não estiver montada para esta ligação, qualquer upload é efectivamente permanente via MCP — obter confirmação explícita antes de fazer upload, e verificar primeiro se está montada.
- `update-media` para correcções de alt text/título/legenda/descrição — seguro, não-destrutivo, boa opção por omissão para passagens de limpeza de acessibilidade/SEO.
## Menus
`menu-read`/`menu-write` são **tools despachadoras** — chamar sem argumento `operation` primeiro para obter a lista de operações de leitura/escrita disponíveis e as formas exactas de argumento para a versão do plugin nessa ligação, depois chamar de novo com `{ operation, arguments }`. Não adivinhar nomes de operação.
## Redirects
`list-redirects`/`find-broken-links` são só-leitura e comummente activas por omissão. `create-redirect`/`update-redirect`/`delete-redirect` estão comummente no deny-list em sites de produção (mutação de routing com impacto directo em SEO) — verificar se estão montadas antes de assumir que se pode agir sobre um achado de link partido; se não estiverem, reportar o achado e deixar o utilizador criar o redirect via wp-admin ou um canal WP-CLI.
`find-broken-links` é só-leitura e seguro de correr a qualquer momento — propõe correcções, não muda nada. Bom primeiro passo antes de qualquer trabalho de redirects.
## Blocos Gutenberg (independente do Elementor)
- `list-blocks`/`get-block-schema` (batch via `names[]`) → `add-block` (markup bruto, `position.mode: append|prepend|before|after|inside`) é o padrão descobrir → schema → aplicar.
- `get-post-blocks` devolve um **PATH em índice por bloco** (ex. `[2,1]`) — chamar sempre isto imediatamente antes de `update-block`/`remove-block`/`move-block`/`duplicate-block` para obter os paths actuais; os paths mudam após cada edição, não fazer cache entre chamadas.
- `list-patterns`/`insert-pattern` para composições de blocos pré-construídas.
- Estas tools operam sobre `post_content`, por isso são a camada certa para posts/CPTs clássicos que não usam Elementor — não para páginas Elementor (`is_elementor: true`), que não têm árvore de blocos Gutenberg significativa.
## Pesquisa/reutilização de conteúdo
`search-content(query)` — pesquisa tipo linguagem natural sobre páginas/templates/widgets/estilos globais, ordenada por relevância. Faz correspondência a substrings/tokens literais no conteúdo indexado, não pesquisa semântica difusa — uma query com uma palavra ausente verbatim de qualquer título/conteúdo devolve zero resultados mesmo que o conceito esteja presente. Se os resultados parecerem desactualizados (edições recentes não reflectidas), chamar `reindex-search` primeiro — o índice também actualiza incrementalmente ao gravar, por isso um reindex completo só é preciso na primeira construção ou para forçar um refresh. Usar isto antes de construir conteúdo novo, para encontrar e reutilizar/clonar uma página/template/widget existente em vez de duplicar trabalho.
## Plugins / Temas / Utilizadores / Definições (nível só-leitura, quase universal)
- `list-plugins`/`search-plugins`, `list-themes`/`search-themes`, `list-users`/`get-user` — listagem e pesquisa comummente activas; instalar/activar/desactivar/actualizar/eliminar plugins ou temas, e criar/actualizar utilizadores, estão comummente no deny-list (categorias de maior risco depois de execução de código). Verificar o que está realmente montado antes de prometer que uma mutação é possível.
- `get-settings`/`update-settings` — um subconjunto curado e allowlisted de definições WordPress (Geral/Leitura/Escrita/Discussão/Media/Permalinks), não `wp_options` bruto. `update-settings` reporta chaves ignoradas (`skipped`) com a razão (só-leitura, não allowlisted, valor inválido) em vez de falhar todo o lote — verificar o array `skipped`, não assumir que todas as chaves passadas foram aplicadas.
- `theme-read`/`theme-write` e leitores específicos de framework (ex. `astra-read`, quando o Astra é o tema activo — só regista se esse tema estiver activo) são **tools despachadoras** também: chamar sem `operation` primeiro para descobrir o que está disponível no tema específico desse site.
## Referência cruzada
- `emcp-page-building` — construção/edição específica de Elementor (atómico + legacy).
- `emcp-site-audit` — scan de segurança, análise de performance, acesso só-leitura a BD/filesystem, ledger de mudanças + rollback.
@@ -0,0 +1,73 @@
---
name: emcp-page-building
description: Construção e edição de páginas Elementor via qualquer ligação MCP EMCP Tools (emcp-descomplicar, emcp-emanuelalmeida, emcp-tools/starter, ou outra). Cobre elementos atómicos Elementor 4.0+, containers/widgets legacy, templates, operações em lote, e as armadilhas reais de parâmetros/comportamento encontradas por teste ao vivo. Usar quando "construir página elementor", "emcp page building", "widget atómico", "add-flexbox", "apply-template", "criar página com mcp".
layer: wiki
---
# /emcp-page-building — Construção de Páginas Elementor via EMCP Tools
Aplica-se a **qualquer** ligação MCP EMCP Tools, não a um site específico. Confirma sempre o site alvo com `core/get-site-info` antes de escrever — nunca assumas pelo nome da ligação.
Factos abaixo verificados ao vivo contra uma instalação EMCP Tools 3.12.1 em produção (Elementor 4.2.2 + Pro), revertidos depois via ledger. Descrevem o comportamento do **plugin**, por isso generalizam a qualquer site com a mesma versão — reverifica numa página de rascunho descartável se o site tiver uma versão materialmente diferente.
## Ordem de descoberta (antes de construir algo desconhecido)
1. `detect-elementor-version` — confirma suporte atómico + `recommended_mode` (`"atomic"` em sites 4.0+, usar tools atómicas por omissão).
2. `list-widgets` (opcional `tier: free|pro|woo`, `category`, ou pesquisa livre) — catálogo curado nativo Elementor/Pro/Woo. **Não inclui widgets de terceiros** (ElementsKit, PowerPack) — ver skills `elementskit-widgets` e `powerpack-widgets`.
3. `get-widget-schema({ widget_type })` — **o parâmetro é `widget_type`, não `type`.** Nome errado falha com um erro genérico de "propriedade obrigatória" que não diz qual campo.
4. `get-container-schema` — schema completo de controlos de container (flex + grid).
5. `get-page-structure({ post_id })` antes de tocar numa página existente — conhece sempre a árvore actual antes de mutar.
## Atómico (Elementor 4.0+) vs legacy — coexistem na mesma página
Confirmado ao vivo: uma página com um `e-flexbox` atómico (com widgets `e-heading`/`e-paragraph` filhos) ao lado de um `container` legacy (com um widget `heading` legacy) gravou e renderizou a estrutura correctamente. Não é preciso escolher um modo por página.
- Containers atómicos: `add-flexbox`, `add-div-block`. Widgets atómicos só ligam a estes (ou a outro container atómico) como `parent_id` — `add-atomic-heading` etc. rejeitam um `container` legacy como pai.
- Containers legacy: `add-container`. Widgets legacy (`add-free-widget`, ex. `widget_type: "heading"`) só ligam a containers legacy.
- Usa legacy para qualquer widget sem equivalente atómico ainda — confirmado ao vivo: widgets de terceiros como `elementskit-*`/`elementskit-wp-forms` só existem como widgets legacy.
- `update-element` funciona em elementos atómicos e legacy genericamente. `update-atomic-widget` é a variante de merge parcial específica para atómicos — preferir para atómicos, mas `update-element` é o fallback universal seguro.
- `batch-update({ post_id, operations: [{element_id, settings}, ...] })` actualiza vários elementos (atómicos ou legacy, misturados) numa só gravação — preferir sempre isto a N chamadas sequenciais de `update-element`.
## 🔴 Armadilhas confirmadas ao vivo — não confiar só na descrição da tool
1. **`set-element-label`**: o campo real obrigatório é `title`, não `label`, apesar da descrição "Sets an element's Navigator label" sugerir `label`. Ler o schema (`read xd://mcp__<ligação>_emcp_tools_set_element_label`) antes da primeira utilização numa ligação nova.
2. **`add-custom-js`**: exige `parent_id` (tem de ser um container **legacy** — insere um widget HTML) e o campo de código é `js`, não `code`. Não liga a um `e-flexbox`/`e-div-block` atómico.
3. **`apply-template` pode substituir TODO o conteúdo existente da página em vez de inserir.** Testado: chamar `apply-template(post_id, template_id)` sem `position` explícito numa página com 7 elementos existentes resultou em substituição total — só ficaram os elementos do template (`elements_added` igual à contagem própria do template). A descrição sugere inserção ("at a given position, inserting its elements"); o comportamento observado foi sobrescrita. **Sequência obrigatória:** `get-page-structure` antes → `apply-template(..., position: -1)` explícito → `get-page-structure` depois, comparar. Se o conteúdo foi apagado, recuperar via `rollback-change` na entrada mais recente de `list-changes` para esse `post_id` (ver skill `emcp-site-audit`) — confirmado a funcionar.
4. **`create-theme-template` fica publicado de imediato**, não como rascunho. Testado: `create-theme-template(type:"single", title:"...")` devolveu `status: "publish"` com `conditions.include: [{"object":"all-singular"}]` por omissão — com conteúdo vazio, aplicar-se-ia de imediato a todo o singular do site. Chamar logo `set-template-conditions` para restringir o alcance antes de construir conteúdo, ou `delete-theme-template` de imediato se criado por engano (confirmado limpo/reversível).
5. **`get-element-settings` normaliza os wrappers `$$type` atómicos; `get-page-structure` e `export-page` não.** Para o mesmo heading atómico, `get-element-settings` devolve `{"title": "string simples"}` enquanto os outros dois mostram a estrutura crua `{"$$type":"html-v3","value":{"content":{"$$type":"string","value":"..."}}}`. Usar `get-element-settings` para "qual é o valor agora"; usar `get-page-structure`/`export-page` para entender ou reproduzir a estrutura crua de prop-types atómicos.
6. **`set-post-terms` com `create_missing:true` deixa termos de taxonomia permanentes** — sem tool de eliminação de termos no conjunto activo em nenhum site testado até agora. Confirmar o nome do termo antes de usar em conteúdo descartável/teste.
## Construir uma página de raiz — sequência recomendada
1. `create-page({ title, status: "draft" })` — nunca construir directamente numa página publicada.
2. `add-flexbox({ post_id, settings: { flex_direction, gap } })` (ou `add-div-block`) para o primeiro container estrutural. `add-container` só se precisares especificamente de comportamento legacy ou nesting sob uma árvore legacy existente.
3. Aninhar widgets atómicos: `add-atomic-heading`/`add-atomic-paragraph`/`add-atomic-button`/`add-atomic-image`/`add-atomic-svg`/`add-atomic-video`/`add-atomic-youtube`/`add-atomic-divider`, ou o genérico `add-atomic-widget` para o que não tenha tool de conveniência.
4. Para qualquer widget legacy necessário (Pro/terceiros ainda sem versão atómica, `add-pro-widget` se o deny-list do site permitir): `add-container` depois `add-free-widget`.
5. Verificar com `get-page-structure` após cada passo relevante, não só no fim.
6. `reorder-elements`, `move-element`, `duplicate-element`, `remove-element` operam por ID de elemento das chamadas de structure/find-element — nunca adivinhar IDs.
7. `set-element-label({ post_id, element_id, title })` para manter o Navigator legível em páginas complexas (campo `title`, ver armadilha #1).
8. `update-page-settings` para definições de página ao nível Elementor (background, padding, CSS custom — funcionalidades Pro degradam de forma graciosa se indisponíveis).
9. Antes de publicar: `get-page-snapshot` para um único digest normalizado (estrutura + contagens + cores/tipografia globais em uso + resumo SEO-lite) em vez de encadear chamadas separadas de structure/global-settings/classes.
## Tokens de design globais
- `get-global-settings` — kit de todo o site (cores, tipografia, espaçamento, breakpoints).
- `update-global-colors`/`update-global-typography` — muta o kit directamente; afecta todas as páginas que usam esses tokens, confirmar com o utilizador primeiro.
- `list-global-classes` — resolve IDs `g-` do Class Manager do Elementor 4.0 para nomes/CSS legíveis. Normalmente só-leitura; `create-global-class`/`update-global-class`/`delete-global-class`/`reorder-global-classes` estão comummente no deny-list (mutação de CSS partilhado) — verificar se estão montadas antes de assumir que funcionam.
## Templates
- `list-templates` (templates guardados na biblioteca Elementor, qualquer tipo: page/section/container) e `save-as-template` para criar novos. **Não há tool de eliminação de template no conjunto comummente activo em nenhum site testado** — guardar um template é efectivamente permanente via MCP. Obter confirmação explícita antes de `save-as-template`.
- `import-template`/`export-page` para estruturas JSON portáveis.
- Theme Builder (`create-theme-template`/`list-theme-templates`/`get-theme-template`/`update-theme-template`/`set-template-conditions`/`resolve-template`/`delete-theme-template`) — ver armadilha #4 acima. `list-condition-targets` antes de `set-template-conditions` para ver selectores/post-types/taxonomias válidos naquele site.
## Padrão de teste seguro
Nunca experimentar numa página publicada real. `create-page(status:"draft")`, construir/mutar livremente, confirmar via `get-page-structure`, depois `delete-post(post_id)` (vai para o Lixo — reversível; só `force:true` se o utilizador pedir explicitamente remoção permanente). Verificar `list-changes` depois — toda a escrita aqui fica registada no ledger, por isso um teste falhado é sempre recuperável com `rollback-change` mesmo antes de chegares a apagar o rascunho.
## Skills relacionadas
- `emcp-content-ops` — operações de conteúdo WordPress (posts/pages/media/menus/redirects/blocos).
- `emcp-site-audit` — scan de segurança, análise de performance, acesso só-leitura a BD/filesystem, ledger de mudanças.
- `elementor-pro-widgets` — catálogo completo de widgets nativos Elementor Pro.
- `elementskit-widgets` / `powerpack-widgets` — catálogos de widgets de terceiros (não estão no catálogo curado do EMCP).
- `emcp-tools` — arquitectura do plugin, inventário completo de ~162 abilities, mecanismo de deny-list.
+58
View File
@@ -0,0 +1,58 @@
---
name: emcp-site-audit
description: Auditoria de segurança, performance, base de dados e filesystem de um site WordPress via qualquer ligação MCP EMCP Tools, e recuperação de uma edição de IA falhada via o ledger de mudanças/rollback. Usar quando "scan-security mcp", "analyze-performance mcp", "rollback-change", "ledger de mudanças elementor", "falso positivo malware uploads", "query base de dados via mcp".
layer: wiki
---
# /emcp-site-audit — Auditoria e Rede de Segurança via EMCP Tools
Aplica-se a qualquer ligação MCP EMCP Tools. Confirma o site alvo com `core/get-site-info`.
## Scan de segurança
`scan-security` — só-leitura, autocontido, audita apenas o site ligado. Quatro áreas: heurísticas de malware PHP (uploads + plugins/temas activos; `deep:true` para a árvore completa), integridade de ficheiros do core WordPress (vs checksums oficiais), hardening de configuração (editor de ficheiros activo, output de debug, username admin, XML-RPC, divulgação de versão, HTTPS, security headers), e software desactualizado/abandonado. Devolve `{summary: {score, grade, counts: {critical, warning, pass, info}}, sections: {...}}`.
**Tratar um achado `critical` como um reporte imediato e separado ao utilizador — nunca enterrado dentro de uma tarefa não relacionada.** Mas **`malware_uploads_php` (qualquer `.php` sob `wp-content/uploads/`, categoria `critical`) é uma heurística grosseira que assinala só a localização, não o conteúdo — tem um padrão confirmado e comum de falso positivo. Ler sempre o ficheiro assinalado com `read-file` e inspeccionar o conteúdo real antes de reportar como ameaça real.**
Confirmado benigno num scan ao vivo (dois hits `critical`, mesmo id de achado, ambos falsos positivos):
- `wp-content/uploads/index.php` — stub próprio do **core WordPress**, incluído por omissão em toda a instalação: `header($_SERVER['SERVER_PROTOCOL'].' 403 Forbidden'); die('403 Forbidden');`. Existe só para bloquear listagem de directório. **Nunca apagar este** — removê-lo *reduz* a segurança ao remover exactamente a protecção que fornece.
- `wp-content/uploads/<plugin>/cache/index.php` (visto: WPForms) — mesmo padrão, incluído pelo plugin, ex. `header(...' 404 Not Found');`. Qualquer plugin que faça cache em `uploads/` tipicamente inclui um destes por directoria de cache.
Regra de diagnóstico: um ficheiro `uploads/*.php` genuinamente malicioso contém lógica executável real (`eval`, `base64_decode`, `system`/`exec`/`shell_exec`, payloads ofuscados, marcadores de webshell) — um stub benigno tem 2-4 linhas, só `header()` + `die()`/`exit` opcional, nada mais. Se `read-file` mostrar só um stub de header/die, despromover o achado para informativo no reporte e **não** apagar o ficheiro. Se mostrar outra coisa — ameaça real, reportar de imediato e confirmar com o utilizador antes de apagar (o caminho de remoção confirmado seguro é `delete-file` se montada para a ligação, ou `wp eval`/`rm` via SSH quando não estiver — ver secção Filesystem abaixo).
Achados `integrity_modified` (ficheiro do core difere dos checksums oficiais) também podem ser rotina — ex. `wp-includes/version.php` difere por desenho (é o próprio ficheiro de versão instalada do WordPress, espera-se que difira de uma baseline de checksum genérica). Cruzar o que mudou (`read-file` no ficheiro assinalado) antes de tratar um achado de integridade como indicador de comprometimento.
## Análise de performance
`analyze-performance` — só-leitura. Analisa configuração do servidor, internals WordPress (tamanho da BD, opções autoloaded, backlog de cron, object cache, OPcache, contagem de plugins), e uma página alvo (por omissão a página inicial; passar `url` ou `post_id` para uma página específica). Devolve a mesma forma `{summary: {score, grade, counts}, sections}` que `scan-security`, com `status` por achado (`pass`/`warning`/`critical`), `message`, e `recommendation` — uma lista de acções priorizada já pronta, não reportar só o score.
## Base de dados (nível só-leitura)
`list-tables` (contagens de linhas + tamanhos), `describe-table({ table })` (colunas/tipos/keys), `query({ sql })` — **só SELECT/SHOW/DESCRIBE/EXPLAIN, escritas e DDL são rejeitadas no servidor.** Resultados têm limite — para conjuntos grandes, restringir a query em vez de esperar paginação. O prefixo das tabelas varia por instalação (visto ao vivo: `wpne_` num site) — sempre `list-tables` primeiro em vez de assumir `wp_`.
`insert-row`/`update-rows`/`delete-rows` existem no plugin mas estão comummente no deny-list em todos os sites de produção testados até agora — escritas SQL directas contornam a camada de validação do próprio WordPress. Não tentar sem confirmar primeiro que estão realmente montadas para essa ligação.
## Filesystem (nível só-leitura)
`list-directory`/`read-file`/`search-files`, confinados à raiz da instalação WordPress. `read-file` recusa ficheiros de segredos no servidor — confirmado ao vivo: pedir `wp-config.php` devolve *"This file holds site secrets ... and cannot be read"* em vez de um erro ou do conteúdo, independentemente da variante de caminho tentada. `search-files({ query, extensions, path })` faz grep file:line numa árvore de directórios, com limite — bom para confirmar versão de plugin/tema ou um padrão de código específico sem SSH.
`write-file`/`edit-file`/`delete-file` existem no plugin mas estão quase universalmente no deny-list (escritas directas no filesystem contornam toda a auditoria de shell/SSH) — tratar como indisponível salvo confirmação explícita de que está montada. Quando um ficheiro confirmadamente malicioso precisa de ser removido e `delete-file` não está montada, usar o canal SSH/wp-cli próprio do site em vez de forçar via MCP.
## Ledger de mudanças + rollback — a rede de segurança para toda escrita Elementor/conteúdo
Toda a chamada mutante que este plugin expõe (edições Elementor, e nalguns sites também escritas de filesystem/BD) fica registada. Testado ao vivo, ponta-a-ponta, e funciona:
1. `list-changes` (opcional filtrar por `domain`: elementor/filesystem/database, `rolled_back`, `reversible`) — mais recente primeiro, cada entrada tem `id`, `summary`, `reversible`, `rolled_back`.
2. `get-change({ id })` para detalhe completo incluindo a referência de rollback (before-image / ponteiro de backup / `after_hash`).
3. `rollback-change({ id })` — reverte essa mudança. Confirmado ao vivo: reverteu uma página exactamente para a árvore de elementos anterior, removendo precisamente o que aquela mudança tinha adicionado e nada das mudanças à volta. Recusa com erro de conflito se o alvo mudou de novo desde a mudança registada (passar `force:true` para sobrepor e aceitar perda de dados no estado mais recente). Nunca faz double-rollback da mesma entrada; regista uma entrada compensatória no ledger quando corre.
**Quando qualquer escrita nesta sessão correr mal ou produzir um resultado inesperado (ver a armadilha de sobrescrita do `apply-template` em `emcp-page-building`), recorrer a `list-changes` → `rollback-change` antes de tentar reconstruir manualmente o estado anterior.** É mais rápido e fiável do que reconstruir a partir do output de `get-page-structure`.
## Espelho de conteúdo (export/restore git-friendly — separado do ledger de mudanças)
`export-content`/`list-content-exports`/`restore-content` — exporta conteúdo de página/template Elementor para ficheiros JSON sob `uploads/emcp-content-mirror/` para um VCS externo poder fazer diff/versionar designs, e restaura a partir de um ficheiro de espelho (um undo baseado em ficheiro, distinto do ledger acima). `list-content-exports` mostra o que está actualmente espelhado em disco. Útil antes de um lote grande de trabalho de construção de páginas como checkpoint extra além do ledger automático.
## Referência cruzada
- `emcp-page-building` — construção de páginas Elementor, incluindo as tools com maior probabilidade de precisar de rollback (`apply-template`, `create-theme-template`).
- `emcp-content-ops` — operações de conteúdo/media/menus/redirects WordPress.
- `emcp-tools` (skill genérica) — inventário completo de ~162 abilities e o mecanismo de deny-list que decide quais das tools referidas acima estão realmente montadas numa dada ligação.
+561
View File
@@ -0,0 +1,561 @@
---
name: emcp-tools
description: Auditoria e gestão de postura de segurança do EMCP Tools (extensão do WP MCP Adapter da Elementskit — expõe dados Elementor, widgets e ferramentas de operação como MCP tools para agentes de IA). Cobre inventário completo das ~162 abilities registadas por categoria (activas vs desligadas, verificado via wp_get_abilities()), os 9 módulos do plugin (prompts, brand-kits, templates, themer, redirects, agent-skills, cloud, image-optimization, svg-support) e o que cada um expõe, o mecanismo de deny-list incremental versionado (34 migrações), activação granular e segura de uma ferramenta individual (ex: só leitura Rank Math), avaliação de risco por categoria, e arquitectura de registo MCP multi-site. Usar quando "emcp tools", "emcp-tools", "mcp adapter wordpress", "ferramentas mcp elementor", "que tools o agente pode chamar", "activar tool emcp", "desligar tool mcp", "postura default-deny mcp", "agentes de ia no wordpress", "módulos emcp tools", "inventário completo mcp tools", "quantas tools estão activas".
---
# /emcp-tools — Auditoria e Postura de Segurança do EMCP Tools (MCP para Elementor/WP)
Plugin `emcp-tools` (Mian Shahzad Raza) — extensão do WP MCP Adapter que expõe
dados Elementor, widgets, page design tools e um conjunto amplo de
operações WordPress como **ferramentas MCP** chamáveis por agentes de IA.
Versão verificada `3.12.1`, instalado e activo em `emanuelalmeida.pt`,
`descomplicar.pt` (`public_html`) e `starter.descomplicar.pt`.
**Fonte:** `Hub/04-Stack/02.04-Sistemas/71.Seguranca/CONFIG-Plugins-Referencia.md`
§10 (EMCP Tools) + `BUNDLE-Excelencia-WP.md` + verificação SSH ao vivo nesta
sessão (16-08-2026) nos três sites do ecossistema.
---
## Arquitectura — como o agente chega às tools deste plugin
O plugin regista um servidor MCP próprio, `emcp-tools-server` (nome visível
"MCP Tools for Elementor Server"), lado a lado com outros servidores do
mesmo WP MCP Adapter (`elementskit-mcp-server`, `mcp-adapter-default-server`,
e nalguns sites `fluent-crm`). A ligação do agente ao site faz-se por SSH +
STDIO, uma entrada por site em `~/.omp/agent/mcp.json`:
```json
"emcp-emanuelalmeida": {
"type": "stdio",
"command": "ssh",
"args": ["-T", "-o", "BatchMode=yes", "server",
"sudo -u ealmeida /usr/local/bin/wp mcp-adapter serve --server=emcp-tools-server --user=ealmeida --path=/home/ealmeida/emanuelalmeida.pt"],
"timeout": 60000,
"enabled": true
}
```
**Três instâncias activas no ecossistema**, uma ligação MCP por site (cada
uma com o seu próprio `--path` e, portanto, a sua própria config
`emcp_tools_*` independente — **não presumir que a postura de um site se
aplica a outro**, ver §3):
| Ligação em `mcp.json` | Site | `--path` |
|---|---|---|
| `emcp-descomplicar` | descomplicar.pt | `/home/ealmeida/public_html` |
| `emcp-tools` | starter.descomplicar.pt | `/home/ealmeida/starter.descomplicar.pt` |
| `emcp-emanuelalmeida` | emanuelalmeida.pt | `/home/ealmeida/emanuelalmeida.pt` |
Comando nativo WP-CLI para inspecção do lado do servidor (sem passar pelo
protocolo MCP):
```bash
wp mcp-adapter list --path=$PATH --allow-root
wp mcp-adapter list --format=json --path=$PATH --allow-root
```
⚠️ **Achado desta sessão — a coluna `Tools` de `wp mcp-adapter list` NÃO é
um proxy fiável do nº de ferramentas realmente activas.** Verificado ao
vivo nos 3 sites (16-08-2026):
| Site | `emcp_tools_disabled_tools` (desligadas) | `wp mcp-adapter list` → coluna `Tools` do `emcp-tools-server` | Activas REAIS (`wp_get_abilities()`, §7) |
|---|---|---|---|
| descomplicar.pt | 78 | `3` | **165** (achado crítico — §8.2) |
| starter.descomplicar.pt | 131 | `3` | 114 |
| emanuelalmeida.pt | 131 | `115` | 115 |
Não há correlação directa (menos desligadas em `descomplicar.pt` devia
implicar mais activas, mas mostra o valor mais baixo). A contagem desta
coluna parece depender de estado de registo/cache do lado do WP-CLI, não do
filtro `emcp_tools_disabled_tools` aplicado em tempo de pedido MCP. **Não
usar esta coluna para avaliar postura de segurança** — usar sempre o
conteúdo real de `emcp_tools_disabled_tools` (§2) para saber o que está
fechado, e o protocolo MCP (`tools/list` via um cliente MCP ligado, ex. a
própria sessão do agente) para saber com exactidão o que está disponível
para chamada num momento dado. A 4ª coluna acima é a contagem definitiva,
obtida com o método de §7 (`wp eval` + `wp_get_abilities()` filtrado, cruzado
slug-a-slug contra `emcp_tools_disabled_tools`) — coincide exactamente com a
coluna `Tools` só em `emanuelalmeida.pt`; nos outros dois sites a coluna do
WP-CLI é enganadora nos dois sentidos (mostra `3` quando o real é 114 **e**
165).
---
## 1. Config verificada (`emanuelalmeida.pt`, `wp_options`)
```bash
wp option list --search='emcp_tools*' --path=$PATH --allow-root
```
| Opção | Valor real | Nota |
|---|---|---|
| `emcp_tools_sandbox_location` | `emcp-sandbox` | Directório isolado para operações de sandbox |
| `emcp_tools_active_modules` | `["prompts","brand-kits","templates","themer","redirects","agent-skills","cloud"]` (7 de 9 semeados) | `image-optimization` e `svg-support` estão **semeados mas não activos** — idêntico nos 3 sites verificados |
| `emcp_tools_disabled_tools` | array indexado com **131** ferramentas desligadas (formato `emcp-tools/<nome-tool>`) | Ver lista completa §2 |
| `emcp_tools_defaults_applied` | `34` | Contador interno de defaults aplicados na instalação |
| `emcp_tools_modules_seeded` / `emcp_tools_legacy_meta_migrated` / `emcp_tools_slug_namespace_migrated` | flags internas de migração | Sem acção |
| `emcp_tools_changelog_db_version` / `emcp_tools_oauth_db_version` / `emcp_tools_redirects_db_version` / `emcp_tools_search_index_db_version` | versões de schema interno | Sem acção |
| `emcp_tools_themer_index` / `emcp_tools_themer_index_healed` | índice interno do módulo Themer | Sem acção |
Outros dois sites (`descomplicar.pt`, `starter.descomplicar.pt`) partilham
o mesmo `emcp_tools_active_modules`; `starter.descomplicar.pt` partilha
exactamente os mesmos 131 nomes desligados que `emanuelalmeida.pt`;
`descomplicar.pt` tem apenas 78 desligados (config mais permissiva —
**verificar antes de assumir postura igual entre sites**). Note-se também
que `templates` e `agent-skills` são módulos **Pro** (`tier() === 'pro'`)
semeados como "activos" em todos os 3 sites por omissão do próprio plugin,
mas nenhum destes sites tem licença Pro (`emcp_tools_fs()->can_use_premium_code()
=== false`), logo `is_available()` devolve `false` e **nenhum dos dois
módulos corre de facto** — ver tabela completa dos 9 módulos em §6, e o
achado crítico específico de `descomplicar.pt` em §8.2 (78 desligados não
significa "mais permissivo apenas um pouco" — significa **zero** tools
nativas fechadas).
---
## 2. Lista completa das 131 ferramentas desligadas por omissão
Confirmada ao vivo (`emanuelalmeida.pt` e `starter.descomplicar.pt`,
formato idêntico): array indexado (não associativo) em
`emcp_tools_disabled_tools`, cada entrada `emcp-tools/<slug-da-tool>`.
```bash
wp option get emcp_tools_disabled_tools --format=json --path=$PATH --allow-root
```
### 2.1 Ficheiros, sistema e execução remota — risco máximo, nunca activar sem supervisão directa
```
write-file, edit-file, delete-file, run-wp-cli, dispatch-wp-cli,
get-wp-cli-job, list-wp-cli-jobs
```
### 2.2 Base de dados directa
```
insert-row, update-rows, delete-rows
```
### 2.3 Gestão de plugins e temas
```
install-plugin, activate-plugin, deactivate-plugin, update-plugin, delete-plugin,
install-theme, switch-theme, update-theme, delete-theme
```
### 2.4 Utilizadores
```
create-user, update-user
```
### 2.5 Redirecções e migração de site
```
create-redirect, update-redirect, delete-redirect,
migrate-site, sync-to-live, sync-content-item
```
### 2.6 Integrações de terceiros — leitura e escrita (granularidade por par read/write)
```
acf-write
woo-read, woo-write
metabox-write
essential-addons-read, premium-addons-read, uae-read, uae-write
cf7-write
wpforms-read, wpforms-write
gravityforms-read, gravityforms-write
fluentforms-read, fluentforms-write
ninjaforms-read, ninjaforms-write
formidable-read, formidable-write
metform-read, metform-write
sureforms-read, sureforms-write
forminator-read, forminator-write
```
### 2.7 SEO — TODOS os plugins, leitura E escrita fechadas (Rank Math dono único da superfície SEO neste bundle)
```
slimseo-write
yoast-read, yoast-write
rankmath-read, rankmath-write
aioseo-read, aioseo-write
seopress-read, seopress-write
seoframework-read, seoframework-write
surerank-read, surerank-write
```
### 2.8 Temas/frameworks de blocos
```
theme-write
astra-write
spectra-write
kadence-write, kadence-blocks-write
generatepress-read, generatepress-write
generateblocks-read, generateblocks-write
blocksy-blocks-read, blocksy-blocks-write, blocksy-extensions-read, blocksy-extensions-write
```
### 2.9 Elementor avançado (widgets Pro, templates de tema, popups, dynamic tags, CSS/classes globais)
```
add-pro-widget, create-elementor-theme-template, set-elementor-template-conditions,
list-dynamic-tags, set-dynamic-tag, create-popup, set-popup-settings,
delete-media, add-custom-css,
create-global-class, update-global-class, delete-global-class, reorder-global-classes
```
### 2.10 Snippets PHP — execução de código arbitrário, risco máximo absoluto
```
add-code-snippet, list-code-snippets,
validate-php-snippet, create-php-snippet, update-php-snippet, get-php-snippet,
list-php-snippets, delete-php-snippet
```
### 2.11 Memória do agente
```
recall, remember, save-session-summary
```
### 2.12 Templates PHP de tema
```
create-theme-php-template, list-theme-php-templates, get-theme-php-template,
update-theme-php-template, delete-theme-php-template
```
### 2.13 SEO/A11y automatizados (geração de conteúdo/schema/contraste)
```
audit-page-seo, extract-keywords-from-content, generate-meta-tags,
generate-schema-markup, set-social-image,
audit-page-a11y, fix-color-contrast, add-alt-text-from-context
```
### 2.14 Widgets e blocos custom (criação/edição/eliminação)
```
list-control-types, validate-widget-spec,
create-custom-widget, update-custom-widget, get-custom-widget, list-custom-widgets,
set-widget-status, delete-custom-widget,
list-block-control-types, validate-block-spec,
create-custom-block, update-custom-block, get-custom-block, list-custom-blocks,
set-block-status, delete-custom-block
```
---
## 3. Como listar ferramentas activas vs desligadas num site
```bash
PATH=/home/USER/SITE
# Quantas e quais estão desligadas
wp option get emcp_tools_disabled_tools --format=json --path=$PATH --allow-root
# Módulos activos (nível mais alto — prompts/brand-kits/templates/themer/redirects/agent-skills/cloud/image-optimization/svg-support)
wp option get emcp_tools_active_modules --format=json --path=$PATH --allow-root
# Servidores MCP registados neste site (não usar a coluna Tools como medida de activo — ver aviso acima)
wp mcp-adapter list --path=$PATH --allow-root
```
**Para saber com exactidão quais tools estão disponíveis para chamada num
dado momento** (a lista real, não a config): ligar via o protocolo MCP
(a própria ligação `emcp-*` em `mcp.json`) e pedir `tools/list` — a config
`emcp_tools_disabled_tools` é aplicada como filtro em tempo de pedido pelo
plugin, não é reflectida de forma fiável pelo WP-CLI.
---
## 4. Activar uma ferramenta específica com segurança (granularidade individual)
O array é **indexado** (não associativo), por isso `wp option patch` não
serve para remover/adicionar uma entrada isolada — usar `wp eval` com
`array_diff`/`array_values` para manter o array limpo e sem duplicados.
### Activar (remover do deny-list)
```bash
wp eval '
$tool = "emcp-tools/rankmath-read";
$disabled = get_option("emcp_tools_disabled_tools", []);
$before = count($disabled);
$disabled = array_values(array_diff($disabled, [$tool]));
update_option("emcp_tools_disabled_tools", $disabled);
printf("antes=%d depois=%d removida=%s\n", $before, count($disabled), $before !== count($disabled) ? "sim" : "nao (ja estava activa ou nome errado)");
' --allow-root --path=$PATH
```
**Exemplo do pedido típico — activar só leitura Rank Math sem abrir
escrita:** activar `emcp-tools/rankmath-read` e deixar
`emcp-tools/rankmath-write` no array (continua desligada). A granularidade
é sempre ao nível do slug individual — nunca existe um toggle "Rank Math
on/off" only por read/write, exactamente como aparece na lista §2.7.
### Reverter (voltar a fechar)
```bash
wp eval '
$tool = "emcp-tools/rankmath-read";
$disabled = get_option("emcp_tools_disabled_tools", []);
if (!in_array($tool, $disabled, true)) { $disabled[] = $tool; }
update_option("emcp_tools_disabled_tools", $disabled);
echo count($disabled) . " tools desligadas\n";
' --allow-root --path=$PATH
```
### Verificar sempre depois, de forma independente
```bash
wp option get emcp_tools_disabled_tools --format=json --path=$PATH --allow-root | grep -o 'rankmath-read' || echo "confirmado: activa (nao esta no deny-list)"
```
---
## 5. Avaliação de risco por categoria — o que activar e o que nunca activar sem supervisão
| Categoria | Postura recomendada | Justificação |
|---|---|---|
| Leitura de design Elementor (o que fica activo por omissão — não está no deny-list) | ✅ Seguro por omissão | Propósito central do plugin; só leitura/inspecção, sem mutação |
| SEO leitura isolada (ex.: só `rankmath-read`) | ✅ Seguro activar caso a caso | Não muta nada; útil para um agente auditar sem escrever — mas Rank Math continua a ser gerido preferencialmente via `/rank-math` (WP-CLI directo), não via MCP |
| SEO escrita (qualquer `*-write` de SEO) | 🔴 Nunca activar sem supervisão | Contorna o fluxo de auditoria/rollback do `/rank-math`; risco de meta/schema incoerente sem revisão humana |
| Redirecções/migração (`create-redirect`, `migrate-site`, `sync-to-live`) | 🔴 Nunca activar sem supervisão | Mutação estrutural com impacto em produção e SEO (redirects mal configurados = perda de ranking) |
| Gestão de plugins/temas (`install-plugin`, `activate-plugin`, …) | 🔴 Nunca activar sem supervisão | Um agente autónomo instalar/remover plugins é a superfície mais perigosa depois de execução de código |
| Snippets PHP (`create-php-snippet`, `validate-php-snippet`, `update-php-snippet`) | 🔴🔴 Nunca activar — risco máximo absoluto | **Execução de PHP arbitrário** persistido no site; equivalente a dar shell ao agente. Único candidato a "nunca, em circunstância nenhuma, sem revisão humana linha a linha antes de guardar" |
| Ficheiros/sistema (`write-file`, `edit-file`, `delete-file`, `run-wp-cli`, `dispatch-wp-cli`) | 🔴🔴 Nunca activar — risco máximo | Escrita directa no filesystem e execução WP-CLI arbitrária via MCP; contorna toda a auditoria de shell/SSH existente |
| Base de dados directa (`insert-row`, `update-rows`, `delete-rows`) | 🔴🔴 Nunca activar — risco máximo | SQL de escrita sem passar por validação da camada WP; risco de corrupção silenciosa |
| Widgets/blocos custom (`create-custom-widget`, `create-custom-block`, …) | 🟡 Só com supervisão activa | Cria código PHP/JS executado no frontend; menos grave que snippets PHP mas ainda superfície de execução |
| Utilizadores (`create-user`, `update-user`) | 🔴 Nunca activar sem supervisão | Escalada de privilégios / criação de contas fora do fluxo humano |
| Memória do agente (`recall`, `remember`, `save-session-summary`) | 🟡 Avaliar por site | Sem risco de segurança directo, mas cria estado persistente fora do controlo humano — activar só se o workflow do site precisar de continuidade entre sessões MCP |
| Integrações de terceiros — leitura (`woo-read`, `wpforms-read`, …) | 🟡 Seguro activar caso a caso, um a um | Só expõe dados existentes; activar apenas as integrações realmente instaladas no site, nunca em bloco |
| Integrações de terceiros — escrita (`woo-write`, `cf7-write`, …) | 🔴 Nunca activar sem supervisão | Mutação em dados de negócio (encomendas, submissões de formulário) sem revisão humana |
**Regra geral (revista após leitura completa do código-fonte, §6-§8):** a
**concepção** do plugin é default-deny bem desenhada — o deny-list incremental
descrito em §8.1 fecha metodicamente cada nova categoria de risco assim que é
lançada. Mas a **postura real, verificada ao vivo, NÃO é uniforme entre os 3
sites**: `emanuelalmeida.pt` e `starter.descomplicar.pt` reflectem essa
concepção (115/162 e 114/160 tools activas, respectivamente, com todas as
categorias 🔴/🔴🔴 de facto fechadas); `descomplicar.pt` diverge de forma
crítica — **165/165 tools registadas estão activas**, incluindo filesystem,
base de dados directa, WP-CLI, PHP snippets e gestão de plugins/utilizadores
(ver achado detalhado em §8.2). Qualquer activação deve ser: (1) ao nível de
uma única tool, nunca em bloco; (2) reversível e verificada de imediato (§4,
com o método correcto de §7/§8.3 — nunca por contagem de arrays); (3)
documentada aqui ou em nota do site, para não perder rasto de porque é que
uma excepção existe.
---
## 6. Módulos — os 9 blocos que a página *Modules* liga/desliga
Módulo ≠ ferramenta individual: é um interruptor de alto nível (`emcp_tools_active_modules`)
que liga/desliga uma funcionalidade inteira (CPT, hooks de front-end, tab de admin e, nalguns
casos, o próprio grupo de MCP tools). Cada módulo estende `EMCP_Tools_Module`
(`includes/modules/class-module.php`) e expõe `tier()` (`free`/`pro`), `default_active()`
(semeado uma única vez, via `EMCP_Tools_Modules_Registry::apply_defaults()`, marcador
`emcp_tools_modules_seeded`) e `is_available()` (gate adicional, ex. licença Pro).
**Um módulo `pro` pode estar "activo" no option e mesmo assim não fazer nada** — `is_available()`
exige `emcp_tools_fs()->can_use_premium_code()`; nos 3 sites verificados (build Free, sem licença),
`templates` e `agent-skills` estão semeados activos mas `is_available()` devolve `false`, logo não
correm.
| Módulo (`id`) | Tier | Activo por omissão | Disponível nesta sessão (build Free, sem licença) | O que liga quando activo+disponível |
|---|---|---|---|---|
| `prompts` | free | sim | sim | Mostra a tab de admin **Prompts** (blueprints de prompts prontos a usar para construir páginas; amostras grátis + biblioteca Pro). Não regista MCP tools — é só UI de admin (`register()` vazio). |
| `brand-kits` | free | sim | sim | Mostra a tab **Brand Kits** (kits de cor+tipografia de um clique; kits grátis + biblioteca Pro). Também só UI de admin, sem MCP tools próprias. |
| `cloud` | free | sim | sim | Arranca `EMCP_Tools_Cloud_Connect::init()` (fluxo OAuth de ligação à conta EMCP Cloud) e — só quando `EMCP_Tools_Cloud::is_connected()` for verdadeiro — regista as 7 ferramentas `cloud-*` (§5 tabela de categorias). Nos 3 sites: módulo activo mas nenhum ligado a uma conta Cloud, logo 0 tools `cloud-*` registadas. |
| `templates` | **pro** | sim (seeded) | **não** (sem licença) | Quando disponível: mostra a tab **Templates** (templates de página Elementor prontos a aplicar). Metadata-only (`register()` vazio) — vive na árvore free só como descrição, a lógica Pro real está fora deste build. |
| `themer` | free | sim | sim | O módulo mais substancial do bundle free: regista o CPT `EMCP_Tools_Themer_CPT`, o índice de condições, o controlador de render no front-end, a metabox de admin, blocos Gutenberg dinâmicos, widgets Elementor dinâmicos e templates PHP (Themer PHP, sub-toggle próprio). Desligar este módulo é kill-switch total — pára o CPT, o front-end takeover, a tab, **e** o registrador de abilities omite as 8 tools `*-theme-template*`/`*-template*` (§5). |
| `redirects` | free | sim | sim | Instala a tabela do Redirect Manager, liga o handler de 301/302 no front-end, o scan de links partidos e as sugestões de redirect ao apagar/renomear uma página. Desligar pára tudo isto **e** remove as 5 tools `*-redirect*`/`find-broken-links` do registo. |
| `agent-skills` | **pro** | sim (seeded) | **não** (sem licença) | Quando disponível: expõe `list-skills`/`get-skill` aos agentes ligados + injecta um catálogo `## Skills` (~900 tokens) no contexto de discovery. Metadata-only na árvore free. |
| `image-optimization` | free | **não** (opt-in) | depende (precisa de editor de imagem com suporte WebP) | Comprime uploads (`wp_generate_attachment_metadata`) e gera/serve WebP (rewriter de URL); regista `resize-media`. 5 opções próprias: `compress`, `webp`, `webp_serve`, `quality` (0-100, default 60), `max_dimension`, `keep_originals`. Um optimizador retomável trata a biblioteca já existente a partir do card do módulo. |
| `svg-support` | free | **não** (opt-in — superfície de segurança) | depende (precisa da lib `enshrined/svg-sanitize` bundled) | Permite upload de SVG (mime `svg`) para quem tiver a capability necessária, corrige `wp_check_filetype_and_ext()` para REST/sideload, e sanitiza cada SVG (fail-closed: um SVG que não possa ser limpo é rejeitado). Uma opção: `admin_only` (restringe upload a administradores). Nota: se o Elementor já permitir SVG (como normalmente acontece), este módulo é redundante excepto pela sanitização extra. |
---
## 7. Inventário verificado das ~162 abilities registadas (build Free, Elementor activo) — por categoria
**Correcção a uma hipótese de partida:** subtrair ingenuamente 162 − 131 = "~31 activas" está
**errado** — 84 dos 131 slugs em `emcp_tools_disabled_tools` são nomes de ferramentas **Pro**
(WooCommerce, 6 SEO plugins além do Slim SEO, 5 dos 6 plugins de formulários, frameworks de
tema além do Astra, memória do agente, Themer PHP, SEO/A11y automático, Widget/Block Builder —
ver lista completa em §2) que **nunca chegam a registar-se** neste build Free — estão pré-semeados
no option para o dia em que o site tiver licença Pro, mas `wp_get_abilities()` não os devolve porque
as classes Pro não existem na árvore free. Contá-los como "desligados" infla artificialmente a
contagem de fechadas e desinfla a de activas.
**Método de verificação usada** (a única fonte fiável — ver aviso sobre `wp mcp-adapter list` no
topo deste documento):
```bash
wp eval '
$abilities = wp_get_abilities();
$emcp = array_filter(array_keys($abilities), function($k){ return strpos($k, "emcp-tools/") === 0; });
$disabled = get_option("emcp_tools_disabled_tools", []);
$active = array_diff($emcp, $disabled);
echo "registadas=" . count($emcp) . " desligadas(option)=" . count($disabled) . " activas=" . count($active) . "\n";
' --path=$PATH --allow-root
```
**Resultado real, verificado ao vivo (16-08-2026):**
| Site | Abilities `emcp-tools/*` registadas | Entradas em `emcp_tools_disabled_tools` | … das quais correspondem a uma ability REALMENTE registada | Activas (registada E não desligada) |
|---|---|---|---|---|
| `emanuelalmeida.pt` | 162 | 131 | 47 | **115** |
| `starter.descomplicar.pt` | 160 | 131 | ~46 | **114** |
| `descomplicar.pt` | 165 | 78 | **0** | **165** (ver achado crítico, §8) |
A tabela abaixo usa `emanuelalmeida.pt` (162 registadas) como referência — é o site com a
config mais representativa da postura default-deny pretendida pelo plugin (`starter` é quase
idêntico; `descomplicar.pt` diverge de forma crítica, ver §8). Por categoria: ferramentas
**ACTIVAS** (chamáveis agora), **DESLIGADAS** (bloqueadas pelo deny-list, mas registadas — activáveis
via §4), e, quando aplicável, ferramentas do grupo que **nem chegam a registar-se** neste site
(dependem de um plugin/tema de terceiros ausente).
| Categoria | O que faz | Activas | Desligadas |
|---|---|---|---|
| **Compact tool mode (dispatcher)** | `list-tools`/`get-tool-schema`/`call-tool` — o modo de despacho compacto (3 ferramentas que expõem indirectamente todas as outras); descrito na arquitectura do servidor MCP no início deste documento. | `call-tool`, `get-tool-schema`, `list-tools` | — |
| **Elementor — descoberta/query (P0)** | Fundação de leitura: percorre containers/elementos, resolve schemas de widget/container, lista páginas Elementor e templates disponíveis. | `find-element`, `get-container-schema`, `get-element-settings`, `get-page-structure`, `get-widget-schema`, `list-pages`, `list-templates`, `list-widgets` | — |
| **Elementor — páginas (P1 CRUD)** | Cria/edita páginas Elementor, exporta o conteúdo de uma página, importa um template para uma página, e ajusta page settings (SEO básico, layout). | `create-page`, `delete-page-content`, `export-page`, `import-template`, `update-page-settings` | — |
| **Elementor — layout/container** | Manipula a árvore de containers/flexbox/elementos: adicionar, mover, duplicar, remover, reordenar, actualizar propriedades, edição em lote (`batch-update`). | `add-container`, `batch-update`, `duplicate-element`, `move-element`, `remove-element`, `reorder-elements`, `set-element-label`, `update-container`, `update-element` | — |
| **Elementor — widgets clássicos (catálogo)** | Coloca widgets Elementor gratuitos no layout e actualiza as suas definições; `add-pro-widget` (Elementor Pro) fica desligado por omissão. | `add-free-widget`, `update-widget` | `add-pro-widget` |
| **Elementor — templates/popups/dynamic tags** | `apply-template`/`save-as-template` activos (aplicar/gravar templates de página); criação de theme templates, popups, dynamic tags e condições de exibição ficam desligados (mutação estrutural). | `apply-template`, `save-as-template` | `create-elementor-theme-template`, `create-popup`, `list-dynamic-tags`, `set-dynamic-tag`, `set-elementor-template-conditions`, `set-popup-settings` |
| **Elementor — configurações globais (cores/tipografia)** | Lê e actualiza o kit global (cores e tipografia do site) — `get-global-settings` está agrupado na categoria Query (P0) mas afecta as mesmas definições. | `get-global-settings`, `update-global-colors`, `update-global-typography` | — |
| **Elementor — build-page composto** | `build-page` — ferramenta de alto nível que constrói uma página inteira a partir de uma especificação estruturada (composição de containers+widgets num único pedido). | `build-page` | — |
| **Elementor — SVG icons** | `upload-svg-icon` — envia um SVG para a biblioteca de ícones Elementor (sanitizado). | `upload-svg-icon` | — |
| **Elementor — custom code (CSS/JS/snippets)** | `add-custom-js` activo (injecta JS Elementor); `add-custom-css`, `add-code-snippet` e `list-code-snippets` ficam desligados (execução/injecção persistente). | `add-custom-js` | `add-code-snippet`, `add-custom-css`, `list-code-snippets` |
| **Elementor 4.0+ — atomic widgets** | Widgets atómicos do Elementor 4.0+ (heading, paragraph, button, image, svg, youtube, video, divider) e o genérico `add-atomic-widget`/`update-atomic-widget` — todos activos. | `add-atomic-button`, `add-atomic-divider`, `add-atomic-heading`, `add-atomic-image`, `add-atomic-paragraph`, `add-atomic-svg`, `add-atomic-video`, `add-atomic-widget`, `add-atomic-youtube`, `update-atomic-widget` | — |
| **Elementor 4.0+ — atomic layout (flexbox/div-block)** | `add-div-block`, `add-flexbox` (containers atómicos) e `detect-elementor-version` (detecta se o site suporta Elementor 4.0+ atomic). | `add-div-block`, `add-flexbox`, `detect-elementor-version` | — |
| **Elementor 4.0+ — global classes (leitura)** | `list-global-classes` — lista as classes CSS globais do Elementor 4.0+. | `list-global-classes` | — |
| **Elementor 4.0+ — global classes (escrita)** | Criar/actualizar/eliminar/reordenar classes globais — todas desligadas por omissão (mutação de CSS partilhado entre todas as páginas). | — | `create-global-class`, `delete-global-class`, `reorder-global-classes`, `update-global-class` |
| **Gutenberg (blocos nativos)** | Conjunto completo de leitura+escrita para blocos Gutenberg nativos (descoberta de blocos/patterns, inserir pattern, CRUD de blocos numa página) — todo activo por omissão (Gutenberg é considerado superfície de baixo risco face ao Elementor neste bundle). | `add-block`, `duplicate-block`, `get-block-schema`, `get-post-blocks`, `insert-pattern`, `list-blocks`, `list-patterns`, `move-block`, `remove-block`, `update-block` | — |
| **Page Snapshot** | `get-page-snapshot` — digest normalizado de uma página (base para outras ferramentas de leitura); sempre activo. | `get-page-snapshot` | — |
| **Transações/rollback (change ledger)** | `get-change`/`list-changes`/`rollback-change` — ledger de alterações feitas via MCP com possibilidade de reverter; sempre activo (é a rede de segurança das escritas). | `get-change`, `list-changes`, `rollback-change` | — |
| **Pesquisa de conteúdo (índice léxico)** | `search-content`/`reindex-search` — índice léxico sobre páginas/templates/widgets/globals. | `reindex-search`, `search-content` | — |
| **Content mirror (export/restore git-friendly)** | `export-content`/`list-content-exports`/`restore-content` — exporta conteúdo de página como ficheiros rastreáveis em git e permite restaurar. | `export-content`, `list-content-exports`, `restore-content` | — |
| **WordPress — conteúdo (posts/CPT/taxonomias)** | CRUD completo de posts/CPT (criar, ler, listar, actualizar, eliminar), listar post types/taxonomias, definir termos — activo por omissão. | `create-post`, `delete-post`, `get-post`, `list-post-types`, `list-posts`, `list-taxonomies`, `set-post-terms`, `update-post` | — |
| **WordPress — media library** | Ler/listar/actualizar/carregar media activos; `delete-media` desligado (eliminação permanente). | `get-media`, `list-media`, `update-media`, `upload-media` | `delete-media` |
| **WordPress — resize de imagem** | `resize-media` só regista quando o módulo Image Optimization está activo (reutiliza o motor de compressão/WebP) — não registado nesta sessão porque o módulo está desligado por omissão. | *(nenhuma — grupo não registado: `resize-media`)* | *(n/a — ver nota)* |
| **WordPress — settings** | `get-settings`/`update-settings` — leitura e escrita de um conjunto curado de definições do site (não é `wp_options` livre). | `get-settings`, `update-settings` | — |
| **WordPress — plugins** | Listar/pesquisar plugins fica activo; instalar/activar/desactivar/actualizar/eliminar plugin ficam desligados (superfície de maior risco depois de execução de código). | `list-plugins`, `search-plugins` | `activate-plugin`, `deactivate-plugin`, `delete-plugin`, `install-plugin`, `update-plugin` |
| **WordPress — temas (gestão)** | Listar/pesquisar temas activo; instalar/mudar/actualizar/eliminar tema desligados. | `list-themes`, `search-themes` | `delete-theme`, `install-theme`, `switch-theme`, `update-theme` |
| **WordPress — utilizadores** | Ler/listar utilizadores activo; criar/actualizar utilizador desligados (escalada de privilégios). | `get-user`, `list-users` | `create-user`, `update-user` |
| **WordPress — menus de navegação** | `menu-read`/`menu-write` — ambos activos por omissão (menus, itens, theme locations, render). | `menu-read`, `menu-write` | — |
| **Tema activo (framework-agnostic)** | `theme-read` activo (`get-theme-context`, `get-mods` — identidade do tema, theme_mods); `theme-write` desligado (`set-mods`, `create-child-theme`). | `theme-read` | `theme-write` |
| **Astra (tema)** | Só regista quando o Astra é o tema activo. `astra-read` activo (definições Astra Customizer); `astra-write` desligado. | `astra-read` | `astra-write` |
| **ACF (Advanced Custom Fields)** | Dispatcher `acf-read`/`acf-write` + 15 operações (field groups, post types, taxonomias, options pages) — só regista se ACF (free ou Pro) estiver activo no site; não registado nesta sessão (ACF ausente). | *(nenhuma — grupo não registado: `acf-read`, `acf-write`, `create-acf-field-group`, `create-acf-post-type`, `create-acf-taxonomy`, `get-acf-field-group`, `get-acf-fields`, `get-acf-post-type`, `get-acf-taxonomy`, `list-acf-field-groups`, `list-acf-options-pages`, `list-acf-post-types`, `list-acf-taxonomies`, `update-acf-field-group`, `update-acf-fields`, `update-acf-post-type`, `update-acf-taxonomy`)* | *(n/a — ver nota)* |
| **Meta Box** | Dispatcher `metabox-read`/`metabox-write` — só regista se o Meta Box estiver activo; não registado nesta sessão. | *(nenhuma — grupo não registado: `metabox-read`, `metabox-write`)* | *(n/a — ver nota)* |
| **Themer (free) — templates de header/footer/single/archive** | CPT + condições de exibição para header/footer/single/archive/search/404 com qualquer page builder. Todo o grupo activo por omissão: listar/obter/criar/actualizar/eliminar template, listar alvos de condição, definir condições, resolver qual template se aplica a um contexto. | `create-theme-template`, `delete-theme-template`, `get-theme-template`, `list-condition-targets`, `list-theme-templates`, `resolve-template`, `set-template-conditions`, `update-theme-template` | — |
| **Themer PHP templates (toggle próprio)** | Sub-funcionalidade do Themer para templates PHP brutos, atrás de um toggle independente (`EMCP_Tools_Themer_PHP::enabled()`) — não activo nesta sessão, por isso as 5 tools não estão sequer registadas. | *(nenhuma — grupo não registado: `create-theme-php-template`, `delete-theme-php-template`, `get-theme-php-template`, `list-theme-php-templates`, `update-theme-php-template`)* | *(n/a — ver nota)* |
| **Redirect Manager** | `list-redirects`/`find-broken-links` activos (leitura + scan de links partidos); `create/update/delete-redirect` desligados (mutação de routing com impacto directo em SEO). | `find-broken-links`, `list-redirects` | `create-redirect`, `delete-redirect`, `update-redirect` |
| **Stock images (busca + sideload + widget)** | `search-images`/`sideload-image`/`add-stock-image` — pesquisa num provider de stock, descarrega para a Media Library, e coloca como widget de imagem; todo activo (não muta nada fora da Media Library). | `add-stock-image`, `search-images`, `sideload-image` | — |
| **Performance Analyzer** | `analyze-performance` — auditoria de performance só de leitura. | `analyze-performance` | — |
| **Security & Malware Scanner** | `scan-security` — scanner de malware/postura de segurança, só leitura, sempre activo. | `scan-security` | — |
| **Filesystem** | `list-directory`/`read-file`/`search-files` activos; `write-file`/`edit-file`/`delete-file` desligados (escrita directa no filesystem). | `list-directory`, `read-file`, `search-files` | `delete-file`, `edit-file`, `write-file` |
| **Base de dados directa** | `describe-table`/`list-tables`/`query` (SELECT) activos; `insert-row`/`update-rows`/`delete-rows` desligados (escrita SQL directa). | `describe-table`, `list-tables`, `query` | `delete-rows`, `insert-row`, `update-rows` |
| **WP-CLI (run + jobs)** | `run-wp-cli`/`dispatch-wp-cli`/`get-wp-cli-job`/`list-wp-cli-jobs` — todo o grupo desligado por omissão (execução arbitrária de comandos WP-CLI). | — | `dispatch-wp-cli`, `get-wp-cli-job`, `list-wp-cli-jobs`, `run-wp-cli` |
| **PHP Snippets (Sandbox)** | `validate/create/update/get/list/delete-php-snippet` — todo o grupo desligado por omissão (execução de PHP arbitrário persistido). | — | `create-php-snippet`, `delete-php-snippet`, `get-php-snippet`, `list-php-snippets`, `update-php-snippet`, `validate-php-snippet` |
| **Sandbox — export/import cloud (bundle)** | `export-sandbox-artifact`/`import-sandbox-artifact` — exporta/importa um artefacto de sandbox (bloco/widget/snippet) como bundle; ambos activos (o bundle em si não executa nada, é só serialização). | `export-sandbox-artifact`, `import-sandbox-artifact` | — |
| **EMCP Cloud (sync remoto)** | `cloud-status`/`cloud-backup`/`cloud-list`/`cloud-pull`/`cloud-config-sync`/`cloud-marketplace-list`/`cloud-marketplace-install` — só registam quando o módulo Cloud está activo E o site está ligado a uma conta EMCP Cloud; não registadas nesta sessão porque nenhum dos 3 sites está ligado a uma conta Cloud (ver módulo `cloud`, §6). | *(nenhuma — grupo não registado: `cloud-backup`, `cloud-config-sync`, `cloud-list`, `cloud-marketplace-install`, `cloud-marketplace-list`, `cloud-pull`, `cloud-status`)* | *(n/a — ver nota)* |
---
## 8. Mecanismo do deny-list incremental — e um achado crítico ao vivo em `descomplicar.pt`
### 8.1 Como o plugin decide o que fecha por omissão
O deny-list não é estático: `includes/admin/class-admin.php`, método
`maybe_apply_default_disabled_tools()` (chamado no boot do admin), aplica **34 blocos de migração
incremental** (`DEFAULTS_VERSION = 34`, contador `emcp_tools_defaults_applied`), cada um
introduzido numa versão do plugin para fechar uma nova categoria de tool assim que ela é lançada
— ex.: v6 fecha escrita de plugins/temas, v9 fecha filesystem, v10 fecha base de dados, v18 fecha
WP-CLI, v28 fecha escrita de global classes, v33-34 fecham as tools de migração/sync. A função só
adiciona slugs (nunca remove uma escolha do utilizador) e só corre os blocos cujo número é maior
que o `$applied` já gravado:
```php
if ( $applied < N ) { $add = array_merge( $add, /* slugs da versão N */ ); }
// ...
update_option( self::OPTION_DISABLED_TOOLS, array_values( array_unique( array_merge( $existing, $add ) ) ) );
update_option( self::OPTION_DEFAULTS_APPLIED, (string) self::DEFAULTS_VERSION );
```
Consequência arquitectural importante: **um site cujo `emcp_tools_defaults_applied` já esteja em
34 nunca mais recebe fecho automático de tools novas**, mesmo que o plugin lance uma v35 no
futuro — nesse cenário a nova categoria nasceria **activa** nesse site até alguém a fechar à mão. E
mais relevante ainda para o presente: se o valor de `emcp_tools_disabled_tools` for alterado
manualmente (via `wp eval`/`array_diff`, como o método §4 deste documento ensina, ou por qualquer
outro caminho) **removendo** entradas em massa, o contador de versão não volta atrás — o site fica
com um deny-list mais pequeno do que o "de fábrica" para essa versão, sem qualquer aviso.
### 8.2 🔴 Achado crítico verificado ao vivo (16-08-2026) — `descomplicar.pt`
Ao aplicar o método de verificação do §7 aos 3 sites, `descomplicar.pt` destacou-se: `active=165`
= `registered=165` — **zero ferramentas efectivamente fechadas**, apesar de
`emcp_tools_disabled_tools` ter 78 entradas. Inspeccionando essas 78 entradas
(`wp option get emcp_tools_disabled_tools --format=json --path=/home/ealmeida/public_html`):
são **exclusivamente** slugs de integrações de terceiros que não estão instaladas neste site
(WooCommerce, Essential/Premium Addons, UAE, os 6 plugins de formulários, os 7 plugins de SEO,
Astra/Spectra/Kadence/GeneratePress/Blocksy, `recall`/`remember`/`save-session-summary`, os 4
achados SEO/A11y automáticos, e os 16 slugs do Widget/Block Builder Pro) — **nenhuma** das
categorias nativas mais perigosas (filesystem, base de dados, WP-CLI, PHP snippets, gestão de
plugins/temas, gestão de utilizadores) aparece nessa lista. Confirmado directamente, ferramenta a
ferramenta:
| Tool | Registada em `descomplicar.pt`? | Está no deny-list? | **Chamável agora via MCP?** |
|---|---|---|---|
| `emcp-tools/write-file` | sim | não | **🔴 SIM** |
| `emcp-tools/edit-file` | sim | não | **🔴 SIM** |
| `emcp-tools/delete-file` | sim | não | **🔴 SIM** |
| `emcp-tools/run-wp-cli` | sim | não | **🔴 SIM** |
| `emcp-tools/dispatch-wp-cli` | sim | não | **🔴 SIM** |
| `emcp-tools/insert-row` | sim | não | **🔴 SIM** |
| `emcp-tools/update-rows` | sim | não | **🔴 SIM** |
| `emcp-tools/delete-rows` | sim | não | **🔴 SIM** |
| `emcp-tools/create-php-snippet` | sim | não | **🔴 SIM** |
| `emcp-tools/update-php-snippet` | sim | não | **🔴 SIM** |
| `emcp-tools/install-plugin` | sim | não | **🔴 SIM** |
| `emcp-tools/activate-plugin` | sim | não | **🔴 SIM** |
| `emcp-tools/create-user` | sim | não | **🔴 SIM** |
| `emcp-tools/update-user` | sim | não | **🔴 SIM** |
Todas as 34 versões de migração já correram (`emcp_tools_defaults_applied = 34`, igual aos outros
2 sites), o que exclui "instalação nunca migrada" como explicação — a hipótese mais provável, dado
o padrão exacto de remoção (sobrevivem só integrações de terceiros; desaparecem só as categorias
nativas de risco máximo/alto de §5), é que o array foi reescrito manualmente nalgum momento — por
exemplo com uma variante do próprio comando de §4 aplicada de forma demasiado ampla (ex.:
`array_diff` contra uma lista de slugs "de terceiros" em vez de um único slug) — sem que a
subtracção tenha sido revertida depois.
**Isto não foi corrigido nesta tarefa** (o âmbito desta expansão de skill é só leitura — ver
constraints). **Requer decisão humana urgente**: ou (a) confirmar que a abertura destas 14+ tools
nativas em `descomplicar.pt` foi intencional e está devidamente supervisionada, ou (b) alinhar
`descomplicar.pt` com o deny-list dos outros 2 sites, repondo pelo menos as categorias 🔴🔴 de §5
(filesystem, base de dados, WP-CLI, snippets PHP) com o padrão do método §4, aplicado entrada a
entrada.
### 8.3 Regra de verificação a aplicar sempre, daqui em diante
**Nunca confiar na contagem de entradas em `emcp_tools_disabled_tools` isoladamente** — nem para
avaliar "quão fechado" está um site (v. `descomplicar.pt`, 78 entradas e 0 tools realmente
fechadas), nem para o inverso (a lista de 131 em `emanuelalmeida.pt`/`starter` também tem 84
entradas "mortas" que nunca vão fechar nada neste build). O único método correcto é o `wp eval` +
`wp_get_abilities()` do §7, comparado slug a slug contra `emcp_tools_disabled_tools` — nunca o
tamanho dos arrays, nunca a coluna `Tools` de `wp mcp-adapter list` (ver aviso na arquitectura, que
mostra `3` para `descomplicar.pt` quando o valor real é 165).
---
## Fonte
`Hub/04-Stack/02.04-Sistemas/71.Seguranca/CONFIG-Plugins-Referencia.md` §10
(EMCP Tools, mapeado 16-08-2026) + `BUNDLE-Excelencia-WP.md` (contexto do
bundle e registo dos 3 sites) + verificação SSH ao vivo nesta tarefa
(`wp option list/get`, `wp plugin get`, `wp mcp-adapter list`,
`~/.omp/agent/mcp.json`) em `descomplicar.pt`, `starter.descomplicar.pt` e
`emanuelalmeida.pt` **+ leitura completa do código-fonte do plugin
(build Free `emcp-tools` 3.12.1) em `emanuelalmeida.pt`**: todos os ficheiros
em `includes/abilities/` (46 classes de abilities + integrações
forms/seo), `includes/modules/` (9 módulos + sub-módulos Image Optimization
e SVG Support), `includes/admin/class-admin.php` (catálogo de tools,
`maybe_apply_default_disabled_tools()`, as 34 versões do deny-list
incremental), `includes/modules/class-modules-registry.php` +
`class-module.php` (ciclo de vida dos módulos), `includes/class-migration.php`
(migração `elementor-mcp` → `emcp-tools`), `emcp-tools.php` (bootstrap,
guarda Free⇄Pro) — e verificação ao vivo via `wp eval` + `wp_get_abilities()`
filtrado a `emcp-tools/*` cruzado slug-a-slug contra
`emcp_tools_disabled_tools`, nos 3 sites (método completo em §7).
+221
View File
@@ -0,0 +1,221 @@
---
name: fluent-crm
description: Conhecimento profundo do FluentCRM (WPManageNinja, plugin `fluent-crm` + addon `fluentcampaign-pro`) — arquitectura, schema de BD (`fc_*`), API de desenvolvimento oficial (`FluentCrmApi()`), modelo de estados de contacto e as suas guardas reais, wp-admin (Subscribers/Tags/Lists/Companies/Campaigns/Automations), automações (Funnels, triggers/actions), Dynamic Segments (Pro), e o estado real em descomplicar.pt (291k contactos). Usar quando "fluentcrm", "email marketing wordpress", "automação de contactos", "funnels fluentcrm", "segmentos dinâmicos", "status de subscritor", "unsubscribe", "empresas crm wordpress", "campanha email estatísticas", ou qualquer trabalho de configuração/estratégia FluentCRM que não seja apenas chamar o MCP. Para execução programática (ler/escrever contactos, tags, listas, empresas, notas, campanhas), usar a skill `mcp-fluent-crm`.
layer: wiki
---
# /fluent-crm — FluentCRM (WPManageNinja) — conhecimento de plugin
Plugin `fluent-crm` (Free, `FLUENTCRM_PLUGIN_VERSION` 3.1.10) + addon comercial
`fluentcampaign-pro` (2.9.86, "FluentCRM Pro"), instalados e activos em **descomplicar.pt**
(produção). Mapeamento feito por leitura completa do código-fonte
(`wp-content/plugins/fluent-crm/app/` e `wp-content/plugins/fluentcampaign-pro/app/`) via SSH,
19-08-2026, antes de construir o MCP `mcp-fluent-crm`.
**Não confundir com FluentBooking** (`fluent-booking`/`fluent-booking-pro`, mesmo fabricante
WPManageNinja, também activo em descomplicar.pt) — é um produto de agendamento separado, sem
relação com contactos/email marketing além de estar na mesma "família Fluent".
---
## 0. As duas camadas — Free vs Pro
| | Free (`fluent-crm`) | Pro (`fluentcampaign-pro`) |
|---|---|---|
| Contactos, tags, listas, empresas | ✅ core | — |
| Campanhas de email | ✅ core | — |
| Automações (Funnels) — motor + triggers/actions base | ✅ core (`FluentFormSubmissionTrigger`, `UserRegistrationTrigger`, apply/detach tag/list/company, send email, wait) | Acrescenta dezenas de triggers/actions por integração (ver §5) |
| Dynamic Segments | — | ✅ (`WooCustomerSegment`, `EddActiveCustomerSegment`, `PMProMembersSegment`, `AffiliateWPSegment`, `WpUserSegment`, `CustomSegment`, `WooSubscriptionActiveSegment`) |
| Abandon Cart | — | ✅ (WooCommerce) |
| Integrações de terceiros (WooCommerce, EDD, SureCart, LearnDash, LifterLMS, TutorLMS, MemberPress, RCP, PMPro, AffiliateWP, WishlistMember, Voxel) | — | ✅ |
**Achado real (descomplicar.pt, 19-08-2026):** nenhuma das plataformas-alvo das integrações Pro
(WooCommerce, EDD, LearnDash, LifterLMS, TutorLMS, MemberPress, RCP, PMPro, AffiliateWP,
WishlistMember) está instalada no site — só **FluentForm** está activo. Logo, das dezenas de
triggers/actions que o Pro acrescenta, **só os de FluentForm têm alvo real hoje**; o resto do
addon Pro está dormente (código carregado, sem integração activável). Reavaliar se algum destes
plugins for instalado no futuro.
---
## 1. Onde vivem os dados
### 1.1 `wp_options` (config do plugin)
| Opção | Conteúdo |
|---|---|
| `_fluentcrm_db_version` | versão do schema de BD instalado |
| `fluentcrm-global-settings` | array serializado: `campaign.from.{name,email}`, `email.emails_per_second` |
| `fluentcrm_is_sending_emails` | flag de lock durante envio de campanha |
| `_fluentcrm_lock_minute_scheduler` / `_five_minute_scheduler` / `_hourly_scheduler` | locks dos cron schedulers internos |
### 1.2 Tabelas `fc_*` (prefixo do site + `fc_`, ex. `wpah_fc_subscribers` em descomplicar.pt)
Confirmado por `DESCRIBE`/`SHOW TABLES` em descomplicar.pt (19-08-2026):
| Tabela | Papel |
|---|---|
| `fc_subscribers` | contactos — `email` UNIQUE, `status`, `contact_type` (`lead`/`customer`), `company_id`, `hash`, `total_points`, `life_time_value`, geo (`ip`/`latitude`/`longitude`/`country`/...) |
| `fc_subscriber_meta` | custom fields por contacto |
| `fc_subscriber_pivot` | associações contacto↔tag/lista — `object_type` = FQCN literal (`FluentCrm\App\Models\Tag` ou `FluentCrm\App\Models\Lists`), `object_id` |
| `fc_subscriber_notes` | notas por contacto — `type` normal é `note`; `_company_note_`/`_system_log_` são tipos internos excluídos por omissão da UI (global scope no modelo) |
| `fc_tags` / `fc_lists` | taxonomias reais de segmentação (81 tags / 9 lists em descomplicar.pt) |
| `fc_terms` / `fc_term_relations` | **tabelas vazias em descomplicar.pt (0 linhas)** — schema preparado para uma futura unificação de taxonomias que ainda não está em uso; não confiar nelas, usar `fc_tags`/`fc_lists`/`fc_subscriber_pivot` |
| `fc_companies` | empresas (CRM B2B) — `name`, `industry`, `owner_id` (contacto responsável), `meta` (JSON com `custom_values`) |
| `fc_campaigns` | campanhas de email — `type='campaign'`, `status` (`draft`/`published`/`paused`/`archived`), `email_body`, `recipients_count` |
| `fc_campaign_emails` | um registo por (campanha × destinatário) — `is_open`, `click_counter`, `status` (`sent`/...) — é aqui que vivem as estatísticas reais de abertura/clique |
| `fc_campaign_url_metrics` | cliques agregados por URL dentro de uma campanha |
| `fc_funnels` / `fc_funnel_sequences` / `fc_funnel_subscribers` / `fc_funnel_metrics` | automações (ver §5) |
| `fc_event_tracking` | eventos custom rastreados via `event_tracker` API (§2) |
| `fc_smart_links` | links rastreáveis reutilizáveis (Pro) |
| `fc_sequence_tracker` | progresso de contactos dentro de sequências de email (automações/campanhas em sequência) |
| `fc_url_stores` | URLs longas por trás de smart links/tracking |
**Gotcha real confirmado (descomplicar.pt):** existem linhas em `fc_subscriber_pivot` com
`object_type` literalmente diferente do FQCN correcto (deteção via `SELECT object_type, COUNT(*)
GROUP BY object_type` — 107 linhas com um padrão de escaping incorrecto vs 325k+342k linhas
correctas). São dados órfãos de alguma migração/import antiga, invisíveis à própria UI do
FluentCRM (que também filtra por FQCN exacto) — não é bug do MCP nem motivo de alarme, só não
contar com essas 107 linhas em auditorias de integridade.
---
## 2. API de desenvolvimento oficial — `FluentCrmApi()`
**Único caminho seguro para escritas programáticas.** Nunca fazer `INSERT`/`UPDATE` directo nas
tabelas `fc_*` — perde-se guardas de estado, `do_action()` hooks (usados por automações/
integrações de terceiros) e invalidação de cache.
```php
FluentCrmApi('contacts') // Contacts — createOrUpdate, getContact, getContactByUserRef, query()
FluentCrmApi('tags') // Tags — importBulk (cria/actualiza por slug)
FluentCrmApi('lists') // Lists — importBulk (idem)
FluentCrmApi('companies') // Companies — createOrUpdate, attachContactsByIds, detachContactsByIds
FluentCrmApi('event_tracker') // Tracker — track($eventData, $isUnique)
FluentCrmApi('extender') // ponto de extensão para integrações de terceiros
```
Fonte: `app/Api/Classes/*.php`, registadas em `app/Api/config.php`. Cada classe expõe também os
métodos `all`/`get`/`find`/`first`/`paginate` do modelo Eloquent-like subjacente via `__call()`.
Métodos de instância directos no modelo `Subscriber` (não passam pela fábrica `FluentCrmApi()`,
chamam-se sobre um objecto `Subscriber` já resolvido): `attachTags($ids)`, `detachTags($ids)`,
`attachLists($ids)`, `detachLists($ids)`, `attachCompanies($ids)`, `detachCompanies($ids)`,
`updateStatus($status)` (mudança directa, sem guardas — ver §3).
---
## 3. Modelo de estados de contacto — e a guarda real contra escalada indevida
```php
fluentcrm_subscriber_statuses() // ['subscribed','pending','unsubscribed','transactional','bounced','complained','spammed']
fluentcrm_strict_statues() // ['unsubscribed','bounced','complained','spammed'] — "suprimidos"
```
`Contacts::createOrUpdate($data, $forceUpdate=false, ...)` → `Subscriber::updateOrCreate()`
implementa uma guarda documentada no próprio código-fonte (comentário explícito no ficheiro,
descrevendo a correcção de uma vulnerabilidade real): **sem `$forceUpdate=true` explícito, um
payload nunca rebaixa um contacto `subscribed` nem ressuscita um contacto suprimido** —
protege contra webhooks/migradores/formulários que resSuscitassem silenciosamente contactos que
se desinscreveram ou tiveram bounce/complaint. Mover **para** um estado suprimido a partir de
qualquer estado é sempre permitido (direcção "fail-safe").
`Subscriber::updateStatus($status)` é o método directo, **sem essas guardas** — dispara sempre
`do_action('fluent_crm/subscriber_status_changed', ...)` e `do_action('fluentcrm_subscriber_status_to_' . $status, ...)`.
É a via correcta quando a intenção de mudar o status é deliberada e já confirmada pelo chamador
(ex. um admin a reactivar manualmente um contacto).
**Implicação prática:** qualquer automação/import que crie/actualize contactos em massa deve usar
`createOrUpdate()` sem `forceUpdate`, salvo decisão explícita e documentada de contornar a guarda.
---
## 4. wp-admin — páginas principais
| Página | Conteúdo |
|---|---|
| **FluentCRM → Subscribers** | lista/pesquisa/filtra contactos, vista de detalhe com tags/listas/empresa/notas/timeline de actividade |
| **FluentCRM → Contact Companies** | CRM B2B — empresas, dono (owner_id), contactos associados (Pro-visível, dados core) |
| **FluentCRM → Email Campaigns** | criar/agendar/enviar campanhas, relatório de abertura/clique/bounce por campanha |
| **FluentCRM → Automations** | construtor visual de Funnels (trigger → sequência de actions/benchmarks/condições) |
| **FluentCRM → Forms** (se FluentForm activo) | mapeamento formulário↔lista/tag na submissão |
| **FluentCRM → Reports** | dashboards agregados (crescimento de lista, performance de campanhas) |
| **FluentCRM → Settings** | remetente por omissão, limite de emails/segundo, dupla-opt-in, campos custom |
| **FluentCRM → Dynamic Segments** (Pro) | segmentos calculados em tempo real a partir de dados externos (§6) |
---
## 5. Automações (Funnels) — arquitectura
Motor em `app/Services/Funnel/` (`FunnelProcessor.php`, `BaseTrigger.php`, `BaseAction.php`,
`BaseBenchMark.php`, `SequencePoints.php`). Um Funnel = 1 trigger + sequência ordenada de
actions/wait-steps/condições, persistida em `fc_funnels` (definição) +
`fc_funnel_sequences` (passos) + `fc_funnel_subscribers` (progresso por contacto) +
`fc_funnel_metrics` (contadores agregados).
**Triggers/Actions core (Free), confirmados no código:**
- Triggers: `FluentFormSubmissionTrigger`, `FluentFormSubscriptionCancelledTrigger`,
`FluentFormSubscriptionPaymentReceivedTrigger`, `UserRegistrationTrigger`.
- Actions: `ApplyTagAction`/`DetachTagAction`, `ApplyListAction`/`DetachListAction`,
`ApplyCompanyAction`/`DetachCompanyAction`, `SendEmailAction`, `WaitTimeAction`.
**Triggers/Actions internas do CRM (Pro, mas sem dependência de plugin externo — sempre
disponíveis com o addon activo):** `ContactCreatedTrigger`, `TagAppliedTrigger`/
`RemoveFromTagTrigger`, `ListAppliedTrigger`/`RemoveFromListTrigger`, `CompanyAppliedTrigger`/
`RemoveFromCompanyTrigger` (`app/Services/Integrations/CRM/`) — permitem encadear automações a
partir de mudanças de tag/lista/empresa feitas por outra automação ou pelo MCP.
**Triggers/Actions Pro dependentes de plugin externo (dormentes em descomplicar.pt — nenhum dos
plugins-alvo está instalado):** WooCommerce (order success/refund/complete, subscription
renew/start/cancel/expire, add order note, create coupon), EDD (payment success/refund, license
expired, recurring payment), SureCart, RCP, PMPro, AffiliateWP, LearnDash/LifterLMS/TutorLMS
(course/lesson/topic completed, enroll, group), MemberPress, WishlistMember, Voxel.
O MCP `mcp-fluent-crm` **não gere Funnels** (fora do v1, deliberado — sequências JSON complexas,
alto risco de erro de configuração). Gerir Funnels via wp-admin.
---
## 6. Dynamic Segments (Pro)
`app/Services/DynamicSegments/` — segmentos calculados em tempo real (não persistidos como lista
estática), 7 tipos confirmados no código: `CustomSegment` (query builder livre), `WpUserSegment`
(por role/meta WP), `WooCustomerSegment`, `WooSubscriptionActiveSegment`, `EddActiveCustomerSegment`,
`PMProMembersSegment`, `AffiliateWPSegment`. Os 5 últimos dependem do plugin-alvo estar instalado
(nenhum está, hoje). Não coberto pelo MCP — gerir via wp-admin.
---
## 7. Estado real em descomplicar.pt (verificado 19-08-2026)
- **291 441 contactos**: 81 881 `subscribed`, 209 559 `unsubscribed`, 1 `transactional`.
- **291 428 `lead`, 13 `customer`** (`contact_type`).
- **81 tags, 9 lists, 0 empresas** (tabela `fc_companies` vazia — CRM B2B nunca usado neste site).
- **37 campanhas** (31 `archived`/enviadas, 2 `draft`, 2 `paused`, 2 `published`).
- Nenhuma licença Pro visível em `cfLicenseKey`-equivalente verificada nesta sessão — addon
`fluentcampaign-pro` estava **instalado mas inactivo** até ser activado nesta mesma sessão via
`emcp-tools/activate-plugin` (não requer autorização de infra — só mutações de ficheiro por SSH
precisam do `infra-auth.sh`).
---
## 8. Gotchas
| Sintoma | Causa | Nota |
|---|---|---|
| `wp fluent_crm <cmd>` não tem `create-contact`/`add-tag`/etc. | O WP-CLI do plugin só cobre operações administrativas (`license_status`, `stats`, `sync_woo_customers`, `sync_edd_customers`, `reindex_wp_user_ids`, `reset_db`, `simulate_funnel`, `cli_send`) — nunca CRUD de contactos | Usar `FluentCrmApi()` (via MCP `mcp-fluent-crm`) ou o wp-admin |
| API REST (`/wp-json/fluent-crm/v2/...`) não responde/autentica | Precisa de uma API key gerada manualmente em **Settings → REST API** — não vinha configurada em nenhum site do bundle nesta verificação | Gerar a key no wp-admin se for necessário REST em vez de SSH; o MCP `mcp-fluent-crm` não depende disto |
| Query a `fc_terms`/`fc_term_relations` devolve sempre vazio | Tabelas preparadas para uma futura unificação de taxonomias, nunca populadas nesta versão | Usar sempre `fc_tags`/`fc_lists` + `fc_subscriber_pivot` |
| `object_type` em `fc_subscriber_pivot` não bate certo com o FQCN esperado numa pequena fracção de linhas | Dados órfãos de import/migração antiga (107 de ~650k linhas em descomplicar.pt) | Ignorar essas linhas — a própria UI do FluentCRM também as ignora |
| Contacto criado/actualizado via automação não muda de status apesar do payload trazer `status` | Guarda anti-escalada do `Contacts::createOrUpdate()` (§3) — comportamento correcto, não bug | Passar `forceUpdate=true` só com intenção explícita, ou usar `updateStatus()` directamente |
---
## Fonte
Leitura completa do código-fonte `fluent-crm` 3.1.10 + `fluentcampaign-pro` 2.9.86 via SSH em
descomplicar.pt (`app/Api/Classes/*.php`, `app/Models/Subscriber.php`, `app/Models/Company.php`,
`app/Models/SubscriberNote.php`, `app/Functions/helpers.php`, `app/Services/Funnel/*`,
`app/Services/Integrations/*`, `app/Services/DynamicSegments/*`), cruzada com `DESCRIBE`/
`SHOW TABLES`/agregações reais via SQL, 19-08-2026 — sessão de construção do MCP
`mcp-fluent-crm`. Ver essa skill para a lista completa de 20 tools programáticas.
+191
View File
@@ -0,0 +1,191 @@
---
name: full-cliente
description: Gestão do plugin FULL.Cliente (broker de licenças WordPress premium, api.full.services) via WP-CLI no servidor CWP. Cobre auditoria de ligação e licenças por site, ligar/desligar sites à conta FULL, reactivação de plugins premium (Elementor Pro, Elements Kit, Rank Math Pro, etc.), e diagnóstico de licenças que NÃO passam pela FULL (fabricante directo, CodeCanyon). Usar quando "full cliente", "full.services", "plugin premium sem licença", "elementor pro expired", "ligar site à full", "reactivar licença", "quantos sites têm licença full", "quem consome esta licença".
---
# /full-cliente — Broker de licenças WordPress premium
O plugin `full-customer` (nome visível "FULL.Cliente") está instalado em cada
site WP da frota. Substitui o updater nativo de cada plugin premium que gere
por um Plugin Update Checker apontado ao servidor da FULL, e serve como
corretor de licenças compradas pela Descomplicar® — uma conta partilhada
(`it@descomplicar.pt`) por toda a frota.
**Fonte:** código lido directamente em `wp-content/plugins/full-customer/`
(v4.1.3), servidor `server.descomplicar.pt` (SSH porta 9443). Namespace REST
local: `fc/v1`. API externa: `https://api.full.services/wp-json/fc-ai`.
---
## Regra de ouro: "geridos" ≠ "comprado"
Duas fontes dizem coisas diferentes, e as duas são reais:
| Endpoint | O que mostra | Quando usar |
|---|---|---|
| `plugin-repository/updates` | Plugins com o updater ACTUALMENTE substituído pela FULL neste site | "Este plugin está a receber updates pela FULL agora?" |
| `account/licenses-extract` | Ficha real de licenças da conta: comprado/disponível/domínios em uso, por produto | "Esta licença é da FULL?" — **fonte de verdade** |
**Nunca concluir "não é FULL" só pela ausência em `updates`.** Um produto pode
estar comprado e a consumir licença em vários domínios (`licenses-extract`) e
mesmo assim estar fora do catálogo activo (`plugin-repository/all`) — o mesmo
padrão do `elementor-pro` `expired`/"retirado dos geridos". Verificar sempre
as duas antes de classificar algo como "fora da FULL". (Erro cometido e
corrigido em sessão real: "Elements Kit" tinha 13 licenças compradas, 2
disponíveis, mas estava fora do catálogo activo — parecia "sem FULL" olhando
só para `updates`.)
---
## Consultas (leitura livre, sem GATE) — sempre via `wp eval-file` seguro
Aspas aninhadas partem o PHP através de ssh. Usar sempre base64 no stdin:
```bash
B64=$(printf '%s' '<?php
// codigo PHP aqui, so aspas simples PHP normais
' | base64 -w0)
timeout 200 ssh -p 9443 -o BatchMode=yes root@server.descomplicar.pt \
"P=/opt/alt/php-fpm82/usr/bin/php; W=/usr/local/bin/wp
echo $B64 | base64 -d | \$P -d memory_limit=512M -d error_reporting=0 \$W eval-file - --allow-root --path=/home/USER/SITE 2>&1"
```
### O site está ligado?
```php
<?php
global $wpdb;
$r = $wpdb->get_row("SELECT user_id, meta_value FROM {$wpdb->usermeta} WHERE meta_key='fc/connection-email' AND meta_value!='' LIMIT 1", ARRAY_A);
echo $r ? "ligado: {$r['meta_value']} (user {$r['user_id']})" : "NAO LIGADO";
```
Atalho sem PHP: `wp option get _full_customer-connection_email --allow-root --path=$S`
— só devolve valor se o site ainda não migrou (ver secção "Porque é que um
site pode não estar ligado").
### Extracto de licenças da conta (fonte de verdade)
```php
<?php
$x = fcDashboardAPI("GET", "account/licenses-extract");
$items = isset($x["data"]["name"]) ? [$x["data"]] : ($x["data"] ?? []);
foreach ($items as $v) {
echo $v["name"] . " | comprado=" . $v["purchased"] . " | disponivel=" . $v["available"]
. " | dominios=" . implode(", ", $v["domainsInUse"] ?? []) . "\n";
}
```
Requer o site já estar ligado (usa o token do site). Corre em qualquer site
ligado, não só na conta de referência.
Para contar sites únicos com licença activa: agregar `domainsInUse` de todos
os produtos, ignorar entradas `"Site excluído"`, desduplicar por domínio.
### Catálogo activo vs geridos neste site
```php
<?php
$catalogo = fcDashboardAPI("GET", "plugin-repository/all")["data"] ?? [];
$geridos = fcDashboardAPI("GET", "plugin-repository/updates")["data"] ?? [];
echo "catalogo activo: " . count($catalogo) . " | geridos neste site: " . count($geridos) . "\n";
```
---
## Ligar um site à conta FULL (grátis — não consome licença)
Mecanismo real (`Actions/ConnectAccount.php::handleConnection`): regista o par
(email, site) no servidor FULL via `account/connect`; só se aceite grava
`fc/connection-email` no usermeta do utilizador WP que executa a acção.
**Pré-requisitos:**
- GATE 5.1 — mutação em produção. Pedir autorização
(`~/.claude/hooks/infra-auth.sh server`, válida 60 min) antes de escrever.
- Um utilizador admin local (`manage_options`). Preferir o `it@descomplicar.pt`
local se existir (fica auditável).
```bash
B64=$(printf '%s' '<?php
$ca = new \FC\Actions\ConnectAccount();
$result = $ca->handleConnection("it@descomplicar.pt");
echo json_encode($result) . "\n";
echo "isConnected: " . (\FC\User::instance()->isConnected() ? "yes" : "no") . "\n";
' | base64 -w0)
# --user=<ID do admin> e OBRIGATORIO: sem ele User::instance() nao esta
# "logged in" e o setMeta() silencia sem gravar nada (ver Erros Comuns).
echo $B64 | base64 -d | php -d memory_limit=512M wp eval-file - --user=<ID> --allow-root --path=$S
```
**Verificar sempre de forma independente**, fora da sessão do eval:
`wp user meta get <ID> fc/connection-email --allow-root --path=$S`. Não
confiar só no `success:true` da resposta.
### Porque é que um site "v4.1.3" pode não estar ligado
`Services/Connection.php::autoConnection()` corre em cada `admin_init` e tenta
migrar sozinho: lê o option legado `_full_customer-connection_email`; se tiver
valor, liga e apaga o legado (`connectionMode = 'upgrade'`). **Se esse option
já estava vazio antes da actualização do plugin, nunca houve nada para
migrar** — o site nunca esteve ligado, nem na geração antiga. Confirmar com
`wp option get _full_customer-connection_email` (vazio = nunca ligado).
---
## Reactivar a licença de um plugin (CUSTA — consome licença e abre ticket)
**Nunca fazer sem autorização explícita.** Cada `reactivate` consome uma
licença disponível e abre um ticket Zendesk do lado da FULL, que fica em fila
— não é instantâneo.
Fluxo (`Actions/PluginReactivate.php`):
1. `plugin-repository/<slug>/reactivate` — consome a licença, abre ticket;
2. `plugin-repository/<slug>/license` — busca a chave;
3. `LocalLicenseProcessor::process($slug, $license, $step, $state)` — aplica
localmente (multi-step para alguns);
4. `plugin-repository/<slug>/license/confirm` + `do_action('fc/updates/invalidate')`.
**Processador por produto (`Services/LocalLicenseProcessor.php`) — nem todos
são one-shot:**
| Produto (slug) | Fluxo |
|---|---|
| `elementorPro` | **OAuth multi-step** com a Elementor: `fetch_consent` → `submit_consent` → `finalize`, cookies de sessão entre passos. Não dá por WP-CLI puro. |
| `essentialAddons` | OTP (`check_otp`) |
| `astra`, `rankMath`, `seoPress`, `perfmatters`, `acfPRO`, `wpRocket`, `jetThemeCore`, `ultimateAddons` | Uma chamada, sem steps |
---
## Não existe forma de libertar uma licença de um domínio
Procurado exaustivamente no código (`grep -rniE "release|remove.?domain|deactivate.?domain|revoke|free.?license"`)
— **zero mecanismo**. `Actions/DisconnectAccount.php` só desliga a ligação
LOCAL (`setConnectionEmail('')`); o próprio texto confirma: "suas ativações e
licenças seguem funcionando normalmente". As únicas acções por produto
(`PluginActivationManager.php`) são "Reativar", "Renovar" e "Solicitar ajuda"
— e "ajuda" é acção de frontend, sem rota REST própria. Para libertar um
domínio (ex.: site descomissionado que ainda consome uma licença comprada), a
única via é abrir um pedido de suporte à FULL — não há atalho técnico.
---
## Licenças que NÃO são da FULL (não confundir)
| Fonte | Como identificar | Exemplo real |
|---|---|---|
| **Fabricante directo** | Option própria com `license_key`, formato do produto (não FULL) | `elementskit_options.license_key` (WPMET), `_ff_fluentform_pro_license_key` (WPManageNinja) |
| **CodeCanyon (compra única)** | Zero option de licença, zero `envato`/`purchase_code` no código — ficheiro entregue sem phone-home, por desenho | `kivicare-pro` |
Confirmar sempre as duas possibilidades (FULL via `licenses-extract` E licença
própria do fabricante) antes de classificar um plugin como "sem licença" — um
produto pode estar em ambas, nenhuma, ou só numa.
---
## Erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| "X não é FULL" (conclusão errada) | Só verificado em `plugin-repository/updates`, faltou `account/licenses-extract` | Verificar sempre as duas fontes (ver "Regra de ouro") |
| `ConnectAccount` devolve `success:true` mas usermeta continua vazio | Faltou `--user=<ID>` no `wp eval-file` — `User::instance()` não estava "logged in", `setMeta()` silencia | Sempre passar `--user=<ID admin>` |
| Erro de sintaxe PHP no `eval-file` | Aspas duplas aninhadas via ssh | Base64 no stdin (ver topo desta skill) |
| Domínio "morto" que na verdade está vivo | DNS aponta para o servidor mas é conta partilhada com outros sites | Confirmar `siteurl` real (`wp option get siteurl`) antes de declarar morto — nunca só presença/ausência de pasta |
+116 -2
View File
@@ -1,7 +1,6 @@
---
name: jetengine
description: Desenvolvimento com JetEngine (Crocoblock). Cobre CPT, CCT, Meta Boxes, Taxonomias, Relations, Query Builder, Listing Grid/Map, Dynamic Content, Dynamic Tags, Macros, Profile Builder, REST API e automação headless.
layer: wiki
---
# /jetengine — JetEngine Development
@@ -173,4 +172,119 @@ JetEngine > Listings > Add New > Template Type: Map
```
%current_user_id% # ID do utilizador logado
%current_post_id%
%current_post_id% # ID do post actual
%queried_object_id% # ID do objecto em arquivo/single
%request_field|name% # Parâmetro URL (?name=valor)
%current_meta|field_name% # Meta field do post actual
%user_meta|field_name% # Meta field do utilizador logado
```
### Usar Macros em Widgets Elementor
```
Textos: escrever macro directamente no campo de texto
Links: usar em URL field
Queries: usar em Meta Query → Valor = macro
```
### Profile Builder
```
JetEngine > Profile Builder > Add New
├── Criar páginas de conta (Dashboard, Submissões, etc.)
├── Cada página = Elementor Template
├── Acesso: utilizadores logados, por role, por condição
└── Menu de Profile: widget JetEngine Profile Menu
```
## Dify KB — Consultar Antes de Implementar
```javascript
// Dataset Crocoblock (JetEngine incluído)
mcp__dify-kb__dify_kb_retrieve_segments({
dataset_id: "bdf85c26-1824-4021-92d1-be20501b35ac",
query: "[funcionalidade JetEngine]"
})
```
## Troubleshooting
| Problema | Causa | Solução |
|----------|-------|---------|
| Listing não mostra resultados | Query sem posts ou condição errada | Debug Query: activar `WP_DEBUG` e verificar |
| Dynamic Tag retorna vazio | Campo meta não preenchido ou nome errado | Verificar key do meta field em JetEngine > Meta Boxes |
| Relações não aparecem | rel_id errado ou sem items relacionados | Verificar nome da relação em JetEngine > Relations |
| CCT não aparece no REST | REST API não activada | JetEngine > Content Types > Edit > Enable REST API |
| Filtros não funcionam | Listing ID errado no filtro | JetEngine Listing Grid > copiar ID do widget |
## Bridge Plugin — Deploy para Integração n8n
Para integração headless com n8n ou agentes IA, fazer deploy do MU plugin incluído:
```bash
PHP="/opt/alt/php-fpm83/usr/bin/php"
WP="$PHP /usr/local/bin/wp --allow-root --path=/home/USER/public_html"
# Copiar bridge plugin para mu-plugins (via SSH)
# O ficheiro está em: assets/descomplicar-jet-bridge.php (nesta skill)
scp descomplicar-jet-bridge.php server.descomplicar.pt:/home/USER/public_html/wp-content/mu-plugins/
# Verificar que está activo (MU plugins carregam automaticamente)
$WP plugin list --skip-plugins --skip-themes # MU plugins não aparecem aqui — normal
```
**Endpoint exposto:** `POST /wp-json/descomplicar/v1/manage`
**Acções disponíveis:**
| Acção | Descrição |
|-------|-----------|
| `create_cct_item` | Criar item num CCT |
| `update_cct_item` | Actualizar item (`_ID` em item_data) |
| `get_cct_item` | Ler item por ID |
| `delete_cct_item` | Eliminar item |
| `manage_relation` | Criar/remover relação (connect: true/false) |
```bash
# Testar endpoint (exemplo: criar fatura)
curl -X POST https://site.pt/wp-json/descomplicar/v1/manage \
-H "Authorization: Basic $(echo -n 'user:app-password' | base64)" \
-H "Content-Type: application/json" \
-d '{
"action": "create_cct_item",
"cct_slug": "faturas",
"item_data": { "titulo": "Fatura #001", "valor": 500 }
}'
```
## JetEngine MCP Server (v3.8.0+)
A partir da versão 3.8.0, o JetEngine tem **MCP Server nativo**:
```
WP Admin > JetEngine > AI Command Center > Enable MCP Server
→ Agentes IA podem inspecionar e criar CPTs/CCTs/Relations via linguagem natural
→ Claude consegue ler o schema completo e instanciar estruturas directamente
```
## Referências Adicionais
- **`assets/descomplicar-jet-bridge.php`** — MU Plugin pronto a deployar: CRUD CCT + Relations via REST API
- **`references/automation.md`** — BD completa (CCT, Relations meta, Custom Meta Storage), WP-CLI batch, PHP hooks, REST API avançada (store_items_type, filtros CCT), cache e guardrails
- **`references/query-listings.md`** — Query Builder avançado, Listing Map, paginação, ordenação dinâmica
- **`references/dynamic-content.md`** — Dynamic Tags, Profile Builder, Macros, Conditional Fields
---
**Versão**: 1.1.0 | **Autor**: Descomplicar® | **Data**: 18-02-2026
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
@@ -0,0 +1,73 @@
---
name: mcp-abilities-elementor
description: Plugin WordPress de terceiros (Devenia, GPLv2) que expõe 89 abilities Elementor via a WordPress Abilities API nativa (core 6.9+) — CRUD de páginas/elementos/templates, gestão de kits, e uma camada de auditoria de qualidade de design sem equivalente no EMCP Tools. Instalado e testado ao vivo em emanuelalmeida.pt (19-08-2026). Usar quando "mcp-abilities-elementor", "wordpress abilities api elementor", "evaluate-design elementor", "audit generic layout", "delete-template elementor", "kit settings elementor mcp", "score-distinctiveness".
layer: wiki
---
# /mcp-abilities-elementor — Plugin de Abilities Elementor (Devenia)
Instalado e testado ao vivo em `emanuelalmeida.pt` (19-08-2026): download `https://downloads.devenia.com/mcp-abilities-elementor.zip` → `wp plugin install ... --activate`. Fonte: `github.com/bjornfix/mcp-abilities-elementor` (GPLv2, 24★). Requer WP 6.9+ (confirmado: site em 7.0.4), PHP 8.0+, Elementor activo; Elementor Pro opcional (Theme Builder/Custom Code/Form Submissions ficam indisponíveis sem Pro — confirmado: `list-custom-code` devolveu `{"success":false,"message":"Elementor custom code post type is not available"}` mesmo com Elementor Pro 4.0.0 activo, porque essa funcionalidade Pro específica não está disponível nesta versão/licença).
## 🔴 Arquitectura — NÃO está acessível por MCP nesta sessão, só por wp-cli/PHP
Este plugin regista abilities na **WordPress Abilities API nativa** (`wp_register_ability()`, ver skill genérica sobre a API). Ele **não** cria o próprio servidor MCP — o seu `mcp-abilities-elementor.php` (228KB) não tem uma única referência a `mcp_adapter`/`McpServer`/`register_server`.
**Causa-raiz confirmada ao vivo (não é falta de instalação, é falta de opt-in):** o WordPress MCP Adapter oficial (`github.com/WordPress/mcp-adapter`) cria automaticamente um "default server" que expõe qualquer ability com a meta-flag `meta.public: true` (ou `meta.mcp.public: true`) através de 3 tools dispatcher (`discover-abilities`/`get-ability-info`/`execute-ability`) — exactamente o padrão que o `mcp-adapter-default-server` (13 tools) já visível em `wp mcp-adapter list` neste site implementa (o EMCP Tools bundla esta mesma biblioteca internamente). Confirmei via `wp eval "wp_get_ability('elementor/get-theme-context')->get_meta()"`: devolve `{"annotations":{...},"show_in_rest":false}` — **sem `public` nem `mcp.public`**. O `mcp-abilities-elementor` nunca marcou nenhuma das 89 abilities como pública para MCP, nem criou um servidor custom a listá-las explicitamente (`$adapter->create_server(...)` com a lista de ability names). Resultado: mesmo com o WordPress MCP Adapter oficial activo neste site, estas abilities continuariam invisíveis ao servidor por omissão — falta o `meta.public`/`meta.mcp.public` no registo, ou um servidor custom que as liste. Isto teria de ser corrigido pelo autor do plugin (Devenia) numa actualização, ou por nós via um mu-plugin que registe um servidor custom com as 89 abilities.
**Caminho de teste usado, funcional e reproduzível:** chamar a ability directamente via `WP_Ability::execute()` por `wp eval-file`, sem transporte MCP nenhum — o mesmo caminho de código que o `execute-ability` do MCP Adapter acabaria por invocar:
```bash
# harness reutilizável (criar uma vez em /tmp/ability-test.php no servidor):
cat > /tmp/ability-test.php << 'PHPEOF'
<?php
$name = $args[0] ?? '';
$input = isset($args[1]) ? json_decode($args[1], true) : [];
$ability = wp_get_ability($name);
if (!$ability) { echo json_encode(['error' => 'ability not found', 'name' => $name]); return; }
try {
echo json_encode($ability->execute($input ?: []), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (\Throwable $e) {
echo json_encode(['error' => 'exception', 'message' => $e->getMessage()]);
}
PHPEOF
# chamar uma ability:
wp eval-file /tmp/ability-test.php 'elementor/get-theme-context' '{}' --path=/caminho/do/site --allow-root --user=1
```
**`--user=1` é obrigatório** — sem utilizador definido, `current_user_can()` dentro do `permission_callback` de cada ability falha silenciosamente/nega acesso, porque o wp-cli corre sem contexto de utilizador autenticado por omissão.
## Catálogo (89 abilities, 7 categorias)
Ver `references/catalogo.md` para a tabela completa com descrição de cada uma. Resumo por categoria: Page/Post Data (43), Authoring Primitives (12), Template Management (11), Theme Builder (2), Custom Code/Pro (5), Form Submissions/Pro (3), Site Tools & Settings (11) — soma o namespace `elementor/*` completo confirmado ao vivo (`wp eval 'count(array_filter(array_keys(wp_get_abilities()), fn($k)=>str_starts_with($k,"elementor/")))'` → 89).
## O que isto tem que o EMCP Tools não tem
- **Camada de auditoria de qualidade de design** — `evaluate-design`, `audit-generic-layout-patterns`, `score-distinctiveness`, `audit-generic-component-repetition`, `audit-surface-overuse`, `audit-emphasis-drift`, `audit-section-rivalry`, `audit-composition-rhythm`, `audit-separator-discipline`, `audit-column-*` (5 variantes), `extract-design-tokens`, `apply-text-hierarchy`, `normalize-section-spacing-rhythm`, `normalize-responsive-values`, `sync-component-variant`. **Testado ao vivo** em `emanuelalmeida.pt` post_id 9 ("Sobre mim"): `evaluate-design` devolveu `score: 88`, `recommendations: ["Top-level section composition is repeating. Increase compositional contrast between adjacent sections."]`, com breakdown por categoria (`generic_layout`, `distinctiveness`, `component_overuse`, etc.) incluindo assinaturas de secção e penalizações concretas. Genuinamente útil — sem equivalente algum no EMCP.
- **CRUD de templates completo**, incluindo `delete-template` — **testado ao vivo**: criei um template de secção (`create-template`, id 18168), depois `delete-template({id:18168, force:true})` apagou-o em definitivo com sucesso. Isto preenche exactamente o gap que documentei em `emcp-page-building` ("Não há tool de eliminação de template no conjunto comummente activo").
- **Multi-kit** — `list-kits`/`get-kit-settings`/`set-active-kit`. Testado: `emanuelalmeida.pt` tem 2 kits (`Kit Padrão` id 14 inactivo, `Default Kit` id 11 activo) — EMCP só expõe o kit activo via `get-global-settings`.
- **`get-official-widget-catalog`/`get-official-pattern-guidance`** — vai buscar o catálogo/orientação oficiais a elementor.com em tempo real, em vez de uma lista estática mantida à mão.
- **Form Submissions CRUD (Pro)** e **Custom Code CRUD (Pro)** completos — EMCP só tem fragmentos, maioritariamente desligados por omissão.
## O que isto NÃO tem / é pior que o EMCP
- **Não suporta elementos atómicos Elementor 4.0** — confirmado ao vivo: `add-container`/`add-heading` neste site (Elementor 4.2.2, atomic-capable) produzem `elType: "container"`/`widgetType: "heading"` **legacy**, nunca `e-flexbox`/`e-heading`. Para construção atómica v4, usar `emcp-page-building` (EMCP), não este plugin.
- **Nenhuma gestão de conteúdo WordPress genérico** — sem posts/pages/CPTs/media/menus/utilizadores/plugins fora do âmbito Elementor. Complementa, não substitui, `emcp-content-ops`.
## 🔴 Armadilhas e bugs confirmados ao vivo
1. **Gate "template-first" em todas as abilities de "Authoring Primitives"** — `add-container`/`add-heading`/etc. recusam-se a correr sem `template_lookup_status` explícito (ou uma chamada prévia a `find-template-for-pattern`). Testado: `add-container` sem esse campo devolveu `{"success":false,"message":"Raw Elementor container authoring requires template lookup status. Call elementor/find-template-for-pattern first...","required_next_step":"elementor/find-template-for-pattern"}`. Para edições pontuais legítimas (não um padrão reutilizável), passar `"template_lookup_status":"not_a_reusable_pattern"` — confirmado a destravar a chamada.
2. **`update-element` espera o elemento COMPLETO em `element_data` (com `id`), não um merge parcial de `settings`.** Diferente da convenção do EMCP (`update-element`/`update-atomic-widget` fazem merge parcial por omissão). Testado: `{"element_id":"x","settings":{...}}` falha ("element_data é obrigatório"); `{"element_data":{"settings":{...}}}` falha de novo ("Element data must include a string 'id'") — só funciona com o objecto completo `{"element_data":{"id":"x","elType":"widget","widgetType":"...","settings":{...}}}`. Confirmado também no changelog (v2.2.9): "guardrails... incomplete replacement payloads do not silently wipe populated Elementor containers/widgets unless `force_replace=true`" — o comportamento por omissão é substituição total, não merge.
3. **`delete-element` recusa apagar sem `force_delete:true`** mesmo num elemento aninhado (não literalmente top-level da página) — testado, mensagem "Refusing to delete a top-level Elementor element without force_delete=true". Passar sempre `force_delete:true` quando a intenção é mesmo apagar.
4. **🐛 Bug confirmado — `add-heading` (e provavelmente as outras "Authoring Primitives") grava os parâmetros de controlo da própria ability dentro do `settings` persistido do widget Elementor.** Testado: depois de `add-heading({..., "template_lookup_status":"not_a_reusable_pattern"})`, `get-element` devolveu `settings: {"parent_id":"980c94d","title":"...","template_lookup_status":"not_a_reusable_pattern","header_size":"h2"}` — `parent_id` e `template_lookup_status` **não são controlos Elementor válidos**, ficaram poluindo os dados reais do widget. Reportável ao autor; entretanto, não confiar cegamente no `settings` devolvido por `get-element`/`get-data` para saber quais chaves são controlos genuínos.
5. **🐛 Bug confirmado — `list-experiments` emite `PHP Warning: Array to string conversion` em `register-site-tools.php:632`.** Não é fatal (a chamada ainda devolve `success:true` com os dados), mas polui stderr/logs.
6. **Aplicação de estilo global (documentada no changelog, não testada nesta sessão):** desde a v2.3.6, escritas rejeitam cores/tipografia locais que não correspondam a um token global do Kit — cores hex locais são normalizadas para o token global correspondente quando possível, senão a escrita é rejeitada com "structured violations". `apply-text-hierarchy` desde então usa por omissão referências de tipografia global do kit em vez de overrides locais. **Verificar isto ao vivo antes de confiar cegamente** — não reproduzi este comportamento nesta sessão (só toquei em `title`, não em cor/tipografia).
7. **`elementor/update-data`, `elementor/clone-data`, `elementor/patch-data`** exigem confirmação explícita por ability antes de escrever dados brutos do documento Elementor (desde v2.3.11) — não testado nesta sessão, mas documentado no changelog; esperar um campo `confirm`-like obrigatório.
## Estado no site (19-08-2026)
Instalado e **activo** em `emanuelalmeida.pt` (`mcp-abilities-elementor` v2.3.36). Todos os artefactos de teste foram removidos (página de rascunho `#18166` apagada em definitivo via `wp post delete --force`; template de secção `#18168` apagado via `elementor/delete-template`). **Decisão em aberto:** o plugin fica activo mas **inacessível por MCP** até se instalar o WordPress MCP Adapter oficial separado ou se estender a config do servidor MCP do EMCP Tools — nenhuma das duas foi feita nesta sessão. Só chamável via `wp eval-file` (padrão acima) até essa decisão ser tomada.
## Skills relacionadas
- `emcp-page-building` — construção atómica Elementor 4.0 via EMCP (este plugin não suporta atómico).
- `emcp-content-ops` / `emcp-site-audit` — gestão de conteúdo WordPress genérico e segurança/performance (fora do âmbito deste plugin).
@@ -0,0 +1,131 @@
# Catálogo completo — 89 abilities `elementor/*` (mcp-abilities-elementor v2.3.36)
Fonte: README oficial do repositório, cruzado com `wp eval` (`count(array_filter(array_keys(wp_get_abilities()), fn($k)=>str_starts_with($k,"elementor/")))` → 89, confirmado ao vivo em emanuelalmeida.pt 19-08-2026).
## Page/Post Data (43)
| Ability | Descrição |
|---|---|
| `elementor/get-data` | Estrutura JSON Elementor de uma página |
| `elementor/clone-data` | Copia dados Elementor + definições de página entre documentos, com confirmação explícita |
| `elementor/get-element` | Um elemento específico por ID |
| `elementor/get-widget-controls` | Inspecciona chaves e metadados de controlo nativo de um tipo de widget |
| `elementor/find-elements` | Encontra elementos por tipo, widget, ou texto |
| `elementor/update-element` | Actualiza um elemento específico por ID (objecto completo, ver gotcha #2) |
| `elementor/merge-element-settings` | Deep-merge de settings num elemento (alternativa ao update-element completo) |
| `elementor/zero-container-padding-subtree` | Zera padding de container numa subárvore |
| `elementor/copy-lane-settings` | Copia settings de largura/gap entre elementos |
| `elementor/copy-row-balance` | Copia ritmo de linha e balanço de colunas filhas entre linhas |
| `elementor/image-widget-to-background-container` | Converte widget de imagem num container com background-image nativo |
| `elementor/fix-visible-gap-rhythm` | Remove espaçamento oculto que quebra o ritmo visual de gaps |
| `elementor/enforce-boundary-coherence` | Normaliza subárvore para full-width verdadeiro ou fronteiras boxed coerentes |
| `elementor/audit-generic-layout-patterns` | Audita padrões de composição genéricos repetidos numa página/subárvore |
| `elementor/score-distinctiveness` | Pontua distintividade composicional sem prescrever estilo |
| `elementor/audit-generic-component-repetition` | Assinala mobília repetida (botões em excesso, cartões repetidos) |
| `elementor/audit-surface-overuse` | Reporta tratamentos de superfície repetidos com cautela |
| `elementor/audit-emphasis-drift` | Verifica se secções de topo têm peso de ênfase demasiado semelhante |
| `elementor/audit-section-rivalry` | Apanha páginas com demasiadas secções a competir como clímax simultâneos |
| `elementor/audit-composition-rhythm` | Inspecciona ritmo/pacing de secções de topo |
| `elementor/audit-separator-discipline` | Avisa quando separadores achatam a hierarquia de secções maiores |
| `elementor/get-theme-context` | Resume tema activo, versão Elementor, kit activo, definições de viewport |
| `elementor/get-official-widget-catalog` | Catálogo oficial completo de widgets Elementor, de elementor.com/widgets |
| `elementor/get-official-pattern-guidance` | Catálogo de orientação oficial elementor.com para widgets/layout |
| `elementor/get-style-guide` | Resumo de style guide a partir do kit activo e tokens |
| `elementor/evaluate-design` | Agrega as auditorias de design num score, lista de issues e recomendações — **testado ao vivo, funciona** |
| `elementor/suggest-design-fixes` | Transforma a avaliação agregada em sugestões concretas de correcção |
| `elementor/evaluate-render-context` | Inspecciona o contexto de render frontend separado da qualidade de conteúdo |
| `elementor/audit-column-patterns` | Audita rácios de coluna repetidos (50/50, terços iguais) |
| `elementor/audit-layout-mechanism-fit` | Recomenda Grid em vez de Flexbox para grupos de colunas simétricos |
| `elementor/audit-native-widget-opportunities` | Sugere widgets nativos/Pro quando um padrão hand-built os recria |
| `elementor/audit-column-dominance` | Assinala splits de coluna iguais que podem esconder um lado dominante |
| `elementor/audit-column-alignment-rhythm` | Reporta rácios semelhantes com ritmos de gutter inconsistentes |
| `elementor/audit-column-balance` | Assinala linhas assimétricas que podem não justificar a assimetria |
| `elementor/audit-column-necessity` | Assinala splits que podem não justificar a complexidade |
| `elementor/reset-negative-margins-subtree` | Limita margens negativas numa subárvore |
| `elementor/extract-design-tokens` | Extrai cores/tipografia/spacing/tokens dimensionais recorrentes — **testado ao vivo, funciona** |
| `elementor/apply-text-hierarchy` | Normaliza tipografia de heading/body/button numa subárvore |
| `elementor/normalize-section-spacing-rhythm` | Ajusta spacing de secção e gaps de linha a um ritmo consistente |
| `elementor/normalize-responsive-values` | Preenche/normaliza valores tablet/mobile a partir do desktop |
| `elementor/sync-component-variant` | Copia settings relevantes de design entre subárvores de componentes |
| `elementor/delete-element` | Apaga um elemento por ID (exige `force_delete:true` — testado) |
| `elementor/update-data` | Substitui o JSON Elementor inteiro de uma página (exige confirmação explícita) |
| `elementor/patch-data` | Find/replace de texto dentro do JSON Elementor (exige confirmação explícita) |
| `elementor/update-page-settings` | Actualiza definições de página Elementor |
## Authoring Primitives (12) — todas sujeitas ao gate "template-first"
| Ability | Descrição |
|---|---|
| `elementor/create-page` | Cria página/post novo com Elementor builder activo — **testado ao vivo, funciona** |
| `elementor/add-container` | Adiciona container top-level ou aninhado (legacy, não atómico) — **testado ao vivo** |
| `elementor/add-widget` | Adiciona qualquer tipo de widget Elementor com settings brutos |
| `elementor/add-post-tabs` | Adiciona Nested Tabs nativo, cada tab com um widget Posts nativo |
| `elementor/add-heading` | Adiciona widget heading — **testado ao vivo** |
| `elementor/add-text-editor` | Adiciona widget text editor |
| `elementor/add-image` | Adiciona widget imagem a partir de attachment ID ou URL |
| `elementor/add-button` | Adiciona widget botão |
| `elementor/move-element` | Move elemento para novo pai/posição |
| `elementor/remove-element` | Remove elemento com guardas de segurança |
| `elementor/duplicate-element` | Duplica subárvore de elemento com IDs novos |
| `elementor/reorder-elements` | Reordena filhos directos de um pai ou ao nível de topo |
## Template Management (11)
| Ability | Descrição |
|---|---|
| `elementor/list-templates` | Lista todos os templates Elementor guardados — **testado ao vivo** |
| `elementor/find-template-for-pattern` | Encontra templates guardados que correspondem a padrões reutilizáveis |
| `elementor/get-template` | Obtém um template com todos os dados |
| `elementor/create-template` | Cria template page/section/popup/header/footer — **testado ao vivo** |
| `elementor/update-template` | Modifica template existente |
| `elementor/delete-template` | Move para lixo ou apaga em definitivo — **testado ao vivo, confirmado funcional** |
| `elementor/restore-template` | Restaura template do lixo |
| `elementor/empty-trash` | Apaga em definitivo todos os templates no lixo |
| `elementor/duplicate-template` | Copia um template |
| `elementor/export-template` | Exporta como JSON |
| `elementor/import-template` | Importa de JSON |
## Theme Builder (2)
| Ability | Descrição |
|---|---|
| `elementor/get-theme-builder-conditions` | Condições de exibição de um template/tipo |
| `elementor/update-theme-builder-conditions` | Actualiza condições de exibição de um template |
## Custom Code (5, requer Elementor Pro)
| Ability | Descrição |
|---|---|
| `elementor/list-custom-code` | Lista snippets — **testado ao vivo: falhou graciosamente, "post type is not available" nesta versão/licença Pro** |
| `elementor/get-custom-code` | Obtém um snippet |
| `elementor/create-custom-code` | Cria snippet |
| `elementor/update-custom-code` | Actualiza snippet |
| `elementor/delete-custom-code` | Apaga snippet |
## Form Submissions (3, requer Elementor Pro)
| Ability | Descrição |
|---|---|
| `elementor/list-form-submissions` | Lista submissões — **testado ao vivo, funciona (0 submissões no site)** |
| `elementor/get-form-submission` | Obtém uma submissão |
| `elementor/delete-form-submission` | Apaga uma submissão |
## Site Tools & Settings (11)
| Ability | Descrição |
|---|---|
| `elementor/list-global-widgets` | Lista todos os widgets globais — **testado ao vivo, 0 no site** |
| `elementor/list-kits` | Lista kits Elementor disponíveis — **testado ao vivo: 2 kits (Kit Padrão id 14, Default Kit id 11 activo)** |
| `elementor/get-kit-settings` | Definições Elementor de todo o site — **testado ao vivo** |
| `elementor/update-kit-settings` | Actualiza cores/tipografia globais |
| `elementor/set-active-kit` | Define o kit activo |
| `elementor/clear-cache` | Limpa cache Elementor (âmbito post ou site) |
| `elementor/replace-urls` | Substitui URLs dentro de dados Elementor |
| `elementor/get-maintenance-mode` | Definições de modo de manutenção — **testado ao vivo** |
| `elementor/update-maintenance-mode` | Activa/actualiza modo de manutenção |
| `elementor/list-experiments` | Lista experiments Elementor — **testado ao vivo: funciona mas emite `PHP Warning: Array to string conversion` em register-site-tools.php:632 (bug confirmado)** |
| `elementor/update-experiment` | Actualiza estado de um experiment |
## Nota sobre a arquitectura legacy vs atómica
Confirmado ao vivo: `add-container`/`add-heading` neste site (Elementor 4.2.2, com suporte atómico) produzem `elType:"container"`/`widgetType:"heading"` — arquitectura **legacy**, não `e-flexbox`/`e-heading` (Elementor 4.0 atómico). Presumir que todas as "Authoring Primitives" desta lista seguem o mesmo padrão legacy até prova em contrário.
+144
View File
@@ -0,0 +1,144 @@
---
name: mcp-ai-engine
description: MCP dedicado (node stdio, ligação `ai-engine` em ~/.omp/agent/mcp.json) para gerir o AI Engine (Meow Apps, slug `ai-engine`) multi-site (bundle Descomplicar) via WP-CLI/SSH — estado/versão, fornecedores de IA e de embeddings/vector DB configurados (chave API SEMPRE mascarada), modelos/ambientes por omissão por tipo de tarefa, módulos/feature-toggles, chatbots/assistentes configurados, estatísticas de uso agregadas (tokens/custos por modelo/mês/dia, sem PII), add-ons, limites de utilização. Escrita: activar/desactivar um módulo, actualizar modelo/instruções de um chatbot. Usar quando "ai engine", "mwai", "meow apps", "chatbot wordpress", "mwai_options", "mwai_chatbots", "chave API openai wordpress", "fornecedor de IA configurado", "estatísticas de uso IA wordpress", "assistente de IA wordpress".
layer: wiki
---
# /mcp-ai-engine — MCP dedicado ao AI Engine (Meow Apps)
Projecto em `/media/ealmeida/Dados/Dev/mcp-ai-engine/` (TypeScript, SDK MCP oficial, stdio). Cada
tool executa `wp option get/patch` via WP-CLI já instalado em `server.descomplicar.pt`. Escritas
viajam em base64 por stdin (`base64 -d | wp option patch update ...`), nunca interpoladas na linha
de comando remota.
Não há skill de conhecimento separada para o AI Engine — esta skill é a única documentação,
incluindo o mapeamento completo de armazenamento (abaixo).
## Âmbito
Estado/configuração/estatísticas do plugin: fornecedores de IA e as suas capacidades, chatbots
configurados, módulos activos, defaults por tipo de tarefa, uso agregado (tokens/custos),
add-ons, limites. **Nunca** chaves API em claro. **Fora do âmbito** (excluído estruturalmente, não
por falta de tempo): histórico de conversas/discussões de visitantes (tabela `wp_mwai_chats` —
PII, e nem sequer existe ainda nos sites verificados porque o módulo `chatbot_discussions` está
desligado em todos), ficheiros enviados (`wp_mwai_files`), tarefas agendadas (`wp_mwai_tasks`),
qualquer escrita em `apikey`/`apiKey` (por desenho — ver Segurança).
## Sites conhecidos
Confirmado por `wp plugin list --format=csv` em todos os 8 sites reais do bundle (19-08-2026):
| Site | Estado AI Engine | Notas |
|---|---|---|
| `starter` (starter.descomplicar.pt) | **ACTIVO**, 3.7.1 | Chatbot "Default" (`botId: default`) configurado, providers OpenAI + Claude registados **sem** chave API. Site principal de teste deste MCP. |
| `ccv` (ccv.descomplicar.pt) | Instalado, **inactivo**, 3.7.1 | Config (`mwai_options`/`mwai_chatbots`) persiste na BD mesmo com o plugin desactivado — continua legível/editável via WP-CLI. |
| `ecommerce-demo` (ecommerce-demo.descomplicar.pt) | Instalado, **inactivo**, 3.7.1 | Sem chatbots configurados (`mwai_chatbots` nunca chegou a ser gravada). |
| `ecommerce` (ecommerce.descomplicar.pt) | Instalado, **inactivo**, 3.7.1 | — |
| `descomplicar` (produção) | **NÃO instalado** | Apesar de referido como "activo" num briefing anterior — confirmado por leitura directa `wp plugin list` que NÃO está no site. |
| `emanuelalmeida`, `care`, `e-commerce` | **NÃO instalado** | — |
`ai_engine_list_sites` devolve esta lista com as notas; qualquer tool aceita também um path
absoluto directo para uma instalação futura noutro site.
## Mapeamento de armazenamento (confirmado por leitura do código-fonte do plugin)
O AI Engine guarda **arrays PHP nativos** (não strings com JSON lá dentro) em 2 options —
confirmado ao vivo com `wp eval "echo gettype(get_option('mwai_options'));"` → `array`. Por isso
`--format=json` é seguro tanto para leitura como para escrita (ao contrário do WP Fastest Cache ou
do TablePress, que serializam JSON como string plana — ver `mcp-wpfc`/`mcp-tablepress`).
| Option | Conteúdo | Formato |
|---|---|---|
| `mwai_options` (`classes/core.php`, `$option_name`) | Config principal: `ai_envs` (fornecedores de IA), `embeddings_envs` (fornecedores de embeddings/vector DB), `module_*`/`workspace_*`/`chatbot_*`/`speech_*`/`mcp_*` (feature toggles), `ai_*_default_env`/`ai_*_default_model` (defaults por tipo de tarefa: texto, fast, visão, imagens, áudio, JSON, embeddings), `ai_usage`/`ai_usage_daily` (estatísticas de uso agregadas), `addons`, `limits`/`default_limits`, `chatbot_defaults`, `logs_path`, etc. | Array PHP nativo |
| `mwai_chatbots` (`$chatbots_option_name`) | Array de chatbots/assistentes: `botId`, `name`, `model`, `instructions` (prompt de sistema), `temperature`, `maxMessages`, `maxTokens`, `apiKey` (override opcional por chatbot), tema/UI, `functions`, `mcpServers`, `crossSite`. | Array PHP nativo |
| `mwai_themes` (`$themes_option_name`) | Temas visuais custom do widget de chat. Vazio/`false` em todos os sites verificados — fora do âmbito deste MCP (não é config de IA). | Array PHP nativo (ou `false` quando nunca gravada) |
**Gotcha confirmado ao vivo**: a option `mwai_options` lida por `wp option get` inclui campos que
o próprio plugin trata como "dinâmicos" no seu código (`populate_dynamic_options()` em
`core.php:1404`, chamado só pelo acessor interno `get_all_options()`, nunca pelo WordPress core
`get_option()`) — nomeadamente `addons` (com `enabled` calculado) e `ai_engines` (catálogo enorme
e estático de modelos por fornecedor). Na prática estes campos **já estão persistidos na BD** em
todos os sites verificados, porque o painel admin (React) faz round-trip do objecto completo que
recebe via REST (que já vem enriquecido) de volta para `update_option()` na gravação. Ou seja: não
é um artefacto deste MCP, é o estado real da BD — `ai_engine_list_addons` lê exactamente isso.
Para não agravar este "bloat" nem arriscar persistir uma cópia desactualizada do catálogo `ai_engines`
(que é grande e evolui com cada release do plugin), **nenhuma tool de escrita deste MCP relê e
regrava a option inteira** — todas usam `wp option patch update <option> <key-path...>`, que
grava só o caminho aninhado pedido dentro do array já existente na BD.
Não existe comando WP-CLI custom (`wp help mwai`/`wp help ai-engine` confirmam "not a registered
command" na v3.7.1) — todo o acesso é via `wp option get/patch`.
## Tools (12)
| Tool | Read-only | Uso |
|---|---|---|
| `ai_engine_list_sites` | sim | Aliases conhecidos, path WP, nota de estado do AI Engine |
| `ai_engine_get_status` | sim | Instalado/activo/versão — `wp plugin list` filtrado a `ai-engine` |
| `ai_engine_list_providers` | sim | `ai_envs` (OpenAI, Claude, Azure, Google, OpenRouter, Replicate, Mistral, etc.) — chave API mascarada |
| `ai_engine_list_embeddings_providers` | sim | `embeddings_envs` (Internal/WordPress DB, OpenAI Vector Store, Pinecone, etc.) — chave API mascarada |
| `ai_engine_get_defaults` | sim | Modelo/ambiente por omissão por tipo de tarefa (texto, fast, visão, imagens, áudio, JSON, embeddings) |
| `ai_engine_list_modules` | sim | Todas as chaves booleanas de `mwai_options` (descoberta dinâmica — `module_*`, `workspace_*`, `chatbot_*`, `speech_*`, `mcp_*`, flags soltas) |
| `ai_engine_list_chatbots` | sim | Chatbots/assistentes configurados (nome, modelo, instruções, limites) — `apiKey` mascarada |
| `ai_engine_get_usage` | sim | `ai_usage`/`ai_usage_daily` — tokens/custos agregados por modelo/mês/dia, sem PII |
| `ai_engine_list_addons` | sim | Add-ons oficiais do AI Engine e se estão `enabled` |
| `ai_engine_get_limits` | sim | Quotas de créditos/queries por período (visitantes/utilizadores/sistema) |
| `ai_engine_set_module` | não | Activa/desactiva 1 chave booleana já existente em `mwai_options` (valida dinamicamente que existe e é booleana antes de escrever) |
| `ai_engine_set_chatbot_field` | não | Actualiza `model` ou `instructions` de 1 chatbot (por `botId`) — allowlist rígida de 2 campos, nunca `apiKey`/`functions`/`mcpServers` |
## Segurança
**Chaves API nunca em claro, em nenhuma tool, em nenhuma circunstância.** `ai_envs[].apikey`,
`embeddings_envs[].apikey` e `mwai_chatbots[].apiKey` são sempre destruturados fora do objecto
devolvido e substituídos por `{ has_key: boolean, key_hint: string | null }` (`src/mask.ts`) —
`key_hint` mostra só os últimos 4 caracteres prefixados de asteriscos (`***abcd`), ou máscara total
se a chave tiver ≤4 caracteres. Nenhuma tool de escrita toca em `apikey`/`apiKey` — `ai_engine_set_module`
só aceita chaves já-booleanas de `mwai_options` (rejeita `ai_envs`/`embeddings_envs`/qualquer chave
não-booleana), `ai_engine_set_chatbot_field` só aceita `field ∈ {model, instructions}` (enum zod,
rejeitado no schema antes mesmo de chegar ao handler).
Escritas usam `wp option patch update <option> <key-path...>` com o valor em base64 sobre stdin
(nunca interpolado no comando SSH remoto). Gotcha herdado do `mcp-gtranslate`: `update_option()`
devolve `false` tanto em falha real como quando o valor gravado já é igual ao actual, e o WP-CLI
nem sempre reporta esse caso como sucesso — em caso de exit code != 0, `patchKey` relê o valor no
caminho pedido e só propaga o erro se continuar diferente do pretendido.
Sem histórico de conversas de visitantes acessível por este MCP — a tabela `wp_mwai_chats`
(módulo `chatbot_discussions`) não é lida por nenhuma tool, mesmo que venha a ser activada no
futuro; é uma exclusão estrutural, não uma limitação de dados actualmente vazios.
## Verificação
Construído e testado ponta-a-ponta (19-08-2026):
- Build TypeScript limpo (`npm run build`), smoke test stdio (`initialize` + `tools/list` →
12 tools).
- `ai_engine_get_status` em `starter` confirmou `installed:true, active:true, version:"3.7.1"`.
- `ai_engine_list_providers`/`ai_engine_list_embeddings_providers`/`ai_engine_list_chatbots`/
`ai_engine_list_modules`/`ai_engine_get_defaults`/`ai_engine_get_usage`/`ai_engine_list_addons`/
`ai_engine_get_limits` em `starter`: 8 leituras reais confirmadas, incluindo `module_addons:true`,
`chatbot_discussions_titling:true` (typing dinâmico de booleanos correcto), 2 fornecedores de IA
e 1 de embeddings (Pinecone) devolvidos com `has_key:false, key_hint:null` (sem chave configurada
no site) e 1 chatbot ("Default") com `instructions` legível e `apiKey` mascarada.
- **Verificação de mascaramento com chave real** (não apenas ausência de chave): gravada
temporariamente uma chave de teste falsa (`sk-TEST-FAKE-...abcdEND`) directamente em
`ai_envs[0].apikey` no site `ccv` (inactivo, baixo risco) via `wp option patch` fora do MCP;
chamada `ai_engine_list_providers` devolveu `has_key:true, key_hint:"***dEND"` — a chave completa
**nunca** apareceu em nenhum campo da resposta JSON-RPC. Valor restaurado a `""` de imediato e
confirmado por leitura directa idêntico ao original.
- Também testado directamente `maskSecret()` isolado (chave longa → `***` + últimos 4 chars; chave
curta ≤4 chars → máscara total; vazio/`null`/`undefined` → `has_key:false`).
- **Escrita `ai_engine_set_module`**: round-trip idempotente em `starter` —
`module_statistics` (`false`→`true`→`false`), confirmado por leitura directa fora do MCP antes/
depois idêntico ao valor original.
- **Escrita `ai_engine_set_chatbot_field`**: round-trip idempotente em `starter` — `model` do
chatbot `default` (`""`→`"gpt-4o-mini"`→`""`), confirmado por leitura directa fora do MCP que o
valor final é byte-a-byte igual ao original e que `instructions` (campo não tocado) permaneceu
inalterado.
## Skills relacionadas
Nenhuma skill de conhecimento separada existe para o AI Engine — este documento cobre tudo. Ver
`wp-cli` para o padrão geral de gestão WordPress via WP-CLI/SSH usado por este MCP, e
`mcp-gtranslate`/`mcp-tablepress` para o padrão de `wp option patch` com arrays nativos vs. options
JSON-em-string.
@@ -0,0 +1,117 @@
---
name: mcp-bit-integrations
description: MCP dedicado multi-site (node stdio, ligação `bit-integrations` em ~/.omp/agent/mcp.json) para o Bit Integrations / Bit Integrations Pro (BitApps) no bundle Descomplicar® — o plugin de automação/workflow que liga formulários (WPForms, FluentBooking, ...) a apps de terceiros (Perfex CRM/Desk, Google Contacts, etc.), distinto do BitSocial (mcp-bit-social, sharing social). Flows (integrações), logs de execução, ligações a apps, estatísticas agregadas, activar/desactivar flow. Único documento de referência para o schema `btcbi_*` — não existe skill de conhecimento separada. Usar quando "bit integrations", "btcbi", "flow bitapps", "porque falhou a integração com o Perfex/Desk", "logs bit integrations", "desactivar flow", "ligações apps terceiras bitapps".
layer: wiki
---
# /mcp-bit-integrations — MCP dedicado multi-site ao Bit Integrations
Projecto em `/media/ealmeida/Dados/Dev/mcp-bit-integrations/` (TypeScript, SDK MCP oficial,
stdio). Sem código PHP novo no WordPress: cada tool corre `wp db query`/`wp option get`/`wp plugin
list` via `ssh server` sobre o WP-CLI já instalado, contra o schema `<wp_prefix>btcbi_*` e as
`wp_options` `btcbi_*`/`bit_integrations_*` do plugin. **Distinto do BitSocial** (mesmo fabricante
BitApps, coberto por `mcp-bit-social`): o BitSocial publica posts do blog em redes sociais; o Bit
Integrations é um motor de automação genérico (tipo Zapier interno) que liga eventos WordPress
(submissão de formulário, marcação de reunião) a acções em CRMs/apps externas.
## Sites conhecidos
Chamar `bi_list_sites` para a lista actual com path e nota de estado. Verificado ao vivo
19-08-2026 (`wp plugin list`), Bit Integrations 2.10.2 está **activo em todo o bundle** —
`descomplicar` (produção), `starter` (staging), `care`, `ecommerce-demo`, `e-commerce`,
`ecommerce`. Só `descomplicar` e `starter` estão no âmbito verificado ponta-a-ponta deste MCP
(pedido original); os restantes 4 estão listados como bónus (mesma arquitectura, tabelas
confirmadas por `wp plugin list`, dados não auditados). Também aceita um path absoluto
directamente para sites fora desta lista.
**Estado real dos dois sites principais (19-08-2026):**
- `descomplicar` (produção, `/home/ealmeida/public_html`, prefixo BD `wpah_`): **10 flows, 75
logs**. Bit Integrations Pro 2.4.7 instalado mas **inactivo**.
- `starter` (staging, `/home/ealmeida/starter.descomplicar.pt`, prefixo BD `wpbk_`): plugin
activo, **0 flows/logs configurados** (tabelas vazias).
## Tools (9)
| Tool | Read-only | Uso |
|---|---|---|
| `bi_list_sites` | sim | Aliases conhecidos, path WP, nota de estado |
| `bi_get_plugin_status` | sim | Versões free/Pro, activo real (`wp plugin list`), db version, licença Pro (chave mascarada) |
| `bi_list_flows` | sim | Lista `btcbi_flow` — filtra por `status`/`triggered_entity`/`search` (nome), paginado. Resumo sem `flow_details` |
| `bi_get_flow` | sim | Detalhe completo de um flow, `flow_details` parseado e **mascarado** (apps ligados, trigger, mapeamento de campos) |
| `bi_set_flow_status` | **não** | Única write tool — activa/desactiva um flow (`status` 0/1 apenas, nunca trash=2). Mirror de `FlowController::updateStatus` |
| `bi_list_logs` | sim | Lista `btcbi_log` — filtra por `flow_id`/`response_type` (success\|error)/`since`, paginado, com nome do flow via JOIN |
| `bi_get_log` | sim | Detalhe completo de um log, `response_obj`/`field_data` parseados e **mascarados** |
| `bi_get_execution_stats` | sim | Contagens sucesso/falha agregadas por flow (`GROUP BY flow_id`), ordenado por volume |
| `bi_list_connections` | sim | Lista `btcbi_connections` (ligações OAuth/API reutilizáveis) — nunca `encrypt_keys`/`auth_details` |
Todas exigem `site` como primeiro parâmetro.
## Modelo de dados — resumo
Ver `references/data-model.md` para o schema completo das 4 tabelas e a forma observada de
`flow_details` por app (PerfexCRM, Google Contacts). Resumo:
- **`btcbi_flow`** — um flow por automação configurada. `triggered_entity` identifica a origem
(`WPF` = WPForms, `FluentBooking`); `flow_details` (longtext JSON) mistura **configuração**
(mapeamento de campos, condições, listas seleccionadas) e **credenciais em claro** da app
terceira (nomes de chave variam por app: `api_token`+`domain` para PerfexCRM, `clientId`+
`clientSecret`+`tokenDetails` para Google Contacts). `status`: 0 disabled, 1 enabled, 2 trashed.
- **`btcbi_log`** — um log por execução. `api_type` é um JSON pequeno (`{"type":"Lead",
"type_name":"Lead creating"}`); `response_obj` é a resposta da app terceira (por vezes HTML de
erro PHP, não JSON — fica como string); `field_data` são os valores do trigger nessa execução.
`response_type` é a string `"success"` ou `"error"` directamente na coluna (sem enum numérico).
- **`btcbi_connections`** — ligações OAuth/API reutilizáveis, independentes de um flow. Vazia nos
dois sites principais: os 10 flows de produção guardam a credencial embutida no próprio
`flow_details` em vez de usar uma connection partilhada (feature mais recente do plugin).
- **`btcbi_auth`** — armazém interno de tokens OAuth (`tokenDetails`, `userInfo`). Sem tool
dedicada — vazio nos sites verificados, e o conteúdo é puro segredo sem informação não-sensível
a expor.
## Segurança — mascaramento recursivo, não exclusão de coluna
Ao contrário do BitSocial (onde o token OAuth vive isolado numa coluna própria, nunca
seleccionada), o Bit Integrations mistura config e credenciais no mesmo blob JSON. A exclusão a
nível de SQL não chega — `src/mask.ts` percorre recursivamente o objecto já parseado e substitui
por `***MASCARADO***` qualquer valor cuja chave (normalizada, sem separadores) contenha uma
palavra da lista: `token`, `secret`, `password`, `credential`, `apikey`, `privatekey`,
`accesskey`, `encryptkeys`, `authdetails`, `userinfo`, `clientsecret`, `clientid`. `domain`
(URL da instância CRM) e `isAuthorized` (flag booleana) ficam de propósito **fora** da lista —
não são segredos e o segundo teria um falso positivo por conter "auth" como substring solta se a
lista fosse menos específica. `bi_get_plugin_status` mascara a chave de licença Pro à parte (só os
últimos 4 caracteres, `license_key_masked`). `bi_list_connections` nunca selecciona
`encrypt_keys`/`auth_details` a nível de SQL (mesma convenção do BitSocial para `custom_apps`).
Texto livre (pesquisa por nome, filtros de app) viaja como SQL escapado (`sqlString`), nunca
interpolado na linha de comando SSH. O prefixo de tabela WordPress (`wpah_`, `wpbk_`, `wpv4_`,
...) nunca é assumido por site — é sempre resolvido ao vivo via `wp db prefix` (`getWpPrefix` em
`src/db.ts`, cache em memória por processo), porque o plugin usa o prefixo curto `btcbi_` (não o
`Config::VAR_PREFIX = 'bit_integrations_'` usado só para hooks/options) e esse prefixo varia por
site.
## Verificação
Construído e testado ponta-a-ponta contra produção e staging 19-08-2026:
- `bi_get_plugin_status` em `descomplicar`: free 2.10.2 activo, Pro 2.4.7 inactivo, licença
mascarada (`********************a2be`, confere com os últimos 4 caracteres da chave real lida
por SQL directo). Em `starter`: free activo, Pro ausente.
- `bi_list_flows` em `descomplicar`: 10/10 flows — confere com `SELECT COUNT(*) FROM
wpah_btcbi_flow` directo. Em `starter`: 0/0 — confere com tabela vazia.
- `bi_get_flow` (id=4 "PerfexCRM", id=7 "Google Contacts"): `flow_details` devolvido com
`api_token`/`clientId`/`clientSecret`/`tokenDetails` mascarados e todo o resto (mapeamento de
campos, listas de clientes/staff, `domain`, `isAuthorized`) legível — confirmado visualmente
linha a linha.
- `bi_list_logs`/`bi_get_log`: filtro `flow_id=4, response_type=error` devolveu 11 logs — confere
com `bi_get_execution_stats` (flow 4: total 20, success 9, error 11) e com a soma
success=52+error=23=75 do total de logs em produção. `bi_get_log` num log de erro real mostrou o
stack trace PHP genuíno (`Unknown column 'rate_limit_checked'` no Desk CRM), sem nenhum segredo.
- `bi_list_connections`: `[]` em ambos os sites, confere com `SELECT COUNT(*)` directo.
- **Write**: `bi_set_flow_status` no flow 13 (`descomplicar`) — round-trip `active → inactive →
active`, cada transição confirmada por SQL directo fora do MCP (`status` 1→0→1,
`updated_at` avançou como esperado). Produção devolvida ao estado original (`status=1`).
## Skills relacionadas
- `mcp-bit-social` — mesmo fabricante (BitApps), mesma convenção SSH+WP-CLI+JSON_OBJECT, mas
single-site e domínio de sharing social em vez de automação.
- `mcp-wpmeteor` / `mcp-wpfc` — mesma convenção multi-site (`site` como parâmetro em cada tool,
`sites.ts` com aliases + path absoluto), mas sobre `wp_options`, não tabelas custom.
@@ -0,0 +1,141 @@
# Bit Integrations — modelo de dados completo
Confirmado por leitura directa de `wp-content/plugins/bit-integrations/backend/Core/Database/DB.php`
(schema DDL) e `backend/Flow/FlowController.php` (semântica de `status`), mais inspecção SQL ao
vivo em produção (`descomplicar.pt`) 19-08-2026.
## Prefixo de tabela
`Config::VAR_PREFIX = 'bit_integrations_'` (`backend/Config.php`) só prefixa **hooks e
`wp_options`**. As 4 tabelas de dados usam o prefixo curto e independente `btcbi_`
(`{$wpdb->prefix}btcbi_flow`, etc. — literal no `DB.php`, não deriva de `Config::VAR_PREFIX`).
`DB::fallbackDB()` mostra que o plugin já mudou de prefixo uma vez no passado (`btcfi_` → `btcbi_`,
provavelmente um rename do produto), com uma migração de `RENAME TABLE` + renomear as options
correspondentes — não há vestígios de tabelas `btcfi_*` nos sites verificados (migração já
aplicada há muito).
O prefixo WordPress em si (`$wpdb->prefix`) varia por site: `wpah_` em `descomplicar.pt`, `wpbk_`
em `starter.descomplicar.pt`, `wpv4_` em `care.descomplicar.pt`. Nunca hardcoded — `db.ts` resolve
via `wp db prefix --path=<site> --allow-root` com cache em memória por processo.
## `<prefix>btcbi_flow`
```
id bigint(20) unsigned PK auto_increment
name varchar(255) nullable
triggered_entity varchar(50) NOT NULL — origem do trigger (ex.: "WPF"=WPForms, "FluentBooking")
triggered_entity_id varchar(100) nullable — ID da origem (ex.: "16446" = ID do formulário WPForms; "fluentBooking-1")
flow_details longtext nullable — JSON, ver secção abaixo
status tinyint(1) default 1 — 0=disabled, 1=enabled, 2=trashed (comentário no schema SQL)
user_id bigint(20) unsigned nullable — utilizador WP que gravou a última alteração
user_ip int(11) unsigned nullable — IP empacotado (ip2long) de quem gravou
created_at datetime
updated_at datetime
```
`FlowController::updateStatus($id, $status)` (única mutação suportada por este MCP via
`bi_set_flow_status`) só grava `status` + `user_id` + `user_ip` + `updated_at`; o MCP replica isto
mas omite `user_id`/`user_ip` (não há utilizador WP autenticado numa sessão WP-CLI/SSH — o
`updateStatus` real do plugin corre dentro de uma sessão wp-admin).
### `flow_details` — forma observada por app (produção, 19-08-2026)
O blob mistura **configuração do mapeamento** e **credenciais em claro da app terceira** — os
nomes de chave da credencial variam por app, não há uma coluna/chave fixa a excluir. Dois
exemplos reais completos:
**PerfexCRM/Desk (flow id=4)** — `JSON_KEYS`: `name, type, api_token, domain, field_map,
actionName, selectedLeadSourceId, selectedLeadStatusId, actionId, customerFields, contactFields,
leadFields, projectFields, actions, condition, customers, perfexCRMFields, isAuthorized, staffs,
selectedStaff, trigger_type`.
- Credencial: `api_token` (string em claro) + `domain` (URL da instância, não sensível por si só).
- Config: `field_map` (array `{formField, perfexCRMFormField}`), `customers`/`staffs` (listas
cacheadas do Desk CRM para preencher dropdowns na UI), `actionName` (ex. `"lead"`).
**Google Contacts (flow id=7)** — `JSON_KEYS`: `name, type, mainAction, clientId, clientSecret,
field_map, default, allActions, actions, condition, tokenDetails, isAuthorized`.
- Credenciais: `clientId` + `clientSecret` (OAuth2 app) + `tokenDetails` (token de acesso/refresh
já trocado).
- Config: `field_map` (array `{formField, googleContactsFormField}`), `mainAction` (`"1"` =
create, `"2"` = update, ver `allActions`).
Padrão geral: `type`/`name` identificam a app; `field_map` (ou equivalente) faz o mapeamento de
campos; `isAuthorized` é uma flag booleana de UI (não uma credencial); `condition` é um bloco de
lógica condicional genérico do plugin, igual em ambos os exemplos.
## `<prefix>btcbi_log`
```
id int(11) unsigned PK auto_increment
flow_id bigint(20) nullable, indexado — FK lógica para btcbi_flow.id
job_id bigint(20) nullable — usado só por integrações assíncronas/em fila (NULL nas 75 linhas observadas)
api_type varchar(255) nullable — JSON pequeno, ex. {"type":"Lead","type_name":"Lead creating"}
response_type varchar(50) nullable — string directa "success" | "error" (SEM enum numérico)
response_obj longtext nullable — resposta da app terceira; por vezes JSON, por vezes HTML de erro PHP cru (ver exemplo abaixo)
field_data longtext nullable — valores do trigger nesta execução (adicionado numa migração posterior — coluna "after response_obj")
parent_id bigint(20) nullable, indexado — liga um log re-executado (filho) ao log original (pai)
created_at datetime NOT NULL
```
`field_data`/`parent_id` foram adicionados numa migração idempotente (`DB::addFieldDataColumn`/
`addParentIdColumn`), suportando "voltar a executar" um log falhado a partir da UI — o
`parent_id` liga a nova tentativa ao log original.
Exemplo real de `response_obj` de erro (log id=142, flow 4/PerfexCRM, `response_type="error"`):
HTML cru de exception não apanhada do CodeIgniter (`desk.descomplicar.pt`), mensagem
`Unknown column 'rate_limit_checked' in 'INSERT INTO'` — um bug real no lado do Desk CRM, não
um problema de credenciais. `parseJsonField` faz fallback para string quando o `JSON.parse` falha
(caso normal para este tipo de payload).
## `<prefix>btcbi_connections`
```
id bigint(20) unsigned PK auto_increment
app_slug varchar(191) NOT NULL, indexado
auth_type varchar(50) NOT NULL, default 'oauth2'
connection_name varchar(255) nullable
account_name varchar(255) nullable, indexado
encrypt_keys text nullable — NUNCA seleccionado pelo MCP
auth_details longtext nullable — NUNCA seleccionado pelo MCP (tokens OAuth)
status tinyint(1) default 1, indexado
user_id bigint(20) unsigned nullable
created_at datetime
updated_at datetime
```
Feature mais recente do plugin — permite reutilizar uma ligação OAuth/API entre vários flows em
vez de embutir a credencial em cada `flow_details`. **Vazia nos dois sites principais** (0 linhas
em `descomplicar` e `starter`, 19-08-2026): os 10 flows de produção são anteriores a esta feature
ou foram configurados antes de ela existir, e continuam a guardar a credencial embutida.
## `<prefix>btcbi_auth`
```
id bigint(20) unsigned PK auto_increment
action_name varchar(255) nullable
tokenDetails longtext nullable — token OAuth em claro
userInfo longtext nullable — perfil OAuth devolvido pela app terceira
created_at datetime
updated_at datetime
```
Armazém interno usado pelo fluxo OAuth (`OAuth2Authorization.php`). Vazio nos sites verificados.
Sem tool dedicada neste MCP — o único conteúdo não-segredo seria `action_name`/`created_at`, sem
valor de diagnóstico suficiente para justificar expor mais uma tabela puramente de credenciais.
## `wp_options` relevantes
| Option | Forma | Uso |
|---|---|---|
| `btcbi_version` / `btcbi_pro_version` | string | Versão instalada (free/Pro) — **preferir `wp plugin list` para "activo"**, esta option não muda quando o Pro é desactivado |
| `btcbi_db_version` / `btcbi_pro_db_version` | string | Versão do schema da BD |
| `btcbi_installed` / `btcbi_pro_installed` | int (unix timestamp) | Quando foi instalado |
| `btcbi_integrate_key_data` | array PHP (`key`, `status`, `expireIn`) | Licença Pro — `key` sempre mascarado no MCP (`license_key_masked`, últimos 4 caracteres) |
| `btcbi_webhook_<uuid>` | array PHP vazio nos sites verificados | Um por webhook registado; sem tool dedicada (nenhum webhook configurado nos sites verificados) |
| `bit_integrations_allow_tracking` etc. | escalares | Telemetria do plugin para a BitApps, sem valor de diagnóstico |
Todas as options `btcbi_*`/`bit_integrations_*` usadas por este MCP são escalares ou arrays PHP
nativos (confirmado com `wp option get <key>` sem `--format=json`, mostrando `array(...)`
desestruturado ou um valor escalar simples) — nunca uma string com JSON lá dentro. `--format=json`
é portanto seguro em leitura, sem risco de dupla codificação (ao contrário do WP Fastest Cache,
ver skill `wp-fastest-cache`).
+81
View File
@@ -0,0 +1,81 @@
---
name: mcp-bit-social
description: MCP dedicado (node stdio, ligação `bit-social` em ~/.omp/agent/mcp.json) para o BitSocial/BitSocial Pro (BitApps) em descomplicar.pt — a automação que publica os artigos do blog em Facebook, LinkedIn, Pinterest e Tumblr. Contas sociais ligadas, schedules de auto-post e "Share Now", logs de publicação, resumo de falhas por plataforma, analytics, estado da licença. Usar quando "bitsocial", "porque falhou a publicação no Pinterest/Facebook/LinkedIn", "contas sociais ligadas", "logs de publicação social", "auto-post settings", "schedule bitsocial", "desactivar conta social", "licença bitsocial pro".
layer: wiki
---
# /mcp-bit-social — MCP dedicado BitSocial / BitSocial Pro
Projecto em `/media/ealmeida/Dados/Dev/mcp-bit-social/` (TypeScript, SDK MCP oficial, stdio).
Independente porque as rotas HTTP do próprio plugin (`backend/routes/*.php`) exigem sessão
wp-admin autenticada + nonce (`Route::noAuth()->middleware('nonce:admin')`) — sem caminho headless
razoável. Sem código PHP novo no WordPress: cada tool corre `wp db query`/`wp option get` via
`ssh server` sobre o WP-CLI já instalado, contra o schema `wpah_bit_social_*` e as `wp_options` do
plugin.
## Âmbito
Diagnóstico e gestão de **contas, schedules e logs de publicação** — o domínio onde as falhas
reais acontecem (produção tem 175 schedules, 1825 logs, ~50% falha histórica em Pinterest por
imagem em falta). Não cobre a UI de composição/edição de templates de post nem o fluxo OAuth de
ligar uma conta nova — isso continua a fazer-se no wp-admin.
## Tools (12)
**Estado e analytics (read-only):**
- `bs_get_plugin_status` — versões free/pro, db versions, licença Pro (chave mascarada), cron externo.
- `bs_get_analytics` — contas activas, schedules activos, publicações OK/falhadas (soma de logs).
**Contas:**
- `bs_list_accounts` (read-only) — filtra por `platform`/`status`. Nunca devolve `details`
(contém o access_token OAuth em claro).
- `bs_set_account_status` — única write tool; liga/desliga uma conta (mirror 1:1 de
`AccountController::updateStatus`, só a coluna `status`). Uma conta desactivada sai da rotação
de auto-post/share-now sem perder a ligação OAuth.
**Configuração (read-only):**
- `bs_list_custom_apps` — apps de API próprias (nunca `credential`, segredo cifrado).
- `bs_list_groups` — grupos de contas (Pro), com as contas membro.
- `bs_get_auto_post_settings` — `bit_social_auto_post_settings`: post types/taxonomias que
disparam auto-post, contas/grupos alvo, atraso de publicação.
- `bs_get_social_templates` — `bit_social_templates_settings`: template de conteúdo por
plataforma (imagem destacada / link card / só texto).
**Schedules e diagnóstico (read-only):**
- `bs_list_schedules` — `schedule_type=schedule_share` (auto-post recorrente) e `direct_share`
("Share Now") vivem na mesma tabela; filtra por status/tipo/nome, paginado.
- `bs_get_schedule` — detalhe completo de um schedule, incluindo `config` (filtros de posts,
contas alvo, templates) e `published_post_ids`.
- `bs_list_logs` — um log por (schedule × plataforma) tentado; `details` inclui o erro da
plataforma em caso de falha. Ferramenta principal de diagnóstico.
- `bs_get_failure_summary` — falhas dos últimos N dias agrupadas por plataforma, até 5 mensagens
de erro distintas por plataforma.
## Segurança
Nunca selecciona `accounts.details` (access_token OAuth em claro) nem `custom_apps.credential`
(segredo cifrado). `bs_get_plugin_status` mascara a chave de licença Pro (só os últimos 4
caracteres). Texto livre (pesquisa por nome) viaja como SQL escapado (`sqlString`), nunca
interpolado na linha de comando SSH.
## Gotcha de infra-estrutura
`wp db query` shella para o `mysql` CLI, que em modo batch **re-escapa backslashes** nos valores —
sem a flag `--raw`, qualquer `JSON_OBJECT`/`JSON_ARRAYAGG` que aninhe uma coluna já-JSON (ex.:
`config`, `details`) fica com JSON inválido (`\"` vira `\\"`). O MCP já corre sempre com `--raw`;
relevante só ao escrever queries SQL BitSocial novas fora deste MCP.
## Verificação
Construído e testado ponta-a-ponta contra produção 19-08-2026: as 12 tools exercitadas com dados
reais (4 contas, 175 schedules, 1825 logs), incluindo o caso de falha real recorrente
("Must have image for create pin in Pinterest.", 39 ocorrências/60 dias) e um round-trip de
escrita idempotente em `bs_set_account_status` (confirmado no `updated_at` da BD).
## Recursos adicionais
- **`references/data-model.md`** — schema completo das tabelas `wpah_bit_social_*`, enums
(`Schedule::status`/`scheduleType`, `Account::accountType`), forma dos campos JSON aninhados
(`config`, `details`, `bit_social_auto_post_settings`, `bit_social_templates_settings`).
- **`references/diagnostics.md`** — playbook: investigar uma publicação falhada, pausar uma conta
sem a desligar, ver o que está configurado para auto-post.
@@ -0,0 +1,126 @@
# BitSocial — modelo de dados (confirmado em produção, 2026-08-19)
Prefixo real de tabela em descomplicar.pt: `wpah_` (WP table prefix) + `bit_social_` (`Config::VAR_PREFIX`).
Todas as tabelas usam `Schema::withPrefix(...)`, `timestamps()` standard (`created_at`/`updated_at`).
## Tabelas
### `wpah_bit_social_accounts` (4 linhas)
| Coluna | Tipo | Notas |
|---|---|---|
| `id` | bigint PK | |
| `custom_app_id` | bigint, nullable, FK → `custom_apps.id` | null quando usa a app proxy da BitApps |
| `profile_id` | string | ID do perfil/utilizador na plataforma |
| `account_id` | string | ID da conta/página na plataforma (ex.: Facebook page id) |
| `account_name` | string | |
| `details` | longtext (JSON) | **NUNCA seleccionar** — contém `access_token` OAuth em claro, `icon`, `category`, etc. |
| `platform` | string | `facebook`, `linkedin`, `pinterest`, `tumblr`, `instagram`, `twitter`, `discord`, `threads`, `tiktok`, `bluesky`, `telegram`, `googleBusinessProfile`, … |
| `account_type` | int | `Account::accountType`: `DEFAULT=1` (app proxy BitApps), `CUSTOM=2` (app própria), `AI_PLATFORM=3` |
| `status` | int (bool) | `1` activo, `0` inactivo |
### `wpah_bit_social_custom_apps` (1 linha)
| Coluna | Tipo | Notas |
|---|---|---|
| `id` | bigint PK | |
| `name` | string | |
| `platform` | string | |
| `credential` | longtext | **NUNCA seleccionar** — segredo de app cifrado (base64 de payload AES) |
| `status` | int (bool) | |
### `wpah_bit_social_schedules` (175 linhas)
Cobre **dois fluxos diferentes na mesma tabela**, distinguidos por `schedule_type`:
| Coluna | Tipo | Notas |
|---|---|---|
| `id` | bigint PK | |
| `name` | string | Ex.: `"Auto Post - Post ID: 60261"` (auto-post) |
| `config` | longtext (JSON) | Ver forma abaixo. Coluna aninhada — precisa de segundo `JSON.parse` no lado do cliente. |
| `published_post_ids` | longtext (JSON, array de ints) | Posts já publicados por este schedule |
| `repeat_schedule` | bool | |
| `schedule_type` | int | `Schedule::scheduleType`: `SCHEDULE_SHARE=1` (auto-post recorrente/agendado), `DIRECT_SHARE=2` ("Share Now") |
| `status` | int | `Schedule::status`: `INACTIVE=0`, `ACTIVE=1`, `COMPLETED=2`, `DRAFT=3`, `MISSED=4` |
| `cron_status` | int | `INACTIVE=0`, `ACTIVE=1` |
| `started_at`, `ended_at`, `last_published_at`, `next_published_at` | timestamp, nullable | |
Forma real de `config` (exemplo de um schedule de auto-post):
```json
{
"settings": { "started_at": "2026-08-11 11:50:34" },
"post_filters": { "post_type": "post", "specific_postIds": [60261] },
"accounts": { "accountIds": [1, 4, 5, 6], "groupIds": [] },
"templates": {
"facebook": { "postingType": "isFeaturedImage", "content": "{post_title}", "trimMessage": false },
"pinterest": { "postingType": "isLinkCard", "content": "{post_title}", "trimMessage": true, "isLinkCard": false },
"...": "uma entrada por plataforma, mesma forma que bit_social_templates_settings"
}
}
```
### `wpah_bit_social_logs` (1825 linhas)
Uma linha por tentativa de publicação (schedule × plataforma).
| Coluna | Tipo | Notas |
|---|---|---|
| `id` | bigint PK | |
| `schedule_id` | bigint, nullable, FK → `schedules.id` | |
| `details` | longtext (JSON) | Ver forma abaixo |
| `platform` | string | |
| `status` | int (bool) | `1` sucesso, `0` falha — confirmado em `AnalyticsController::index` (`Log::where('status', true)`) |
Forma de `details` em sucesso:
```json
{
"account_id": "164743196715367",
"account_name": "Descomplicar - Agência de Aceleração Digital",
"response": { "id": "164743196715367_122245726064096762", "post_supports_client_mutation_id": true },
"post_id": 60261,
"post_url": "https://fb.com/164743196715367_122245726064096762"
}
```
Forma de `details` em falha (o caso real mais comum, Pinterest):
```json
{
"account_id": "1051238806710440771",
"account_name": "Blog Descomplicar",
"post_id": 60261,
"response": { "status": "error", "error_msg": "Must have image for create pin in Pinterest." },
"post_url": null
}
```
O erro está sempre em `details.response.error_msg` nos casos observados; `extractErrorMessage()`
no MCP também tenta `response.message`, `error`, `message` como fallback para plataformas com
forma diferente.
### `wpah_bit_social_groups` / `wpah_bit_social_groups_accounts` (Pro, 0 linhas em produção)
`groups`: `id`, `name`, `status`. `groups_accounts`: pivot `group_id` × `account_id`, cascade on
delete. Um grupo pode substituir uma lista explícita de `accountIds` num `config.accounts.groupIds`.
## `wp_options` relevantes
| Chave | Conteúdo |
|---|---|
| `bit_social_version` / `bit_social_pro_version` | string, ex. `"1.16.0"` |
| `bit_social_db_version` / `bit_social_pro_db_version` | string |
| `bit_social_installed` / `bit_social_pro_installed` | bool |
| `bit_social_auto_post_settings` | `{isEnabled, keepLogs, taxonomies[], accounts:{accountIds[],groupIds[]}, postType[], postDelay:{every,unit}}` |
| `bit_social_templates_settings` | um objecto por plataforma, mesma forma que `config.templates` num schedule |
| `bit_social_pro_settings` | `{cron:{isExternalCronEnabled}}` |
| `bit_social_pro_license_data` | `{key, status, expireIn}` — `key` **nunca** deve ser devolvida sem máscara |
| `bit_social_secret_key` | segredo interno do plugin — nunca ler/expor |
## Análogo AnalyticsController (referência, não reimplementação 1:1)
`AnalyticsController::index` só calcula `active_account_count`, `published_post_count` (logs com
`status=true`) e `active_schedule_count`. `bs_get_analytics` estende com `failed_post_count`
(logs com `status=0`) porque o controller original omite essa contagem apesar de ser a métrica
mais accionável.
@@ -0,0 +1,54 @@
# BitSocial — playbook de diagnóstico
## Investigar porque uma publicação falhou
1. Encontrar o schedule pelo nome do post (`bs_list_schedules({search: "Post ID: 60261"})`) ou
pelo id se já conhecido.
2. `bs_get_schedule({schedule_id})` — confirmar `status` (`completed`/`missed`/…), `config.accounts`
(que contas estavam alvo) e `config.templates` (que tipo de conteúdo cada plataforma ia usar,
ex. `postingType: "isLinkCard"` para Pinterest).
3. `bs_list_logs({schedule_id})` — uma linha por plataforma tentada; `status: 0` é falha,
`details.response.error_msg` tem a razão devolvida pela API da plataforma.
4. Cruzar com `bs_list_accounts({platform})` para confirmar que a conta ainda está `active` — uma
conta desligada não é a causa de uma falha já registada em log (o log só existe se a tentativa
chegou a correr), mas explica ausência de tentativa nenhuma.
## Falha recorrente conhecida: Pinterest sem imagem
`"Must have image for create pin in Pinterest."` é o erro mais comum em produção (39 ocorrências
em 60 dias, confirmado com `bs_get_failure_summary({days: 60})`). Causa: o `postingType` do
template Pinterest (`bs_get_social_templates`) está como `isLinkCard`
(`"isLinkCard": false` no exemplo real — nome do campo enganador, o efectivo é o `postingType`)
mas o post de origem não tem imagem destacada elegível para a Pinterest API. Não é um bug do
MCP nem do plugin — é um requisito da API do Pinterest (todo pin precisa de imagem). Diagnóstico
correcto: confirmar se o post WordPress em causa tem featured image antes de o auto-post disparar;
o MCP não cobre a correcção (isso é conteúdo/publicação, fora do âmbito de `mcp-bit-social`).
## Ver o que está configurado para disparar auto-post
`bs_get_auto_post_settings` — confirma `isEnabled`, que `postType`s (ex.: `["post", "podcast"]`) e
taxonomias disparam publicação automática, atraso (`postDelay`) e que contas/grupos são alvo por
omissão. Cruzar com `bs_get_social_templates` para ver o formato de conteúdo por plataforma.
## Pausar uma conta sem a desligar (perder OAuth)
`bs_set_account_status({account_id, status: "inactive"})` — sai da rotação de auto-post e "Share
Now" imediatamente (mirror de `AccountController::updateStatus`), sem apagar a ligação OAuth
(`accounts.details` continua intacto). Reactivar com o mesmo tool e `status: "active"`.
Não confundir com apagar a conta (não coberto por este MCP — fazer via wp-admin se necessário,
apagar tem cascade sobre `groups_accounts` e pode invalidar schedules já criados que a referenciam).
## Saúde geral rápida
`bs_get_plugin_status` (versões/licença) + `bs_get_analytics` (contas activas, schedules activos,
publicações OK/falhadas) — dois tools, sem argumentos, para um snapshot inicial antes de investigar
mais fundo.
## Fora do âmbito deste MCP (fazer via wp-admin)
- Repetir uma publicação falhada (`RetryController` faz um pedido HTTP real à plataforma social —
lógica de negócio, não um `UPDATE` SQL).
- Mudar o estado de um schedule directamente (`ScheduleController::updateStatus` tem checks
condicionais de `COMPLETED`/`repeat` que não são um simples `UPDATE status=…`).
- Ligar uma conta social nova (fluxo OAuth) ou criar uma custom app.
- Editar o conteúdo/template de publicação por plataforma.
+109
View File
@@ -0,0 +1,109 @@
---
name: mcp-click-to-chat
description: MCP dedicado multi-site (node stdio, ligação `click-to-chat` em ~/.omp/agent/mcp.json) para gerir o Click to Chat for WhatsApp (HoliThemes) em qualquer site do bundle Descomplicar® via WP-CLI/SSH — estado do plugin, config completa, número de WhatsApp, mensagem pré-preenchida, call-to-action, activar/desactivar o widget globalmente, posição e estilo do botão flutuante. Site é um parâmetro em cada tool, não uma ligação fixa. Usar quando "click to chat mcp", "número whatsapp site", "widget whatsapp wordpress", "botão flutuante whatsapp", "mensagem pré-preenchida whatsapp", "ht_ctc_chat_options", "desactivar botão whatsapp", "posição botão whatsapp".
layer: wiki
---
# /mcp-click-to-chat — MCP dedicado multi-site ao Click to Chat for WhatsApp
Projecto em `/media/ealmeida/Dados/Dev/mcp-click-to-chat/` (TypeScript, SDK MCP oficial, stdio).
Sem código PHP novo no WordPress — cada tool executa `wp option get/update --format=json` via
`ssh server` sobre o WP-CLI já instalado. **`site` é um parâmetro em cada tool**, não uma ligação
fixa: o plugin está espalhado por vários sites reais do bundle com dados diferentes por site.
Não existe skill de conhecimento separada para este plugin — esta skill É a documentação
completa, incluindo o mapeamento de options. O plugin tem duas interfaces internas ("new" 2019+ e
"prev" legada, ver `common/class-ht-ctc-switch.php`); **todos os sites do bundle usam a interface
"new"** (a legada só se activa se a option `ccw_options` tiver a chave `number`, o que não
acontece em nenhum site verificado) — por isso este MCP só lê/escreve as options `ht_ctc_*`.
## Âmbito
Config do widget flutuante de WhatsApp (versão **Free** — nenhum site do bundle tem o addon PRO
`click-to-chat-pro.php` instalado, por isso não há múltiplos agentes/departamentos, apenas um
número principal). Não cobre: activação/desactivação do próprio plugin (`emcp-tools
activate-plugin`/`deactivate-plugin` cobre isso genericamente), estilos de cor detalhados por
variante (`ht_ctc_s1`..`ht_ctc_s9` — cosméticos por `style_desktop`/`style_mobile` escolhido, fora
do âmbito deste MCP), integração de Analytics/GTM/Pixel (`ht_ctc_othersettings`), Grupos de
WhatsApp ou Share button (`ht_ctc_group`/`ht_ctc_share`), WooCommerce (`ht_ctc_woo_options`).
## Sites conhecidos
Chamar `ctc_list_sites` para a lista actual com path e nota de estado. Verificado ao vivo
19-08-2026 (`wp plugin list` por site, slug `click-to-chat-for-whatsapp` 4.42.1, update 4.43
disponível em todos):
- **Activos:** `emanuelalmeida` (número real configurado, `+351919109155`), `starter` (activo mas
sem número configurado — config vazia), `ccv` (número real configurado), `ecommerce` (activo).
- **Instalado mas inactivo:** `descomplicar` (produção, `public_html`), `ecommerce-demo`.
- **Não instalado:** `care`, `e-commerce`.
Também aceita um path absoluto directamente (`/home/ealmeida/<site>`) para sites fora desta lista,
sem exigir rebuild do MCP.
## Mapeamento de dados
Option principal: **`ht_ctc_chat_options`** (array PHP nativo — ver §Segurança). Campos usados por
este MCP:
| Campo | Tool que lê/escreve | Descrição |
|---|---|---|
| `number` | `ctc_get_number` / `ctc_update_number` | Número de WhatsApp principal, com indicativo. Legado: `cc`+`num` (interface "prev", sempre limpos por `ctc_update_number`) |
| `pre_filled` | `ctc_get_number` / `ctc_update_message` | Mensagem já escrita na conversa ao clicar |
| `call_to_action` | `ctc_get_number` / `ctc_update_message` | Texto do tooltip/balão junto ao botão |
| `display.global_display` | `ctc_get_config` / `ctc_toggle_widget` | `show`/`hide` — liga/desliga o widget em todo o site |
| `display.home`/`pages`/`posts`/`list_showon_pages`/etc. | `ctc_get_config` (só leitura) | Excepções por tipo de página ao valor global — não alteradas por este MCP |
| `style_desktop`/`style_mobile` | `ctc_get_config` / `ctc_update_position_style` | Id do estilo pré-definido do botão, `"1"`–`"9"` (paleta de cada um em `ht_ctc_s1`..`ht_ctc_s9`, fora do âmbito) |
| `side_1`/`side_1_value` | `ctc_get_config` / `ctc_update_position_style` | Lado vertical (`top`/`bottom`) + offset CSS (ex. `"15px"`) |
| `side_2`/`side_2_value` | `ctc_get_config` / `ctc_update_position_style` | Lado horizontal (`left`/`right`) + offset CSS |
| `mobile_side_*`, `same_settings` | `ctc_get_config` (só leitura) | Posição mobile — segue a desktop enquanto `same_settings` = `"1"` (comportamento nativo) |
## Tools (8)
| Tool | Read-only | Uso |
|---|---|---|
| `ctc_list_sites` | sim | Aliases conhecidos, path WP, nota de estado |
| `ctc_get_status` | sim | Instalado/activo, versão, update disponível — chamar antes dos outros tools |
| `ctc_get_config` | sim | Option `ht_ctc_chat_options` completa (número, mensagem, posição, estilo, regras de exibição) |
| `ctc_get_number` | sim | Só `number`, `pre_filled`, `call_to_action`, `intl_country` — resposta enxuta. Free = um único número, sem multi-agente |
| `ctc_update_number` | não | Actualiza `number` com validação (8-15 dígitos após limpeza, igual a `HT_CTC_Formatting::wa_number()` do próprio plugin). Limpa `cc`/`num` legados |
| `ctc_update_message` | não | Actualiza `pre_filled` e/ou `call_to_action` — passa só o(s) campo(s) a mudar |
| `ctc_toggle_widget` | não | `enabled: true/false` → `display.global_display` = `show`/`hide`. Não toca nas excepções por página |
| `ctc_update_position_style` | não | `style_desktop`/`style_mobile` (`"1"`-`"9"`) e/ou `side_1`/`side_1_value`/`side_2`/`side_2_value` — passa só o(s) campo(s) a mudar |
Todas as tools de escrita fazem read-modify-write (merge), nunca substituição completa da option.
`site` é obrigatório em todas.
## Segurança
Allowlist fixa de 3 nomes de option `ht_ctc_*` relevantes (`src/wpcli.ts`, `OPTION_KEYS`) — nunca
`wp option update` genérico sobre uma chave arbitrária. Escritas via base64 em stdin, nunca
interpoladas no comando SSH remoto. Validação de número de WhatsApp em `ctc_update_number`
(mínimo 8, máximo 15 dígitos após remover tudo excepto dígitos).
**Formato de armazenamento confirmado (caso seguro):** `ht_ctc_chat_options` e as restantes
options `ht_ctc_*` são gravadas pelo próprio plugin via `update_option( 'ht_ctc_xxx', $array_php
)` — array PHP nativo, não uma string com JSON lá dentro (confirmado lendo
`new/admin/db/class-ht-ctc-db.php`, e comparando ao vivo a saída de `wp option get` sem formato
vs. `--format=json` em `emanuelalmeida.pt` — ambas descodificam o mesmo array, sem dupla
codificação). Por isso `getOption`/`setOption` deste MCP usam `--format=json` directamente, ao
contrário do `mcp-wpfc` (ver essa skill para o caso inverso, o gotcha de string-com-JSON-lá-dentro
que corrompeu produção nesse plugin).
## Verificação
Construído e testado ponta-a-ponta contra produção 19-08-2026: `ctc_get_status`/`ctc_get_config`/
`ctc_get_number` confirmaram os dados reais em `emanuelalmeida` (número `+351919109155`,
call-to-action "Vamos conversar?"); `ctc_update_message` (call-to-action idempotente),
`ctc_toggle_widget` (`enabled:true` idempotente) e `ctc_update_position_style` (valores
idempotentes) testados em `ccv`, cada um confirmado por leitura SSH directa antes/depois —
`ht_ctc_chat_options` ficou byte-a-byte igual, incluindo o objecto aninhado `display`;
`ctc_update_number` testado em `starter` com um número real (`+351 910 000 000` → normalizado para
`+351910000000`, confirmado por leitura SSH directa), depois restaurado ao estado original vazio
(também confirmado por leitura).
## Skills relacionadas
- `mcp-wpfc` — mesmo padrão de arquitectura, mas com o gotcha inverso de formato de armazenamento
(string PHP com JSON lá dentro) — ler antes de construir um MCP novo sobre outro plugin.
- `emcp-tools` — activação/desactivação genérica de plugins, fora do âmbito deste MCP.
@@ -0,0 +1,90 @@
---
name: mcp-cloudflare-app
description: MCP dedicado (HTTP StreamableHTTP local, ligação `cloudflare-app` em ~/.claude.json, porta 3201) para a zona Cloudflare gerida pelo plugin App for Cloudflare® (`app-for-cf`) em descomplicar.pt — definições de zona SSL/TLS/Speed/Caching/Network/Scrape Shield, Bot Management + Super Bot Fight Mode, WAF via Managed Ruleset, HSTS, Certificate Transparency, Leaked Credential Checks, purge de cache. Fala directamente com a Cloudflare API v4, sem passar pelo EMCP Tools nem pelo WordPress em runtime. Usar quando "waf cloudflare vazio", "bot management ai", "purge cache cloudflare", "hsts cloudflare", "managed ruleset", "is_robots_txt_managed", "certificate transparency cloudflare", "leaked credential checks", ou qualquer alteração de definição de zona Cloudflare via MCP em vez de curl manual.
layer: wiki
---
# /mcp-cloudflare-app — MCP dedicado à zona Cloudflare (App for Cloudflare®)
Projecto em `/media/ealmeida/Dados/Dev/mcp-cloudflare-app/` (TypeScript, SDK MCP oficial,
StreamableHTTP). Não é uma extensão do EMCP Tools nem fala com o WordPress em runtime — o plugin
App for Cloudflare® não expõe REST próprio (confirmado por leitura completa do código-fonte, ver
skill `app-for-cloudflare`). Toda a configuração de zona vive na Cloudflare API v4, não em
`wp_options`; este MCP fala directamente com essa API usando um token extraído uma vez de
`wp_options.app_for_cf.cloudflareAuth.token` e guardado em `.env` local (não em cada chamada).
**Relação com a skill `app-for-cloudflare`:** essa skill é a fonte de conhecimento (mapeamento
completo plugin↔Cloudflare API, gotchas, scopes de token, as ~52 definições de zona). Esta skill é
o "como executar" — qual tool chamar para cada operação. Consultar `app-for-cloudflare` para
entender uma definição; usar as tools abaixo para lê-la/escrevê-la.
## Âmbito
Zona actual configurada: **descomplicar.pt**. Para gerir outra zona do bundle, extrair
`cloudflareAuth.token`/`cfZoneId` de `wp_options.app_for_cf` desse site (SQL read-only, ver
"Credenciais" abaixo) e actualizar o `.env` do MCP — não há multi-zona numa única ligação.
Não cobre: R2 media storage, Zero Trust Access, Page/Cache Rules, Firewall Rules custom, DMARC,
Web Analytics, ferramentas de diagnóstico (request trace/IP/domain/WHOIS) — todas Pro-only na UI do
plugin ou fora do âmbito "definições de zona + segurança + purge" decidido na criação do MCP
(19-08-2026). Gerir manualmente via dashboard Cloudflare ou via curl directo (ver skill
`app-for-cloudflare` §10/§2 para os endpoints).
## Tools (14)
| Tool | Read-only | Uso |
|---|---|---|
| `cf_list_zone_settings` | sim | Lê todas as definições de zona em bloco |
| `cf_get_zone_setting` | sim | Lê uma definição por id (ex. `rocket_loader`, `cache_level`) |
| `cf_update_zone_setting` | não | Escreve uma definição por id — bloqueia ids de bot_management/`waf` com redirect accionável para a tool certa |
| `cf_get_bot_management` | sim | Lê Bot Management/SBFM completo (inclui campos fora da UI do plugin: `ai_bots_protection`, `is_robots_txt_managed`), sinaliza risco GEO |
| `cf_update_bot_management` | não | Escreve Bot Management/SBFM — merge parcial (lê, aplica só os campos dados, PUT completo; remove `using_latest_model` do payload automaticamente) |
| `cf_get_waf_status` | sim | Verifica se a fase `http_request_firewall_managed` tem regras `execute` reais (o toggle legado `waf` não garante protecção) |
| `cf_deploy_managed_ruleset` | não | Implanta o Cloudflare Managed Free Ruleset na fase managed — disponível em qualquer plano |
| `cf_get_hsts_settings` / `cf_update_hsts_settings` | sim/não | HSTS (`security_header`) |
| `cf_get_certificate_transparency` / `cf_update_certificate_transparency` | sim/não | Alertas CT (`ct/alerting`) |
| `cf_get_leaked_cred_checks` / `cf_update_leaked_cred_checks` | sim/não | Leaked Credential Checks |
| `cf_purge_cache` | não | Purga tudo (omitir `files`) ou até 30 URLs específicos |
Padrão Descomplicar® para Bot Management (GEO-crítico — ver skill `app-for-cloudflare` §5a):
`cf_update_bot_management` com `{"ai_bots_protection":"disabled","content_bots_protection":"disabled","crawler_protection":"disabled","ai_training":"disabled","ai_search":"disabled","ai_user":"disabled","fight_mode":false,"is_robots_txt_managed":false}`
— nunca aceitar o "good" que o próprio plugin recomenda para `ai_bots_protection`/`crawler_protection`
(`block`/`enabled`), é o oposto da decisão Descomplicar.
## Segurança
Escritas em `bot_management` fazem sempre GET→merge→PUT (nunca um PUT parcial cego — a Cloudflare
substitui o objecto inteiro). `cf_update_zone_setting` recusa explicitamente ids que pertencem ao
endpoint `bot_management` (`waf`, `bot_fight_mode`, `ai_bots_protection`, `crawler_protection`,
`bot_likely_automated`, `bot_definitely_automated`, `bot_verified_bots`,
`bot_static_resource_protection`, `bot_optimize_wordpress`, `bot_enable_js`) e devolve `isError`
com a tool certa a usar, em vez de deixar a Cloudflare rejeitar com `10405 Method not allowed`.
## Credenciais
`.env` local (`CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ZONE_ID`, `CLOUDFLARE_ACCOUNT_ID`,
`CLOUDFLARE_ZONE_NAME`), não versionado. Reextrair para outro site via SQL read-only (mais rápido
que SSH+WP-CLI se já houver uma ligação EMCP a esse site):
```sql
SELECT option_value FROM <prefixo>_options WHERE option_name = 'app_for_cf';
```
O valor é um array PHP serializado — extrair `cloudflareAuth.token` e `cfZoneId`. Confirmar o
prefixo real da tabela primeiro (`SHOW TABLES LIKE '%options%'`; em descomplicar.pt é `wpah_`, não
`wp_`).
## Verificação
Construído e testado ponta-a-ponta contra a zona real `descomplicar.pt` 19-08-2026: 12/12
evaluations (`npm run eval:ci`), incluindo leitura real que confirmou dois achados de produção
ainda por corrigir — WAF Managed Ruleset vazio (zero regras `execute`) e HSTS desligado. Bot
Management já correcto (sem risco GEO). Deploy como processo `hub` persistente (`mcp-cloudflare-app`,
porta 3201, `http://127.0.0.1:3201/mcp`), registado em `~/.claude.json` como `cloudflare-app`.
## Skills relacionadas
- `app-for-cloudflare` — mapeamento completo plugin↔Cloudflare API v4, gotchas, scopes de token,
as ~52 definições de zona, EasyConfig, WAF, Bot Management, HSTS, o modelo `wp_options.app_for_cf`.
- `wordfence` — se a decisão for usar Wordfence em vez do WAF Cloudflare como camada primária, é essa
a skill certa para regras/bloqueios ao nível da aplicação (este MCP só cobre o WAF de edge).
+42
View File
@@ -0,0 +1,42 @@
---
name: mcp-complianz
description: MCP dedicado ao Complianz GDPR para auditoria e gestão de cookies, consentimento, banner e serviços de terceiros na frota CWP
version: 1.0.0
author: Descomplicar®
---
# MCP Complianz GDPR (`mcp-complianz`)
> Servidor MCP StreamableHTTP (porta 3208) para auditoria e gestão de privacidade, consentimento de cookies e políticas legais (**Complianz GDPR** v7.5+) nos sites da frota WordPress no CWP.
---
## 1. Ferramentas Disponíveis
| Ferramenta | Descrição | Parâmetros |
|---|---|---|
| `complianz_list_supported_sites` | Lista os sites com Complianz GDPR na frota. | `{}` |
| `complianz_get_status` | Estado geral do plugin (política ativa, datas de scan/sync, contagem de cookies e serviços). | `site?` |
| `complianz_list_cookies` | Lista todos os cookies detetados na base de dados (nome, finalidade, retenção, domínio). | `site?`, `limit?`, `offset?` |
| `complianz_list_services` | Lista os serviços e tecnologias de terceiros detetados (Google, Elementor, Wordfence, etc.). | `site?` |
| `complianz_get_banner_settings` | Configuração do banner de consentimento de cookies (posição, textos, botões, cores). | `site?` |
---
## 2. Exemplo de Utilização
```typescript
// Obter estado e contagem de cookies/serviços
const status = await use_mcp_tool({
server_name: "complianz",
tool_name: "complianz_get_status",
arguments: { site: "starter.descomplicar.pt" }
});
// Listar cookies detetados
const cookies = await use_mcp_tool({
server_name: "complianz",
tool_name: "complianz_list_cookies",
arguments: { site: "starter.descomplicar.pt", limit: 20 }
});
```
@@ -0,0 +1,49 @@
---
name: mcp-element-pack
description: MCP dedicado (node stdio, ligação `element-pack` em ~/.omp/agent/mcp.json) para gerir o Element Pack Pro (BdThemes) em descomplicar.pt — licença (read-only), módulos, mega menu, white-label, custom CSS/JS, via WP-CLI/SSH sem passar pelo EMCP Tools. Usar quando "element pack licença", "módulos element pack", "mega menu toggle", "white label element pack", "activar/desactivar widget element pack".
layer: wiki
---
# /mcp-element-pack — MCP dedicado Element Pack Pro (admin settings)
Projecto em `/media/ealmeida/Dados/Dev/mcp-element-pack/` (TypeScript, SDK MCP oficial, stdio).
Não é uma extensão do EMCP Tools — é um MCP independente porque o EMCP Tools é plugin comercial
(Freemius) com catálogo de tools fixo, curado pelo autor, sem mecanismo de terceiros registarem
abilities visíveis no `list-tools`. Sem código PHP novo no WordPress: cada tool executa
`wp option get/update` via `ssh server` sobre o WP-CLI já instalado.
## Âmbito
Gestão de **configuração admin** do Element Pack Pro. Para **inserir/editar widgets** em páginas
Elementor (qualquer um dos 168+95 catalogados), usar a skill `element-pack-widgets` via ligação EMCP
normal — este MCP não cobre isso.
## Tools (9)
- `ep_get_license_status` (read-only) — licença nunca é escrita por aqui; activação exige handshake
com a API da BdThemes (`Element_Pack_Base::get_lic_key_param`), não é um simples `option update`.
- `ep_get_modules` / `ep_set_module_state` — grupos `active_modules`, `third_party_widget`,
`elementor_extend`, `other_settings`; read-modify-write, nunca substitui o array inteiro.
- `ep_get_mega_menu_toggle` / `ep_set_mega_menu_toggle` — só o toggle global
(`element_pack_other_settings.mega-menu`); edição por item de menu (postmeta `_ep_mega_menu_*`)
fica fora do âmbito v1.
- `ep_get_white_label` / `ep_set_white_label`
- `ep_get_custom_code` / `ep_set_custom_code` — blocos `ep_custom_css`/`_js` (1 e 2).
## Segurança
Allowlist fixa de option keys (`src/wpcli.ts`, `element_pack_*`/`ep_*`) — nunca `wp option update`
genérico. Escritas via base64 em stdin, nunca interpoladas no comando SSH remoto.
## Verificação
Construído e testado ponta-a-ponta contra produção 19-08-2026: leitura real (`ep_get_license_status`
→ `it@descomplicar.pt`; `ep_get_mega_menu_toggle` → `off`, ambos confirmados contra `wp option get`
directo) e escrita real com round-trip (`ep_set_custom_code`/`ep_get_custom_code` num slot vazio,
sem alterar comportamento do site).
## Skills relacionadas
- `element-pack-widgets` — catálogo de widgets e workflow de inserção via EMCP (`get-widget-schema`
com `full:true`).
- `full-cliente` — se a licença Element Pack Pro algum dia passar a ser gerida via FULL.Cliente em
vez de licença directa BdThemes, é essa a skill certa, não esta.
@@ -0,0 +1,86 @@
---
name: mcp-fluent-booking
description: MCP dedicado (node stdio, ligação `fluent-booking` em ~/.omp/agent/mcp.json) para o FluentBooking/FluentBooking Pro (WPManageNinja) em descomplicar.pt — a agenda de marcação de reuniões (`/marcar-reuniao/`). Calendários, reservas/marcações, disponibilidade, reports, cancelar/reagendar. Usar quando "fluent booking", "marcações", "reuniões agendadas", "quem marcou reunião", "cancelar reserva", "reagendar reunião", "disponibilidade calendário", "reservas fluentbooking", "reenviar confirmação de reunião", "licença fluent booking pro".
layer: wiki
---
# /mcp-fluent-booking — MCP dedicado FluentBooking / FluentBooking Pro
Projecto em `/media/ealmeida/Dados/Dev/mcp-fluent-booking/` (TypeScript, SDK MCP oficial, stdio).
Ao contrário de `mcp-bit-social`/`mcp-element-pack`, **não usa SSH/WP-CLI para os dados de
negócio** — o FluentBooking regista uma REST API real (`register_rest_route` sob
`fluent-booking/v2`, confirmado em `vendor/wpfluent/framework/.../Route.php`) com
`permission_callback` baseado em capabilities (`current_user_can`), não em nonce de sessão
wp-admin. Autentica com uma **Application Password** WordPress (Basic Auth), e cada escrita passa
pela lógica de negócio real do plugin (emails, activity log, side-effects de pagamento) em vez de
um `UPDATE` SQL directo.
## Auth
Application Password dedicada (utilizador `ealmeida`, nome `mcp-fluent-booking`), guardada só no
`env:` da ligação MCP (`~/.omp/agent/mcp.json`) — nunca em ficheiro commitado. Revogar em
WordPress → Utilizadores → perfil → Application Passwords se deixar de ser necessária. Recriar com
`wp user application-password create ealmeida mcp-fluent-booking --porcelain`.
## Tools (15)
**Estado (read-only):**
- `fb_get_plugin_status` — versões free/pro (via WP-CLI, único tool que ainda usa SSH — a API não
expõe versão) + estado da licença Pro (via API).
**Calendários e event types (read-only):**
- `fb_list_calendars` / `fb_get_calendar` — páginas de agendamento por anfitrião.
- `fb_get_event` — um tipo de evento (ex.: "Diagnóstico Digital Gratuito, 30min").
**Reservas — o domínio principal:**
- `fb_list_bookings` (read-only) — filtros `period` (upcoming/completed/cancelled/pending/
no_show/latest_bookings/**all**), `calendar_id`, `event_id`, `event_type`, `email`, `search`,
intervalo de datas, paginação. **`period` por omissão é `upcoming`** — usar `all` para histórico.
- `fb_get_booking` (read-only) — reserva completa: convidado, campos de formulário custom, local,
pagamento.
- `fb_list_activities` (read-only) — timeline de uma reserva (`booking_id`) ou log do site inteiro
(sem argumento).
- `fb_update_booking_status` (**write**) — muda `status` para
`scheduled|completed|cancelled|rejected|no_show` pela API real (`PUT /schedules/{id}`,
`column=status`). Único caminho de escrita de estado; dispara os efeitos correctos do plugin
(email de cancelamento com `cancel_reason`, log de actividade, marcação de pagamento).
- `fb_send_confirmation_email` (**write**) — reenvia o email de confirmação a um convidado real.
Envia um email de verdade — usar com intenção, nunca para "testar".
**Disponibilidade e reports (read-only):**
- `fb_list_availability_schedules` / `fb_get_availability_schedule` — templates de horário
semanal reutilizáveis entre event types.
- `fb_get_reports` — overview (totais, últimas reservas).
- `fb_get_graph_reports` — série temporal (reservas/concluídas/canceladas por dia), intervalo
`date_from`/`date_to` opcional.
- `fb_get_general_settings` — moeda, remetente de email, cancelamento automático.
- `fb_list_hosts` — utilizadores WordPress com calendário próprio.
## Fora do âmbito
Equipa/permissões, métodos de pagamento, integrações Zoom/Google/CalDAV, Twilio SMS, webhooks,
cupões, activação/desactivação de licença — gestão de credenciais/integrações externas fica no
wp-admin. Criar reservas em nome de alguém (`POST /bookings/create/{event_id}`) também fica fora:
o payload depende de slots computados dinamicamente e uma reserva mal formada dispara emails reais
a um convidado.
## Segurança
`fb_send_confirmation_email` e o ramo `cancelled`/`rejected` de `fb_update_booking_status`
disparam emails reais a convidados verdadeiros — nunca invocar como "teste". Verificação sem
mutação real: mudar um booking para o estado que **já tem** devolve 422 "No changes found" do
próprio plugin (confirma o caminho de escrita sem alterar nada).
## Verificação
Construído e testado ponta-a-ponta contra produção 19-08-2026: as 15 tools exercitadas com dados
reais (1 calendário, 39 reservas no histórico, licença Pro "Agency License" válida), incluindo o
caso 422 "No changes found" acima (escrita testada sem mutar dados reais) e 404 correcto em id
inexistente (`fb_get_booking`, `fb_send_confirmation_email`).
## Recursos adicionais
- **`references/api-reference.md`** — endpoints REST completos, filtros (`period`, `author`,
`range`), enums, forma real das respostas (`booking`, `calendar`, `event`, `availability`).
- **`references/diagnostics.md`** — playbook: quem marcou uma reunião, cancelar/reagendar
correctamente, health check rápido, o que fazer quando `fb_list_bookings` devolve vazio.
@@ -0,0 +1,98 @@
# FluentBooking — referência da REST API (confirmado em produção, 2026-08-19)
Namespace: `fluent-booking/v2` (WP REST API real, `wp-json/fluent-booking/v2/...`). Auth: Basic
Auth com Application Password — o `permission_callback` de cada rota é uma `Policy` que verifica
`current_user_can()`/`PermissionManager`, não nonce de sessão. 100 rotas registadas entre free e
Pro; este ficheiro documenta só as usadas pelo MCP.
## Calendários (`/calendars`)
`GET /calendars` — lista todos, cada um com `slots[]` (event types) aninhados, incluindo
`settings.weekly_schedules` completo por dia. `GET /calendars/{id}` — um só, mesma forma.
Campos relevantes de um calendar: `id`, `title`, `slug`, `status` (`active`/…), `type`
(`simple`/…), `visibility` (`public`/…), `author_timezone`, `public_url`
(`https://descomplicar.pt/?fluent-booking=calendar&host=<slug>`), `author_profile` (nome, avatar,
telefone).
## Event types (`/events/{event_id}`, aninhado em `calendars[].slots[]`)
`GET /events/{event_id}` → `{calendar_event: {...}}`. Campos: `id`, `calendar_id`, `duration`
(minutos), `title`, `slug`, `description` (HTML), `settings.schedule_type`
(`weekly_schedules`/…), `settings.weekly_schedules.<dia>.{enabled, slots:[{start,end}]}`.
**Gotcha de nomes:** `GET /calendars/event-lists` (sem id) devolve sempre
`{"code":"plugin_exception", ...}` em produção — não é um bug do MCP, o controller
`CalendarController@getCalendarEventLists` está partido standalone nesta instalação. Não usar
este endpoint; listar event types via `fb_list_calendars`/`fb_get_calendar` (já vêm aninhados).
## Reservas (`/schedules`)
`GET /schedules` — parâmetros via `filters[...]` (bracket notation, o MCP trata isto
automaticamente a partir de campos planos no input schema):
| Filtro | Valores | Nota |
|---|---|---|
| `filters[period]` | `upcoming` (omissão), `completed`, `cancelled`, `pending`, `no_show`, `latest_bookings`, `all` | Valor fora da lista cai silenciosamente para `upcoming` |
| `filters[author]` | id numérico do calendário, `me`, `all` | Sem permissão "ver todas as reservas", o utilizador só vê as suas mesmo que peça `all` |
| `filters[event]` | id numérico do event type | |
| `filters[event_type]` | `single`/`group`/… | |
| `filters[email]` | email do convidado | |
| `filters[range]` | `[date_from, date_to]` | Filtra por `created_at`, não pela hora da reunião |
| `search` | texto livre | nome/email do convidado |
| `page` | inteiro | paginação Laravel-style |
Forma de uma reserva (`schedules.data[]`): `id`, `hash`, `calendar_id`, `event_id`, `group_id`
(agrupa reservas de grupo), `host_user_id`, `start_time`/`end_time` (timezone do site),
`first_name`/`last_name`/`email`/`phone`/`message`, `location_details`
(`{type, online_meeting_link}` ou presencial), `status`
(`scheduled`/`completed`/`cancelled`/`rejected`/`no_show`), `payment_status`, `source_url`
(página onde o convidado marcou), `custom_form_data` (campos custom do formulário de marcação,
por chave), `calendar_event` (event type completo aninhado, redundante mas presente sempre),
`happening_status`, `booking_status_text`, `reschedule_url`.
`GET /schedules/{id}` — mesma forma para uma reserva.
`PUT /schedules/{id}` (`fb_update_booking_status`) — body `{column:"status", value:<estado>,
cancel_reason?, reject_reason?, refund_payment?}`. Colunas válidas no controller real:
`internal_note`, `email`, `phone`, `first_name`, `last_name`, `status`, `payment_status` — o MCP
só expõe `status` deliberadamente (as outras deixam editar dados do convidado sem justificação de
negócio clara; ver SKILL.md "Fora do âmbito"). Side-effects reais confirmados no código
(`SchedulesController::patchBooking`):
- `status=cancelled` → `Booking::cancelMeeting($reason, 'host', $userId)` + email de cancelamento.
- `status=rejected` → `Booking::rejectMeeting($reason, $userId)`.
- `status=scheduled` com `payment_method`+`payment_order` associados → marca a order como `paid`.
- Valor igual ao actual → `422 {"message":"No changes found"}` (não é um bug, é a validação do
próprio plugin — útil para testar o caminho de escrita sem mutar nada).
`POST /schedules/{id}/send-confirmation-email` (`fb_send_confirmation_email`) — sem body, envia
email real.
## Actividade (`/schedules/{id}/activities`, `/reports/activities`)
Mesma forma nos dois: `{id, booking_id, parent_id, created_by, status, type, title, description,
created_at, updated_at}`. `/reports/activities` é o log do site inteiro (todas as reservas);
`/schedules/{id}/activities` filtra a uma só.
## Disponibilidade (`/availability`)
`GET /availability` — lista templates reutilizáveis (`{id, host_name, title, usage_count,
settings:{timezone, date_overrides[], weekly_schedules}}`). `GET /availability/{id}` — um só,
envelope `{schedule: {...}}` em vez de `{availabilities: {data:[...]}}`.
## Reports (`/reports`, `/reports/graph-reports`)
`GET /reports` → `{overview: [{title, period?, number, content, stat}], latest_books: [...]}`.
Métricas confirmadas: Total Bookings, Completed Bookings, Cancelled Bookings, Total Guests.
`GET /reports/graph-reports?date_range[0]=YYYY-MM-DD&date_range[1]=YYYY-MM-DD` →
`{booked_stats, completed_stats, cancelled_stats}`, cada um `{"YYYY-MM-DD": count}`. Sem
`date_range`, o controller usa uma janela por omissão (~7 dias, granularidade adaptada ao
intervalo — dia/semana/mês).
## Definições e equipa
`GET /settings/general` → `{payments, emailing, administration}` (moeda, remetente/rodapé de
email, `auto_cancel_timing`, dia de início da semana). `GET /admin/all-hosts` →
`{hosts: [{id, name, label, avatar, calendar_id, deleted_user}]}`. `GET /settings/license` (Pro)
→ `{status, variation_title, expires, is_expired, activation_hash, renew_url, purchase_url}`.
@@ -0,0 +1,57 @@
# FluentBooking — playbook de diagnóstico
## "Quem marcou uma reunião?" / ver a agenda
`fb_list_bookings({period: "upcoming"})` para o que vem aí (omitir `period` já faz isto por
omissão). Para histórico completo (incluindo já realizadas/canceladas), usar
`fb_list_bookings({period: "all"})` — **omitir `period` NUNCA mostra o histórico**, só o que
ainda não aconteceu. Filtrar por convidado com `email` ou `search` (nome).
Cada resultado já traz `location_details` (link da reunião online), `custom_form_data` (respostas
ao formulário de marcação — telefone, empresa, cargo, conforme configurado no event type) e
`source_url` (de que página do site veio a marcação).
## Cancelar ou reagendar uma reunião
Cancelar: `fb_update_booking_status({booking_id, status: "cancelled", cancel_reason: "..."})` —
dispara o email de cancelamento ao convidado automaticamente, não é preciso fazer mais nada.
`refund_payment: true` só faz sentido se a reserva tiver `payment_method` associado (evento pago).
Reagendar não é uma tool deste MCP — a reserva tem um `reschedule_url` próprio
(`https://descomplicar.pt/?fluent-booking=booking&meeting_hash=...&type=reschedule`) que o
convidado usa directamente; não há endpoint admin para reatribuir hora sem passar por aí.
## Marcar como "não compareceu" / concluída manualmente
`fb_update_booking_status({booking_id, status: "no_show"})` ou `status: "completed"`. Sem motivo
associado (`cancel_reason`/`reject_reason` só se aplicam a `cancelled`/`rejected`).
## Reenviar confirmação a um convidado
`fb_send_confirmation_email({booking_id})` — só se o convidado pedir explicitamente ou perdeu o
email original. Dispara um envio real; não usar para "confirmar que a tool funciona".
## `fb_list_bookings` devolve vazio
Quase sempre é o `period` por omissão (`upcoming`) sem reservas futuras — confirmar com
`fb_get_reports` (overview mostra o total histórico) antes de assumir que a agenda está vazia; se
o total for >0 mas `upcoming` vier vazio, repetir com `period: "all"`.
## Investigar uma reunião específica em detalhe
`fb_get_booking({booking_id})` para os dados completos, depois `fb_list_activities({booking_id})`
para a timeline (emails de lembrete enviados, mudanças de estado, quem cancelou e porquê — o
`created_by` fica `null` quando a acção foi automática/pelo sistema).
## Health check rápido
`fb_get_plugin_status` (versões + licença Pro) + `fb_get_reports` (totais) — dois tools sem
argumentos para um snapshot inicial. `fb_list_hosts` confirma que anfitriões têm calendário activo
antes de investigar "porque é que ninguém consegue marcar reunião com X".
## Fora do âmbito deste MCP (fazer via wp-admin)
- Criar uma reserva em nome de alguém (payload de slots dinâmico, risco de email indevido).
- Gerir equipa/permissões, métodos de pagamento, integrações Zoom/Google Calendar/CalDAV, SMS
(Twilio), webhooks, cupões.
- Activar/desactivar a licença Pro (`fb_get_plugin_status` é read-only).
+79
View File
@@ -0,0 +1,79 @@
---
name: mcp-fluent-crm
description: MCP dedicado (HTTP StreamableHTTP local, ligação `fluent-crm` em ~/.claude.json, porta 3202) para FluentCRM 3.1.10 + FluentCRM Pro 2.9.86 em descomplicar.pt (produção, 291k+ contactos reais) — contactos, tags, listas, empresas, notas, campanhas e estatísticas. Escritas sempre via FluentCrmApi()/modelos do próprio plugin (nunca SQL directo), por SSH sem escrever ficheiros no servidor. Usar quando "fluentcrm", "contactos crm", "tags fluentcrm", "listas fluentcrm", "estatísticas subscritores", "campanha email estatísticas", "empresas fluentcrm", "subscriber status", "unsubscribe contacto", ou qualquer gestão de contactos/marketing automation em descomplicar.pt.
layer: wiki
---
# /mcp-fluent-crm — MCP dedicado FluentCRM (descomplicar.pt)
Projecto em `/media/ealmeida/Dados/Dev/mcp-fluent-crm/` (TypeScript, SDK MCP oficial,
StreamableHTTP). FluentCRM não tem WP-CLI para CRUD (só `license_status`/`stats`/`sync_*`) nem
API REST configurada (sem chave gerada) — as escritas seguras vivem na camada de serviço oficial
do plugin (`FluentCrmApi('contacts')->createOrUpdate()`, `Subscriber::attachTags()`, etc.), lida
por completo do código-fonte antes de desenhar as tools.
**Zona-alvo: descomplicar.pt (produção), 291 441 contactos reais** (81 881 subscribed, 209 559
unsubscribed, 1 transactional, 13 customer/291 428 lead). Não confundir com
`starter.descomplicar.pt` (staging, FluentCRM Free sem Pro, dataset de teste) onde o mapeamento
inicial do código foi feito.
## Arquitectura — sem tocar no filesystem do servidor
Leituras: SQL directo via `$wpdb` (rápido, tipado). Escritas: SEMPRE via `FluentCrmApi()`/modelos
— nunca INSERT/UPDATE directo, para preservar guardas de estado, hooks e invalidação de cache.
Ambas correm por SSH via `wp eval`, com o corpo PHP + dados de entrada codificados em base64
dentro de uma variável de ambiente remota — **nunca ficheiros escritos/apagados no servidor**
(`cat >`/`rm`/redirecção), o que evitaria deliberadamente o GATE 5.1 de mutação de infra-estrutura
(bloqueia mutações de ficheiro em `server` sem `~/.claude/hooks/infra-auth.sh server`; escritas de
base de dados via `wp eval` não são bloqueadas por este gate). Ver `src/wp-bridge.ts` (`runPhp()`).
## Tools (20)
**Leitura (10):** `fcrm_list_contacts` (filtros status/pesquisa/tag_id/list_id/company_id — usar
sempre, nunca listar sem filtro/limit com 291k linhas), `fcrm_get_contact`,
`fcrm_list_contact_notes`, `fcrm_list_tags`, `fcrm_list_lists`, `fcrm_list_companies`,
`fcrm_get_company`, `fcrm_list_campaigns`, `fcrm_get_campaign_stats`, `fcrm_get_subscriber_stats`.
**Escrita (10):** `fcrm_create_or_update_contact`, `fcrm_update_contact_status`,
`fcrm_attach_tags`, `fcrm_detach_tags`, `fcrm_attach_lists`, `fcrm_detach_lists`,
`fcrm_add_contact_note`, `fcrm_create_tag`, `fcrm_create_list`, `fcrm_create_or_update_company`.
## Guardas de segurança em status (contactos reais, cuidado)
- `fcrm_create_or_update_contact` nunca rebaixa um contacto `subscribed` nem ressuscita um
suprimido (`unsubscribed`/`bounced`/`complained`/`spammed`) sem `force_update=true` — mesma
guarda que o `Contacts::createOrUpdate()` do próprio plugin implementa (o código documenta isto
como correcção de uma escalada de estado indevida que existia antes).
- `fcrm_update_contact_status` (mudança directa, sem as guardas do createOrUpdate) exige
`confirm=true` explícito nas duas direcções de risco: `subscribed`→suprimido, ou
suprimido→qualquer outro estado. Sem `confirm`, a tool recusa com erro accionável.
- Estados válidos: `subscribed`, `pending`, `unsubscribed`, `transactional`, `bounced`,
`complained`, `spammed`. Suprimidos: `unsubscribed`/`bounced`/`complained`/`spammed`.
## Fora do v1 (decisão deliberada)
Criação/envio de campanhas (dispara emails reais, sem dry-run, blast-radius incompatível com um
v1), CRUD de funnels/automações (sequências JSON complexas), event tracking de escrita, segmentos
dinâmicos (Pro), gestão de licença. Ler/gerir manualmente no wp-admin.
## Credenciais
`.env` local (`WP_SSH_HOST=server`, `WP_PATH=/home/ealmeida/public_html`), não versionado. Sem
tokens/API keys — a autenticação é a própria sessão SSH root já configurada em `~/.ssh/config`.
## Verificação
Construído e testado ponta-a-ponta contra a zona real 19-08-2026: todas as 20 tools exercitadas
manualmente (incl. guarda de status bloqueada e depois confirmada) + 14/14 evaluations
automatizadas (`npm run eval:ci`), que criam e limpam os seus próprios artefactos
(`__mcp_eval__`) sem tocar em dados reais. Deploy como processo `hub` persistente
(`mcp-fluent-crm`, porta 3202, `http://127.0.0.1:3202/mcp`), registado em `~/.claude.json` como
`fluent-crm`.
## Skills relacionadas
- `fluent-crm` — conhecimento profundo do plugin: arquitectura Free/Pro, schema completo `fc_*`,
API de desenvolvimento (`FluentCrmApi()`), modelo de estados e a guarda anti-escalada (§3),
wp-admin, arquitectura de Automations/Funnels e Dynamic Segments (Pro) — consultar antes de
desenhar uma automação ou entender uma decisão de status que este MCP não cobre (Funnels,
Dynamic Segments, campanhas de envio ficam fora do v1 desta ferramenta — gerir via wp-admin).
+194
View File
@@ -0,0 +1,194 @@
---
name: mcp-fluent-smtp
description: MCP dedicado multi-site (node stdio, ligação `fluent-smtp` em ~/.omp/agent/mcp.json) para FluentSMTP 2.3.1 (WPManageNinja) em descomplicar.pt, starter.descomplicar.pt e emanuelalmeida.pt — controla a entrega de email do site, tratado como superfície de alto risco. Estado geral, connections configuradas (segredos SEMPRE mascarados), logs de email com filtros/paginação, detalhe de log, estatísticas de envio, reenvio controlado de 1 email falhado (confirm:true obrigatório). NENHUMA escrita de configuração de connections. Usar quando "fluentsmtp", "fluent smtp", "email não enviado", "logs de email wordpress", "reenviar email falhado", "connection smtp wordpress", "fallback connection smtp", "estatísticas de envio de email", ou qualquer diagnóstico/consulta de entrega de email nestes 3 sites.
layer: wiki
---
# /mcp-fluent-smtp — MCP dedicado FluentSMTP (multi-site)
Projecto em `/media/ealmeida/Dados/Dev/mcp-fluent-smtp/` (TypeScript, SDK MCP oficial, stdio).
Não existe skill de conhecimento separada sobre o FluentSMTP — esta skill é a ÚNICA
documentação, incluindo o mapeamento de dados (schema/option) que noutros casos vive numa skill
"irmã" (ver `wp-fastest-cache`↔`mcp-wpfc` ou `fluent-crm`↔`mcp-fluent-crm` para esse padrão).
**FluentSMTP controla a entrega de email do site inteiro — tratado como superfície de alto
risco.** Por isso este MCP é deliberadamente maioritariamente READ, e a única escrita (reenvio de
1 email) exige `confirm:true` e nunca toca em configuração de connection.
## Sites conhecidos
Chamar `fluentsmtp_list_sites` para a lista actual. Confirmados ao vivo 19-08-2026, FluentSMTP
2.3.1 active nos três:
| Alias | Domínio | Prefixo de tabela | Connections | Fallback |
|---|---|---|---|---|
| `descomplicar` | descomplicar.pt (produção) | `wpah_` | 2 (Elastic Email + SMTP directo porta 25, default) | configurado mas **órfão** — aponta para um ID que já não existe em `connections` |
| `starter` | starter.descomplicar.pt (staging) | `wpbk_` | 1 (SMTP) | não configurado (chave ausente) |
| `emanuelalmeida` | emanuelalmeida.pt | `wpne_` | 1 (SMTP) | presente mas vazio (não configurado) |
Também aceita um path absoluto directamente (`/home/ealmeida/<site>`) para sites fora desta lista,
sem exigir rebuild do MCP — mas nesse caso confirmar primeiro que o FluentSMTP está activo lá.
**Nota:** `emanuelalmeida.pt` tem também um mu-plugin próprio `descomplicar-fluentsmtp-auto-retry`
(descoberto via `wp plugin list`, must-use) — fora do âmbito deste MCP, mas relevante para
diagnóstico de falhas nesse site especificamente (pode já estar a reenviar automaticamente antes
de este MCP ser chamado).
## Mapeamento de dados (schema)
FluentSMTP não expõe WP-CLI custom commands nem API REST própria. Toda a leitura/escrita passa
pela camada de serviço PHP do plugin, via `wp eval` (ver secção Arquitectura).
**Config (`wp_options.fluentmail-settings`)** — option nativa PHP array (não string JSON dentro de
string; `wp option get --format=json` funciona correctamente sem dupla-codificação):
```
{
connections: {
"<connId hex32>": {
title: string,
provider_settings: {
provider: "smtp"|"ses"|"mailgun"|"sendgrid"|"sendinblue"|"sparkpost"|"pepipost"|
"postmark"|"elasticmail"|"gmail"|"outlook"|"tosend"|"cloudflare",
sender_name, sender_email, force_from_name, force_from_email,
// campo secreto por provider (ver mapa completo abaixo) — sempre AES-256-CTR
// cifrado (LOGGED_IN_KEY/SALT) + base64 quando use_encrypt="yes"
password | secret_key | api_key | client_secret,
host, port, auth, encryption, auto_tls, return_path, // só provider=smtp
key_store: "db", // ou "config" se a credencial vier de wp-config.php (não visto ainda)
}
}, ...
},
mappings: { "<email>": "<connId>" }, // roteamento por remetente
misc: {
default_connection: "<connId>",
fallback_connection: "<connId>", // PODE ficar órfão após apagar a connection alvo
log_emails: "yes"|"no",
log_saved_interval_days: "<N>", // retenção do log em dias
disable_fluentcrm_logs: "yes"|"no",
send_as_text: "yes"|"no",
simulate_emails: "yes"|"no", // modo simulação — nunca envia de verdade
},
use_encrypt: "yes"|"", // se "", segredos ficam em claro na option (visto em starter)
test: "<blob cifrado usado como canário de decifração>",
}
```
Mapa provider→campo secreto (replicado de `helpers.php`, `fluentMailGetSettings`/`SetSettings` —
manter sincronizado se o plugin actualizar):
`smtp→password`, `ses→secret_key`, `mailgun/sendgrid/sendinblue/sparkpost/pepipost/postmark/
elasticmail/tosend/cloudflare→api_key`, `gmail/outlook→client_secret`.
**Logs (`wp<prefixo>_fsmpt_email_logs`, prefixo `FLUENT_MAIL_DB_PREFIX`='fsmpt_')** — tabela
custom, prefixo de tabela WordPress **varia por site** (ver tabela acima):
| Coluna | Tipo | Nota |
|---|---|---|
| `id` | int PK | |
| `to` / `headers` / `attachments` | longtext | PHP-serializado — precisa `maybe_unserialize()` |
| `from`, `subject` | varchar(255) | |
| `body` | longtext | HTML/texto completo do email |
| `status` | varchar(20) | `pending`\|`sent`\|`failed` |
| `response` | text | serializado — erro do provider quando `failed` |
| `extra` | text | serializado — `provider`, `send_time_ms`, histórico `resends[]` (cada um com `at`,`to`,`by`,`sent`,`ms`) |
| `retries` / `resent_count` | int | tentativas automáticas vs reenvios manuais |
| `created_at` / `updated_at` | timestamp | índice composto `(created_at, status)` |
## Arquitectura — `wp eval` sobre a camada de serviço oficial, nunca SQL/`wp option` directo
Três razões concretas para este plugin (documentadas em `src/wp-bridge.ts`):
1. **Prefixo de tabela varia por site** (`wpah_`/`wpbk_`/`wpne_` confirmados ao vivo) —
`$wpdb->prefix` resolve isto dentro do PHP; não faz sentido rastrear isto em TS por site.
2. **Segredos cifrados** — `fluentMailGetSettings()` já decifra/mascarara consistentemente; não
vale a pena reimplementar AES-256-CTR + LOGGED_IN_KEY/SALT em TypeScript.
3. **Reenvio tem de passar pelo mecanismo real** (`Logger::resendEmailFromLog()`) — conversão de
anexos, cabeçalhos, `resent_count`, histórico em `extra` — nunca um UPDATE SQL a simular isto.
Corpo PHP + dados de entrada (JSON) viajam em base64 dentro de uma variável de ambiente remota,
nunca interpolados no comando SSH — **nenhum ficheiro é escrito/apagado no servidor**, o que
evitaria deliberadamente o GATE 5.1 de mutação de infra-estrutura. Ver `src/wp-bridge.ts`
(`runPhp()`) e `src/php.ts` (os 6 corpos PHP, um por tool).
**Gotcha de paginação — não replicar sem testar primeiro:** `Logger::get()`/
`QueryBuilderHandler::paginate()` (biblioteca interna `wpfluent`) lêem a página/per_page de
`$_GET['page']`/`$_REQUEST['per_page']` — pensado para pedidos HTTP do admin, inexistentes num
`wp eval` de CLI. Sem preencher esses dois superglobais antes de chamar `Logger::get()`, a
paginação colapsa sempre para `page=1`/`per_page=15` **ignorando silenciosamente** qualquer
`page`/`per_page` passado nos dados — reproduzido ao vivo antes de corrigir (`fluentsmtp_list_email_logs`
com `page:2` devolvia sempre a página 1 até `$_GET['page']`/`$_REQUEST['per_page']` serem
preenchidos explicitamente no corpo PHP, isolado a este processo efémero). Os filtros
`where()`/`whereBetween()` (status/pesquisa/intervalo de datas) não sofrem deste problema —
sobrevivem à chamada de `paginate()` porque ficam no query builder antes dela ser invocada.
## Tools (6)
| Tool | Read-only | Uso |
|---|---|---|
| `fluentsmtp_list_sites` | sim | Aliases conhecidos, path WP, prefixo de tabela, nota de estado |
| `fluentsmtp_get_status` | sim | Versão, nº connections, default/fallback (com verificação de existência — fallback pode ficar órfão), definições de log/retenção. Nunca devolve segredos |
| `fluentsmtp_list_connections` | sim | Todas as connections — provider, remetente, `secret_configured: true/false` (NUNCA o valor, mesmo cifrado), qual é default/fallback, e-mails mapeados |
| `fluentsmtp_list_email_logs` | sim | Filtros `status`/`search`/`date_from`+`date_to`, paginação real (`page`/`per_page`) |
| `fluentsmtp_get_email_log` | sim | Detalhe completo de 1 log por `id` — corpo truncado a 5000 caracteres, cabeçalhos, resposta do provider, histórico de reenvios em `extra` |
| `fluentsmtp_get_stats` | sim | Totais vitalícios (`sent`/`failed`), totais do período por status, série diária/semanal/mensal do volume total |
| `fluentsmtp_resend_email` | **não** | Reenvia de verdade 1 email via `Logger::resendEmailFromLog()`. **Exige `confirm:true`.** Aceita `recipients` para redireccionar o envio para endereço(s) controlados em vez do(s) destinatário(s) original(is) |
Todas as tools exigem `site` como primeiro parâmetro.
## Decisão deliberada: sem escrita de configuração de connections
Nenhuma tool escreve `fluentmail-settings` (criar/editar/apagar connection, mudar
default/fallback, activar/desactivar simulação). Razão: um erro nessa escrita — chave errada,
provider mal configurado, `default_connection` a apontar para um ID inexistente (já aconteceu
organicamente em produção, ver `fallback_connection` órfão em `descomplicar`) — parte o envio de
email do **site inteiro** (resets de password, notificações, formulários, FluentCRM), sem forma
fácil de detectar isso a partir de fora até um utilizador reportar "não recebi o email". O
risco/benefício não compensa para um v1. Se for necessário no futuro: replicar o padrão
guarda-corpo + `confirm:true` + leitura-antes-de-escrever de `mcp-fluent-crm`
(`fcrm_update_contact_status`), nunca um `update_option` cego.
## Segurança
- Segredos de connection **nunca** devolvidos em nenhuma tool, mesmo cifrados — só o booleano
`secret_configured` por connection (`fluentsmtp_list_connections`).
- `fluentsmtp_resend_email` exige `confirm:true` (zod `z.literal(true)`, rejeitado no schema antes
de chegar ao handler — testado ao vivo com `confirm:false`, erro `-32602` antes de qualquer SSH).
- `recipients` em `fluentsmtp_resend_email` permite testar/reenviar sem depender do(s)
destinatário(s) original(is) — usado nos testes deste MCP para nunca enviar a um terceiro
externo sem necessidade.
- Corpo do log truncado a 5000 caracteres em `fluentsmtp_get_email_log` — não é segredo, mas evita
devolver megabytes de HTML/anexos codificados.
## Verificação
Construído e testado ponta-a-ponta contra os 3 sites reais em 19-08-2026, via processo stdio real
(não só chamadas SSH soltas):
- `fluentsmtp_get_status` em `descomplicar`: 2 connections, default=SMTP directo,
`fallback_connection_exists: false` (órfão, confirmado por leitura directa da option);
em `starter`: 1 connection, `secrets_encrypted_at_rest: false` (confirmado, `use_encrypt` ausente
nesse site); em `emanuelalmeida`: 1 connection, fallback presente mas vazio.
- `fluentsmtp_list_connections` em `descomplicar` e `emanuelalmeida`: segredos correctamente
mascarados (`secret_configured: false` no Elastic Email sem API key, `true` no SMTP directo com
password cifrada preenchida) — nunca o valor devolvido.
- `fluentsmtp_list_email_logs` em `descomplicar` com `status:failed`, `page:2`, `per_page:3`:
`total: 19` (confirmado por `SELECT COUNT(*) ... WHERE status='failed'` directo), ids
correctos para a página 2 depois da correcção do gotcha de paginação.
- `fluentsmtp_get_email_log` para o log `#50`: destinatário, assunto e erro do provider
(`"SMTP Error: Could not authenticate."`) confirmados coincidentes com a linha da tabela.
- `fluentsmtp_get_stats` em `descomplicar` (01-19 ago 2026): `lifetime.sent=58`,
`lifetime.failed=19` — confirmado por `SELECT COUNT(*) GROUP BY status` directo; em
`emanuelalmeida`: `lifetime.sent=20`, `lifetime.failed=0`.
- `fluentsmtp_resend_email` (escrita real, única): `confirm:false` rejeitado no schema antes de
qualquer SSH (`-32602 Invalid literal value, expected true`). `confirm:true` no log `#74`
(`descomplicar`, falhado em 2026-08-14) com `recipients:["it@descomplicar.pt"]` — redireccionado
do destinatário original para este endereço interno da própria empresa, nunca a um terceiro
externo. Resultado: `status: sent`, `resent_count: 1`, confirmado por leitura directa da linha
(`extra` com o registo do reenvio: `to=it@descomplicar.pt`, `sent=true`, `ms=207.2`).
## Skills relacionadas
- `mcp-fluent-crm` — mesmo mecanismo `wp eval` sobre camada de serviço oficial, para contactos e
campanhas em descomplicar.pt; FluentSMTP é o transporte que o FluentCRM usa para enviar.
- `mcp-wpfc` / `mcp-wpmeteor` — outros MCPs multi-site do mesmo bundle, padrão `site` como
parâmetro em cada tool.
+167
View File
@@ -0,0 +1,167 @@
---
name: mcp-fluentform
description: MCP dedicado multi-site (node stdio, ligação `fluentform` em ~/.omp/agent/mcp.json) para o FluentForms 6.2.12 (WPManageNinja) em descomplicar.pt (produção) e starter.descomplicar.pt (staging) — formulários, submissions (entries), contagens por estado e settings gerais, via WP-CLI/SSH sobre a camada de serviço oficial do plugin (FormService/SubmissionService/GlobalSettingsService). Escritas limitadas a estado da entry (unread/read/spam/trashed) e favorita/star — nunca schema de formulário nem integrações de terceiros. Usar quando "fluentforms", "formulários wordpress", "entries fluentform", "submissions fluentform", "marcar entry como lida", "favoritar entry", "spam fluentform", "trash entry fluentform", "contagem de submissions", "settings fluentforms", ou qualquer execução (não estratégia) sobre o FluentForms.
layer: wiki
---
# /mcp-fluentform — MCP dedicado FluentForms (multi-site)
Projecto em `/media/ealmeida/Dados/Dev/mcp-fluentform/` (TypeScript, SDK MCP oficial, stdio). Não
existe skill de conhecimento separada sobre o FluentForms — esta skill é a ÚNICA documentação,
incluindo o mapeamento de dados (tabelas/serviços) que noutros casos vive numa skill "irmã" (ver
`fluent-crm`↔`mcp-fluent-crm` para esse padrão).
**Achado relevante desta sessão:** o FluentForms 6.2.12 já traz o seu próprio módulo MCP interno
(`app/Modules/MCP/`, abilities `fluentform/list-submissions`, `fluentform/update-submission-status`,
etc., via WP Abilities API), mas só fica acessível externamente se o site tiver o plugin
`mcp-adapter` ou `fluent-toolkit` activo e o toggle "MCP" ligado nas definições do FluentForms —
nenhuma das duas condições está preenchida hoje nos dois sites. Este MCP **não depende disso**:
chama directamente a camada de serviço PHP subjacente (`FormService`/`SubmissionService`/
`GlobalSettingsService`), a mesma que o módulo MCP nativo do plugin também usa por baixo.
## Sites conhecidos
Chamar `fluentform_list_sites` para a lista actual. Confirmados ao vivo 19-08-2026, FluentForms
6.2.12 active nos dois — são os únicos dois sites do bundle com o plugin activo (`wp plugin list`):
| Alias | Domínio | Prefixo de tabela | Formulários | Submissions |
|---|---|---|---|---|
| `descomplicar` | descomplicar.pt (produção) | `wpah_` | 3 (`Contact Form Demo` unpublished, `Subscription Form` unpublished, `Blank Form (#3)` published) | 1 (form #3, status `read`) |
| `starter` | starter.descomplicar.pt (staging) | `wpbk_` | 2 (`Contact Form Demo`, `Subscription Form`, ambos published) | 0 |
Também aceita um path absoluto directamente (`/home/ealmeida/<site>`) para sites fora desta lista,
sem exigir rebuild do MCP — mas confirmar primeiro que o FluentForms está activo lá
(`wp plugin list --allow-root --path=<path>`).
## Mapeamento de dados
FluentForms não expõe WP-CLI CRUD (`wp fluentform` só tem `activate_license`/`license_status`/
`stats`). Toda a leitura/escrita deste MCP passa pela camada de serviço PHP oficial do plugin, via
`wp eval` — nunca SQL directo, nunca `wp option update`.
**Tabelas custom** (prefixo de tabela varia por site, ver acima; resolvido internamente pelo ORM
do plugin, nunca hardcoded em TypeScript):
| Tabela (sem prefixo) | Papel |
|---|---|
| `fluentform_forms` | formulários — `title`, `status` (`published`/`unpublished`), `type`, `form_fields` (JSON do schema), `has_payment` |
| `fluentform_submissions` | entries — `form_id`, `response` (JSON das respostas), `status` (`unread`/`read`/`spam`/`trashed`), `is_favourite`, `serial_number`, `user_id`, `payment_*` |
| `fluentform_submission_meta` | meta por submission (ex. `_entry_uid_hash` para link público) |
| `fluentform_entry_details` | valores desnormalizados por campo (usado por relatórios/exportação) |
| `fluentform_form_meta` | meta por formulário (settings, notificações, integrações) |
| `fluentform_form_analytics` | contadores de vistas/conversão por formulário |
| `fluentform_logs` | log de eventos por submission/formulário |
**Classes de serviço oficiais usadas por este MCP** (`FluentForm\App\Services\*` +
`FluentForm\App\Models\*`, todas instanciáveis directamente com `new`, sem container DI):
- `Form::query()` — listagem/filtro de formulários (`orderBy`, `where('title','LIKE',...)`, `where('status',...)`, `->paginate($perPage,['*'],'page',$page)`).
- `FormService::getInputsAndLabels($formId)` — schema de campos (`inputs`/`labels`), usado por `fluentform_get_form` e para rotular os valores em `fluentform_get_submission`.
- `Submission::customQuery($attrs)` — o MESMO método que o wp-admin usa para a listagem de entries; aceita `form_id`, `entry_type` (nome real do parâmetro de estado — não `status`; `'favorites'` é tratado à parte via `is_favourite=1`), `search`, `date_range: [from,to]`, `sort_by`.
- `SubmissionService::find($id)` — detalhe completo de 1 entry. **EFEITO SECUNDÁRIO REAL DO PLUGIN:** marca automaticamente `unread`→`read` (mesmo comportamento de abrir a entry no wp-admin) — documentado na descrição da tool, não é bug deste MCP.
- `SubmissionService::updateStatus(['entry_id'=>,'status'=>])` — `unread`/`read`/`spam`/`trashed`; dispara `do_action('fluentform/after_submission_status_update', ...)`.
- `SubmissionService::toggleIsFavorite($id)` — toggle puro, devolve `[mensagem, novo_estado]`.
- `SubmissionService::resources(['form_id'=>,'counts'=>true])` → `Submission::countByGroup($formId)` — contagens `unread`/`read`/`spam`/`trashed`/`all` (exclui trashed)/`favorites`.
- `GlobalSettingsService::get(['key'=>[...]])` — só lê chaves com prefixo `fluentform_`/`_fluentform_`/`fluentform-`/`_fluentform-` (allowlist própria do plugin); usado por `fluentform_get_settings`.
## Arquitectura — `wp eval` sobre a camada de serviço, nunca SQL/WP-CLI directo
Corpo PHP fixo (nunca construído a partir de texto do chamador) + dados de entrada (JSON) viajam
em base64 dentro de uma variável de ambiente remota, nunca interpolados como texto dinâmico no
comando SSH — mesmo padrão de `mcp-fluent-crm/src/wp-bridge.ts` (`runPhp()`), adaptado a
multi-site: o path WordPress é um parâmetro por chamada (`src/sites.ts`), não uma config fixa.
**Gotcha de datas — não replicar sem testar primeiro:** `$model->created_at`/`updated_at` não
devolvem uma string — devolvem um objecto `FluentForm\Framework\Support\DateTime` (Carbon-like).
`json_encode()` directo sobre esse objecto produz `{"date":"...","timezone_type":3,"timezone":"..."}`
em vez de uma string, porque o `serializeDate()` custom do `Model.php` do plugin só é aplicado em
`toArray()`/`toJson()`, nunca num `json_encode()` PHP nativo isolado. Corrigido em todos os pontos
deste MCP com `(string) $form->created_at` — o `DateTime` do framework implementa `__toString()`
devolvendo `Y-m-d H:i:s` correctamente.
**Gotcha de race condition — confirmado ao vivo durante a construção:** duas chamadas de escrita
dependentes (ex. dois `fluentform_toggle_favorite` seguidos, para testar round-trip) NUNCA podem
correr em paralelo — cada `wp eval` é um processo PHP efémero independente; disparar dois
concorrentes faz ambos lerem o mesmo estado "antigo" e o resultado final fica indeterminado (visto
ao vivo: 1º toggle devolveu `is_favourite:true`, mas a leitura SQL imediatamente a seguir mostrou
`0` porque o 2º toggle, corrido em paralelo, tinha lido o `0` original em vez do `true` já escrito
pelo 1º). Corrigido correndo sequencialmente; qualquer teste/uso futuro deste MCP com múltiplas
escritas na mesma entry deve aguardar o resultado de cada chamada antes da seguinte.
## Tools (9)
| Tool | Read-only | Uso |
|---|---|---|
| `fluentform_list_sites` | sim | Aliases conhecidos, path WP, prefixo de tabela |
| `fluentform_list_forms` | sim | `search`/`status`(published\|unpublished)/`sort_by`/paginação — id, título, estado, tipo, `has_payment`, datas |
| `fluentform_get_form` | sim | Detalhe de 1 formulário + schema de campos (`key`/`label`/`element`) — necessário para saber as chaves esperadas em `fluentform_get_submission` |
| `fluentform_list_submissions` | sim | Entries de UM formulário (`form_id` obrigatório) — filtros `status`(unread\|read\|spam\|trashed\|favorites)/`search`/`date_from`+`date_to`(YYYY-MM-DD)/`sort_by`, paginação |
| `fluentform_get_submission` | **efeito secundário** | Detalhe completo de 1 entry (campos rotulados). Marca `unread`→`read` automaticamente (comportamento real do plugin) |
| `fluentform_count_submissions` | sim | Contagens por estado de um formulário: `unread`/`read`/`spam`/`trashed`/`all`/`favorites` |
| `fluentform_update_submission_status` | não | `unread`/`read`/`spam`/`trashed`. `trashed` é soft-delete (reversível chamando de novo com outro estado) |
| `fluentform_toggle_favorite` | não | Toggle puro — chamar duas vezes seguidas (sequencialmente!) devolve ao estado original |
| `fluentform_get_settings` | sim | Options com allowlist `fluentform_`/`_fluentform_` — por omissão: `_fluentform_global_form_settings` (layout/misc), `fluentform_global_modules_status` (integrações ligadas/desligadas, nunca credenciais), `_fluentform_installed_version`. Aceita `keys` para ler chaves específicas dentro do mesmo allowlist |
Todas as tools exigem `site` como primeiro parâmetro.
## Decisão deliberada: sem escrita de schema/integrações
Nenhuma tool cria/edita formulários, campos, notificações ou integrações de terceiros
(Mailchimp/Slack/etc). Razão (do enunciado desta tarefa): risco alto, fora do âmbito de um MCP de
gestão de entries — mudar `form_fields`/integrações parte a experiência de submissão pública do
site sem forma fácil de detectar isso a partir de fora. Gerir formulários/integrações via
wp-admin (**FluentForms → Forms/Settings**).
## Segurança
- Nenhuma tool constrói SQL ou nomes de tabela a partir de input do chamador — todas as queries
passam por métodos do ORM do próprio plugin (`Form::query()`, `Submission::customQuery()`,
`Model::find()`), nunca `whereRaw`/interpolação directa.
- `fluentform_get_settings` só lê chaves com o prefixo allowlisted pelo próprio
`GlobalSettingsService` — não há forma de ler uma option arbitrária fora do namespace
FluentForms através deste MCP.
- Corpo PHP + payload sempre em base64 dentro de env var remota — nunca interpolados como texto no
comando SSH (ver `src/wp-bridge.ts`).
- `fluentform_get_submission` devolve valores de campos submetidos pelo público (texto não
confiável) sem qualquer fencing adicional — ao contrário do módulo MCP nativo do plugin (que
aplica marcadores `[[UNTRUSTED_USER_INPUT]]`), porque este MCP é de uso interno da equipa, não
exposto a um agente autónomo com acesso à internet pública. Reavaliar se o uso mudar.
## Verificação
Construído e testado ponta-a-ponta contra produção e staging em 19-08-2026, via protocolo MCP real
(stdin/stdout, não só chamadas SSH soltas):
- `npm run build` limpo (tsc, sem erros).
- Smoke test stdio: `initialize` + `tools/list` devolveu os 9 tools esperados.
- Leituras reais confirmadas por SQL directo fora do MCP: `fluentform_list_forms` em
`descomplicar` (3 formulários) e `starter` (2 formulários, ambos `published`);
`fluentform_get_form` no formulário #3 (`descomplicar`) devolveu 8 campos incluindo um campo
FluentBooking (`fcal_booking`); `fluentform_list_submissions` no formulário #3 (`descomplicar`,
1 entry, `status:read`); `fluentform_count_submissions` no mesmo formulário
(`{unread:0,read:1,spam:0,trashed:0,all:1,favorites:0}`); `fluentform_get_settings` em
`descomplicar` devolveu `_fluentform_global_form_settings`/`fluentform_global_modules_status`/
`_fluentform_installed_version` correctos.
- `fluentform_get_submission` na entry #1 (`descomplicar`): 7 campos rotulados correctamente
(nome, email, empresa, cargo, data de reserva FluentBooking, 2 checkboxes), datas devolvidas como
string `Y-m-d H:i:s` (não o objecto Carbon cru).
- **Escrita 1 (idempotente):** `fluentform_update_submission_status` na entry #1 com
`status:"read"` (valor já-corrente) — devolveu `{entry_id:1,status:"read"}`, confirmado por SQL
directo que `status`/`is_favourite` ficaram inalterados (só `updated_at` mudou, efeito normal de
qualquer update).
- **Escrita 2 (round-trip):** `fluentform_toggle_favorite` na entry #1 chamado sequencialmente duas
vezes — 1ª chamada devolveu `is_favourite:true` (confirmado `is_favourite=1` por SQL directo),
2ª chamada devolveu `is_favourite:false` (confirmado `is_favourite=0` por SQL directo, estado
final idêntico ao inicial).
- Tratamento de erro confirmado: `fluentform_get_form` com `form_id:9999` devolveu
`isError:true` com "Formulário #9999 não encontrado."; `fluentform_list_forms` com
`site:"unknown-site"` devolveu `isError:true` listando os aliases conhecidos.
## Skills relacionadas
- `mcp-fluent-crm` — mesmo mecanismo `wp eval` sobre camada de serviço oficial, para contactos e
campanhas em descomplicar.pt; FluentCRM tem automações que disparam a partir de submissões
FluentForm (`FluentFormSubmissionTrigger`).
- `mcp-fluent-smtp` — transporte de email usado pelas notificações de submissão do FluentForms.
- `mcp-wpfc` / `mcp-wpmeteor` — outros MCPs multi-site do mesmo bundle, padrão `site` como
parâmetro em cada tool.
@@ -0,0 +1,62 @@
---
name: mcp-full-cliente
description: MCP dedicado (HTTP StreamableHTTP local, ligação `full-cliente` em ~/.claude.json, porta 3204) para gestão do plugin FULL.Cliente (full-customer, broker de licenças WordPress premium, api.full.services) no cluster CWP — ligação de sites, extracto real de licenças da conta, catálogo activo, plugins geridos e reactivação controlada via WP-CLI/wp eval sobre full-customer. Usar quando "full cliente", "licença full", "licenças compradas", "reactivar licença", "extracto licenças", "ligar site à full", "desligar site full", "auditoria licenças frota", ou qualquer consulta/gestão de licenças premium geridas pelo broker FULL.Cliente.
layer: wiki
---
# /mcp-full-cliente — MCP dedicado FULL.Cliente (broker de licenças WordPress)
Projecto em `/media/ealmeida/Dados/Dev/mcp-full-cliente/` (TypeScript, SDK MCP
oficial, StreamableHTTP, porta `3204`). Substitui os comandos manuais
`wp eval-file` documentados na skill de conhecimento `full-cliente` por tools
estruturadas e tipadas com validação Zod.
**Multi-site:** aceita o parâmetro `site` em todas as tools (default:
`descomplicar.pt`). Sites suportados: `descomplicar.pt`,
`starter.descomplicar.pt`, `emanuelalmeida.pt`, `ccv.descomplicar.pt`,
`care.descomplicar.pt`, `ecommerce.descomplicar.pt`,
`ecommerce-demo.descomplicar.pt`, `e-commerce.descomplicar.pt`.
## Regra de ouro: "geridos" ≠ "comprado"
- `fc_list_managed_plugins` (`plugin-repository/updates`) — plugins cujo
updater está actualmente substituído pela FULL **neste site**.
- `fc_get_account_licenses` (`account/licenses-extract`) — **fonte de verdade
da conta**: extracto de produtos comprados, licenças disponíveis e lista de
domínios em uso por produto.
## Tools (9)
### Leitura (6)
1. `fc_connection_status` — estado da ligação do site à FULL (`fc/connection-email` no usermeta de admin ou option legado) e email/user associados.
2. `fc_list_managed_plugins` — plugins geridos neste site específico.
3. `fc_list_catalog` — catálogo completo da FULL com pesquisa por nome/slug.
4. `fc_get_account_licenses` — extracto real de licenças da conta (compradas, disponíveis, domínios em uso por produto).
5. `fc_audit_fleet_licenses` — agregação dos domínios activos em toda a conta vs sites do cluster.
6. `fc_list_supported_sites` — lista estática dos sites suportados e caminhos de docroot.
### Escrita com guardas (3)
7. `fc_connect_site` — liga um site à conta partilhada (`it@descomplicar.pt`), descobrindo automaticamente o ID de admin local ou aceitando `user_id`. Grátis (não consome licenças).
8. `fc_disconnect_site` — desliga localmente (requer `confirm: true`). **Aviso:** não liberta licenças no servidor da FULL (para libertar um domínio é necessário suporte da FULL).
9. `fc_reactivate_plugin` — reactiva licença de um plugin suportado de passo único (`astra-addon`, `seo-by-rank-math-pro`, `advanced-custom-fields-pro`, `wp-rocket`, `perfmatters`, `wpforms`, etc.). Requer `confirm: true` porque consome licença e abre ticket Zendesk na FULL. Exclui fluxos multi-step como `elementorPro` (OAuth) e `essentialAddons` (OTP).
## Credenciais
`.env` local (`WP_SSH_HOST=server`), sem tokens/API keys próprios — a
autenticação usa a sessão SSH root já configurada e o token de ligação
armazenado no usermeta do WordPress de cada site ligado.
## Verificação
Construído e testado ponta-a-ponta contra a API real da FULL e os sites
`descomplicar.pt` e `emanuelalmeida.pt` em 19-08-2026: 13/13 evaluations
automatizadas (`npm run eval:ci`). Deploy como processo `hub` persistente
(`mcp-full-cliente`, porta 3204, `http://127.0.0.1:3204/mcp`), registado em
`~/.claude.json` como `full-cliente`.
## Skills relacionadas
- `full-cliente` — skill de conhecimento profundo do broker: arquitectura do
plugin `full-customer`, endpoints da API `api.full.services`, fluxos
multi-step (OAuth Elementor Pro), e diagnóstico de licenças de fabricante
directo vs CodeCanyon vs FULL.
@@ -0,0 +1,41 @@
---
name: mcp-google-site-kit
description: MCP dedicado ao Google Site Kit para diagnóstico de módulos (Search Console, GA4, Tag Manager, PageSpeed, AdSense) e tracking na frota CWP
version: 1.0.0
author: Descomplicar®
---
# MCP Google Site Kit (`mcp-google-site-kit`)
> Servidor MCP StreamableHTTP (porta 3207) para diagnóstico, verificação e gestão de integrações oficiais Google (**Google Site Kit** v1.185+) nos 10 sites da frota WordPress no CWP.
---
## 1. Ferramentas Disponíveis
| Ferramenta | Descrição | Parâmetros |
|---|---|---|
| `sitekit_list_supported_sites` | Lista os sites com Google Site Kit na frota. | `{}` |
| `sitekit_get_status` | Estado global dos módulos (Search Console, GA4, Tag Manager, PageSpeed, AdSense). | `site?` |
| `sitekit_get_module_settings` | Opções detalhadas de um módulo (`search-console`, `analytics-4`, `tagmanager`, `pagespeed-insights`, `adsense`). | `site?`, `module` |
| `sitekit_get_tracking_info` | Detalhes exatos de tracking GA4 (Measurement ID, Tag ID, injeção de snippet, filtros de exclusão). | `site?` |
---
## 2. Exemplo de Utilização
```typescript
// Obter estado geral das integrações Google no site
const status = await use_mcp_tool({
server_name: "google-site-kit",
tool_name: "sitekit_get_status",
arguments: { site: "care.descomplicar.pt" }
});
// Verificar Measurement ID e Snippet ativo
const tracking = await use_mcp_tool({
server_name: "google-site-kit",
tool_name: "sitekit_get_tracking_info",
arguments: { site: "care.descomplicar.pt" }
});
```
+98
View File
@@ -0,0 +1,98 @@
---
name: mcp-gtranslate
description: MCP dedicado multi-site (node stdio, ligação `gtranslate` em ~/.omp/agent/mcp.json) para gerir o GTranslate (widget de tradução frontend) via WP-CLI/SSH — idiomas activos e ordem (widget principal e flutuante), estilo/posição do widget, idioma de origem, estado da licença Pro/Enterprise (read-only). Site é um parâmetro em cada tool, não uma ligação fixa. Usar quando "gtranslate mcp", "idiomas activos gtranslate", "estilo widget traducao", "widget flutuante gtranslate", "licenca pro gtranslate", "mudar idioma origem site".
layer: wiki
---
# /mcp-gtranslate — MCP dedicado multi-site ao GTranslate
Projecto em `/media/ealmeida/Dados/Dev/mcp-gtranslate/` (TypeScript, SDK MCP oficial, stdio). Sem
código PHP novo no WordPress: cada tool executa `wp option get`/`wp option patch update` via
`ssh server` sobre o WP-CLI já instalado. `site` é um parâmetro em cada tool, não uma ligação
fixa — hoje só `descomplicar.pt` (produção) tem o GTranslate instalado, mas o MCP aceita qualquer
alias conhecido ou path absoluto.
Não existe skill de conhecimento separada para o GTranslate — esta skill é a ÚNICA documentação,
incluindo o mapeamento de dados (a única option envolvida).
## Âmbito
Toda a config do GTranslate Free vive numa única option, `GTranslate` (confirmado por grep
completo a `get_option`/`update_option`/`add_option` em `gtranslate.php` — sem tabela custom, sem
comando WP-CLI próprio registado). Não cobre: activação/desactivação do próprio plugin
(`emcp-tools activate-plugin`/`deactivate-plugin` já cobre isso genericamente), a config do
"URL Translation Addon" (`url_addon/config.php`, um ficheiro PHP editado directamente pelo plugin
quando Pro/Enterprise está activo — fora do âmbito, o site actual é Free), nem activação de
licença Pro/Enterprise (handshake client-side contra gtranslate.net, sem endpoint WP-CLI/SSH).
## Sites conhecidos
Chamar `gtranslate_list_sites` para a lista actual com path e nota de estado. Verificado ao vivo
19-08-2026: **só `descomplicar.pt` (produção) tem o GTranslate instalado e activo** (v3.1.1, Free
— `pro_version`/`enterprise_version` ambos vazios). Os outros 7 sites reais do bundle
(`starter`, `emanuelalmeida`, `ccv`, `care`, `ecommerce-demo`, `e-commerce`, `ecommerce`) não o
têm — todos só têm o Loco Translate instalado, um plugin de tradução de ficheiros `.po`/`.mo`
sem qualquer relação com o GTranslate. Também aceita um path absoluto directamente
(`/home/ealmeida/<site>`) para sites fora desta lista, sem exigir rebuild do MCP.
## Tools (9)
| Tool | Read-only | Uso |
|---|---|---|
| `gtranslate_list_sites` | sim | Aliases conhecidos, path WP, nota de estado |
| `gtranslate_get_status` | sim | Instalado/activo/versão — chamar sempre antes dos outros tools |
| `gtranslate_get_config` | sim | Option `GTranslate` completa (array PHP nativo) |
| `gtranslate_get_license_status` | sim | `pro_version`/`enterprise_version` normalizados a booleano + `tier` (free/pro/enterprise). Não activa licença |
| `gtranslate_get_languages` | sim | `default_language`, `incl_langs` (widget principal), `fincl_langs` (widget flutuante), mais a lista completa dos 103 idiomas suportados |
| `gtranslate_toggle_language` | não | Activa/desactiva 1 código de idioma em `incl_langs`/`fincl_langs`/`both`, preservando a ordem dos restantes. Recusa esvaziar uma lista |
| `gtranslate_get_widget_style` | sim | `widget_look`, `flag_size`/`flag_style`, `globe_size`/`globe_color`, cores, `floating_language_selector` (posição do flutuante, `no` = desligado), direcções de abertura, `select_language_label`, `wrapper_selector`, `custom_css` |
| `gtranslate_set_widget_style` | não | Grava, chave a chave via `wp option patch update`, só as chaves de estilo fornecidas. `widget_look`/`floating_language_selector`/`flag_style`/direcções validados contra os enums reais do plugin |
| `gtranslate_set_default_language` | não | Muda `default_language` (idioma de origem/nativo do conteúdo). Não mexe em `incl_langs`/`fincl_langs` |
Todas as tools exigem `site` como primeiro parâmetro.
## Formato de armazenamento — confirmado, oposto ao WP Fastest Cache
`GTranslate` é um **array PHP nativo** (confirmado ao vivo: `wp option get GTranslate` sem
`--format=json` devolve `array(...)` desenrolado, não uma string com JSON lá dentro).
`--format=json` é portanto **correcto** aqui em leitura e escrita — mesma verificação prévia
obrigatória que gerou o gotcha inverso no `mcp-wpfc` (ver skill `wp-fastest-cache` §16.2), mas
com resultado oposto: aqui não há dupla codificação a evitar.
## Gotcha de escrita idempotente (WP-CLI)
`wp option patch update` quando o valor gravado é **idêntico** ao actual às vezes devolve
`Success: ... is unchanged` (exit 0), mas foi reproduzido ao vivo a devolver
`Error: Could not update option` (exit != 0) para o mesmo caso — `update_option()` do WordPress
devolve `false` quando o valor não muda, e o WP-CLI nem sempre trata esse `false` como sucesso.
`patchKey()` (`src/wpcli.ts`) relê a chave após qualquer falha e só propaga o erro se o valor no
site continuar diferente do pretendido — evita que uma chamada idempotente (ex. desactivar um
idioma já desactivado) rebente por um falso negativo do WP-CLI.
## Segurança
Allowlist fixa de 1 option (`GTranslate`) — nunca `wp option update` genérico sobre uma chave
arbitrária. Escritas via base64 em stdin, nunca interpoladas no comando SSH remoto. Sem
licença/API key para mascarar: confirmado por grep a "license"/"api_key"/"secret"/"token" em todo
o código-fonte do plugin — `pro_version`/`enterprise_version` são flags booleanas gravadas por
checkbox, a validação real da licença é feita client-side contra gtranslate.net pelo domínio do
site, não por um segredo em `wp_options`.
## Verificação
Construído e testado ponta-a-ponta contra produção (`descomplicar.pt`) 19-08-2026:
`gtranslate_get_status` confirmou v3.1.1 activo; `gtranslate_get_languages` confirmou
`default_language: "pt"`, `incl_langs` com 10 idiomas e `fincl_langs` com 3, ambos batendo certo
com leitura SQL/WP-CLI directa fora do MCP; `gtranslate_get_widget_style` confirmou
`widget_look: "flags"`, `floating_language_selector: "no"`; `gtranslate_get_license_status`
confirmou `tier: "free"`. Escrita testada com `gtranslate_toggle_language` (activou "ja" em
`incl_langs`, confirmado por `wp option get` directo, depois desactivou — testado 3x seguidas
incluindo o caso idempotente que expôs o gotcha do WP-CLI acima — estado final idêntico ao
original, confirmado por leitura SSH directa); `gtranslate_set_widget_style` e
`gtranslate_set_default_language` testados com os valores já-correntes (no-op idempotente),
confirmado sem alteração via leitura SSH directa.
## Skills relacionadas
- `wp-fastest-cache` — o gotcha de formato inverso que motivou a verificação obrigatória aqui.
- `mcp-wpmeteor` — mesmo padrão de option nativa + `wp option patch update` atómico.
+108
View File
@@ -0,0 +1,108 @@
---
name: mcp-happyfiles
description: MCP dedicado multi-site (node stdio, ligação `happyfiles` em ~/.omp/agent/mcp.json) para gerir as pastas da media library organizadas pelo HappyFiles Pro em qualquer site do bundle Descomplicar® onde o plugin esteja activo — listar pastas com contagem e hierarquia, criar/renomear/apagar pastas, listar ficheiros de uma pasta ou sem pasta, mover ficheiros entre pastas. Apagar uma pasta NUNCA apaga ficheiros. Site é um parâmetro em cada tool, não uma ligação fixa. Usar quando "happyfiles mcp", "pastas media library", "organizar biblioteca de media", "mover ficheiros de pasta wordpress", "criar pasta happyfiles", "apagar pasta happyfiles", "ficheiros sem pasta", "uncategorized happyfiles".
layer: wiki
---
# /mcp-happyfiles — MCP dedicado multi-site ao HappyFiles Pro
Projecto em `/media/ealmeida/Dados/Dev/mcp-happyfiles/` (TypeScript, SDK MCP oficial, stdio). Sem
código PHP novo no WordPress — cada tool corre `wp eval` via `ssh server` sobre a **API nativa de
taxonomias do WordPress** (`get_terms`, `wp_insert_term`, `wp_update_term`, `wp_delete_term`,
`wp_set_object_terms`, `wp_delete_object_term_relationships`), nunca SQL directo. `site` é um
parâmetro em cada tool, não uma ligação fixa por site.
## Arquitectura de dados (confirmada no código-fonte do plugin em 19-08-2026)
O HappyFiles Pro **não tem tabelas próprias** para pastas de media library. Cada "pasta" é um termo
da taxonomia custom `happyfiles_category` (constante `HAPPYFILES_TAXONOMY`), registada só para o
post type `attachment`, **hierárquica** (usa `parent`/`term_id` nativos do WordPress). Cada
ficheiro (attachment) pertence a uma pasta através de uma relação normal
`wp_term_relationships` — exactamente como categorias/tags em posts.
- **Contagem de ficheiros por pasta** = `WP_Term->count`, mantida automaticamente pelo WordPress via
`_update_generic_term_count` (callback registado pelo próprio plugin) sempre que
`wp_set_object_terms`/`wp_delete_term` corre. Nunca precisa de ser recalculada manualmente.
- **Metadados extra por pasta** (termmeta, opcionais): `happyfiles_position` (inteiro, ordenação
manual na UI) e `happyfiles_folder_color` (cor custom). Ambos ausentes por omissão.
- **"Sem pasta" (Uncategorized)** não é um termo real — é `tax_query` com `operator => NOT EXISTS`
sobre `happyfiles_category`. O próprio `Data::get_folders()` do plugin usa a convenção
`term_id = -1` para esta pseudo-pasta e `-2` para "Todos os ficheiros"; este MCP replica a mesma
convenção internamente (`folderId: -1` nas tools de listagem de ficheiros).
- **Gotcha WP_Query confirmado ao vivo:** `tax_query` por omissão tem `include_children => true`
(taxonomia hierárquica) — listar ficheiros de uma pasta-pai sem `include_children: false` devolve
também os ficheiros de todas as subpastas (reproduzido: pasta "Clientes" mostrava 120 ficheiros em
vez dos 7 directamente associados). Este MCP força sempre `include_children => false`, replicando
o mesmo padrão que o próprio `Actions::delete_folder()` do plugin usa para não apagar relações de
subpastas por engano.
- **`query_var` da taxonomia é `false`** (`public => false` na chamada `register_taxonomy`, confirmado
via `get_taxonomy('happyfiles_category')->query_var`) — `wp post list --happyfiles_category=X`
**não funciona**, nem via WP-CLI nem em pedidos normais; o próprio plugin nunca depende disto,
constrói sempre `tax_query` explícito em PHP. Por isso este MCP também nunca usa esse atalho.
- **Apagar pasta nunca apaga ficheiros:** `Actions::delete_folder()` (e o `wp_delete_term()` nativo
que ele chama) só remove a linha da taxonomia e as relações `wp_term_relationships` — os posts
`attachment` continuam a existir, só passam a "Sem pasta". Confirmado no código-fonte
(`includes/actions.php`) e testado ao vivo neste MCP (ver secção Verificação).
- **Uma pasta por ficheiro por omissão:** `wp_set_object_terms($id, [$folderId], $taxonomy, false)`
com `append=false` **substitui** a pasta anterior — é o comportamento por omissão do HappyFiles
Pro (`happyfiles_multiple_folders` = false). Este MCP replica exactamente essa semântica em
`happyfiles_move_files`.
## Sites conhecidos
Chamar `happyfiles_list_sites` para a lista actual. Confirmados HappyFiles Pro 1.8.3 activo ao vivo
em 19-08-2026: `descomplicar` (produção, `/home/ealmeida/public_html` — 66 pastas reais em uso,
1218 attachments, 893 sem pasta), `emanuelalmeida` (`emanuelalmeida.pt`), `starter`
(`starter.descomplicar.pt`). Também aceita um path absoluto directamente
(`/home/ealmeida/<site>`) para sites fora desta lista, sem exigir rebuild do MCP.
## Tools (9)
| Tool | Read-only | Uso |
|---|---|---|
| `happyfiles_list_sites` | sim | Aliases conhecidos, path WP, nota de estado |
| `happyfiles_get_status` | sim | Plugin instalado/activo + versão |
| `happyfiles_list_folders` | sim | Todas as pastas: id, nome, slug, `parent`, `count`, `position`, `color`, mais `all_files_count`/`uncategorized_count` |
| `happyfiles_create_folder` | não | Nova pasta, `parentId` opcional para subpasta |
| `happyfiles_rename_folder` | não | Novo nome + slug recalculado |
| `happyfiles_delete_folder` | não | **Nunca apaga ficheiros.** `deleteSubfolders:true` apaga em cascata; por omissão as subpastas sobem para o pai (comportamento nativo `wp_delete_term`) |
| `happyfiles_list_files_in_folder` | sim | Ficheiros **directamente** numa pasta (não inclui subpastas), paginado |
| `happyfiles_list_uncategorized_files` | sim | Ficheiros sem nenhuma pasta atribuída, paginado |
| `happyfiles_move_files` | não | 1+ ficheiros para uma pasta (substitui a pasta anterior) ou `folderId: null` para desassociar (Sem pasta) |
Todas as tools exigem `site` como primeiro parâmetro.
## Segurança
Nenhuma escrita usa SQL directo nem classes internas do plugin (não são autoload-seguras fora do
wp-admin) — só as funções nativas de taxonomia do WordPress, as mesmas que os handlers AJAX do
próprio HappyFiles Pro chamam (`includes/actions.php`, `includes/ajax.php`), pelo que os hooks
(`created_term`, `edited_term`, `delete_term`, `set_object_terms`) disparam sempre. O corpo PHP
corrido por `wp eval` é sempre um template estático sem texto do utilizador interpolado; nomes de
pasta e IDs de ficheiros viajam em JSON codificado em base64 pelo **STDIN** do processo remoto
(`php://stdin` dentro do PHP, `stream_get_contents(STDIN)`), nunca na linha de comando — evita por
completo ter de escapar aspas/unicode num nome de pasta para a shell remota.
## Verificação
Construído e testado ponta-a-ponta contra produção e staging em 19-08-2026:
- **Leituras cruzadas independentes** (`descomplicar`, produção): `happyfiles_list_folders` devolveu
66 pastas, `all_files_count: 1218` e `uncategorized_count: 893` — confirmados byte-a-byte via
`wp eval`/`wp post list --format=count` directos fora do MCP. `happyfiles_list_files_in_folder`
na pasta "Clientes" (id 42) devolveu `total: 7` depois de corrigido o gotcha `include_children`
(tinha devolvido 120 antes da correcção); confirmado via `wp post term list <id>
happyfiles_category` em vários ficheiros individuais devolvidos.
- **Ciclo de escrita completo em `starter`** (site vazio de pastas, 7 attachments — sandbox seguro):
`happyfiles_create_folder` → `happyfiles_rename_folder` → `happyfiles_move_files` (ficheiro 19
para a pasta nova, confirmado via `wp post term list` directo) → `happyfiles_list_files_in_folder`
(1 item) / `happyfiles_list_uncategorized_files` (6 itens, confirmado) → `happyfiles_move_files`
de volta a `null` (confirmado `wp post term list` vazio de novo) → `happyfiles_delete_folder` →
estado final confirmado idêntico ao inicial (`wp term list` vazio, `wp post list --format=count`
ainda 7, `wp post get 19` com o título original intacto).
## Skills relacionadas
Nenhuma skill de conhecimento dedicada ao HappyFiles Pro existe neste bundle — esta skill é a única
documentação (arquitectura de dados + tools). Ver `wp-cli` para o padrão geral de gestão via
WP-CLI no servidor CWP.
+42
View File
@@ -0,0 +1,42 @@
---
name: mcp-ifthenpay
description: MCP dedicado Ifthenpay WooCommerce (Multibanco, MB WAY, Payshop, CC, Cofidis Pay) para gestão de gateways, estado e URLs de callback na frota CWP
version: 1.0.0
author: Descomplicar®
---
# MCP Ifthenpay WooCommerce (`mcp-ifthenpay`)
> Servidor MCP StreamableHTTP (porta 3206) para gestão, diagnóstico e teste de gateways de pagamento **Ifthenpay** em lojas WooCommerce nos sites da frota CWP.
---
## 1. Ferramentas Disponíveis
| Ferramenta | Descrição | Parâmetros |
|---|---|---|
| `ifthen_list_supported_sites` | Lista os sites WooCommerce conhecidos na frota. | `{}` |
| `ifthen_get_gateways_status` | Estado resumido e configuração de todos os 5 métodos Ifthenpay. | `site?` |
| `ifthen_get_gateway_config` | Opções completas de configuração de um método específico (chaves mascaradas). | `site?`, `gateway` (`multibanco` \| `mbway` \| `payshop` \| `creditcard` \| `cofidispay`) |
| `ifthen_get_order_payment_info` | Obtém entidade/referência Multibanco ou telemóvel MB WAY de uma encomenda. | `site?`, `order_id` |
| `ifthen_get_callback_url` | Gera o URL de callback exato para configurar no Backoffice da Ifthenpay. | `site?`, `gateway` |
---
## 2. Exemplo de Utilização
```typescript
// Obter estado de todos os gateways Ifthenpay na loja demo
const status = await use_mcp_tool({
server_name: "ifthenpay",
tool_name: "ifthen_get_gateways_status",
arguments: { site: "ecommerce-demo.descomplicar.pt" }
});
// Obter URL de callback para MB WAY
const callback = await use_mcp_tool({
server_name: "ifthenpay",
tool_name: "ifthen_get_callback_url",
arguments: { site: "ecommerce-demo.descomplicar.pt", gateway: "mbway" }
});
```
@@ -0,0 +1,134 @@
---
name: mcp-insert-headers-footers
description: MCP dedicado multi-site (node stdio, ligação `insert-headers-footers` em ~/.omp/agent/mcp.json) para ler e escrever os 3 blocos globais de código (header/body/footer) do plugin Insert Headers and Footers (agora rebrandeado "WPCode Lite", slug `insert-headers-and-footers`) via WP-CLI/SSH. ⚠️ Superfície de risco real — estes 3 blocos injectam HTML/JS/CSS arbitrário em TODAS as páginas do site (produção incluída); uma escrita mal formada pode partir o site inteiro ou abrir um vector XSS. Site é um parâmetro em cada tool. Usar quando "insert headers and footers", "wpcode lite", "header footer mcp", "injectar script no site todo", "código global wordpress", "meta pixel site inteiro", "gtm noscript global", "ihaf mcp".
layer: wiki
---
# /mcp-insert-headers-footers — MCP dedicado multi-site ao Insert Headers and Footers (WPCode Lite)
Projecto em `/media/ealmeida/Dados/Dev/mcp-insert-headers-footers/` (TypeScript, SDK MCP oficial,
stdio). Sem código PHP novo no WordPress — cada tool executa `wp eval` com um snippet PHP estático
(nunca dados do utilizador interpolados no comando) via `ssh server` sobre o WP-CLI já instalado.
**`site` é um parâmetro em cada tool**, não uma ligação fixa — o plugin está confirmado `active`
(WPCode Lite 2.3.8) nos 8 sites reais do bundle.
## ⚠️ Superfície de risco
Este plugin injecta os 3 blocos em **todas as páginas de todo o site**, via `wp_head`,
`wp_body_open` e `wp_footer`. Não há "sandbox" nem preview automático — o que estiver gravado sai
directamente no HTML servido a todos os visitantes assim que a cache de página for purgada (ou
imediatamente, se não houver cache activa). Riscos concretos:
- **Quebra visual/funcional site-wide** — uma tag HTML não fechada, um `<script>` malformado, ou
CSS inline quebrado no bloco `body` (que corre logo a seguir a `<body>`) pode desalinhar ou
esconder o site inteiro para todos os visitantes.
- **Vector XSS real** — qualquer conteúdo aqui corre sem sanitização nenhuma no frontend
(`echo wp_unslash($code)` directo, sem `esc_html`/`wp_kses`). Nunca aceitar conteúdo de origem
não confiável para estes blocos.
- **Perda de conteúdo** — a escrita é **substituição total** do bloco, não merge. Ler sempre
primeiro com `ihaf_get_blocks`; se o bloco já tiver código em produção (ex. Meta Pixel + GTM
noscript em `descomplicar`), o conteúdo completo desejado (antigo + novo) tem de ser fornecido
explicitamente.
A tool de escrita (`ihaf_set_block`) está marcada `destructiveHint:true` e a sua descrição já
inclui este aviso — mas o agente que a chama é responsável por: (1) ler o bloco antes de escrever,
(2) testar em `starter` (staging) sempre que possível antes de `descomplicar` (produção), (3)
purgar cache de página e verificar o site visualmente depois de escrever.
## Sites conhecidos
Chamar `ihaf_list_sites` para a lista actual com path e nota de estado. Todos os 8 sites do bundle
verificados `active` (WPCode Lite 2.3.8) ao vivo em 19-08-2026: `descomplicar` (produção — bloco
`body` **já em uso**: Meta Pixel + GTM noscript, não sobrescrever sem ler primeiro),
`emanuelalmeida`, `starter` (os 3 blocos vazios — site preferencial para testes), `ccv`, `care`,
`ecommerce-demo`, `e-commerce`, `ecommerce`. Também aceita um path absoluto directamente
(`/home/ealmeida/<site>`) para sites fora desta lista.
## Tools (4)
| Tool | Read-only | Uso |
|---|---|---|
| `ihaf_list_sites` | sim | Aliases conhecidos, path WP, nota de estado |
| `ihaf_get_plugin_status` | sim | Instalado/activo + versão — chamar antes se não tiver a certeza de que o plugin está activo nesse site |
| `ihaf_get_blocks` | sim | Os 3 blocos (`header`/`body`/`footer`) no texto lógico exacto que é servido ao frontend, mais `headersFootersMode` informativo. Chamar SEMPRE antes de `ihaf_set_block` |
| `ihaf_set_block` | não — `destructiveHint:true` | Substitui por inteiro UM dos 3 blocos (`block`: `header`\|`body`\|`footer`). `content` é o texto final completo, sem escaping adicional (o MCP trata do `wp_slash()` internamente) |
Todas as tools exigem `site` como primeiro parâmetro.
## Mapeamento de dados (a única documentação destes 3 blocos)
Não há skill de conhecimento separada para este plugin — este é o mapeamento completo.
Os 3 blocos vivem em **3 `wp_options` planas** (não uma option única, não uma tabela custom):
| Bloco | Option | Hook de output | Posição no HTML |
|---|---|---|---|
| `header` | `ihaf_insert_header` | `wp_head` | Dentro de `<head>` |
| `body` | `ihaf_insert_body` | `wp_body_open` (prioridade 1) | Logo a seguir à abertura de `<body>` |
| `footer` | `ihaf_insert_footer` | `wp_footer` | Antes de `</body>` |
Código fonte confirmado (`includes/global-output.php`): cada option é lida com `get_option()` cru
e impressa com `echo wp_unslash( $code )`, sem qualquer sanitização. Ficam vazias por omissão
(`get_option()` devolve `''`), o output é simplesmente ignorado se `empty( trim( $code ) )`.
Também são ignoradas em `is_admin()`, `is_feed()`, `is_robots()`, `is_trackback()`.
### ⚠️ Gotcha crítico de formato — slashing, não JSON
Diferente do gotcha do WP Fastest Cache (JSON string vs option tipada), aqui o problema é
**slashing do WordPress**, não JSON. O ecrã de admin do plugin grava directamente a partir de
`$_REQUEST` (`includes/admin/pages/class-wpcode-admin-page-headers-footers.php`):
```php
update_option( 'ihaf_insert_header', $_REQUEST['ihaf_insert_header'] );
```
Como o WordPress aplica `addslashes()` globalmente a todos os superglobais logo no arranque
(`wp_magic_quotes()`), o valor **gravado em `wp_options` fica sempre "slashed"** — aspas e
backslashes escapados. No frontend, o output faz sempre `wp_unslash( $code )` antes de imprimir.
Ou seja: **a option em repouso na base de dados nunca é o texto lógico da página** — é sempre esse
texto com `addslashes()` aplicado.
Confirmado ao vivo em produção (19-08-2026), bloco `body`:
```
BD (raw): style=\"display:none\"
Frontend: style="display:none"
```
Por isso este MCP **nunca** usa `wp option get/update` em bruto sobre estas 3 keys. Usa sempre
`wp eval` com as funções nativas `wp_unslash()` (leitura) e `wp_slash()` (escrita) — nunca
reimplementa `addslashes`/`stripslashes` em TypeScript. `ihaf_get_blocks` devolve sempre o texto
lógico (pós-`wp_unslash`, o que realmente sai no HTML); `ihaf_set_block` recebe o texto lógico
desejado e aplica `wp_slash()` do lado do servidor antes de `update_option()`. Sem este passo,
qualquer backslash literal no conteúdo (regex JS, `\n` em atributos, etc.) seria corrompido no
próximo `wp_unslash()` do frontend.
Qualquer MCP futuro sobre uma option que seja alimentada a partir de `$_REQUEST`/formulário admin
deve verificar primeiro se o plugin em causa depende deste padrão de slashing implícito do
WordPress antes de escolher `wp option get/update` directo.
## Segurança
Allowlist fixa das 3 option keys (`src/wpcli.ts`, `BLOCK_OPTION_KEYS`) — nunca `wp option update`
genérico sobre uma chave arbitrária. Conteúdo sempre via base64 em stdin, nunca interpolado no
comando SSH remoto; o único dado interpolado directamente no snippet PHP é o nome da option, que
vem de uma allowlist fixa de 3 valores (não de input do utilizador), tal como o padrão já usado em
`mcp-wpfc`. A escrita também invalida a page cache do próprio plugin quando disponível
(`wpcode_clear_all_plugins_page_cache('global')`), mas **não** purga WP Fastest Cache/Cloudflare —
isso é responsabilidade de quem chama a tool (ver `mcp-wpfc`/`mcp-cloudflare-app`).
## Verificação
Construído e testado ponta-a-ponta 19-08-2026: `ihaf_get_plugin_status` e `ihaf_get_blocks`
confirmados contra produção (`descomplicar`) — bloco `body` com Meta Pixel + GTM noscript lido
correctamente já unslashed, idêntico ao confirmado por leitura SSH directa fora do MCP
(`wp_unslash(get_option(...))`). Write round-trip testado em `starter` (staging, blocos vazios):
escrita de conteúdo com aspas e backslash duplo via `ihaf_set_block`, releitura via
`ihaf_get_blocks` confirmou o texto lógico exacto preservado (aspas e backslashes intactos),
depois revertido para `""` e confirmado por leitura SSH directa (`get_option()` == `""`) e por
`ihaf_get_blocks` limpo, sem alterar o estado original do site.
## Skills relacionadas
- `mcp-wpfc` — purga de page cache local, complementar depois de alterar estes blocos.
- `wp-cli` — padrões gerais de WP-CLI multi-site usados como base deste MCP.
+107
View File
@@ -0,0 +1,107 @@
---
name: mcp-jetengine
description: MCP dedicado (node stdio, ligação `jetengine` em ~/.omp/agent/mcp.json) para JetEngine (Crocoblock) em descomplicar.pt — proxy transparente stdio<->HTTPS do MCP nativo do próprio plugin (Features API, v3.8.0+): registar CPT, CCT, taxonomias, meta boxes, queries do Query Builder, listings, glossários, gerir módulos, ler configuração/website-config/macros. Inclui playbooks (sequências de chamadas) para directório dinâmico CPT+Query+Listing, CCT de alta performance, activação de módulos opcionais. Não duplica lógica do plugin — reencaminha `tools/list`/`tools/call` tal como o JetEngine os expõe. Usar quando "mcp jetengine", "jetengine mcp", "criar cpt via mcp", "criar cct via mcp", "add-cpt", "add-cct", "query builder mcp", "jetengine features api", "ai command center jetengine", "playbook jetengine", "directorio dinamico jetengine", "activar modulo jetengine".
layer: wiki
---
# /mcp-jetengine — MCP dedicado ao JetEngine
Projecto em `/media/ealmeida/Dados/Dev/mcp-jetengine/` (TypeScript, SDK MCP oficial, stdio).
**Diferente dos MCPs irmãos (`mcp-wpfc`, `mcp-webp-express`):** o JetEngine, a partir da v3.8.0,
já expõe o seu próprio servidor MCP nativo (`Jet_Engine\MCP_Tools`, "Features API") em
`{site}/wp-json/jet-engine/v1/mcp` — JSON-RPC 2.0 sobre HTTP simples (POST, uma única resposta
JSON por pedido, sem SSE, sem sessão), autenticado por `current_user_can('manage_options')`. Este
projecto **não reimplementa nenhuma lógica de negócio** do plugin: é apenas uma ponte
stdio↔HTTPS com autenticação Basic (Application Password), porque o harness liga-se a servidores
MCP locais por stdio, não a endpoints HTTPS arbitrários fora do gateway. `tools/list` e
`tools/call` são reencaminhados verbatim — os 11 tools, nomes, descrições e schemas vistos pelo
agente são exactamente os que o próprio plugin regista, não uma cópia mantida à mão.
## Site
Único site do bundle com JetEngine activo (19-08-2026): **descomplicar.pt**
(`jet-engine` v3.8.11.1). Configurável via `JETENGINE_SITE_URL`/`JETENGINE_APP_USER`/
`JETENGINE_APP_PASSWORD` no `env` da entrada `jetengine` em `mcp.json` — sem parâmetro `site` por
tool (ao contrário de `wpfc`/`webpx`) porque só há um site a servir; adicionar suporte
multi-site (registo tipo `sites.ts`) se outro site do bundle activar o plugin.
**Activação feita nesta sessão (19-08-2026):** `jet-engine-misc-settings` não existia
(`enable_features_api` teria o default `true` mesmo assim, mas foi escrito explicitamente);
Application Password dedicada `mcp-jetengine` criada para `ealmeida`.
## Âmbito
Tudo o que o plugin regista na sua própria Features API. Não cobre: Relations (tabela
`wp_jet_rel_default` — sem tool nativo, ver `references/automation.md` da skill `jetengine` para
gestão via PHP/SQL), Profile Builder, Dynamic Tags dentro do Elementor, bridge MU-plugin
(`descomplicar-jet-bridge.php`, endpoint próprio `POST /wp-json/descomplicar/v1/manage` — projecto
à parte, ver skill `jetengine` §"Bridge Plugin").
## Tools (11, nomes nativos do plugin — 19-08-2026)
| Tool | Read-only | Uso |
|---|---|---|
| `resource-get-configuration` | sim | CPTs, taxonomias, meta boxes, options pages, queries, relations, CCTs, endpoints REST, glossários — visão geral em JSON, por `parts` |
| `resource-get-website-config` | sim | Post types/taxonomias/plugins activos registados no WordPress (não só JetEngine) |
| `resource-get-macros` | sim | Todas as macros JetEngine com argumentos e exemplos de uso |
| `tool-manage-modules` | sim/não (`operation: list\|activate\|deactivate`) | Listar ou activar/desactivar módulos do JetEngine |
| `tool-add-cpt` | não | Registar Custom Post Type — labels, storage, meta fields, admin columns/filters |
| `tool-add-cct` | não | Registar Custom Content Type (tabela SQL própria) com campos tipados |
| `tool-add-taxonomy` | não | Registar taxonomia e associar a post types |
| `tool-add-meta-box` | não | Meta Box com campos e condições de visibilidade |
| `tool-add-query` | não | Query do Query Builder — `sql`, `posts`, `terms`, `users`, `comments`, `repeater`, `current-wp-query`, `merged-query`, `relations-query` |
| `tool-add-listing` | não | Listing item ligado a uma query existente (`elementor`/`bricks`/`twig`/`blocks`) |
| `tool-add-glossary` | não | Glossário manual (`source: "manual"`) com pares `value`/`label` |
Schemas de input completos: chamar `tools/list` (ou o `initialize` seguido de `tools/list` — o
proxy devolve exactamente o que `wp-json/jet-engine/v1/mcp` responde). Não documentados campo a
campo aqui para evitar duplicação que fica desactualizada — a fonte de verdade é o próprio plugin.
## Playbooks — como combinar as tools
Referência da API acima diz o que cada tool faz isoladamente; `references/playbooks.md` mostra
sequências reais de chamadas:
1. **Ler antes de criar** — `resource-get-configuration` para evitar slugs/queries duplicados.
2. **Módulos** — confirmar/activar módulo opcional (`custom-content-types`, `maps-listings`, etc.)
antes de usar a tool que depende dele; CPT/Taxonomia/Meta Box/Query/Listing normais não
precisam de nenhum módulo.
3. **CPT + Taxonomia + Meta Box** — conteúdo editorial estruturado, três chamadas em sequência.
4. **CCT** — dados de alta performance (requer módulo `custom-content-types`).
5. **Glossário** — opções reutilizáveis para campos `select`/`checkbox`/`radio`.
6. **Query Builder + Listing** — directório dinâmico; inclui o gotcha do `enum` de `query_type`
não incluir `"custom-content-type"` apesar da descrição da tool o mencionar.
7. **Levantar contexto** — macros, config, plugins activos antes de escrever conteúdo dinâmico.
## Segurança
- Autenticação por Application Password de um utilizador `administrator` (`manage_options`) — o
próprio plugin recusa (`rest_forbidden`, 401/403) sem essa capability; o proxy traduz isso num
`McpError` claro em vez de deixar a chamada rebentar em silêncio.
- Sem lógica local de bloqueio de chaves — todo o `tools/call` chega ao plugin tal como recebido
do agente. As guardas de segurança (validação de slug, sanitização de campos, etc.) são as do
próprio JetEngine, não deste MCP.
- Erros JSON-RPC de nível transporte (rede, HTTP != 200, `error` no envelope JSON-RPC — método
desconhecido, parse error) tornam-se `McpError`. Erros de execução de tool (`isError: true` no
`CallToolResult`, ex. "Tool not found") são devolvidos tal como o plugin os construiu — nunca
convertidos em excepção, para o agente poder ler o texto do erro.
## Verificação
Construído e testado ponta-a-ponta 19-08-2026: `tools/list` via stdio devolve os 11 tools
idênticos ao pedido directo por `curl` contra `wp-json/jet-engine/v1/mcp` (mesmos nomes,
descrições, schemas); `tools/call` real (`resource-get-macros`) devolve o mesmo JSON do plugin;
`tools/call` com nome inexistente devolve `isError: true` + `"Tool not found"` passado
verbatim. Ferramentas de escrita (`tool-add-cpt`, `tool-add-cct`, etc.) não foram exercidas contra
produção nesta sessão — mesmo caminho de código (`callJetEngine` + `tools/call`) já validado pelas
chamadas de leitura e pelo caso de erro.
## Skills relacionadas
- `jetengine` — conhecimento do plugin (CPT vs CCT, Relations, Query Builder, Listings, Macros,
Profile Builder, bridge MU-plugin). Consultar para entender o que uma tool faz antes de a
chamar; este MCP é o "como executar".
- `mcp-wpfc` / `mcp-webp-express` — MCPs irmãos para plugins sem API nativa (reimplementam a
gestão via WP-CLI/SSH). `mcp-jetengine` é o primeiro caso do bundle em que o próprio plugin já
fala MCP — o padrão aqui é ponte de transporte, não reimplementação.
@@ -0,0 +1,152 @@
# Playbooks — mcp-jetengine
Sequências de chamadas às 11 tools nativas para tarefas reais. Todos os exemplos usam
`tools/call` com `{name, arguments}` tal como o agente vê através do proxy (`mcp-jetengine`
reencaminha verbatim). Exemplos escritos a partir do `inputSchema` real de cada tool
(`tools/list`, 19-08-2026) — não exercidos contra produção nesta sessão (ver SKILL.md
§Verificação); ler o `inputSchema` ao vivo antes de confiar cegamente num nome de campo.
## 0. Regra geral — ler antes de criar
Antes de registar qualquer estrutura nova, chamar `resource-get-configuration` (com os `parts`
relevantes: `post_types`, `taxonomies`, `meta_boxes`, `queries`, `custom_content_types`,
`glossaries`) para confirmar que o slug não existe já e para reutilizar queries/glossários
existentes em vez de duplicar. Evita a classe de erro "criei um CPT com o mesmo slug de outro já
existente" e mantém o site em conformidade com a regra anti-duplicação do projecto.
## 1. Módulos — pré-requisito antes de CCT, Mapas, REST API Listings
`custom-content-types`, `maps-listings`, `rest-api-listings`, `gallery-grid`, `gallery-slider`,
`qr-code`, `calendar`, `listing-injections`, `profile-builder`, `dynamic-visibility`,
`data-stores`, `fullwidth-block-editor` são **módulos opcionais** (JetEngine > Modules) — só
`booking-forms` estava activo em descomplicar.pt a 19-08-2026. CPT, Taxonomias, Meta Boxes,
Query Builder e Listings "normais" (não-mapa) são **núcleo do plugin**, sempre disponíveis, sem
módulo a activar.
```json
// 1. Confirmar estado actual
{"name": "tool-manage-modules", "arguments": {"operation": "list"}}
// 2. Activar o que falta (ex. antes de criar um CCT)
{"name": "tool-manage-modules", "arguments": {"operation": "activate", "modules": ["custom-content-types"]}}
```
`operation: "activate"`/`"deactivate"` aceitam vários slugs de uma vez em `modules`. Chamar
`tool-add-cct` sem o módulo `custom-content-types` activo é o erro mais provável de quem salta
este passo.
## 2. CPT + Taxonomia + Meta Box — conteúdo editorial estruturado
Fluxo para um tipo de conteúdo público (ex. "Casos de Estudo", "Formadores"): CPT primeiro,
taxonomia associada ao CPT, depois campos.
```json
// 1. Post Type
{"name": "tool-add-cpt", "arguments": {
"general_settings": {"name": "Casos de Estudo", "slug": "caso-estudo"},
"advanced_settings": {"public": true, "show_in_rest": true, "supports": ["title", "editor", "thumbnail"]}
}}
// 2. Taxonomia associada
{"name": "tool-add-taxonomy", "arguments": {
"general_settings": {"name": "Sectores", "object_type": ["caso-estudo"]},
"advanced_settings": {"public": true, "hierarchical": true, "show_in_rest": true}
}}
// 3. Meta Box com campos
{"name": "tool-add-meta-box", "arguments": {
"general_settings": {"name": "Detalhes do Caso", "object_type": "post", "allowed_post_type": ["caso-estudo"]},
"meta_fields": [
{"name": "cliente", "type": "text", "title": "Cliente", "is_required": true},
{"name": "resultado_percent", "type": "number", "title": "Resultado (%)"}
]
}}
```
`meta_fields` aceita descritores simplificados (`name`/`type`/`title`, como acima) OU o array
JetEngine "cru" completo (`object_type: "field"`, mesmo formato usado pela UI/`references/
automation.md` da skill `jetengine`) quando é preciso uma opção que o descritor simplificado não
cobre (ex. referência a um glossário — ver §4).
## 3. CCT — dados de alta performance
Usar quando o volume/velocidade de leitura importa mais do que integração nativa WP (ver tabela
CPT vs CCT na skill `jetengine`). Requer o módulo `custom-content-types` activo (§1).
```json
{"name": "tool-add-cct", "arguments": {
"name": "Leads Formulário",
"slug": "leads_formulario",
"fields": [
{"field_name": "nome", "field_type": "text", "is_key_field": "yes"},
{"field_name": "email", "field_type": "text", "is_key_field": "yes"},
{"field_name": "criado_em", "field_type": "datetime", "save_as_timestamp": true}
]
}}
```
## 4. Glossário — opções reutilizáveis
Glossário = lista de pares `value`/`label` reutilizável em vários campos `select`/`checkbox`/
`radio`. A tool nativa só cobre o **registo do glossário**:
```json
{"name": "tool-add-glossary", "arguments": {
"name": "Estados de Proposta",
"source": "manual",
"fields": [
{"value": "rascunho", "label": "Rascunho", "is_checked": true},
{"value": "enviada", "label": "Enviada"},
{"value": "aceite", "label": "Aceite"}
]
}}
```
**Não confirmado nesta sessão:** o schema simplificado de `tool-add-meta-box.meta_fields` não
expõe uma propriedade explícita "glossário" — só `options`/`choices` inline. Ligar um campo a um
glossário existente é uma funcionalidade da UI do JetEngine (campo → Options Type → Glossary);
para o fazer via este MCP, passar o item do array `meta_fields` no formato JetEngine "cru"
(`object_type: "field"`, com a chave de opções apontando para o glossário) — confirmar a chave
exacta lendo `resource-get-configuration` num meta box existente que já use um glossário, ou a
UI, antes de replicar às cegas.
## 5. Query Builder + Listing — directório dinâmico
```json
// 1. Query (posts normais)
{"name": "tool-add-query", "arguments": {
"name": "Casos de Estudo Recentes",
"query_type": "posts",
"query_args": {"post_type": "caso-estudo", "posts_per_page": 12, "orderby": "date", "order": "DESC"}
}}
// 2. Listing ligado à query
{"name": "tool-add-listing", "arguments": {
"title": "Grid Casos de Estudo",
"query_id": 123,
"view_type": "elementor"
}}
```
**Gotcha de schema (JetEngine, não deste proxy):** a descrição de `tool-add-query` menciona
`query_type: "custom-content-type"` para consultar um CCT, mas o `enum` real do campo `query_type`
não inclui esse valor — só `sql`, `posts`, `terms`, `users`, `comments`, `repeater`,
`current-wp-query`, `merged-query`, `relations-query`. Para consultar um CCT usar
`query_type: "posts"` com `query_args.post_type` no formato `"<cct-slug>/<name>"` (mesmo exemplo
que a descrição da tool dá) — o `enum` é a fonte vinculativa, a descrição está desactualizada.
Confirmar contra `tools/list` ao vivo antes de assumir que isto mudou.
Query `sql` exige `query_args.sql` não-vazio; qualquer outro tipo exige argumentos estilo
`WP_Query` (`tax_query`, `meta_query`, etc.) — chamadas sem `query_args` são rejeitadas pelo
plugin ("Requests without query_args are invalid").
## 6. Levantar contexto (macros, config, plugins activos)
```json
{"name": "resource-get-macros", "arguments": {}}
{"name": "resource-get-website-config", "arguments": {"parts": {"post_types": true, "taxonomies": true, "active_plugins": true}}}
{"name": "resource-get-configuration", "arguments": {"parts": {"queries": true, "custom_content_types": true, "glossaries": true}}}
```
Útil antes de escrever texto/URLs dinâmicos num Listing Template ou numa Query — `%macro%` só
funciona se a macro existir e aceitar os argumentos passados.
+112
View File
@@ -0,0 +1,112 @@
---
name: mcp-kivicare
description: MCP dedicado (node stdio, ligação `kivicare` em ~/.omp/agent/mcp.json) para o KiviCare Clinic Management System (Free e Pro) em WordPress via WP-CLI/SSH. 24 tools tipadas — clínicas, médicos/profissionais, horários semanais, pacientes (perfil 360º e histórico médico), consultas (listar/detalhe/agendar/mudar estado com disparo de hooks nativos/reagendar), encontros clínicos, prescrições e facturação. Usar quando "kivicare", "consulta médica", "agendar consulta", "médicos clínica", "paciente kivicare", "encontro clínico", "familyclinic.pt" ou qualquer operação clínica no KiviCare.
---
# /mcp-kivicare — MCP KiviCare Clinic Management System
MCP dedicado ao **KiviCare Clinic Management System** (Free e Pro) em WordPress via WP-CLI/SSH (ambiente padrão `familyclinic.pt` em `/home/familycl/public_html`, com suporte a `care.descomplicar.pt`).
- **Código-fonte:** `/media/ealmeida/Dados/Dev/mcp-kivicare/` (TypeScript)
- **Ligação:** `kivicare` em `~/.omp/agent/mcp.json` (transporte `stdio` via Node)
- **Target:** `familyclinic.pt` (servidor Hetzner CWP, prefixo `wpej_`, DB `familycl_wp417`)
---
## 1. Arquitectura & Dupla Ponte
O `mcp-kivicare` resolve todas as operações através de uma arquitectura de dupla ponte:
1. **Bridge SQL Directo** (`wp db query --raw` sobre SSH):
- Executa consultas estruturadas com MariaDB `JSON_OBJECT()` e `JSON_ARRAYAGG()`.
- Paginação optimizada por subquery (`LIMIT` / `OFFSET` na query interna).
- Respostas instantâneas em milissegundos sem arranque/bootstrap do WordPress.
2. **Bridge PHP Seguro** (`wp eval-file -` com JSON via STDIN):
- Para mutações (criação de pacientes, agendamento de consultas, mudança de estado, reagendamento, novos encontros clínicos).
- Garante transacções atómicas na base de dados e dispara as acções e filtros nativos do WordPress/KiviCare (ex: `do_action('kc_appointment_status_update', ...)`), permitindo que plugins integradores (WhatsSMS, SincCare, email) enviem as notificações automáticas.
- No check-in de consulta (estado `4`), cria automaticamente o encontro clínico (`kc_patient_encounters`) caso ainda não exista.
---
## 2. Catálogo Completo de Tools (24)
### 2.1 Clínicas & Dashboard
| Tool | O que faz |
|---|---|
| `kc_get_clinic_summary` | Resumo estatístico geral: consultas por estado, pacientes, médicos, encontros, receita paga vs pendente e consultas de hoje. |
| `kc_list_clinics` | Lista todas as clínicas registadas com contactos, moradas e especialidades. |
| `kc_get_clinic` | Detalhes completos de uma clínica pelo seu ID. |
### 2.2 Médicos & Profissionais
| Tool | O que faz |
|---|---|
| `kc_list_doctors` | Lista médicos com especialidades, contactos e clínicas atribuídas. |
| `kc_get_doctor` | Perfil detalhado de um médico pelo ID. |
| `kc_get_doctor_sessions` | Horários e sessões semanais de atendimento (dias da semana, início/fim, slot em minutos). |
| `kc_list_receptionists` | Lista de funcionários da recepção por clínica. |
### 2.3 Serviços & Preçário
| Tool | O que faz |
|---|---|
| `kc_list_services` | Catálogo de serviços clínicos e psicológicos com preços base e categorias. |
| `kc_get_service_doctor_mappings` | Atribuição de serviços a médicos específicos com preços customizados e durações. |
### 2.4 Pacientes
| Tool | O que faz |
|---|---|
| `kc_list_patients` | Pesquisa e lista pacientes com paginação e filtros (nome, email, telefone, clínica). |
| `kc_get_patient` | Perfil 360º: dados pessoais, grupo sanguíneo, contagens e últimas consultas/encontros. |
| `kc_get_patient_medical_history` | Histórico clínico, alergias e patologias registadas do paciente. |
| `kc_create_patient` | Regista novo paciente no KiviCare (utilizador WP `kivicare_patient` + `basic_data` + clínica). |
| `kc_update_patient` | Actualiza contactos e dados pessoais de um paciente existente. |
### 2.5 Consultas & Marcações
| Tool | O que faz |
|---|---|
| `kc_list_appointments` | Lista e filtra consultas por médico, paciente, clínica, estado (0-4) ou datas. |
| `kc_get_today_appointments` | Agenda de consultas do dia actual. |
| `kc_get_appointment` | Detalhe completo de uma consulta com serviços associados, notas e estado. |
| `kc_create_appointment` | Agenda nova consulta com médico, paciente, clínica e serviços. |
| `kc_update_appointment_status` | Altera estado de consulta (0=Cancelled, 1=Booked, 2=Pending, 3=Check-Out, 4=Check-In) com hooks. |
| `kc_reschedule_appointment` | Reagenda uma consulta para nova data e hora. |
### 2.6 Encontros Clínicos & Prescrições
| Tool | O que faz |
|---|---|
| `kc_list_encounters` | Lista encontros clínicos abertos (1) ou fechados (0). |
| `kc_get_encounter` | Detalhes do encontro com sintomas, prescrições e factura associada. |
| `kc_create_encounter` | Abre um novo encontro clínico. |
### 2.7 Facturação
| Tool | O que faz |
|---|---|
| `kc_list_bills` | Lista facturas com filtro por estado (`paid`/`unpaid`), clínica e datas. |
| `kc_get_bill` | Detalhes da factura com itens, quantidades, preços e descontos. |
---
## 3. Códigos de Estado das Consultas
No KiviCare, o campo `status` das consultas (`kc_appointments`) segue a seguinte convenção canónica:
- `0` — **Cancelled** (Consulta cancelada)
- `1` — **Booked** (Consulta agendada / confirmada)
- `2` — **Pending** (Consulta pendente de confirmação)
- `3` — **Check-Out** (Consulta concluída / finalizada)
- `4` — **Check-In** (Paciente presente / em atendimento — cria encontro clínico automaticamente)
---
## 4. Variáveis de Ambiente
| Variável | Padrão | Descrição |
|---|---|---|
| `KC_SSH_HOST` | `server` | Host SSH no `~/.ssh/config` |
| `KC_WP_PATH` | `/home/familycl/public_html` | Directório raiz da instalação WordPress |
| `KC_PHP_BIN` | `/opt/alt/php-fpm82/usr/bin/php` | Caminho do executável PHP no servidor |
| `KC_WP_CLI` | `/usr/local/bin/wp` | Caminho do WP-CLI no servidor |
| `KC_DB_PREFIX` | `wpej_` | Prefixo das tabelas da base de dados |
---
*mcp-kivicare | Descomplicar® | v1.0.0 | 2026-08-19*
@@ -0,0 +1,65 @@
---
name: mcp-loco-translate
description: MCP dedicado ao Loco Translate para gerir e editar traduções de plugins, temas e core WordPress (PO/MO/POT) na frota CWP
version: 1.0.0
author: Descomplicar®
---
# MCP Loco Translate (`mcp-loco-translate`)
> Servidor MCP StreamableHTTP (porta 3205) para gestão de ficheiros de tradução `.po`, `.mo` e `.pot` através das APIs internas do **Loco Translate** (v2.8.8+) nos 13 sites da frota WordPress no CWP.
---
## 1. Ferramentas Disponíveis
| Ferramenta | Descrição | Parâmetros |
|---|---|---|
| `loco_list_supported_sites` | Lista todos os 8 sites suportados na infraestrutura. | `{}` |
| `loco_list_bundles` | Lista todos os plugins, temas e core com suporte de tradução. | `site?`, `type?: "all" \| "plugin" \| "theme" \| "core"` |
| `loco_get_bundle_details` | Devolve projetos, domínios e ficheiros PO/MO/POT por locale. | `site?`, `handle` (obrigatório) |
| `loco_get_po_strings` | Lê mensagens (msgid, msgstr, estado) de um ficheiro PO específico. | `site?`, `po_path`, `limit?`, `offset?`, `filter?` |
| `loco_update_translation` | Atualiza o `msgstr` de um `msgid` no PO e compila o binário MO. | `site?`, `po_path`, `msgid`, `msgstr` |
| `loco_compile_mo` | Compila diretamente um ficheiro PO para o formato binário MO. | `site?`, `po_path` |
---
## 2. Exemplo de Utilização
### Listar Plugins e Ficheiros de Tradução
```typescript
// 1. Obter detalhes do bundle
const details = await use_mcp_tool({
server_name: "loco-translate",
tool_name: "loco_get_bundle_details",
arguments: {
site: "emanuelalmeida.pt",
handle: "click-to-chat-for-whatsapp/click-to-chat.php"
}
});
// 2. Ler strings do ficheiro PO em pt_PT
const strings = await use_mcp_tool({
server_name: "loco-translate",
tool_name: "loco_get_po_strings",
arguments: {
site: "emanuelalmeida.pt",
po_path: "plugins/click-to-chat-for-whatsapp/languages/click-to-chat-pt_PT.po",
filter: "WhatsApp"
}
});
```
### Atualizar Tradução e Compilar MO
```typescript
await use_mcp_tool({
server_name: "loco-translate",
tool_name: "loco_update_translation",
arguments: {
site: "emanuelalmeida.pt",
po_path: "plugins/click-to-chat-for-whatsapp/languages/click-to-chat-pt_PT.po",
msgid: "Chat on WhatsApp",
msgstr: "Conversar no WhatsApp"
}
});
```
+117
View File
@@ -0,0 +1,117 @@
---
name: mcp-pro-elements
description: MCP dedicado (node stdio, ligação `pro-elements` em ~/.omp/agent/mcp.json) para gerir o PRO Elements (fork GPL do Elementor Pro) em starter/ccv/care.descomplicar.pt — estado/versão do plugin, os 4 módulos com toggle próprio via Elementor Experiments (mega_menu, size_variables, atomic_form, pro_interactions), reCAPTCHA v2/v3 das Forms (secret sempre mascarada), licença (vestígio histórico, sempre mascarada), restrições do Element Manager por role (read-only), via WP-CLI/SSH. Usar quando "pro elements", "elementor pro fork", "modulos pro elements", "experiment mega menu", "recaptcha forms elementor", "licenca pro elements", "elementor experiment toggle".
layer: wiki
---
# /mcp-pro-elements — MCP dedicado ao PRO Elements
Projecto em `/media/ealmeida/Dados/Dev/mcp-pro-elements/` (TypeScript, SDK MCP oficial, stdio).
Sem código PHP novo no WordPress — cada tool executa `wp option get/update` via `ssh server` sobre
o WP-CLI já instalado. Não é uma extensão do EMCP Tools (plugin comercial Freemius, catálogo de
tools fixo curado pelo autor, sem mecanismo de terceiros).
## O que é o PRO Elements (achado por leitura de código-fonte, 19-08-2026)
**Não** é um addon pack de terceiros como o Element Pack Pro (BdThemes) — é literalmente o
código-fonte do **Elementor Pro**, rebrandeado: `pro-elements.php` define
`ELEMENTOR_PRO_VERSION`/`namespace ElementorPro`, os mesmos `core/`, `modules/`, `base/` do
Elementor Pro genuíno. A diferença está em `license/api.php`: `License\API::get_license_data()`
está **hardcoded** para devolver sempre `success:true, expires:'lifetime'` com a lista completa de
features Pro — não há licença real a validar. `wp elementor-pro license activate/deactivate`
(comando WP-CLI próprio) tem os métodos vazios (no-ops). A própria description do plugin em
care.descomplicar.pt confirma: *"This plugin enables GPL features of Elementor Pro... is not a
substitute for Elementor Pro."*
## Âmbito
Ao contrário do Element Pack Pro (~150 widgets num único array com toggle individual), o PRO
Elements **não tem ecrã "Modules" com toggle por widget**. Só **4 dos ~80 módulos** têm toggle
próprio, via **Elementor Experiments** (`wp_options.elementor_experiment-<nome>`, string simples
`active`/`inactive`/`default`):
| Tool id | Experiment | Default | Disponível |
|---|---|---|---|
| `mega_menu` | `mega-menu` | inactive | 3.31.2 e 3.35.0 (confirmado nos 3 sites) |
| `size_variables` | `e_pro_variables` | active | só 3.35.0 (starter/ccv) — pasta ausente em 3.31.2 (care) |
| `atomic_form` | `e_pro_atomic_form` | inactive | só 3.35.0 |
| `pro_interactions` | `e_pro_interactions` | inactive | só 3.35.0 |
Os restantes módulos (query-control, popup, theme-builder, forms, woocommerce, etc.) activam
sempre — `Module::is_active()` por omissão `true` + licença hardcoded sempre completa.
A única **configuração geral real e gravável** é o **reCAPTCHA das Forms** (v2/v3). Não existe
"Google Maps API key" própria do PRO Elements — o Google Maps usa a key do Elementor **core**
(`elementor_google_maps_api_key`), fora do âmbito deste MCP.
## Sites conhecidos
Confirmado por `wp plugin get pro-elements` nos 3 (19-08-2026) — únicos do bundle com o plugin
activo:
- `starter` → starter.descomplicar.pt, v3.35.0
- `ccv` → ccv.descomplicar.pt, v3.35.0
- `care` → care.descomplicar.pt, v3.31.2
`descomplicar.pt` (produção), `emanuelalmeida.pt` e os sites `*ecommerce*` **não** têm o plugin.
## Tools (8)
| Tool | Read-only | Uso |
|---|---|---|
| `pe_list_sites` | sim | Aliases conhecidos + versão confirmada, sem SSH |
| `pe_get_status` | sim | Versão/estado do plugin via `wp plugin get` |
| `pe_get_modules` | sim | Estado dos 4 módulos: `installed_in_this_version` (dir existe nesta versão), `raw_state` (valor gravado ou `null`), `effective_active` (resolvido incl. default) |
| `pe_set_module_state` | não | `active`/`inactive`/`default` num módulo; recusa se `installed_in_this_version=false` (nunca escreve lixo sem efeito) |
| `pe_get_recaptcha_settings` | sim | Site keys em claro (públicas), secret keys sempre mascaradas (`{configured, length}`) |
| `pe_set_recaptcha_settings` | não | Actualiza campos v2/v3 passados; `threshold` só válido em v3, 0–1 |
| `pe_get_license_status` | sim | Vestígio de `elementor_pro_license_key` mascarado + nota fixa sobre o bypass hardcoded |
| `pe_get_role_restrictions` | sim | `elementor_pro_element_manager_role_permission` — restrições de widgets por role (vazio nos 3 sites) |
## Segurança
- Allowlist fixa de option keys (`src/wpcli.ts`) — nunca `wp option update` genérico.
- **Secrets nunca em claro**: `secret_key` do reCAPTCHA e `elementor_pro_license_key` passam
sempre por `describeSecret()` → só `{configured: boolean, length: number}`. Site keys reCAPTCHA
são públicas por natureza (embutidas em JS no browser) e são devolvidas em claro.
- Escritas via base64 em stdin, nunca interpoladas no comando SSH remoto.
- `elementor_experiment-*` e as chaves `elementor_pro_recaptcha_*` são strings escalares simples
(nunca `--format=json` — corromperia o valor, mesmo gotcha da skill `mcp-wpfc`).
`elementor_pro_element_manager_role_permission` é o único array PHP nativo do allowlist —
lido com `--format=json`, nunca escrito por aqui (formato complexo por-widget-por-role gerido
pela app React do editor, sem valor real a expor um write raw).
- `elementor_pro_license_key` fica fora da allowlist de escrita — o runtime ignora-a de qualquer
forma (ver acima), escrevê-la não activaria nada.
## Verificação
Construído e testado ponta-a-ponta contra produção (19-08-2026), os 3 sites:
- `pe_get_status` idêntico a `wp plugin get pro-elements` directo em starter (3.35.0), ccv
(3.35.0) e care (3.31.2).
- `pe_get_modules(care)` confirmou `installed_in_this_version` correcto — só `mega_menu=true`
(os outros 3 `false`, ausência das pastas `modules/{variables,atomic-form,interactions}`
confirmada em disco); `pe_get_modules(ccv)` confirmou os 4 `true` e `size_variables.
effective_active=true` (resolve o default `active` porque `raw_state` nunca foi gravado).
- `pe_get_recaptcha_settings(care)` idêntico à leitura directa (`site_key`/`secret_key` vazios,
`threshold=0.5`).
- `pe_get_license_status`: `care` → `{configured:true, length:45}` (o vestígio real de uma
instalação Elementor Pro genuína anterior, encontrado em `wp_options`); `starter`/`ccv` →
`{configured:false}` (nunca tiveram licença real).
- **Escrita com round-trip idempotente**, duas vezes: `pe_set_module_state(care, mega_menu,
"default")` — valor antes/depois idêntico (`"default"`), confirmado por `wp option get`
directo fora do MCP; `pe_set_recaptcha_settings(care, v3, threshold=0.5)` — valor antes/depois
idêntico (`0.5`), confirmado da mesma forma.
- Guard testado: `pe_set_module_state(care, size_variables, "active")` recusado com erro claro
(módulo não existe em 3.31.2) — nenhuma escrita tentada.
- Nenhum segredo devolvido em claro em qualquer chamada.
## Skills relacionadas
- `mcp-element-pack` — Element Pack Pro (BdThemes), um addon pack de terceiros genuíno, com
~150 widgets num único array de toggle — **não confundir** com este MCP (o PRO Elements é o
próprio Elementor Pro rebrandeado, mecanismo de toggle completamente diferente).
- `element-pack-widgets` — catálogo/inserção de widgets Element Pack via EMCP; não aplicável ao
PRO Elements (os widgets Pro do Elementor inserem-se pela ligação EMCP normal do site, não por
este MCP de configuração admin).
- `mcp-wpfc` — mesmo padrão de gotcha JSON vs string escalar em `wp option get/update`.
+113
View File
@@ -0,0 +1,113 @@
---
name: mcp-rank-math
description: MCP dedicado (node stdio, ligação `rank-math` em ~/.omp/agent/mcp.json) para o Rank Math SEO/Rank Math Pro em descomplicar.pt via WP-CLI/SSH. 27 tools tipadas — meta de post/taxonomia, schema markup, redirects, módulos, sitemap, 404 monitor, analytics GSC, IndexNow, validação SEO ao vivo. Preferir a este MCP em vez dos snippets manuais `wp eval`/`wp db query` da skill `/rank-math` sempre que a operação já esteja coberta aqui. Usar quando "rank math", "meta SEO", "schema markup", "redirects seo", "404 monitor", "módulos rank math", "seo score", "sitemap rank math", "indexnow", "validar seo página".
layer: wiki
---
# /mcp-rank-math — MCP dedicado Rank Math SEO / Rank Math Pro
Projecto em `/media/ealmeida/Dados/Dev/mcp-rank-math/` (TypeScript, SDK MCP oficial, stdio).
Substitui a execução manual dos comandos `wp eval`/`wp db query`/`wp option patch` documentados na
skill `/rank-math` por 27 tools tipadas, com validação de input e capability boundaries claras.
**Ambas as skills coexistem** — `/rank-math` continua a ser a referência para o que ainda não tem
tool dedicada (ver "Fora do âmbito" abaixo); este MCP é o caminho preferido para tudo o resto.
## Porquê duas pontes internas (SQL vs PHP-eval)
Rank Math guarda a maior parte dos dados **serializados em PHP** — a tabela
`rank_math_redirections.sources`, quase todas as `rank-math-options-*`, e qualquer meta de
post/term com valor array (`rank_math_robots`, `rank_math_schema_*`). SQL directo lê o blob
serializado em bruto; só o WordPress core (`get_option()`, `get_post_meta()`,
`maybe_unserialize()`) descodifica isso correctamente. Por isso o MCP usa duas pontes:
- **SQL directo** (`wp db query --raw`) — só contra tabelas confirmadas 100% planas:
`posts`/`postmeta` (joins de auditoria), `rank_math_404_logs`, `rank_math_analytics_gsc`.
- **`wp eval-file -`** (PHP inteiro recebido via STDIN, nunca interpolado na linha de comando) —
tudo o resto: meta com arrays, schema, redirects (via `\RankMath\Redirections\DB`, para disparar
os side-effects certos, nunca `INSERT`/`UPDATE` SQL directo nessa tabela), módulos, options com
`wp option patch` (nunca `wp option update`, que substitui a option inteira e apaga as outras
sub-chaves).
Valores dinâmicos (títulos, descrições, schema JSON) nunca são interpolados directamente numa
string PHP: viajam como JSON (`json_decode(...)` no lado PHP), evitando tanto injecção como o bug
clássico de `"$5 off"` a ser lido como interpolação de variável dentro de uma string PHP de aspas
duplas.
## Gotcha real de produção — `rank_math_modules`
A option tem **chaves não-sequenciais** (buracos de remoções antigas de módulos sem
`array_values()`). `wp option get --format=json` faz `json_encode()` directo do array PHP
subjacente — um array PHP com chaves não-consecutivas serializa como **objecto** JSON
(`{"0":"a","3":"b"}`), não array. O MCP normaliza isto internamente (`rm_get_plugin_status`,
`rm_list_modules`); se algum dia se ler `rank_math_modules` directamente via `wp option get`
fora do MCP, **não assumir que a saída é sempre um array JSON**.
## Tools (27)
**Estado/auditoria:** `rm_get_plugin_status` (versões Free/Pro, licença, nº módulos activos),
`rm_audit_missing_meta`, `rm_audit_seo_scores`, `rm_get_top_404s`, `rm_clear_404_logs`
(`confirm: true` para apagar tudo)
**Meta de post:** `rm_get_post_meta`, `rm_update_post_meta` (parcial — `null` num campo apaga-o),
`rm_bulk_fill_missing_meta` (`dry_run: true` por omissão — só lista, não escreve)
**Meta de taxonomia:** `rm_get_term_meta`, `rm_update_term_meta`
**Schema markup:** `rm_get_post_schema`, `rm_set_post_schema` (`schema_type` alfanumérico —
"Article", "FAQPage", "Product", "LocalBusiness", etc.), `rm_delete_post_schema` (omitir
`schema_type` apaga todos — requer `confirm: true`)
**Options/módulos:** `rm_get_option`, `rm_set_option` (sempre `wp option patch` internamente —
nunca perde sub-chaves irmãs), `rm_list_modules`, `rm_toggle_module`
**Redirects:** `rm_list_redirects`, `rm_create_redirect`, `rm_update_redirect`,
`rm_delete_redirects`
**Sitemap:** `rm_regenerate_sitemap`, `rm_list_sitemap_urls` (HTTP directo ao `sitemap_index.xml`
público — sem SSH, reflecte exactamente o que o Google vê)
**IndexNow:** `rm_submit_indexnow` (HTTP directo à API do IndexNow; só lê a chave via WP-CLI)
**Analytics:** `rm_get_analytics_overview` — degrada com `available:false` se o módulo/tabela GSC
não existir, em vez de erro
**Backup/validação:** `rm_export_seo_snapshot` (devolve os dados; não escreve ficheiro no
servidor), `rm_validate_live_seo` (fetch directo ao URL publicado — título, meta description,
canonical, robots, OG tags, tipos JSON-LD tal como renderizados, sem SSH)
## Segurança
- `rm_clear_404_logs` sem `older_than_days` e `rm_delete_post_schema` sem `schema_type` exigem
`confirm: true`.
- `rm_set_option` nunca aceita substituir a option inteira — só `wp option patch` num caminho de
sub-chave.
- `rm_submit_indexnow` e o ramo de escrita de `rm_update_redirect`/`rm_create_redirect` têm efeito
público real (notificação a motores de busca, redireccionamento de tráfego real) — confirmar
intenção antes de invocar em massa.
## Fora do âmbito (usar `/rank-math` para estes)
WooCommerce SEO em massa (categorias de produto, atributos — os padrões `wp eval` já documentados
na skill `/rank-math` §9 continuam válidos), News/Video Sitemap (meta dedicada por post, §10),
hooks/filtros PHP persistentes (§11), migração Yoast→Rank Math (§migração), provisioning inicial
de um site novo (§13 — sequência de setup, não uma operação repetível), gestão de licença/ligação
de conta Pro (ver skill `/full-cliente`).
## Verificação
Construído e testado ponta-a-ponta contra produção 19-08-2026: leituras confirmadas com dados
reais (distribuição de scores 383 bons/36 ok/116 fracos, 404s reais, 21 módulos activos, sitemap
com 9 entradas, validação ao vivo de uma página publicada com título/description/canonical/OG
idênticos ao que `rm_get_post_meta` devolve para o mesmo post). Escritas testadas sem alterar
dados reais: `rm_set_option`/`rm_update_post_meta` escreveram o mesmo valor já existente (no-op
real, confirmado por leitura pós-escrita); ciclo completo `rm_create_redirect`→
`rm_delete_redirects` com um redirect de teste, confirmado removido a seguir. Um bug real
(`rank_math_modules` object-vs-array) foi apanhado neste smoke test e corrigido antes de
considerar o MCP pronto — ver README do projecto.
## Nota
`rm_get_post_meta`/`rm_get_term_meta` num ID inexistente devolvem campos vazios em vez de erro
(comportamento do próprio `get_post_meta()`/`get_term_meta()` do WordPress core, que não avisa
para ID inválido) — confirmar a existência do post/termo antes de interpretar "tudo vazio" como
"sem meta definida".
@@ -0,0 +1,81 @@
---
name: mcp-seriously-simple-podcasting
description: MCP dedicado (HTTP StreamableHTTP local, ligação `seriously-simple-podcasting` em ~/.claude.json, porta 3203) para o Seriously Simple Podcasting em emanuelalmeida.pt (produção, 39 episódios reais em 8 shows) — episódios (CPT `podcast`), shows/séries (taxonomia `series`) e definições de show (título, owner, categorias iTunes, explicit/complete). Escritas sempre via bootstrap WordPress (`wp_insert_post`/`wp_update_post`/`wp_insert_term`/`update_option`), nunca SQL directo. Usar quando "episódio podcast", "criar episódio", "show podcast", "definições de show", "categoria itunes", "feed rss emanuelalmeida", "apagar episódio", "estatísticas podcast", ou qualquer gestão de conteúdo de podcast em emanuelalmeida.pt.
layer: wiki
---
# /mcp-seriously-simple-podcasting — MCP dedicado Seriously Simple Podcasting (emanuelalmeida.pt)
Projecto em `/media/ealmeida/Dados/Dev/mcp-seriously-simple-podcasting/` (TypeScript, SDK MCP
oficial, StreamableHTTP). O REST API nativo do plugin é escrita-capaz para episódios mas
**read-only para definições de show** (`wp/v2/series` só regista `get_callback`, achado
confirmado lendo `Rest_Api_Controller::create_api_series_fields()`) — por isso este MCP usa um
único mecanismo consistente para tudo (episódios e shows): o bootstrap WordPress completo via
`wp eval`, nunca REST nem SQL directo para escritas.
**Zona-alvo: emanuelalmeida.pt (produção), 39 episódios reais em 8 shows.**
## Arquitectura — sem tocar no filesystem do servidor
Leituras: SQL directo via `$wpdb` (rápido, tipado). Escritas: SEMPRE pelas funções nativas do
WordPress (`wp_insert_post`/`wp_update_post`/`wp_delete_post`, `wp_insert_term`/`wp_delete_term`,
`wp_set_post_terms`, `update_post_meta`, `update_option`) — nunca INSERT/UPDATE directo, para
preservar hooks de feed/permalinks/sanitização nativa. Ambas correm por SSH via `wp eval`, com o
corpo PHP + dados de entrada codificados em base64 dentro de uma variável de ambiente remota —
**nunca ficheiros escritos/apagados no servidor**, o que evitaria deliberadamente o GATE 5.1 de
mutação de infra-estrutura. Ver `src/wp-bridge.ts` (`runPhp()`) — mesmo mecanismo do
`mcp-fluent-crm`.
## Tools (14)
**Leitura (7):** `ssp_list_episodes` (filtros série/estado/pesquisa), `ssp_get_episode`,
`ssp_list_series`, `ssp_get_series_settings` (corrige o bug do REST nativo para
explicit/complete), `ssp_get_stats`, `ssp_get_feed_url`, `ssp_get_itunes_categories` (referência
estática, sem chamada SSH).
**Escrita (7):** `ssp_create_episode`, `ssp_update_episode`, `ssp_delete_episode` (trash por
omissão, `force=true` apaga permanentemente), `ssp_set_episode_series`, `ssp_create_series`,
`ssp_update_series_settings`, `ssp_delete_series` (destructive — ver guarda abaixo).
## Guardas de segurança
- `ssp_update_series_settings`/`ssp_create_series` validam `category`/`category2`/`category3` e
`subcategory`/`subcategory2`/`subcategory3` contra a lista oficial Apple Podcasts **antes** de
qualquer chamada SSH (rejeita categoria inexistente ou par categoria/subcategoria incoerente com
erro accionável).
- `ssp_delete_series` recusa remover um show com episódios associados sem `force=true` — **mas
atenção:** a contagem usada (`term->count`) só reflecte episódios **publicados**; um show só com
episódios em `draft` passa como "vazio" mesmo tendo conteúdo associado. Confirmar sempre com
`ssp_list_episodes({series_id, status: 'any'})` antes de forçar a remoção.
- `ssp_delete_episode` por omissão move para o lixo (recuperável); `force=true` apaga
permanentemente.
## Fora do v1 (decisão deliberada)
Gestão da ligação Castos/hosting (não aplicável — desligada neste site), upload directo de
ficheiros de áudio (o plugin só aceita URL, sem endpoint de upload próprio — usar Media Library
via ferramentas WordPress genéricas e passar o URL resultante), CRUD de tags normais (`post_tag`)
— usar ferramentas WordPress genéricas para isso, o CPT `podcast` é standard.
## Credenciais
`.env` local (`WP_SSH_HOST=server`, `WP_PATH=/home/ealmeida/emanuelalmeida.pt`), não versionado.
Sem tokens/API keys — a autenticação é a própria sessão SSH root já configurada em
`~/.ssh/config`.
## Verificação
Construído e testado ponta-a-ponta contra a zona real 19-08-2026: todas as 14 tools exercitadas
manualmente (incl. guarda de categoria inválida e guarda de show com episódio publicado) + 13/13
evaluations automatizadas (`npm run eval:ci`), que criam e limpam os seus próprios artefactos
(`__mcp_eval__`) sem tocar em dados reais. Deploy como processo `hub` persistente
(`mcp-seriously-simple-podcasting`, porta 3203, `http://127.0.0.1:3203/mcp`), registado em
`~/.claude.json` como `seriously-simple-podcasting`.
## Skills relacionadas
- `seriously-simple-podcasting` — conhecimento profundo do plugin: arquitectura CPT/taxonomia,
achado central do REST nativo (episódios escrita-capaz, shows read-only), schema de
`wp_options` das definições de show (com o gotcha explicit/complete), categorias iTunes, feed
RSS, wp-admin — consultar antes de desenhar uma integração ou entender um comportamento que este
MCP não cobre.
+112
View File
@@ -0,0 +1,112 @@
---
name: mcp-skyboot-icons
description: MCP dedicado (node stdio, ligação `skyboot-icons` em ~/.omp/agent/mcp.json) para o Skyboot Custom Icons for Elementor — confirmado activo só em descomplicar.pt (produção). Estado do plugin, catálogo dos 15 icon packs bundlados (Elegant, Linearicons, Themify, Simple Line, Line, Ionicons, Icofont, Line Awesome, Material Design, Devicons, Icomoon, Elusive, Iconic, Brands, Open Iconic) com prefixo CSS e contagem de ícones ao vivo, listar os nomes de ícone de um pack activo, activar/desactivar um pack no selector de ícones do Elementor. Usar quando "skyboot icons", "custom icons elementor", "icon pack elementor", "activar pack de ícones", "material design icons elementor", "brands icon elementor", "quantos ícones tem o pack X".
layer: wiki
---
# /mcp-skyboot-icons — MCP dedicado ao Skyboot Custom Icons for Elementor
Projecto em `/media/ealmeida/Dados/Dev/mcp-skyboot-icons/` (TypeScript, SDK MCP oficial, stdio).
Sem código PHP novo no WordPress — leituras via `wp option get`/`wp plugin list` e `wp eval` sobre o
próprio filtro que o plugin usa (`elementor/icons_manager/additional_tabs`); a única escrita é
`wp option update` sobre o option nativo do plugin.
## Âmbito real do plugin (investigado no código-fonte v1.2.0 em 19-08-2026)
O Skyboot Custom Icons for Elementor **não gere ícones custom enviados pelo utilizador** — é um
plugin utilitário que embrulha **15 fontes de ícones de terceiros já bundladas** (CSS + fontes
estáticas em `assets/`, ~18k linhas de PHP a registá-las) e as regista no selector de ícones nativo
do Elementor via `add_filter('elementor/icons_manager/additional_tabs', ...)`, uma classe PHP
dedicada por pack em `classes/icons/class-<pack>-icon.php`.
A **única configuração real e gravável** é um interruptor on/off por pack, tudo dentro de UM único
wp_option:
- **`skb_cife_manage_icon`** — array associativo nativo `{ pack_key: "on"|"off" }`, um por cada um
dos 15 packs. Confirmado como **array PHP nativo** (não string com JSON dentro) — `wp option get`
com e sem `--format=json` devolvem a mesma estrutura, logo `--format=json` é seguro tanto para
leitura como escrita. `Skb_Cife_Base::include_files()` só faz `require_once` da classe de um pack
se a sua chave estiver `"on"` — um pack desactivado nunca chega a registar o seu tab no Elementor
nem a enfileirar o CSS (`classes/class-scripts-manager.php` segue exactamente a mesma condição
para o `wp_enqueue_style`).
- **`skb_cife_version`** — string simples com a versão gravada na última activação/upgrade
(`Skb_Cife_Base::activate()`/`check_for_updates()`).
Não há upload de packs SVG custom, não há gestão de licença PRO gravável (o gate PRO é só
`defined('SKB_CIFE_PRO_VERSION')` + filtro `skb_cife_is_pro`, sem UI de licença na Free), e não há
nenhuma outra opção do utilizador exposta pelo `settings-api/` além deste único toggle por pack —
confirmado por leitura de `settings-api/settings-fields.php::get_icon_packs()` (15 entradas, sem
mais nada). Por isso este MCP é **maioritariamente só-leitura com uma única tool de escrita real**,
em vez de inventar capacidades que o plugin não tem.
## Sites conhecidos
Chamar `skyboot_list_sites` para a lista actual. Verificado ao vivo via `wp plugin list` nos 8 sites
reais do bundle em 19-08-2026: **só `descomplicar` (produção, `/home/ealmeida/public_html`)** tem o
plugin instalado (v1.2.0, activo). Os outros 7 (`starter`, `emanuelalmeida`, `ccv`, `care`,
`ecommerce-demo`, `e-commerce`, `ecommerce`) não o têm. Também aceita um path absoluto directamente
(`/home/ealmeida/<site>`) para sites fora desta lista, sem exigir rebuild do MCP.
Estado verificado em produção em 19-08-2026: **todos os 15 packs activos** (`"on"`), PRO **não**
activo, Elementor 4.2.2, PHP 8.2.31.
## Tools (5)
| Tool | Read-only | Uso |
|---|---|---|
| `skyboot_list_sites` | sim | Aliases conhecidos, path WP, nota de estado |
| `skyboot_get_status` | sim | Instalado/activo, versão (plugin list + option), PRO activo, Elementor/PHP vistos pelo plugin, contagem de packs activos/totais |
| `skyboot_list_icon_packs` | sim | 15 packs: nome, descrição, fonte, contagem aproximada estática, estado activo/inactivo (`skb_cife_manage_icon`) e, se activo, prefixo CSS real + contagem exacta de ícones ao vivo |
| `skyboot_get_icon_pack_icons` | sim | Nomes de ícone (sufixo, sem prefixo CSS) de UM pack **activo**, com pesquisa por substring e paginação (`limit`/`offset`) |
| `skyboot_set_icon_pack_state` | não | Activa/desactiva um pack — merge sobre o `skb_cife_manage_icon` actual, as outras chaves ficam intactas |
Todas as tools exigem `site` como primeiro parâmetro. `skyboot_get_icon_pack_icons` e
`skyboot_set_icon_pack_state` exigem `pack` como uma das 15 chaves de `ICON_PACK_KEYS` (validado
pelo próprio schema Zod da tool — `elegant_icon`, `linearicons_icon`, `themify_icon`,
`simpleline_icon`, `line_icon`, `ion_icon`, `icofont_icon`, `lineawesome_icon`,
`materialdesign_icon`, `devicons_icon`, `icomoon_icon`, `elusive_icon`, `iconic_icon`,
`brands_icon`, `open_iconic_icon`).
**Gotcha ao consultar ícones:** `skyboot_get_icon_pack_icons` só funciona para um pack **já
activo** — se estiver desactivado a sua classe PHP nunca é carregada e o Elementor nunca viu o tab
(`tab_not_found`, mensagem de erro explícita a sugerir activar primeiro com
`skyboot_set_icon_pack_state`). Não há forma segura de ler os nomes de ícone de um pack desactivado
sem carregar código PHP arbitrário fora do fluxo normal do WordPress.
## Segurança
Todas as escritas usam `wp option update` sobre o option nativo do plugin, nunca SQL directo. O
merge (ler o option completo → alterar só a chave do pack pedido → escrever o objecto completo)
replica exactamente o que acontece quando um humano grava a página "Manage Icons" via
`options.php` (confirmado em `settings-api/class.settings-api.php::sanitize_options()` — o
`register_setting` por secção substitui o option inteiro, por isso o form da própria página também
submete sempre todas as checkboxes de uma vez). O JSON completo viaja em base64 pelo STDIN do
processo SSH remoto (`base64 -d | wp option update ... --format=json`), nunca interpolado na linha
de comando. As leituras dinâmicas (`skyboot_list_icon_packs`, `skyboot_get_icon_pack_icons`) usam
`wp eval` com um corpo PHP estático (template fixo, só os parâmetros `tabKey`/`search`/`limit`/
`offset` são interpolados via `JSON.stringify`, nunca texto livre do utilizador sem escaping JSON).
## Verificação
Construído e testado ponta-a-ponta contra produção (`descomplicar.pt`) em 19-08-2026:
- `npm run build` limpo, smoke test stdio (`initialize` + `tools/list`, 5 tools confirmadas).
- **3 leituras reais**: `skyboot_get_status` devolveu `v1.2.0 activo, PRO false, Elementor 4.2.2,
PHP 8.2.31, 15/15 packs activos` — confirmado byte-a-byte contra `wp plugin list`/`wp option get`
directos fora do MCP. `skyboot_list_icon_packs` devolveu as 15 entradas com contagens ao vivo
idênticas às lidas manualmente via `wp eval` (ex. `materialdesign_icon` 5346, `brands_icon` 934,
`icofont_icon` 2095). `skyboot_get_icon_pack_icons(brands_icon, search="adobe")` devolveu as 14
entradas Adobe (`adobe`, `adobeacrobatreader`, …, `adobexd`), idênticas ao array PHP lido
directamente do código-fonte da classe.
- **Escrita testada com ciclo completo idempotente**: `skyboot_set_icon_pack_state(open_iconic_icon,
false)` → confirmado `wp option get skb_cife_manage_icon` com `"open_iconic_icon":"off"` e as
outras 14 chaves intactas, e `apply_filters('elementor/icons_manager/additional_tabs', [])` já sem
o tab `skb_cife-open_iconic-icon` (`false`) → `skyboot_set_icon_pack_state(open_iconic_icon, true)`
→ confirmado `wp option get skb_cife_manage_icon` byte-a-byte idêntico ao estado inicial (todos os
15 packs `"on"`).
## Skills relacionadas
Nenhuma skill de conhecimento dedicada ao Skyboot Custom Icons for Elementor existe neste bundle —
esta skill é a única documentação (âmbito real + tools). Ver `elementor` para o ecossistema
Elementor/Crocoblock mais amplo e `wp-cli` para o padrão geral de gestão via WP-CLI no servidor CWP.
+48
View File
@@ -0,0 +1,48 @@
---
name: mcp-tablepress
description: MCP dedicado ao TablePress para consulta, listagem e edição de tabelas responsivas na frota CWP
version: 1.0.0
author: Descomplicar®
---
# MCP TablePress (`mcp-tablepress`)
> Servidor MCP StreamableHTTP (porta 3209) para gestão e edição programática de tabelas de dados (**TablePress** v3.3+) nos sites da frota WordPress no CWP.
---
## 1. Ferramentas Disponíveis
| Ferramenta | Descrição | Parâmetros |
|---|---|---|
| `tablepress_list_supported_sites` | Lista os sites com TablePress na frota. | `{}` |
| `tablepress_list_tables` | Lista todas as tabelas (ID, nome, descrição, linhas, colunas, shortcode). | `site?` |
| `tablepress_get_table` | Obtém dados completos e matriz de células de uma tabela. | `site?`, `table_id` |
| `tablepress_update_cell` | Atualiza o valor de uma célula específica `[linha, coluna]` (requer `confirm: true`). | `site?`, `table_id`, `row`, `col`, `value`, `confirm: true` |
---
## 2. Exemplo de Utilização
```typescript
// Listar tabelas disponíveis
const tables = await use_mcp_tool({
server_name: "tablepress",
tool_name: "tablepress_list_tables",
arguments: { site: "descomplicar.pt" }
});
// Atualizar valor de uma célula
await use_mcp_tool({
server_name: "tablepress",
tool_name: "tablepress_update_cell",
arguments: {
site: "descomplicar.pt",
table_id: "1",
row: 2,
col: 3,
value: "299€ / mês",
confirm: true
}
});
```
+129
View File
@@ -0,0 +1,129 @@
---
name: mcp-updraftplus
description: MCP dedicado multi-site (node stdio, ligação `updraftplus` em ~/.omp/agent/mcp.json) para consultar o UpdraftPlus (backup) em qualquer site do bundle Descomplicar® via WP-CLI/SSH — estado do plugin, agendamento, retenção, destinos de storage (nunca expõe credenciais), histórico de backups, último resultado. Superfície de ALTO RISCO tratada como só-leitura: a única escrita é disparar um backup manual (aditivo, nunca apaga nada), atrás de `confirm:true`. Site é um parâmetro em cada tool, não uma ligação fixa. Usar quando "updraftplus mcp", "estado backup wordpress", "agendamento updraftplus", "destinos storage backup", "histórico backups", "disparar backup manual mcp", "DISABLE_WP_CRON backup", "backup nunca corre sozinho".
layer: wiki
---
# /mcp-updraftplus — MCP dedicado multi-site ao UpdraftPlus
Projecto em `/media/ealmeida/Dados/Dev/mcp-updraftplus/` (TypeScript, SDK MCP oficial, stdio).
Mesmo padrão dos outros MCPs desta família (`mcp-wpfc`, `mcp-wpforms`, `mcp-fluentform`,
`mcp-fluent-smtp`, `mcp-bit-integrations`, `mcp-insert-headers-footers`): sem código PHP novo no
WordPress, cada tool executa `wp option get`/`wp plugin list`/`wp cron event list` via
`ssh server` sobre o WP-CLI já instalado. **`site` é um parâmetro em cada tool**, não uma ligação
fixa. Não existe comando WP-CLI próprio do UpdraftPlus (confirmado: `wp help` não lista nada,
`wp updraftplus` dá "not a registered command") — tudo passa por `wp_options` genéricas.
## Porque é que este MCP é (quase) só-leitura
Backups são a última linha de defesa. Este MCP **nunca** apaga, substitui ou reconfigura nada —
zero `update_option`/`delete_option` sobre qualquer chave `updraft_*`. A única acção que muda
estado é `updraftplus_trigger_backup_now`, e mesmo essa é puramente **aditiva** (dispara um novo
backup; nunca toca no que já existe) e exige `confirm: true` (zod `z.literal(true)`, rejeitado ao
nível do schema antes de o handler correr — testado ao vivo). Não há tools para desligar
agendamento, apagar um backup do histórico, ou mudar destinos de storage — de propósito, fora do
âmbito deste v1 por decisão de risco, não por limitação técnica.
## Sites conhecidos
Chamar `updraftplus_list_sites` para a lista actual. Verificado ao vivo 19-08-2026 via
`wp plugin list --format=json` nos 8 sites do bundle: só 4 têm o UpdraftPlus (Free 1.26.6, sem
addon Premium) — `starter`, `ccv`, `ecommerce-demo`, `ecommerce`. Os outros 4
(`descomplicar`/produção, `emanuelalmeida`, `care`, `e-commerce`) não têm o plugin, omitidos de
propósito do mapa de aliases. Também aceita um path absoluto directamente
(`/home/ealmeida/<site>`) para sites fora desta lista.
**Achado de risco real (19-08-2026):** nos 4 sites, `DISABLE_WP_CRON=true` está definido em
`wp-config.php` (confirmado por leitura directa), e nenhum tem `updraft_interval` alguma vez
gravado pela UI — ou seja, mesmo que alguém configurasse um agendamento na UI, ele nunca
dispararia sozinho neste servidor (WP-Cron pseudo-cron desligado, sem substituto por cron de
sistema para estes sites). `ccv` tem ainda um `updraft_last_backup`/`updraft_backup_history`
**ilusório**: os nomes de ficheiro lá dentro (`Starter_Descomplicar_...`) mostram que foi herdado
de um clone de base de dados do site `starter`, não um backup real de `ccv` — confirmado por
`siteurl` distinto entre os dois. `starter` teve 1 backup manual real em 2025-06-11. `ecommerce`
e `ecommerce-demo` nunca correram um backup (sem `updraft_backup_history` sequer). Ver notas por
site em `updraftplus_list_sites`/`src/sites.ts`.
## Tools (7)
| Tool | Read-only | Uso |
|---|---|---|
| `updraftplus_list_sites` | sim | Aliases conhecidos, path WP, nota de estado |
| `updraftplus_get_status` | sim | Free/Premium instalado+activo+versão, `DISABLE_WP_CRON`, última mensagem de log — chamar sempre primeiro |
| `updraftplus_get_schedule` | sim | Intervalo/retenção/horas de ficheiros e BD, flags de inclusão, próximos eventos de cron reais (`wp cron event list` filtrado) |
| `updraftplus_get_storage_destinations` | sim | Os ~15 destinos nativos, activo/não, `configuredInstanceCount` — **nunca devolve credenciais** |
| `updraftplus_get_backup_history` | sim | Histórico completo (`updraft_backup_history`): data, nonce, entidades, tamanho total, destino, versão — `limit` opcional |
| `updraftplus_get_last_backup` | sim | `updraft_last_backup`: sucesso/erros do resultado mais recente |
| `updraftplus_trigger_backup_now` | **não** | Dispara backup completo em background (não bloqueia), exige `confirm:true`. Ver secção própria abaixo |
Todas as tools exigem `site` como primeiro parâmetro.
## Segurança — mascaramento de credenciais
Cada destino de storage nativo (`updraft_dropbox`, `updraft_s3`, `updraft_googledrive`,
`updraft_ftp`, `updraft_sftp`, etc.) tem uma option própria cujo blob JSON PODE conter
credenciais em claro — confirmado por leitura directa (`appkey`/`secret`/`tk_access_token` no
Dropbox, `accesskey`/`secretkey` no S3, `clientid`/`secret`/`token` no Google Drive,
`host`/`user`/`pass` no FTP — mesmo quando vazias, a estrutura já existe). `getStorageDestinations`
(`src/wpcli.ts`) lê essas options só para calcular localmente um booleano `configured`
(`hasConfiguredValue` — recursivo, conta strings não-vazias; números e booleanos nunca contam,
excepto exclusão explícita do campo `authurl` que vem pré-preenchido pelo próprio plugin com o
endpoint fixo da Rackspace mesmo sem nunca ter sido tocado — falso positivo confirmado e corrigido
durante os testes). **O conteúdo em si nunca sai da função** — o tool só devolve
`{slug, label, active, instanceCount, configuredInstanceCount}`.
## `updraftplus_trigger_backup_now` — mecânica e decisões de risco
Invoca literalmente a mesma `do_action('updraft_backupnow_backup_all', array())` que o botão
"Backup Now" da UI dispara (`admin.php:2700`, `request_backupnow()`), com opções vazias = inclui
tudo o que o site tiver configurado para incluir, envia para os destinos já activos. **Nunca
apaga nem substitui um backup existente** — só adiciona um novo ao histórico.
Corre em **background** no servidor via `nohup … & disown` — a chamada SSH regressa em segundos
mesmo que o backup real demore minutos, confirmado ao vivo (PID devolvido imediatamente,
processo real confirmado a correr via `ps aux` depois da ligação SSH já ter fechado). Isto é
necessário, não cosmético: como `DISABLE_WP_CRON=true` nestes 4 sites, agendar via
`wp cron event schedule … now` nunca dispararia sozinho — o único caminho real para um backup
correr fora de alguém clicar na UI é invocar a `do_action` directamente. O próprio plugin já
protege contra execuções concorrentes (semáforo interno `UpdraftPlus_Semaphore`), por isso
disparar duas vezes seguidas é seguro — a segunda fica bloqueada com log "another backup is
apparently already running", não corrompe nada.
**Gotcha de infra local — GATE 5.1:** o comando remoto NUNCA usa `>`/`>>` para o log
(`~/.config/agent-gates/infra-mutation-check.sh`, o motor que bloqueia mutações de infra sem
autorização humana prévia, trata qualquer `>` seguido de caminho absoluto como potencialmente
destrutivo — testado ao vivo, `>>` também dispara porque a regex só procura um carácter `>`
seguido de `/`, independente de quantos `>` há). A saída é capturada com
`| tee -a "$logFile" > /dev/null` — `tee -a` não contém `>` no texto do comando, e a redirecção
final para `/dev/null` tem excepção explícita no motor do gate. Validado directamente contra
`infra-mutation-check.sh` (exit 0) antes de confirmar o desenho.
Depois de disparar, usar `updraftplus_get_last_backup` ou `updraftplus_get_backup_history`
passados alguns minutos para confirmar sucesso — o tool não espera pelo resultado.
## Fora de âmbito (decisão deliberada, não lacuna técnica)
- Desligar/alterar agendamento (`updraft_interval`, `updraft_retain`, etc.) — só leitura.
- Apagar um backup do histórico ou de storage remoto.
- Configurar/alterar destinos de storage (credenciais nunca são escritas por este MCP).
- Restaurar um backup — operação com risco de perda de dados muito maior que disparar um novo
backup; fora do v1 por decisão de risco explícita, não avaliada tecnicamente.
## Verificação
Construído e testado ponta-a-ponta contra produção 19-08-2026: `updraftplus_get_status` em
`starter` confirmou Free 1.26.6 activo, Premium ausente, `DISABLE_WP_CRON=true`;
`updraftplus_get_schedule` confirmou `interval: null` (nunca configurado) com nota explicativa;
`updraftplus_get_storage_destinations` confirmou os 16 destinos com `configuredInstanceCount: 0`
em `starter` (nenhum falso positivo depois da correcção do `authurl`);
`updraftplus_get_backup_history`/`updraftplus_get_last_backup` confirmaram o backup real de
2025-06-11 em `starter` e o dado ilusório herdado em `ccv`; `updraftplus_trigger_backup_now`
disparado com sucesso em `ecommerce-demo` (`confirm:true`), devolveu PID em ~4s, confirmado por
`ps aux` fora do MCP a mostrar o processo `wp eval` real a correr depois da ligação SSH ter
fechado. Schema `confirm: z.literal(true)` testado a rejeitar `confirm` ausente e `confirm:false`
antes de o handler correr.
## Skills relacionadas
Nenhuma skill de conhecimento separada para o UpdraftPlus existe no bundle — esta skill é a única
documentação, incluindo o mapeamento de options acima.
@@ -0,0 +1,92 @@
---
name: mcp-webp-express
description: MCP dedicado multi-site (node stdio, ligação `webpx` em ~/.omp/agent/mcp.json) para gerir o WebP Express em qualquer site do bundle Descomplicar® via WP-CLI/SSH — estado do plugin, ficheiro de config completo (modo de operação, qualidade jpeg/png, stack de conversores), conversão em massa, flush de webps. Site é um parâmetro em cada tool, não uma ligação fixa. Usar quando "converter webp via mcp", "webpx mcp", "config webp express mcp", "conversores webp mcp", "flush webp mcp", "convert webp mcp".
layer: wiki
---
# /mcp-webp-express — MCP dedicado multi-site ao WebP Express
Projecto em `/media/ealmeida/Dados/Dev/mcp-webp-express/` (TypeScript, SDK MCP oficial, stdio).
Mesmo padrão do `mcp-wpfc`/`mcp-element-pack`: sem código PHP novo no WordPress, cada tool executa
`wp option get`, leitura/escrita directa no ficheiro de config, ou
`wp webp-express convert|flushwebp` via `ssh server` sobre o WP-CLI já instalado. `site` é um
parâmetro em cada tool, não uma ligação fixa — WebP Express está activo nos 8 sites reais do
bundle.
**Relação com a skill `webp-express`:** essa skill é a fonte de conhecimento (mapeamento completo
de todas as chaves do ficheiro de config, modos de operação, conversores, redirection rules,
alter-html, web-service, gotchas de produção — §1-13). Esta skill é o "como executar" — qual tool
chamar para cada operação. Consultar `webp-express` para entender uma definição; usar as tools
abaixo para lê-la/escrevê-la.
## Âmbito
Config, stack de conversores, conversão em massa e limpeza do WebP Express. Não cobre:
activação/desactivação do próprio plugin (`emcp-tools activate-plugin`/`deactivate-plugin`),
optimização/compressão genérica de imagem (nenhum plugin equivalente no bundle hoje), Cloudflare
Polish (fora de âmbito — Polish está `off` no plano Free, ver §13 da skill `webp-express`).
## Sites conhecidos
Chamar `webpx_list_sites` para a lista actual com path e nota de estado. Aliases verificados ao
vivo 19-08-2026, todos com WebP Express v0.25.15 activo: `descomplicar`/`emanuelalmeida`
(`operation-mode: cdn-friendly`), `starter`/`ccv`/`care`/`ecommerce-demo`/`e-commerce`/`ecommerce`
(`operation-mode: varied-image-responses`). Também aceita um path absoluto directamente
(`/home/ealmeida/<site>`) para sites fora desta lista, sem exigir rebuild do MCP.
## Tools (9)
| Tool | Read-only | Uso |
|---|---|---|
| `webpx_list_sites` | sim | Aliases conhecidos, path WP, nota de estado |
| `webpx_get_status` | sim | Plugin instalado/activo/versão + `webp-express-state` resumido — chamar sempre antes de forçar `converter` em `webpx_convert` |
| `webpx_get_state` | sim | `webp-express-state` em bruto: `workingConverterIds`, `configured`, regras `.htaccess` gravadas |
| `webpx_get_config` / `webpx_set_config` | sim/não | Ficheiro `config.<hash>.json` completo — merge raso; recusa chaves de diagnóstico só-leitura; avisa quando a chave exige re-save manual em wp-admin para regenerar `.htaccess` |
| `webpx_get_converters` / `webpx_set_converter_options` | sim/não | `converters[]` — opções por conversor + reordenação (`position`). Nunca toca em `working` |
| `webpx_convert` | não | `wp webp-express convert [<location>] [flags]` — sem `location`, dry preview |
| `webpx_flushwebp` | não | `wp webp-express flushwebp [--only-png]` — apaga `.webp` gerados, nunca originais registados |
Todas as tools exigem `site` como primeiro parâmetro.
## Segurança
- `webpx_set_config` recusa gravar as chaves de diagnóstico
(`environment-when-config-was-saved`, `base-htaccess-on-these-capability-tests`,
`document-root`, `paths-used-in-htaccess`) — snapshots de ambiente gravados pelo próprio plugin,
nunca destinados a edição manual.
- Escrita no ficheiro de config preserva owner:group:permissões originais capturados via `stat`
antes de sobrescrever — a ligação SSH corre como root; sem isto o ficheiro ficaria `root:root`
e o wp-admin desse site deixaria de conseguir gravar as próprias definições.
- Valor viaja sempre em base64 sobre stdin, nunca interpolado no comando SSH remoto.
**Gotcha de formato (mesma classe do já documentado em `mcp-wpfc`):** `webp-express-state` é uma
STRING JSON dentro de um wp_option (`json_encode()` gravado directamente), não um array WP-CLI
tipado — `wp option get webp-express-state --format=json` devolve JSON duplamente codificado.
`getState()` descodifica uma segunda vez. Ao contrário do WPFC, aqui a CONFIG principal nem
sequer é um wp_option — vive num ficheiro com hash no nome (`webp-express-config-hash` resolve o
caminho, nunca hardcoded).
**Gotcha de SDK:** `structuredContent` do resultado de uma tool MCP tem de ser um objecto — nunca
um array. Passar um array (ex. `converters[]`) causa `MCP error -32602: expected record, received
array`. `jsonResult()` só anexa `structuredContent` quando os dados são um objecto simples.
## Verificação
Construído e testado ponta-a-ponta contra produção (`emanuelalmeida.pt`) 19-08-2026: leitura
completa da config e da stack de conversores; escrita+releitura+reversão de `enable-logging`
(`false→true→false`) e de `gd.skip-pngs` via `webpx_set_converter_options`
(`false→true→false`), ambas confirmadas por `stat` a manter `ealmeida:ealmeida 644` antes/depois;
bloqueio confirmado ao tentar gravar `document-root`; `webpx_convert` em dry preview (0 ficheiros
por converter, config já estável). `webpx_flushwebp` e `webpx_convert` com `location`/`reconvert`
reais não foram exercidos contra produção — mesmo código-caminho (`runSsh` + construção de
comando) já validado via `convert` em dry-run.
## Skills relacionadas
- `webp-express` — mapeamento completo de todas as chaves de config, modos de operação,
conversores, qualidade/encoding, redirection rules, alter-html, web-service, decision tree de
comandos WP-CLI equivalentes.
- `mcp-wpfc` — MCP irmão para WP Fastest Cache, mesmo padrão arquitectural; primeira ocorrência
documentada do gotcha de JSON duplamente codificado.
- `wp-font-perf` — cobre WebP em `background-image` CSS, que o WebP Express não converte (fora do
âmbito deste MCP e dessa skill de origem).
+88
View File
@@ -0,0 +1,88 @@
---
name: mcp-wpfc
description: MCP dedicado multi-site (node stdio, ligação `wpfc` em ~/.omp/agent/mcp.json) para gerir o WP Fastest Cache (Free + Premium) em qualquer site do bundle Descomplicar® via WP-CLI/SSH — estado do plugin, config principal com guarda-corpo Premium, purga, exclusões, Clearing Specific Pages, CDN, Varnish, caminho da cache, restrições de toolbar. Site é um parâmetro em cada tool, não uma ligação fixa. Usar quando "purgar cache via mcp", "wpfc mcp", "activar premium wp fastest cache", "config wpfc", "regras de exclusão wpfc", "clearing specific pages mcp", "cdn nativa wpfc", "varnish wpfc", "toolbar wpfc".
layer: wiki
---
# /mcp-wpfc — MCP dedicado multi-site ao WP Fastest Cache
Projecto em `/media/ealmeida/Dados/Dev/mcp-wpfc/` (TypeScript, SDK MCP oficial, stdio). Não é uma
extensão do EMCP Tools — mesma razão que `mcp-element-pack`: sem código PHP novo no WordPress,
cada tool executa `wp option get/update` ou `wp fastest-cache clear` via `ssh server` sobre o
WP-CLI já instalado. Diferença estrutural face a `element-pack`/`bit-social`/`cloudflare-app`:
**`site` é um parâmetro em cada tool**, não uma ligação fixa por site — WP Fastest Cache está
espalhado por 8 sites reais do bundle com estados diferentes (ver skill `wp-fastest-cache` §16.1).
**Relação com a skill `wp-fastest-cache`:** essa skill é a fonte de conhecimento (mapeamento
completo de todas as options e o seu significado, comportamento por versão Free/Premium, gotchas
de produção). Esta skill é o "como executar" — qual tool chamar para cada operação. Consultar
`wp-fastest-cache` para entender uma definição; usar as tools abaixo para lê-la/escrevê-la.
## Âmbito
Config e purga do WP Fastest Cache (Free + Premium). Não cobre: activação/desactivação do próprio
plugin (`emcp-tools activate-plugin`/`deactivate-plugin` já cobre isso genericamente), Redis
Object Cache (skill `redis-object-cache`), purga Cloudflare edge (App for Cloudflare® ou
`mcp-cloudflare-app`), WP Meteor (skill `wp-meteor`).
## Sites conhecidos
Chamar `wpfc_list_sites` para a lista actual com path e nota de estado. Aliases verificados ao
vivo 19-08-2026: `descomplicar` (produção, Free+Premium inactivos), `emanuelalmeida` (Free
active), `starter` (Free active), `ccv` (Free active), `care`/`ecommerce-demo`/`ecommerce`
(WPFC não instalado), `e-commerce` (Free inactive). Também aceita um path absoluto directamente
(`/home/ealmeida/<site>`) para sites fora desta lista, sem exigir rebuild do MCP.
## Tools (18)
| Tool | Read-only | Uso |
|---|---|---|
| `wpfc_list_sites` | sim | Aliases conhecidos, path WP, nota de estado |
| `wpfc_get_status` | sim | Free/Premium instalado, activo, versão — chamar sempre antes de `wpfc_set_config` |
| `wpfc_get_config` | sim | Option `WpFastestCache` completa |
| `wpfc_set_config` | não | Merge de chaves na `WpFastestCache` (read-modify-write) — recusa chaves só-Premium sem `wp-fastest-cache-premium` activo ou `force_premium:true` |
| `wpfc_purge_cache` | não | `mode`: `all` / `all_minified` / `post` (exige `post_id`). Não purga Cloudflare — usar `mcp-cloudflare-app`/App for Cloudflare® à parte |
| `wpfc_get_exclude` / `wpfc_set_exclude` | sim/não | `WpFastestCacheExclude` — regras de exclusão de páginas/cookies/user-agents. Set é **substituição completa**, não merge |
| `wpfc_get_csp` / `wpfc_set_csp` | sim/não | `WpFastestCacheCSP` — Clearing Specific Pages (purga extra de páginas agregadoras). Set substitui por inteiro |
| `wpfc_get_cdn` / `wpfc_set_cdn` | sim/não | `WpFastestCacheCDN` — integrações CDN nativas (Cloudflare/MaxCDN/BunnyCDN/CloudFront/genérica). Set substitui por inteiro |
| `wpfc_get_varnish` / `wpfc_set_varnish` | sim/não | `WpFastestCacheVarnish` — `{server, status?}`. Set substitui por inteiro |
| `wpfc_get_pathSettings` / `wpfc_set_pathSettings` | sim/não | `WpFastestCachePathSettings` — `{cachepath, optimizedpath}`. Set substitui por inteiro |
| `wpfc_get_toolbarSettings` / `wpfc_set_toolbarSettings` | sim/não | `WpFastestCacheToolbarSettings` — roles sem acesso à toolbar de admin. Set substitui por inteiro |
Todas as tools exigem `site` como primeiro parâmetro.
## Segurança
Allowlist fixa dos 7 nomes de option WPFC (`src/wpcli.ts`, `OPTION_KEYS`) — nunca `wp option
update` genérico sobre uma chave arbitrária. Escritas via base64 em stdin, nunca interpoladas no
comando SSH remoto. Guarda-corpo Premium em `wpfc_set_config`: chaves em `PREMIUM_ONLY_CONFIG_KEYS`
(Lazy Load, Minify JS, Combine JS, Render Blocking, Google Fonts, Mobile Theme, Delay JS,
`wpFastestCachePremium`) só gravam se `wpfc_get_status` confirmar `wp-fastest-cache-premium`
activo nesse site, ou se o chamador passar `force_premium:true` já com essa confirmação feita.
**Gotcha crítico de formato — não replicar noutros MCPs sem verificar primeiro:** as 7 options
WPFC são strings PHP planas cujo conteúdo é texto JSON (`json_encode()` gravado directamente),
**não** options JSON tipadas do WP-CLI. `getOption`/`setOption` deste MCP nunca usam
`--format=json` — leem/escrevem a string crua e fazem `JSON.parse`/`JSON.stringify` uma única vez
do lado do cliente. Usar `--format=json` aqui causa dupla codificação e corrompe a option (foi
reproduzido em produção durante o build e corrigido — ver skill `wp-fastest-cache` §16.2 para o
relato completo). Qualquer extensão futura deste MCP, ou um MCP novo sobre outro plugin, deve
testar `wp option get <key>` **sem** `--format=json` primeiro para confirmar o tipo real
armazenado antes de escolher o padrão de leitura/escrita.
## Verificação
Construído e testado ponta-a-ponta contra produção 19-08-2026: `wpfc_get_status` em `descomplicar`
e `ccv` confirmou os dados reais (Free/Premium inactivos em `descomplicar`, Free activo em `ccv`);
`wpfc_set_config` recusou correctamente uma chave Premium (`wpFastestCacheGoogleFonts`) em
`descomplicar` (Premium inactivo); escreveu e releu correctamente uma chave Free
(`wpFastestCacheDisableEmojis`), confirmado por leitura SSH directa fora do MCP;
`wpfc_get_exclude`/`wpfc_set_exclude` testado em `ccv` (fallback `[]` → escrita `[]` → releitura
`[]`); `wpfc_purge_cache` executado com sucesso em `ccv`.
## Skills relacionadas
- `wp-fastest-cache` — mapeamento completo de todas as options e comportamento Free vs Premium,
gotchas de produção, decision tree de comandos WP-CLI equivalentes.
- `mcp-cloudflare-app` — purga da camada edge (Cloudflare), complementar à purga local deste MCP.
- `redis-object-cache` — a terceira camada de cache do bundle, fora do âmbito deste MCP.
+156
View File
@@ -0,0 +1,156 @@
---
name: mcp-wpforms
description: MCP dedicado multi-site (node stdio, ligação `wpforms` em ~/.omp/agent/mcp.json) para gerir o WPForms + WPForms Form Abandonment em descomplicar.pt e emanuelalmeida.pt via WP-CLI/SQL/SSH — formulários, entries (listar/ler/contar/marcar lida/estrela/spam), entries abandonadas com contexto capturado, settings gerais (captcha, licença), estado Akismet/spam. Site é um parâmetro em cada tool. Única documentação de mapeamento de dados WPForms — não existe skill de conhecimento separada. Usar quando "wpforms mcp", "entries wpforms", "formulário abandonado", "form abandonment", "spam wpforms", "akismet wpforms", "schema formulário wpforms", "wpforms_entries", "marcar entry como lida", "settings wpforms".
layer: wiki
---
# /mcp-wpforms — MCP dedicado multi-site ao WPForms + Form Abandonment
Projecto em `/media/ealmeida/Dados/Dev/mcp-wpforms/` (TypeScript, SDK MCP oficial, stdio). Sem
código PHP novo no WordPress: cada tool executa `wp db query` (SELECTs `JSON_OBJECT`/`JSON_ARRAYAGG`
e `UPDATE`s de 1 coluna), `wp option get` ou `wp post get` via `ssh server` sobre o WP-CLI já
instalado. `site` é um parâmetro em cada tool, não uma ligação fixa — como `mcp-wpfc`/`mcp-wpmeteor`.
Não existe skill de conhecimento "wpforms" separada (só o EMCP Tools referencia o ícone do addon
Form Abandonment, nada mais). **Esta skill é a única documentação do mapeamento de dados WPForms**
neste repositório — inclui aqui o schema das tabelas e das options que noutros MCPs (ex.
`wp-fastest-cache` ↔ `mcp-wpfc`) viveria numa skill "irmã".
## Âmbito
Formulários (leitura do schema), entries (listar/ler/contar, marcar lida/estrela/spam), entries
abandonadas do addon Form Abandonment, settings gerais (captcha, comportamento) e licença
(mascarada), estado Akismet. **Não cobre**: criação/edição do schema de campos de um formulário
(risco de corromper o formulário — fora de âmbito por desenho), reenvio de notificações de email,
tabelas de analytics/payments/tasks_meta/file_restrictions/protected_files do WPForms (não usadas
por nenhuma tool), o plugin `styler-for-wpforms` (inactivo em emanuelalmeida.pt).
## Sites conhecidos
Chamar `wpforms_list_sites` para a lista actual. Ao contrário de `mcp-wpfc`, **não** aceita um path
arbitrário — as queries SQL directas precisam do prefixo de tabela real do site (`table_prefix`),
que varia por instalação e não pode ser adivinhado com segurança. Só sites explicitamente
verificados são aceites. Confirmado ao vivo 19-08-2026 (`wp plugin list`, `wp config get
table_prefix`):
| Alias | Domínio | Path | `table_prefix` | WPForms | Form Abandonment |
|---|---|---|---|---|---|
| `descomplicar` | descomplicar.pt (produção) | `/home/ealmeida/public_html` | `wpah_` | 2.0.0.4, activo, licença Elite | 1.16.0, activo |
| `emanuelalmeida` | emanuelalmeida.pt | `/home/ealmeida/emanuelalmeida.pt` | `wpne_` | 2.0.0.4, activo | não instalado |
Akismet não está instalado em nenhum dos dois sites (`wpforms_get_spam_status` devolve
`akismet_plugin_installed: false`).
## Mapeamento de dados (confirmado por leitura directa do código-fonte e da BD)
### Formulários — `wp_posts` (post_type = `wpforms`)
O schema de cada formulário (campos + settings) vive em `post_content` como **texto JSON puro**
(não serializado PHP) — `{"fields": {...}, "settings": {...}, "id", "field_id", "search_terms",
"providers", "meta"}`. `fields` é um objecto indexado por field-id (não um array), cada entrada com
pelo menos `id`, `type`, `label`, `required`. As settings interessantes: `form_abandonment` (`"1"`
= activo), `form_abandonment_fields` (`""` = só guarda se tiver email/telefone, `"all"` = guarda
sempre), `form_abandonment_duplicates` (`"1"` = evita duplicados na mesma hora), `store_spam_entries`
(`"0"`/`"1"`), `akismet` (só existe se o plugin Akismet estiver instalado — nenhum dos 2 sites tem).
`wpforms_get_form_schema` extrai `fields` para uma lista simplificada (id/type/label/required/
description) e devolve `settings` completo tal-e-qual.
`wpforms_list_forms` usa `JSON_EXTRACT`/`JSON_UNQUOTE` directamente sobre `post_content` (sem
descodificar o JSON completo) para expor `form_abandonment_enabled`/`store_spam_entries` de forma
barata em massa — truque reutilizável para qualquer outra flag de settings sem custo de ler o
schema completo por formulário.
### Entries — tabelas custom `{prefix}wpforms_entries` / `entry_fields` / `entry_meta` / `logs`
`wpforms_entries`: `entry_id`, `form_id`, `post_id`, `user_id`, `status`, `type`, `viewed`
(tinyint), `starred` (tinyint), `fields` (longtext — JSON `{field_id: {name, value, id, type}}`,
os dados submetidos tal como o WPForms os mostra no admin), `meta` (longtext, **não usado** em
produção — sempre vazio, não confundir com a tabela `entry_meta`), `date`, `date_modified`,
`ip_address`, `user_agent`, `user_uuid`.
**`status` — valores confirmados por leitura do código-fonte** (não documentados centralmente pelo
próprio plugin, espalhados por `src/Pro/AntiSpam/SpamEntry.php` e pelo addon Form Abandonment):
- `''` (string vazia) — entry normal.
- `'abandoned'` — capturada pelo addon Form Abandonment (`WPFormsFormAbandonment\Plugin::process_entries`).
- `'spam'` — marcada como spam (`SpamEntry::ENTRY_STATUS`). Desmarcar spam **repõe sempre `''`**,
mesmo que a entry estivesse antes `abandoned`/`trash` — é o comportamento nativo de
`SpamEntry::set_as_not_spam`, replicado tal-e-qual em `wpforms_set_entry_spam`.
- `'trash'` — enviada para o lixo.
`entry_fields`: uma linha por (entry, field) para pesquisa/filtragem indexada — não usada por
nenhuma tool deste MCP (a coluna `fields` da própria entry já tem os mesmos dados, mais legível).
`entry_meta`: linhas `{entry_id, form_id, user_id, status, type, data, date}`. `type` observados:
`log` (histórico tipo "Entry read."), e — só em entries `abandoned` — `page_url`, `page_title`,
`page_id`, `url_referer`, `user_id` (o contexto capturado pelo Form Abandonment no momento do
abandono). `wpforms_list_abandoned_entries` faz o pivot destes 4 tipos para campos directos na
resposta.
`logs` (`{prefix}wpforms_logs`): log administrativo do plugin (título/mensagem/tipo), não exposto
por nenhuma tool — fora do âmbito pedido.
### Settings / licença — `wp_options`
`wpforms_settings` — array PHP nativo (`a:20:{...}`), **seguro usar `--format=json`** em leitura
(confirmado por `wp option get wpforms_settings --format=json`, sem dupla codificação). Contém
`captcha-provider` (`recaptcha`/`hcaptcha`/`turnstile`/vazio), chaves reCAPTCHA/hCaptcha/Turnstile,
`modern-markup`, etc. `wpforms_license` — também array nativo: `key`, `type` (`elite` em
descomplicar.pt), `is_expired`. `wpforms_get_settings` mascara sempre a chave de licença (só os
últimos 4 caracteres visíveis), nunca a devolve em claro.
## Tools (12)
| Tool | Read-only | Uso |
|---|---|---|
| `wpforms_list_sites` | sim | Aliases conhecidos, path, `table_prefix`, se tem Form Abandonment |
| `wpforms_list_forms` | sim | Todos os formulários (qualquer post_status): título, datas, `form_abandonment_enabled`, `store_spam_entries`, `entry_count` |
| `wpforms_get_form_schema` | sim | Schema completo de 1 formulário: campos simplificados + `settings` completo do `post_content` |
| `wpforms_list_entries` | sim | Entries com filtros `form_id`/`status` (normal/abandoned/spam/trash)/`date_from`/`date_to`, paginado — resumo sem `fields`/`meta` |
| `wpforms_get_entry` | sim | 1 entry completa: dados + `fields` submetidos + toda a `entry_meta` (log, contexto de abandono, razão de spam) |
| `wpforms_count_entries` | sim | Contagem por estado (`total`/`normal`/`abandoned`/`spam`/`trash`), agrupada por formulário |
| `wpforms_list_abandoned_entries` | sim | Entries `abandoned` com `fields` parciais + `page_url`/`page_title`/`url_referer`/`captured_user_id` (pivot de `entry_meta`) |
| `wpforms_get_settings` | sim | `wpforms_settings` (captcha, geral) + resumo de licença (chave sempre mascarada) |
| `wpforms_get_spam_status` | sim | Akismet instalado/activo, contagem global de spam, `store_spam_entries` + contagem de spam por formulário |
| `wpforms_set_entry_viewed` | não | `entries.viewed` — mirror da acção "Read"/"Unread" |
| `wpforms_set_entry_starred` | não | `entries.starred` — mirror da acção "Star"/"Unstar" |
| `wpforms_set_entry_spam` | não | `entries.status` — mirror exacto de `SpamEntry::set_as_spam`/`set_as_not_spam` (ver nota sobre repor `''`) |
## Segurança
Escritas restritas a `UPDATE` de 1 coluna (`viewed`, `starred`, `status`) sobre `entries`, sempre
precedidas de leitura para confirmar que a entry existe (`requireEntry`, lança erro claro se não).
Nunca escreve schema de formulário nem `post_content`. Identificadores de tabela/coluna validados
por `assertSafeIdentifier` (allowlist `[a-z0-9_]+`); filtros de texto livre (datas) escapados por
`sqlString`; texto SQL viaja sempre pelo stdin do processo SSH, nunca interpolado na linha de
comando remota. Site restrito à allowlist de `sites.ts` (sem path arbitrário) precisamente para
impedir uma query SQL correr contra um prefixo de tabela errado.
## Verificação
Construído e testado ponta-a-ponta contra produção 19-08-2026, via chamadas reais ao protocolo MCP
(stdio) e confirmação cruzada por SQL directo fora do MCP:
- `wpforms_list_forms` em `descomplicar` (1 formulário, "Pedido de Orçamento", 26 entries) e
`emanuelalmeida` (6 formulários, ex. "Formulário de contacto" com 17 entries) — confirmados por
`SELECT COUNT(*)`/`wp post list` directos antes de qualquer chamada ao MCP.
- `wpforms_count_entries` em `descomplicar`: `total=26, normal=11, abandoned=13, spam=0, trash=2` —
igual ao `GROUP BY status` feito directamente na BD.
- `wpforms_get_entry` (entry 37) e `wpforms_list_abandoned_entries` — campos submetidos (email,
telefone, empresa) e contexto de abandono (`page_url`, `page_title`) conferem com as colunas
`fields`/`entry_meta` lidas directamente.
- `wpforms_get_form_schema` (formulário 16446) — 157 campos extraídos correctamente do
`post_content`, incluindo `layout`/`pagebreak`.
- `wpforms_get_settings` — `captcha-provider: "recaptcha"` e licença `type: "elite"` com chave
mascarada, confirmados contra `wp option get wpforms_settings/wpforms_license --format=json`.
- `wpforms_get_spam_status` em ambos os sites — Akismet correctamente reportado como não instalado,
`store_spam_entries` extraído por `JSON_EXTRACT` a bater com o `post_content` bruto.
- Escrita testada e confirmada por SQL directo antes/depois: `wpforms_set_entry_viewed(descomplicar,
entry_id=37, viewed=true)` sobre uma entry já `viewed=1` — round-trip idempotente, valor
inalterado em produção, sem qualquer efeito colateral.
- Erros testados: formulário/entry inexistente devolvem mensagem clara (`isError: true`) em vez de
rebentar ou devolver dados parciais.
## Skills relacionadas
Nenhuma skill de conhecimento "wpforms" existe neste repositório — ver secção "Mapeamento de
dados" acima para o schema completo. `mcp-wpfc`/`mcp-wpmeteor` — mesmo padrão de MCP dedicado
multi-site sobre WP-CLI/SSH, para outros plugins do bundle.
+84
View File
@@ -0,0 +1,84 @@
---
name: mcp-wpmeteor
description: MCP dedicado multi-site (node stdio, ligação `wpmeteor` em ~/.omp/agent/mcp.json) para gerir o WP Meteor (delay/reorder de JavaScript não crítico) em qualquer site do bundle Descomplicar® via WP-CLI/SSH — estado do plugin, config completa, toggle de módulo, delay do ultimate-reorder, exclusões manuais, telemetria detected. Site é um parâmetro em cada tool. Usar quando "gestão wp meteor", "wpmeteor mcp", "desligar ultimate reorder via mcp", "mudar delay wp meteor", "exclusões wp meteor mcp", "activar módulo elementor-pp".
layer: wiki
---
# /mcp-wpmeteor — MCP dedicado multi-site ao WP Meteor
Projecto em `/media/ealmeida/Dados/Dev/mcp-wpmeteor/` (TypeScript, SDK MCP oficial, stdio). Sem
código PHP novo no WordPress: cada tool executa `wp option get` / `wp option patch` via
`ssh server` sobre o WP-CLI já instalado. O plugin **não regista nenhum comando WP-CLI próprio**
(confirmado por grep a todo o código-fonte — ver skill `wp-meteor`); toda a config vive numa
única option, `wp-meteor-settings`.
**Relação com a skill `wp-meteor`:** essa skill é a fonte de conhecimento completa — os 5 módulos
reais, a semântica exacta do `delay`, as duas camadas de exclusão automática embutidas (`gdpr`,
`Compatibility`), os mecanismos de bypass automático, diagnóstico de CLS/CWV. Esta skill é o "como
executar" — qual tool chamar para cada operação. Consultar `wp-meteor` para entender uma
definição antes de a alterar por aqui.
## Âmbito
Config do WP Meteor: 5 módulos, delay do `ultimate-reorder`, exclusões manuais
(`exclude.value`), leitura da telemetria `detected`. Não cobre: activação/desactivação do
próprio plugin (`emcp-tools activate-plugin`/`deactivate-plugin` já cobre isso genericamente),
diagnóstico de CLS/CWV com Chrome DevTools (workflow manual, ver skill `wp-meteor` §"Testar
isoladamente via Chrome DevTools").
## Sites conhecidos
Chamar `wpmeteor_list_sites` para a lista actual com path e nota de estado. **Verificado ao vivo
19-08-2026: só `emanuelalmeida` tem o WP Meteor instalado** — os outros 7 sites reais do bundle
(`descomplicar`, `starter`, `ccv`, `care`, `ecommerce-demo`, `e-commerce`, `ecommerce`) não o têm.
Também aceita um path absoluto directamente para sites fora desta lista.
## Tools (8)
| Tool | Read-only | Uso |
|---|---|---|
| `wpmeteor_list_sites` | sim | Aliases conhecidos, path WP, nota de estado |
| `wpmeteor_get_status` | sim | Instalado/activo/versão |
| `wpmeteor_get_config` | sim | Option `wp-meteor-settings` completa |
| `wpmeteor_set_module` | não | Liga/desliga um dos 5 módulos (`ultimate-reorder`, `gdpr`, `exclude`, `elementor-animations`, `elementor-pp`) — escrita atómica de `<module>.enabled`, não toca nos outros |
| `wpmeteor_set_delay` | não | `ultimate-reorder.delay` — string numérica; "0" = zero delay, N positivo = timeout fixo de N segundos, negativo (ex. "-1") = só corre na primeira interacção |
| `wpmeteor_get_exclusions` / `wpmeteor_set_exclusions` | sim/não | `exclude.value` — array de URLs/palavras-chave/regex. Set é **substituição completa**, não merge. Não liga o módulo `exclude` sozinho |
| `wpmeteor_get_detected` | sim | Telemetria interna (`detected`), sem tool de escrita — não é configurável |
Todas exigem `site` como primeiro parâmetro.
## Padrão de escrita — atómico por chave, não read-modify-write
Diferente do `mcp-wpfc` (que faz merge em JS de toda a option `WpFastestCache`), aqui cada
escrita usa `wp option patch update wp-meteor-settings <key-path...> [--format=json]` — o próprio
WP-CLI localiza e substitui só a chave aninhada pedida, atomicamente, sem ler+reescrever a option
inteira. Menos risco de pisar uma gravação concorrente do wp-admin.
## Segurança
Allowlist fixa de 1 option (`wp-meteor-settings`) e 5 módulos conhecidos (`src/wpcli.ts`,
`MODULES`). Escritas via base64 em stdin, nunca interpoladas no comando SSH remoto.
**Confirmado antes de escrever a bridge (lição de `mcp-wpfc`/skill `wp-fastest-cache` §16.2):**
`wp-meteor-settings` é um **array PHP nativo** — `wp option get` sem `--format=json` devolve
`array(...)` desestruturado, não uma string. `--format=json` é portanto o padrão correcto aqui em
leitura e escrita (ao contrário do WP Fastest Cache, onde `--format=json` corrompe a option).
Qualquer MCP novo sobre `wp option` deve continuar a verificar isto primeiro, caso a caso — não
assumir nenhum dos dois padrões por omissão.
## Verificação
Construído e testado ponta-a-ponta contra produção 19-08-2026 (`emanuelalmeida.pt`, único site com
o plugin instalado): `wpmeteor_get_status` confirmou activo v3.4.18 lá e ausente em
`descomplicar`; `wpmeteor_get_config`/`wpmeteor_get_exclusions` leram a config real; `wpmeteor_set_module`,
`wpmeteor_set_delay` e `wpmeteor_set_exclusions` escreveram e relerem correctamente com valores
já-correntes (round-trip idempotente, sem alterar comportamento do site); `wpmeteor_get_detected`
confirmou array vazio. Integridade final da option confirmada por leitura SSH directa, idêntica ao
estado original.
## Skills relacionadas
- `wp-meteor` — mapeamento completo de todos os módulos, semântica do delay, exclusões
automáticas embutidas, mecanismos de bypass, diagnóstico de CLS/CWV.
- `mcp-wpfc` — mesma convenção (MCP multi-site, `site` por parâmetro), mas para o WP Fastest
Cache; ver lá o gotcha de formato de armazenamento que motivou a verificação prévia aqui.
@@ -0,0 +1,48 @@
---
name: powerpack-widgets
description: Catálogo completo e verificado dos 97 widgets PowerPack Elements Pro para Elementor, com o widgetType real de cada um extraído do código-fonte (classes/class-pp-config.php) — não estão no catálogo curado do EMCP Tools (list-widgets), por isso precisam de get-widget-schema com full:true. Usar quando "powerpack", "pp-widget", "advanced menu elementor", "hotspots elementor", "flip box powerpack", "woo product elementor", "info box powerpack".
layer: wiki
---
# /powerpack-widgets — Catálogo de Widgets PowerPack Elements Pro
PowerPack Elements (Team IdeaBox, plugin `powerpack-elements`) regista os próprios widgets Elementor **fora** do catálogo curado do EMCP Tools — `list-widgets` não os lista. Este catálogo foi extraído directamente do array de configuração fonte (`wp-content/plugins/powerpack-elements/classes/class-pp-config.php`, método `get_widget_info()`) num site em produção com PowerPack Elements Pro 2.11.7 — **não adivinhado por convenção de nomes**, o que importa porque a convenção `pp-<nome-módulo>` tem excepções reais confirmadas (ver notas abaixo da tabela).
Catálogo completo (97 widgets) em `references/catalogo.md` — esta página tem o workflow e os widgets mais úteis para arranque rápido.
## Como usar (workflow)
1. Confirmar que `powerpack-elements` está activo: `list-plugins`, procurar o slug.
2. `get-widget-schema({ widget_type: "pp-<slug>", full: true })` — **não estão no catálogo curado**, a chamada sem `full:true` devolve erro "Not in the curated catalog". `full:true` devolve o schema de controlos bruto.
3. Adicionar com `add-free-widget({ post_id, parent_id, widget_type, settings })` num container **legacy** (`add-container`) — mesma mecânica de `elementskit-widgets`, sem equivalente atómico.
4. **Nunca assumir o `widgetType` pelo nome da pasta do módulo** — verificado no código-fonte: `Hotspots` → `pp-image-hotspots` (não `pp-hotspots`), `Onepage_Nav` → `pp-one-page-nav` (não `pp-onepage-nav`), `Popup_Box` (pasta `modal-popup`) → `pp-modal-popup`, `Link_Effects` → **`pa-link-effects`** (prefixo `pa-`, não `pp-`!). Confirmar sempre com `get-widget-schema(full:true)` antes de construir em produção — ver tabela completa para todas as excepções.
## Widgets mais usados (ver `references/catalogo.md` para os 97 completos)
| `widgetType` | Título | Uso |
|---|---|---|
| `pp-info-box` | Info Box | ícone/imagem + título + descrição, muito usado em secções de features |
| `pp-flipbox` | Flip Box | cartão de dois lados com flip ao hover |
| `pp-advanced-menu` | Advanced Menu | menu de navegação avançado com mega-menu |
| `pp-image-hotspots` | Image Hotspots | pontos interactivos sobre imagem (excepção de nome — pasta `hotspots`) |
| `pp-pricing-table` | Pricing Table | tabela de preços |
| `pp-team-member` | Team Member | cartão de membro de equipa |
| `pp-testimonials` | Testimonials | testemunhos com carrossel |
| `pp-posts` | Advanced Posts | grelha/lista de posts avançada com query control |
| `pp-login-form` | Login Form | formulário de login frontend |
| `pp-business-hours` | Business Hours | horário de funcionamento |
| `pp-countdown` | Countdown Timer | temporizador |
| `pp-table-of-contents` | Table of Contents | índice automático |
| `pp-instafeed` | Instagram Feed | feed do Instagram |
| `pp-modal-popup` | Popup Box | popup modal (excepção de nome — pasta `modal-popup`) |
| `pp-woo-product-price` | Woo — Product Price | preço de produto WooCommerce, um de ~15 widgets `pp-woo-*` |
## Notas
- 15 dos 97 widgets são específicos WooCommerce (`pp-woo-*`) e só têm efeito útil com WooCommerce activo e no contexto de um produto/loja.
- Alguns módulos listados no directório `modules/` (`display-conditions`, `dynamic-tags`, `presets-style`, `query-control`, `devices`) são **extensões transversais**, não widgets autónomos — não aparecem no catálogo porque não têm `widgetType` próprio; afectam definições de outros widgets PowerPack (o mesmo padrão visto no `elementskit-widgets` com `ekit_cursor_text_label`).
## Skills relacionadas
- `emcp-page-building` — mecânica de coexistência atómico/legacy, sequência de construção.
- `elementor-pro-widgets` — catálogo nativo Elementor Pro (curado, no `list-widgets`).
- `elementskit-widgets` — catálogo do addon concorrente ElementsKit Lite (também fora do catálogo curado).
@@ -0,0 +1,113 @@
# Catálogo completo — 97 widgets PowerPack Elements Pro
Extraído ao vivo de `wp-content/plugins/powerpack-elements/classes/class-pp-config.php`, método `get_widget_info()`, num site em produção com PowerPack Elements Pro 2.11.7. Coluna `Classe PHP` é o identificador interno do módulo (útil para localizar o ficheiro fonte em `modules/<pasta>/widgets/`); coluna `widgetType` é o valor real gravado em `_elementor_data` e usado em `get-widget-schema`/`add-free-widget`.
| widgetType | Título | Classe PHP |
|---|---|---|
| `pp-advanced-accordion` | Advanced Accordion | Advanced_Accordion |
| `pp-advanced-menu` | Advanced Menu | Advanced_Menu |
| `pp-advanced-tabs` | Advanced Tabs | Advanced_Tabs |
| `pp-album` | Album | Album |
| `pp-author-list` | Author List | Author_List |
| `pp-breadcrumbs` | Breadcrumbs | Breadcrumbs |
| `pp-business-hours` | Business Hours | Business_Hours |
| `pp-business-reviews` | Business Reviews | Business_Reviews |
| `pp-buttons` | Buttons | Buttons |
| `pp-card-slider` | Card Slider | Card_Slider |
| `pp-categories` | Categories | Categories |
| `pp-contact-form-7` | Contact Form 7 | Contact_Form_7 |
| `pp-content-reveal` | Content Reveal | Content_Reveal |
| `pp-content-ticker` | Content Ticker | Content_Ticker |
| `pp-countdown` | Countdown Timer | Countdown |
| `pp-counter` | Counter | Counter |
| `pp-coupons` | Coupons | Coupons |
| `pp-devices` | Devices | Devices |
| `pp-divider` | Divider | Divider |
| `pp-faq` | FAQ | Faq |
| `pp-flipbox` | Flip Box | Flipbox |
| `pp-fluent-forms` | Fluent Forms | Fluent_Forms |
| `pp-formidable-forms` | Formidable Forms | Formidable_Forms |
| `pp-image-gallery` | Image Gallery | Image_Gallery |
| `pp-image-slider` | Image Slider | Image_Slider |
| `pp-google-maps` | Google Maps | Google_Maps |
| `pp-gravity-forms` | Gravity Forms | Gravity_Forms |
| `pp-dual-heading` | Dual Heading | Dual_Heading |
| `pp-fancy-heading` | Fancy Heading | Fancy_Heading |
| `pp-image-hotspots` | Image Hotspots | Hotspots |
| `pp-how-to` | How To | How_To |
| `pp-icon-list` | Icon List | Icon_List |
| `pp-image-accordion` | Image Accordion | Image_Accordion |
| `pp-image-comparison` | Image Comparison | Image_Comparison |
| `pp-info-box` | Info Box | Info_Box |
| `pp-info-box-carousel` | Info Grid & Carousel | Info_Box_Carousel |
| `pp-info-list` | Info List | Info_List |
| `pp-info-table` | Info Table | Info_Table |
| `pp-instafeed` | Instagram Feed | Instafeed |
| `pa-link-effects` | Link Effects | Link_Effects |
| `pp-login-form` | Login Form | Login_Form |
| `pp-logo-carousel` | Logo Carousel | Logo_Carousel |
| `pp-logo-grid` | Logo Grid | Logo_Grid |
| `pp-magazine-slider` | Magazine Slider | Magazine_Slider |
| `pp-ninja-forms` | Ninja Forms | Ninja_Forms |
| `pp-offcanvas-content` | Offcanvas Content | Offcanvas_Content |
| `pp-one-page-nav` | One Page Navigation | Onepage_Nav |
| `pp-modal-popup` | Popup Box | Popup_Box |
| `pp-posts` | Advanced Posts | Posts |
| `pp-price-menu` | Price Menu | Price_Menu |
| `pp-pricing-table` | Pricing Table | Pricing_Table |
| `pp-promo-box` | Promo Box | Promo_Box |
| `pp-random-image` | Random Image | Random_Image |
| `pp-recipe` | Recipe | Recipe |
| `pp-registration-form` | Registration Form | Registration_Form |
| `pp-review-box` | Review Box | Review_Box |
| `pp-scroll-image` | Scroll Image | Scroll_Image |
| `pp-showcase` | Showcase | Showcase |
| `pp-sitemap` | Sitemap | Sitemap |
| `pp-table` | Table | Table |
| `pp-tabbed-gallery` | Tabbed Gallery | Tabbed_Gallery |
| `pp-table-of-contents` | Table of Contents | Table_Of_Contents |
| `pp-team-member` | Team Member | Team_Member |
| `pp-team-member-carousel` | Team Member Carousel | Team_Member_Carousel |
| `pp-testimonials` | Testimonials | Testimonials |
| `pp-tiled-posts` | Tiled Posts | Tiled_Posts |
| `pp-timeline` | Timeline | Timeline |
| `pp-toggle` | Toggle | Toggle |
| `pp-twitter-buttons` | Twitter Buttons | Twitter_Buttons |
| `pp-twitter-grid` | Twitter Grid | Twitter_Grid |
| `pp-twitter-timeline` | Twitter Timeline | Twitter_Timeline |
| `pp-twitter-tweet` | Twitter Tweet | Twitter_Tweet |
| `pp-video` | Video | Video |
| `pp-video-gallery` | Video Gallery | Video_Gallery |
| `pp-wpforms` | WP Forms | WP_Forms |
| `pp-woo-add-to-cart` | Woo — Add To Cart | Woo_Add_To_Cart |
| `pp-woo-cart` | Woo — Cart | Woo_Cart |
| `pp-woo-categories` | Woo — Categories | Woo_Categories |
| `pp-woo-checkout` | Woo — Checkout | Woo_Checkout |
| `pp-woo-mini-cart` | Woo — Mini Cart | Woo_Mini_Cart |
| `pp-woo-offcanvas-cart` | Woo — Off Canvas Cart | Woo_Offcanvas_Cart |
| `pp-woo-products` | Woo — Products | Woo_Products |
| `pp-woo-my-account` | Woo — My Account | Woo_My_Account |
| `pp-woo-product-tabs` | Woo — Product Tabs | Woo_Product_Tabs |
| `pp-woo-product-title` | Woo — Product Title | Woo_Product_Title |
| `pp-woo-product-meta` | Woo — Product Meta | Woo_Product_Meta |
| `pp-woo-product-price` | Woo — Product Price | Woo_Product_Price |
| `pp-woo-product-rating` | Woo — Product Rating | Woo_Product_Rating |
| `pp-woo-product-stock` | Woo — Product Stock | Woo_Product_Stock |
| `pp-woo-product-short-description` | Woo — Product Short Description | Woo_Product_Short_Description |
| `pp-woo-product-content` | Woo — Product Content | Woo_Product_Content |
| `pp-woo-product-images` | Woo — Product Images | Woo_Product_Images |
| `pp-woo-product-reviews` | Woo — Product Reviews | Woo_Product_Reviews |
| `pp-woo-product-upsell` | Woo — Product Upsell | Woo_Product_Upsell |
| `pp-woo-add-to-cart-notification` | Woo — Add to Cart Notification | Woo_Add_To_Cart_Notification |
| `pp-woo-archive-description` | Woo — Archive Description | Woo_Archive_Description |
| `pp-woo-single-product` | Woo — Single Product | Woo_Single_Product |
## Excepções confirmadas ao nome-de-pasta óbvio
- `Hotspots` (pasta `modules/hotspots`) → `pp-image-hotspots`, não `pp-hotspots`.
- `Onepage_Nav` (pasta `modules/onepage-nav`) → `pp-one-page-nav`, não `pp-onepage-nav`.
- `Popup_Box` (pasta `modules/modal-popup`) → `pp-modal-popup`.
- `Link_Effects` (pasta `modules/link-effects`) → **`pa-link-effects`** — único widget com prefixo `pa-` em vez de `pp-` em todo o catálogo.
- `WP_Forms` → `pp-wpforms` (sem hífen entre "wp" e "forms").
Regra geral: confirmar sempre `widgetType` com `get-widget-schema({ widget_type, full: true })` antes de o usar em `add-free-widget` — este ficheiro é a fonte mais fiável disponível, mas widgets podem ser adicionados/renomeados em versões futuras do plugin.
File diff suppressed because it is too large Load Diff
@@ -137,28 +137,52 @@ wp option pluck rank-math-options-sitemap pt_attachment_sitemap # on/off (defau
## Módulos Rank Math
`rank_math_modules` e um **array indexado de slugs activos** (nao um map `slug=>bool`). Lista completa verificada ao vivo em `descomplicar.pt` (Pro) + filesystem — ver `SKILL.md` §15 para a tabela completa Free vs PRO.
```bash
# Ver todos os módulos
# Ver todos os módulos activos
wp option get rank_math_modules --format=json --allow-root
# Activar módulo
wp option patch update rank_math_modules MODULE_NAME "1" --allow-root
# Activar módulo (wp eval — wp option patch NAO funciona aqui, nao ha chave por slug)
wp eval '
$modules = get_option("rank_math_modules", []);
if (!in_array("MODULE_SLUG", $modules)) { $modules[] = "MODULE_SLUG"; update_option("rank_math_modules", $modules); }
' --allow-root
# Desactivar módulo
wp option patch update rank_math_modules MODULE_NAME "0" --allow-root
wp eval '
$modules = get_option("rank_math_modules", []);
$modules = array_values(array_diff($modules, ["MODULE_SLUG"]));
update_option("rank_math_modules", $modules);
' --allow-root
```
| Módulo | Slug | Função |
|--------|------|--------|
| SEO Analysis | `seo-analysis` | Score SEO no editor |
| Link Counter | `link-counter` | Contagem links internos/externos |
| Redirections | `redirections` | Sistema de redirects |
| 404 Monitor | `404-monitor` | Log de 404s |
| Schema | `rich-snippet` | Schema markup |
| Sitemap | `sitemap` | XML Sitemap |
| Instant Indexing | `instant-indexing` | Google Indexing API (PRO) |
| Local SEO | `local-seo` | Schema LocalBusiness |
| WooCommerce | `woocommerce` | SEO WooCommerce |
| Módulo | Slug | Função | Tier |
|--------|------|--------|------|
| SEO Analysis | `seo-analysis` | Score SEO no editor | Free |
| Link Counter | `link-counter` | Contagem links internos/externos | Free |
| Redirections | `redirections` | Sistema de redirects | Free |
| 404 Monitor (dedicado, tabela própria) | `404-monitor` | Log de 404s | Free (opção básica) / PRO (módulo completo) |
| Schema | `rich-snippet` | Schema markup | Free (1/página) / PRO (ilimitado + Builder) |
| Sitemap | `sitemap` | XML Sitemap | Free |
| Instant Indexing | `instant-indexing` | IndexNow; Google Indexing API é PRO | Free |
| WooCommerce | `woocommerce` | SEO WooCommerce | Free |
| ACF | `acf` | Integração Advanced Custom Fields | Free |
| Analytics | `analytics` | GSC/GA no wp-admin | Free (básico) / PRO (avançado) |
| Role Manager | `role-manager` | Capabilities por role | Free |
| BuddyPress | `buddypress` | Integração BuddyPress | Free |
| bbPress | `bbpress` | Integração bbPress | Free |
| Web Stories | `web-stories` | SEO para Web Stories | Free |
| AI Visibility | `ai-visibility` | Monitoriza citações em AI Overviews/LLMs (achado 16-08-2026) | Free |
| Content AI | `content-ai` | Assistente de conteúdo (requer conta ligada) | Free (limitado) / PRO (ilimitado) |
| Image SEO | `image-seo` | Alt/title automático em imagens | **PRO exclusivo** |
| Local SEO | `local-seo` | Schema LocalBusiness multi-localização | **PRO exclusivo** |
| LLMs.txt | `llms-txt` | Gera `/llms.txt` automaticamente | **PRO exclusivo** |
| News Sitemap | `news-sitemap` | Sitemap Google News | **PRO exclusivo** |
| Video Sitemap | `video-sitemap` | Sitemap de vídeos | **PRO exclusivo** |
| Marketplace | `marketplace` | Addons Rank Math | **PRO exclusivo** |
| Link Genius | `link-genius` | Sugestões de link interno automáticas | **PRO exclusivo** |
| Podcast | `podcast` | SEO para podcasts | **PRO exclusivo** |
---
@@ -367,13 +391,20 @@ wp db query "SELECT id, sources, url_to, header_code FROM wp_rank_math_redirecti
| `wp rankmath sitemap generate` | ✅ | ✅ | Único comando nativo |
| Meta por post | ✅ | ✅ | `wp post meta` |
| Options globais | ✅ | ✅ | `wp option patch` |
| Schema markup | ✅ | ✅ | `wp eval` |
| Schema markup | ✅ (1/página) | ✅ (ilimitado + Schema Builder) | `wp eval` |
| Redirections | ✅ | ✅ | `\RankMath\Redirections\DB` |
| 404 Monitor | ✅ | ✅ | `wp_rank_math_404_logs` |
| Instant Indexing | ❌ | ✅ | `\RankMath\Instant_Indexing\Api` |
| Analytics GSC | ❌ | ✅ | Tabelas analytics_gsc |
| Analytics GA | ❌ | ✅ | Tabelas analytics_ga |
| 404 Monitor (opção básica, `404_monitor_mode`) | ✅ | ✅ | Sem tabela própria em Free |
| 404 Monitor (módulo dedicado, `wp_rank_math_404_logs`) | ❌ | ✅ | Verificado ao vivo 16-08-2026 — só existe com Pro licenciado |
| Instant Indexing — IndexNow | ✅ | ✅ | Módulo `instant-indexing`, ambos free e PRO |
| Instant Indexing — Google Indexing API | ❌ | ✅ | `\RankMath\Instant_Indexing\Api` |
| Analytics (básico, wp-admin) | ✅ | ✅ | Módulo `analytics` presente em ambos |
| Analytics GSC/GA (histórico alargado) | ❌ | ✅ | Tabelas `analytics_gsc`/`analytics_ga` |
| Image SEO | ❌ | ✅ | Módulo `image-seo` — confirmado PRO exclusivo |
| Local SEO | ❌ | ✅ | Módulo `local-seo` — confirmado PRO exclusivo |
| LLMs.txt (`/llms.txt` automático) | ❌ | ✅ | Módulo `llms-txt` — confirmado PRO exclusivo (achado 16-08-2026) |
| News/Video Sitemap | ❌ | ✅ | Módulos `news-sitemap`/`video-sitemap` |
| Marketplace / Link Genius / Podcast | ❌ | ✅ | Só existem no filesystem `seo-by-rank-math-pro` |
---
*Referência Rank Math WP-CLI | Descomplicar® | v1.0.0 | 18-02-2026*
*Referência Rank Math WP-CLI | Descomplicar® | v1.1.0 | 16-08-2026*
@@ -0,0 +1,460 @@
---
name: redis-object-cache
description: Gestão e diagnóstico do plugin Redis Object Cache (redis-cache, Till Krüss) via WP-CLI em servidores CWP. Cobre estado da ligação (wp redis status), drop-in object-cache.php, isolamento por site via WP_REDIS_DATABASE/WP_REDIS_PREFIX no bundle partilhado, PING/PONG, flush pós-SQL directo, e diferença entre object cache e page cache (WP Fastest Cache/WP Meteor). Usar quando "redis object cache", "redis-cache", "wp redis status", "object cache", "isolamento redis", "colisão de base redis", "WP_REDIS_DATABASE", "cache não invalida", "predis vs phpredis".
---
# /redis-object-cache — Gestão e Diagnóstico Redis Object Cache
Plugin `redis-cache` (Till Krüss), versão `2.8.0` confirmada em produção.
Object cache persistente — cache de objectos/queries WordPress, **não** é
page cache (ver secção "Object cache vs page cache" abaixo).
**Fonte:** `CONFIG-Plugins-Referencia.md` secção 6 (Redis Object Cache,
mapeado 16-08-2026) + verificação SSH ao vivo desta sessão (16-08-2026) em
`emanuelalmeida.pt` e nos 7 sites do bundle + expansão de cobertura
(16-08-2026) por leitura integral do código-fonte do plugin
(`includes/object-cache.php`, `includes/class-plugin.php`,
`includes/class-predis.php`, `includes/class-metrics.php`,
`includes/diagnostics.php`, `includes/cli/class-commands.php`,
`wp-includes/load.php` do core) — mapeamento exaustivo de todas as
constantes `WP_REDIS_*`, subcomandos WP-CLI, hooks e semântica de grupos,
não apenas o subconjunto tocado na auditoria pontual original.
---
## Contexto CWP — sempre obrigatório
```bash
# Via SSH directo (mais rápido para wp redis, comando nativo)
ssh server "sudo -u USER /usr/local/bin/wp redis status --path=/home/USER/SITE_PATH"
# Formato CWP completo (PHP versionado)
sudo -u USER /opt/alt/php-fpm82/usr/bin/php /usr/local/bin/wp redis status \
--allow-root --path=/home/USER/public_html
```
```
servidor: server.descomplicar.pt | porta: 9443 | user: root
```
**Único comando nativo do plugin:** `wp redis <subcomando>`. Confirmado ao
vivo (`wp redis --help --path=$PATH`):
| Subcomando | Faz |
|---|---|
| `wp redis status` | Mostra ligação, client, host/porta/DB/prefixo, grupos, drop-in |
| `wp redis enable` | Activa a cache (escreve o drop-in `object-cache.php`) |
| `wp redis disable` | Desactiva a cache (remove o drop-in) |
| `wp redis update-dropin` | Actualiza o drop-in para a versão do plugin instalado |
**Gotcha real confirmado nesta sessão:** `wp redis flush` **não existe**.
A própria ajuda do comando diz: *"To flush call `wp cache flush`"*. Usar
sempre o comando genérico do WP-CLI (`wp cache flush`), que funciona porque
o drop-in `object-cache.php` está activo e intercepta o wrapper nativo.
**Reconfirmado nesta expansão (16-08-2026):** `wp help redis --path=$PATH`
ao vivo devolve exactamente os mesmos 4 subcomandos. O código-fonte
(`includes/cli/class-commands.php`, classe `Commands extends WP_CLI_Command`)
só define 4 métodos públicos — `status()`, `enable()`, `disable()`,
`update_dropin()` (mapeado para `update-dropin` via `@subcommand`) — pelo que
esta lista é exaustiva, não apenas a amostra testada. Não existe `wp redis
flush` nem `wp redis info` nesta versão (2.8.0).
---
## 1. Diagnóstico rápido — `wp redis status`
```bash
PATH=/home/USER/SITE_PATH
wp redis status --path=$PATH
```
Output real (`emanuelalmeida.pt`, DB 12):
```
Status: Ligado
Client: Predis (v2.4.0)
Drop-in: Valid
Disabled: No
Ping: PONG
Errors: []
PhpRedis: Not loaded
Relay: Not loaded
Predis: 2.4.0
PHP Version: 8.2.31
Plugin Version: 2.8.0
Redis Version: 5.0.3
WP_REDIS_HOST: "127.0.0.1"
WP_REDIS_PORT: 6379
WP_REDIS_DATABASE: 12
WP_REDIS_PREFIX: "emanuelalmeida_pt_"
WP_CACHE_KEY_SALT: "..."
Timeout: 1
Read Timeout: 1
Global Groups: [...24 grupos: blog-details, users, site-transient, ...]
Ignored Groups: ["counts", "plugins", "theme_json", "WPForms_Entry_Handler", "themes"]
Drop-ins: ["Redis Object Cache Drop-In v2.8.0 by Till Krüss"]
```
| Campo a verificar | Valor esperado | Se falhar |
|---|---|---|
| `Status` | `Ligado` | `Não ligado` → drop-in ausente ou desactivado, correr `wp redis enable` |
| `Ping` | `PONG` | Qualquer outra coisa → Redis não responde na porta 6379, verificar serviço `redis-server` no host |
| `Drop-in` | `Valid` | `Invalid`/`Outdated` → correr `wp redis update-dropin` |
| `Client` | `Predis 2.4.0` | Confirmado — `PhpRedis`/`Relay` **não instalados** no servidor (extensão PHP nativa ausente); Predis é fallback 100% funcional em PHP puro, apenas mais lento. Não é erro, é a configuração real e estável do servidor |
| `WP_REDIS_DATABASE` | número único por site | Ver secção "Isolamento entre sites" |
Ler `object-cache.php` como drop-in activo (não apenas plugin) é o
diferencial: `wp plugin is-active redis-cache` só confirma que o plugin
está ligado, **não** confirma que o drop-in está a interceptar as chamadas
de cache. `wp redis status` é a única fonte fiável de estado real.
---
## 2. Isolamento entre sites do bundle — verificação de integridade
Todos os sites do bundle Descomplicar® partilham a **mesma instância Redis**
(`127.0.0.1:6379`, um único servidor Redis no host). O isolamento é feito por
duas camadas independentes — `WP_REDIS_DATABASE` (índice de base lógica
Redis, 0-15 por omissão) **e** `WP_REDIS_PREFIX` (prefixo de chave) — para
que uma colisão numa camada não cause colisão real de dados.
### Comando repetível — listar DB de todos os sites e confirmar zero colisão
```bash
#!/usr/bin/env bash
# Corre no servidor (via ssh server "...") ou local com ssh -p 9443 root@server.descomplicar.pt
declare -A SITES=(
[descomplicar.pt]="ealmeida:/home/ealmeida/public_html"
[emanuelalmeida.pt]="ealmeida:/home/ealmeida/emanuelalmeida.pt"
[carstuff.pt]="carstuff:/home/carstuff/public_html"
[familyclinic.pt]="familycl:/home/familycl/public_html"
[ignitionvortex.pt]="ignition:/home/ignition/public_html"
[solarfvengenharia.com]="solarfv:/home/solarfv/public_html"
[watercontrol.pt]="wtc:/home/wtc/public_html"
)
DBS=""
for site in "${!SITES[@]}"; do
user="${SITES[$site]%%:*}"; path="${SITES[$site]#*:}"
active=$(sudo -u "$user" /usr/local/bin/wp plugin is-active redis-cache --path="$path" 2>/dev/null && echo ACTIVE || echo INACTIVE)
db=$(sudo -u "$user" /usr/local/bin/wp config get WP_REDIS_DATABASE --path="$path" 2>/dev/null)
prefix=$(sudo -u "$user" /usr/local/bin/wp config get WP_REDIS_PREFIX --path="$path" 2>/dev/null)
printf "%-24s %-9s DB=%-3s prefix=%s\n" "$site" "$active" "$db" "$prefix"
[ "$active" = "ACTIVE" ] && DBS="$DBS $db"
done
echo "--- DBs activas: $DBS"
echo "--- duplicados:"; echo $DBS | tr ' ' '\n' | sort | uniq -d
```
**Resultado real desta sessão (16-08-2026):**
| Site | Plugin | `WP_REDIS_DATABASE` | `WP_REDIS_PREFIX` |
|---|---|---|---|
| descomplicar.pt | **inactive** | `0` (default, não usado) | — |
| emanuelalmeida.pt | active | `12` | `emanuelalmeida_pt_` |
| carstuff.pt | active | `1` | `carstuff_` |
| familyclinic.pt | active | `3` | `familycl_` |
| ignitionvortex.pt | active | `4` | `ignition_` |
| solarfvengenharia.com | active | `6` | `solarfv_` |
| watercontrol.pt | active | `7` | `wtc_` |
**Zero colisões** — DBs activas `{1, 3, 4, 6, 7, 12}` são todas distintas, e
mesmo que duas colidissem, o `WP_REDIS_PREFIX` distinto por site seria a
segunda barreira. `descomplicar.pt` (site principal Descomplicar®) tem o
plugin **instalado mas inactivo** — não usa object cache Redis; confirmar
antes de assumir que está coberto pelo bundle.
**Gotcha:** a linha `echo $DBS | tr ' ' '\n' | sort | uniq -d` fica vazia
quando não há duplicados — silêncio = sucesso. Um output não-vazio ali é
alarme real de colisão de DB entre dois sites do bundle.
---
## 3. Object cache vs page cache — não conflitam, são camadas diferentes
| Camada | Plugin | O que guarda | Onde vive |
|---|---|---|---|
| **Object cache** | Redis Object Cache (este) | Resultados de queries SQL, `wp_options` autoload, transients, meta — substitui o cache-in-memory por pedido do WordPress (`WP_Object_Cache`) por um persistente entre pedidos | Redis (`127.0.0.1:6379`) |
| **Page cache** | WP Fastest Cache / WP Meteor | HTML final já renderizado de uma página, servido antes do WordPress arrancar | Ficheiros estáticos em disco (`wp-content/cache/`) |
São independentes e **complementares**: o page cache evita mesmo carregar o
WordPress; quando falha um cache de página (visitante logado, POST,
página não cacheável) é o object cache Redis que absorve o custo das
queries repetidas. Não há necessidade de coordenar TTLs ou invalidação
entre os dois — cada um limpa-se pelo seu próprio mecanismo (`wp cache
flush` para Redis; purga própria do WP Fastest Cache/WP Meteor para HTML).
---
## 4. Gotcha crítico — SQL directo não invalida o Redis
**Mesma nota da skill `/rank-math`:** qualquer alteração feita via `wp db
query` (UPDATE/DELETE directo em `wp_options`, `wp_postmeta`, etc.) **não
passa pelas funções WordPress** (`update_option()`, `update_post_meta()`)
que disparam a invalidação de cache. Com o object cache Redis activo, os
dados antigos continuam servidos da cache até expirarem ou até um flush
explícito — o que pode demorar minutos a horas dependendo do grupo.
**Regra:** SEMPRE `wp cache flush --path=$PATH` imediatamente a seguir a
qualquer `wp db query` de escrita, antes de validar o resultado no site.
```bash
wp db query "UPDATE ${PREFIX}options SET option_value='...' WHERE option_name='...';" --path=$PATH
wp cache flush --path=$PATH # OBRIGATÓRIO — sem isto o Redis serve dados stale
```
Sem este passo, uma auditoria/correcção pode parecer ter falhado (o browser
mostra o valor antigo) quando na verdade a escrita na BD foi bem-sucedida —
é só a camada de cache que ainda não sabe.
---
## 5. Grupos ignorados por omissão
Confirmado via `wp redis status`:
```
Ignored Groups: ["counts", "plugins", "theme_json", "WPForms_Entry_Handler", "themes"]
```
Estes grupos ficam **fora** do Redis mesmo com o object cache activo —
mudam com frequência ou têm de reflectir estado imediato (contagem de
comentários, lista de plugins/temas). É a configuração por omissão do
plugin, correcta para não introduzir dados desactualizados nestes pontos.
Os restantes 24 grupos (`users`, `site-transient`, `usermeta`,
`redis-cache`, etc.) são cacheados normalmente — ver output completo de
`wp redis status` para a lista exaustiva por site.
---
## 6. Constantes completas suportadas (`WP_REDIS_*`)
Levantamento exaustivo por leitura integral de
`includes/object-cache.php` (drop-in, 3066 linhas), `includes/class-plugin.php`
(1685 linhas), `includes/class-predis.php`, `includes/class-metrics.php` e
`includes/diagnostics.php` — não apenas as 5 constantes usadas no bundle
Descomplicar®. Todas por definir em `wp-config.php` **antes** do WordPress
arrancar (mesmo princípio do `WP_REDIS_DATABASE`/`WP_REDIS_PREFIX` já usado).
### Ligação básica
| Constante | Default | Descrição |
|---|---|---|
| `WP_REDIS_HOST` | `127.0.0.1` | Host do servidor Redis |
| `WP_REDIS_PORT` | `6379` | Porta TCP |
| `WP_REDIS_PATH` | — | Caminho do socket Unix; só usado quando `WP_REDIS_SCHEME=unix` (nesse caso `host`/`port` são descartados) |
| `WP_REDIS_SCHEME` | `tcp` | `tcp` / `unix` / `tls` (`rediss`) |
| `WP_REDIS_DATABASE` | `0` | Índice da base lógica Redis (0-15 por omissão) — usado no isolamento entre sites (secção 2) |
| `WP_REDIS_PASSWORD` | — | String (só password) ou array `[username, password]` para ACL Redis 6+. Mascarado como `••••••••` em `wp redis status`/diagnostics |
| `WP_REDIS_USERNAME` | — | Username ACL explícito; sobrepõe-se ao `[0]` de `WP_REDIS_PASSWORD` quando ambos definidos (só cliente Predis) |
| `WP_REDIS_TIMEOUT` | `1` | Timeout de ligação, em segundos |
| `WP_REDIS_READ_TIMEOUT` | `1` | Timeout de leitura, em segundos |
| `WP_REDIS_RETRY_INTERVAL` | `null` | Intervalo entre tentativas de reconexão, em ms |
| `WP_REDIS_SSL_CONTEXT` | — | Array de opções de stream context PHP para TLS, ex. `['verify_peer' => false]` |
| `WP_REDIS_CLIENT` | auto (`phpredis` se a extensão existir, senão `predis`) | `phpredis` / `pecl` (alias de `phpredis`) / `relay` / `credis` / `predis` — força o cliente |
| `WP_REDIS_IGBINARY` | `false` | bool — usa serialização `igbinary` se a extensão `igbinary` estiver carregada |
**Mecanismo:** `build_parameters()` em `object-cache.php` itera um array fixo
de settings (`scheme, host, port, path, password, database, timeout,
read_timeout, retry_interval`) e resolve cada um via
`sprintf('WP_REDIS_%s', strtoupper($setting))` — é assim, literalmente, que
o nome de cada constante é gerado a partir do parâmetro Redis correspondente.
### Alta disponibilidade / escala
| Constante | Descrição |
|---|---|
| `WP_REDIS_CLUSTER` | Array de DSNs (ou string = id de ligação nomeada) — activa modo Redis Cluster |
| `WP_REDIS_SHARDS` | Array de servidores — sharding client-side via `RedisArray` (só cliente `phpredis`) |
| `WP_REDIS_SENTINEL` | Nome do serviço Sentinel — requer `WP_REDIS_SERVERS` com os hosts dos sentinels |
| `WP_REDIS_SERVERS` | Array de servidores — replicação Predis (`options['replication']='predis'`) ou lista de sentinels quando `WP_REDIS_SENTINEL` também está definida |
Nenhuma destas está em uso no bundle Descomplicar® (todos os sites usam uma
única instância Redis local, sem cluster/replicação) — documentado aqui por
cobertura completa do plugin, não por uso confirmado em produção.
### Comportamento da cache
| Constante | Default | Descrição |
|---|---|---|
| `WP_REDIS_PREFIX` | — | Prefixo de chave (isolamento, ver secção 2); herda de `WP_CACHE_KEY_SALT` se esta estiver definida e `WP_REDIS_PREFIX` não estiver; em Cloudways é preenchido automaticamente a partir de `HTTP_X_APP_USER` |
| `WP_REDIS_SELECTIVE_FLUSH` | `false` | bool — `wp cache flush`/`flush()` só apaga chaves com o prefixo (script Lua via `SCAN`+`DEL`) em vez de `FLUSHDB` total à base lógica inteira |
| `WP_REDIS_GLOBAL_GROUPS` | 17 grupos por omissão (ver secção 9) | array — **substitui** (não soma) a lista de grupos partilhados entre sites de uma rede multisite |
| `WP_REDIS_IGNORED_GROUPS` | `[]` (vazio no plugin) | array — **substitui** a lista de grupos que nunca são persistidos no Redis |
| `WP_REDIS_UNFLUSHABLE_GROUPS` | `[]` (vazio) | array — **substitui** a lista de grupos que continuam em cache mas sobrevivem a um `flush()` |
| `WP_REDIS_MAXTTL` | — | int (segundos) — tecto aplicado a QUALQUER TTL, incluindo entradas "para sempre" (`expire=0`); `validate_expiration()` força o valor para o máximo sempre que `expiration === 0 \|\| expiration > max` |
| `WP_REDIS_FLUSH_TIMEOUT` | `5` | int (segundos) — timeout de socket aplicado só durante o flush selectivo/scripts Lua (`execute_lua_script()`) |
| `WP_REDIS_DISABLE_GROUP_FLUSH` | `false` | bool — `flush_group()` deixa de ser selectivo e cai num `flush()` total à base |
| `WP_REDIS_GRACEFUL` | `false` | bool — falha silenciosamente (sem excepção fatal) se o Redis estiver inacessível na ligação inicial |
| `WP_REDIS_DISABLED` | `false` | bool — desliga TODO o drop-in (nenhuma função `wp_cache_*` chega a ser definida, pois envolve o ficheiro inteiro num `if`); ver nuance de segurança na secção "Erros comuns" |
### Admin / UI / segurança
| Constante | Default | Descrição |
|---|---|---|
| `WP_REDIS_DISABLE_BANNERS` | `false` | bool — esconde todos os banners de upsell "Object Cache Pro" (dashboard, JS admin `disable_pro`, aviso WooCommerce) |
| `WP_REDIS_DISABLE_DROPIN_BANNERS` | `false` | bool — esconde só os avisos "drop-in em falta/desactualizado" no admin |
| `WP_REDIS_DISABLE_ADMINBAR` | `false` | bool — remove o nó "Object Cache" da barra de admin |
| `WP_REDIS_DISABLE_COMMENT` | `false` | bool — suprime o comentário HTML de rodapé "Performance optimized by Redis Object Cache" (hook `shutdown`) |
| `WP_REDIS_MANAGER_CAPABILITY` | `manage_options` (`manage_network_options` em multisite) | string — sobrepõe-se à capability necessária para gerir o plugin (menu, acções, admin bar); passa também pelo filtro `redis_cache_manager_capability` |
| `WP_REDIS_DISABLE_DROPIN_CHECK` | `false` | bool — salta o teste completo de escrita (copiar/verificar versão/apagar ficheiro `object-cache.tmp`), só confirma existência+permissão de escrita do drop-in já instalado |
| `WP_REDIS_DISABLE_DROPIN_AUTOUPDATE` | `false` | bool — desliga a auto-actualização do drop-in em `admin_init` quando a versão instalada do plugin muda |
### Métricas
| Constante | Default | Descrição |
|---|---|---|
| `WP_REDIS_DISABLE_METRICS` | `false` | bool — desliga completamente a recolha de métricas (esconde tab "Metrics", widget de dashboard, secção da admin bar) |
| `WP_REDIS_METRICS_MAX_TIME` | `3600` (`HOUR_IN_SECONDS`) | int (segundos) — janela de retenção/visualização das métricas; usado tanto para o gráfico como para a purga via cron `rediscache_discard_metrics` |
### Caminho do plugin
| Constante | Descrição |
|---|---|
| `WP_REDIS_PLUGIN_PATH` | Só tem efeito se definida **antes** do ficheiro principal do plugin carregar (`if (!defined('WP_REDIS_PLUGIN_PATH')) define(...)`) — é o mecanismo que o bundle partilhado Descomplicar® usa para apontar o drop-in de cada site para a cópia partilhada do plugin em vez de `wp-content/plugins/redis-cache` local |
---
## 7. Grupos de cache — semântica exacta dos 3 tipos
O construtor de `WP_Object_Cache` (`object-cache.php` linhas ~508-529) usa
três arrays distintos, cada um com comportamento próprio:
| Tipo | Efeito | Constante de override | Lista por omissão do plugin |
|---|---|---|---|
| `global_groups` | Partilhado entre todos os sites de uma rede multisite (não leva `blog_prefix`) | `WP_REDIS_GLOBAL_GROUPS` (substitui) | 17 grupos: `blog-details, blog-id-cache, blog-lookup, global-posts, networks, rss, sites, site-details, site-lookup, site-options, site-transient, users, useremail, userlogins, usermeta, user_meta, userslugs` (+ `redis-cache` sempre adicionado) |
| `ignored_groups` | Nunca chega a ir para o Redis — fica só em memória do pedido actual | `WP_REDIS_IGNORED_GROUPS` (substitui) | **`[]` vazio no próprio plugin** |
| `unflushable_groups` | Fica em cache normalmente, mas sobrevive a um `flush()`/`wp cache flush` | `WP_REDIS_UNFLUSHABLE_GROUPS` (substitui) | `[]` vazio |
**Correcção/precisão sobre a secção 5 acima:** os 5 "grupos ignorados por
omissão" vistos em `wp redis status`
(`counts, plugins, theme_json, WPForms_Entry_Handler, themes`) **não são
configuração do plugin `redis-cache`** — o array `$ignored_groups` do
próprio plugin nasce vazio. São chamadas em runtime à função wrapper
`wp_cache_add_non_persistent_groups()` (que faz `array_merge`, não
substitui) feitas por:
- **WordPress core**, em `wp-includes/load.php::wp_start_object_cache()`
(confirmado ao vivo nesta expansão): `wp_cache_add_non_persistent_groups(
['counts', 'plugins', 'theme_json'] )`, logo a seguir a carregar o drop-in;
- **WordPress core**, em `wp-includes/class-wp-theme.php:264`:
`wp_cache_add_non_persistent_groups( 'themes' )`;
- **Plugin WPForms** (`WPForms_Entry_Handler`), específico deste site — noutro
site sem WPForms este grupo não aparece.
Ou seja: a lista de grupos ignorados varia por site consoante os plugins
activos, não é fixa do `redis-cache`. Da mesma forma, o WordPress core
regista o seu próprio conjunto (mais amplo, 22 grupos incluindo
`blog_meta`, `image_editor`, `network-queries`, `site-queries`,
`theme_files`, `translation_files`, `user-queries`) via
`wp_cache_add_global_groups()` no mesmo `wp_start_object_cache()`, que se
soma (`array_unique(array_merge(...))`) aos 17 do plugin.
---
## 8. Arquitectura — zero `wp_options`, tudo por constante
Confirmado ao vivo nesta expansão:
```bash
wp option list --search='*redis*' --path=$PATH --format=json # []
wp option list --search='*rediscache*' --path=$PATH --format=json # []
```
O `redis-cache` **não grava nenhuma opção na base de dados**. Não há página
de definições com formulário — `includes/ui/settings.php` é só um wrapper
de tabs (Overview/Metrics/Diagnostics) que lê constantes e o estado ao vivo
do Redis. Toda a configuração vive em `wp-config.php` (constantes) e o único
estado persistente do plugin é:
- o próprio ficheiro drop-in `wp-content/object-cache.php` (presença =
cache activa);
- as métricas, gravadas **dentro do próprio Redis** como sorted set na chave
`<prefix>metrics:redis-cache` (via `ZADD`/`ZRANGEBYSCORE`), purgadas por
hora via cron `rediscache_discard_metrics` — não em `wp_options` nem em
ficheiro.
Isto tem implicação directa para migração/clonagem de sites: copiar
`wp_options` entre sites nunca traz nem perde configuração deste plugin —
só o `wp-config.php` (constantes) e o `wp-content/object-cache.php`
(presença do drop-in) importam.
---
## 9. Interface admin — o que existe na prática
- **3 tabs** na página `Definições → Redis` (`options-general.php?page=redis-cache`,
ou `settings.php?page=redis-cache` em multisite): **Overview**, **Metrics**
(desactivado se `Metrics::is_enabled()` for falso, i.e.
`WP_REDIS_DISABLE_METRICS`), **Diagnostics** (o mesmo output de
`wp redis status`, renderizado em HTML).
- **Admin bar**: nó "Object Cache" (removível com `WP_REDIS_DISABLE_ADMINBAR`)
com submenu "Flush Cache" (AJAX, acção `roc_flush_cache`) e "Settings";
mostra hit-ratio/hits/misses/tamanho da página actual em hover.
- **Widget de dashboard** ("Redis Object Cache"), só visível a quem tem a
capability de gestão e só se `Metrics::is_enabled()`.
- **Integração Query Monitor**: regista um collector/output próprio
(`class-qm-collector.php`, `class-qm-output.php`) que acrescenta um painel
"Cache" ao Query Monitor quando este plugin está activo — automático, sem
configuração.
- **Aviso específico WooCommerce**: notice de upsell Pro nas páginas de
encomendas/produtos/analytics do WooCommerce (silenciável com
`WP_REDIS_DISABLE_BANNERS`).
### Acções da página de definições vs comandos WP-CLI
A página admin usa links com nonce (`?action=<x>&_wpnonce=...`) que chamam
exactamente a mesma lógica dos comandos WP-CLI:
| Acção admin (`action=`) | Equivalente WP-CLI | Efeito |
|---|---|---|
| `enable-cache` | `wp redis enable` | Copia `includes/object-cache.php` para `wp-content/object-cache.php` |
| `disable-cache` | `wp redis disable` | Apaga `wp-content/object-cache.php` |
| `update-dropin` | `wp redis update-dropin` | Reescreve o drop-in com a versão actual do plugin |
| `flush-cache` | `wp cache flush` | Chama `wp_cache_flush()` directamente (sem passar pelo WP-CLI genérico) |
---
## 10. Hooks disponíveis para integração
| Hook | Tipo | Quando dispara |
|---|---|---|
| `redis_object_cache_enable` | action | Depois de copiar o drop-in ao activar (via admin ou `wp redis enable`) — argumento: `bool $result` |
| `redis_object_cache_disable` | action | Depois de apagar o drop-in ao desactivar |
| `redis_object_cache_update_dropin` | action | Depois de `wp redis update-dropin` / auto-update |
| `redis_object_cache_flush` | action | Em cada `flush()` — argumentos: `$results, $deprecated, $selective, $salt, $execute_time` |
| `redis_cache_manager_capability` | filter | Sobrepõe a capability de gestão (default `manage_options`/`manage_network_options`), além/em vez de `WP_REDIS_MANAGER_CAPABILITY` |
| `redis_cache_validate_dropin` | filter | Sobrepõe a validação de "o drop-in instalado é o deste plugin" (compara `PluginURI`) |
| `redis_cache_add_non_persistent_groups` | filter | Filtra a lista de grupos antes de `add_non_persistent_groups()` os fundir em `ignored_groups` |
---
## 11. Auto-resolução de conflitos com outros plugins de cache
O plugin desactiva proactivamente os object caches nativos de outros
plugins de cache/performance para evitar dois drop-ins a lutar pelo mesmo
ficheiro:
```php
add_filter( 'perflab_disable_object_cache_dropin', '__return_true' ); // WordPress Performance Lab
add_filter( 'w3tc_config_item_objectcache.enabled', '__return_false' ); // W3 Total Cache
add_action( 'litespeed_init', [ $this, 'litespeed_disable_objectcache' ] ); // LiteSpeed Cache
```
Relevante para o bundle Descomplicar®: confirma que activar `redis-cache`
num site que também tenha W3TC ou LiteSpeed Cache instalado (mesmo que
inactivo o respectivo módulo de object cache) não gera conflito — o
`redis-cache` desliga automaticamente a componente de object cache desses
plugins via filtro, sem intervenção manual.
## Erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| `wp redis flush` → "not a registered command" | Subcomando não existe no plugin | Usar `wp cache flush` (genérico WP-CLI, funciona via drop-in activo) |
| Dados alterados por `wp db query` não aparecem no site | Redis serve versão em cache, SQL directo não invalida | `wp cache flush --path=$PATH` logo a seguir a qualquer escrita SQL |
| `wp redis status` mostra `Client: Predis`, não `PhpRedis` | Extensão PHP nativa `redis` não instalada no servidor | Não é erro — Predis é fallback 100% funcional, apenas mais lento; upgrade futuro se a extensão for instalada |
| Dois sites com o mesmo `WP_REDIS_DATABASE` (colisão) | `wp-config.php` copiado entre sites sem ajustar `WP_REDIS_DATABASE`/`WP_REDIS_PREFIX` | Correr a verificação da secção 2 após qualquer clone/migração de site no mesmo servidor; atribuir DB livre (0-15) e prefixo único |
| `wp redis status` → `Status: Não ligado` mas plugin activo | Drop-in `object-cache.php` ausente/removido de `wp-content/` (ex.: apagado por engano numa migração) | `wp redis enable` reescreve o drop-in |
| Confundir "plugin activo" com "cache activa" | `wp plugin is-active redis-cache` só confirma o plugin, não o drop-in | Confirmar sempre com `wp redis status` → `Ping: PONG` |
| Definir `WP_REDIS_DISABLED=true` sem remover o drop-in | Comportamento intencional, não um bug | Não é preciso apagar `object-cache.php`; a constante sozinha faz com que nenhuma função `wp_cache_*` seja definida pelo drop-in, e o WordPress core (`wp-includes/load.php::wp_start_object_cache()`) deteta isso e carrega automaticamente o fallback nativo `wp-includes/cache.php` — confirmado por leitura do core nesta expansão |
| `wp redis status` → grupos ignorados diferentes de site para site | Não é config do plugin — WordPress core adiciona `counts/plugins/theme_json/themes` e plugins activos (ex. WPForms → `WPForms_Entry_Handler`) via `wp_cache_add_non_persistent_groups()` em runtime | Normal; comparar só via `WP_REDIS_IGNORED_GROUPS` se quiseres saber o que o `redis-cache` em si define (por omissão, nenhum) |
@@ -0,0 +1,526 @@
---
name: seguranca-descomplicar
description: Gestão do plugin próprio "Segurança Descomplicar" (seguranca-descomplicar, v1.4.2) via WP-CLI e REST API nos 7 sites do bundle CWP. Cobre os 5 módulos — Security Headers (CSP/XFO/security.txt), Wordfence Agent (endpoint REST autenticado para sites sem SSH), Login/dash (URL de login customizada), Descomplicar Monitor (telemetria para dash.descomplicar.pt), Admin Hardening (Classic Editor, emails de update, nags) — com comandos `wp seguranca-descomplicar`, options `segdesc_*`, e gotchas reais de produção. Usar quando "seguranca-descomplicar", "plugin de segurança Descomplicar", "CSP", "X-Frame-Options", "security.txt", "wordfence agent", "login dash", "/dash", "descomplicar monitor", "admin hardening", "classic editor forçado", "esconder avisos de update", "token do agente", "allowlist IP wordfence agent".
---
# /seguranca-descomplicar — Plugin próprio de segurança WordPress (5 módulos)
Plugin normal (não mu-plugin, decisão explícita), instalado em
`wp-content/plugins/seguranca-descomplicar/`, **gerido inteiramente pela
Descomplicar®** — código-fonte único, versionado em
`71.Seguranca/plugin/seguranca-descomplicar/seguranca-descomplicar.php`,
distribuído por SCP/cp aos 7 sites do bundle. Substitui: `wordsense-agent.php`
(plugin antigo), mu-plugin `descomplicar-security-headers.php`, plugin
autónomo `descomplicar-monitor`, e todos os snippets WPCode/Code Snippets
de login `/dash` e admin hardening que estavam clonados byte-a-byte em 6 dos
7 sites.
**Fonte:** código-fonte completo lido nesta sessão (1494 linhas) +
`CONFIG-Plugins-Referencia.md` §2 (config ao vivo verificada nos 7 sites) +
`index.md` (changelog de 8 sessões de produção, bugs reais e correcções).
**Gerido/consumido por (ferramentas externas, fora desta skill):**
`71.03-SecHeaders/tools/secheaders.py` (módulo 1) e
`71.01-Wordsense/tools/wordsense.py`, transporte `http` (módulo 2).
---
## Contexto CWP — sempre obrigatório
```bash
# Local (quando o path existe no filesystem do agente)
wp seguranca-descomplicar <comando> --path=$PATH
# Via SSH (servidor real)
ssh -p 9443 root@server.descomplicar.pt \
"wp seguranca-descomplicar <comando> --path=$PATH --allow-root"
```
Namespace REST do plugin: `seguranca-descomplicar/v1` (todos os endpoints
exigem `Authorization: Bearer <token>`, token de 64 chars hex em
`segdesc_agent_token`).
---
## Decision tree — qual módulo/comando usar
| Preciso de | Módulo | Comando/opção |
|---|---|---|
| Ver/mudar CSP, XFO, security.txt | 1 | `wp seguranca-descomplicar headers status\|set` |
| Diagnosticar/ajustar Wordfence sem SSH (via HTTP) | 2 | REST `seguranca-descomplicar/v1/wordfence/*` |
| Ligar/mudar a URL de login custom | 3 | `wp seguranca-descomplicar login status\|enable\|disable\|slug\|block-wp-login` |
| Ver estado da telemetria para o dashboard | 4 | `wp option get descomplicar_monitor_*` (sem comando CLI próprio) |
| Ligar/desligar Classic Editor, emails de update, nags | 5 | `wp seguranca-descomplicar hardening status\|enable\|disable` |
| Rodar o token do agente REST | — | `wp seguranca-descomplicar token --rotate` |
| Restringir o REST endpoint a IPs específicos | — | `wp seguranca-descomplicar allowlist --set=IP1,IP2` |
---
## Referência de constantes (`SEGDESC_*`)
Todas as constantes definidas no plugin (topo do ficheiro, antes de
qualquer hook), agrupadas por função — confirma cobertura total do
código-fonte (26 constantes no total).
**Identificação**
| Constante | Valor | Nota |
|---|---|---|
| `SEGDESC_VERSION` | `'1.4.2'` | Devolvida em `wordfence/status.agent_version` |
| `SEGDESC_NS` | `'seguranca-descomplicar/v1'` | Namespace REST |
**Options (`segdesc_*`, persistidas em `wp_options`)** — ver tabelas de
cada módulo abaixo para defaults/comportamento; nomes reais:
`segdesc_agent_token`, `segdesc_headers_enabled`, `segdesc_csp`,
`segdesc_xfo`, `segdesc_csp_report_only`, `segdesc_security_txt`,
`segdesc_ip_allowlist`, `segdesc_nginx_csp_covered`,
`segdesc_login_enabled`, `segdesc_login_slug`,
`segdesc_login_block_wplogin`, `segdesc_hardening_enabled`,
`segdesc_hardening_classic_editor`, `segdesc_hardening_update_emails`,
`segdesc_hardening_update_nags`.
**Defaults/valores hardcoded (literais PHP usados como fallback, não são options)**
| Constante | Valor |
|---|---|
| `SEGDESC_DEFAULT_LOGIN_SLUG` | `'dash'` |
| `SEGDESC_DEFAULT_CSP` | CSP de enforcement (ver secção 1) |
| `SEGDESC_DEFAULT_XFO` | `'SAMEORIGIN'` |
| `SEGDESC_DEFAULT_CSP_REPORT_ONLY` | CSP Report-Only (ver secção 1) |
| `SEGDESC_DANGEROUS_KEYS` | `serialize(['waf_status', 'isPaid', 'apiKey'])` — chaves `wfconfig` que exigem `force_dangerous=true` em `/wordfence/config` |
**Rate-limit do endpoint REST** (só editável no código, sem option/wp-cli)
| Constante | Valor | Nota |
|---|---|---|
| `SEGDESC_RATE_LIMIT_WINDOW` | `60` (segundos) | Janela do tecto geral de pedidos |
| `SEGDESC_RATE_LIMIT_MAX` | `60` | Pedidos nominais/IP/janela (~30 reais, ver nota da dupla invocação) |
| `SEGDESC_RATE_LIMIT_FAIL_MAX` | `10` | Falhas nominais/IP/janela antes do lockout (~5 reais) |
| `SEGDESC_RATE_LIMIT_FAIL_LOCKOUT` | `900` (segundos = 15 min) | Duração do lockout por IP |
---
## 1. Security Headers — CSP, X-Frame-Options, security.txt
Hook `send_headers` (envia CSP/XFO/CSP-Report-Only) + hook `init` (serve
`/.well-known/security.txt`, RFC 9116).
### Options (`segdesc_*`)
| Option | Default | Nota |
|---|---|---|
| `segdesc_headers_enabled` | `'1'` (definida na activação) | Liga/desliga o módulo inteiro |
| `segdesc_csp` | `SEGDESC_DEFAULT_CSP` (definida na activação) | CSP de **enforcement** — só é enviada se `nginx_csp_covered` ≠ `'1'` |
| `segdesc_xfo` | `SAMEORIGIN` (definida na activação) | Sempre enviada, independente do nginx |
| `segdesc_csp_report_only` | `SEGDESC_DEFAULT_CSP_REPORT_ONLY` (fallback em código, não persistida na activação) | Sempre enviada — telemetria; único mecanismo de report em sites sem template nginx |
| `segdesc_security_txt` | `''` | Sem default — vazio = ficheiro não é servido; conteúdo bruto do `.well-known/security.txt` |
| `segdesc_ip_allowlist` | `''` (vazio = sem restrição) | CSV de IPs — só afecta os endpoints REST do módulo 2, não o site público |
| `segdesc_nginx_csp_covered` | `'0'` | **`'1'` em sites com o template nginx aplicado** — desliga o CSP de enforcement do plugin (evita intersecção de directivas entre 2 CSPs); XFO continua sempre activo |
### Comandos
```bash
# Estado
wp seguranca-descomplicar headers status --path=$PATH
# Actualizar CSP/XFO
wp seguranca-descomplicar headers set --csp="default-src 'self'; script-src 'self' https:" --path=$PATH
wp seguranca-descomplicar headers set --xfo=SAMEORIGIN --path=$PATH
# security.txt a partir de ficheiro local
wp seguranca-descomplicar headers set --security-txt-file=/tmp/security.txt --path=$PATH
# Ligar/desligar o módulo inteiro
wp seguranca-descomplicar headers set --enabled=1 --path=$PATH
# Opções que o wp-cli custom NÃO cobre — usar wp option directo
wp option get segdesc_nginx_csp_covered --path=$PATH
wp option update segdesc_nginx_csp_covered 1 --path=$PATH # sites com template nginx aplicado
wp option get segdesc_ip_allowlist --path=$PATH
```
### CSP real em produção (v1.4.2 — permissivo por directiva, sem lista branca de domínios)
```
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;
style-src 'self' 'unsafe-inline' https:;
img-src 'self' data: https:;
font-src 'self' data: https:;
frame-src https:;
frame-ancestors 'self';
object-src 'none'
```
Decisão explícita do utilizador (10ª sessão): uma lista branca por domínio
(`frame-src youtube.com google.com ...`) é manutenção infinita — cada embed
novo obriga a voltar a editar. `frame-src https:` permite qualquer iframe
servido por HTTPS; o que interessa bloquear é `http://` não encriptado e
`data:`/`javascript:`. `object-src 'none'` e `frame-ancestors 'self'`
mantêm-se restritos (sem caso de uso legítimo / protecção anti-clickjacking
deste site).
**Nota de produção (16-08-2026):** na prática, no bundle CWP, CSP/XFO são
geridos maioritariamente pelo **template nginx** (`71.03-SecHeaders/`,
nível de servidor, sobrevive a cache/preload de plugin) nos sites que o
têm aplicado — o módulo do plugin fica então limitado a `security.txt` +
CSP Report-Only (telemetria, ver `segdesc_nginx_csp_covered`). Em
`emanuelalmeida.pt` (piloto desde a 11ª sessão), os headers passaram a
ser servidos por **Cloudflare Transform Rules** (token do plugin App for
Cloudflare®) — nginx e este plugin continuam a enviar os seus próprios
headers, mas ficam invisíveis ao tráfego real porque o `set` do
Cloudflare se sobrepõe antes de chegar ao browser; servem só de fallback
para acesso directo à origem (bypass do Cloudflare).
---
## 2. Wordfence Agent — endpoint REST autenticado (sites sem SSH)
8 rotas REST sob `seguranca-descomplicar/v1`, autenticação partilhada via
`segdesc_check_auth()` (allowlist → lockout → rate-limit → Bearer token).
Registadas sempre; `/wordfence/status` indica se o Wordfence está mesmo
activo no site.
| Rota | Método | Payload | Função |
|---|---|---|---|
| `/headers/status` | GET | — | Estado do módulo 1 |
| `/headers/set` | POST | `csp`, `xfo`, `security_txt`, `enabled` | Ajusta módulo 1 remotamente |
| `/wordfence/status` | GET | — | `wordfence_active`, `version`, `db_prefix` |
| `/wordfence/query` | POST | `sql` | **Só SELECT/SHOW contra exactamente 1 tabela `wf*`** — bloqueia JOIN, subquery, UNION, INSERT/UPDATE/DELETE/DROP/etc. |
| `/wordfence/config` | POST | `key`, `value`, `force_dangerous` | UPSERT em `{prefix}wfconfig`; chaves perigosas (`waf_status`, `isPaid`, `apiKey`) exigem `force_dangerous=true` |
| `/wordfence/unblock-ip` | POST | `ip` | `DELETE FROM {prefix}wfblockediplog WHERE IP = INET6_ATON(ip)` |
| `/wordfence/whitelist-ip` | POST | `ip`, `reason` | Acrescenta `"{ip} #{reason}"` a `neverBlockIP` em `wfconfig` |
| `/wordfence/scan` | POST | — | `wfScanEngine::startScan()` se a classe existir; senão `do_action('wordfence_doScan')` (fallback cron) |
**`/headers/*` — nota de proveniência:** os 2 endpoints REST de headers
(`status`/`set`) foram desenhados para o transporte `http` de
`71.03-SecHeaders/tools/secheaders.py` (comentário no código-fonte:
"usados por secheaders.py transporte 'http' (futuro, hoje só SSH)") —
presentes e funcionais, mas **ainda não consumidos em produção**:
`secheaders.py` aplica correcções via SSH/wp-cli directo, não via este
REST. Único consumidor real hoje é o próprio `wp seguranca-descomplicar
headers`.
### Autenticação e rate-limit (importante para clientes REST)
```
Authorization: Bearer <segdesc_agent_token>
```
- **Allowlist de IP** (opcional, `segdesc_ip_allowlist`) verificada primeiro.
- **Rate-limit** por IP: 60 pedidos/60s nominal — **mas o `WP_REST_Server`
invoca `permission_callback` DUAS VEZES por pedido HTTP real** (confirmado
ao vivo em `carstuff.pt`), pelo que o limite prático é ~30 pedidos reais,
não 60.
- **Lockout por falhas de auth:** 10 falhas nominais (~5 reais) → lockout de
15 min (900s), chaveado por IP via transient.
- **IP do cliente** (`segdesc_client_ip()`) só confia em `X-Forwarded-For`
quando `REMOTE_ADDR` é um proxy local confiável (`127.0.0.1`/`::1`) — as
portas backend do CWP (2083/2031/2095) são acessíveis directamente da
internet, por isso um pedido directo ao backend usa sempre `REMOTE_ADDR`,
nunca o XFF do atacante.
### Comandos WP-CLI relacionados
```bash
# Ver/rodar o token de autenticação do agente
wp seguranca-descomplicar token --path=$PATH
wp seguranca-descomplicar token --rotate --path=$PATH
# Allowlist de IP para os endpoints REST
wp seguranca-descomplicar allowlist --path=$PATH
wp seguranca-descomplicar allowlist --set=203.0.113.5,203.0.113.6 --path=$PATH
wp seguranca-descomplicar allowlist --clear --path=$PATH
```
### Exemplo de pedido real (curl)
```bash
TOKEN=$(wp seguranca-descomplicar token --path=$PATH)
curl -s https://site.pt/wp-json/seguranca-descomplicar/v1/wordfence/status \
-H "Authorization: Bearer $TOKEN"
```
---
## 3. Login /dash — URL de login customizada e segura
Substitui os antigos snippets WPCode "Alterar /wp-admin para /dash" (que
deixaram de executar por `E_DEPRECATED` em PHP 8.2 — ver Gotchas). Handler
em `template_redirect` prioridade 1 (antes do `redirect_canonical`,
prioridade 10).
### Comportamento quando `segdesc_login_enabled = '1'`
- `/<slug>` (default `dash`): mostra o form nativo de login WP se não
autenticado; `wp_redirect(admin_url())` se autenticado.
- `wp-login.php` directo (GET): **404** para não autenticados. O **POST**
do form (submissão real do login) continua a funcionar — o bloqueio só
intercepta GET.
- `/wp-admin/` directo: **404** para não autenticados, excepto
`admin-ajax.php` (necessário para plugins front-end).
### Hooks internos
| Hook | Prioridade | Função |
|---|---|---|
| `init` → `segdesc_login_rewrite_rule` | default (10) | Regista `add_rewrite_rule('^<slug>/?$', 'index.php?segdesc_login=1', 'top')` — só se o módulo estiver ligado |
| `query_vars` → `segdesc_login_query_var` | default (10) | Regista a query var `segdesc_login` (necessária para o WP reconhecer o parâmetro da rewrite rule) |
| `template_redirect` → `segdesc_login_handle` | **1** | Handler principal — antes do `redirect_canonical` (prioridade 10) |
| `init` → `segdesc_login_block_wp_login` | default (10) | Bloqueio de `wp-login.php` directo (GET) |
| `init` → `segdesc_login_block_wp_admin` | default (10) | Bloqueio de `/wp-admin/` directo (excepto `admin-ajax.php`) |
| `update_option_segdesc_login_slug` → `segdesc_login_flush_rules_on_slug_change` | default (10) | `flush_rewrite_rules()` automático sempre que `segdesc_login_slug` muda — **mesmo por `wp option update` directo**, não só pelo subcomando `login slug` |
### Options
| Option | Default | Nota |
|---|---|---|
| `segdesc_login_enabled` | `'0'` | Módulo desligado por omissão — só activo onde pedido |
| `segdesc_login_slug` | `'dash'` (constante `SEGDESC_DEFAULT_LOGIN_SLUG`, não persistida a menos que alterada) | Sanitizado para `[a-z0-9_-]` |
| `segdesc_login_block_wplogin` | `'1'` | Bloqueia `wp-login.php` directo (GET) |
### Comandos
```bash
wp seguranca-descomplicar login status --path=$PATH
wp seguranca-descomplicar login enable --path=$PATH # faz flush_rewrite_rules() automático
wp seguranca-descomplicar login disable --path=$PATH
wp seguranca-descomplicar login slug --set=acesso --path=$PATH
wp seguranca-descomplicar login block-wp-login --on --path=$PATH
wp seguranca-descomplicar login block-wp-login --off --path=$PATH
```
`status` devolve `enabled`, `slug`, `block_wp_login`, `login_url` (URL
completo já resolvido com `home_url()`).
**Nota carstuff.pt (achado 10ª sessão, fora do âmbito deste módulo):**
`/dash` naquele site serve login e `wp-login.php` redirecciona para
`/dash` por um mecanismo que **não é o Módulo 3** (nunca activado em
carstuff.pt até à migração completa) nem o snippet WPCode legado (sempre
em draft) — candidato mais provável é o `branda-white-labeling`
(login-screen custom). Login do admin funciona normalmente; não confundir
com este módulo ao diagnosticar `/dash` nesse site especificamente.
---
## 4. Descomplicar Monitor — telemetria para dash.descomplicar.pt
Classe `Descomplicar_Monitor` (singleton), **funde o antigo plugin autónomo
`descomplicar-monitor`** — reutiliza as MESMAS options
(`descomplicar_monitor_*`, prefixo diferente do resto do plugin, propositado
para preservar dados na migração) e o MESMO cron
(`descomplicar_monitor_cron`). Guard
`is_plugin_active('descomplicar-monitor/descomplicar-monitor.php')`: só
instancia se o plugin antigo não estiver activo (evita duplicar envios).
**Sem comando WP-CLI próprio** — este módulo não regista `WP_CLI::add_command`.
Interage-se via `wp option`, `wp eval`, ou a página de definições em
**Definições → Descomplicar Monitor** (`add_options_page`, capability
`manage_options`; grupo de settings `descomplicar_monitor_settings` via
`register_settings`, hook `admin_init`; AJAX `descomplicar_monitor_test` /
`descomplicar_monitor_send_now`, ambos com nonce próprio via
`check_ajax_referer`).
### Hooks internos
| Hook | Função |
|---|---|
| `admin_menu` → `add_admin_menu` | Regista a página **Definições → Descomplicar Monitor** |
| `admin_init` → `register_settings` | Regista o grupo `descomplicar_monitor_settings` (campos `api_key`, `enabled`) |
| `descomplicar_monitor_cron` → `send_monitoring_data` | Handler do cron — envia a telemetria |
| `cron_schedules` → `add_cron_interval` | Regista um intervalo **próprio** chamado `twice_daily` (43200s = 12h) — **não é o `twicedaily` nativo do WordPress** (nomes diferentes, mesmo valor); `wp_schedule_event` usa este nome custom |
| `wp_ajax_descomplicar_monitor_test` → `ajax_test_connection` | AJAX "Testar Conexão" |
| `wp_ajax_descomplicar_monitor_send_now` → `ajax_send_now` | AJAX "Enviar Agora" |
| `init` → `ensure_cron` | Reagenda `descomplicar_monitor_cron` se `wp_next_scheduled()` devolver vazio |
**Sem `register_deactivation_hook`** (nenhum módulo do plugin tem) — ao
desactivar o plugin, o evento `descomplicar_monitor_cron` continua
agendado no wp-cron, mas o código deixa de carregar, logo o disparo do
evento não faz nada (sem handler registado) — consome um ciclo de cron
sem efeito, sem erro visível. Reactivar retoma o envio automaticamente,
sem reconfigurar nada (options preservadas).
### Options
| Option | Nota |
|---|---|
| `descomplicar_monitor_enabled` | `'1'`/`'0'` — sem module flag `segdesc_*` (nome legado preservado) |
| `descomplicar_monitor_api_key` | Chave enviada como header `X-API-Key` |
| `descomplicar_monitor_last_sent` | Timestamp `Y-m-d H:i:s`, actualizado em cada tentativa (sucesso ou falha) |
| `descomplicar_monitor_last_status` | `'success'` ou a mensagem de erro |
### Dados recolhidos (POST JSON para `https://dash.descomplicar.pt/api/wp-monitor`)
URL, nome, **email de admin**, versão WP/PHP/MySQL, **flag multisite**,
**timestamp ISO 8601** (`current_time('c')`) e **timezone** do site, tema
(+ parent), lista de plugins activos c/ versão, updates pendentes
(core/plugins/themes, com bloco `counts` agregado), contagem de
posts/páginas/utilizadores/comentários, estatísticas da BD (**nº de
tabelas `{prefix}*`**, tamanho em MB, **prefix**, transients expirados,
tamanho do autoload em KB), memory_limit, upload máximo, debug mode, SSL,
cron disabled, cache habilitada, e um bloco `health` (status
good/warning/critical com lista de issues: `WP_DEBUG` sem log, sem SSL,
uploads sem permissão de escrita, memória PHP < 64MB).
**Nota:** `get_pending_updates()` chama `wp_update_plugins()` e
`wp_update_themes()` directamente antes de ler `get_plugin_updates()`/
`get_theme_updates()` — força uma verificação síncrona a
`api.wordpress.org` em **cada execução do cron de 12h**, mesmo que o
WordPress já tivesse um resultado em cache válido. Corre em contexto de
cron (não bloqueia carregamento de página), por isso não é o mesmo
problema de performance do Gotcha #3 (que era em `admin_init`/page load).
### Comandos úteis
```bash
# Estado
wp option get descomplicar_monitor_enabled --path=$PATH
wp option get descomplicar_monitor_last_status --path=$PATH
wp option get descomplicar_monitor_last_sent --path=$PATH
# Forçar envio imediato (fora do cron 12h)
wp eval 'echo json_encode(Descomplicar_Monitor::get_instance()->send_monitoring_data());' --path=$PATH
# Confirmar que o cron está agendado (reagenda-se sozinho em cada 'init' se limpo)
wp cron event list --path=$PATH --fields=hook,next_run_relative | grep descomplicar_monitor_cron
```
---
## 5. Admin Hardening — Classic Editor, emails de update, nags
Substitui 3 snippets WPCode/Code Snippets clonados byte-a-byte (`md5sum`
idêntico) em 6 dos 7 sites CWP (origem: tutorial WPBeginner). O snippet de
emails tinha um bug real herdado: `add_filter('auto_core_update_send_email',
'wpb_stop_auto_update_emails', ...)` referenciava uma função nunca definida
(`wpb_stop_update_emails`, sem `_auto`), gerando `E_WARNING` silencioso em
cada auto-update. O módulo simplifica para `__return_false` directo.
### Options
| Option | Default (quando `hardening_enabled='1'`) | Nota |
|---|---|---|
| `segdesc_hardening_enabled` | `'0'` | Módulo desligado por omissão |
| `segdesc_hardening_classic_editor` | `'1'` | `use_block_editor_for_post{,_type}` → `__return_false` |
| `segdesc_hardening_update_emails` | `'1'` | `auto_{core,plugin,theme}_update_send_email` → `__return_false` |
| `segdesc_hardening_update_nags` | `'1'` | Wipe de `admin_notices`/`all_admin_notices`/`network_admin_notices` (incl. avisos Wordfence) + CSS para esconder badges de contagem |
### Comandos
```bash
wp seguranca-descomplicar hardening status --path=$PATH
wp seguranca-descomplicar hardening enable --path=$PATH
wp seguranca-descomplicar hardening enable --no-classic-editor --path=$PATH # liga o módulo mas deixa o bloco editor
wp seguranca-descomplicar hardening enable --no-update-emails --path=$PATH
wp seguranca-descomplicar hardening enable --no-update-nags --path=$PATH
wp seguranca-descomplicar hardening disable --path=$PATH
```
### Como o bloqueio de nags funciona de facto (decisão de negócio: agressivo)
Decisão do Emanuel: bloquear TAMBÉM avisos Wordfence e visibilidade de
updates pendentes — clientes reencaminhavam esses avisos/emails achando que
eram falha da Descomplicar. Dois mecanismos distintos, **nunca tocando nos
transients de update** (ver Gotchas #3):
1. **`in_admin_header` prioridade 999** → `unset($wp_filter['admin_notices'])`
(+ `all_admin_notices` + `network_admin_notices`). Tem de ser
`in_admin_header`, não `admin_init` (ver Gotchas #2).
2. **`admin_head`** → injecta `<style>` que esconde
`#adminmenu .update-plugins`, `#wpadminbar .update-plugins`,
`#adminmenu .wf-menu-badge` (o badge "Wordfence N" no menu não é um hook
— é HTML injectado directamente no título do item de menu por
`wordfenceClass.php`; só CSS apanha).
---
## Deploy aos 7 sites do bundle
Fonte do plugin canónico:
`71.Seguranca/plugin/seguranca-descomplicar/` → copiado para
`wp-content/plugins/seguranca-descomplicar/` em cada site
(`tools/distribuir_e_activar_plugin.py`: `cp -r` local se o `path` existir
no filesystem, senão `mkdir -p` + `scp -P 9443` + `ssh ... wp plugin
activate` via `root@server.descomplicar.pt`).
| Domínio | Conta CWP | Path |
|---|---|---|
| `descomplicar.pt` | `ealmeida` | `/home/ealmeida/public_html` |
| `emanuelalmeida.pt` | `ealmeida` | `/home/ealmeida/emanuelalmeida.pt` |
| `carstuff.pt` | `carstuff` | `/home/carstuff/public_html` |
| `familyclinic.pt` | `familycl` | `/home/familycl/public_html` |
| `ignitionvortex.pt` | `ignition` | `/home/ignition/public_html` |
| `solarfvengenharia.com` | `solarfv` | `/home/solarfv/public_html` |
| `watercontrol.pt` | `wtc` | `/home/wtc/public_html` |
```bash
python3 tools/distribuir_e_activar_plugin.py
```
`tools/executar_pipeline_completa.py` orquestra o ciclo completo de
segurança nos mesmos 7 domínios (`SITES_CONFIG`, campo `tipo:
"nossos_sites"`): 1) diagnóstico externo (`seguranca-check.py`) → 2)
diagnóstico de postura interno (`postura.py` → wordsense + softcheck) → 3)
correcção automática de headers (`secheaders.py`, template nginx ou este
plugin) → 4) re-análise de verificação → 5) relatório visual. Usa
`--confirm-fix` para aplicar correcções (sem a flag, é dry-run/diagnóstico).
**Estado ao vivo confirmado (16-08-2026):** 5 módulos activos e coerentes
nos 7 sites — `headers_enabled=1`, `login_enabled=1` (7/7, antes eram 5/7),
`hardening_enabled=1` (7/7), `bridge`/monitor migrado 7/7. Único desvio real
por site: `segdesc_nginx_csp_covered` só é `'1'` nos sites com o template
nginx aplicado (todos excepto casos pontuais em rollout).
**Scripts sem flags de plugin:** `distribuir_e_activar_plugin.py` não
aceita argumentos CLI — itera sempre a lista `SITES` completa hardcoded
(`cp -r` local se o `path` existir, senão `mkdir -p` + `scp -P 9443` +
`ssh ... wp plugin activate` via `root@server.descomplicar.pt`, porta
9443). `executar_pipeline_completa.py` só tem uma flag, `--confirm-fix`
(sem ela = dry-run/diagnóstico só); a lista `SITES_CONFIG` mapeia cada
domínio para `wordsense_site`/`softcheck_account` — usados por
`postura.py`/`wordsense.py` (fora desta skill), não por este plugin
directamente.
---
## Gotchas / erros reais corrigidos em produção
| # | Sintoma | Causa raiz | Correcção |
|---|---|---|---|
| 1 | Actualizar `post_content` de um snippet WPCode via `$wpdb->update` directo não desliga o snippet legado | O WPCode mantém uma cache compilada em `wp_options.wpcode_snippets`, separada de `wp_posts` — não invalida por escrita SQL directa | Apagar a option `wpcode_snippets` ou chamar `wpcode()->cache->cache_all_loaded_snippets()` depois de qualquer edição directa em BD |
| 2 | Aviso "ElementsKit Lite Premium" (ou outro plugin) sobrevive ao wipe de `admin_notices` | Alguns plugins registam o callback em `admin_notices` **de dentro** de outro callback pendurado em `admin_head` — que corre **depois** de `admin_init` na sequência de bootstrap (`admin_init → load-{page} → admin_head → in_admin_header → admin_notices`) | Wipe tem de estar em `in_admin_header` prioridade 999 (o último ponto seguro antes de `admin_notices` disparar), nunca em `admin_init` |
| 3 | Regressão de performance real: ~1,3s extra em **todo** o carregamento do wp-admin (medido: core 0,4s + plugins 0,5s + temas 0,3s) | Bloquear `pre_site_transient_update_*` para esconder nags impede a cache nativa de 12h do WordPress de "pegar" — `_maybe_update_core/plugins/themes()` vêem sempre "nunca verificado" e disparam pedido HTTP síncrono a `api.wordpress.org` em cada page load | Nunca tocar nos transients de update; esconder badges/contadores só por CSS (`.update-plugins`) |
| 4 | Badge "Wordfence N" no menu lateral continua visível apesar do wipe de `admin_notices` | Não vem de nenhum hook — é HTML injectado directamente no título do item de menu por `wordfenceClass.php` | Só CSS resolve: `#adminmenu .wf-menu-badge { display: none !important; }` |
| 5 | `/dash` dá 404, `wp-login.php` também dá 404 — login completamente inacessível | Snippet WPCode legado usava `basename(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH))` sem protecção — em PHP 8.2 `parse_url` pode devolver `null`, `basename(null)` dispara `E_DEPRECATED`; o WPCode apanha o erro e marca o snippet em `wpcode_snippets_errors`, **deixando de o executar por completo** (rewrite rule, query var e redirect todos mortos) | Migrado para código de plugin normal (Módulo 3), sem essa fragilidade; `wp rewrite flush --hard` depois de qualquer mudança de slug |
| 6 | Regra de rewrite `/<slug>` não persistia em alguns sites recém-activados | Rewrite rules não flushed após `login enable` em certas condições de cache de permalinks | `wp rewrite flush --hard` explícito após activar o módulo 3 |
| 7 | `security.txt` continha `mailto:seguranca@descomplicar.pt` — mailbox **inexistente** | Valor hardcoded em `tools/executar_pipeline_completa.py` (`--contact seguranca@descomplicar.pt`), replicado para os 7 sites no deploy inicial. Não foi apanhado pela auditoria automática (só validava presença/formato do ficheiro, não se o mailbox era real) | Corrigido nos 7 sites (`wp_options` ao vivo) e na origem (script) para `mailto:it@descomplicar.pt` — mesmo endereço usado em `alertEmails` do Wordfence, `rua`/`ruf` do DMARC, e utilizador `only-me` do WSAL |
| 8 | reCAPTCHA/YouTube bloqueados por CSP mesmo com `frame-src` a permitir | CSP duplicado (nginx + plugin) — browsers aplicam a directiva mais restritiva de **cada** CSP na intersecção; o nginx não declarava `frame-src`, caindo no fallback mais restrito | `segdesc_nginx_csp_covered = '1'` nos sites com template nginx — nginx passa a ser a única autoridade de enforcement, plugin só mantém XFO + Report-Only |
| 9 | `WP_REST_Server` conta o dobro dos pedidos reais nos limiares de rate-limit | `permission_callback` é invocado **duas vezes** por pedido HTTP real (confirmado por transient antes/depois de 1 pedido) | Limiares nominais (`FAIL_MAX=10`, `MAX=60`) valem, na prática, metade (~5 e ~30) — mantido assim de propósito, mais conservador não prejudica uso legítimo |
| 10 | `X-Forwarded-For` falsificável permitia contornar allowlist/rate-limit/lockout do endpoint REST | Portas backend do CWP (2083/2031/2095) acessíveis directamente da internet — confiar incondicionalmente em XFF permitia a um atacante que batesse directo no backend falsificar o IP visto | `segdesc_client_ip()` só confia em XFF quando `REMOTE_ADDR` é um proxy local confiável (`127.0.0.1`/`::1`), e só no último valor da cadeia (o que o próprio nginx anexa) |
| 11 | Desactivar o plugin não limpa cron/options/rewrite rules | Nenhum módulo regista `register_deactivation_hook` — só existe `register_activation_hook` (gera token + activa headers com CSP/XFO default) | `descomplicar_monitor_cron` fica agendado mas sem handler (evento inofensivo, sem efeito); opções `segdesc_*`/`descomplicar_monitor_*` e a rewrite rule do slug de login persistem; reactivar retoma o estado exactamente onde ficou — desejável para reinstalação, mas exige limpeza manual (`wp option delete`) numa desinstalação definitiva |
---
## Fonte
- `71.Seguranca/plugin/seguranca-descomplicar/seguranca-descomplicar.php`
(v1.4.2, 1494 linhas) — código-fonte completo, relido linha a linha
nesta expansão: todas as 26 constantes `SEGDESC_*`, os 6 hooks do
Módulo 3, os 7 hooks do Módulo 4 (incl. `cron_schedules` custom), os
8 endpoints REST (não 7 — corrigido nesta expansão), e os 5 subcomandos
WP-CLI (`token`, `allowlist`, `headers`, `login`, `hardening`)
confirmados exaustivos (sem `WP_CLI::add_command` adicional no
ficheiro).
- `71.Seguranca/CONFIG-Plugins-Referencia.md` §2 (`seguranca-descomplicar`)
— config ao vivo verificada via wp-cli nos 7 sites, 16-08-2026.
- `71.Seguranca/tools/distribuir_e_activar_plugin.py` e
`tools/executar_pipeline_completa.py` — relidos por completo; confirmado
que nenhum dos dois expõe flags de configuração do plugin em si (só
`--confirm-fix` no pipeline, sem argumentos no script de distribuição).
- `71.Seguranca/index.md` — lido na íntegra (linhas 1-627, não só
183-636 como na versão anterior desta skill); confirmou a contagem real
de 8 rotas REST (tabela de bundle da 12ª sessão: "8 rotas REST
(`/wordfence/*` × 6, `/headers/*` × 2)"), a nota de que CSP/XFO ficam
hoje maioritariamente a cargo do nginx/Cloudflare (módulo do plugin na
prática só serve `security.txt` nos sites com essa cobertura), e o caso
carstuff.pt (mecanismo de `/dash` alheio a este plugin).
+212 -2
View File
@@ -2,7 +2,6 @@
name: seo-post
description: Optimizar posts WordPress com RankMath Pro via WP-CLI. Auditar, corrigir e aplicar SEO completo incluindo schema, OG, robots e links internos. Usar quando "seo post", "rankmath", "schema", "optimizar artigo", "seo wordpress".
context: fork
layer: wiki
---
# /seo-post — Optimizar Posts WordPress com RankMath Pro
@@ -131,4 +130,215 @@ wp --allow-root post meta update POST_ID rank_math_twitter_card_type 'summary_la
wp --allow-root post update POST_ID --post_name='slug-curto-keyword'
```
### Passo 6: SEO Score (OBRIGATORIO — posts via CLI nao cal
### Passo 6: SEO Score (OBRIGATORIO — posts via CLI nao calculam score automaticamente)
O RankMath calcula o SEO Score apenas no editor Gutenberg (JavaScript). Posts criados via WP-CLI ficam com score "N/A" no dashboard. Este passo calcula e grava o score server-side.
```bash
wp --allow-root eval '
$post_id = POST_ID;
$post = get_post($post_id);
$title = get_post_meta($post_id, "rank_math_title", true);
$desc = get_post_meta($post_id, "rank_math_description", true);
$kw_raw = get_post_meta($post_id, "rank_math_focus_keyword", true);
$keywords = array_map("trim", explode(",", $kw_raw));
$kw = strtolower($keywords[0] ?? "");
$content = $post->post_content;
$slug = $post->post_name;
$word_count = str_word_count(strip_tags($content));
$score = 0;
// Testes SEO (20 criterios, max 86 pontos brutos -> normalizado 0-100)
if ($kw) $score += 5;
if ($title) $score += 5;
if ($desc) $score += 5;
if ($title && stripos($title, $kw) !== false) $score += 5;
if ($desc && stripos($desc, $kw) !== false) $score += 5;
if (stripos($slug, str_replace(" ", "-", $kw)) !== false) $score += 5;
if ($title && stripos($title, $kw) === 0) $score += 3;
$tlen = strlen($title); if ($tlen >= 30 && $tlen <= 70) $score += 5;
$dlen = strlen($desc); if ($dlen >= 100 && $dlen <= 160) $score += 5;
if (has_post_thumbnail($post_id)) $score += 5;
if ($word_count >= 600) $score += 5;
if ($word_count >= 300) $score += 3;
if (stripos($content, $kw) !== false) $score += 5;
if (preg_match_all("/href=[\"\\x27]https?:\\/\\/descomplicar\\.pt/i", $content) >= 1) $score += 3;
if (preg_match_all("/href=[\"\\x27]https?:\\/\\/(?!descomplicar\\.pt)/i", $content) >= 1) $score += 3;
if (preg_match("/<h[23]/i", $content)) $score += 3;
if (preg_match("/<h[23][^>]*>.*" . preg_quote($kw, "/") . "/si", $content)) $score += 3;
if (preg_match("/alt=[\"\\x27][^\"\\x27]+[\"\\x27]/i", $content)) $score += 2;
if (get_post_meta($post_id, "rank_math_rich_snippet", true)) $score += 3;
if (get_post_meta($post_id, "rank_math_primary_category", true)) $score += 2;
$final = min(100, max(0, round(($score / 86) * 100)));
update_post_meta($post_id, "rank_math_seo_score", $final);
echo "SEO Score: $final/100\n";
' --user=2
```
### Passo 7: Verificar
```bash
curl -s "URL" | grep -o '"@type":"[^"]*"' | sort -u
wp --allow-root post meta list POST_ID --format=table | grep rank_math
```
## Modo bulk (/seo-post bulk)
Aplicar schema a multiplos posts. Util para corrigir posts antigos.
```bash
# Aplicar schema Article a todos os guias sem schema
wp --allow-root eval '
$schema = [
"metadata" => ["title" => "Article", "type" => "custom", "shortcode" => "s-" . uniqid(), "isPrimary" => "1"],
"@type" => "Article",
"headline" => "%seo_title%",
"description" => "%seo_description%",
"keywords" => "%keywords%",
"image" => ["@type" => "ImageObject", "url" => "%post_thumbnail%"],
"author" => ["@type" => "Organization", "name" => "Descomplicar", "url" => "https://descomplicar.pt"],
"publisher" => ["@type" => "Organization", "name" => "Descomplicar", "url" => "https://descomplicar.pt"],
"datePublished" => "%date(Y-m-dTH:i:sP)%",
"dateModified" => "%modified(Y-m-dTH:i:sP)%",
"articleSection" => "%categories%"
];
$posts = get_posts(["numberposts" => -1, "post_status" => "publish", "meta_query" => [["key" => "rank_math_rich_snippet", "compare" => "NOT EXISTS"]]]);
$count = 0;
foreach ($posts as $p) {
update_post_meta($p->ID, "rank_math_rich_snippet", "article");
update_post_meta($p->ID, "rank_math_snippet_article_type", "BlogPosting");
update_post_meta($p->ID, "rank_math_schema_Article", $schema);
$count++;
}
echo "$count posts actualizados com schema Article\n";
' --user=2
```
### Bulk: calcular SEO Score para todos os posts sem score
```bash
wp --allow-root eval '
$posts = get_posts(["numberposts" => -1, "post_status" => "publish", "meta_query" => [
["key" => "rank_math_focus_keyword", "compare" => "EXISTS"],
["relation" => "OR",
["key" => "rank_math_seo_score", "compare" => "NOT EXISTS"],
["key" => "rank_math_seo_score", "value" => "", "compare" => "="]
]
]]);
$count = 0;
foreach ($posts as $p) {
$title = get_post_meta($p->ID, "rank_math_title", true);
$desc = get_post_meta($p->ID, "rank_math_description", true);
$kw = strtolower(trim(explode(",", get_post_meta($p->ID, "rank_math_focus_keyword", true))[0] ?? ""));
$slug = $p->post_name;
$content = $p->post_content;
$wc = str_word_count(strip_tags($content));
$s = 0;
if ($kw) $s += 5;
if ($title) $s += 5;
if ($desc) $s += 5;
if ($title && stripos($title, $kw) !== false) $s += 5;
if ($desc && stripos($desc, $kw) !== false) $s += 5;
if (stripos($slug, str_replace(" ", "-", $kw)) !== false) $s += 5;
if ($title && stripos($title, $kw) === 0) $s += 3;
$tl = strlen($title); if ($tl >= 30 && $tl <= 70) $s += 5;
$dl = strlen($desc); if ($dl >= 100 && $dl <= 160) $s += 5;
if (has_post_thumbnail($p->ID)) $s += 5;
if ($wc >= 600) $s += 5;
if ($wc >= 300) $s += 3;
if (stripos($content, $kw) !== false) $s += 5;
if (preg_match_all("/href=[\"\\x27]https?:\\/\\/descomplicar\\.pt/i", $content) >= 1) $s += 3;
if (preg_match_all("/href=[\"\\x27]https?:\\/\\/(?!descomplicar\\.pt)/i", $content) >= 1) $s += 3;
if (preg_match("/<h[23]/i", $content)) $s += 3;
if (preg_match("/<h[23][^>]*>.*" . preg_quote($kw, "/") . "/si", $content)) $s += 3;
if (preg_match("/alt=[\"\\x27][^\"\\x27]+[\"\\x27]/i", $content)) $s += 2;
if (get_post_meta($p->ID, "rank_math_rich_snippet", true)) $s += 3;
if (get_post_meta($p->ID, "rank_math_primary_category", true)) $s += 2;
$final = min(100, max(0, round(($s / 86) * 100)));
update_post_meta($p->ID, "rank_math_seo_score", $final);
$count++;
}
echo "$count posts actualizados com SEO score\n";
' --user=2
```
## Categorias de noticias (referencia)
| Categoria | ID | Slug |
|-----------|-----|------|
| Noticias (pai) | 1188 | noticias |
| IA e Agentes | 1189 | ia-agentes |
| Automacao | 1190 | automacao |
| Marketing Digital | 1191 | marketing-digital |
| Tecnologia | 1192 | tecnologia |
| Regulacao e Europa | 1193 | regulacao-europa |
| Negocios e PMEs | 1194 | negocios-pmes |
## Erros comuns
| Erro | Causa | Solucao |
|------|-------|---------|
| Schema: Off no dashboard | Faltam campos legacy | Definir rank_math_rich_snippet + rank_math_snippet_article_type |
| Schema nao aparece no HTML | Falta rank_math_schema_* | Usar wp eval com array PHP |
| Robots corrompidos | Sem --format=json | SEMPRE --format=json para arrays |
| User ID invalido | --user=1 nao existe | Usar --user=2 (ealmeida) |
| Heredoc no conteudo | cat <<EOF | NUNCA heredoc, aspas simples directas |
| SEO Score N/A no dashboard | Post criado via CLI sem Gutenberg | Executar Passo 6 (calculo server-side) |
## Referencia
- **Skill relacionada:** `/rank-math` — gestao completa RankMath (audit, bulk, redirects, indexing, analytics, backup)
- Manual completo: `Hub/06-Operacoes/Documentacao/Manuais/WP-CLI/Rank-Math-WP-CLI-Manual-Definitivo.md`
- Pesquisa Claude: `Hub/06-Operacoes/Documentacao/Manuais/WP-CLI/Rank-Math-SEO-WP-CLI-Skils-Pesquisa-Claude.md`
- Pesquisa Gemini: `Hub/06-Operacoes/Documentacao/Manuais/WP-CLI/Rank-Math-SEO-WP-CLI-Skills-Pesquisa-Gemini.md`
---
## Media Bank (D7-MB-001)
**Quando consultar:** antes de definir featured image ou imagens de suporte num post WP. O `description_en` do asset deve servir de base para gerar o `alt` text optimizado para SEO (traduzido para PT-PT + focus keyword).
**Workflow tentativa-primeiro:**
1. Pesquisar via MCP `media-bank`:
```
mcp__media-bank__search_media(
query="<focus keyword do post em EN>",
type="image",
tema="<um dos 9 temas>",
orientation="landscape",
min_quality=7,
min_width=1200,
exclude_used_in_last_days=30,
limit=5
)
```
2. Avaliar candidatos por `description_en` + `quality_score` + match com focus keyword.
3. Se ≥1 adequado → usar `path_local`; derivar `alt` text a partir de `description_en` traduzido para PT-PT e enriquecido com keyword.
4. Se nenhum → fallback para geração IA.
5. Marcar uso: `mcp__media-bank__mark_used(asset_id, {type:"seo-post", ref:"<post_id>", date:"<YYYY-MM-DD>"})`.
**Fallback REST** (se MCP indisponível):
```bash
SK=$(grep SERVICE_KEY /media/ealmeida/Dados/Hub/05-Projectos/Banco-Imagens-Videos/scripts/.env | cut -d= -f2)
curl -s "https://mem.descomplicar.pt/rest/v1/media_bank_assets?type=eq.image&orientation=eq.landscape&quality_score=gte.7&deleted_at=is.null&order=quality_score.desc&limit=5" \
-H "apikey: $SK" -H "Authorization: Bearer $SK"
```
**Regra inviolável:** matéria-prima local. Usar como input para composição — NUNCA inserir o ficheiro raw directamente em publicação pública.
Ver `Hub/06-Operacoes/Procedimentos/D7-Tecnologia/MCP/PROC-Media-Bank.md` (D7-MB-001) para detalhes.
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
@@ -0,0 +1,162 @@
---
name: seriously-simple-podcasting
description: Conhecimento profundo do Seriously Simple Podcasting (Castos, plugin `seriously-simple-podcasting`) — arquitectura, CPT `podcast` (episódios) e taxonomia `series` (shows), meta fields custom, REST API nativo (o que é escrita-capaz vs read-only), wp_options de definições de show (`ss_podcasting_*`), categorias iTunes/Apple Podcasts, feed RSS, e o estado real em emanuelalmeida.pt (39 episódios, 8 shows, sem addon Castos/Pro). Usar quando "seriously simple podcasting", "podcast wordpress", "episódio podcast", "show podcast", "feed rss podcast", "categorias itunes", "apple podcasts categoria", "castos", "ss_podcasting", "cpt podcast", ou qualquer trabalho de configuração/estratégia de podcast que não seja apenas chamar o MCP. Para execução directa (CRUD de episódios/shows), usar a tool MCP `mcp-seriously-simple-podcasting`.
layer: wiki
---
# /seriously-simple-podcasting — Seriously Simple Podcasting (Castos) — conhecimento de plugin
Plugin `seriously-simple-podcasting` (Free, `SSP_VERSION` 3.17.0), instalado e activo em
**emanuelalmeida.pt**. **Sem addon Castos/Pro** — não há `fluentcampaign-pro`-equivalente neste
site (confirmado: pasta `php/` do plugin não tem addon Pro, `ssp_is_connected_to_castos()` devolve
falso, sem hospedagem/sincronização externa de ficheiros). Mapeamento feito por leitura completa
do código-fonte (`wp-content/plugins/seriously-simple-podcasting/php/`) via SSH, 19-08-2026, antes
de construir o MCP `mcp-seriously-simple-podcasting`.
---
## 0. Free vs addon Castos (não instalado aqui)
| | Free (activo) | Addon Castos/Pro (ausente) |
|---|---|---|
| Episódios (CPT `podcast`), shows (taxonomia `series`), feed RSS | ✅ core | — |
| Upload/hosting de ficheiros de áudio | ❌ (URL externo só) | ✅ hosting Castos + analytics avançado |
| Sincronização automática (`podmotor_*` meta) | campos existem mas vazios (nunca usados) | ✅ |
| Transcrições, capítulos | não encontrado no código lido (sem módulo dedicado nesta versão) | — |
Todos os 39 episódios reais têm `audio_file` a apontar para `episodes.castos.com` (CDN legado de
uma ligação Castos anterior, hoje desligada) — os ficheiros continuam a servir de lá, mas o plugin
já não está autenticado/sincronizado com a conta.
---
## 1. Onde vivem os dados
### 1.1 Episódios — CPT `podcast` (constante `SSP_CPT_PODCAST`)
Post standard (`show_in_rest => true`), **sem tabela própria** — tudo em `wp_posts`/`wp_postmeta`.
Meta fields registados via `register_meta()` (todos `show_in_rest => true`, confirmado em
`CPT_Podcast_Handler::register_meta()`):
| Meta key | Tipo | Nota |
|---|---|---|
| `episode_type` | `audio`\|`video` | |
| `audio_file` | URL | ficheiro (ou stream) do episódio |
| `cover_image` / `cover_image_id` | URL / attachment ID | imagem quadrada, `cover_image_id` é interno (não escrever directamente) |
| `duration` | texto | ex. `00:32:14`, calculado automaticamente se possível, mas aceita override manual |
| `filesize` / `filesize_raw` | texto / bytes | `filesize_raw` é interno/calculado, usado no RSS — não escrever directamente |
| `date_recorded` | `Y-m-d` | distinto de `post_date` (data de publicação) |
| `explicit` / `block` | checkbox (`'on'`\|`''`) | `block` esconde de directórios iTunes/Google |
| `itunes_episode_number` / `itunes_season_number` | número | tags iTunes WWDC 2017 |
| `itunes_title` | texto | título específico para o feed iTunes (sem número de episódio/show) |
| `itunes_episode_type` | `full`\|`trailer`\|`bonus` | |
| `podmotor_file_id` / `podmotor_episode_id` / `castos_file_data` / `sync_status` | — | só-Castos, sempre vazios neste site |
### 1.2 Shows — taxonomia `series` (função `ssp_series_taxonomy()`, devolve `'series'`)
Termo hierárquico, `show_in_rest => true`, `rest_base => 'series'`. Um episódio pode pertencer a
0-N shows (`wp_set_post_terms`). Tags normais (`post_tag`) também estão ligadas ao CPT por
omissão (filtro `ssp_use_post_tags`, `true` neste site) — existe também uma taxonomia alternativa
`podcast_tags`, mas só é registada se `ssp_use_post_tags` for desligado (não é o caso aqui).
### 1.3 Definições de show — `wp_options`, NUNCA numa tabela/post
Cada show tem um bloco de opções `ss_podcasting_data_{campo}_{series_id}` (título, subtítulo,
descrição, autor, imagem, owner_name/email, idioma, copyright, category/subcategory 1-3) **mais**
duas excepções sem o infixo `data_`: `ss_podcasting_explicit_{series_id}` e
`ss_podcasting_complete_{series_id}` (gotcha — ver §6). Confirmado por SQL directo em
`emanuelalmeida.pt`: 18 opções distintas escritas para o show "Afirmações" (id 120).
Existe também um conjunto **sem sufixo `_{series_id}`** — os defaults globais do site (usados
quando um show não tem override próprio), lidos em
`Rest_Api_Controller::get_default_podcast_settings()`.
---
## 2. REST API — o que é escrita-capaz vs read-only (achado central)
| Endpoint | Leitura | Escrita |
|---|---|---|
| `GET/POST/PUT /wp/v2/podcast` | ✅ episódios completos + todos os meta fields do §1.1 | ✅ (CPT standard, `show_in_rest`, capacidades `edit_podcasts` mapeadas para editor/administrator) |
| `GET/POST/PUT /wp/v2/series` | ✅ termos + **campos extra injectados** (title/subtitle/author/owner_name/owner_email/language/copyright/image/category1-3/explicit_option/complete_option) | ❌ os campos extra são só-leitura — `register_rest_field()` em `Rest_Api_Controller::create_api_series_fields()` só regista `get_callback`, **sem `update_callback`**. Confirmado lendo o código-fonte; um `PUT` no termo não altera nenhuma destas definições. |
| `GET /ssp/v1/episodes` | ✅ query cross-post-type (namespace próprio) | endpoint dedicado só para `podmotor_episode_id`/`audio_file` (uso interno Castos) |
**Implicação prática:** episódios podem ser geridos 100% via REST nativo (Application Password),
mas **definições de show só podem ser escritas via `wp_options` directamente** (WP-CLI `wp option
update` ou, como o MCP faz, `update_option()` dentro de um `wp eval`). É por isto que
`mcp-seriously-simple-podcasting` usa o bootstrap WordPress completo (`wp eval`) para tudo, em vez
de misturar REST para episódios e SQL para shows — um único mecanismo, sempre consistente.
---
## 3. wp-admin — páginas principais
| Página | Conteúdo |
|---|---|
| **Podcasting → All Episodes** | lista de episódios (CPT `podcast`), com metabox de todos os campos do §1.1 |
| **Podcasting → Add New Episode** | criação, campo de show (taxonomia `series`) na sidebar |
| **Podcasting → Settings → General** | defaults globais do site |
| **Podcasting → Settings → Feed Details** | por show (selector "feed-series" no topo) — título/subtítulo/owner/categorias/idioma/copyright/imagem — é aqui que se editam manualmente as opções do §1.3 |
| **Podcasting → Settings → Hosting** | ligação Castos (desligada neste site) |
| **Podcasting → Settings → Import** | importar de outro feed RSS |
---
## 4. Categorias iTunes/Apple Podcasts
19 categorias de topo (`Arts`, `Business`, `Comedy`, `Education`, `Fiction`, `Government`,
`History`, `Health & Fitness`, `Kids & Family`, `Leisure`, `Music`, `News`,
`Religion & Spirituality`, `Science`, `Society & Culture`, `Sports`, `Technology`, `True Crime`,
`TV & Film`), a maioria com subcategorias próprias (`Government`/`History`/`Technology`/
`True Crime` não têm) — é a taxonomia oficial Apple Podcasts, lida de
`php/config/settings/feed-categories.php` e `feed-subcategories.php`. Cada show suporta até 3
pares categoria/subcategoria (`category`/`subcategory`, `category2`/`subcategory2`,
`category3`/`subcategory3`). Lista completa disponível na tool `ssp_get_itunes_categories` do MCP
(sem chamada SSH — é referência estática).
---
## 5. Feed RSS
URL construído a partir de `home_url()` + `feed/podcast` (ou `?feed=podcast` sem permalinks
bonitos) + `/{slug-do-show}` para o feed de um show específico. Cada show tem o seu próprio feed
filtrado (`Episode_Repository::get_feed_url()`); o feed "geral" (sem `series_id`) inclui episódios
de todos os shows.
---
## 6. Estado real em emanuelalmeida.pt (verificado 19-08-2026)
- **39 episódios**, todos `publish`, distribuídos por **8 shows**: Meditação (11), DDE Desafio
Detox Emocional (10), Afirmações (8), Reprogramação mental (6), Motivacionais (3), + 3 shows
menores/vazios (incl. "Emanuel Almeida", 0 episódios).
`audio_file` sempre aponta para o CDN legado `episodes.castos.com` (ligação Castos desligada,
ficheiros continuam a servir de lá).
- **Nenhum episódio marcado `explicit`** (único valor visto na coluna: `''`).
- Plugins activos relevantes no mesmo site: Rank Math SEO PRO, Wordfence, WP Meteor, WP Fastest
Cache, `seguranca-descomplicar` — nenhum interfere com REST/feeds do SSP nesta verificação
(`curl` a `/wp-json/wp/v2/podcast` e `/feed/podcast/...` devolveram 200 sem bloqueio).
---
## 7. Gotchas
| Sintoma | Causa | Nota |
|---|---|---|
| `wp/v2/series` devolve `explicit_option`/`complete_option` sempre vazios ou iguais ao default global, mesmo com override por show definido | Bug do próprio plugin: `series_get_field_value()` faz fallback genérico para `ss_podcasting_data_{campo}_{series_id}`, mas `explicit`/`complete` são gravados em `ss_podcasting_{campo}_{series_id}` (sem `data_`) — o REST nunca encontra o override | Ler via `ssp_get_series_settings` do MCP (lê directamente com o prefixo correcto) em vez de confiar no REST nativo para estes dois campos |
| `PUT /wp/v2/series/{id}` com `title`/`owner_email`/etc no payload não faz nada | Campos são `get_callback`-only (§2) | Escrever via `ssp_update_series_settings` do MCP (`update_option`) |
| `ssp_delete_series`/qualquer verificação de "show tem episódios" baseada em `term->count` dá 0 mesmo com episódios associados | `term_taxonomy.count` do WordPress só conta posts **publicados** — episódios em `draft`/`pending`/`future` não entram na contagem | Não assumir "count=0 → seguro apagar"; confirmar sempre com `ssp_list_episodes` incluindo `status=any` antes de remover um show |
| Nenhum registo de transcrições/capítulos encontrado no código | Este site usa a versão Free sem addon; não há módulo de transcrição na árvore lida (`grep -i transcript` só encontra uma referência de string solta num metabox, sem implementação funcional associada) | Não assumir suporte — verificar de novo se o plugin for actualizado/o addon Castos for religado |
---
## Fonte
Leitura completa do código-fonte `seriously-simple-podcasting` 3.17.0 via SSH em
emanuelalmeida.pt (`php/classes/handlers/class-cpt-podcast-handler.php`,
`class-series-handler.php`, `php/classes/repositories/class-episode-repository.php`,
`php/classes/rest/class-rest-api-controller.php`, `class-episodes-rest-controller.php`,
`php/classes/controllers/class-settings-controller.php`, `php/config/settings/*.php`,
`php/includes/ssp-functions.php`), cruzada com `SHOW TABLES`/`DESCRIBE`/agregações reais via SQL e
chamadas REST reais (`curl`), 19-08-2026 — sessão de construção do MCP
`mcp-seriously-simple-podcasting`. Ver essa skill para a lista completa de 14 tools programáticas.
+701
View File
@@ -0,0 +1,701 @@
---
name: webp-express
description: Gestão do plugin WebP Express (conversão automática de imagens para WebP) via WP-CLI e ficheiro de config em servidores CWP. Cobre localizar o ficheiro de config com hash variável, modos de operação (CDN friendly, Varied image responses, No conversion, Tweaked), reconversão em massa via `wp webp-express convert`, flush de webps, conversores disponíveis (imagemagick/imagick/gd vs cwebp/vips/ffmpeg/graphicsmagick/wpc/ewww) e as respectivas opções próprias, estrutura JSON completa do ficheiro de configuração (qualidade/encoding/near-lossless/alpha-quality por tipo de imagem, cache-control, alter HTML, redirection rules, web service), regras `.htaccess`, a fronteira com o Cloudflare, e o MCP `webpx` construído para gestão programática multi-site (§14). Usar quando "webp express", "converter imagens webp", "webp nao funciona", "reconverter imagens", "flush webp", "conversor imagemagick gd", "operation mode webp", "cdn friendly webp", "webp quality", "near lossless", "alter html webp", "cache control webp", "web service webp express", "mcp webp express", "webpx".
---
# /webp-express — Conversão de imagens para WebP via WP-CLI
Gestão programática do plugin WebP Express (`webp-express`) em sites WordPress
da frota, via WP-CLI no servidor CWP (`server.descomplicar.pt`, SSH porta
9443).
**Fonte:** `CONFIG-Plugins-Referencia.md` §5 (WebP Express) + verificação ao
vivo em `emanuelalmeida.pt` (versão `0.25.15`) + leitura completa do
código-fonte nesta sessão: `lib/classes/CLI.php`, `lib/classes/CachePurge.php`,
`lib/classes/BulkConvert.php`, e **todos** os ficheiros `.inc`/`.php` de
`lib/options/options/` (general, conversion-options incl.
`converter-options/*.php` por conversor, redirection-rules, serve-options,
alter-html, web-service-options, operation-mode.inc) — 43 ficheiros no total.
Config JSON completo lido ao vivo do disco (não só campos vistos na UI).
---
## Contexto CWP — sempre obrigatório
```bash
PATH=/home/USER/public_html
sudo -u USER /usr/local/bin/wp <comando> --path=$PATH
```
**Acesso via SSH:** `ssh -p 9443 root@server.descomplicar.pt` (alias `server`
já configurado localmente).
---
## 1. Onde vive a configuração — NÃO é `wp_options` normal
A config principal do WebP Express vive num **ficheiro JSON com nome
imprevisível** (hash no nome, muda por instalação/reinstalação):
```
wp-content/webp-express/config/config.<hash>.json
```
**Nunca assumir o hash.** Duas formas de o descobrir, por ordem de
preferência:
```bash
# 1. Forma rápida — o hash está guardado numa option (sem precisar de find)
wp option get webp-express-config-hash --path=$PATH
# 2. Confirmar/descobrir via find quando a option não existe ou for preciso
# validar o caminho real no disco
find $PATH/wp-content/webp-express/config -iname 'config.*.json'
```
O ficheiro em si:
```bash
HASH=$(wp option get webp-express-config-hash --path=$PATH)
cat $PATH/wp-content/webp-express/config/config.$HASH.json | python3 -m json.tool
```
### Estado operacional (`wp_options`, separado da config)
Ao contrário da config, o **estado** vive mesmo em `wp_options` normais,
todas com o prefixo `webp-express-`:
```bash
wp option get webp-express-state --format=json --path=$PATH
wp option get webp-express-migration-version --path=$PATH
wp option get webp-express-config-migrated-cve-2025-11379 --path=$PATH
wp option get webp-express-alter-html-options --format=json --path=$PATH
```
`webp-express-state` traz o resumo mais útil de uma só vez:
`workingConverterIds`, `workingAndActiveConverterIds`, `configured`,
`htaccess-rules-saved-at-some-point`, `active-htaccess-dirs`.
### ⚠️ Gotcha: `convert_webp_status` e `webp_offset_*` NÃO são do WebP Express
Numa auditoria com `wp option list --search='*webp*'` aparecem também
`convert_webp_status` e vários `webp_offset_*` (`webp_offset_thumbnail`,
`webp_offset_large`, etc.). **São opções órfãs de outro plugin** (não
presente na lista activa de plugins do site) — não pertencem ao WebP
Express e não devem ser lidas/alteradas ao auditar este plugin. Confirmar
sempre com `wp plugin list --format=json | grep -i webp` que só
`webp-express` está activo antes de interpretar options com "webp" no nome.
---
## 2. Modos de operação (`operation-mode`)
Fonte: `lib/options/options/operation-mode.inc`. Quatro valores possíveis:
| Valor | Nome na UI | Comportamento |
|---|---|---|
| `varied-image-responses` | Varied image responses | Redirecciona jpeg/png para webp via `.htaccess`, mas só quando o browser envia `Accept: image/webp`. A mesma URL responde de forma diferente consoante o cliente — **problemático para CDNs que não fazem `Vary` no header `Accept`** |
| `cdn-friendly` | CDN friendly | jpeg é **sempre** servido como jpeg; em vez de variar a resposta, o WebP Express reescreve o HTML (`<picture>`/`<img srcset>`) para apontar directamente ao ficheiro `.webp` estático. Melhor HIT ratio em CDN, sem exigir forward do header `Accept` |
| `no-conversion` | No conversion | Não converte nada — usado quando outro plugin já faz a conversão e só se quer aproveitar a reescrita de HTML/redirecção do WebP Express |
| `tweaked` | Tweaked | Sem preset — todas as opções ficam editáveis individualmente (mostra secções `redirection-rules` e `serve-options` completas, que ficam escondidas nos outros modos); ao voltar a um preset perdem-se as afinações |
**Verificado em `emanuelalmeida.pt`:** `operation-mode: "cdn-friendly"` —
escolha correcta dado o site estar atrás do Cloudflare (evita depender de o
Cloudflare fazer `Vary: Accept` correctamente, que nem sempre acontece nos
planos gratuitos).
```bash
HASH=$(wp option get webp-express-config-hash --path=$PATH)
wp eval "echo json_decode(file_get_contents('wp-content/webp-express/config/config.$HASH.json'))->{'operation-mode'};" --path=$PATH
```
Para mudar de modo é preciso editar o ficheiro JSON directamente (não há
comando `wp option patch` — não é uma option serializada) e depois correr
`wp webp-express convert --reconvert` para regenerar tudo com as novas
definições. **Mutação em produção — nunca fazer sem autorização.**
**Nota de UI condicional por modo** (relevante ao ler o código de
`lib/options/options/general/general.inc`): as secções `scope.inc`,
`destination-folder.inc` e `destination-structure.inc` só aparecem quando
`operation-mode != 'no-conversion'`; `cache-control.inc` só aparece em
`tweaked`, `varied-image-responses` ou `cdn-friendly`. O JSON de config
mantém sempre todos os campos gravados, independentemente do que a UI
mostra para o modo actual.
---
## 3. Reconverter imagens em massa — comando nativo `wp webp-express`
O plugin regista um comando WP-CLI próprio (`lib/classes/CLI.php`), **não é
preciso passar por `wp eval`**. Fonte lida na íntegra nesta sessão — o
plugin só regista **dois** subcomandos, não há mais:
```bash
wp help webp-express --path=$PATH
```
```
NAME
wp webp-express
SUBCOMMANDS
convert Convert images to webp
flushwebp Flush webps
```
### `wp webp-express convert`
```bash
# Ver o que falta converter, sem converter (dry preview por pasta)
wp webp-express convert --path=$PATH
# Converter tudo o que ainda não está convertido (media library + tema)
wp webp-express convert uploads --path=$PATH
wp webp-express convert themes --path=$PATH
# Forçar RECONVERSÃO de tudo (substitui webps já existentes)
wp webp-express convert uploads --reconvert --path=$PATH
# Limitar a uma subpasta
wp webp-express convert uploads/2026 --path=$PATH
# Só PNG / só JPEG
wp webp-express convert uploads --only-png --path=$PATH
wp webp-express convert uploads --only-jpeg --path=$PATH
# Forçar um conversor específico (ignora o stack de fallback)
wp webp-express convert uploads --converter=gd --path=$PATH
# Sobrepor qualidade/encoding só nesta corrida (não altera a config gravada)
wp webp-express convert uploads --quality=75 --encoding=lossy --path=$PATH
# Sobrepor near-lossless e alpha-quality só nesta corrida
wp webp-express convert uploads --near-lossless=60 --path=$PATH
wp webp-express convert uploads --alpha-quality=80 --path=$PATH
```
`<location>` aceita `uploads`, `themes`, `plugins`, `wp-content` ou `index`
como "raiz de imagem", opcionalmente seguido de `/subpasta`. Sem argumento,
lista quantos ficheiros por converter há em cada grupo, sem converter nada.
**Flags completas do `convert`** (confirmadas em `CLI.php`, docblock
`## OPTIONS`):
| Flag | Efeito | Aplica-se a `max-quality` E `png-quality` |
|---|---|---|
| `[<location>]` | Restringe a uma raiz de imagem (`uploads`\|`themes`\|`plugins`\|`wp-content`\|`index`), opc. `/subpasta` | — |
| `--reconvert` | Reconverte mesmo o que já está convertido | — |
| `--only-png` | Só PNG (`image-types = 2`) | — |
| `--only-jpeg` | Só JPEG (`image-types = 1`) | — |
| `--quality=<n>` | Sobrepõe qualidade (0-100) | Sim — aplica ao mesmo valor `max-quality` e `png-quality` |
| `--near-lossless=<n>` | Sobrepõe near-lossless (0-100) | Sim — aplica a `png-near-lossless` e `jpeg-near-lossless` |
| `--alpha-quality=<n>` | Sobrepõe alpha-quality (0-100) | Só `alpha-quality` (PNG com transparência) |
| `--encoding=<auto\|lossy\|lossless>` | Sobrepõe encoding | Sim — aplica a `png-encoding` e `jpeg-encoding` |
| `--converter=<id>` | Força um conversor específico, ignora o stack. Valores válidos: `cwebp \| vips \| ewww \| imagemagick \| imagick \| gmagick \| graphicsmagick \| ffmpeg \| gd \| wpc` | — |
Nenhuma destas flags altera a config gravada em disco — são só para essa
corrida.
### `wp webp-express flushwebp` — limpar webps gerados
```bash
# Apaga TODOS os .webp gerados (força reconversão no próximo acesso/convert)
wp webp-express flushwebp --path=$PATH
# Só os que vieram de PNG
wp webp-express flushwebp --only-png --path=$PATH
```
Internamente chama `CachePurge::purge()` (`lib/classes/CachePurge.php`),
que apaga recursivamente `.webp` em três locais: o cache dir
(`Paths::getCacheDirAbs()`), o upload dir (só se
`destination-folder == 'mingled'`) e o dir de "bigger than source dummy
files". Nunca apaga o original de um attachment registado na media library
(`MediaLibraryHelper::isRegisteredAttachmentOrThumbnail`) — protecção
interna contra apagar acidentalmente ficheiros errados quando modo
`mingled` mistura originais e webps na mesma pasta.
Útil depois de mudar `quality`/`encoding`/conversor na config, antes de
correr `convert` de novo — garante que não ficam webps antigos com as
definições anteriores misturados com os novos.
**Alternativa via wp-admin (sem SSH):** Settings → WebP Express → aba
"Bulk convert" tem os mesmos botões ("Bulk Convert" → popup de conversão em
massa via AJAX; "Delete converted files" → popup que chama
`processAjaxPurgeCache()`, o mesmo `CachePurge::purge()` do `flushwebp`).
Há ainda um botão "Delete log files" na secção "Enable logging" (ver §7).
O WP-CLI é preferível em produção por ser scriptável e não depender de
timeout do browser em sites com muitas imagens. **Nota:** a conversão em
massa via UI usa sempre as **últimas definições gravadas** (comentário no
código-fonte de `bulk-convert.inc`: *"the bulk conversion is using the
last saved settings"*) — se acabaste de mudar qualidade/encoding no forms
mas ainda não gravaste (Save), o bulk convert usa a versão antiga.
---
## 4. Conversores — stack de fallback e opções por conversor
`webp-express` tenta uma lista ordenada de conversores até um funcionar; cada
entrada tem `working: true/false` gravado em `webp-express-state` (testado
no arranque, não em cada conversão):
| Conversor | Estado em `emanuelalmeida.pt` | Nota |
|---|---|---|
| `imagemagick` (binário CLI) | ✅ `working: true`¹ | Fallback preferido |
| `imagick` (extensão PHP) | ✅ `working: true`¹ | |
| `gd` (extensão PHP, sempre disponível) | ✅ `working: true` | Último recurso, sempre presente em qualquer instalação PHP |
| `cwebp` (binário Google) | ❌ `working: false` | Não instalado no servidor CWP |
| `vips` | ❌ `working: false` | Não instalado |
| `graphicsmagick`/`gmagick` | ❌ `working: false` | Não instalado |
| `ffmpeg` | ❌ `working: false` | Não instalado |
| `wpc` (WebP Cloud / Remote WebP Express, API paga ou remota) | ❌ `working: false` | Não configurado (sem API key/URL) |
| `ewww` (requer plugin EWWW Image Optimizer) | ❌ `working: false` | Plugin não instalado neste site |
¹ Nota lida directamente no JSON de config nesta sessão: no bloco
`converters[]` do config actual, `imagemagick` e `graphicsmagick` aparecem
com `"working": false` (o binário de sistema não responde), enquanto só
`gd` tem `"working": true`. O `webp-express-state` (cache separada,
recalculada a cada teste de auto-detecção) pode divergir ligeiramente do
snapshot gravado no `converters[]` do JSON — **confirmar sempre pelo
`webp-express-state` para saber o estado corrente**, o array `converters`
do config json é sobretudo a lista de opções por conversor + ordem.
**Sem impacto operacional** — `gd` cobre 100% dos casos como último
recurso; os conversores em falta são apenas alternativas mais
rápidas/melhor compressão que não compensam instalar a nível de servidor
só para este plugin. Verificar sempre `workingConverterIds` antes de forçar
`--converter=X` num `wp webp-express convert` — se `X` não estiver na
lista, o comando falha.
```bash
wp option get webp-express-state --format=json --path=$PATH | python3 -c "import json,sys; print(json.load(sys.stdin)['workingConverterIds'])"
```
### Opções específicas por conversor (bloco `converters[].options` no JSON)
Fonte: `lib/options/options/conversion-options/converter-options/*.php` (um
ficheiro por conversor) + o array `converters` do config JSON ao vivo.
| Conversor | Opções (`options{}`) | Descrição |
|---|---|---|
| `cwebp` | `use-nice` (bool), `try-common-system-paths` (bool), `try-supplied-binary-for-os` (bool), `method` (0-6), `low-memory`/"set-size"+`size-in-percentage`, `command-line-options` (string livre passada ao binário) | `method`: trade-off velocidade/qualidade (0=rápido, 6=melhor). "Size" força tamanho-alvo em % do original em vez de qualidade (~2.5x mais lento). `command-line-options` aceita qualquer flag do `cwebp` real, ex. `-low_memory -af -f 50 -sharpness 0 -mt` |
| `vips` | `smart-subsample` (bool), `preset` (`none`\|`default`\|`photo`\|`picture`\|`drawing`\|`icon`\|`text`) | Preset ajusta várias opções internas consoante o tipo de conteúdo (não sobrepõe qualidade) |
| `imagemagick` | `use-nice` (bool) | Executa o binário `convert`; sem outras opções específicas |
| `graphicsmagick` (`gmagick`) | `use-nice` (bool) | Executa `gm convert`; sem outras opções específicas |
| `ffmpeg` | `use-nice` (bool), `method` (0-6) | Idêntico ao `imagemagick` mas via ffmpeg |
| `gd` | `skip-pngs` (bool) | Se `true`, PNGs passam para o próximo conversor da stack (Gd historicamente pior a comprimir PNG e teve bugs de transparência, hoje resolvidos) |
| `imagick` | — (sem opções próprias) | "imagick has no special options" no código |
| `ewww` | `api-key`, `api-key-2` (fallback) | Serviço cloud pago (ewww.io); fee único, sem custo por conversão webp |
| `wpc` (Remote WebP Express / WPC) | `api-version` (0\|1), `api-url`, `secret`/`api-key`, `crypt-api-key-in-transfer` (bool) | Delega a conversão a outra instalação WebP Express ou a um serviço `webp-convert-cloud-service`. Api 0 é legado (v0.1); usar 1 para instâncias WebP Express normais |
`use-nice`: presente em quase todos os conversores baseados em binário —
executa com prioridade reduzida (`nice`) para poupar recursos à custa de
conversão ligeiramente mais lenta.
`Use nice`, `try-common-system-paths`, `try-supplied-binary-for-os`: o
plugin traz binários `cwebp` pré-compilados para várias plataformas dentro
do próprio plugin (`try-supplied-binary-for-os`) — útil quando o binário de
sistema não existe mas o hosting permite `exec()`.
---
## 5. Qualidade, encoding, near-lossless e metadata (`conversion-options`)
Fonte: `lib/options/options/conversion-options/{quality,jpeg,png,metadata}.inc`
+ valores ao vivo do JSON de `emanuelalmeida.pt`.
### 5.1. JPEG → WebP
| Campo JSON | Valor ao vivo | Descrição |
|---|---|---|
| `jpeg-encoding` | `"auto"` | `lossy` (sempre lossy) ou `auto` (o plugin experimenta lossy e lossless e fica com o mais pequeno). `Gd`/`Ewww` não suportam `auto` |
| `quality-auto` | `true` | Se `true`, tenta detectar a qualidade original do jpeg e usar a mesma no webp (requer imagick/imagemagick/gmagick capazes de detectar qualidade); se `false`, usa `quality-specific` fixo |
| `max-quality` | `80` | Tecto para `quality-auto`: mesmo que o jpeg original tenha qualidade mais alta, nunca ultrapassa este valor. Recomendado 50-85 |
| `quality-specific` | `70` | Qualidade fixa usada quando `quality-auto=false` OU como fallback quando a detecção de qualidade falhar |
| `jpeg-enable-near-lossless` | `true` | Aplica-se quando o encoding resultante é lossless: liga o pré-processamento "near-lossless" em vez de 100% lossless puro. Só suportado por `cwebp` e `vips` |
| `jpeg-near-lossless` | `60` | Nível 0 (máximo pré-processamento) a 100 (sem pré-processamento, = lossless puro) |
### 5.2. PNG → WebP
| Campo JSON | Valor ao vivo | Descrição |
|---|---|---|
| `png-encoding` | `"auto"` | `lossless` (default para PNG) ou `auto` (experimenta lossy e lossless, fica com o mais pequeno — não existe opção "sempre lossy" isolada para PNG na UI) |
| `png-quality` | `85` | Qualidade quando o resultado é lossy (via `auto`). Recomendado 60-90, tipicamente mais alto que jpeg pois PNG é usado para ícones/gráficos |
| `alpha-quality` | `80` | Qualidade específica do canal alfa (transparência), só relevante em encoding lossy com transparência. `Gd`/`Ewww` ignoram esta opção e mantêm o canal alfa sempre lossless |
| `png-enable-near-lossless` | `true` | Idêntico ao `jpeg-enable-near-lossless`, mas para PNG |
| `png-near-lossless` | `60` | Idêntico ao `jpeg-near-lossless` |
### 5.3. Metadata
| Campo JSON | Valor ao vivo | Descrição |
|---|---|---|
| `metadata` | `"none"` | `none` (não copia EXIF/etc. para o webp — mais pequeno) ou `all` (copia toda a metadata). **`Gd` não suporta cópia de metadata** — ignora esta opção sempre que `Gd` é o conversor efectivo |
### 5.4. Outras opções gerais de conversão
| Campo JSON | Valor ao vivo | Descrição |
|---|---|---|
| `convert-on-upload` | `true` | Converte a imagem (incl. todos os thumbnails gerados) no momento do upload para a media library. Hooka em `handle_upload` e `image_make_intermediate_size`. Pode abrandar o upload em temas com muitos tamanhos de thumbnail |
| `image-types` | `3` | Bitmask: `1`=jpeg, `2`=png, `3`=ambos, `0`=desactivado. Controla que tipos são convertidos/processados |
| `prevent-using-webps-larger-than-original` | `true` | Webps maiores que o original ficam sempre em disco mas não são usados (nem em `.htaccess` nem em Alter HTML) se esta opção estiver activa. Desde 0.25.5: em Alter HTML com `picture` + `srcset`, só omite o `<source webp>` se **todos** os webps do srcset forem maiores — considerar desactivar se muitas imagens têm srcset |
| `enable-logging` | `false` | Grava resultado de cada conversão em `wp-content/webp-express/log/conversions/`. Tem botão dedicado "Delete log files" na UI (popup `purgelogpopup`) |
---
## 6. Estrutura completa do ficheiro de configuração (JSON)
Volcado ao vivo de `emanuelalmeida.pt`
(`wp-content/webp-express/config/config.<hash>.json`) — esta é a **lista
completa de todas as chaves de topo** que o plugin grava, incluindo as que
não aparecem directamente numa única aba da UI:
```jsonc
{
// --- Modo e âmbito ---
"operation-mode": "cdn-friendly", // varied-image-responses | cdn-friendly | no-conversion | tweaked
"scope": ["uploads", "themes"], // subset de: uploads, themes, plugins, wp-content, index
"image-types": 3, // bitmask: 1=jpeg 2=png 3=ambos 0=nenhum
// --- Destino dos ficheiros webp ---
"destination-folder": "separate", // separate | mingled
"destination-extension": "append", // append (image.jpg.webp) | set (image.webp)
"destination-structure": "image-roots", // doc-root | image-roots
// --- Cache-Control header ---
"cache-control": "no-header", // no-header | set | custom
"cache-control-custom": "public, max-age=31536000, stale-while-revalidate=604800, stale-if-error=604800",
"cache-control-max-age": "one-week", // one-second|one-minute|one-hour|one-day|one-week|one-month|one-year
"cache-control-public": false, // true=public, false=private (só usado quando cache-control=set)
// --- Robustez ---
"prevent-using-webps-larger-than-original": true,
"enable-logging": false,
// --- Redirection rules (.htaccess) ---
"enable-redirection-to-converter": false, // só editável em modo "tweaked"/"varied-image-responses"
"only-redirect-to-converter-on-cache-miss": false,
"only-redirect-to-converter-for-webp-enabled-browsers": true,
"do-not-pass-source-in-query-string": true,
"redirect-to-existing-in-htaccess": false,
"forward-query-string": true,
"enable-redirection-to-webp-realizer": true, // "criar webp sob pedido" (chave em cdn-friendly)
// --- Qualidade/encoding jpeg ---
"jpeg-encoding": "auto", // lossy | auto
"jpeg-enable-near-lossless": true,
"jpeg-near-lossless": 60,
"quality-auto": true,
"max-quality": 80,
"quality-specific": 70,
// --- Qualidade/encoding png ---
"png-encoding": "auto", // lossless | auto
"png-enable-near-lossless": true,
"png-near-lossless": 60,
"png-quality": 85,
"alpha-quality": 80,
// --- Stack de conversores, ordem = prioridade ---
"converters": [
{ "converter": "cwebp", "options": { "use-nice": true, "try-common-system-paths": true, "try-supplied-binary-for-os": true, "method": 6, "low-memory": true, "command-line-options": "" }, "working": false },
{ "converter": "vips", "options": { "smart-subsample": false, "preset": "none" }, "working": false },
{ "converter": "imagemagick", "options": { "use-nice": true }, "working": false },
{ "converter": "graphicsmagick", "options": { "use-nice": true }, "working": false },
{ "converter": "ffmpeg", "options": { "use-nice": true, "method": 4 }, "working": false },
{ "converter": "wpc", "working": false, "options": { "api-key": "" } },
{ "converter": "ewww", "working": false },
{ "converter": "imagick", "working": false },
{ "converter": "gmagick", "working": false },
{ "converter": "gd", "options": { "skip-pngs": false }, "working": true }
],
// --- Metadata / upload automático ---
"metadata": "none", // none | all
"convert-on-upload": true,
// --- Serve options (resposta ao browser) ---
"fail": "original", // original | 404 | report
"success-response": "original", // original | converted
// --- Alter HTML ---
"alter-html": {
"enabled": true,
"replacement": "picture", // picture | url
"hooks": "ob", // content-hooks | ob (output buffering completo da página)
"only-for-webp-enabled-browsers": false,
"only-for-webps-that-exists": false,
"alter-html-add-picturefill-js": true,
"hostname-aliases": [] // hostnames extra (CDN) cujas URLs também devem ser reescritas
},
// --- Web service (partilhar conversão com outros sites) ---
"web-service": {
"enabled": false,
"whitelist": [] // [{ip, label, api-key, crypt-in-transfer}, ...]
},
// --- Snapshot de ambiente à data do save (diagnóstico, não editável) ---
"environment-when-config-was-saved": { "...": "..." },
"base-htaccess-on-these-capability-tests": { "...": "..." },
"document-root": "/home/USER/site",
"paths-used-in-htaccess": { "wod-url-path": "wp-content/plugins/webp-express/wod/webp-on-demand.php" }
}
```
### 6.1. Cache-Control header — detalhe
Fonte: `lib/options/options/general/cache-control.inc`.
| `cache-control` | Comportamento |
|---|---|
| `no-header` | Não define header — deixa o servidor/CDN decidir |
| `set` | Usa `cache-control-public` (public/private) + `cache-control-max-age` (preset de duração) para montar o header automaticamente |
| `custom` | Usa literalmente a string em `cache-control-custom` — **única forma de definir `stale-while-revalidate`/`stale-if-error`**, não expostos nos presets |
O texto de ajuda do próprio plugin recomenda `custom` com
`stale-while-revalidate`/`stale-if-error` para melhor resiliência em falhas
momentâneas do servidor de origem.
### 6.2. Scope — mapeamento de valores
Fonte: `lib/options/options/general/scope.inc`. O array `scope[]` é
qualquer subconjunto ordenado alfabeticamente de `uploads`, `themes`,
`plugins`, `wp-content`, `index`; a UI mapeia combinações comuns para
nomes amigáveis:
| `scope` (array) | Nome na UI |
|---|---|
| `["uploads"]` | Uploads only |
| `["themes"]` | Themes only |
| `["themes", "uploads"]` | Uploads and themes |
| `["plugins", "themes", "uploads", "wp-content"]` | All content |
| `["index", "plugins", "themes", "uploads", "wp-content"]` | Everything (including wp-admin) |
| Qualquer outra combinação | Mostrado como "Custom: …" |
**Verificado em `emanuelalmeida.pt`:** `["uploads", "themes"]` → "Uploads
and themes" (plugins/wp-content fora de âmbito).
### 6.3. Destination folder / extension / structure
| Opção | Valores | Efeito |
|---|---|---|
| `destination-folder` | `separate` (pasta dedicada `wp-content/webp-express/webp-images/`) \| `mingled` (webp ao lado do original, só dentro de `uploads`) | `mingled` é recomendado ao combinar com plugins de cache de página que sabem servir por extensão (ex. Cache Enabler), ou com Shortpixel |
| `destination-extension` | `append` (`imagem.jpg.webp`) \| `set` (`imagem.webp`) | `set` conflitua se existir `logo.jpg` E `logo.png` na mesma pasta (colidem no mesmo `logo.webp`) |
| `destination-structure` | `doc-root` (espelha caminho a partir da doc root) \| `image-roots` (espelha caminho a partir da raiz de imagem — uploads/themes/etc) | Recomendado `image-roots` em hosts com doc root mal configurada; `doc-root` reduz regras de rewrite necessárias em Nginx |
**Verificado em `emanuelalmeida.pt`:** `destination-folder: "separate"`,
`destination-extension: "append"`, `destination-structure: "image-roots"`.
---
## 7. Redirection rules (`.htaccess`) — detalhe por opção
Fonte: `lib/options/options/redirection-rules/*.inc`. Estas opções só são
todas editáveis em modo `tweaked`; nos outros modos, o preset do modo
decide-as automaticamente (mas o valor gravado no JSON reflecte sempre o
que está realmente activo em `.htaccess`).
| Campo JSON | Descrição |
|---|---|
| `redirect-to-existing-in-htaccess` | Redirect interno directo para o `.webp` já existente, sem passar por PHP — mais rápido. Regra fica **acima** da regra de conversão |
| `enable-redirection-to-converter` | Redirige jpeg/png (não convertidos) para `webp-on-demand.php`, que converte, grava e serve. Regra fica **abaixo** da anterior (só actua se ainda não existir webp) |
| `enable-redirection-to-webp-realizer` | "Criar webp files upon request": redirige pedidos por um `.webp` que **ainda não existe** para `webp-realizer.php`, que procura o jpg/png correspondente, converte e serve — permite referenciar webps no HTML antes de existirem fisicamente. É a opção chave em modo `cdn-friendly` |
| `only-redirect-to-converter-for-webp-enabled-browsers` | Só actua a regra do `enable-redirection-to-converter` quando o `Accept` header contém `image/webp` |
| `only-redirect-to-converter-on-cache-miss` | Condição extra equivalente à funcionalidade usada por `enable-redirection-to-webp-realizer`, mas aplicada ao `enable-redirection-to-converter` — desnecessária se `redirect-to-existing-in-htaccess` já estiver activo |
| `do-not-pass-source-in-query-string` | Se `true` (default recomendado), passa o path da imagem por env var em vez de query string, para não disparar firewalls que bloqueiam certas query strings |
| `forward-query-string` | Encaminha a query string original do pedido para o script de conversão |
---
## 8. Serve options (resposta ao browser após conversão)
Fonte: `lib/options/options/serve-options/*.inc` — só visível na UI em modo
`tweaked`.
| Campo JSON | Valores | Descrição |
|---|---|---|
| `fail` | `original` (serve o jpg/png original) \| `404` \| `report` (relatório de erro em texto simples) | Recomendado `original` em produção |
| `success-response` | `original` \| `converted` | `converted` envia header `Vary: Accept` (indica que a resposta depende do Accept). Usar `original` se combinado com plugins de cache de página que não sabem variar por Accept (ex. Cache Enabler sem suporte dual-cache) |
**Verificado em `emanuelalmeida.pt`:** `fail: "original"`,
`success-response: "original"`.
---
## 9. Alter HTML — detalhe completo
Fonte: `lib/options/options/alter-html/*.inc`.
| Campo JSON (`alter-html.*`) | Valores | Descrição |
|---|---|---|
| `enabled` | bool | Recomendado activar mesmo com redirecção activa: melhora cache em CDN e mantém extensão correcta ao fazer download de imagem |
| `replacement` | `picture` \| `url` | **`picture`**: substitui `<img>` por `<picture>` com `<source type="image/webp">` — funciona bem com cache de página, mas pode quebrar CSS `div > img` (usar `div img`). **`url`**: substitui directamente os URLs de imagem (src, srcset, lazy-load attrs, inline styles) — mais abrangente mas não funciona com cache de página excepto Cache Enabler |
| `hooks` | `content-hooks` \| `ob` | `content-hooks`: hooka em `the_content`, `the_excerpt`, `post_thumbnail_html`, `get_avatar`, `woocommerce_product_get_image`, `acf_the_content`, `dynamic_sidebar_before/after` — mais leve. `ob`: intercepta a página inteira via output buffering — necessário se o tema/plugin não usa os hooks standard. HTML > 600kb ignora a alteração por protecção de memória |
| `only-for-webp-enabled-browsers` | bool | Só relevante com `replacement=url`: só faz a substituição quando o browser suporta webp (evita servir só-webp a browsers antigos sem JS de fallback) |
| `only-for-webps-that-exists` | bool | Se `false` ("Reference webps that haven't been converted yet" activo), permite referenciar webps que ainda não existem — requer `enable-redirection-to-webp-realizer` activo para funcionar |
| `alter-html-add-picturefill-js` | bool | Só relevante com `replacement=picture`: injecta polyfill `picturefill.js` para browsers sem suporte nativo a `<picture>` (~94% suporte nativo actual) |
| `hostname-aliases` | array de strings | Hostnames adicionais (ex. CDN) cujas imagens também devem ser processadas — útil se outro plugin já reescreve URLs para um CDN antes do WebP Express actuar |
**Verificado em `emanuelalmeida.pt`:** `enabled: true`,
`replacement: "picture"`, `hooks: "ob"`,
`only-for-webp-enabled-browsers: false`,
`only-for-webps-that-exists: false`,
`alter-html-add-picturefill-js: true`, `hostname-aliases: []`.
---
## 10. Web service — partilhar conversão entre sites
Fonte: `lib/options/options/web-service-options/*.inc`. Permite que **este**
site actue como servidor de conversão remota para outros sites que usam o
conversor `wpc`/"Remote WebP Express" (ver §4).
| Campo JSON (`web-service.*`) | Descrição |
|---|---|
| `enabled` | Activa o endpoint (URL exposto: `Paths::getWebServiceUrl()`) |
| `whitelist[]` | Lista de sites autorizados a usar este endpoint, cada entrada com: `label` (referência própria), `ip` (aceita wildcard `*`, ex. `212.91.*` ou `*` para qualquer IP), `api-key` (chave arbitrária escolhida pelo utilizador, não um segredo gerado), `crypt-api-key-in-transfer` (bool — cifra a api-key no transporte; alguns setups antigos não suportam) |
**Verificado em `emanuelalmeida.pt`:** `web-service.enabled: false`,
`whitelist: []` — não usado como servidor remoto neste site.
---
## 11. Bulk convert e cache purge (via UI e via CLI)
Fonte: `lib/options/options/conversion-options/bulk-convert.inc` +
`lib/classes/BulkConvert.php` + `lib/classes/CachePurge.php`.
- **UI → "Bulk Convert"** dispara AJAX que usa `BulkConvert::getList()` +
conversão sequencial das últimas definições **gravadas**. Equivalente
CLI: `wp webp-express convert <scope> [--reconvert]`.
- **UI → "Delete converted files"** dispara AJAX
`CachePurge::processAjaxPurgeCache()`. Equivalente CLI:
`wp webp-express flushwebp [--only-png]`.
- **UI → "Delete log files"** (dentro da secção "Enable logging") apaga
`wp-content/webp-express/log/conversions/*`; sem equivalente CLI
dedicado — usar `wp eval` ou apagar directamente via `find`/`rm` se
necessário (nunca sem autorização em produção).
- O filtro de listagem interno (`BulkConvert::defaultListOptions`) usa
sempre `only-unconverted: true` por default (não reconverte o que já
existe) e `max-depth: 100` — nunca varre mais de 100 níveis de
subpastas.
- `CachePurge::purge()` nunca apaga `.webp` que estejam registados como
attachment/thumbnail na media library (protecção em modo `mingled`, onde
originais e webps partilham pasta).
---
## 12. Regras `.htaccess`
`webp-express-state.configured: true` e
`htaccess-rules-saved-at-some-point: true` confirmam que as regras já foram
gravadas — normalmente na pasta de cache do próprio plugin
(`active-htaccess-dirs: ["cache"]`), não na raiz do site. Isto é o que faz
o browser/Cloudflare receberem directamente o ficheiro `.webp` estático em
vez de passar por PHP a cada pedido (modo `cdn-friendly`).
Se `configured: false` ou o site deixar de servir webps depois de uma
migração/restauro, o passo de correcção é voltar à página de definições do
plugin (wp-admin) e gravar de novo (o botão "Save" regrava o `.htaccess`) —
não há comando WP-CLI dedicado a isto; o mais próximo é
`wp webp-express flushwebp` seguido de `convert`, que não recria as regras
`.htaccess` por si só.
---
## 13. Interacção com Cloudflare — quem faz o quê
**Achado desta sessão, importante para não duplicar trabalho:** a
conversão/optimização de imagem feita pelo **Cloudflare "Polish"** está
`off` neste site (não editável no plano Free) — o WebP não vem do
Cloudflare, vem inteiramente do WebP Express a correr no WordPress. O
Cloudflare aqui é só **cache/CDN de distribuição** do ficheiro `.webp` já
gerado, não faz qualquer transformação de imagem própria. Não há
sobreposição de funcionalidade a resolver, mas também não há optimização
de imagem "grátis" via Cloudflare a contar — se o WebP Express falhar a
converter, o Cloudflare não compensa.
Modo `cdn-friendly` foi escolhido precisamente por causa disto: ao reescrever
o HTML para apontar directamente ao `.webp` estático (em vez de depender do
`Accept` header e do Cloudflare fazer `Vary` correctamente), garante HIT
ratio alto no cache do Cloudflare independentemente do plano.
---
## 14. MCP `webpx` — gestão programática (construído 19-08-2026)
Existe um MCP dedicado (`mcp-webp-express`, registado como `webpx` em
`~/.omp/agent/mcp.json`) que expõe as operações deste documento como tools,
multi-site (todos os 8 aliases do bundle Descomplicar, mesmo padrão do
`wpfc`/`mcp-element-pack`): `spawn("ssh", ...)`, valor em base64 sobre
stdin, sem ledger/rollback (fora do âmbito — ver `emcp-tools` para mudanças
que precisem disso).
**Diferença chave vs os outros MCPs da frota:** a config deste plugin vive
num ficheiro com hash no nome (§1), não numa `wp_option` — o MCP resolve o
hash via `webp-express-config-hash` em cada chamada (nunca hardcoded) e
preserva owner:group:permissões do ficheiro ao escrever (a ligação SSH
corre como root; sem isto o wp-admin do site deixaria de conseguir gravar
as próprias definições). `webp-express-state` também exigiu tratamento
especial: o plugin grava-a como STRING JSON, não como array — um
`--format=json` ingénuo devolve JSON duas vezes codificado.
### Tools (9)
- `webpx_list_sites` — sites conhecidos.
- `webpx_get_status` / `webpx_get_state` — plugin instalado/activo/versão +
`webp-express-state` (workingConverterIds, configured, htaccess-rules-
saved-at-some-point).
- `webpx_get_config` / `webpx_set_config` — ficheiro `config.<hash>.json`
completo (§6).
- `webpx_get_converters` / `webpx_set_converter_options` — stack de
conversores (§4), opções por conversor + reordenação.
- `webpx_convert` — `wp webp-express convert` (§3).
- `webpx_flushwebp` — `wp webp-express flushwebp` (§3).
### Guarda-corpos
- `webpx_set_config` recusa gravar as chaves de diagnóstico só-leitura de
§6 (`environment-when-config-was-saved`,
`base-htaccess-on-these-capability-tests`, `document-root`,
`paths-used-in-htaccess`).
- `webpx_set_config` devolve um aviso explícito quando a chave alterada só
tem efeito via `.htaccess` (operation-mode, scope, image-types,
destination-*, redirection rules) — regenerar essas regras continua a
exigir abrir wp-admin → Save uma vez (§12), sem equivalente WP-CLI.
**Testado ao vivo em `emanuelalmeida.pt` (19-08-2026):** leitura completa
da config, escrita+leitura de volta com `enable-logging` (revertido),
escrita+leitura de volta de `gd.skip-pngs` via `webpx_set_converter_options`
(revertido), bloqueio confirmado ao tentar gravar `document-root`, `convert`
em modo preview (0 ficheiros por converter, config já estável). Owner:group
do ficheiro de config confirmado `ealmeida:ealmeida 644` antes e depois da
escrita. `webpx_flushwebp` e `convert` com `location`/`reconvert` reais não
foram exercidos contra produção (mesmo padrão de comando já validado via
`convert` em dry-run — risco desnecessário para a validação).
## Erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| `find`/`cat` no ficheiro de config falha, "No such file" | Hash do nome do ficheiro mudou (reinstalação/migração) | Nunca hardcode o hash — ler sempre `wp option get webp-express-config-hash` primeiro |
| Confundir `convert_webp_status`/`webp_offset_*` com definições do WebP Express | Options órfãs de outro plugin já removido, mesmo prefixo textual "webp" | Confirmar `wp plugin list` antes de interpretar qualquer option com "webp" no nome |
| `wp webp-express convert --converter=cwebp` falha | Conversor não está na lista `workingConverterIds` (binário não instalado no servidor) | Verificar `webp-express-state` antes de forçar um conversor específico |
| Imagens não aparecem como WebP no browser depois de `convert` | `.htaccess` não gravado (`configured: false`) ou modo `varied-image-responses` com CDN sem `Vary: Accept` | Confirmar `configured: true` em `webp-express-state`; preferir `cdn-friendly` atrás de Cloudflare |
| Webps antigos com qualidade errada persistem depois de mudar `quality`/`encoding` na config | `convert` normal só converte o que falta, não regenera o que já existe | `wp webp-express flushwebp` seguido de `wp webp-express convert` (ou `convert --reconvert` directo) |
| Bulk convert via UI usa qualidade/encoding antigos apesar de ter mudado o form | O bulk convert usa sempre as **últimas definições gravadas** (Save), não o estado não-gravado do formulário | Gravar (Save) antes de disparar "Bulk Convert" |
| Metadata (EXIF) desaparece do webp apesar de `metadata: "all"` | `Gd` está a ser o conversor efectivo — Gd não suporta cópia de metadata, ignora sempre a opção | Confirmar qual conversor está `working`/activo antes de assumir que `metadata: "all"` está a ser respeitado |
| Alpha quality/near-lossless parecem não ter efeito | Conversor efectivo (`Gd`/`Ewww`) não suporta essas opções — são ignoradas silenciosamente | Confirmar o conversor: near-lossless só em `cwebp`/`vips`; alpha-quality ignorado por `Gd`/`Ewww` |
| Selector CSS `div > img` deixa de funcionar depois de activar Alter HTML | `replacement: "picture"` insere um elemento `<picture>` extra entre o `div` e o `img` | Usar selector `div img` (descendente) em vez de `div > img` (filho directo), ou mudar para `replacement: "url"` |
---
**Fonte:** `CONFIG-Plugins-Referencia.md` §5 + auditoria ao vivo original em
`emanuelalmeida.pt` + expansão nesta sessão com leitura completa do
código-fonte: `lib/classes/CLI.php`, `CachePurge.php`, `BulkConvert.php`, e
os 43 ficheiros `.inc`/`.php` de `lib/options/options/` (general,
conversion-options + converter-options/*.php, redirection-rules,
serve-options, alter-html, web-service-options, operation-mode.inc). Config
JSON completo (`config.<hash>.json`) lido ao vivo do disco — todas as
chaves de topo documentadas em §6 vêm directamente desse ficheiro, não só
das que a UI mostra por modo.
+113 -3
View File
@@ -1,14 +1,13 @@
---
name: woocommerce-cli
description: Gestão WooCommerce via WP-CLI no servidor CWP. Cobre produtos, encomendas, clientes, shipping, gateways de pagamento, importação/exportação em massa e automação.
layer: wiki
---
# /woocommerce-cli — WooCommerce via WP-CLI no CWP
Gestão completa de lojas WooCommerce via linha de comandos no servidor CWP. Cobre produtos, encomendas, clientes, configuração, automação e scripting.
**Servidor:** server.descomplicar.pt (176.9.3.158) | porta SSH: 9443
**Servidor:** server.descomplicar.pt | porta SSH: 9443
**Manual:** `Hub/06-Operacoes/Documentacao/Manuais/WooCommerce com WP-CLI_ Guia Completo.md`
**Skill relacionada:** `/woocommerce` — desenvolvimento e extensões WooCommerce
@@ -158,4 +157,115 @@ PATH_WP="/home/USER/public_html"
# Conclusão massiva de todas as encomendas em processamento
WP="/opt/alt/php-fpm83/usr/bin/php /usr/local/bin/wp --allow-root --path=$PATH_WP"
for ID in $($WP wc shop_order list --user=1 --status=processing --format=ids); do
$WP wc shop_order update $ID --us
$WP wc shop_order update $ID --user=1 --status=completed --porcelain
done
```
---
## Clientes
```bash
# Listar clientes com métricas
/opt/alt/php-fpm83/usr/bin/php /usr/local/bin/wp wc customer list \
--user=1 --fields=id,email,orders_count,total_spent \
--format=table --allow-root --path=$PATH_WP
# Criar cliente
/opt/alt/php-fpm83/usr/bin/php /usr/local/bin/wp wc customer create \
--user=1 \
--email="cliente@exemplo.pt" \
--first_name="João" \
--last_name="Silva" \
--billing='{"address_1":"Rua X","city":"Lisboa","postcode":"1000-001","country":"PT"}' \
--allow-root --path=$PATH_WP
```
---
## Configurações da Loja
```bash
PATH_WP="/home/USER/public_html"
# Listar ferramentas disponíveis
/opt/alt/php-fpm83/usr/bin/php /usr/local/bin/wp wc tool list \
--user=1 --fields=id,name --allow-root --path=$PATH_WP
# Sequência de limpeza pós-alterações (executar sempre após mudanças)
WP="/opt/alt/php-fpm83/usr/bin/php /usr/local/bin/wp --allow-root --path=$PATH_WP"
$WP wc tool run clear_transients --user=1 # Limpar transients WC
$WP wc tool run recount_terms --user=1 # Recalcular contadores
$WP wc tool run delete_orphaned_variations --user=1 # Variações órfãs
$WP cache flush # Object cache
$WP rewrite flush # Rewrite rules
# Listar payment gateways
/opt/alt/php-fpm83/usr/bin/php /usr/local/bin/wp wc payment_gateway list \
--user=1 --fields=id,title,enabled --allow-root --path=$PATH_WP
# Desactivar gateway (emergência)
/opt/alt/php-fpm83/usr/bin/php /usr/local/bin/wp wc payment_gateway update paypal \
--user=1 --enabled=false --allow-root --path=$PATH_WP
```
---
## Cron do Sistema (CWP)
Desactivar WP-Cron virtual — adicionar em `wp-config.php`:
```php
define('DISABLE_WP_CRON', true);
```
Configurar cron real (`crontab -e` como root):
```bash
* * * * * /opt/alt/php-fpm83/usr/bin/php /usr/local/bin/wp cron event run --due-now --quiet --allow-root --path=/home/USER/public_html > /dev/null 2>&1
```
---
## Erros Comuns
| Erro | Causa | Solução |
|------|-------|---------|
| `Error: This does not appear to be a WooCommerce installation` | WooCommerce inactivo | `wp plugin activate woocommerce` |
| `Error: Sorry, you cannot list resources` | Falta `--user=` | Adicionar `--user=1` |
| `PHP Fatal error: memory` | Catálogo grande | Adicionar `-d memory_limit=2G` antes de `/usr/local/bin/wp` |
| `wp wc update` não actualiza BD | Bug conhecido | `wp option update woocommerce_db_version "X.X.X"` manual |
| Wizard de onboarding bloqueia | Sem comando CLI | `wp option update woocommerce_task_list_hidden "yes"` |
---
## Recursos Adicionais
### Referência Completa
- **`references/commands.md`** — Todos os tipos de produto (variável, externo, agrupado, virtual, downloadable), shipping zones completo, tributação, cupões, WP All Import, scripts de automação
### Manual Hub
- **`Hub/06-Operacoes/Documentacao/Manuais/WooCommerce com WP-CLI_ Guia Completo.md`** — Guia completo adaptado para CWP (v2.0)
### Skills Relacionadas
- **`/woocommerce`** — Desenvolvimento WooCommerce (PHP, hooks, extensões)
- **`/wp-cli`** — WP-CLI geral no CWP (core, plugins, temas, BD)
---
*WooCommerce CLI no CWP | Descomplicar® | v1.0.0 | 18-02-2026*
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
+91 -2
View File
@@ -1,7 +1,6 @@
---
name: woocommerce
description: Desenvolvimento e optimização de lojas WooCommerce. Personalização de checkout, integração de gateways de pagamento portugueses (Multibanco, MB Way, IfthenPay), funcionalidades custom e performance.
layer: wiki
---
# /woocommerce - WooCommerce Development
@@ -187,4 +186,94 @@ wp transient delete --all
// Auto-criar factura após pedido pago
add_action('woocommerce_order_status_completed', 'create_moloni_invoice');
functi
function create_moloni_invoice($order_id) {
$order = wc_get_order($order_id);
// Chamar API Moloni via MCP
// mcp__moloni__create_invoice(...)
}
```
### CTT Tracking
```php
// Adicionar tracking number
add_action('woocommerce_order_status_processing', 'add_tracking_number');
function add_tracking_number($order_id) {
$tracking = generate_ctt_tracking($order_id);
update_post_meta($order_id, '_tracking_number', $tracking);
}
```
## Performance WooCommerce
```php
// Desactivar scripts desnecessários
add_action('wp_enqueue_scripts', function() {
// Desactivar em páginas que não são WooCommerce
if (!is_woocommerce() && !is_cart() && !is_checkout()) {
wp_dequeue_style('woocommerce-general');
wp_dequeue_style('woocommerce-layout');
wp_dequeue_style('woocommerce-smallscreen');
wp_dequeue_script('wc-cart-fragments');
}
}, 100);
// Optimizar queries
add_filter('woocommerce_product_query', function($q) {
$q->set('no_found_rows', true); // Se não precisar paginação
});
```
## Checklist Nova Loja
```
[ ] WordPress + WooCommerce instalados
[ ] Tema compatível configurado
[ ] Páginas: Shop, Cart, Checkout, My Account, Terms
[ ] Gateways de pagamento activos e testados
[ ] Shipping zones Portugal configuradas
[ ] Taxas IVA: 23% (Continental), 13% (Madeira), 6% (Açores)
[ ] Emails transaccionais personalizados
[ ] SSL instalado e forçado
[ ] RGPD: Política Privacidade, Cookies
[ ] Teste de compra completo (real + teste)
```
## Datasets Dify
| Dataset | ID | Prioridade |
|---------|----|-----------:|
| **Wordpress** | `9da0b2b9-5051-4b99-b9f6-20bf35067092` | 1 |
| **WooCommerce** | (disponível) | 1 |
| **E-commerce** | (disponível) | 2 |
### Como Consultar
```javascript
# PRIMARIO: NotebookLM (Gemini 2.5 RAG)
# mcp__notebooklm__notebook_query({notebook_id: "5be0d1a6-00f2-4cd9-b835-978cb7721601", query: "<tema>"}) // WordPress e Elementor
# FALLBACK: Dify KB (se NotebookLM insuficiente)
// Hooks e customizações WooCommerce
mcp__dify-kb__dify_kb_retrieve_segments({
dataset_id: "9da0b2b9-5051-4b99-b9f6-20bf35067092",
query: "woocommerce hooks checkout cart filters"
})
```
---
**Versão**: 1.0.0 | **Autor**: Descomplicar®
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
+738
View File
@@ -0,0 +1,738 @@
---
name: wordfence
description: Auditoria e gestão completa do Wordfence Security via SQL directo em servidores CWP — cobre as 313 chaves de `wfconfig`, a tabela separada `wfls_settings` do módulo Login Security (2FA/passkeys/CAPTCHA/XML-RPC), armazenamento do WAF em ficheiros (`wp-content/wflogs/config*.php`), WAF (modos, categorias de regra, blacklist premium), bloqueio de IPs/países/padrões (UA/referrer/hostname), Live Traffic, os 5 limitadores de Rate Limiting, Diagnostics, Import/Export de config e Wordfence Central. Detecção de IP real atrás do Cloudflare (howGetIPs), scan settings e interacção com plugins de cache. Usar quando "wordfence", "waf wordpress", "firewall wordpress", "2fa wordpress", "passkeys wordpress", "bloqueio de login", "wfconfig", "wfls_settings", "cloudflare real ip wordfence", "howGetIPs", "scan malware wordpress", "brute force wordpress", "rate limiting wordpress", "live traffic wordfence", "wordfence central", "bloqueio de país wordpress".
---
# /wordfence — Auditoria e Configuração Wordfence via SQL
Gestão do Wordfence Security via `wp db query` directo no servidor CWP.
**A configuração NÃO vive toda no mesmo sítio:**
- Grosso da config (WAF nível aplicação, scan, alertas, blocking,
rate limiting) → tabela própria `wfconfig` (chave/valor, colunas
`name`/`val`), **não** `wp_options` — `wp option get/patch` não
funciona para nada disto.
- 2FA, passkeys, CAPTCHA de login, XML-RPC, "remember device" (módulo
Login Security) → tabela **separada** `wfls_settings` (colunas
`name`/`value` — nomes de coluna diferentes de `wfconfig`). Ver §6.
- Config do motor WAF usada para arrancar antes do WordPress (Extended
Protection/auto-prepend) → **ficheiros PHP serializados** em
`wp-content/wflogs/config*.php`, fora de qualquer tabela SQL,
sincronizados automaticamente a partir de `wfconfig`. Ver §10.
**Fonte:** `CONFIG-Plugins-Referencia.md` §8 (Wordfence, mapeado
16-08-2026, site piloto `emanuelalmeida.pt`, versão `9.0.0` gratuita) +
leitura completa do código-fonte do plugin (466 ficheiros PHP) e
verificação SQL/SSH ao vivo feitas nesta sessão — detalhe completo da
cobertura no rodapé "Fonte" no final do documento.
---
## Contexto CWP — sempre obrigatório
```bash
PATH=/home/USER/public_html
PREFIX=$(wp db prefix --allow-root --path=$PATH) # ex.: wpne_, wpah_ — nunca assumir wp_
```
**Acesso via SSH:** `server.descomplicar.pt`, porta `9443`, user `root`.
Servidor executa comandos como o utilizador do site: `sudo -u USER wp ... --path=$PATH`.
**Tabela de config real (schema confirmado):**
```
DESCRIBE ${PREFIX}wfconfig;
name varchar(100) PRI
val longblob -- valor (string, número ou JSON serializado)
autoload enum('no','yes')
```
Não há `wp option patch` equivalente — é sempre `SELECT`/`UPDATE` directo
sobre `name`/`val`. **313 linhas** de config no site piloto.
---
## Decision tree — qual comando usar
| Operação | Comando |
|---|---|
| Ler 1 chave de config | `wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name='...';"` |
| Ler várias chaves | `SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN (...);` |
| Alterar 1 chave (produção) | `UPDATE ${PREFIX}wfconfig SET val='...' WHERE name='...';` — **preferir sempre a UI do wp-admin** para chaves que disparam hooks (`wordfence_changed_ip_source`, `wordfence_changed_license_key`) |
| Contar utilizadores com 2FA | `SELECT COUNT(*) FROM ${PREFIX}wfls_2fa_secrets;` (tabela moderna) — **não** `usermeta` (legado, pode ficar desactualizado) |
| Ver todas as tabelas do plugin | `SHOW TABLES LIKE '${PREFIX}wf%';` |
| Ver issues de scan pendentes | `SELECT COUNT(*) FROM ${PREFIX}wfpendingissues;` |
| Ver IPs bloqueados pelo WAF | `SELECT COUNT(*) FROM ${PREFIX}wfblockediplog;` |
| Confirmar recomendação de proxy do próprio Wordfence | `SELECT val FROM ${PREFIX}wfconfig WHERE name='detectProxyRecommendation';` |
| Ler settings do módulo Login Security (2FA/passkeys/CAPTCHA) | `SELECT name, value FROM ${PREFIX}wfls_settings ORDER BY name;` — **tabela diferente**, colunas `name`/`value` (não `val`) — ver §6 |
| Ver ficheiros de config do WAF (bootstrap fora do WordPress) | `cat wp-content/wflogs/config.php` / `config-synced.php` — **não é SQL**, é PHP serializado com `__halt_compiler()` — ver §10 |
---
## 1. Auditoria de estado (leitura, sem risco)
```bash
PATH=/home/USER/public_html
PREFIX=$(wp db prefix --allow-root --path=$PATH)
# Estado geral do WAF/firewall
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('firewallEnabled','waf_status','disableWAFIPBlocking','isPaid');" --allow-root --path=$PATH
# Login security (rate-limit + 2FA)
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('loginSec_maxFailures','loginSec_countFailMins','loginSec_lockoutMins',
'loginSec_lockInvalidUsers','allowLegacy2FA','loginSec_requireAdminTwoFactor');" --allow-root --path=$PATH
# Quantos utilizadores TÊM 2FA activo de facto (tabela moderna do módulo login-security)
wp db query "SELECT COUNT(*) AS total_2fa FROM ${PREFIX}wfls_2fa_secrets;" --allow-root --path=$PATH
# Detecção de IP real (crítico atrás de proxy/CDN)
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('howGetIPs','howGetIPs_trusted_proxies','howGetIPs_trusted_proxy_preset',
'detectProxyRecommendation');" --allow-root --path=$PATH
# Scans activos
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'scansEnabled_%';" --allow-root --path=$PATH
# Alertas/email
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('alertEmails','email_summary_enabled','liveTrafficEnabled','blockFakeBots','autoUpdate','cacheType');" --allow-root --path=$PATH
# Volume de actividade (contexto do site)
wp db query "SELECT COUNT(*) FROM ${PREFIX}wfblockediplog;" --allow-root --path=$PATH # IPs bloqueados pelo WAF
wp db query "SELECT COUNT(*) FROM ${PREFIX}wfpendingissues;" --allow-root --path=$PATH # issues de scan em aberto
wp db query "SHOW TABLES LIKE '${PREFIX}wf%';" --allow-root --path=$PATH # inventário de tabelas do plugin
```
### Valores reais confirmados (`emanuelalmeida.pt`, gratuito, 16-08-2026)
| Chave | Valor | Avaliação |
|---|---|---|
| `firewallEnabled` | `1` | ✅ |
| `waf_status` | `enabled` | ✅ WAF a funcionar |
| `disableWAFIPBlocking` | `0` | ✅ bloqueio de IP activo (325 IPs bloqueados no log) |
| `isPaid` | vazio | Versão **gratuita** — sem WAF em tempo real (updates de regras com atraso), sem scan de reputação de IP premium |
| `alertEmails` | `it@descomplicar.pt` | ✅ |
| `email_summary_enabled` | `1` | ✅ |
| `liveTrafficEnabled` | `1` | ✅ |
| `blockFakeBots` | `1` | ✅ bloqueia falsos Googlebot/etc. |
| `autoUpdate` | `1` | ✅ |
| `cacheType` | `disabled` | ✅ **deliberado** — ver §4 |
| `scansEnabled_core/plugins/themes/malware/diskSpace/suspiciousAdminUsers/wafStatus/...` | todos `1` | ✅ cobertura de scan quase completa |
| `scansEnabled_highSense` | `0` | Sensibilidade alta de scan desligada — reduz falsos positivos, aceitável |
| `scansEnabled_scanImages` | `0` | Não faz scan binário a imagens — aceitável, baixo risco típico |
| `scansEnabled_checkHowGetIPs` | `1` | O próprio Wordfence **já tem um scan check dedicado a esta má-configuração** (ver §3) |
| `loginSec_maxFailures` / `_countFailMins` / `_lockoutMins` | `5` / `60` / `240` | Bloqueia ao fim de 5 falhas em 60 min, lockout de 4h |
| `loginSec_lockInvalidUsers` | `0` | Não bloqueia por tentar logins com username inexistente (só falhas de password em conta real) |
| `allowLegacy2FA` | `0` | ✅ não permite métodos 2FA fracos/antigos |
---
## 2. GAP real — 2FA não forçado, zero utilizadores protegidos
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name='loginSec_requireAdminTwoFactor';" --allow-root --path=$PATH
# → 0 = 2FA NÃO é obrigatório para administradores
wp db query "SELECT COUNT(*) FROM ${PREFIX}wfls_2fa_secrets;" --allow-root --path=$PATH
# → 0 = zero utilizadores com 2FA activo, incluindo o admin
```
**Achado real (`emanuelalmeida.pt`):** `loginSec_requireAdminTwoFactor=0` e
**zero** linhas em `wfls_2fa_secrets` — o login continua protegido só por
password + rate-limit (5 tentativas/60min/lockout 4h), sem segundo factor,
mesmo sendo o site pessoal do fundador da empresa.
### Forçar 2FA para administradores
**Auditoria/leitura sempre segura.** Escrita é mutação em produção —
confirmar autorização antes de aplicar:
```bash
# Ler estado actual antes de alterar
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='loginSec_requireAdminTwoFactor';" --allow-root --path=$PATH
# Forçar 2FA para o role Administrator
wp db query "UPDATE ${PREFIX}wfconfig SET val='1' WHERE name='loginSec_requireAdminTwoFactor';" --allow-root --path=$PATH
```
**Cuidado operacional:** activar isto **sem** um administrador já ter um
autenticador configurado bloqueia o próprio acesso ao wp-admin no login
seguinte (o Wordfence força a configuração do 2FA no momento do login, não
tranca por fora — mas confirmar sempre com o utilizador antes, e nunca
aplicar em sessão headless sem acesso de recuperação garantido, ex. acesso
SSH/SQL directo para reverter o valor para `0` se algo correr mal).
---
## 3. GAP real — `howGetIPs` vazio atrás do Cloudflare
**Este é o achado mais concreto desta auditoria.** O próprio Wordfence já
tinha calculado a recomendação correcta e guardado numa chave separada, mas
**nunca a aplicou** à chave activa:
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('howGetIPs','detectProxyRecommendation');" --allow-root --path=$PATH
```
```
name val
detectProxyRecommendation HTTP_CF_CONNECTING_IP
howGetIPs (vazio)
```
`detectProxyRecommendation` é preenchido automaticamente pelo Wordfence
quando detecta que o tráfego chega via um proxy conhecido (Cloudflare, neste
caso) — mas **fica só como sugestão**; `howGetIPs` (a chave que o WAF
realmente usa para decidir qual IP é o "real") continua vazia. Com
`howGetIPs` vazio, o Wordfence cai no fallback de ordem de headers
(`lib/wfUtils.php`: `HTTP_CF_CONNECTING_IP` → `HTTP_X_REAL_IP` →
`REMOTE_ADDR` → `HTTP_X_FORWARDED_FOR`), o que **na maioria dos casos até
funciona**, mas não é o comportamento explícito/auditável e não activa o
scan check dedicado (`scansEnabled_checkHowGetIPs=1` já está ligado — é
precisamente este scan que devia estar a assinalar isto como issue, mas
`wfpendingissues` está vazia, i.e. o scan periódico pode não ter corrido
recentemente ou o alerta expirou).
### Valores válidos de `howGetIPs` (confirmado no código, `wfConfig.php`/`wfUtils.php`)
Cabeçalhos HTTP reconhecidos pelo Wordfence para determinar o IP real:
| Valor | Quando usar |
|---|---|
| `HTTP_CF_CONNECTING_IP` | Site atrás de **Cloudflare** — este é o caso do bundle Descomplicar® (todos os sites usam App for Cloudflare®) |
| `HTTP_X_REAL_IP` | Proxy tipo nginx reverso simples (sem CDN) |
| `HTTP_X_FORWARDED_FOR` | Load balancer/proxy genérico |
| (vazio) | `REMOTE_ADDR` directo — só correcto se não houver proxy/CDN nenhum à frente |
Existe ainda `howGetIPs_trusted_proxy_preset`, que aponta para uma entrada
em `ipResolutionList` (JSON de ~8KB, sincronizado da API Wordfence, contém
blocos de IP de CDNs conhecidas — ex. `cloudflare`, `cloudfront` — usados
para validar que o pedido realmente vem de um proxy confiável antes de
aceitar o header). Consultar presets disponíveis:
```bash
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='ipResolutionList';" --allow-root --path=$PATH \
| python3 -c "import json,sys; print(list(json.load(sys.stdin).keys()))"
```
### Corrigir (copiar a própria recomendação do Wordfence)
```bash
# Ler antes de escrever
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='detectProxyRecommendation';" --allow-root --path=$PATH
# Aplicar — usa o valor que o próprio Wordfence já recomendou
wp db query "UPDATE ${PREFIX}wfconfig SET val='HTTP_CF_CONNECTING_IP' WHERE name='howGetIPs';" --allow-root --path=$PATH
# Confirmar
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name='howGetIPs';" --allow-root --path=$PATH
```
Sem isto configurado, o rate-limiting de login, os bloqueios de IP do WAF e
o Live Traffic podem estar a agir sobre o IP do proxy Cloudflare em vez do
IP real do atacante — mais grave em sites com muito tráfego atrás do mesmo
edge, onde o "IP" observado pode ser partilhado por múltiplos visitantes.
---
## 4. Interacção com cache de página — `cacheType=disabled` é deliberado
```bash
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='cacheType';" --allow-root --path=$PATH
# → disabled
```
O Wordfence tem o seu próprio sistema de cache de página interno
(`Enable Falcon Engine`), mas em sites do bundle Descomplicar® **a cache de
página já é feita por outro plugin dedicado** (WP Fastest Cache ou
equivalente). `cacheType=disabled` está **correcto** — dois sistemas de
cache HTML activos ao mesmo tempo competem, servem versões desalinhadas e
dificultam o purge coordenado. **Nunca activar o cache Falcon do Wordfence
num site que já tem WP Fastest Cache/WP Meteor a fazer cache de página** —
confirmar sempre qual plugin de cache está activo antes de alterar
`cacheType` num site novo.
---
## 5. Scan settings — cobertura completa
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'scansEnabled_%';" --allow-root --path=$PATH
```
| Categoria | Chave | Estado (piloto) |
|---|---|---|
| Core WordPress | `scansEnabled_core`, `_coreUnknown`, `_oldVersions` | `1` |
| Plugins/temas | `scansEnabled_plugins`, `_themes` | `1` |
| Malware/ficheiros suspeitos | `scansEnabled_malware`, `_fileContents`, `_fileContentsGSB`, `_suspectedFiles` | `1` |
| Espaço em disco | `scansEnabled_diskSpace` | `1` |
| Utilizadores admin suspeitos | `scansEnabled_suspiciousAdminUsers` | `1` |
| Opções de BD suspeitas | `scansEnabled_options`, `_suspiciousOptions` | `1` |
| Passwords fracas | `scansEnabled_passwds` | `1` |
| Comentários/posts (spam/malware injectado) | `scansEnabled_comments`, `_posts` | `1` |
| GeoIP / Google Safe Browsing | `scansEnabled_geoipSupport`, `_checkGSB` | `1` |
| Config legível externamente | `scansEnabled_checkReadableConfig` | `1` |
| **Detecção de IP mal configurada** | `scansEnabled_checkHowGetIPs` | `1` — ver §3 |
| Estado do WAF | `scansEnabled_wafStatus` | `1` |
| WPScan (directory listing, full path disclosure) | `scansEnabled_wpscan_directoryListingEnabled`, `_wpscan_fullPathDisclosure` | `1` |
| Sensibilidade alta | `scansEnabled_highSense` | `0` (desligado, reduz falsos positivos) |
| Scan binário de imagens | `scansEnabled_scanImages` | `0` (desligado) |
**Avaliação:** cobertura de scan quase total (23 de 25 checks activos),
única omissão relevante é a sensibilidade alta — aceitável para reduzir
ruído em sites com actividade legítima elevada.
---
## 6. Two-Factor / Passkeys / CAPTCHA — tabela SEPARADA `wfls_settings`
**Achado arquitectural importante desta expansão:** o módulo "Login
Security" (2FA, passkeys, CAPTCHA de login, XML-RPC, "remember device",
integração WooCommerce, NTP) **não guarda a sua configuração em
`wfconfig`** — vive numa tabela própria, `wfls_settings`, gerida pela
classe `WordfenceLS\Controller_Settings` (código em
`modules/login-security/classes/controller/settings.php`). Schema:
```
DESCRIBE ${PREFIX}wfls_settings;
name varchar(191) PRI
value longtext -- nota: coluna chama-se "value", não "val"
autoload enum('no','yes')
```
```bash
wp db query "SELECT name, value FROM ${PREFIX}wfls_settings ORDER BY name;" --allow-root --path=$PATH
```
As chaves `loginSec_requireAdminTwoFactor` e `loginSec_enableSeparateTwoFactor`
em `wfconfig` (§2) são a versão **legada** — um switch global único. A
versão actual do plugin é **baseada em papéis (roles)**:
`migrate_admin_2fa_requirements_to_roles()` migra o switch legado para
chaves por role (`required-2fa-role.<role>`) dentro de `wfls_settings` e
depois apaga a chave legada do `wfconfig`. Um site "antigo" pode ainda ter
o valor legado; um site já migrado tem-no ausente/zero e a fonte de
verdade passa a ser `wfls_settings` + capabilities.
### Chaves confirmadas em `wfls_settings` (`emanuelalmeida.pt`)
| Chave | Valor confirmado | O que controla |
|---|---|---|
| `xmlrpc-enabled` | `1` | Exigir 2FA também em pedidos XML-RPC autenticados |
| `allow-xml-rpc` | `1` | Permitir XML-RPC de todo (independente do 2FA) |
| `ip-source` | vazio (`automatic`) | Fonte do IP real para o módulo login-security — **independente** de `howGetIPs` do `wfconfig` (§3); valores: `automatic`, `REMOTE_ADDR`, `X-Forwarded-For`, `X-Real-IP` |
| `ip-trusted-proxies` | vazio | Lista de proxies confiáveis (newline-separated) para o cálculo de IP deste módulo |
| `2fa-user-grace-period` | `10` | Dias de período de graça depois de 2FA se tornar obrigatório por role, antes de bloquear o login |
| `require-2fa.administrator` | vazio | Legado (ver acima); use `required-2fa-role.administrator` |
| `passkey-sign-count-mode` | `reject-lower` | Política anti-clonagem de passkeys: `allow` / `reject-lower` / `reject-lower-and-zero` |
| `passkey-relying-party-override` | vazio | Override do hostname RP para passkeys (multi-domínio) |
| `remember-device` | vazio (`false`) | "Lembrar este dispositivo" para saltar 2FA X dias |
| `remember-device-duration` | `2592000` (30 dias) | Duração do "remember device" em segundos |
| `always-show-login-security-menu` | `1` | Mostrar sempre o menu Login Security mesmo sem nada configurado |
| `enable-auth-captcha` | vazio | CAPTCHA (reCAPTCHA) no formulário de login |
| `recaptcha-site-key` / `recaptcha-secret` | vazio | Credenciais reCAPTCHA — só relevantes se `enable-auth-captcha=1` |
| `recaptcha-threshold` | `0.5` | Limiar de pontuação do reCAPTCHA v3 (0–1) |
| `enable-woocommerce-integration` / `-account-integration` | vazio | Aplicar 2FA/passkeys também na área de conta WooCommerce |
| `use-ntp` | `1` | Sincronizar relógio via NTP para códigos TOTP (evita drift do relógio do servidor) |
| `whitelisted` | vazio | IPs isentos de exigência de 2FA (não confundir com `whitelisted` do `wfconfig`, que é do WAF) |
| `schema-version` | `3` | Versão do schema interno do módulo — não editar manualmente |
### Modelo de permissões por role (capabilities, não settings)
"2FA obrigatório" e "2FA opcional" por role **não** são uma chave de
configuração simples — são geridos como **capabilities** do WordPress,
sincronizadas por `WordfenceLS\Controller_Permissions`:
| Capability | Significado |
|---|---|
| `wf2fa_activate_2fa_self` | O utilizador pode activar/desactivar 2FA na própria conta |
| `wf2fa_activate_2fa_others` | Pode activar/desactivar 2FA de outros utilizadores |
| `wf2fa_manage_settings` | Pode editar as definições do módulo Login Security |
| `wfls_manage_passkey_self` / `_others` | Equivalente para passkeys |
| `wfls_show_login_security` | Capability interna que mostra/esconde o menu (sincronizada automaticamente) |
```bash
wp eval 'var_export(get_role("administrator")->capabilities);' --allow-root --path=$PATH | grep -i 'wf2fa\|wfls'
# Confirmado em emanuelalmeida.pt: administrator TEM wf2fa_activate_2fa_self (pode activar),
# mas isso é OPCIONAL — só se torna obrigatório se required-2fa-role.administrator tiver timestamp > -1
```
O estado "obrigatório desde quando" fica em `required-2fa-role.<role>`
(timestamp Unix) ou `-1` (não obrigatório) — chave dinâmica dentro de
`wfls_settings`, não aparece na lista de defaults porque só existe depois
de alguém a definir pela UI.
### Tabelas de dados do módulo (não config, mas relevantes para auditoria)
| Tabela | Conteúdo |
|---|---|
| `wfls_2fa_secrets` | Segredos TOTP activos por utilizador (contagem = utilizadores com 2FA activo) |
| `wfls_passkeys` | Passkeys (WebAuthn) registadas por utilizador |
| `wfls_role_counts` | Cache de contagens de utilizadores por role, usada pela UI |
---
## 7. Live Traffic — configuração completa
Todas as chaves vivem em `wfconfig` (ao contrário do módulo 2FA). Tabela
de dados: `wflivetraffichuman` (visitas humanas) + `wfhits`, `wfhoover`,
`wfcrawlers`, `wflocs`, `wflogins` (dados de suporte ao Live Traffic e ao
dashboard de tráfego).
| Chave | Valor (piloto) | O que controla |
|---|---|---|
| `liveTrafficEnabled` | `1` | Liga/desliga o registo de Live Traffic |
| `liveTraf_ignorePublishers` | `1` | Não regista visitas de utilizadores autenticados com capacidade de publicar (reduz ruído do próprio staff) |
| `liveTraf_ignoreUsers` | vazio | Lista de usernames (separados por vírgula) a ignorar |
| `liveTraf_ignoreIPs` | vazio | Lista de IPs (separados por vírgula) a ignorar |
| `liveTraf_ignoreUA` | vazio | User-agent a ignorar (substring) |
| `liveTraf_maxRows` | `2000` | Quantidade máxima de registos guardados |
| `liveTraf_maxAge` | `30` | Dias máximos de retenção dos registos |
| `liveTraf_displayExpandedRecords` | `0` | Preferência de UI (registos expandidos por omissão) — não afecta captura |
**Overrides que desligam Live Traffic independentemente da config:**
`wfConfig::liveTrafficEnabled()` força `false` se a constante
`WORDFENCE_DISABLE_LIVE_TRAFFIC` estiver definida, ou se
`WF_IS_WP_ENGINE` for verdadeiro (hosts WP Engine desactivam sempre este
módulo, independentemente do valor em `wfconfig`) — útil para explicar a
um cliente porque "liguei e não aparece nada" nesse tipo de hosting.
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'liveTraf%' OR name='liveTrafficEnabled';" --allow-root --path=$PATH
```
---
## 8. Blocking completo — País, IP manual, Padrões (UA/Referrer/Hostname)
Todos os bloqueios (manuais e automáticos) vivem numa única tabela,
`wfBlocks7` (não em `wfconfig`), com um campo `type` que distingue a
natureza do bloqueio (`models/block/wfBlock.php`):
| Constante | Valor | Origem |
|---|---|---|
| `TYPE_IP_MANUAL` | 1 | Bloqueio de IP manual pelo admin |
| `TYPE_WFSN_TEMPORARY` | 2 | Resposta automática da Wordfence Security Network (rede partilhada entre sites) |
| `TYPE_COUNTRY` | 3 | Bloqueio por país |
| `TYPE_PATTERN` | 4 | Bloqueio avançado (IP range + hostname + user-agent + referrer) |
| `TYPE_RATE_BLOCK` | 5 | Gerado pelo rate limiting (§9), acção `block` |
| `TYPE_RATE_THROTTLE` | 6 | Gerado pelo rate limiting (§9), acção `throttle` |
| `TYPE_LOCKOUT` | 7 | Lockout de login (falhas de password/2FA) |
| `TYPE_IP_AUTOMATIC_TEMPORARY` / `_PERMANENT` | 8 / 9 | Bloqueios automáticos do WAF, temporário vs promovido a permanente por acção do admin |
```bash
# Ver todos os bloqueios activos por tipo
wp db query "SELECT type, COUNT(*) FROM ${PREFIX}wfblocks7 WHERE expiration=0 OR expiration>UNIX_TIMESTAMP() GROUP BY type;" --allow-root --path=$PATH
```
### Bloqueio por país
Campos guardados em `parameters` (JSON): `blockLogin` (bloquear só a
página de login), `blockSite` (bloquear o resto do site), `countries`
(array de códigos ISO). Comportamento configurado globalmente em
`wfconfig`:
| Chave | Valor (piloto) | O que controla |
|---|---|---|
| `cbl_action` | `block` | O que fazer a um país bloqueado: `block` (503) ou `redirect` (redireccionar) |
| `cbl_redirURL` | vazio | URL de destino se `cbl_action=redirect` |
| `cbl_loggedInBlocked` | `0` | Bloquear país mesmo que o visitante já tenha sessão iniciada |
| `cbl_bypassRedirURL` / `cbl_bypassRedirDest` | vazio | Um visitante de país bloqueado que aceda a este URL relativo é reencaminhado e recebe um cookie de bypass permanente |
| `cbl_bypassViewURL` | vazio | Um visitante permitido que veja este URL relativo recebe o cookie de bypass antecipadamente (para o caso de mudar de país/VPN depois) |
| `cbl_cookieVal` | gerado automaticamente | Valor único do cookie de bypass — regenerar invalida bypasses concedidos |
Nenhum destes está configurado no piloto (site sem bloqueio de país
activo) — confirmar sempre `hasCountryBlock()`/`countryBlocks()` antes de
assumir que não há impacto em visitantes internacionais.
### Bloqueio manual de IP / IP range
```bash
# Confirmar que um IP não está na whitelist antes de o bloquear (a UI faz isto automaticamente)
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='whitelisted';" --allow-root --path=$PATH
```
`wfBlock::isWhitelisted()` verifica dois níveis: a lista manual em
`whitelisted` (`wfconfig`, uma entrada por linha, IP/CIDR) e uma lista de
serviços conhecidos (`whitelistedServices`, JSON — vazio `{}` no piloto;
quando preenchido guarda ranges de IP de serviços como monitorização
externa que o Wordfence nunca bloqueia mesmo com regras activas).
### Bloqueio avançado (Custom Pattern) — hostname / user-agent / referrer
Bloqueio combinável por 4 critérios (pelo menos 1 obrigatório),
`type=custom-pattern` na validação (`wfBlock::validate()`):
| Campo | Formato | Exemplo de uso |
|---|---|---|
| `ipRange` | IP único, CIDR, ou range `1.2.3.4-1.2.3.9` (IPv4 e IPv6 não podem misturar-se no mesmo bloqueio) | Bloquear uma subnet inteira |
| `hostname` | wildcard `*` e `.` (regex `^[a-z0-9\.\*\-]+$`) | Bloquear por reverse-DNS de um datacenter conhecido |
| `userAgent` | substring livre | Bloquear scrapers com UA identificável |
| `referrer` | substring livre | Bloquear tráfego de referrer spam |
```bash
wp db query "SELECT id, blockedTime, reason, expiration FROM ${PREFIX}wfblocks7 WHERE type=4 AND (expiration=0 OR expiration>UNIX_TIMESTAMP());" --allow-root --path=$PATH
```
### Como o Wordfence decide não bloquear crawlers do Google
```bash
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='neverBlockBG';" --allow-root --path=$PATH
# Confirmado no piloto: neverBlockVerified
```
| Valor de `neverBlockBG` | Comportamento |
|---|---|
| `neverBlockVerified` (default, activo no piloto) | Verifica UA **e** faz PTR/reverse-DNS lookup para confirmar que é mesmo um IP do Google antes de isentar de bloqueio |
| `neverBlockUA` | Confia só no User-Agent, sem verificação de IP — menos seguro (fácil de fingir), mas mais barato |
| `treatAsOtherCrawlers` | Não dá tratamento especial a "Google" — pode ser bloqueado como qualquer outro crawler |
---
## 9. Rate Limiting completo — 5 tipos, `throttle` vs `block`
`models/block/wfRateLimit.php` define **5 limitadores independentes**,
cada um com um par de chaves `<nome>` (limiar) + `<nome>_action`
(`throttle` ou `block`). `DISABLED` como valor do limiar desactiva esse
limitador especificamente.
| Limitador | Chave limiar | Chave acção | Valor confirmado (piloto) | Aplica-se a |
|---|---|---|---|---|
| Global | `maxGlobalRequests` | `maxGlobalRequests_action` | `240` / `throttle` | **Qualquer** pedido, humano ou crawler — se activo, sobrepõe-se aos outros 4 |
| Page views de crawlers | `maxRequestsCrawlers` | `maxRequestsCrawlers_action` | `DISABLED` / `throttle` | Só User-Agents identificados como crawler (`wfCrawl::isCrawler`) |
| 404s de crawlers | `max404Crawlers` | `max404Crawlers_action` | `60` / `throttle` | Idem, mas só páginas 404 |
| Page views de humanos | `maxRequestsHumans` | `maxRequestsHumans_action` | `DISABLED` / `throttle` | Visitantes não identificados como crawler |
| 404s de humanos | `max404Humans` | `max404Humans_action` | `60` / `throttle` | Idem, mas só páginas 404 |
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'max%Requests%' OR name LIKE 'max404%' OR name='blockedTime';" --allow-root --path=$PATH
```
**No piloto, o rate limit global (`maxGlobalRequests=240`) está
ACTIVO** — qualquer IP (humano ou bot) que exceda 240 pedidos na janela
de 1 minuto (tabela `wfTrafficRates`, campo `eMin`) é afectado. Os
limitadores dedicados a crawlers/humanos de page views estão desligados;
só os de 404 (60 num minuto) estão activos, o que é uma configuração
razoável (protege contra scans de 404 sem penalizar navegação normal).
**`throttle` vs `block` não são só rótulos — têm durações e severidade
diferentes:**
- `throttle` → `wfBlock::createRateThrottle()`, duração **fixa de 60
segundos** (`wfRateLimit::rateLimitThrottleDuration()`) — resposta 503
temporária, o visitante volta a conseguir aceder ao fim de 1 minuto.
- `block` → `wfBlock::createRateBlock()`, duração = `blockedTime`
(`wfconfig`, **300s / 5 min no piloto**, configurável) — bloqueio mais
longo, e pode ser promovido a permanente manualmente pelo admin na UI
de Blocking (`TYPE_RATE_BLOCK` → `TYPE_IP_AUTOMATIC_PERMANENT`).
**Excepção comum a 404s permitidos:** `allowed404s` (`wfconfig`, uma
entrada por linha, suporta wildcard `*`) — URLs que nunca contam para os
limitadores de 404, mesmo que devolvam 404 de facto. Default do piloto:
`/favicon.ico`, `/apple-touch-icon*.png`, `/*@2x.png`,
`/browserconfig.xml` (ícones/manifestos pedidos por browsers que geram
404 legítimos e não devem disparar rate limiting).
---
## 10. WAF — arquitectura de armazenamento, modos e categorias de regra
**Achado arquitectural crítico:** para o WAF funcionar *antes* do
WordPress carregar (Extended Protection / auto-prepend), o Wordfence
**não pode depender só da base de dados** — mantém uma cópia da config
relevante em ficheiros PHP no filesystem, fora de `wp_content/plugins`:
```bash
ls -la wp-content/wflogs/
# config.php — config nuclear do WAF (wafStatus, authKey, versão do formato) — 6 chaves só
# config-synced.php — MIRROR de chaves relevantes do wfconfig + bloqueios activos, sincronizado
# sempre que algo muda (país/IP/pattern blocks, howGetIPs, apiKey, etc.)
# config-livewaf.php — config activa/em uso pelo processo WAF autoprepended
# config-transient.php— cache de dados de ataque (grande, ~1.7 MB no piloto)
# rules.php — ficheiro PHP executável (não serializado) com as regras do WAF, incluído
# directamente no bootstrap; NÃO editar/apagar manualmente
# attack-data.php, ips.php — logs internos de ataques/IPs vistos pelo WAF
```
Estes ficheiros começam com `<?php exit('Access denied');
__halt_compiler(); ?>` seguido de dados serializados em PHP — só são
legíveis fazendo `substr()` a partir do offset de `__halt_compiler()` e
`unserialize()`. **Nunca editar estes ficheiros manualmente**; qualquer
alteração de config deve passar pela UI ou por `wfconfig`, que depois é
sincronizada automaticamente para estes ficheiros (via
`wfWAFIPBlocksController::setNeedsSynchronizeConfigSettings()`, chamado
sempre que `wfBlock::create*()` corre).
O motor de armazenamento é escolhido por `WFWAF_STORAGE_ENGINE`
(`waf/bootstrap.php`): **`file`** por omissão (confirmado no piloto —
ficheiros em `wflogs/` existem e têm conteúdo recente), ou **`mysqli`**
forçado automaticamente em hosts WP Engine/Flywheel (ou por constante
explícita), caso em que a config do WAF passa a viver numa ligação MySQL
directa (bypass ao `$wpdb`) em vez de ficheiros.
### Modos e níveis do WAF (`models/firewall/wfFirewall.php`)
| Constante | Valores possíveis | Significado |
|---|---|---|
| `firewallMode()` | `disabled` / `learning-mode` / `enabled` | Estado geral; **`enabled`** no piloto (§1, `waf_status`) |
| `protectionMode()` | `basic` / `extended` | `extended` requer `WFWAF_AUTO_PREPEND` activo (ficheiro `.user.ini`/`.htaccess` a fazer `auto_prepend_file` para `wordfence-waf.php` antes do WordPress arrancar) — sem isto, o WAF só corre depois do `muplugins_loaded`, mais tarde no ciclo, "Basic Protection" |
| `ruleMode()` | `community` / `premium` | Depende de `isPaid` — piloto usa feed `community` (regras com atraso de propagação face a sites Premium) |
| `blacklistMode()` | `disabled` / `enabled` | "Real-Time IP Blocklist" — **só disponível com `isPaid`**, mesmo que `disableWAFBlacklistBlocking=0` |
| `learningModeStatus()` | `false` / `true` / timestamp | Se em Learning Mode, indica se há mudança automática agendada para Enabled (`learningModeGracePeriodEnabled` + `learningModeGracePeriod`) |
### Categorias de regra (fail-score) — confirmado no `rules.php` do piloto
O motor de regras usa 3 categorias de ataque com pontuação de risco
própria (`$this->failScores[...]` em `wflogs/rules.php`):
```bash
ssh server "grep -o \"failScores\\['[a-z]*'\\]\" .../wp-content/wflogs/rules.php | sort -u"
# failScores['rce'] — Remote Code Execution
# failScores['sqli'] — SQL Injection
# failScores['xss'] — Cross-Site Scripting
```
Regras individuais podem ser desactivadas via `disabledRules` — essa
chave vive **dentro do storage do WAF** (`wp-content/wflogs/config.php`
no motor `file`), não em `wfconfig`; o piloto não tem nenhuma regra
desactivada (chave ausente = todas activas). A "pontuação de segurança"
mostrada no Dashboard (`wfFirewall::overallStatus()`) combina: % de
regras activas (35%), blacklist premium activa (35%), Extended Protection
(20%), Rate Limiting/Advanced Blocking ligado (10%) — pesado a favor do
WAF (80% do total), com brute-force protection a contar só 20%.
---
## 11. Diagnostics (Tools → Diagnostics)
Gerado por `lib/wfDiagnostic.php`, sem persistência em BD — é sempre
calculado on-demand quando a página carrega. Secções e o que cada uma
verifica:
| Secção | O que reporta |
|---|---|
| Wordfence Status | Versão instalada, versão da base GeoIP, jobs de cron em atraso (>30min) |
| Filesystem | Se o webserver consegue ler/escrever `wp-content/plugins/wordfence` e `wp-content/wflogs` |
| Wordfence Config | Teste de escrita/leitura real contra `wfconfig` (básico e serializado) — falha aqui = sem permissões de escrita na tabela |
| Wordfence Firewall | Auto-prepend activo, motor de storage configurado vs activo, path dos logs, permissões de ficheiros, conteúdo de `.htaccess`/`.user.ini` relacionado com o WAF |
| MySQL | Versão do MySQL + privilégios reais do utilizador da BD: `SELECT`/`INSERT`/`UPDATE`/`DELETE`/`CREATE`/`ALTER`/`DROP`/`TRUNCATE` (via `SHOW GRANTS FOR current_user()`) |
| PHP Environment | Versão PHP vs mínimo suportado, dono do processo PHP, suporte OpenSSL/cURL (versões, protocolos), `display_errors` |
| Connectivity | Ligação de saída a servidores Wordfence (https), ligação de volta ao próprio site (IPv4 e IPv6 — útil para detectar Cloudflare a bloquear o próprio site) |
| Time | Hora da rede Wordfence, hora do servidor, offset NTP, fonte de tempo usada para TOTP (2FA), fuso horário do WordPress |
**Casos de uso práticos:**
- `userCanAlter`/`userCanDrop` a `false` → migrações/upgrades do plugin
podem falhar silenciosamente; verificar antes de reportar "scan não
actualiza issues".
- `connectToSelf`/`connectToSelfIpv6` a falhar com sinais de Cloudflare
(`cf-mitigated: challenge`, `Cloudflare Ray ID` no body) → o próprio
Cloudflare está a desafiar o loopback request do Wordfence; relevante
para sites do bundle Descomplicar® (todos atrás de Cloudflare via
App for Cloudflare®) em diagnóstico de scans que nunca terminam.
- A secção "WordPress" (via `wfDiagnostic::getWordpressValues()`, não
listada acima por ser genérica ao core) expõe constantes como
`WP_DEBUG`, `DISALLOW_FILE_EDIT`, `WP_CACHE`, `FORCE_SSL_ADMIN` — útil
para confirmar hardening sem abrir `wp-config.php` directamente.
---
## 12. Tools → Import/Export de configuração
**Não é um ficheiro local** (ao contrário de outros plugins do bundle) —
`wfImportExportController` (`lib/wfImportExportController.php`) faz
`export()`/`import()` **através da API do wordfence.com**: o export
gera um token remoto (`export_options`), que depois se cola manualmente
no site de destino (`import_options` + token). Isto exige que ambos os
sites tenham `apiKey` válida e liguem para fora ao wordfence.com — não
funciona offline nem é um download/upload de ficheiro local.
O que é exportado: todas as `wfConfig::getExportableOptionsKeys()`
(essencialmente os grupos `checkboxes` + `otherParams` do
`$defaultConfig`, não o grupo `defaultsOnly`), o agendamento de scans
(`scanSched`, serializado), e todos os registos da tabela de bloqueios
(`wfBlock::exportBlocks()` — inclui bloqueios de país, IP e pattern, mas
não lockouts nem bloqueios automáticos temporários).
**Não é adequado para "clonar config para outro site" via SQL directo**
(copiar a tabela `wfconfig` entre sites não é suportado oficialmente e
arrasta chaves específicas da instalação como `encKey`/`longEncKey` —
preferir sempre o fluxo de token oficial acima ou reconfigurar via UI).
---
## 13. Wordfence Central — gestão remota multi-site
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'wordfenceCentral%';" --allow-root --path=$PATH
```
**Achado confirmado no piloto: este site ESTÁ ligado ao Wordfence
Central** (`wordfenceCentralConnected=1`), associado a
`it@descomplicar.pt` desde 27/02/2025
(`wordfenceCentralConnectTime=1740679066`), com um `wordfenceCentralSiteID`
próprio (UUID). Central é a consola de gestão remota multi-site do
Wordfence (central.wordfence.com) — permite monitorizar/alertar e (em
planos pagos) configurar Wordfence em vários sites a partir de um único
painel, autenticado via JWT (`wordfenceCentralAccessToken`,
`wordfenceCentralUserSiteAccessToken`) trocado através de um par de
chaves pública/privada gerado no site (`wordfenceCentralPK`,
`wordfenceCentralSecretKey`).
| Chave relevante | O que é |
|---|---|
| `wordfenceCentralConnected` | `1`/`0` — liga/desliga a integração |
| `wordfenceCentralConnectEmail` / `_ConnectTime` | Quem ligou e quando |
| `wordfenceCentralPluginAlertingDisabled` | Se `1`, silencia alertas duplicados no próprio wp-admin porque já chegam via Central |
| `wordfenceCentralConfigurationIssue` | Flag interna — `1` indica falha na renovação do token JWT, útil para diagnosticar "Central parou de reportar" |
| `wordfenceCentralSiteData` | Cache JSON da última resposta da API Central sobre este site |
**Widget do dashboard nativo do WordPress:**
`email_summary_dashboard_widget_enabled` (`wfconfig`, `1` no piloto)
controla o widget "Activity Report" no `/wp-admin/index.php` (WordPress
Dashboard nativo) — **não confundir** com o próprio Dashboard do
Wordfence (`/wp-admin/admin.php?page=Wordfence`, componente Vue sem
chave de config individual para mostrar/esconder secções).
---
## Gotchas / erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| `wp option get wordfence_...` devolve vazio/erro | Config do Wordfence não vive em `wp_options` | Usar sempre `wp db query` contra `${PREFIX}wfconfig` (colunas `name`/`val`) |
| `SELECT value FROM wfconfig` falha (`Unknown column 'value'`) | A coluna chama-se `val`, não `value` | `SELECT name, val FROM ${PREFIX}wfconfig` |
| Contagem de 2FA por `usermeta.meta_key='wordfence_2fa_secret'` parece desactualizada | Essa é a chave legada; o módulo login-security moderno guarda em tabela própria | Preferir `SELECT COUNT(*) FROM ${PREFIX}wfls_2fa_secrets` |
| `howGetIPs` vazio mas o site parece "funcionar" na mesma | O Wordfence tem fallback de ordem de headers (`HTTP_CF_CONNECTING_IP` → `HTTP_X_REAL_IP` → `REMOTE_ADDR` → `HTTP_X_FORWARDED_FOR`) — mascara o problema em vez de o resolver | Configurar explicitamente `howGetIPs`, não confiar no fallback silencioso |
| `wfpendingissues` vazia mesmo com `scansEnabled_checkHowGetIPs=1` e `howGetIPs` mal configurado | O scan periódico pode não ter corrido recentemente, ou o alerta já expirou/foi dispensado | Não usar `wfpendingissues` vazia como prova de "nada errado" — cruzar sempre com leitura directa da config |
| Activar `loginSec_requireAdminTwoFactor` sem aviso prévio | Bloqueia o fluxo normal de login até o admin configurar um autenticador | Confirmar com o utilizador antes; garantir acesso alternativo (SSH/SQL) para reverter |
| Prefixo de tabela assumido como `wp_` | Sites CWP podem ter prefixo custom (confirmado `wpne_` no piloto) | `wp db prefix --allow-root --path=$PATH` sempre primeiro |
| Editar 2FA/passkeys/CAPTCHA via `UPDATE ${PREFIX}wfconfig` sem efeito | Essas definições vivem em `wfls_settings` (colunas `name`/`value`, não `name`/`val`) — ver §6 | `UPDATE ${PREFIX}wfls_settings SET value='...' WHERE name='...';` |
| Editar regras/bloqueios do WAF directamente em `wp-content/wflogs/*.php` | São ficheiros PHP serializados com `__halt_compiler()`, regenerados a partir da BD — editar à mão desalinha-os da BD e pode partir o WAF | Alterar sempre via `wfconfig`/UI; a sincronização para ficheiro é automática (§10) |
| Achar que `loginSec_requireAdminTwoFactor=0` prova que 2FA nunca é obrigatório | Essa chave é legada; sites migrados usam `required-2fa-role.<role>` dentro de `wfls_settings` | Verificar sempre `required-2fa-role.*` e as capabilities `wf2fa_*` do role antes de concluir (§6) |
| Assumir que `maxRequestsHumans`/`maxRequestsCrawlers`="DISABLED" significa "sem rate limiting nenhum" | Cada um dos 5 limitadores (§9) é independente — `maxGlobalRequests` pode estar activo e a aplicar-se a toda a gente mesmo com os outros 4 desligados | Verificar sempre os 5 pares `<limite>`/`<limite>_action`, não só um |
---
*Fonte: `CONFIG-Plugins-Referencia.md` §8 (Wordfence) + verificação
SQL/SSH ao vivo em `emanuelalmeida.pt` (16-08-2026, WP 7.0.4/PHP 8.2.31,
Wordfence 9.0.0 gratuito) — schema de `wfconfig` (313 chaves confirmadas
via `SELECT DISTINCT name`), contagem real de 2FA/passkeys,
`detectProxyRecommendation`, valores de scan e login security. **Expandido
nesta sessão** com leitura completa do código-fonte do plugin (466
ficheiros PHP, ~147k linhas) — `lib/wfConfig.php` (schema completo de
defaults, 3 grupos: `checkboxes`/`otherParams`/`defaultsOnly`),
`lib/wfJavascriptBridge.php` (labels humanos de todas as opções da UI),
`models/block/wfBlock.php` e `wfRateLimit.php` (tipos de bloqueio e os 5
limitadores de rate limiting), `models/firewall/wfFirewall.php` (modos e
scoring do WAF), `lib/wfDiagnostic.php` (secções de diagnóstico),
`lib/wfImportExportController.php` (mecanismo real de import/export via
token wordfence.com), `lib/wfCentralAPI.php` (integração Central) e o
módulo `modules/login-security/` completo — incluindo a descoberta de que
2FA/passkeys/CAPTCHA vivem numa tabela SQL separada (`wfls_settings`,
colunas `name`/`value`) e que o WAF mantém uma cópia de config em
ficheiros PHP serializados (`wp-content/wflogs/config*.php`), fora de
`wfconfig`. Confirmado ao vivo: `wfls_settings` completa, capabilities
`wf2fa_*`/`wfls_*` do role administrator, ficheiros WAF (`config.php`,
`config-synced.php`, `rules.php`), categorias de regra (`sqli`/`xss`/`rce`)
e ligação activa a Wordfence Central desde 27/02/2025.*
+492
View File
@@ -0,0 +1,492 @@
---
name: wp-activity-log
description: Gestão e auditoria do WP Activity Log (Melapress, slug wp-security-audit-log — rebranding do antigo "WP Security Audit Log") via WP-CLI/SQL em servidores CWP. Cobre consulta ao log de eventos (tabelas wsal_occurrences/wsal_metadata), replicação do padrão stealth mode + log restrito a um único utilizador, confirmação do gotcha "wp-cli bypassa restrições de UI", e gestão de retenção/volume do log. Usar quando "wp activity log", "wsal", "wp security audit log", "log de auditoria wordpress", "quem alterou isto no site", "histórico de acções wordpress", "esconder plugin de auditoria", "stealth mode plugin", "restringir log a um utilizador".
---
# /wp-activity-log — WP Activity Log (Melapress) via WP-CLI/SQL
Plugin de log de auditoria WordPress (equivalente a Syslog para WP). Nome
comercial mudou de "WP Security Audit Log" para **"WP Activity Log"**
(rebranding Melapress) mas o **slug mantém-se `wp-security-audit-log`** — usar
sempre este slug em `wp plugin get/activate/deactivate`, nunca o nome
comercial.
**Fonte:** `CONFIG-Plugins-Referencia.md` §9 + verificação SSH ao vivo nesta
sessão (`emanuelalmeida.pt`, versão `5.6.5`, prefixo de tabelas `wpne_`).
---
## Contexto CWP — sempre obrigatório
```bash
PATH=/home/USER/public_html
PREFIX=$(wp db prefix --allow-root --path=$PATH) # ex.: wpne_, wp_, wpah_
```
**Acesso via SSH:** `server.descomplicar.pt`, porta 9443. **Existe sim um
comando WP-CLI nativo** — corrige o que estava documentado antes: o plugin
regista `WP_CLI::add_command( 'wsal_cli_commands', ... )` em
`classes/class-wp-security-audit-log.php`. Ver §1.1 para a lista completa
de subcomandos. Fora desses 7 comandos, o resto passa por `wp option`,
`wp db query` e `wp eval`.
---
## 1. Identificação e configuração (18 opções `wp_options`)
Prefixos: `wsal_*` (config principal), `Wsal*` (raro), `fs_wsalp` (estado
Freemius). Ler tudo de uma vez:
```bash
wp option list --search='wsal*' --format=table --fields=option_name,option_value --allow-root --path=$PATH
wp option get fs_wsalp --allow-root --path=$PATH
wp plugin get wp-security-audit-log --format=json --allow-root --path=$PATH
```
### Tabela de configuração real (`emanuelalmeida.pt`, verificado ao vivo)
| Opção | Valor | O que faz |
|---|---|---|
| `wsal_restrict-log-viewer` | `only_me` | Só o utilizador em `only-me-user-id` vê o log no wp-admin |
| `wsal_restrict-plugin-settings` | `only_me` | Só esse utilizador altera a configuração do plugin |
| `wsal_only-me-user-id` | `17` | ID do utilizador autorizado — confirmar sempre com `wp user get <ID>` que é a conta de IT correcta, não um utilizador órfão/comprometido |
| `wsal_hide-plugin` | `yes` | **Stealth mode** — esconde o plugin da lista `wp-admin/plugins.php` (filtro `all_plugins`); um atacante com sessão de browser comprometida não sabe que o site está a ser auditado |
| `wsal_mwp-child-stealth-mode` | `yes` | Idem, mas para dashboards multisite externos (ManageWP e similares) |
| `wsal_disabled-alerts` | array de IDs (ver §3) | Tipos de evento que NÃO são registados |
| `wsal_plugin_version` | `5.6.5` | Versão instalada |
| `wsal_freemius_state` / `fs_wsalp` | `skipped` / `no` | Não activou licenciamento Freemius (versão free) |
| `wsal_built-in-notifications` | array serializado | Config de e-mail de resumo diário (`daily_summary_notification`, `daily_email_address`) |
| `wsal_notified_plugin_updates`, `wsal_notified_theme_updates`, `wsal_notified_wp_core_update` | arrays/strings | Cache interna de versões já notificadas — não é config de segurança, ignorar em auditorias |
| `wsal_feature-highlight-notice-show`, `wsal_upgrade-notice-show`, `wsal_notification-modal-dismissed`, `wsal_free-search-try` | flags de UI | Estado de notices dispensados — irrelevante para segurança |
### 1.1 Comandos WP-CLI nativos (`wp wsal_cli_commands <subcomando>`)
Confirmado em `classes/Controllers/WP_CLI/class-wp-cli-commands.php`
(registados desde a v4.6, `set_retention` inclusive). Todos aceitam
`--allow-root --path=$PATH`.
| Subcomando | Argumentos | O que faz |
|---|---|---|
| `remove_wizard` | nenhum | Marca o wizard de setup inicial como dispensado (`setup-modal-dismissed=true`) |
| `remove_daily_notification` | nenhum | Desactiva o resumo diário por e-mail (`built-in-notifications.daily_summary_notification=false`) — dispara evento `6310` |
| `remove_weekly_notification` | nenhum | Desactiva o resumo semanal por e-mail (`weekly_summary_notification=false`) — dispara evento `6319` |
| `disable_enable_alert` | `<alert_id>` | Activa/desactiva um `alert_id` específico (alternativa CLI ao array `wsal_disabled-alerts`) |
| `set_hide_plugin` | `<1\|true\|0\|false>` | Equivalente CLI a `wp option update wsal_hide-plugin` (stealth mode) |
| `login_page_notification` | `--enabled=<bool>` `--text=<html>` | Activa/desactiva o aviso "este site tem log de actividade" na página de login (`wsal_login-page-notification` + `wsal_login-page-notification-text`) — **não confirmado activo em nenhum site auditado**; texto aceita HTML |
| `set_retention` | `--enabled=<bool>` `--pruning-value=<int>` `--pruning-unit=<days\|months\|years>` | Configura a purga automática (pruning) — ver §6 |
```bash
wp wsal_cli_commands disable_enable_alert 1002 --allow-root --path=$PATH
wp wsal_cli_commands set_hide_plugin 1 --allow-root --path=$PATH
wp wsal_cli_commands set_retention --enabled=true --pruning-value=6 --pruning-unit=months --allow-root --path=$PATH
```
---
## 2. Consultar o log de eventos (`wsal_occurrences` + `wsal_metadata`)
O log real vive em **duas tabelas dedicadas** (não em `postmeta`/`options`):
```bash
wp db query "SHOW TABLES LIKE '%wsal%';" --allow-root --path=$PATH
```
### Estrutura de `${PREFIX}wsal_occurrences` (1 linha = 1 evento)
| Campo | Tipo | Nota |
|---|---|---|
| `id` | bigint PK | |
| `site_id` | bigint | Para multisite |
| `alert_id` | bigint | Tipo de evento (ver tabela §3) |
| `created_on` | double | Unix timestamp com microssegundos — usar `FROM_UNIXTIME(created_on)` |
| `client_ip` | varchar | IP de origem |
| `severity` | varchar | Código numérico (ver §3) |
| `object` | varchar | Categoria (`user`, `post`, `plugins`, `system`, ...) |
| `event_type` | varchar | `created`/`modified`/`deleted`/`login`/... |
| `username`, `user_id`, `user_roles` | | Quem fez a acção — `user_id=0` é sistema/anónimo |
| `session_id`, `user_agent` | | |
| `post_status`, `post_type`, `post_id` | | Preenchido só em eventos de conteúdo |
Detalhe extra (nome do plugin alterado, ficheiro, etc.) fica em
`${PREFIX}wsal_metadata` (`occurrence_id`, `name`, `value` — schema
key/value, `LEFT JOIN` por `occurrence_id`).
### Queries úteis
```bash
PREFIX=$(wp db prefix --allow-root --path=$PATH)
# Volume total + janela temporal coberta
wp db query "SELECT COUNT(*) total, FROM_UNIXTIME(MIN(created_on)) primeiro, FROM_UNIXTIME(MAX(created_on)) ultimo FROM ${PREFIX}wsal_occurrences;" --allow-root --path=$PATH
# Últimos 50 eventos de um utilizador específico
wp db query "SELECT FROM_UNIXTIME(created_on) quando, alert_id, object, event_type, client_ip FROM ${PREFIX}wsal_occurrences WHERE username='it' ORDER BY id DESC LIMIT 50;" --allow-root --path=$PATH
# Eventos num intervalo de datas
wp db query "SELECT FROM_UNIXTIME(created_on) quando, username, object, event_type FROM ${PREFIX}wsal_occurrences WHERE created_on BETWEEN UNIX_TIMESTAMP('2026-08-01') AND UNIX_TIMESTAMP('2026-08-16') ORDER BY id;" --allow-root --path=$PATH
# Filtrar por tipo de evento (alert_id) — ex.: falhas de login (1002)
wp db query "SELECT FROM_UNIXTIME(created_on) quando, username, client_ip FROM ${PREFIX}wsal_occurrences WHERE alert_id=1002 ORDER BY id DESC LIMIT 30;" --allow-root --path=$PATH
# Distribuição por tipo de evento (para ver o que domina o log)
wp db query "SELECT alert_id, COUNT(*) total FROM ${PREFIX}wsal_occurrences GROUP BY alert_id ORDER BY total DESC LIMIT 20;" --allow-root --path=$PATH
# Distribuição por severidade
wp db query "SELECT severity, COUNT(*) total FROM ${PREFIX}wsal_occurrences GROUP BY severity ORDER BY total DESC;" --allow-root --path=$PATH
# Ver metadados de um evento específico (ex.: qual plugin foi alterado)
wp db query "SELECT name, value FROM ${PREFIX}wsal_metadata WHERE occurrence_id=<ID>;" --allow-root --path=$PATH
```
---
## 3. Tipos de evento e severidade (dados reais observados, `emanuelalmeida.pt`)
`severity` (código Melapress padrão): `200`=informational, `250`=user
activity, `300`=notification, `400`=warning, `500`=critical.
Top `alert_id` observados em 60.337 eventos (~1,5 anos de histórico) — usar
como referência de "normal" ao auditar um site novo, não como lista
exaustiva (o WSAL tem 100+ tipos de evento definidos):
| `alert_id` | Ocorrências | Interpretação provável |
|---|---|---|
| `6070` | 26.476 | Verificação periódica de sistema (cron interno do WSAL) |
| `1003` | 20.276 | Actividade de sessão/login |
| `2008` | 3.202 | Conteúdo modificado (post/página) |
| `6066` | 2.615 | Verificação periódica de sistema |
| `6069` | 2.329 | Verificação periódica de sistema |
| `1002` | 1.965 | Login falhado |
| `1000` | 389 | Login com sucesso |
| `2053`/`2054` | 305/133 | Conteúdo criado/eliminado |
**Não confiar em interpretações de `alert_id` sem confirmar** contra a
documentação oficial Melapress (`https://melapress.com/support/kb/`) ou o
código do plugin (`wp-content/plugins/wp-security-audit-log/`) — os números
acima são inferência a partir do padrão de volume, não confirmados
individualmente nesta sessão.
### 3.1 Categorias de eventos (mapa completo, `defaults.php`)
Ficheiro central de definições: `defaults.php` (raiz do plugin, função
`set_wsal_alerts()`, ~3.760 linhas). Define **as categorias core** — os
eventos de integrações de terceiros (WooCommerce, Yoast, ACF, Rank Math,
etc.) vivem em ficheiros separados (ver §3.2). Categorias core, por ordem
de aparição no ficheiro (nome + intervalo aproximado de `alert_id`):
| Categoria (grupo topo) | Subcategorias | Intervalo `alert_id` | Conteúdo |
|---|---|---|---|
| Users Logins & Sessions Events | User Activity | `1000`–`1099` | Login/logout, falhas de login, sessões, alteração de password |
| Content & Comments | Content, Categories, Custom Fields, Custom Fields (ACF), Comments, Widgets, Menus, Custom Post Types, Pages | `2000`–`2999` | Posts/páginas criados/editados/eliminados, categorias, comentários, widgets, menus, custom post types, ACF |
| User Accounts | User Profiles, Multisite User Profiles | `4000`–`4999` | Utilizadores criados/editados/eliminados, mudança de role, password reset |
| Plugins & Themes | Plugins, Themes, Themes on Multisite | `5000`–`5599` | Instalação/activação/desactivação/actualização de plugins e temas |
| Activity Logs | Activity log plugin | `6000`–`6099`, `6300`–`6399` | Config do próprio WSAL alterada (ex.: `6310`/`6311`/`6312`/`6319` já usados pelos comandos WP-CLI de notificação — ver §1.1); também cobre eventos de "Notifications & Integrations" (Slack/SMS/e-mail custom) |
| WordPress & System | System | `6000`–`6099` | Cron interno do WSAL, verificações periódicas (os IDs `6066`/`6069`/`6070` de alto volume observados em §3 pertencem aqui) |
| — | WordPress Site Settings | `4700`–`4799` (aprox.) | Alteração de opções core do WP (site title, timezone, permalinks, etc.) |
| — | Email Events | `8800`–`8899` (aprox.) | Falhas de envio de e-mail via `wp_mail` |
| — | Database Events | `9000`–`9099` (aprox.) | Erros de query, updates de schema da BD |
**Nota:** as gamas de `alert_id` acima são aproximadas (extraídas da ordem
de definição em `defaults.php`, não documentadas oficialmente pela
Melapress como ranges fixos) — para confirmar a categoria exacta de um
`alert_id` específico, o método fiável é `grep -A6 "^\s*$ALERT_ID,$"
defaults.php` no código-fonte do servidor.
### 3.2 Alertas de integrações de terceiros (activados só se o plugin correspondente estiver presente)
Ficheiros em `classes/WPSensors/Alerts/` (18 ficheiros, um por integração
suportada) — cada um define o próprio bloco de `alert_id`, só carregado se
o plugin alvo estiver activo no site:
| Ficheiro | Integração | Volume aprox. de alertas |
|---|---|---|
| `class-woocommerce-custom-alerts.php` | WooCommerce | ~417 linhas de definição (produtos, encomendas, cupões, definições da loja) |
| `class-yoast-custom-alerts.php` | Yoast SEO | Meta SEO, sitemaps, redirects |
| `class-rank-math-custom-alerts.php` | Rank Math | Meta SEO, schema, redirects |
| `class-acf-custom-alerts.php` | Advanced Custom Fields | Grupos de campos, campos criados/editados |
| `class-gravity-forms-custom-alerts.php` | Gravity Forms | Formulários, entradas, notificações |
| `class-wpforms-custom-alerts.php` | WPForms | Formulários, entradas |
| `class-wp-2fa-custom-alerts.php` | WP 2FA (Melapress) | Activação/desactivação de 2FA por utilizador |
| `class-memberpress-custom-alerts.php` | MemberPress | Subscrições, membros |
| `class-paid-memberships-pro-custom-alerts.php` | Paid Memberships Pro | Níveis, membros |
| `class-learndash-custom-alerts.php` | LearnDash | Cursos, inscrições |
| `class-bbpress-custom-alerts.php` | bbPress | Fóruns, tópicos |
| `class-tablepress-custom-alerts.php` | TablePress | Tabelas |
| `class-redirection-custom-alerts.php` | Redirection | Regras de redirect |
| `class-termly-custom-alerts.php` | Termly | Consentimento de cookies |
| `class-ai-wp-plugin-alerts.php` | AI (Melapress AI features) | Uso de funcionalidades AI do próprio WSAL |
| `class-multisite-custom-alerts.php` | WordPress Multisite | Criação/eliminação de sites na rede |
| `class-mainwp-server-custom-alerts.php` | MainWP (servidor gerido) | Propagação de definições via dashboard MainWP — ver aviso em `defaults.php:2593` ("definições do WSAL foram sobrepostas a partir do MainWP") |
Confirmar quais estão activos num site: `wp plugin list --status=active
--allow-root --path=$PATH` e cruzar com a tabela acima — só os que
correspondem a um plugin activo geram eventos.
---
## 4. Replicar o padrão de segurança num site novo (checklist)
Baseado na configuração real confirmada em `emanuelalmeida.pt` (avaliada
como "acima da média"):
```bash
PATH=/home/USER/public_html
# 1. Instalar/activar (slug correcto — nome comercial mudou, slug não)
wp plugin install wp-security-audit-log --activate --allow-root --path=$PATH
# 2. Identificar o ID do utilizador de IT/admin de confiança (NÃO um admin genérico partilhado)
wp user list --role=administrator --fields=ID,user_login,user_email --allow-root --path=$PATH
# 3. Restringir visualização do log e config a esse utilizador
wp option update wsal_restrict-log-viewer only_me --allow-root --path=$PATH
wp option update wsal_restrict-plugin-settings only_me --allow-root --path=$PATH
wp option update wsal_only-me-user-id <ID_do_utilizador_IT> --allow-root --path=$PATH
# 4. Activar stealth mode — esconder o plugin da lista wp-admin
wp option update wsal_hide-plugin yes --allow-root --path=$PATH
wp option update wsal_mwp-child-stealth-mode yes --allow-root --path=$PATH
# 5. Confirmar (ler de volta, nunca confiar só no exit code)
wp option get wsal_restrict-log-viewer --allow-root --path=$PATH
wp option get wsal_only-me-user-id --allow-root --path=$PATH
wp user get <ID_do_utilizador_IT> --field=user_login --allow-root --path=$PATH
```
Não mexer em `wsal_disabled-alerts` por defeito — deixar todos os ~900+
tipos de evento activos (contando módulos de terceiros — ver §3.1). O padrão
observado de apenas 13 IDs desligados, todos de baixo valor informativo, é a
postura recomendada; desligar mais do que isso reduz cobertura de auditoria
sem ganho real.
---
## 5. GOTCHA CRÍTICO — stealth mode e "only_me" não protegem contra WP-CLI/SQL
**Confirmado nesta sessão por teste directo:** apesar de `hide-plugin=yes`
esconder o plugin da lista em `wp-admin/plugins.php` (via hook PHP no ecrã
de admin), o plugin continua **totalmente visível e operável via WP-CLI**:
```bash
wp plugin list --allow-root --path=$PATH | grep security-audit
# → wp-security-audit-log,active,none,5.6.5,,on (aparece normalmente)
wp plugin status wp-security-audit-log --allow-root --path=$PATH
# → Status: Active (nenhuma restrição aplicada)
```
**Implicação de segurança real:** o padrão "esconder + restringir a
`only_me`" protege apenas contra um atacante que compromete uma **sessão de
browser wp-admin** de um utilizador administrador genérico (não vê o plugin
na UI, não consegue desactivá-lo nem ler o log pela interface). **Não
protege** contra:
- Acesso SSH/WP-CLI ao servidor (qualquer comando `wp plugin deactivate`,
`wp option update wsal_hide-plugin no`, ou `wp db query "DELETE FROM
wsal_occurrences"` funciona sem qualquer verificação de `only-me-user-id`);
- Acesso directo à base de dados (phpMyAdmin, mysql CLI);
- Um plugin malicioso que corra `update_option()`/`deactivate_plugins()` em
PHP no mesmo processo WordPress (o hook de stealth só filtra a
*renderização* da lista, não a capability real).
Estas restrições são **apenas de UI**, implementadas por filtros como
`all_plugins`/menu — não são um controlo de acesso ao nível dos dados. Ao
avaliar a robustez do log de auditoria de um site, tratar sempre acesso
SSH/WP-CLI e acesso à BD como "full trust" que ignora completamente esta
protecção — a defesa real contra esses vectores é a segurança do próprio
servidor (SSH keys, `mysql` sem exposição externa), não a config do plugin.
---
## 6. Retenção, arquivo e volume do log
O plugin **não faz purga agressiva por defeito** — confirmado:
`emanuelalmeida.pt` tinha 60.337 eventos cobrindo Mar/2025 a Ago/2026 (~1,5
anos) sem gaps aparentes, ocupando `~13 MB` (`wsal_occurrences`) + `~50 MB`
(`wsal_metadata`, guarda os detalhes de cada evento — cresce mais rápido
que a tabela principal).
```bash
# Verificar tamanho actual antes de decidir sobre arquivo/purga
wp db query "SELECT table_name, ROUND(((data_length+index_length)/1024/1024),2) size_mb, table_rows FROM information_schema.TABLES WHERE table_schema=DATABASE() AND table_name LIKE '%wsal%';" --allow-root --path=$PATH
```
### 6.1 Purga automática (pruning) — `wsal_pruning-*`, classe `Plugin_Settings_Helper`/`Settings_Helper`
Ao contrário do que a auditoria pontual anterior sugeria, a purga
automática **não é exclusiva da versão paga** — a lógica e as opções vivem
no core (`classes/Helpers/class-plugin-settings-helper.php` +
`class-settings-helper.php`) e há um comando WP-CLI dedicado (`set_retention`,
ver §1.1). Confirmado nesta expansão: nenhuma destas opções está definida em
`emanuelalmeida.pt` (usa os defaults do código, purga desligada).
| Opção | Default no código | O que faz |
|---|---|---|
| `wsal_pruning-date-e` | `false` (desligado) | Activa/desactiva a purga automática por data |
| `wsal_pruning-date` | calculado por `get_default_pruning_date()` | Data-limite (`strtotime`-compatible, ex. `"6 months"`) — eventos mais antigos são apagados no próximo cron |
| `wsal_pruning-unit` | `months` | Unidade usada no picker da UI (`days`/`months`/`years`) |
| `wsal_pruning-limit-e` | não confirmado | Activa purga por **limite de contagem** (em vez de por data) |
| `wsal_pruning-limit` | `1` (mínimo) | Nº máximo de eventos a manter, quando `pruning-limit-e=true` |
| `wsal_delete-data` | `false` | Se `true`, ao desinstalar o plugin todos os dados (`wsal_occurrences`/`wsal_metadata`/opções) são apagados; se `false`, ficam órfãos na BD |
```bash
# Ler estado actual de purga
wp option list --search='wsal_pruning*' --format=table --allow-root --path=$PATH
# Activar purga a 6 meses via WP-CLI (equivalente ao form da UI)
wp wsal_cli_commands set_retention --enabled=true --pruning-value=6 --pruning-unit=months --allow-root --path=$PATH
wp option get wsal_pruning-date-e --allow-root --path=$PATH
wp option get wsal_pruning-date --allow-root --path=$PATH
```
### 6.2 Arquivo (archiving) para BD externa — `wsal_archiving-*`
Funcionalidade distinta da purga: em vez de apagar, **move** eventos
antigos para uma segunda ligação de base de dados (`Archive_Records::archive()`
em `classes/Entities/Archive/class-archive-records.php`) via cron. Requer
configurar previamente uma "connection" de arquivo (ver §6.3) — sem
connection definida, `is_archiving_set_and_enabled()` devolve `false` e o
cron não corre, mesmo com `archiving-e=yes`.
| Opção | O que faz |
|---|---|
| `wsal_archiving-e` | Liga/desliga o arquivo automático |
| `wsal_archiving-date` + `wsal_archiving-date-type` (`days`/`months`/`years`, default `1`/`months`) | Antiguidade a partir da qual um evento é arquivado |
| `wsal_archiving-run-every` | Frequência do cron de arquivo (default `hourly`) |
| `wsal_archiving-stop` | Pausa temporária do arquivo sem desligar a config |
| `wsal_archiving-cron-started` | Flag interna — cron já agendado |
```bash
wp option list --search='wsal_archiving*' --format=table --allow-root --path=$PATH
```
**Não confirmado activo em nenhum site auditado** (`emanuelalmeida.pt` não
tem nenhuma destas opções definidas).
### 6.3 Espelhamento (mirroring) para BD/serviço externo — `wsal_wsal-mirror-*`
Envia uma cópia em tempo real de cada evento para uma ligação externa
(outra tabela MySQL, ou syslog/SIEM de terceiros via plugin de mirror
dedicado — **o WSAL não tem integração nativa de syslog/SIEM no core**, só
o mecanismo genérico de "connections" reutilizado por mirror e archiving).
`Settings_Helper::get_all_mirrors()` lê todas as opções com prefixo
`WSAL_MIRROR_PREFIX` (`wsal-mirror-`); cada uma é um array serializado
(`connection`, filtros de `alert_id`, etc.).
```bash
wp option list --search='wsal-mirror-*' --format=table --allow-root --path=$PATH
wp option list --search='wsal_connection-*' --format=table --allow-root --path=$PATH
```
Se `database_logging_enabled` for `false` E existir pelo menos um mirror
configurado, o WSAL passa a **não gravar mais em `wsal_occurrences` local**
— todo o log vai só para o(s) destino(s) espelhados. Confirmar sempre
`Settings_Helper::is_database_logging_enabled()` (via `wp eval`) antes de
assumir que as queries SQL do §2 devolvem o histórico completo num site com
mirror activo.
**Nota operacional (aplica-se a §6.1–§6.3):** a ausência de purga é boa
para auditoria (histórico completo disponível), mas em sites com muita
actividade o crescimento é linear e sem limite. Se `wsal_metadata`
ultrapassar dimensões incómodas (centenas de MB), considerar:
- Activar `wsal_pruning-date-e` com uma janela realista (6–12 meses) via
`set_retention`;
- Arquivar eventos antigos para uma tabela `_archive` antes de apagar
(nunca `DELETE` directo sem backup — perde-se rasto de auditoria);
- Configurar uma connection de arquivo (§6.2) se o volume justificar
automação em vez de purga manual.
Não foi necessário agir em `emanuelalmeida.pt` nesta sessão — volume actual
(~63 MB total) é irrelevante para performance.
---
## 7. Notificações, relatórios e integrações
### 7.1 Notificações incorporadas (resumo diário/semanal por e-mail — gratuito)
Opção `wsal_built-in-notifications` (array serializado, constante
`Notifications::BUILT_IN_NOTIFICATIONS_SETTINGS_NAME`). Confirmado em
`emanuelalmeida.pt`: só contém config de resumo diário
(`daily_summary_notification`, `daily_email_address`); a v5.3+ acrescentou
resumo **semanal** (`weekly_summary_notification`, evento `6319` ao
desactivar — ver §1.1 `remove_weekly_notification`).
```bash
wp option get wsal_built-in-notifications --format=json --allow-root --path=$PATH
```
### 7.2 Notificações customizadas (regra "se `alert_id` X ocorrer, notifica Y") — Premium
Opção `wsal_custom-notifications` (constante
`CUSTOM_NOTIFICATIONS_SETTINGS_NAME`) — permite regras condicionais por
utilizador/IP/tipo de evento, com entrega por e-mail, **Slack**
(`classes/Controllers/slack/`) ou **SMS via Twilio**
(`classes/Controllers/twilio/`). Não confirmado activo em nenhum site
auditado; `wsal_notifications` (nota: nome sem `custom-`, é outra opção)
só guarda config global de encurtamento de URLs (`shorten_notification_urls`,
`notification_bitly_shorten_key`) — confirmado vazio/desligado em
`emanuelalmeida.pt`.
```bash
wp option get wsal_custom-notifications --format=json --allow-root --path=$PATH
wp option get wsal_notifications --format=json --allow-root --path=$PATH
```
### 7.3 Exportação e relatórios
- **CSV** — gratuito, core. Acção AJAX `wsal_export_csv_results`
(`classes/Writers/class-csv-writer.php`), disponível no ecrã do log em
wp-admin (botão "Export"). Não há comando WP-CLI dedicado — a exportação
é sempre via browser/AJAX; para extrair dados por CLI usar as queries SQL
do §2 e `wp db query ... --format=csv` como alternativa equivalente.
- **Relatórios agendados** (geração automática + entrega por e-mail,
white-label) — feature **Premium** (`classes/Views/class-premium-features.php`,
`AuditLog.php:470` mostra link de upsell "Get scheduled reports... with
Premium" nos eventos de tipo `2000`/`2001`/`2100`/`4000`). Não existe em
wp-options nem endpoint WP-CLI na versão gratuita.
### 7.4 SIEM/Syslog externo
**Não existe integração nativa de Syslog/SIEM no core do plugin** —
confirmado por ausência de qualquer classe/ficheiro com `syslog`/`SIEM` no
código-fonte (`grep -rl` devolveu zero resultados). A única via de exportar
eventos em tempo real para um sistema externo é o mecanismo de
**espelhamento (mirroring)** descrito em §6.3, que só suporta ligações
configuradas como "connections" do próprio plugin (outra BD MySQL) — não um
protocolo Syslog/CEF nativo. Para integração SIEM real, a Melapress
documenta um add-on comercial separado; confirmar sempre no site do
fornecedor antes de assumir que existe suporte nativo.
---
## 8. Erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| `wp plugin activate wp-activity-log` falha ("plugin not found") | Usou o nome comercial em vez do slug | Slug é sempre `wp-security-audit-log`, independentemente do rebranding |
| Query em `wsal_occurrences` devolve datas erradas | `created_on` é Unix timestamp `double` (com microssegundos), não `DATETIME` | Usar sempre `FROM_UNIXTIME(created_on)` |
| Achar que o plugin "não está lá" num site com `hide-plugin=yes` | Confundir stealth mode de UI com ausência real | Confirmar sempre via `wp plugin list`/`wp plugin status` — o WP-CLI nunca respeita esta flag (ver §5) |
| `only-me-user-id` aponta para um ID que já não existe/foi desactivado | Utilizador de IT mudou de conta e a opção não foi actualizada | Confirmar sempre com `wp user get <ID>` antes de assumir que a restrição é válida — um ID órfão equivale a log inacessível a todos via UI |
| Achar `wsal_disabled-alerts` vazio = tudo activo | O array serializado inclui sempre um `s:0:""` inicial (artefacto de serialização PHP), não é um alerta desligado real | Ignorar a primeira entrada vazia ao contar IDs desligados |
| Achar que "não há WP-CLI para este plugin" | Confusão histórica — o WSAL regista `wp wsal_cli_commands` desde a v4.6 | Usar `wp cli cmd-dump \| grep wsal` para confirmar; ver §1.1 |
| Achar que purga automática (`pruning`) é só Premium | `set_retention`/`wsal_pruning-*` são core, disponíveis na versão gratuita | Confirmar sempre no código (`class-plugin-settings-helper.php`) antes de recomendar upgrade pago só para ligar retenção |
| Confundir `wsal_notifications` com `wsal_custom-notifications` | Nomes parecidos, funções diferentes — a primeira é só config de encurtamento de URL, a segunda são as regras de alerta customizado (Premium) | Ler sempre as duas e não assumir que uma vazia implica a outra desligada |
| Assumir que o log SQL (§2) tem o histórico completo num site com mirroring activo | Se `database_logging_enabled=false` com mirror configurado, o WSAL deixa de escrever localmente | Confirmar `Settings_Helper::is_database_logging_enabled()` via `wp eval` antes de tirar conclusões de volume/gaps |
---
**Fonte:** `CONFIG-Plugins-Referencia.md` §9 (WP Security Audit Log / WP
Activity Log) + verificação SSH ao vivo nesta sessão (`emanuelalmeida.pt`,
opções `wsal_*`, estrutura e contagem de `wsal_occurrences`/`wsal_metadata`,
teste de bypass `wp plugin list`) + **leitura completa do código-fonte do
plugin nesta expansão** (`wp-content/plugins/wp-security-audit-log/`,
versão `5.6.5`, ~99.000 linhas PHP): `defaults.php` (definições de todos os
eventos core, função `set_wsal_alerts()`), `classes/WPSensors/Alerts/*`
(18 integrações de terceiros), `classes/Controllers/WP_CLI/class-wp-cli-commands.php`
(comandos WP-CLI nativos), `classes/Helpers/class-settings-helper.php` +
`class-plugin-settings-helper.php` (todas as opções `wsal_*` — pruning,
archiving, mirroring, notificações), `classes/Entities/Archive/class-archive-records.php`
(mecanismo de arquivo), `classes/notification/*` (notificações
incorporadas e customizadas, Slack/Twilio), `classes/Views/class-premium-features.php`
e `AuditLog.php` (features gated como Premium: relatórios agendados,
notificações custom, mirroring/archiving).
+155 -3
View File
@@ -1,14 +1,13 @@
---
name: wp-cli
description: Gestão WordPress via WP-CLI no servidor CWP. Cobre core, plugins, temas, base de dados, utilizadores, migração, search-replace e automação com suporte multi-PHP.
layer: wiki
---
# /wp-cli - Gestão WordPress via WP-CLI no CWP
Operações WordPress via linha de comandos no servidor CWP (server.descomplicar.pt). Cobre core, plugins, temas, base de dados, utilizadores, migração e automação.
**Servidor:** server.descomplicar.pt (176.9.3.158) | porta SSH: 9443
**Servidor:** server.descomplicar.pt | porta SSH: 9443
**Fonte:** `Hub/06-Operacoes/Documentacao/Manuais/WP-CLI no CWP.md`
**NotebookLM:** [WordPress Config CLI](https://notebooklm.google.com/notebook/fb2f26bd-8cb0-4d4c-bafc-4f1ebb51c51d)
@@ -182,4 +181,157 @@ wp user update USERNAME --user_pass=NOVA_SENHA --allow-root --path=$PATH
wp user set-role USERNAME administrator --allow-root --path=$PATH
# Remover utilizador (transferindo conteúdo)
wp user delete USERNAME
wp user delete USERNAME --reassign=1 --allow-root --path=$PATH
# Actualizar email admin
wp option update admin_email "novo@email.pt" --allow-root --path=$PATH
```
---
## Cache e Transientes
```bash
# Limpar object cache (Redis/Memcached)
wp cache flush --allow-root --path=$PATH
# Apagar todos os transientes
wp transient delete --all --allow-root --path=$PATH
# Regenerar rewrite rules
wp rewrite flush --allow-root --path=$PATH
# Sequência completa pós-alteração
wp cache flush --allow-root --path=$PATH && \
wp transient delete --all --allow-root --path=$PATH && \
wp rewrite flush --allow-root --path=$PATH
```
---
## Modo Manutenção
```bash
# Activar modo manutenção
wp maintenance-mode activate --allow-root --path=$PATH
# Verificar estado
wp maintenance-mode status --allow-root --path=$PATH
# Desactivar
wp maintenance-mode deactivate --allow-root --path=$PATH
```
---
## Cron WordPress
```bash
# Listar eventos cron
wp cron event list --allow-root --path=$PATH
# Executar evento manualmente
wp cron event run HOOK_NAME --allow-root --path=$PATH
# Ver schedules disponíveis
wp cron schedule list --allow-root --path=$PATH
# Apagar evento
wp cron event delete HOOK_NAME --allow-root --path=$PATH
```
---
## Operações em Massa (Todos os Sites)
```bash
# Encontrar todas as instalações WordPress
find /home -name wp-config.php -not -path "*/backup*" 2>/dev/null | while read config; do
dir=$(dirname "$config")
user=$(stat -c '%U' "$dir")
echo "=== $user: $dir ==="
done
# Actualizar todos os sites (com PHP auto-detect)
find /home -name wp-config.php -not -path "*/backup*" 2>/dev/null | while read config; do
dir=$(dirname "$config")
user=$(stat -c '%U' "$dir")
echo "--- Actualizando $user ---"
php_fpm=$(ps aux | grep "php-fpm: pool ${user}" | grep -v grep | head -1 | grep -o 'php-fpm[0-9][0-9]' | head -1)
php_ver=${php_fpm:-php-fpm82}
/opt/alt/${php_ver}/usr/bin/php /usr/local/bin/wp core update --allow-root --path="$dir" 2>/dev/null
/opt/alt/${php_ver}/usr/bin/php /usr/local/bin/wp plugin update --all --allow-root --path="$dir" 2>/dev/null
done
```
---
## Erros Comuns e Soluções
| Erro | Causa | Solução |
|------|-------|---------|
| `Operation not permitted` | Sem `--allow-root` | Adicionar `--allow-root` |
| `PHP Fatal error: memory` | Limite memória | `define('WP_MEMORY_LIMIT','256M')` no wp-config.php |
| `max_execution_time exceeded` | Timeout em ops longas | `--exec-time=300` ou php.ini |
| `Error establishing database connection` | BD em baixo | `wp db check --allow-root` |
| `Warning: Could not get lock` | Outro processo activo | `wp cli cache clear --allow-root` |
| PHP version wrong | Site usa PHP diferente | `ps aux \| grep "pool USER"` para detectar |
| `wp elementor flush_css` apaga CSS de páginas já OK | O comando limpa **todo** o directório `uploads/elementor/css/`, não só a página-alvo — qualquer página não revisitada fica com `post-N.css` 404/HTML até ser visitada de novo | Depois de qualquer flush, fazer loop `curl` por **todos** os permalinks publicados (`wp post list --field=url`) para forçar regeneração; confirmar `content-type: text/css` em cada `post-N.css`; purgar Cloudflare duas vezes se persistir (origem pode ficar um ciclo atrás do edge) |
| `wp option update NOME --format=json` parte o site (fatal `json_decode(): array given`) | Alguns plugins guardam a config como **string JSON literal** em `wp_options` (o próprio plugin faz `json_decode()` sobre ela). `--format=json` faz o WP-CLI decodificar o input e gravar o **array PHP resultante** (serializado), não a string — o plugin recebe um array onde esperava string e crasha no bootstrap, ANTES de qualquer comando conseguir corrigir (inclui `wp cache flush`, que também precisa de arrancar o WP) | Verificar sempre `wp option get NOME --format=json` primeiro: resultado entre aspas escapadas `"{...}"` = string (editar com `wp db query "UPDATE wp_options SET option_value='...' WHERE option_name='NOME'"`, nunca `--format=json`); resultado sem aspas exteriores = já é array. Se ficar preso (site em baixo, `wp cache flush` também crasha) e o site tiver Redis/Memcached object cache, o valor corrompido fica cacheado e bloqueia até o WP arrancar — limpar directo com `redis-cli -n DB flushdb` (não precisa de WP a correr) antes de repetir a correcção |
---
## Diagnóstico Rápido
```bash
# Informações completas do ambiente
wp --info --allow-root --path=$PATH
# Verificar conectividade BD
wp db check --allow-root --path=$PATH
# Ver configuração PHP activa
wp eval 'echo PHP_VERSION."\n".php_ini_loaded_file()."\n".ini_get("memory_limit")."\n";' --allow-root --path=$PATH
# Ver URL e email admin
wp option get siteurl --allow-root --path=$PATH
wp option get admin_email --allow-root --path=$PATH
# Listar utilizadores admin
wp user list --role=administrator --allow-root --path=$PATH
```
---
## Recursos Adicionais
### Scripts
- **`scripts/wp-auto.sh`** — Auto-detecta PHP do utilizador e executa WP-CLI com a versão correcta
### Referências
- **`references/commands.md`** — Referência completa: todos os comandos com flags, WooCommerce CLI, options, wp eval avançado
### Documentação Hub
- **Manual:** `Hub/06-Operacoes/Documentacao/Manuais/WP-CLI no CWP.md`
- **NotebookLM:** [WordPress Config CLI](https://notebooklm.google.com/notebook/fb2f26bd-8cb0-4d4c-bafc-4f1ebb51c51d)
- **Skill relacionada:** `/rank-math` — operações Rank Math SEO via WP-CLI
---
*WP-CLI no CWP | Descomplicar® | v1.0.0 | 18-02-2026*
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"2026-08-16","issue":"wp option update WpFastestCache --format=json gravou array PHP serializado em vez de string JSON; o proprio plugin faz json_decode() sobre o valor e partiu com TypeError fatal no bootstrap do WP, derrubando o site (emanuelalmeida.pt); Redis object cache tinha o valor corrompido cacheado, bloqueando ate wp cache flush (que tambem precisa arrancar o WP)","fix":"nunca usar wp option update --format=json para opcoes que o plugin le como string JSON bruta; editar via wp db query UPDATE wp_options SET option_value=... (sempre string literal, nunca decodificado); se object cache (Redis/Memcached) tiver o valor mau cacheado e qualquer comando wp-cli crashar, limpar directo via redis-cli -n DB flushdb (bypassa o bootstrap do WP por completo) antes de tentar de novo","source":"auto"}
{"date":"2026-08-16","issue":"wp elementor flush_css limpa TODO o directorio uploads/elementor/css/, nao so a pagina visada; um flush pontual para corrigir 1 pagina (post-9.css 404) reintroduziu 404 noutra pagina ja corrigida antes na mesma sessao (post-6.css)","fix":"apos qualquer flush_css, loop curl por TODOS os permalinks publicados (wp post list --field=url) para regenerar tudo; verificar content-type text/css (nao text/html) em cada ficheiro; purgar Cloudflare 2x se persistir 404 no edge apos origem ja estar OK","source":"auto"}
```
*Adicionar nova linha após cada erro corrigido.*
@@ -1,6 +1,6 @@
# WP-CLI no CWP — Referência Completa de Comandos
**Servidor:** server.descomplicar.pt (176.9.3.158) | porta 9443
**Servidor:** server.descomplicar.pt | porta 9443
**PHP padrão:** `/opt/alt/php-fpm83/usr/bin/php`
---
@@ -0,0 +1,113 @@
---
name: wp-content-seo-gate
description: "Regras e padrões obrigatórios para geração de conteúdo WordPress com RankMath Pro. INVOCAR AUTOMATICAMENTE quando: (1) escrevendo ou revendo prompts que gerem artigos, posts, descrições de podcast ou qualquer texto para publicar em WordPress, (2) criando, modificando ou auditando sistemas/pipelines/scripts que publicam conteúdo automaticamente em WordPress (publishers, crons, geradores), (3) gerando directamente conteúdo para posts WP. Previne os erros RankMath mais comuns: slug sem focus keyword, título sem número, keyword density baixa, meta description fora do limite."
---
# WP Content SEO Gate
Aplicar ANTES de escrever prompts de geração e ANTES de implementar qualquer sistema que publique em WordPress.
## Regras Obrigatórias para Prompts de Geração
Todo o prompt que instruir um LLM a gerar conteúdo WP DEVE incluir estas regras literais:
| Campo | Regra a incluir no prompt |
|-------|--------------------------|
| `focus_keyword` | 2-5 palavras, foco no benefício/problema, não no nome da ferramenta |
| `slug` | Começar com palavras da focus_keyword (hifenado, sem acentos, ≤75 chars) |
| `rank_math_title` | 50-60 chars, contém focus_keyword E um número (ano corrente ou dado concreto: "47%", "3x") |
| `rank_math_description` | 140-160 chars, focus_keyword nos primeiros 120 chars |
| `content` | focus_keyword nos primeiros 200 chars + ≥2x no corpo + ≥1 H2/H3 com a keyword |
| `image_alt` | 40-100 chars descritivo, contém focus_keyword |
| `secondary_keywords` | 3-5 keywords complementares, cada uma ≥1x no content |
**Checklist mental no prompt (incluir literalmente):**
- [ ] focus_keyword aparece em: title, rank_math_title, rank_math_description, slug, primeiros 200 chars, ≥1 H2, ≥2x no corpo
- [ ] rank_math_title contém um número (ano ou dado concreto)
- [ ] slug começa com as palavras da focus_keyword
- [ ] ≥600 palavras (RankMath penaliza abaixo deste limite)
## Padrões Obrigatórios em Publishers/Pipelines
Todo o sistema que publica em WP via código DEVE implementar estes dois auto-fixes **depois** de receber o JSON do LLM e **antes** de publicar:
### Auto-fix 1 — Slug sem focus keyword
```python
def slugify(text: str) -> str:
text = unicodedata.normalize("NFD", text.lower())
text = "".join(c for c in text if unicodedata.category(c) != "Mn")
text = re.sub(r'[^a-z0-9\s-]', '', text)
return re.sub(r'[-\s]+', '-', text.strip()).rstrip('-')
def has_keyword_in_slug(slug: str, fk: str) -> bool:
# Match sem stopwords e sem acentos
stopwords = {"a","o","as","os","de","da","do","das","dos","e","em","no","na","nos","nas","para","por","pelo","pela"}
norm = lambda s: " ".join(t for t in slugify(s).replace("-"," ").split() if t not in stopwords)
return norm(fk) in norm(slug)
# Aplicar após generate_article():
if not has_keyword_in_slug(article['slug'], article['focus_keyword']):
fk_slug = slugify(article['focus_keyword'])
orig_words = [w for w in article['slug'].split('-') if w not in fk_slug.split('-') and len(w) > 3][:2]
article['slug'] = (fk_slug + ('-' + '-'.join(orig_words) if orig_words else ''))[:75].rstrip('-')
```
### Auto-fix 2 — Título sem número (RankMath Title Readability)
```python
import time, re
if not re.search(r'\d', article.get('rank_math_title', '')):
year = time.strftime('%Y')
t = article['rank_math_title'].rstrip()
article['rank_math_title'] = (t[:55].rstrip() + f' {year}') if len(t) > 55 else f'{t} {year}'
```
## Checklist de Validação Pré-publicação
Verificar em qualquer publisher antes do `wp post create`:
- [ ] slug contém focus_keyword (sem acentos, hifenado)
- [ ] rank_math_title tem ≥1 dígito e ≤60 chars
- [ ] rank_math_description entre 140-160 chars e com focus_keyword
- [ ] image_alt contém focus_keyword
- [ ] content ≥600 palavras
- [ ] focus_keyword nos primeiros 200 chars do content
- [ ] ≥1 H2 com a focus_keyword
- [ ] Link externo à fonte (`rel="noopener"`)
- [ ] 1-3 links internos descomplicar.pt no corpo
- [ ] Artigos da categoria **Biblioteca**: terminam sempre com secção FAQ (formato exacto na Secção 10 do PROC — não inventar outro formato, o accordion do site só reconhece formatos já mapeados)
- [ ] `content` nunca vazio — confirmar corpo real, não só os campos RankMath (já aconteceu publicar posts com meta completa e corpo vazio)
- [ ] `post_author` definido pela categoria do conteúdo, não deixado no autor por omissão (mapa categoria→autor na Secção 11 do PROC — Notícias=it, Redes Sociais/Design=juju, Tecnologia=f35, IA=maisum, Gestão/Estratégia/E-commerce=ealmeida, resto da Biblioteca=info)
## Erros RankMath mais comuns
| Erro no painel | Causa | Fix |
|----------------|-------|-----|
| "Focus Keyword not found in URL" | LLM gera slug sem a FK | Auto-fix 1 obrigatório |
| "Title doesn't contain a number" | Título sem ano/dado | Auto-fix 2 + instruir LLM |
| "FK not in first 10%" | Lead não inclui a FK | Regra de prompt: FK nos primeiros 200 chars |
| "Keyword density too low/high" | <1% ou >3% | Verificar count antes de publicar |
| SEO Score N/A no dashboard | Post criado via CLI sem Gutenberg | Usar cálculo server-side de `/seo-post` Passo 6 |
| Schema ausente no HTML | Falta `rank_math_schema_*` | Ver `/seo-post` Modo aplicar Passo 3 |
## PROC Canónico
**Ler antes de agir:** `Hub/06-Operacoes/Procedimentos/D7-Tecnologia/WordPress/PROC-Conteudo-RankMath.md` (D7-WP-RM-001)
Este PROC é a fonte única de verdade para:
- Estrutura obrigatória do conteúdo (headings, distribuição FK, elementos por tipo)
- Tabela completa de campos RankMath com valores aceitáveis
- Comandos WP-CLI exactos para aplicar meta, schema e calcular score
- Checklist de validação final (9 pontos)
- Tabela de erros comuns e resolução
Quando o detalhe de uma regra não está nesta skill, está no PROC.
## Referências Adicionais
- `/seo-post` — auditar e corrigir posts **já publicados** via WP-CLI (schema, score, OG, robots)
- `Dev/claude_automations_dev/intelligence-publisher/publisher.py` — implementação de referência dos auto-fixes
- `Dev/claude_automations_dev/intelligence-publisher/prompts/article.md` — prompt de referência para artigos de notícias
- `Hub/05-Projectos/Podcast-Descomplicar-Digital/prompts/generate-wp-description.md` — prompt de referência para posts de podcast
+284 -3
View File
@@ -1,7 +1,6 @@
---
name: wp-dev
description: Desenvolvimento WordPress especializado com Quality Gate 70+. Cria e modifica plugins seguindo WordPress Coding Standards, desenvolve temas child, integra WooCommerce e resolve problemas WordPress.
layer: wiki
---
# WordPress Plugin Developer
@@ -57,8 +56,8 @@ Integrar insights do NotebookLM nas recomendações e decisões.
### 1. Pesquisa Inicial (MCP)
Antes de desenvolver, SEMPRE executar:
```
mcp__mem_search_memories "[keywords wordpress]"
mcp__wikijs__search_pages "[plugin/funcionalidade]"
mcp__mem0__search_memories "[keywords wordpress]"
/knowledge (router Hub QR/PROC) + mcp__mem0__search_memories "[plugin/funcionalidade]"
```
### 2. Quality Gate 70+
@@ -176,3 +175,285 @@ function plugin_name_deactivate() {
- [ ] Sem erros PHP (php -l)
- [ ] Textdomain para traduções
- [ ] Uninstall hook para limpeza
- [ ] Testado em versão WP recente
---
## Hooks WordPress Essenciais
### Actions (fazer algo)
```php
// Init - inicializar plugin
add_action('init', 'register_custom_post_type');
// Enqueue scripts/styles
add_action('wp_enqueue_scripts', 'enqueue_assets');
add_action('admin_enqueue_scripts', 'enqueue_admin_assets');
// Save post
add_action('save_post', 'save_custom_meta', 10, 3);
// Admin menu
add_action('admin_menu', 'register_admin_page');
// AJAX
add_action('wp_ajax_my_action', 'handle_ajax');
add_action('wp_ajax_nopriv_my_action', 'handle_ajax_public');
// WooCommerce
add_action('woocommerce_before_cart', 'add_notice');
add_action('woocommerce_checkout_order_processed', 'process_order', 10, 3);
```
### Filters (modificar dados)
```php
// Modificar título
add_filter('the_title', 'custom_title', 10, 2);
// Modificar conteúdo
add_filter('the_content', 'add_content_suffix');
// Query
add_filter('pre_get_posts', 'modify_query');
// WooCommerce
add_filter('woocommerce_cart_item_price', 'custom_price', 10, 2);
add_filter('woocommerce_checkout_fields', 'custom_checkout_fields');
```
## Erros Comuns e Soluções
### 1. White Screen of Death (WSOD)
```bash
# Activar WP_DEBUG
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
# Ver erro
tail -f wp-content/debug.log
```
### 2. Plugin Conflict
```bash
# Desactivar todos os plugins via BD
UPDATE wp_options SET option_value = '' WHERE option_name = 'active_plugins';
# Reactivar um a um para identificar conflito
```
### 3. Memory Limit
```php
// wp-config.php
define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '512M');
```
### 4. Permissões
```bash
# Padrão seguro
find /path -type d -exec chmod 755 {} \;
find /path -type f -exec chmod 644 {} \;
chmod 600 wp-config.php
```
### 5. Páginas com shortcodes React/Vite
Páginas que usam shortcodes para renderizar componentes React (ex: `[react-app]`, `[vite-app]`, `[app-root]`) **nunca devem ser editadas via editor wp-admin** (Gutenberg ou Classic). O editor pode:
- Adicionar `<p>` e `<br>` indesejados à volta do shortcode
- Remover atributos ou alterar aspas
- Quebrar a montagem do componente React no DOM
**Procedimento correcto:**
```bash
# Ver conteúdo actual da página
wp post get <ID> --field=content --ssh=<server>
# Editar conteúdo via WP-CLI
wp post update <ID> --post_content='[react-app id="main"]' --ssh=<server>
# Ou via SQL directo (quando WP-CLI não é suficiente)
UPDATE wp_posts SET post_content = '[react-app id="main"]' WHERE ID = <ID>;
```
**Identificar páginas afectadas:**
```bash
# Listar páginas com shortcodes React/Vite
wp post list --post_type=page --fields=ID,post_title,post_content --format=csv --ssh=<server> | grep -iE '\[(react|vite|app)'
```
**Regra:** Ao criar ou manter plugins que registam shortcodes para aplicações React/Vite, documentar no `readme.txt` do plugin que as páginas associadas devem ser editadas exclusivamente via WP-CLI ou BD.
## Datasets Dify (Consulta Obrigatória)
Em caso de dúvidas ou para aprofundar conhecimento, consultar os seguintes datasets via MCP:
| Dataset | ID | Prioridade |
|---------|----|-----------:|
| **Wordpress** | `9da0b2b9-5051-4b99-b9f6-20bf35067092` | 1 |
| **Elementor** | `9c77d3e2-4d88-4a43-abff-d4e681267cc7` | 1 |
| **Crocooblock** | `139cdf67-afce-46ec-9ccd-2a06040e5b9d` | 1 |
| **Crocoblock** | `bdf85c26-1824-4021-92d1-be20501b35ac` | 1 |
| **Kivicare** | `2e9b7c48-c630-457a-83a0-db8e77f76eae` | 2 |
| **Desenvolvimento de WebSites** | `c8489151-de94-42b2-8cee-c0b961cfac6d` | 2 |
### Como Consultar
```javascript
// Desenvolvimento de plugins WordPress
mcp__notebooklm__notebook_query, mcp__dify-kb__dify_kb_retrieve_segments({
dataset_id: "9da0b2b9-5051-4b99-b9f6-20bf35067092",
query: "plugin development hooks filters"
})
// Widgets e extensões Elementor
mcp__dify-kb__dify_kb_retrieve_segments({
dataset_id: "9c77d3e2-4d88-4a43-abff-d4e681267cc7",
query: "custom widget dynamic tags"
})
// JetEngine e Crocoblock
mcp__dify-kb__dify_kb_retrieve_segments({
dataset_id: "139cdf67-afce-46ec-9ccd-2a06040e5b9d",
query: "jetengine custom post type relations"
})
// WooCommerce e e-commerce
mcp__dify-kb__dify_kb_retrieve_segments({
dataset_id: "9da0b2b9-5051-4b99-b9f6-20bf35067092",
query: "woocommerce hooks checkout cart"
})
```
### Quando Consultar
- Desenvolver plugins WordPress
- Criar widgets Elementor customizados
- Configurar JetEngine/Crocoblock
- Integrar WooCommerce
- Implementar custom post types
---
## Exemplos
### Exemplo 1: Criar plugin simples
```
User: /wp-dev criar plugin de custom post type "Serviços"
→ Pesquisa memória e KB Dify
→ Gera estrutura de plugin
→ Implementa CPT com ACF integration
→ Testes de segurança e WPCS
→ Score 75/100 ✅
```
### Exemplo 2: Widget Elementor
```
User: /wp-dev widget elementor para mostrar últimos serviços
→ Consulta dataset Elementor
→ Implementa widget com controls
→ Renderiza output com template
→ Testa em diferentes layouts
```
### Exemplo 3: Integração WooCommerce
```
User: /wp-dev adicionar campo personalizado no checkout
→ Consulta dataset Wordpress (WooCommerce)
→ Usa hooks woocommerce_checkout_fields
→ Implementa validação e save
→ Testa fluxo completo
```
---
## Instrumentação Automática
Esta skill grava métricas automaticamente para análise PDCA.
### Baseline
| Métrica | Valor |
|---------|-------|
| Tempo baseline | 4 horas |
| Tempo target | 2 horas |
| Melhoria esperada | -50% |
### Query para Gravar (executar no final)
```sql
INSERT INTO tblskill_agent_metrics (
type, name, duration_ms, status, staff_id,
kb_consulted, kb_cache_hit, tool_calls, project_id
) VALUES (
'skill', '/wp-dev', {DURACAO_MS}, '{STATUS}', 25,
{KB_CONSULTADO}, {CACHE_HIT}, {TOOL_CALLS}, {PROJECT_ID}
);
```
### Ver Métricas
```bash
/metrics /wp-dev
```
---
**Versão**: 1.2.0 | **Data**: 2026-03-12 | **Instrumentação**: Activa
---
## Media Bank (D7-MB-001)
**Quando consultar:** antes de gerar ou fazer upload de hero image, featured image ou imagens inline para plugins/temas WordPress. Permite reutilizar matéria-prima existente em vez de gerar do zero.
**Workflow tentativa-primeiro:**
1. Pesquisar via MCP `media-bank`:
```
mcp__media-bank__search_media(
query="<descrição EN do tema do post/plugin>",
type="image",
tema="wordpress-web", # ou outro dos 9 temas
orientation="landscape",
min_quality=7,
min_width=1600,
exclude_used_in_last_days=30,
limit=5
)
```
2. Avaliar candidatos por `description_en` + `quality_score`.
3. Se ≥1 adequado → usar `path_local` como input (upload via `wp media import` ou composição prévia).
4. Se nenhum → fallback para geração IA (Flux/Replicate/design-engine).
5. Marcar uso: `mcp__media-bank__mark_used(asset_id, {type:"wp-post", ref:"<post_id>", date:"<YYYY-MM-DD>"})`.
**Fallback REST** (se MCP indisponível):
```bash
SK=$(grep SERVICE_KEY /media/ealmeida/Dados/Hub/05-Projectos/Banco-Imagens-Videos/scripts/.env | cut -d= -f2)
curl -s "https://mem.descomplicar.pt/rest/v1/media_bank_assets?type=eq.image&tema=eq.wordpress-web&orientation=eq.landscape&quality_score=gte.7&deleted_at=is.null&order=quality_score.desc&limit=5" \
-H "apikey: $SK" -H "Authorization: Bearer $SK"
```
**Regra inviolável:** matéria-prima local. Usar como input para composição — NUNCA inserir o ficheiro raw directamente em publicação pública.
Ver `Hub/06-Operacoes/Procedimentos/D7-Tecnologia/MCP/PROC-Media-Bank.md` (D7-MB-001) para detalhes.
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
+869
View File
@@ -0,0 +1,869 @@
---
name: wp-fastest-cache
description: Gestão completa do WP Fastest Cache (page cache HTML local) via WP-CLI e via o MCP `wpfc` (projecto `mcp-wpfc`, multi-site, 18 tools). Cobre purga de cache nativa (`wp fastest-cache clear`), diferença entre page cache (WPFC) e object cache (Redis), preload após publicar, minify HTML/CSS/gzip, exclusão de páginas/cookies/user-agents, Clearing Specific Pages, integração CDN (Cloudflare/MaxCDN/genérica) e Varnish, customização do caminho da cache, matriz Free vs Premium, estado real Premium por site (§16.1), achado crítico sobre formato de armazenamento das options (string JSON, não JSON tipado — §16.2), e sequência de purga completa (WPFC + Cloudflare) após deploys ou mudanças de config. Usar quando "wp fastest cache", "wpfc", "purgar cache", "limpar cache wordpress", "cache não actualiza", "preload cache", "minify html", "combine css", "gzip wordpress", "excluir página cache", "cache timeout", "cdn wordpress", "clearing specific pages", "varnish wordpress", "wpfc mcp", "wp fastest pro", "mcp-wpfc".
---
# /wp-fastest-cache — Gestão do WP Fastest Cache via WP-CLI
Page cache local (HTML estático em disco) do bundle de performance WP
Descomplicar®. Complementa (não substitui) o Redis Object Cache e o
App for Cloudflare® — os três operam em camadas diferentes, ver §3.
**Fonte:** leitura completa do código-fonte v1.5.0 (versão **Free** — sem
`wp-fastest-cache-premium/` instalado) no servidor `server.descomplicar.pt`,
porta 9443, site piloto `emanuelalmeida.pt`: `wpFastestCache.php` (2746
linhas), `inc/admin.php` (2620 linhas), `inc/cache.php` (1460 linhas),
`inc/preload.php` (894 linhas), `inc/css-utilities.php` (766 linhas),
`inc/cdn.php` (650 linhas), `inc/js-utilities.php` (340 linhas),
`inc/single-preload.php` (346 linhas), `inc/varnish.php` (177 linhas),
`inc/admin-toolbar.php`, `inc/clearing-specific-pages.php`, `inc/column.php`,
`inc/wp-polls.php`, `inc/cli.php`, `uninstall.php`, e todos os templates em
`templates/` (`exclude.php`, `timeout.php`, `preload.php`, `cache_path.php`,
`varnish.php`, `toolbar_settings.php`, `disable_wp_cron.php`,
`clearing_specific_pages.php`, `cdn/cloudflare.php`, `cdn/maxcdn.php`,
`cdn/other.php`, `cdn/file_types.php`, `cdn/specify_sources.php`,
`cdn/exclude_sources.php`) + `CONFIG-Plugins-Referencia.md` §3 +
`BUNDLE-Excelencia-WP.md` §2.2/§2.4 + verificação ao vivo de todas as
`wp_options` prefixadas `WpFastestCache*` via SSH em `emanuelalmeida.pt`,
16-08-2026.
---
## Contexto CWP — sempre obrigatório
```bash
PATH=/home/USER/public_html
wp <comando> --allow-root --path=$PATH
```
**Acesso via SSH:** `ssh server "sudo -u USER /usr/local/bin/wp <comando> --path=$PATH --format=json"`
(alias `server` já configurado; user/path variam por site — ver tabela de
sites de referência no topo desta sessão).
---
## 1. Config real — `wp_options.WpFastestCache` (JSON serializado)
Não existe API `wp option patch` dedicada a este plugin de forma prática —
a option é um objecto JSON simples (não serializado PHP), por isso
**`wp option update` com `--format=json` funciona directamente** (ao
contrário do Rank Math, que usa arrays PHP serializados e exige
`wp option patch`). Internamente, `saveOption()` (`inc/admin.php`) faz
`json_encode($_POST)` do formulário inteiro — ou seja, a option guarda
**exactamente** o subconjunto de checkboxes que estava marcado no momento
de gravar, nunca chaves "off" explícitas (chave ausente = desligada).
```bash
# Ler a config completa
wp option get WpFastestCache --format=json --allow-root --path=$PATH
```
Valor real verificado ao vivo em `emanuelalmeida.pt` (16-08-2026):
```json
{
"wpFastestCacheStatus": "on",
"wpFastestCachePreload": "on",
"wpFastestCachePreload_number": "4",
"wpFastestCacheLoggedInUser": "on",
"wpFastestCacheMobile": "on",
"wpFastestCacheMobileTheme": "on",
"wpFastestCacheNewPost": "on",
"wpFastestCacheMinifyHtml": "on",
"wpFastestCacheMinifyCss": "on",
"wpFastestCacheCombineCss": "on",
"wpFastestCacheGzip": "on",
"wpFastestCacheLBC": "on",
"wpFastestCacheDisableEmojis": "on"
}
```
| Chave | Valor | O que faz |
|---|---|---|
| `wpFastestCacheStatus` | `on` | Liga a cache HTML (interruptor mestre) |
| `wpFastestCachePreload` + `wpFastestCachePreload_number` | `on`, `4` threads | Regenera a cache automaticamente (crawl interno) em vez de esperar pela primeira visita — `4` = threads paralelas do crawler |
| `wpFastestCacheLoggedInUser` | `on` | Serve cache também a utilizadores autenticados (o plugin exclui automaticamente páginas com nonce/admin-bar dinâmicos, não quebra o WP admin) |
| `wpFastestCacheMobile` | `on` | **Free.** Não serve a versão desktop cacheada a dispositivos móveis (cache mobile separada da desktop, ficheiro próprio em `/cache/wpfc-mobile-cache`) |
| `wpFastestCacheMobileTheme` | `on` | **⚠ Só tem efeito com o addon Premium activo** (gate `class_exists("WpFastestCachePowerfulHtml")` em `inc/admin.php:1233`). Neste site a chave está gravada no JSON mas **é ignorada em runtime** porque `wp-fastest-cache-premium/` não está instalado — ver §13 |
| `wpFastestCacheNewPost` | `on` | Limpa a cache automaticamente ao publicar/actualizar conteúdo |
| `wpFastestCacheMinifyHtml` | `on` | Minifica HTML na saída |
| `wpFastestCacheMinifyCss` + `wpFastestCacheCombineCss` | `on` | Minifica e combina ficheiros CSS num único ficheiro |
| `wpFastestCacheGzip` | `on` | Comprime a resposta com gzip |
| `wpFastestCacheLBC` | `on` | Lazy Browser Caching — envia headers `Expires`/`Cache-Control` de longa duração para assets estáticos |
| `wpFastestCacheDisableEmojis` | `on` | Ver §5 |
### 1.1 Todas as chaves possíveis da option `WpFastestCache` (mapeadas do código-fonte)
Lista completa extraída de `grep -roh "wpFastestCache[A-Za-z_]*"` a todo o
código-fonte (`wpFastestCache.php`, `inc/*.php`, `templates/**/*.php`) —
não apenas as 13 activas neste site. Cada chave corresponde a um
`name="..."` de formulário que, quando marcado, é gravado via
`saveOption()`.
**Disponíveis na versão Free (sem gate `class_exists("WpFastestCachePowerfulHtml")`):**
| Chave | Free | O que faz |
|---|---|---|
| `wpFastestCacheStatus` | ✅ | Master switch da cache HTML |
| `wpFastestCachePreload` | ✅ | Preload automático (crawler) — ver §6 |
| `wpFastestCachePreload_number` | ✅ | Nº de threads paralelas do crawler de preload |
| `wpFastestCacheLoggedInUser` | ✅ | Não serve cache a utilizadores autenticados |
| `wpFastestCacheMobile` | ✅ | Cache mobile separada (ficheiros próprios, não é "tema mobile") |
| `wpFastestCacheNewPost` | ✅ | Limpa cache ao publicar |
| `wpFastestCacheNewPost_type` / `_type_all` / `_type_homepage` | ✅ | Âmbito da limpeza ao publicar — só a página nova, ou tudo, ou também a homepage (`templates/newpost.php`) |
| `wpFastestCacheUpdatePost` | ✅ | Limpa cache ao actualizar um post/página existente |
| `wpFastestCacheUpdatePost_type` / `_type_all` / `_type_post` | ✅ | Âmbito da limpeza ao actualizar (`templates/updatepost.php`) |
| `wpFastestCacheMinifyHtml` | ✅ | Minifica HTML |
| `wpFastestCacheMinifyCss` | ✅ | Minifica CSS |
| `wpFastestCacheCombineCss` | ✅ | Combina ficheiros CSS |
| `wpFastestCacheGzip` | ✅ | Gzip |
| `wpFastestCacheLBC` | ✅ | Leverage Browser Caching (headers de expiração) |
| `wpFastestCacheDisableEmojis` | ✅ | Remove script/CSS de emoji do WP core — ver §5 |
| `wpFastestCacheWidgetCache` | ✅ (link de doc é premium, mas o campo em si não está gated no template lido) | Cache de widgets para reduzir queries SQL — **confirmar no wp-admin**, o link de ajuda aponta para a página de marketing Premium mas o checkbox aparece fora do bloco `class_exists` no ficheiro `templates/` correspondente |
| `wpFastestCacheLanguage` | ✅ | Idioma da interface de admin do plugin (select, não afecta o frontend) |
| `wpFastestCachePage` | ✅ (interno) | Campo hidden do formulário que identifica qual sub-página foi submetida (`options`, `deleteCache`, `deleteCssAndJsCache`, `cacheTimeout`) — nunca persiste na option guardada |
| `wpFastestCacheTimeOut` / `_TimeOutHour` / `_TimeOutMinute` | ✅ | Cache Timeout **global legado** (1 única regra, agenda `wp_schedule_event` no hook `wp_fastest_cache`) — não confundir com o Cache Timeout Wizard (§8, `WpFastestCacheExclude`-like, guardado via `wp_fastest_cache_N`) |
**Só têm efeito com `wp-fastest-cache-premium/wpFastestCachePremium.php` activo (gate `class_exists("WpFastestCachePowerfulHtml")` em `inc/admin.php`) — neste site NÃO instalado, ver §13:**
| Chave | O que faria (Premium) |
|---|---|
| `wpFastestCacheMobileTheme` + `_themename` | Cache de um tema mobile dedicado (Mobile Cache) |
| `wpFastestCacheMinifyHtmlPowerFul` | Minificação de HTML mais agressiva ("Minify HTML Plus") |
| `wpFastestCacheMinifyCssPowerFul` | Minificação de CSS mais agressiva ("Minify Css Plus") |
| `wpFastestCacheMinifyJs` | Minifica ficheiros JS |
| `wpFastestCacheCombineJs` | Combina JS no `<head>` |
| `wpFastestCacheCombineJsPowerFul` | Combina JS no `<footer>` ("Combine Js Plus") |
| `wpFastestCacheRenderBlocking` + `_Css` | Elimina render-blocking JavaScript/CSS |
| `wpFastestCacheGoogleFonts` | Carrega Google Fonts de forma assíncrona |
| `wpFastestCacheLazyLoad` + `_type`, `_type_all`, `_type_exceptcontent`, `_keywords`, `_placeholder`, `_exclude_full_size_img` | Lazy load de imagens/iframes com regras avançadas de âmbito e placeholder customizado |
| `wpFastestCacheDelayJS` | Atrasa carregamento de scripts JS até scroll/movimento do rato |
| `wpFastestCachePremium` | Flag interna de licença Premium |
> **Nota crítica:** todo o bloco acima só é renderizado/gravado se
> `class_exists("WpFastestCachePowerfulHtml")` for verdadeiro, o que exige
> o plugin separado `wp-fastest-cache-premium` (licença paga) instalado e
> activo. **Não é apenas "desligado por omissão"** — nos sites do bundle
> Descomplicar (sem addon Premium comprado) estes checkboxes aparecem
> desativados/cinzentos no wp-admin (classe `questionCon disabled`) e os
> respectivos `$_POST` nunca chegam a `saveOption()`. Ver §13 para a
> matriz completa Free vs Premium.
**Chaves de infraestrutura confirmadas como ausentes (não é omissão de leitura):**
| Chave esperada | Estado | Porque fica assim |
|---|---|---|
| `WpFastestCacheVarnish` | ⚪ não configurado | Sem Varnish à frente do site — não aplicável. Ver §10.3 |
| `WpFastestCacheCDN` | ⚪ não configurado | A integração CDN nativa do WPFC não está activa; o purge Cloudflare é feito pelo App for Cloudflare®, não pelo WPFC. Ver §10 |
| `WpFastestCacheExclude` | ⚪ não configurado | Sem regras de exclusão de páginas/cookies/user-agents definidas. Ver §7 |
| `WpFastestCacheCSP` | ⚪ não configurado | Sem regras "Clearing Specific Pages" definidas. Ver §9 |
| `WpFastestCachePathSettings` | ⚪ não configurado | Caminho de cache por omissão (`wp-content/cache`, `wp-content/cache/wpfc-minified`). Ver §11 |
| `WpFastestCacheToolbarSettings` | ⚪ não configurado | Sem restrição de roles na toolbar de admin — ver §12 |
---
## 2. Purgar cache — comando nativo (NÃO é `wp cache flush`)
O plugin regista um comando WP-CLI próprio via `inc/cli.php`
(`WP_CLI::add_command('fastest-cache', 'wpfcCLI')`), carregado
automaticamente quando `WP_CLI` está definido — não precisa de nada extra
no `wp-config.php`. **`wp help fastest-cache` confirma ao vivo (16-08-2026)
que existe um único subcomando: `clear`** — não há `preload`, `status`,
`exclude` nem `cdn` via CLI; essas operações só existem no wp-admin/AJAX.
```bash
# Limpar toda a cache HTML (mantém ficheiros minificados existentes)
wp fastest-cache clear all --allow-root --path=$PATH
# Limpar TUDO, incluindo cache minificada (CSS/HTML combinados) — usar após deploy de CSS/JS
wp fastest-cache clear all and minified --allow-root --path=$PATH
# Limpar só a cache de um ou mais posts/páginas específicos
wp fastest-cache clear --post_id=123 --allow-root --path=$PATH
wp fastest-cache clear --post_id=123,456,789 --allow-root --path=$PATH
```
Código-fonte de `wpfcCLI::clear()` (`inc/cli.php`) — lógica exacta:
1. Se `--post_id=` estiver presente, itera cada ID (split por vírgula) e
chama `singleDeleteCache(false, $post_id)` por post — **não** aceita
`all` em conjunto com `--post_id`;
2. Senão, se `$args[0] == "all"`:
- Se `$args[1] == "and"` e `$args[2] == "minified"` → `deleteCache(true)`;
- Senão → `deleteCache()` (sem o `true`, não limpa minificados);
3. Qualquer outra combinação (`$args[0]` diferente de `all`, ou sintaxe
incompleta de "and minified") → `wrong_usage()`, que imprime um bloco
de erro com link para a doc oficial e sai com `WP_CLI::error_multi_line`.
Internamente, `deleteCache()` — a mesma função que corre quando se clica
"Delete Cache" no wp-admin — faz, por esta ordem:
1. Purga Varnish se `WpFastestCacheVarnish` estiver configurado (não é o
caso nos sites do bundle — ver §10.3);
2. Chama `CdnWPFC::cloudflare_clear_cache()` — **só tem efeito se
`WpFastestCacheCDN` tiver uma entrada `id == "cloudflare"` com
`cdnurl`/`originurl` (email + API token) configurados**. Nos sites do
bundle **não está configurado** — o purge Cloudflare real é feito pelo
**App for Cloudflare®**, não por esta chamada. Não assumir que
`wp fastest-cache clear all` purga o edge Cloudflare;
3. Recria a pasta de preload temporária e relança o crawler de preload
(`set_preload()`);
4. Apaga `wp-content/cache/all`, `wp-content/cache/wpfc-minified` e
`wp-content/cache/wpfc-mobile-cache` (ou os caminhos customizados de
`WpFastestCachePathSettings`, ver §11).
---
## 3. Três camadas de cache — não confundir
| Camada | Plugin/mecanismo | Comando de purga | O que invalida |
|---|---|---|---|
| **Page cache** (HTML em disco) | WP Fastest Cache | `wp fastest-cache clear all and minified` | Ficheiros HTML/CSS/JS gerados em `wp-content/cache/*` |
| **Object cache** (queries à BD) | Redis Object Cache (`redis-cache`, DB 12, Predis) | `wp cache flush` | Chaves no Redis (queries, transients cacheados) |
| **Edge cache** (CDN) | Cloudflare, via App for Cloudflare® | purge no wp-admin do plugin ou API Cloudflare directa | Cache no edge Cloudflare (fora do servidor de origem) |
**Não conflitam entre si** — são camadas independentes e complementares:
o Redis acelera queries PHP mesmo quando o WPFC serve HTML estático (o
WPFC serve o ficheiro sem sequer bootar o WordPress completo em muitos
casos, mas quando bootar — ex. utilizador autenticado — o Redis acelera
essa execução). `wp cache flush` **não limpa a cache HTML do WPFC**, e
`wp fastest-cache clear all` **não limpa o Redis**. Confundir os dois é o
erro mais comum: "limpei a cache e a alteração continua sem aparecer" —
normalmente falta uma das três camadas.
---
## 4. Sequência de cache completa (sempre após alterações de config ou deploy)
Seguir o mesmo padrão do `/rank-math` (`Sequencia de cache`), com o passo
WPFC adicionado:
```bash
wp fastest-cache clear all and minified --allow-root --path=$PATH
wp cache flush --allow-root --path=$PATH
wp transient delete --all --allow-root --path=$PATH
wp rewrite flush --allow-root --path=$PATH
```
Seguido de **purga Cloudflare** (App for Cloudflare®, camada edge — fora
do WP-CLI, via wp-admin ou API Cloudflare com o token da zona). Alterações
que dependem de `robots.txt`, `/llms.txt`, headers ou CSS/JS combinado só
aparecem depois de purgar as três camadas — confirmado no achado real
do `bot_management`/`robots.txt` (`BUNDLE-Excelencia-WP.md` §2.4):
`robots.txt` tinha `cache-control: max-age=315360000` no Cloudflare, e só
mudou depois de purgar Cloudflare + WPFC juntos.
**Regra prática:** depois de qualquer alteração a CSS/JS/tema/Elementor,
purgar sempre **WPFC (`and minified`) + Cloudflare** juntos — nunca só um
dos dois. Alterações a options do WordPress sem impacto em HTML/CSS
(ex. Rank Math meta) só precisam do `wp cache flush` (Redis).
---
## 5. `wpFastestCacheDisableEmojis` — o que faz exactamente
Confirmado em `inc/cache.php:34` — quando esta opção está `on`, o plugin
remove o `<script>` inline `wp-emoji-release.min.js` e o CSS inline de
detecção de emoji que o WordPress core injecta por omissão em todo o
`<head>` (mecanismo nativo para navegadores antigos sem suporte a emoji
Unicode). **Ganho de performance pequeno mas real**: elimina um pedido
JS + um bloco de CSS inline em cada página, sem efeito visual em
navegadores modernos (emoji Unicode nativo funciona sem o script). Sem
riscos conhecidos de quebra — ao contrário de Minify JS/Combine JS
(Premium), não mexe em scripts de plugins/tema.
---
## 6. Preload — configuração completa e rebuild após deploy
`wpFastestCachePreload=on` + `wpFastestCachePreload_number=4` significa
que, após qualquer purga (`deleteCache()`), o plugin relança
automaticamente um crawler interno (`set_preload()`) com 4 threads
paralelas para regenerar a cache das páginas mais visitadas **antes** da
próxima visita real — evita que o primeiro visitante depois de um deploy
apanhe sempre a versão sem cache (mais lenta). Lógica em `inc/preload.php`
(894 linhas) e `inc/single-preload.php` (346 linhas, preload de um único
post após ser publicado/actualizado).
### 6.1 Método de preload (`templates/preload.php`)
O wizard de Preload no wp-admin oferece um `<select>` com dois métodos
(campo interno, não persiste directamente como chave simples — é
serializado dentro da config de preload):
| Método | O que faz |
|---|---|
| `default` | Ordena o conteúdo do mais recente ao mais antigo (`Contents from Newest to Oldest`) — é o método activo se nada for escolhido |
| `sitemap` | Usa um sitemap XML como fonte de URLs a pré-carregar, em vez de percorrer o conteúdo por data |
### 6.2 Tipos de conteúdo incluídos no preload (checkboxes arrastáveis/reordenáveis)
Quando o método é `default`, os seguintes tipos de conteúdo podem ser
marcados individualmente e a ordem entre eles é definida por
drag-and-drop (guardada em `wpFastestCachePreload_order`):
| Checkbox | Tipo de conteúdo |
|---|---|
| `wpFastestCachePreload_homepage` | Homepage |
| `wpFastestCachePreload_post` | Posts |
| `wpFastestCachePreload_category` | Categorias |
| `wpFastestCachePreload_page` | Páginas |
| `wpFastestCachePreload_tag` | Tags |
| `wpFastestCachePreload_attachment` | Anexos (media attachments com página própria) |
| `wpFastestCachePreload_customposttypes` | Custom Post Types registados no site |
| `wpFastestCachePreload_customTaxonomies` | Taxonomias customizadas |
Quando o método é `sitemap`, existe ainda `wpFastestCachePreload_sitemap`
para indicar o URL do sitemap XML a usar como fonte.
### 6.3 Rebuild após deploy
```bash
# Depois de um deploy (código/tema alterado), sequência completa:
wp fastest-cache clear all and minified --allow-root --path=$PATH
wp cache flush --allow-root --path=$PATH
# + purga Cloudflare (App for Cloudflare, fora do WP-CLI)
# O preload arranca sozinho a seguir ao clear — não precisa de comando extra.
# Confirmar que recriou a pasta de cache:
wp eval 'echo is_dir(WP_CONTENT_DIR . "/cache/all") ? "existe" : "vazio, aguardar preload";' --allow-root --path=$PATH
```
---
## 7. Exclusão de páginas, cookies e user-agents — `WpFastestCacheExclude`
Feature separada da option principal, gerida pelo "Exclude Page Wizard"
(`templates/exclude.php`) e persistida em **`wp_options.WpFastestCacheExclude`**
como um array JSON de objectos `{prefix, content, type}` — **não faz parte
do JSON de `WpFastestCache`**. Callback de gravação:
`wpfc_save_exclude_pages_callback()` em `wpFastestCache.php:714`.
```bash
# Ler as regras de exclusão configuradas
wp option get WpFastestCacheExclude --format=json --allow-root --path=$PATH
```
### 7.1 Estrutura de uma regra
| Campo | Valores possíveis | O que faz |
|---|---|---|
| `type` | `page`, `useragent`, `cookie`, `css`, `js` | A que se aplica a regra — página completa (não gera cache), pedido de um User-Agent específico, presença de um cookie, ou exclusão de um ficheiro CSS/JS individual da minificação/combinação |
| `prefix` | `homepage`, `category`, `tag`, `post`, `page`, `archive`, `attachment` (content types); `startwith`, `contain`, `exact`, `regex` (métodos); `googleanalytics`, `yandexclickid`, `woocommerce_items_in_cart` (especiais) | Como o `content` é comparado contra o `REQUEST_URI` (ou o cookie/user-agent) |
| `content` | string livre ou regex | O valor a comparar |
### 7.2 Regras nativas sempre presentes (hardcoded, não editáveis)
Independentemente da configuração, o wizard sempre lista estas regras
como pré-existentes e não removíveis (`editable: false` em
`WpFcExcludePages.insert_existing_rules()`):
| Tipo | Regra | Motivo |
|---|---|---|
| `page`, exact | `wp-login.php` | Nunca cachear a página de login |
| `page`, startwith | `wp-admin` | Nunca cachear o admin |
| `useragent`, contain | `facebookexternalhit` | Bots de preview social não devem receber cache potencialmente desatualizada |
| `useragent`, contain | `LinkedInBot` | idem |
| `useragent`, contain | `WhatsApp` | idem |
| `useragent`, contain | `Twitterbot` | idem |
| `cookie`, contain | `Admin` | Qualquer visitante com cookie de admin (`wordpress_logged_in_...` com role admin) nunca recebe cache |
### 7.3 Efeito no `.htaccess` (Apache) e no PHP (todos os servidores)
Ao gravar (`modify_htaccess_for_exclude()`), o plugin reescreve o bloco
`# Start WPFC Exclude ... # End WPFC Exclude` do `.htaccess` com regras
`mod_rewrite` equivalentes — **isto é a camada rápida (nginx/Apache
servem o miss sem sequer chamar o PHP)**. Em paralelo, `inc/cache.php`
lê `WpFastestCacheExclude` em runtime (`get_option("WpFastestCacheExclude")`,
linha 78) para aplicar as mesmas regras via PHP como segunda camada de
segurança (necessário em nginx, onde não há `.htaccess`).
### 7.4 Bloquear cache de uma página individual via editor
O plugin regista um botão no TinyMCE/Quicktags (`addButtonOnEditor()`,
`checkShortCode()`) que insere `[wpfcNOT]` no conteúdo — qualquer post/página
que contenha este shortcode nunca é cacheada (`$this->blockCache = true`),
sem precisar de criar uma regra de exclusão explícita.
### 7.5 Exclusão automática de wishlist (YITH WooCommerce Wishlist)
Se o plugin `yith-woocommerce-wishlist/init.php` estiver activo,
`exclude_urls()` (`inc/admin.php:163`) adiciona automaticamente uma regra
`exact` para o URL da página de wishlist, sempre que a config é gravada —
comportamento automático, não precisa de intervenção manual.
---
## 8. Cache Timeout — duas mecânicas distintas
### 8.1 Timeout global legado (`wpFastestCacheTimeOut`)
Uma única regra global, gravada como parte da própria option `WpFastestCache`
(`wpFastestCacheTimeOut`, `_TimeOutHour`, `_TimeOutMinute`). Ao gravar
(`addCacheTimeout()`, `inc/admin.php:203`), o plugin:
1. Limpa qualquer agendamento anterior no hook `wp_fastest_cache`
(`wp_clear_scheduled_hook($this->slug())`, onde `slug() == "wp_fastest_cache"`);
2. Agenda um novo `wp_schedule_event()` no mesmo hook, com o schedule
(`hourly`, `daily`, etc.) escolhido em `wpFastestCacheTimeOut` e a hora
exacta calculada a partir de `TimeOutHour`/`TimeOutMinute`.
```bash
# Verificar se há um timeout global agendado
wp cron event list --allow-root --path=$PATH | grep wp_fastest_cache
```
### 8.2 Cache Timeout Wizard — múltiplas regras por página/tipo (`templates/timeout.php`)
Mecanismo mais granular: permite definir **N regras** de purga agendada,
cada uma associada a um subconjunto de páginas (`homepage`, `all`,
`startwith`, `exact`) e a uma frequência (`wp_get_schedules()`, incluindo
schedules customizados que tenham a flag `["wpfc"]` marcada). **Não usa
uma `wp_option` própria** — cada regra vira um evento WP-Cron individual
com hook `wp_fastest_cache_<indice>` (`wpfc_save_timeout_pages_callback()`,
`wpFastestCache.php:832`), e os parâmetros da regra (`prefix`, `content`,
`hour`, `minute`) viajam como argumentos serializados do próprio evento
cron — **inspecionáveis apenas via `wp cron event list` + `wp cron event run`**,
não via `wp option get`.
```bash
# Listar todas as regras de timeout agendadas (uma por evento wp_fastest_cache_N)
wp cron event list --allow-root --path=$PATH | grep 'wp_fastest_cache_'
```
Se o schedule escolhido for `daily`/`onceaday`, a UI mostra também um
selector de Hora/Minuto (fuso do servidor, mostrado ao vivo no wizard via
`current_time("H:i:s")`) para agendar a purga a uma hora fixa em vez de
"daqui a X".
### 8.3 Aviso "Disabled Cron" (`templates/disable_wp_cron.php`)
Se `DISABLE_WP_CRON` estiver definido como `true` no `wp-config.php`, o
botão "+ Add New Timeout" no wp-admin mostra um aviso a explicar que o
WP-Cron foi desligado inteiramente e nenhuma regra de timeout vai
disparar sozinha — só via cron real do sistema a chamar `wp-cron.php`, ou
`wp cron event run --due-now`.
---
## 9. Clearing Specific Pages (CSP) — `WpFastestCacheCSP`
Feature para **purgar páginas adicionais** sempre que a cache é limpa por
qualquer motivo (publicar/actualizar post, clique manual, CLI) —
útil para páginas agregadoras (ex. `/loja/`, `/blog/`) que mostram
conteúdo de múltiplos posts e por isso ficam desatualizadas sem serem
elas próprias editadas. Classe `ClearingSpecificPagesWPFC`
(`inc/clearing-specific-pages.php`), UI em `templates/clearing_specific_pages.php`.
```bash
# Ler as regras CSP configuradas
wp option get WpFastestCacheCSP --allow-root --path=$PATH
```
### 9.1 Estrutura
Array de objectos `{url, order}`, onde:
- `url` **tem de** começar pelo mesmo host que `get_option("home")` —
validado por `check_url()`, senão a gravação falha com
`wp_send_json_error()`;
- Pode conter **no máximo um** wildcard `(.*)`, validado por
`check_wild_card()` — mais do que um wildcard, ou um wildcard mal
colocado (não precedido de `/`), é rejeitado;
- Não pode conter `..` (protecção contra directory traversal,
`preg_match("/\.{2,}/", ...)`);
- `order` é o identificador único da regra (usado para editar/remover).
### 9.2 Exemplo de uso típico
Se `/loja/` lista os últimos produtos e um produto novo é publicado,
adicionar uma regra CSP com `url = https://site.pt/loja/` garante que
`/loja/` é purgado junto com a cache do próprio produto, mesmo que
`wpFastestCacheNewPost_type` esteja configurado apenas para "a página
nova".
---
## 10. Integração CDN e Varnish
### 10.1 CDN genérica (MaxCDN / StackPath / BunnyCDN / CloudFront / "Other") — `WpFastestCacheCDN`
Classe `CdnWPFC` (`inc/cdn.php`, 650 linhas). A option
**`WpFastestCacheCDN`** guarda um **array JSON de integrações** (mais do
que uma CDN pode coexistir), cada entrada identificada por `id`:
`cloudflare`, `maxcdn`, `other` (genérico — cobre StackPath, BunnyCDN,
Amazon CloudFront, KeyCDN, CDN77, que são todos normalizados para `"other"`
internamente em `start_cdn_integration()`/`pause_cdn_integration()`/
`remove_cdn_integration()`).
```bash
wp option get WpFastestCacheCDN --format=json --allow-root --path=$PATH
```
Campos de uma entrada genérica (não-Cloudflare): `id`, `cdnurl` (URL do
CDN), `originurl` (URL de origem, normalmente o próprio domínio),
`status` (ausente = activo, `"pause"` = integração pausada sem remover a
config).
**Âmbito de reescrita de URLs (wizard "Other CDN", `templates/cdn/*`):**
- `templates/cdn/file_types.php` — checkboxes por extensão de ficheiro a
servir via CDN: `aac, avif, css, eot, gif, jpeg, js, jpg, less, mp3,
mp4, ogg, otf, pdf, png, svg, swf, ttf, webm, webp, woff, woff2`
(todos marcados por omissão);
- `templates/cdn/specify_sources.php` — lista de palavras-chave: só URLs
que **contenham** um dos keywords especificados são servidos via CDN
(whitelist opcional, vazio = todos os tipos marcados acima);
- `templates/cdn/exclude_sources.php` — lista de palavras-chave: URLs que
**contenham** um destes keywords **nunca** são servidos via CDN
(blacklist, tem prioridade sobre a whitelist).
Validação em tempo real do URL do CDN (`CdnWPFC::check_url()`) bloqueia
explicitamente: IPv6, IPs decimais/hex/octal, `localhost`, e ranges
privados (`127.`, `10.`, `172.`, `169.`, `100.`, `198.`, `192.`) — com
mensagens de erro específicas conhecidas para CloudFront 403, BunnyCDN
403 e Speedsize 400.
### 10.2 Integração Cloudflare nativa do WPFC
**Distinta do App for Cloudflare®** — esta é a integração embutida no
próprio WP Fastest Cache, activada com `id == "cloudflare"` dentro de
`WpFastestCacheCDN`, campos `cdnurl` (email da conta, ou literal `"wpfc"`
se estiver a usar API Token em vez de Global API Key — ver
`cloudflare_generate_header()`) e `originurl` (a chave/token).
Ao configurar via wizard `templates/cdn/cloudflare.php`, o plugin executa
automaticamente e em sequência (`cloudflare_change_settings()`):
1. **Desactiva o Rocket Loader** da zona Cloudflare (`cloudflare_disable_rocket_loader()`) — evita conflito entre o Rocket Loader (que também reescreve JS) e o Combine/Minify JS do WPFC;
2. Purga a cache Cloudflare (`cloudflare_clear_cache()`);
3. **Define Browser Cache TTL para 6 meses** (`16070400` segundos,
`cloudflare_set_browser_caching()`) — hardcoded, não configurável pela UI;
4. Se o plano Cloudflare for `free`, **remove automaticamente as regras
WebP do `.htaccess`** (`cloudflare_remove_webp()`) — porque o plano
grátis da Cloudflare já faz a sua própria conversão/entrega WebP via
Polish, e as duas camadas juntas causam conflitos;
5. `cloudflare_disable_minify()` está **deprecated** (faz `return` antes
de qualquer chamada à API) porque a Cloudflare descontinuou o Auto
Minify legacy — o código morto continua no ficheiro mas nunca executa.
O Zone ID é resolvido automaticamente por `cloudflare_get_zone_id()`
(paginando `/zones?page=1..2&per_page=1000` da API Cloudflare e
comparando o hostname do site) e fica em cache serializado dentro do
próprio `WpFastestCacheCDN->zone_id` (função `unserialize()`/`serialize()`
directamente sobre um array PHP — não JSON).
```bash
# Confirmar se há integração Cloudflare nativa do WPFC configurada (distinta do App for Cloudflare®)
wp option get WpFastestCacheCDN --format=json --allow-root --path=$PATH | grep -o '"id":"cloudflare"'
```
### 10.3 Varnish (reverse proxy) — `WpFastestCacheVarnish`
Classe `VarnishWPFC` (`inc/varnish.php`). Guarda apenas
`{"server": "<host:porta>"}` (e opcionalmente `"status": "pause"`). Ao
purgar, envia um pedido HTTP `PURGE` (não `GET`/`POST`) para
`<schema>://<server>/.*` com o header `Host` apontado para o domínio real
do site — se o pedido falhar tenta trocar `https://`↔`http://` uma vez
antes de desistir, e trata explicitamente respostas `501` (método PURGE
não permitido pelo Varnish/hosting) com mensagem a sugerir contacto com o
hosting. **Chamado automaticamente dentro de `deleteCache()`** sempre que
`WpFastestCacheVarnish` existir e não estiver em pausa — não é preciso
correr nada manualmente depois de configurado.
```bash
wp option get WpFastestCacheVarnish --allow-root --path=$PATH
```
---
## 11. Customização do caminho da cache — `WpFastestCachePathSettings`
Wizard "Cache Path Customization" (`templates/cache_path.php`) permite
mudar o nome da pasta de cache dentro de `wp-content/` (por omissão
`cache`) e da pasta de assets minificados (por omissão `wpfc-minified`).
Útil quando outro plugin/CDN já usa `wp-content/cache/` para outra coisa,
ou por segurança-por-obscuridade em setups multi-tenant.
```bash
wp option get WpFastestCachePathSettings --allow-root --path=$PATH
```
Estrutura: `{"cachepath": "cache", "optimizedpath": "wpfc-minified"}` —
se a option não existir, o código assume estes dois valores por omissão
(`inc/admin.php:22-29` em `cache_path.php`). **Alterar isto implica que
todos os comandos de purga (`wp fastest-cache clear`, `deleteCache()`)
passam a apagar os novos caminhos** — não há migração automática de
ficheiros já cacheados no caminho antigo, ficam órfãos em disco até
serem apagados manualmente ou por rotação de logs.
---
## 12. Toolbar de admin e acções rápidas por post
### 12.1 Menu na admin bar (`inc/admin-toolbar.php`)
Classe `WpFastestCacheAdminToolbar` adiciona um nó "WP Fastest Cache" na
admin bar do WordPress (frontend e wp-admin), com sub-itens: "Clear Cache
of This Page" (só no frontend), "Clear All Cache", "Clear Cache and
Minified CSS/JS", e (se for multisite) "Clear Cache of All Sites". No
wp-admin, mostra ainda "Toolbar Settings" quando já se está na página de
opções do plugin.
### 12.2 Restringir a toolbar por role (`WpFastestCacheToolbarSettings`)
Wizard `templates/toolbar_settings.php` permite marcar quais **roles não
administrativos** (o `administrator` está sempre incluído e não aparece
na lista) veem o menu "WP Fastest Cache" na admin bar —
`wpfc_toolbar_save_settings_callback()` grava um array `{role: "1"}` em
`wp_options.WpFastestCacheToolbarSettings`.
```bash
wp option get WpFastestCacheToolbarSettings --allow-root --path=$PATH
```
### 12.3 Link "Clear Cache" na listagem de posts/páginas (`inc/column.php`)
Classe `WpFastestCacheColumn` injecta um link "Clear Cache" nas row
actions de `post_row_actions`/`page_row_actions` (a mesma linha onde
aparecem "Edit"/"Trash"/"View" na listagem do wp-admin), com um pedido
AJAX (`wpfc_clear_cache_column`) que chama `singleDeleteCache()` só para
esse post — equivalente a `wp fastest-cache clear --post_id=<ID>` mas
disparado pela UI sem passar pela CLI.
### 12.4 Integração WP-Polls (`inc/wp-polls.php`)
Se o plugin WP-Polls estiver activo, o WPFC intercepta a votação em
enquetes via AJAX próprio (`wpfc_wppolls_ajax_request`) para garantir que
o resultado do voto não fica "preso" numa página cacheada — sem isto, um
visitante veria sempre a enquete por votar mesmo depois de votar, porque
a página HTML estática não reflecte o estado de sessão.
---
## 13. Matriz Free vs Premium (addon `wp-fastest-cache-premium`)
Confirmado no código: `class_exists("WpFastestCachePowerfulHtml")` é o
gate central usado em `inc/admin.php` para decidir o que renderizar/gravar.
Essa classe só existe se o plugin separado
`wp-fastest-cache-premium/wpFastestCachePremium.php` estiver instalado e
activo (licença paga, `get_premium_version()` lê a versão do cabeçalho
**No site piloto desta secção (`emanuelalmeida.pt`) este addon NÃO está
instalado** — confirmado por `find` no `wp-content/plugins/` desse site,
que só devolve a pasta `wp-fastest-cache/` (Free). **Correcção 19-08-2026:
isto não é verdade para todo o bundle** — ver §16.1, `descomplicar.pt` tem
o Premium 1.7.7 instalado no disco (inactivo).
| Funcionalidade | Free | Premium | Onde no código |
|---|---|---|---|
| Page cache HTML, minify HTML/CSS, combine CSS, Gzip, Leverage Browser Caching | ✅ | ✅ | `inc/admin.php` (fora do gate) |
| Preload (crawler automático) | ✅ | ✅ | `inc/preload.php` |
| Exclusão de páginas/cookies/user-agents | ✅ | ✅ | `inc/cache.php`, `WpFastestCacheExclude` |
| Clearing Specific Pages | ✅ | ✅ | `inc/clearing-specific-pages.php` |
| CDN genérica (MaxCDN/Other) + Cloudflare nativa | ✅ | ✅ | `inc/cdn.php` |
| Varnish | ✅ | ✅ | `inc/varnish.php` |
| Cache Timeout (global e por regra) | ✅ | ✅ | `inc/admin.php`, `templates/timeout.php` |
| Disable Emojis | ✅ | ✅ | `inc/cache.php` |
| Mobile cache (separar cache mobile/desktop) | ✅ | ✅ | `wpFastestCacheMobile` |
| **Mobile Theme** (tema mobile dedicado) | ❌ | ✅ | `inc/admin.php:1233`, `wp-fastest-cache-premium/pro/library/mobile-cache.php` |
| **Minify HTML Plus / Minify Css Plus** | ❌ | ✅ | `inc/admin.php:1289,1311` |
| **Minify JS / Combine JS / Combine JS Plus** | ❌ | ✅ | `inc/admin.php:1330-1373` |
| **Render Blocking JS/CSS** | ❌ | ✅ | `inc/admin.php:1400`, doc: `wpfastestcache.com/premium/render-blocking-js/` |
| **Google Fonts optimizado** | ❌ | ✅ | `inc/admin.php:1426` |
| **Lazy Load** (imagens/iframes) | ❌ | ✅ | `inc/admin.php:1450`, `wp-fastest-cache-premium/pro/library/lazy-load.php` (incluído via `cache.php:987`) |
| **Delay JS** | ❌ | ✅ | `inc/admin.php:1488`, `wp-fastest-cache-premium/pro/library/delay-js.php` (incluído via `cache.php:875`) |
| **Image optimisation/compressão** | ❌ | ✅ | `wpFastestCache.php:214`, `WpFastestCacheImageOptimisation` |
| **Logs de actividade** | ❌ | ✅ | `wpFastestCache.php:1556`, `pro/library/logs.php` |
| **DB auto-cleanup** (limpeza de transients/revisões) | ❌ | ✅ | `inc/admin.php:2231`, `pro/templates/db-auto-cleanup.php` |
| **Static file caching avançado** | ❌ | ✅ | `wpFastestCache.php:249`, `pro/library/statics.php` |
**Gotcha de auditoria:** se uma option JSON `WpFastestCache` mostrar
chaves Premium marcadas `"on"` (ex. `wpFastestCacheMobileTheme`,
`wpFastestCacheLazyLoad`) **isso não prova que a funcionalidade está
activa** — só prova que a chave ficou gravada num momento em que o addon
Premium podia estar instalado, ou que veio de uma migração/import. A
única forma fiável de confirmar se o Premium está realmente activo é:
```bash
wp plugin list --path=$PATH --format=json | grep -i "fastest-cache"
# Se só aparecer "wp-fastest-cache" (sem "-premium"), todas as chaves Premium são no-op.
```
---
## 14. Gotchas / erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| "Limpei a cache e não mudou nada" | Confundir camadas — só correu `wp cache flush` (Redis) ou só `wp fastest-cache clear` (page cache), faltou Cloudflare | Sempre as 3 camadas juntas para mudanças visuais (§4) |
| `wp fastest-cache clear all` não purga Cloudflare | `WpFastestCacheCDN` sem entrada `cloudflare` com credenciais válidas — a chamada `CdnWPFC::cloudflare_clear_cache()` corre mas fica sem efeito por falta de email/API key/zone ID | Purgar Cloudflare à parte (App for Cloudflare® ou API) |
| Comando `wp fastest-cache` "command not found" | `inc/cli.php` só é carregado `if(defined('WP_CLI') && WP_CLI)` — falha se o WP-CLI não inicializar o plugin correctamente (ex. `--skip-plugins`) | Nunca usar `--skip-plugins` com este comando; confirmar `wp plugin is-active wp-fastest-cache` primeiro |
| `wp fastest-cache clear all and minified` dá "Wrong usage" | Sintaxe exacta exigida: exactamente `all`, `and`, `minified` como 3 argumentos posicionais separados por espaço — qualquer typo (`--minified`, `all-minified`) cai em `wrong_usage()` | Copiar o comando literal da doc, sem flags extra |
| Checkbox Premium (Lazy Load, Minify JS, Render Blocking...) não tem efeito nenhum apesar de aparecer marcado no JSON | Addon `wp-fastest-cache-premium` não está instalado — o checkbox nem chega a ser submetido no wp-admin (`disabled`), a chave só sobrevive na option se veio de outro contexto | Confirmar `wp plugin list` (§13) antes de investigar "porque é que a Lazy Load não funciona" |
| Minify JS/Combine JS activados "quebram" o site | Nos sites do bundle **estas opções nem estão disponíveis** (Premium-only) — se aparecerem activas nalgum sítio é sinal de config legada ou de outro plugin | Confirmar se o Premium está mesmo activo antes de desligar; se não estiver, a causa do problema é outra |
| `robots.txt`/`/llms.txt` não reflecte alteração recente | `max-age` alto no Cloudflare + cache HTML do WPFC — as duas mascaram a alteração na origem | Purgar WPFC + Cloudflare juntos (§4), não só um |
| `--post_id=` não limpa nada | ID de post inválido ou página não estava em cache (nunca visitada) | Confirmar que a página tem ficheiro em `wp-content/cache/all/<slug>/` (ou no caminho customizado de `WpFastestCachePathSettings`, §11) antes de assumir falha |
| Regra de exclusão gravada no wp-admin não bloqueia a cache em produção | Site em nginx (sem `.htaccess`) e `WpFastestCacheExclude` não está a ser lido correctamente pelo PHP, ou regex mal escapado (`prefix=regex`) | Testar a regra isoladamente com `wp option get WpFastestCacheExclude --format=json`, validar a regex fora do WordPress primeiro |
| Página agregadora (`/loja/`, `/blog/`) mostra conteúdo desatualizado mesmo com `New Post` activo | `wpFastestCacheNewPost_type` só limpa a página nova, não as páginas que a listam | Adicionar regra Clearing Specific Pages (§9) para essas páginas agregadoras |
| Cache Timeout Wizard "desaparece" depois de gravar | `DISABLE_WP_CRON` está `true` no `wp-config.php` — a regra fica registada como evento mas nunca dispara sozinha | Confirmar cron real do sistema a chamar `wp-cron.php`, ou usar `wp cron event run --due-now` manualmente/via systemd timer |
---
## 15. Decision tree — qual comando/opção usar
| Operação | Comando/Opção |
|---|---|
| Purgar tudo (HTML) | `wp fastest-cache clear all` |
| Purgar tudo incl. minificados (após deploy CSS/JS) | `wp fastest-cache clear all and minified` |
| Purgar só 1+ posts/páginas | `wp fastest-cache clear --post_id=ID[,ID...]` |
| Purgar object cache (Redis) | `wp cache flush` |
| Purgar edge (Cloudflare) | wp-admin App for Cloudflare® ou API Cloudflare directa (ou integração nativa WPFC se configurada, §10.2) |
| Ler config completa | `wp option get WpFastestCache --format=json` |
| Alterar 1 chave da config | `wp option update WpFastestCache '{"...json completo com a chave alterada..."}' --format=json` (é JSON simples, não serializado PHP — reescrever o objecto completo) |
| Confirmar se está a servir cache | `wp eval 'echo is_dir(WP_CONTENT_DIR."/cache/all") ? "sim" : "nao";'` |
| Ver regras de exclusão de páginas/cookies/UA | `wp option get WpFastestCacheExclude --format=json` (§7) |
| Ver regras de purga extra (páginas agregadoras) | `wp option get WpFastestCacheCSP` (§9) |
| Ver integrações CDN configuradas | `wp option get WpFastestCacheCDN --format=json` (§10) |
| Ver config Varnish | `wp option get WpFastestCacheVarnish` (§10.3) |
| Ver caminho de cache customizado | `wp option get WpFastestCachePathSettings` (§11) |
| Ver regras de Cache Timeout (por página) | `wp cron event list | grep wp_fastest_cache_` (§8.2) |
| Ver timeout global legado | `wp cron event list | grep 'wp_fastest_cache '` (hook exacto `wp_fastest_cache`, §8.1) |
| Confirmar se o addon Premium está activo | `wp plugin list --format=json | grep fastest-cache` (§13) |
| Bloquear cache de 1 página sem criar regra | Inserir `[wpfcNOT]` no conteúdo via botão do editor (§7.4) |
## 16. MCP construído (`wpfc`, projecto `mcp-wpfc`) — 19-08-2026, ao vivo
### 16.1 Estado real por site (corrige a afirmação de §13)
Verificado ao vivo via `wp plugin list --format=json` em 8 sites WordPress
reais do bundle (não 7 — `care.descomplicar.pt`, `ecommerce-demo.descomplicar.pt`
e `ecommerce.descomplicar.pt` são instalações WP reais adicionais sem WPFC):
| Site | `wp-fastest-cache` | `wp-fastest-cache-premium` | Cache HTML activa? |
|---|---|---|---|
| `emanuelalmeida.pt` | active, v1.5.0 | não instalado | sim (Free) |
| `starter.descomplicar.pt` | active, v1.5.0 | não instalado | sim (Free) |
| `ccv.descomplicar.pt` | active, v1.5.0 | não instalado | sim (Free) |
| `descomplicar.pt` (produção) | **inactive**, v1.5.0 | **inactive**, v1.7.7 | **não** — nem Free nem Premium activos |
| `e-commerce.descomplicar.pt` | inactive, v1.5.0 | não instalado | não |
| `care.descomplicar.pt` | não instalado | não instalado | não |
| `ecommerce-demo.descomplicar.pt` | não instalado | não instalado | não |
| `ecommerce.descomplicar.pt` | não instalado | não instalado | não |
`descomplicar.pt` é o único site confirmado com o addon Premium **no disco**
(1.7.7), mas está inactivo tal como o Free — a config antiga em
`WpFastestCache` (§1) sobreviveu de um período em que esteve activo, incluindo
a chave Premium `wpFastestCacheGoogleFonts: "on"` (só tem efeito com
`WpFastestCachePowerfulHtml` a existir, ou seja, nunca teve efeito real desde
que o Premium está inactivo).
**Pergunta em aberto de §16.2 (versão anterior) respondida:** `descomplicar.pt`
não tem NENHUM plugin de HTML page-cache activo hoje (confirmado via
`wp plugin list` — nem WPFC Free/Premium, nem outro concorrente; `webp-express`
é o único plugin de optimização activo). Também sem drop-in
`object-cache.php` (Redis inactivo) e sem headers `cache-control`/`x-cache`
na resposta ao vivo — o site depende só da edge Cloudflare (proxy laranja)
para qualquer camada de cache. **Conclusão: seguro construir e testar o MCP
em produção; activar o Premium continua por decidir à parte** (fora do
âmbito desta sessão — activação de plugin já é coberta pelo `emcp-tools
activate-plugin` genérico, não precisa deste MCP).
### 16.2 Achado crítico de implementação: formato de armazenamento das options
**As 7 options WPFC (`WpFastestCache`, `WpFastestCacheExclude`,
`WpFastestCacheCSP`, `WpFastestCacheCDN`, `WpFastestCacheVarnish`,
`WpFastestCachePathSettings`, `WpFastestCacheToolbarSettings`) são strings
PHP planas cujo conteúdo é texto JSON — NÃO são options JSON tipadas do
WP-CLI.** Confirmado via `wp option get WpFastestCache` (sem `--format=json`):
devolve um array PHP `{0 => '{', 1 => '"', 2 => 'w', ...}` — ou seja, o
*valor* já é uma string de caracteres, e o WP-CLI está a mostrar essa string
char a char porque o plugin fez `json_encode($array)` ao gravar, não
`update_option()` sobre um array PHP nativo.
**Bug reproduzido e corrigido durante o build:** a primeira versão do MCP
usava `wp option get/update --format=json` (o padrão copiado de
`mcp-element-pack`, correcto lá porque as options do Element Pack Pro são
arrays PHP nativos). Em `WpFastestCache` isto causa **dupla codificação**:
1. Leitura com `--format=json` de uma string já-JSON devolve essa string
**entre aspas** (`"{\"wpFastestCacheStatus\": \"on\", ...}"`). Um único
`JSON.parse()` do lado do cliente devolve uma **string**, não um objecto.
2. `{...string, ...novasChaves}` em JavaScript faz *spread* de uma string
como se fosse um array de caracteres — produz um objecto `{0: '{', 1: '"',
2: 'w', ...}` com as novas chaves misturadas.
3. Escrever esse objecto de volta com `--format=json` grava um array PHP
indexado por número na base de dados — **corrompe a option**, o plugin
deixa de conseguir lê-la (`json_decode()` falha silenciosamente,
`getOption()` volta a defaults).
Isto **corrompeu de facto `WpFastestCache` em `descomplicar.pt`** durante o
teste (verificado via `wp option get` sem `--format=json`, mostrando o array
de caracteres). Restaurado imediatamente a partir do valor exacto capturado
antes do bug (`wp option get --format=json` da leitura de diagnóstico
original desta sessão) — confirmado idêntico ao original, só difere um `\n`
final inofensivo.
**Correcção aplicada:** `getOption`/`setOption` do MCP nunca usam
`--format=json`. Leitura: `wp option get '<key>' --path=... --allow-root`
(raw), depois `JSON.parse()` uma única vez no cliente. Escrita: o valor
JavaScript é `JSON.stringify()`-ado e enviado como string crua (via base64
em stdin) para `wp option update '<key>' --path=... --allow-root` (sem
`--format=json`), replicando exactamente o que o próprio plugin faz.
**Lição para MCPs futuros que envolvam `wp option`:** nunca assumir que
`--format=json` é seguro só porque o valor "parece JSON" no `get` — testar
sempre `wp option get <key>` **sem** a flag primeiro, para confirmar se o
tipo real na BD é uma string-com-JSON-dentro (como aqui) ou um array PHP
nativo (como no Element Pack Pro, `element_pack_active_modules` etc.) antes
de escolher o padrão de leitura/escrita.
### 16.3 Sites cobertos e tools (18)
Projecto `/media/ealmeida/Dados/Dev/mcp-wpfc`, registado em
`~/.omp/agent/mcp.json` como `wpfc`. Parâmetro `site` em cada tool (alias
conhecido de §16.1 ou path absoluto) — cobre os 8 sites reais do bundle
numa só ligação, ao contrário de `element-pack`/`rank-math-mcp` (uma
ligação fixa por site). **Como executar cada tool: ver skill `mcp-wpfc`** — esta secção fica só
com o desenho e a verificação; o "como usar" tool a tool vive na skill dedicada.
- `wpfc_list_sites` — sites conhecidos com path e nota de estado.
- `wpfc_get_status` — instalado/activo/versão, Free e Premium.
- `wpfc_get_config` / `wpfc_set_config` — option `WpFastestCache` completa;
`wpfc_set_config` faz merge (read-modify-write) e **recusa gravar
qualquer chave só-Premium** (lista em `PREMIUM_ONLY_CONFIG_KEYS`,
§1.1 desta skill) a menos que confirme `wp-fastest-cache-premium` activo
ou receba `force_premium: true`.
- `wpfc_purge_cache` — `all` / `all_minified` / `post` (`--post_id`).
- `wpfc_get_exclude` / `wpfc_set_exclude` — `WpFastestCacheExclude` (§7).
- `wpfc_get_csp` / `wpfc_set_csp` — `WpFastestCacheCSP` (§9).
- `wpfc_get_cdn` / `wpfc_set_cdn` — `WpFastestCacheCDN` (§10.1).
- `wpfc_get_varnish` / `wpfc_set_varnish` — `WpFastestCacheVarnish` (§10.3).
- `wpfc_get_pathSettings` / `wpfc_set_pathSettings` — `WpFastestCachePathSettings` (§11).
- `wpfc_get_toolbarSettings` / `wpfc_set_toolbarSettings` — `WpFastestCacheToolbarSettings` (§12.2).
As 6 options estruturadas (`set_exclude`/`csp`/`cdn`/`varnish`/`pathSettings`/`toolbarSettings`)
são **substituição completa**, não merge — enviar sempre a lista/objecto
desejado por inteiro.
**Testado ponta-a-ponta contra produção (19-08-2026):** `wpfc_get_status`
em `descomplicar` e `ccv` confirmou os dados reais de §16.1;
`wpfc_set_config` com uma chave Premium (`wpFastestCacheGoogleFonts`) foi
correctamente recusado em `descomplicar` (Premium inactivo); com uma chave
Free (`wpFastestCacheDisableEmojis: "on"`, já `"on"` — sem alterar
comportamento) escreveu e releu correctamente, confirmado por leitura SSH
directa fora do MCP; `wpfc_get_exclude`/`wpfc_set_exclude` testado em
`ccv` (fallback `[]` → escrita `[]` → releitura `[]`); `wpfc_purge_cache`
executado com sucesso em `ccv` (`Success: The cache has been cleared!`).
---
**Fonte:** `CONFIG-Plugins-Referencia.md` §3 (WP Fastest Cache) +
`BUNDLE-Excelencia-WP.md` §2.2/§2.4 (modelo de performance, achado do
purge Cloudflare+WPFC no `robots.txt`) + leitura directa e completa do
código-fonte do plugin v1.5.0 (Free, sem addon Premium instalado) no
servidor `server.descomplicar.pt`, site piloto `emanuelalmeida.pt`:
`wpFastestCache.php` (2746 linhas, 100% lido), `inc/admin.php` (2620
linhas, secções de settings/save/render lidas integralmente), `inc/cache.php`
(1460 linhas), `inc/preload.php` (894 linhas), `inc/css-utilities.php`
(766 linhas), `inc/cdn.php` (650 linhas, 100% lido), `inc/js-utilities.php`
(340 linhas), `inc/single-preload.php` (346 linhas), `inc/varnish.php`
(177 linhas, 100% lido), `inc/clearing-specific-pages.php` (100% lido),
`inc/admin-toolbar.php`, `inc/column.php`, `inc/wp-polls.php`, `inc/cli.php`
(100% lido), e todos os templates relevantes em `templates/` e
`templates/cdn/` (100% lidos) + `wp help fastest-cache` e `wp option
list --search='WpFastestCache*'` ao vivo em `emanuelalmeida.pt`,
16-08-2026.
+437
View File
@@ -0,0 +1,437 @@
---
name: wp-font-perf
description: Cortar o peso de webfonts e de imagens em background CSS em sites WordPress/Elementor da frota Descomplicar, com medição real de Performance. Cobre o subsetting de fontes de ícones (IconSax, Font Awesome, eicons, elementskit) com `pyftsubset` e rede de segurança por `unicode-range` complementar, a substituição de folhas do Elementor via `style_loader_tag` (e porque um `wp_dequeue_style` não chega), WebP em `background-image` que o WebP Express não cobre, preload de fonte variável para travar CLS, e as duas armadilhas que partem o site ou invalidam medições (purge do Cloudflare avariado, apagar `wpfc-minified`). Usar quando "performance wordpress", "lighthouse", "LCP alto", "CLS", "fontes pesadas", "subset de fontes", "icon fonts", "webfonts", "peso da página", "optimizar site", "core web vitals", "PageSpeed".
---
# /wp-font-perf — Peso de webfonts e imagens CSS, com medição real
Reduzir peso real de páginas WordPress/Elementor na frota Descomplicar. Validado
em piloto em `emanuelalmeida.pt` a 17-08-2026: fontes de ícones **1256KB → 10KB**,
peso total **3125KB → 1258KB**, Performance mobile **60 → 68**, desktop **79 → 88**,
LCP mobile **14,2s → 6,1s**, CLS **0 estável**.
**Fonte:** `04-Stack/02.04-Sistemas/71.Seguranca/BUNDLE-Excelencia-WP.md` §2.2.1
e §2.2.2 (relato completo, incluindo o incidente).
---
## 0. Antes de tudo: medir Performance a sério
**O `lighthouse_audit` do MCP chrome-devtools NÃO mede a categoria Performance.**
Exclui-a por desenho. Um relatório "100/100/100/100" desse MCP pode conviver com
um LCP de 14 segundos — aconteceu, duas sessões seguidas.
```bash
# Instalar/usar Lighthouse local (não depende da quota da API do PSI)
npx -y lighthouse@12 "https://SITE/" --only-categories=performance \
--form-factor=mobile --screenEmulation.mobile --throttling-method=simulate \
--output=json --output-path=/tmp/lh.json --chrome-flags="--headless=new --no-sandbox" --quiet
# desktop
npx -y lighthouse@12 "https://SITE/" --only-categories=performance \
--form-factor=desktop --screenEmulation.disabled --throttling-method=simulate \
--throttling.rttMs=40 --throttling.throughputKbps=10240 --throttling.cpuSlowdownMultiplier=1 \
--output=json --output-path=/tmp/lhd.json --chrome-flags="--headless=new --no-sandbox" --quiet
```
**Regra de evidência: mediana de 5 corridas, nunca uma.** A variância entre
corridas isoladas da mesma configuração chega a **25 pontos** (medido: 69 e 62
para o mesmo estado). Uma corrida não é evidência de nada.
Peso por tipo (é aqui que se decide onde atacar):
```bash
python3 -c "
import json, collections
lr=json.load(open('/tmp/lh.json'))
r=(lr['audits']['network-requests'].get('details') or {}).get('items',[])
by=collections.Counter(); n=collections.Counter()
for x in r:
t=x.get('resourceType') or 'other'; by[t]+=x.get('transferSize',0); n[t]+=1
print('total %.0fKB / %d pedidos' % (sum(by.values())/1024, len(r)))
for t,v in by.most_common(): print(f' {t:12} {n[t]:>3} {v/1024:>7.0f}KB')
print('perf', round(lr['categories']['performance']['score']*100))
for k in ('largest-contentful-paint','cumulative-layout-shift','first-contentful-paint'):
print(' ', k, lr['audits'][k].get('displayValue'))
"
```
---
## 1. Fontes de ícones: o suspeito número um
Sites Elementor com icon packs acumulam megabytes para desenhar dezenas de
glifos. No piloto: **1,2MB para 27 glifos**.
|Fonte|Original|Subset|Glifos usados / existentes|
|---|---|---|---|
|IconSax|355.632 B **TTF**|3.172 B|12 / 890|
|IconSax-Fino|355.692 B **TTF**|536 B|1 / 906|
|eicons|108.000 B|636 B|3 / 515|
|fa-brands-400|81.612 B|1.028 B|4|
|fa-solid-900|78.196 B|780 B|8|
|elementskit (ekiticons)|252KB|**0** (suprimido)|0 / 12|
Icon packs custom carregados pelo Elementor (`uploads/elementor/custom-icons/`)
vêm frequentemente em **TTF/WOFF sem woff2** — formato sem compressão web.
### 1.1 Levantar os glifos em uso em TODO o site
Nunca só na homepage. Varrer todos os permalinks publicados:
```bash
ssh server "sudo -u USER /usr/local/bin/wp post list --post_type=page,post \
--post_status=publish --field=url --path=WEBROOT 2>/dev/null"
```
Depois `curl` a cada URL e recolher as classes (`icon-sax-*`, `fa-*`, `eicon-*`).
Mapear classe → codepoint nos CSS (`content:"\eXXX"`).
**Método mais fiável, no browser** (apanha o que vem de CSS de widgets e não só
de classes no HTML) — itera os pseudo-elementos e lê o `content` computado:
```js
const want = /Font Awesome|eicons|IconSax|elementskit/i;
const hits = {};
for (const el of document.querySelectorAll('*'))
for (const ps of ['::before','::after']) {
const cs = getComputedStyle(el, ps);
if (!want.test(cs.fontFamily || '')) continue;
for (const ch of (cs.content||'').replace(/^["']|["']$/g,'')) {
const cp = ch.codePointAt(0);
if (cp >= 0xE000) (hits[cs.fontFamily.replace(/["']/g,'').split(',')[0].trim()+'|'+cs.fontWeight] ??= new Set()).add(cp.toString(16));
}
}
```
### 1.2 Gerar os subsets
Requer `fonttools` + `brotli` (`pyftsubset` em `~/.local/bin`):
```bash
pyftsubset FONTE.ttf --unicodes=U+E915,U+E969,... --flavor=woff2 \
--layout-features= --no-hinting --desubroutinize --output-file=NOME.subset.woff2
# fonte completa convertida para woff2 (fallback mais leve que o TTF original)
pyftsubset FONTE.ttf --unicodes=U+E000-F8FF --flavor=woff2 \
--layout-features= --no-hinting --output-file=NOME.full.woff2
```
### 1.3 Rede de segurança obrigatória: `unicode-range` complementar
**Nunca servir só o subset.** Declarar duas `@font-face` para a mesma família:
o subset com o `unicode-range` dos glifos em uso, e a fonte completa com o
`unicode-range` **complementar**. Assim um ícone novo adicionado no Elementor
não fica "tofu" — o browser vai buscar a completa só para esse codepoint — e
enquanto isso não acontecer não se paga um byte por ela.
```python
def complement(used, lo=0xE000, hi=0xF8FF):
u=sorted(int(c,16) for c in used); out=[]; s=lo
for c in u:
if c>s: out.append((s,c-1))
s=c+1
if s<=hi: out.append((s,hi))
return out # formatar como U+XXXX ou U+XXXX-YYYY
```
**Confirmar que a completa não é descarregada:**
```js
[...document.fonts].filter(f=>/IconSax|Font Awesome|eicons/.test(f.family))
.map(f=>f.family+'|'+f.weight+'|'+f.status)
// esperado: subsets 'loaded', completas 'unloaded'
```
### 1.4 Substituir a folha, não acrescentar `@font-face` depois
**Um `wp_dequeue_style` não alcança estas folhas** e um `@font-face` acrescentado
na fase de enqueue perde: o Elementor enfileira as folhas de icon library
**durante o render do body** (há widgets com esses ícones no `_elementor_data`),
pelo que `wp_style_is()` devolve `false`, o CSS nosso sai **antes** do original no
documento, perde o desempate "última declaração vence" e **o TTF continua a ser
descarregado** (medido, não presumido).
Padrão correcto:
1. Copiar a folha do Elementor, **manter todas as regras de classe** e trocar só
os `@font-face`. Verificar que não há `url()` fora dos `@font-face` antes de
cortar (backgrounds embutidos).
2. Enfileirar a cópia com `wp_enqueue_style`.
3. Suprimir a original na **impressão**, imune à ordem de enqueue:
```php
add_filter( 'style_loader_tag', function ( $tag, $handle ) {
if ( ! is_admin() && in_array( $handle, array(
'elementor-icons-IconSax', 'elementor-icons-IconSax-Fino',
'elementor-icons-ekiticons', 'elementor-icons-fa-brands',
'elementor-icons-fa-solid', 'elementor-icons',
), true ) ) { return ''; }
return $tag;
}, 10, 2 );
```
Handles reais confirmam-se no HTML: `grep -oE "id='[^']*(icon|ekit)[^']*-css'"`.
### 1.5 Icon packs 100% mortos
`elementor-icons-ekiticons` traz `elementskit.woff` (252KB) e um CSS que o
Lighthouse dá como 100% não usado: define 12 ícones do **painel** do ElementsKit
(tiktok, x-twitter, scroll-reveal, smart-post-list…). Antes de suprimir,
confirmar que as classes presentes no HTML não têm regra:
```bash
ssh server "grep -rlE '(icon-checked|icon-menu-11):before' WEBROOT/wp-content/plugins/ WEBROOT/wp-content/themes/"
# sem resultado = classes órfãs de versão antiga, não desenham glifo
```
---
## 2. CLS: preload da fonte de TEXTO, não das de ícones
Ao reduzir o preload, o heading do hero volta a re-layoutar quando a fonte de
texto chega (no piloto: **CLS 0,213**, com 0,2055 atribuídos a um único `h4`).
**Não preloadar tudo** — foi o erro que criou o problema de LCP. Preloadar **um**
ficheiro: as Google Fonts modernas são **variáveis**, e um só subset latin cobre
os pesos 200-900.
```bash
# descobrir qual ficheiro serve latin + todos os pesos
ssh server "grep -oE '@font-face\{[^}]*\}' WEBROOT/wp-content/uploads/elementor/google-fonts/css/nunito.css" \
# procurar o que tem unicode-range U+0000-00FF (latin) e font-style: normal
```
39KB resolveram o CLS que 1410KB resolviam antes. Confirmar com **5 corridas**,
não uma.
---
## 3. WebP em `background-image`: ponto cego do WebP Express
**O WebP Express reescreve as tags `<img>` do HTML mas não os `background-image`
do CSS.** E em CWP o **nginx serve os estáticos directamente, sem passar pelo
`.htaccess` do plugin**, pelo que também não há negociação por `Accept`.
Resultado no piloto: `foto-1.png` servia **458KB** (36% da página, e era o recurso
LCP) apesar de existir `webp-express/webp-images/uploads/foto-1.png.webp` com
181KB, gerado pelo plugin e nunca servido.
Detectar:
```bash
# candidatos: imagens grandes referenciadas em CSS do Elementor
ssh server "grep -rl 'NOME.png' WEBROOT/wp-content/uploads/elementor/css/"
ssh server "find WEBROOT/wp-content/webp-express -name 'NOME*' -printf '%s %p\n'"
```
Corrigir com `image-set()` (o PNG fica como segundo candidato para browsers sem
suporte). O `!important` é necessário porque o `post-N.css` do Elementor é
enfileirado depois:
```php
$css = sprintf(
'%s{background-image:image-set(url("%s") type("image/webp"),url("%s") type("image/png")) !important}',
$selector, $webp_url, $png_url
);
wp_register_style( 'desc-lcp-webp', false, array(), '1.0.0' );
wp_enqueue_style( 'desc-lcp-webp' );
wp_add_inline_style( 'desc-lcp-webp', $css );
```
Preloadar também a imagem LCP (`background-image` só é descoberta depois de o CSS
ser descarregado e parseado — Load Delay medido de 8,2s = 87% do LCP):
```php
printf('<link rel="preload" as="image" fetchpriority="high" href="%s">', esc_url($url));
```
---
## 4. Outros cortes verificados
- **`dashicons` no frontend:** 750ms de render-blocking + 35KB para **zero**
glifos `dashicons-*` no HTML. Remover só para visitantes não autenticados (a
admin bar precisa dele). `grep -c 'dashicons-'` na página confirma o uso real.
- **Google Fonts remoto duplicado:** `astra-google-fonts` pede o Nunito a
`fonts.googleapis.com` (render-blocking ~770ms + ligação a terceiros) quando o
Elementor já o serve local. Confirmar que o local cobre os pesos antes de
desenfileirar (`grep -c 'font-weight' nunito.css`).
---
## 5. 🔴 Armadilhas que partem o site ou invalidam medições
### 5.1 O purge do Cloudflare reporta sucesso sem purgar
`wp app-for-cf purge-cache` devolve sempre `Success: Cloudflare cache purged.` e
**não purga**. Medido em `emanuelalmeida.pt` (17-08-2026):
```
antes: cf-cache-status: HIT age: 27
comando: Success: Cloudflare cache purged.
depois: cf-cache-status: HIT age: 34 -> 37 -> 40 # nunca invalidou
```
**É bug do comando, não das credenciais.** O token do plugin verifica `active` e
faz `purge_everything` pela API sem problema (`success: true`, `MISS` na visita
seguinte). Com `cfPageCachingSeconds=21600`, as alterações ficam **até 6 horas
invisíveis** — medições no URL limpo medem HTML pré-alteração.
**Purge que funciona** (ferramenta da frota; lê as credenciais do próprio plugin,
resolve a zona real e confirma o efeito, com exit≠0 se continuar `HIT`):
```bash
04-Stack/02.04-Sistemas/71.Seguranca/tools/cf-purge.py \
--account ealmeida --path /home/ealmeida/emanuelalmeida.pt \
--url https://emanuelalmeida.pt/
```
**⚠️ Armadilha ao diagnosticar: há vários plugins Cloudflare com credenciais no
mesmo site.** Registei duas vezes a causa errada ("token inválido", "falta
permissão `Zone → Cache Purge`") por testar as credenciais do plugin errado.
|Opção|Plugin|
|---|---|
|`app_for_cf.cloudflareAuth.token` (`cfut_…`, 53 car.) + `app_for_cf.cfZoneId`|**App for Cloudflare** — as que contam|
|`swcfpc_config.cf_apitoken` / `cf_zoneid`|Super Page Cache — pode ter token inválido e zona errada|
|`wp_turbosafe_cloudflare_api_token`|wp-turbosafe — cifrado, inutilizável|
**Confirmar sempre no código de onde vem a credencial** antes de a declarar
inválida — `app-for-cf` usa `$this->option('cloudflareAuth.token')` em
`src/DigitalPoint/Cloudflare/Api/Cloudflare.php`. E distinguir os erros da API:
`Invalid API Token` (1000) = o valor não corresponde a nenhum token activo;
falta de permissão tem outro código. Se houver `zone_id` divergentes,
`GET /zones?name=<dominio>` desempata.
### 5.2 CLS: o CSS do Elementor sai **depois** de `</head>`
Em sites Elementor, o CSS de cada widget é enfileirado **durante o render do
body** → o browser pinta o header sem estilos e volta a fazer layout quando as
folhas chegam. Em `emanuelalmeida.pt` eram **78KB em 7 folhas** e dava
**CLS 0,45–1,27 em 8 de 10 cargas** (limite bom = 0,1; pico medido 1,319).
**Diagnóstico em dois comandos.** Primeiro, contar folhas fora do head:
```bash
python3 - <<'EOF'
import re,subprocess,time
h=subprocess.run(['curl','-s',f'https://SITE/?x={int(time.time())}'],capture_output=True,text=True).stdout
he=h.find('</head>')
L=[(m.start(),m.group(0)) for m in re.finditer(r"<link[^>]*rel=['\"]stylesheet['\"][^>]*>",h)]
print("HEAD:",len([1 for p,_ in L if p<he]),"| BODY:",len([1 for p,_ in L if p>he]))
for p,l in L:
if p>he: print(" ", (re.search(r"id='([^']+)'",l) or re.search(r'id="([^"]+)"',l)).group(1))
EOF
```
Depois, medir o CLS **a sério** — não confiar no Lighthouse com
`--throttling-method=simulate`, que devolvia 0 quase sempre neste caso. Usar
`PerformanceObserver`, tab nova por corrida, 6 corridas:
```js
await page.evaluateOnNewDocument(()=>{window.__s=[];
new PerformanceObserver(l=>{for(const e of l.getEntries()){
if(!e.hadRecentInput) window.__s.push({v:e.value,t:Math.round(e.startTime),
src:(e.sources||[]).map(s=>s.node&&s.node.tagName+'.'+String(s.node.className||'').split(' ')[0])});
}}).observe({type:'layout-shift',buffered:true});});
await tab.goto(URL,{waitUntil:'networkidle2'});
await new Promise(r=>setTimeout(r,3000));
```
⚠️ **Dois erros de método a evitar** (cometi ambos): registar `evaluateOnNewDocument`
várias vezes na mesma tab **soma observers** e inflaciona o total (vi 5,15 onde o
shift real era 0,79); e um `first-paint` ~100ms antes do shift, com
`domInteractive` no mesmo instante, é a assinatura de CSS no body — não de fontes.
**Correcção:** mu-plugin `elementor-css-head.php` (cópia canónica em
`04-Stack/02.04-Sistemas/71.Seguranca/mu-plugins/`). Aprende que handles saíram
depois do head (`wp_styles()->done` no fecho do `wp_head` vs. no `wp_footer`),
guarda num transient por URL (12h) e antecipa-os para o head na visita seguinte.
Resultado: **CLS 0 em 6/6**, 0 nas medianas Lighthouse mobile/desktop, 0 na
auditoria MCP.
**❌ Não perder tempo com:** desligar `elementor_experiment-e_optimized_css_loading`
(no Elementor 4.x já não desliga o carregamento condicional — as folhas continuam
no body); e descartar o WP Meteor por A/B antes de acusar o defer de JS (`?wp-meteor-nooptimize=true`
deu o mesmo CLS, logo não era ele).
**🔴 Aquecimento obrigatório de DUAS passagens depois do deploy** — no primeiro
pedido de cada URL o mu-plugin ainda não aprendeu nada, e é esse HTML que fica
em cache:
```bash
wp post list --post_type=page,post --post_status=publish --field=url --path=$SITE > /tmp/urls.txt
ssh server "rm -rf $SITE/wp-content/cache/all/*"
while read -r u; do curl -s -o /dev/null "$u?p1=$(date +%s%N)"; done < /tmp/urls.txt # aprende
ssh server "rm -rf $SITE/wp-content/cache/all/*"
while read -r u; do curl -s -o /dev/null "$u?p2=$(date +%s%N)"; done < /tmp/urls.txt # HTML bom
while read -r u; do curl -s -o /dev/null -w "%{http_code} $u\n" "$u"; done < /tmp/urls.txt
```
**Custo aceite:** as folhas passam a bloquear no head em vez do body — mesmos
bytes, mais cedo. Performance mediana mobile 68→64, desktop 88→86 (dentro da
variância). CLS é Core Web Vital e estava 4× a 12× acima do limite: a troca paga-se.
### 5.3 NUNCA apagar `wpfc-minified`
Com `MinifyCss` do WP Fastest Cache ligado, os `<link>` apontam a
`wp-content/cache/wpfc-minified/<hash>/hcd9a.css`. Apagar essa pasta enquanto
existe HTML cacheado (no WPFC **ou no Cloudflare**) a referenciar os hashes
antigos → **CSS a 404 em massa, site sem estilos**. Foi assim que o piloto
apareceu desfigurado ao utilizador.
```bash
# ERRADO
rm -rf WEBROOT/wp-content/cache/wpfc-minified/*
# CERTO — forçar HTML novo sem tocar nos ficheiros minificados
rm -rf WEBROOT/wp-content/cache/all/*
```
### 5.4 Não filtrar a saída do `wp_head` com `ob_start`
Tentar remover uma tag já emitida com `ob_start`/`ob_get_clean` no `wp_head`
partiu o `<head>`. Se a origem de um preload órfão não se encontra, **deixar
documentado em vez de mexer em buffers** — no piloto eram 38KB, não valia.
---
## 6. Verificação de fecho (obrigatória)
1. **Todas as folhas do HTML devolvem conteúdo** — foi isto que apanhou o
incidente sem ambiguidade:
```bash
python3 - <<'EOF'
import subprocess, re
h=subprocess.run(['curl','-s','https://SITE/'],capture_output=True,text=True).stdout
links=re.findall(r"<link rel='stylesheet' id='([^']+)' href='([^']+)'", h)
bad=[]
for hid,href in links:
u = href if href.startswith('http') else 'https:'+href
o=subprocess.run(['curl','-s','-o','/dev/null','-w','%{http_code} %{size_download}',u],capture_output=True,text=True).stdout.split()
if o[0]!='200' or int(o[1])==0: bad.append((hid,o))
print('folhas:',len(links),'| com problema:',len(bad),bad)
EOF
```
2. **Zero glifos em falta, em várias páginas** — medir a largura de cada
codepoint em uso contra a largura de um codepoint ausente ("tofu"):
```js
const c=document.createElement('canvas').getContext('2d');
const w=(fam,wt,cp)=>{c.font=wt+' 32px "'+fam+'"';return c.measureText(String.fromCharCode(parseInt(cp,16))).width;};
const tofu=w('IconSax','400','2764');
// falha se w(fam,wt,cp) === tofu
```
3. **Screenshot** do hero e da secção de ícones.
4. **CLS em 5 corridas**, não uma.
---
## 7. Estado do rollout
Aplicado só em **`emanuelalmeida.pt`** (piloto, 17-08-2026). Pendente #19 do
`BUNDLE-Excelencia-WP.md`: replicar nos outros 6 sites. O padrão do WebP em
background CSS (§3) e o `dashicons` (§4) são os mais prováveis de existirem em
todos.
+213 -3
View File
@@ -1,12 +1,11 @@
---
name: wp-hardening
description: Aplicar hardening de seguranca standard a sites WordPress via SSH. Desactiva XML-RPC, file editing, directory listing, esconde versao WP, verifica permissoes e security headers.
layer: wiki
---
# /wp-hardening - Hardening WordPress
Aplica medidas de hardening standard a sites WordPress no servidor CWP (176.9.3.158).
Aplica medidas de hardening standard a sites WordPress no servidor CWP (server.descomplicar.pt).
> **Nota:** Esta skill APLICA alteracoes. Para auditar sem alterar, usar `/security-check`.
@@ -155,4 +154,215 @@ grep -q "DISALLOW_FILE_EDIT" ${WP_PATH}/wp-config.php || \
# Directorios: 755
find ${WP_PATH} -type d -exec chmod 755 {} \;
# Ficheiros
# Ficheiros: 644
find ${WP_PATH} -type f -exec chmod 644 {} \;
# wp-config.php: 440 (leitura apenas para owner e grupo)
chmod 440 ${WP_PATH}/wp-config.php
# Corrigir ownership
chown -R ${WP_USER}:${WP_USER} ${WP_PATH}
```
#### 2.4 Esconder versao WordPress
```bash
# Adicionar ao functions.php do tema activo (via wp-cli)
THEME_PATH=$(sudo -u ${WP_USER} wp theme path --path=${WP_PATH})/$(sudo -u ${WP_USER} wp theme list --status=active --field=name --path=${WP_PATH})
# Verificar se ja tem o filtro
grep -q "remove_action.*wp_head.*wp_generator" ${THEME_PATH}/functions.php 2>/dev/null
if [ $? -ne 0 ]; then
# Alternativa robusta: usar mu-plugin (nao se perde em updates de tema)
mkdir -p ${WP_PATH}/wp-content/mu-plugins
cat > ${WP_PATH}/wp-content/mu-plugins/dsc-hardening.php << 'MUEOF'
<?php
/**
* Descomplicar - WordPress Hardening
* Esconde versao WP, desactiva XML-RPC via filtro, remove headers desnecessarios.
*/
// Esconder versao WP
remove_action('wp_head', 'wp_generator');
add_filter('the_generator', '__return_empty_string');
// Remover versao dos scripts e styles
function dsc_remove_version_scripts_styles($src) {
if (strpos($src, 'ver=')) {
$src = remove_query_arg('ver', $src);
}
return $src;
}
add_filter('style_loader_src', 'dsc_remove_version_scripts_styles', 9999);
add_filter('script_loader_src', 'dsc_remove_version_scripts_styles', 9999);
// Desactivar XML-RPC via filtro (complemento ao .htaccess)
add_filter('xmlrpc_enabled', '__return_false');
// Remover link do REST API do header (opcional, reduz surface)
remove_action('wp_head', 'rest_output_link_wp_head');
remove_action('wp_head', 'wp_oembed_add_discovery_links');
remove_action('wp_head', 'rsd_link');
remove_action('wp_head', 'wlwmanifest_link');
remove_action('wp_head', 'wp_shortlink_wp_head');
MUEOF
chown ${WP_USER}:${WP_USER} ${WP_PATH}/wp-content/mu-plugins/dsc-hardening.php
chmod 644 ${WP_PATH}/wp-content/mu-plugins/dsc-hardening.php
fi
```
#### 2.5 Desactivar directory listing
```bash
# Adicionar ao .htaccess se nao existir
grep -q "Options -Indexes" ${WP_PATH}/.htaccess || \
sed -i '1i Options -Indexes' ${WP_PATH}/.htaccess
```
#### 2.6 Security headers no .htaccess
```bash
# Verificar e adicionar headers em falta
if ! grep -q "X-Frame-Options" ${WP_PATH}/.htaccess; then
cat >> ${WP_PATH}/.htaccess << 'HTEOF'
# BEGIN Security Headers
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</IfModule>
# END Security Headers
HTEOF
fi
```
#### 2.7 Proteger debug.log
```bash
# Se debug.log existe, bloquear acesso
if [ -f "${WP_PATH}/wp-content/debug.log" ]; then
cat >> ${WP_PATH}/wp-content/.htaccess << 'DLEOF'
<Files debug.log>
Require all denied
</Files>
DLEOF
fi
# Desactivar WP_DEBUG se estiver activo em producao
grep -q "define.*WP_DEBUG.*true" ${WP_PATH}/wp-config.php && \
sed -i "s/define.*'WP_DEBUG'.*true.*/define('WP_DEBUG', false);/" ${WP_PATH}/wp-config.php
```
### Passo 3: Verificar estado (depois)
Re-executar todos os testes do Passo 1 e registar como DEPOIS.
### Passo 4: Verificar site funcional
```bash
# Verificar que o site responde correctamente
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://${SITE})
echo "HTTP: ${HTTP_CODE}"
# Verificar conteudo por erros criticos
curl -s https://${SITE} | grep -i "critical error\|fatal error\|error establishing" | head -3
```
**Se o site reportar erro apos hardening:** reverter imediatamente usando os backups (.bak).
### Passo 5: Corrigir ownership final
```bash
chown -R ${WP_USER}:${WP_USER} ${WP_PATH}
```
---
## Output Template
```markdown
# WP Hardening Report - [site.pt]
**Data:** YYYY-MM-DD
**Modo:** Aplicado / Dry Run
---
## Checklist Hardening
| # | Item | Antes | Depois | Accao |
|---|------|-------|--------|-------|
| 1 | XML-RPC desactivado | / | / | .htaccess + mu-plugin |
| 2 | File editing desactivado | / | / | wp-config.php |
| 3 | Permissoes dirs (755) | / | / | chmod recursivo |
| 4 | Permissoes ficheiros (644) | / | / | chmod recursivo |
| 5 | Versao WP escondida | / | / | mu-plugin |
| 6 | Directory listing desactivado | / | / | .htaccess |
| 7 | Security headers | / | / | .htaccess |
| 8 | wp-config.php (440) | / | / | chmod |
| 9 | debug.log protegido | / | / | .htaccess |
| 10 | WP_DEBUG desactivado | / | / | wp-config.php |
---
## Ficheiros alterados
- `wp-config.php` (DISALLOW_FILE_EDIT, WP_DEBUG)
- `.htaccess` (XML-RPC, directory listing, security headers)
- `wp-content/mu-plugins/dsc-hardening.php` (criado)
## Backups criados
- `.htaccess.bak.YYYYMMDD`
- `wp-config.php.bak.YYYYMMDD`
## Verificacao pos-hardening
- HTTP: [200/301/etc]
- Erros criticos: [Nenhum / Lista]
---
*Gerado via /wp-hardening v1.0.0 - YYYY-MM-DD*
```
---
## Notas importantes
1. **Backups obrigatorios** antes de qualquer alteracao
2. **mu-plugin** preferido sobre functions.php (sobrevive a updates de tema)
3. **JetPack** requer XML-RPC — verificar SEMPRE antes de desactivar
4. **Content-Security-Policy** nao incluido como header standard porque varia por site. Adicionar manualmente se necessario
5. **Reverter:** os backups `.bak.YYYYMMDD` permitem rollback rapido
6. **Ownership:** SEMPRE `chown -R USER:USER` no final (regra #11)
---
## Changelog
### v1.0.0 (2026-03-12)
- Versao inicial
- 10 verificacoes de hardening
- Modo dry run (--dry)
- mu-plugin dsc-hardening.php para medidas PHP
- Security headers standard no .htaccess
- Backup automatico antes de alteracoes
- Verificacao pos-hardening com rollback
- Tarefa Desk CRM #443 (Projecto #65, Discussao #33)
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
+525
View File
@@ -0,0 +1,525 @@
---
name: wp-meteor
description: Diagnóstico e configuração do WP Meteor (delay/reorder de JavaScript não crítico) via WP-CLI em servidores CWP e via o MCP `wpmeteor` (projecto `mcp-wpmeteor`, multi-site, 8 tools — ver skill `mcp-wpmeteor` para o "como executar"). Cobre os 5 módulos reais do plugin — `ultimate-reorder` (atraso de execução de JS até interacção do utilizador, com modo "zero delay" e modo "só interacção"), `exclude` (exclusões manuais por regex), `gdpr` (exclusões automáticas para 8 plugins de cookies), `elementor-animations` e `elementor-pp` (emulação do menu Elementor PowerPack Pro — NÃO é "Elementor Pro") — mais o módulo interno `Compatibility` (sempre activo, não configurável, com exclusões automáticas para dezenas de outros optimizadores). Cobre também os mecanismos de bypass automático (query strings, user agents, page builders, conflito com WP Rocket/Nitropack/FastPixel), o endpoint REST `wpmeteor/v1/detect`, e diagnóstico de CLS/CWV com Chrome DevTools performance trace. Usar quando "wp meteor", "wpmeteor", "atraso javascript", "delay js", "ultimate reorder", "cls alto", "layout shift", "core web vitals wordpress", "facade pattern js", "js não crítico", "elementor powerpack pro", "zero delay mode", "wpmeteordisable", "wpmeteordebug", "wpmeteor mcp".
---
# /wp-meteor — Diagnóstico e config do WP Meteor (delay de JavaScript)
Plugin de performance de terceiros (`wp-meteor`, autor Aleksandr Guidrevitch,
v`3.4.18`). Melhora TBT/INP **atrasando a execução de JavaScript não crítico**
até à primeira interacção do utilizador ou até um timeout fixo. Config
completa numa única option serializada, `wp_options.wp-meteor-settings`
(array PHP, **5 módulos reais** + versão + campo interno `detected`).
Catálogo confirmado por leitura completa do código-fonte do plugin
(5718 linhas em todos os `.php`, sessão 16-08-2026) — não há módulos
escondidos por descobrir; o que está documentado abaixo é o total.
Confirmado activo em `emanuelalmeida.pt` (16-08-2026, esta sessão). **Verificado ao vivo
19-08-2026 nos 8 sites WordPress reais do bundle Descomplicar (`wp plugin list`):
`emanuelalmeida.pt` é o ÚNICO site com o WP Meteor instalado** — `descomplicar.pt` (produção),
`starter.descomplicar.pt`, `ccv.descomplicar.pt`, `care.descomplicar.pt`,
`ecommerce-demo.descomplicar.pt`, `e-commerce.descomplicar.pt` e `ecommerce.descomplicar.pt` não
o têm. Verificar sempre por site antes de assumir que está presente.
**Gestão activa (ligar/desligar módulos, mudar delay, exclusões) via MCP:** ver skill
`mcp-wpmeteor` — MCP dedicado multi-site (`wpmeteor` em `~/.omp/agent/mcp.json`, 8 tools),
testado ponta-a-ponta contra produção 19-08-2026. Esta skill continua a ser a fonte de
conhecimento (o que cada chave significa, gotchas); a skill `mcp-wpmeteor` é o "como executar".
---
## Comandos base (SSH + WP-CLI, servidor CWP)
```bash
PATH=/home/USER/public_html # ex: /home/ealmeida/emanuelalmeida.pt
# Ler a option completa (fonte de verdade)
sudo -u USER /usr/local/bin/wp option get wp-meteor-settings --format=json --path=$PATH
# Confirmar versão e estado do plugin
sudo -u USER /usr/local/bin/wp plugin get wp-meteor --format=json --path=$PATH
# Via SSH directo (alias `server` já configurado, porta 9443)
ssh server "sudo -u USER /usr/local/bin/wp option get wp-meteor-settings --path=$PATH --format=json 2>/dev/null"
```
Os avisos PHP (`WP_DEBUG_LOG already defined`) que aparecem em `stderr` são
ruído do ambiente — ignorar, não é erro do plugin.
---
## Estrutura real da option `wp-meteor-settings` (verificado ao vivo)
```json
{
"ultimate-reorder": {
"enabled": true,
"id": "ultimate-reorder",
"delay": "2",
"description": ""
},
"gdpr": {
"enabled": true,
"id": "gdpr",
"description": "Most of the time, you want your GDPR/Cookie to be displayed instantly..."
},
"exclude": {
"enabled": true,
"id": "exclude",
"description": "Specify URLs or keywords or regular expressions... GA and GTM",
"value": []
},
"elementor-animations": { "enabled": true, "id": "elementor-animations", "description": "" },
"elementor-pp": { "enabled": true, "id": "elementor-pp", "description": "" },
"v": "3.4.18"
}
```
| Módulo | Estado real | O que faz |
|---|---|---|
| `ultimate-reorder` | `enabled=true`, **`delay=2`** (segundos, string) | Mecanismo central — ver secção seguinte |
| `gdpr` | `enabled=true` | Impede que o banner de cookies/GDPR seja atrasado — aparece instantaneamente |
| `exclude` | `enabled=true`, **`value: []` — lista vazia** | ⚠️ Lista de URLs/palavras-chave/regex a excluir do atraso. **Nada está excluído hoje.** A própria descrição da opção sugere excluir menus, carrosséis do hero, GA e GTM — nenhum destes está configurado |
| `elementor-animations` | `enabled=true` | Compatibilidade com animações nativas do Elementor (deixa correr entrance animations mesmo com scripts atrasados) |
| `elementor-pp` | `enabled=true` | ⚠️ **Não é "Elementor Pro"** — emula o menu do plugin de terceiros **Elementor PowerPack Pro** (Livemesh). Título real no código: `"Emulate Elementor Powerpack Pro menu"`. Se o site não usa PowerPack Pro, este módulo é irrelevante e pode ficar ligado sem efeito |
| `v` | `"3.4.18"` | Versão do plugin no momento em que a option foi gravada (não confundir com a versão instalada — confirmar sempre via `wp plugin get`) |
| `detected` | não presente hoje (array, quando existe) | Campo interno alimentado pelo endpoint REST `wpmeteor/v1/detect` (JS de admin reporta ferramentas de terceiros detectadas, ex. `"marketo"`, `"hubspot"` — dispara um aviso admin sugerindo contacto com o autor do plugin para optimizações extra) |
Estes **5 módulos são o catálogo completo e definitivo** desta versão do
plugin (`3.4.18`) — confirmado por leitura de todo o código-fonte, não só
da option gravada. **O plugin NÃO tem** (verificado directamente no
código, ausência confirmada, não assumida):
- Lazy loading de imagens/iframes — zero.
- Defer/async de scripts como toggle independente — o mecanismo de delay
é tudo-ou-nada por script, controlado só pelo `ultimate-reorder`.
- Remoção de query strings de assets estáticos.
- Preconnect/dns-prefetch configurável — existe um `preconnect: true`
interno calculado automaticamente (`frontend_adjust_wpmeteor` em
`UltimateReorder.php`), mas não há opção na UI nem chave na option para
o utilizador controlar.
- Critical CSS / CSS não usado.
- Qualquer módulo específico de WooCommerce (existe apenas uma exclusão
automática fixa para um script de troca de classe `woocommerce-no-js` →
`woocommerce-js`, dentro do módulo interno `Compatibility`, não é um
módulo à parte nem é configurável).
Não confundir com outros plugins de performance (WP Rocket, Autoptimize,
etc.) que têm estas features — o WP Meteor é deliberadamente um plugin de
um truque só (delay de JS), bem executado, com módulos de compatibilidade
à volta desse truque.
---
## Como funciona o `ultimate-reorder` (delay/facade pattern)
Padrão clássico de optimização de TBT/INP: em vez de deixar o browser
executar todo o JavaScript da página logo no carregamento (bloqueando a
main thread), o plugin **intercepta os `<script>` da página, remove-os da
execução imediata, e só os dispara quando**:
1. o utilizador interage pela primeira vez (`mousemove`, `scroll`,
`touchstart`, `click`, `keydown` — eventos típicos deste padrão), **ou**
2. o `delay` configurado expira — aqui **2 segundos fixos**.
O que ganha: a página fica "interactive-looking" mais cedo, porque o
JavaScript pesado (analytics, widgets, sliders, tracking) não compete pela
main thread durante o first paint/LCP.
O que pode perder: **qualquer script que controle dimensões, posição ou
visibilidade de elementos visíveis acima da dobra fica também atrasado** —
porque o mecanismo não distingue "JS de tracking" de "JS que a página
precisa para se renderizar correctamente". Se um elemento Elementor depende
de JS para calcular a sua altura final, ícones (`sub-arrow`, Font Awesome),
carrosséis do hero, ou animações que definem `opacity`/`transform` no
carregamento, esses elementos **saltam de posição/tamanho quando o script
finalmente corre** (aos 2s ou na primeira interacção) — a definição exacta
de Cumulative Layout Shift.
### Valores possíveis para `ultimate-reorder.delay` (confirmado no código, `UltimateReorder.php`)
A chave `delay` não é só "número de segundos" — tem 3 comportamentos
distintos consoante o valor gravado (a option guarda string, o PHP faz
`(int)` antes de usar):
| Valor gravado | Comportamento em runtime (`rdelay`, ms enviados ao JS) | Quando usar |
|---|---|---|
| `"0"` | `rdelay = 0` — scripts disparam assim que possível, sem espera fixa (chamado **"zero delay mode"** pelo próprio autor, promovido num aviso admin permanente em `Enqueue.php`) | Sites onde o TBT já é baixo e só se quer o reorder/facade, sem atraso artificial |
| `"2"`, `"5"`, etc. (positivo) | `rdelay = delay * 1000` ms — timeout fixo, script corre ao fim de N segundos **mesmo sem interacção** | Caso geral — garante que scripts de tracking correm mesmo em sessões sem interacção (bounces) |
| `"-1"` (ou qualquer negativo) | `rdelay = 86400000` (24h em ms, na prática "nunca por timeout") — **scripts só correm na primeira interacção real do utilizador**, nunca por timeout | Máxima agressividade de TBT/INP; risco: sessões sem interacção (bots de SEO/Lighthouse, utilizadores que só leem) nunca disparam analytics/pixels |
| módulo `enabled=false` | `rdelay = 0` e o rewrite de scripts não corre — comportamento idêntico a ter o plugin desligado | Teste de isolamento (ver secção de diagnóstico CLS) |
Nota de compatibilidade legada (irrelevante para `v3.4.18` mas presente no
código): em options gravadas por versões `< 2.3.6`, `delay=3` era mapeado
automaticamente para o comportamento "-1" (só interacção). Não se aplica a
options novas.
---
## Configurar exclusões (o gap real encontrado nesta sessão)
`exclude.value` é um array de strings (URL, palavra-chave ou regex) que
identificam o `src`/conteúdo inline de scripts a **não atrasar**. Hoje está
vazio — nada escapa ao atraso de 2s.
```bash
# Adicionar exclusões (substitui o array inteiro — ler primeiro, escrever depois)
sudo -u USER /usr/local/bin/wp option patch update wp-meteor-settings exclude value \
'["gtag","googletagmanager","gtm.js","analytics","elementor-pro/assets/js/frontend","font-awesome"]' \
--format=json --path=$PATH
# Alterar só o delay (ex.: reduzir de 2s para 0.5s para testar impacto)
sudo -u USER /usr/local/bin/wp option patch update wp-meteor-settings ultimate-reorder delay "0.5" --path=$PATH
# Desligar o módulo por completo (teste de isolamento — ver secção seguinte)
sudo -u USER /usr/local/bin/wp option patch update wp-meteor-settings ultimate-reorder enabled false --format=json --path=$PATH
# Confirmar
sudo -u USER /usr/local/bin/wp option get wp-meteor-settings --format=json --path=$PATH
```
**Candidatos reais a excluir** (com base na descrição da própria opção +
nesta investigação de CLS):
- Google Analytics / GTM (`gtag`, `googletagmanager`, `gtm.js`) — nunca
deve ser atrasado, quebra tracking dos primeiros 2s de cada sessão.
- Chat widgets (Tawk, Crisp, WhatsApp floating button) — se aparecem
instantaneamente no design, precisam de correr logo.
- Menus e carrosséis do hero — mencionados explicitamente na descrição da
opção como candidatos.
- Formulários acima da dobra (Elementor Pro Forms) — se o utilizador pode
interagir antes dos 2s, o JS de validação/submit tem de já estar activo.
- JS que controla ícones/dimensões de elementos visíveis no first paint
(Font Awesome, `elementor-pro/assets/js/frontend`) — candidato directo
ligado à investigação de CLS 0,89 desta sessão.
**⚠️ Não alterar em produção sem GATE de mutação** — esta é uma option viva
a servir tráfego real. Qualquer patch aqui é imediato e sem staging.
---
## Exclusões automáticas embutidas (não vêm da option, vêm do código — `GDPR.php` + `Compatibility.php`)
Além do `exclude.value` manual, há **duas camadas de exclusão automática**
que correm sempre que o módulo respectivo está `enabled=true` (ou sempre,
no caso de `Compatibility`), independentes do que está gravado na option.
Isto explica por que alguns scripts "escapam" ao atraso mesmo com
`exclude.value: []`.
### Módulo `gdpr` — regex por plugin de cookies detectado (`blocker/Exclusions/GDPR.php`)
Quando `gdpr.enabled=true` (default), o plugin testa `is_plugin_active()`
para 8 plugins de cookies conhecidos e, para cada um activo, adiciona
automaticamente os padrões correspondentes à lista de "não atrasar":
| Plugin de cookies detectado | Padrões excluídos automaticamente |
|---|---|
| Sempre (independente de plugin) | `ap.legalblink.it/api/scripts/lb_cs.js` (LegalBlink) |
| Complianz (`complianz-gdpr(-premium)`) | id `cmplz-cookiebanner` |
| Cookie Notice (`cookie-notice`) | `var cnArgs`, `/cookie-notice/js/front(.min).js` |
| GDPR Cookie Compliance / Moove (`gdpr-cookie-compliance`) | jQuery, `var moove_frontend_gdpr_scripts`, `var gdpr_consent__strict`, `/gdpr-cookie-compliance/dist/scripts/main.js` |
| Cookie Law Info / CookieYes (`cookie-law-info`) | `var _ckyConfig`, `/cookie-law-info/lite/frontend/js/script(.min).js` |
| EU Cookie Law (`eu-cookie-law-compliance`) | jQuery, `window.hasPolisClConsent` |
| Iubenda (`iubenda-cookie-law-solution`) | `var _iub`, `cdn.iubenda.com` |
| Cookiebot (`cookiebot`) | `consent.cookiebot.com/uc.js` |
| Cookie Script (`cookie-script-com`) | `cookie-script.com/s/…js` |
⚠️ Se `function_exists('is_plugin_active')` falhar (raro, contexto sem
`wp-admin/includes/plugin.php` carregado), o código assume **todos** os
plugins como activos e aplica todos os padrões de uma vez — não é um bug a
corrigir, é o comportamento de fallback do próprio plugin.
### Módulo interno `Compatibility` — sempre activo, sem toggle, sem entrada na option (`blocker/Exclusions/Compatibility.php`)
Este blocker **não aparece em nenhum separador da UI nem tem chave própria
na option** — corre sempre, com prioridade 100 (a mais baixa, ou seja,
aplicado por último). Contém uma lista fixa de ~25 padrões regex para não
atrasar scripts-chave de outros optimizadores/plugins, para evitar
conflitos duplos de optimização:
- Lazy loaders: Autoptimize (`lazyLoadOptions`), LazySizes
(`lazySizesConfig`, `lazysizes(.min).js` — com excepções para Avada/
WPSol addons), WP Rocket lazy load, Rocket Lazy Load CPCSS
(`wprRemoveCPCSS`), Easy Image Optimizer (`eio_lazy_vars`), EWWW
(`ewww_webp_supported`), Smush (`smush-lazy-load(-native).min.js`),
Jetpack Lazy Images (`jetpack-lazy-images-js-enabled`, `lazy-images.js`).
- Fast Velocity Minify (`function fvmuag(`).
- Page builders / temas: Divi (`function et_core_page_resource_fallback(`,
troca de classe `no-js`→`js`), Avada/Fusion (`fusionNavIsCollapsed`),
UpSolution (`window.\$us === undefined`).
- Swift Performance Lazyload (`data-swift-image-lazyload`).
- New Relic (`js-agent.newrelic.com`).
- WooCommerce (troca de classe `woocommerce-no-js`→`woocommerce-js`) — o
único ponto de contacto do plugin com WooCommerce, **não é um módulo
WooCommerce dedicado**.
- WPForms (`var wpforms_settings`).
- Complianz — camada extra dinâmica: lê o filtro `cmplz_known_script_tags`
e a option `complianz_options_custom-scripts` (bloqueios custom
configurados no próprio Complianz) e exclui esses URLs também, em cima
da lista fixa do módulo `gdpr` acima.
---
## Diagnóstico CLS/CWV — ligação à investigação em aberto
Contexto completo: `BUNDLE-Excelencia-WP.md` §2.2 (Performance) e
`CONFIG-Plugins-Referencia.md` §4 (WP Meteor).
Estado da investigação (`emanuelalmeida.pt`, LCP mobile 4,7s / **CLS
desktop 0,89**, não resolvido):
1. **Rocket Loader (Cloudflare, App for Cloudflare®)** — testado e
**descartado** nesta sessão: desligado, purge de cache, novo trace →
CLS manteve-se em 0,89. Não era a causa.
2. **WP Meteor `ultimate-reorder`** — **ainda não testado**, é o candidato
mais forte agora: atraso fixo de 2s, **zero exclusões configuradas**,
mecanismo bem mais agressivo que a reordenação leve do Rocket Loader.
O `<i class="fa">` (Font Awesome) encontrado no HTML durante a
investigação do CLS vem exactamente deste módulo.
Este é o próximo passo concreto documentado — **não uma conclusão**. Testar
antes de decidir se se desliga o módulo, se se reduz o `delay`, ou se se
preenche a lista de exclusões.
---
## Testar isoladamente via Chrome DevTools performance trace
Padrão usado nesta sessão para isolar a causa do CLS: baseline → mudar UMA
variável → purge de todos os caches → novo trace → comparar.
### 1. Baseline (config actual: `ultimate-reorder` ligado, `delay=2`, sem exclusões)
```
mcp__chrome_devtools_navigate_page(url: "https://emanuelalmeida.pt/")
mcp__chrome_devtools_performance_start_trace(reload: true, autoStop: true)
# aguardar conclusão do trace (autoStop com timeout suficiente para cobrir o delay de 2s)
mcp__chrome_devtools_performance_stop_trace()
mcp__chrome_devtools_performance_analyze_insight(insightName: "CLSCulprits") # ou "LayoutShifts"
```
Registar o CLS reportado e os elementos culpados (`culprits`) — normalmente
identifica o selector/elemento exacto que salta.
### 2. Alterar a variável a testar (via SSH, fora do browser)
```bash
# Opção A — desligar o módulo por completo (teste mais bruto, isola se é o WP Meteor)
sudo -u USER /usr/local/bin/wp option patch update wp-meteor-settings ultimate-reorder enabled false --format=json --path=$PATH
# Opção B — manter ligado mas excluir os scripts suspeitos (teste mais cirúrgico)
sudo -u USER /usr/local/bin/wp option patch update wp-meteor-settings exclude value '["font-awesome","elementor-pro"]' --format=json --path=$PATH
```
### 3. Purgar TODOS os caches antes de repetir o trace
O HTML/JS servido está em pelo menos duas camadas de cache — sem purgar
ambas, o browser continua a ver a versão antiga:
```bash
# WP Fastest Cache
sudo -u USER /usr/local/bin/wp cache flush --path=$PATH
# (ou, se o WPFC não expõe flush nativo por wp-cli, apagar via admin/plugin action)
# Cloudflare (App for Cloudflare® — purge via wp-admin ou API directa da zona)
```
### 4. Repetir o trace e comparar
```
mcp__chrome_devtools_navigate_page(url: "https://emanuelalmeida.pt/")
mcp__chrome_devtools_performance_start_trace(reload: true, autoStop: true)
mcp__chrome_devtools_performance_stop_trace()
mcp__chrome_devtools_performance_analyze_insight(insightName: "CLSCulprits")
```
Comparar o valor de CLS e os `culprits` antes/depois. Se o CLS cair de
forma consistente com o módulo desligado (opção A) mas os `culprits`
apontarem para os mesmos elementos com exclusões configuradas (opção B),
confirma que o `ultimate-reorder` é a causa e que a exclusão cirúrgica
resolve sem perder o ganho de TBT nos restantes scripts.
### 5. Reverter se o teste não resolver
Se o CLS não mudar com o módulo desligado, **reverter imediatamente** ao
estado original (não deixar o site sem o ganho de performance do WP Meteor
por um teste que não confirmou a hipótese):
```bash
sudo -u USER /usr/local/bin/wp option patch update wp-meteor-settings ultimate-reorder enabled true --format=json --path=$PATH
sudo -u USER /usr/local/bin/wp option get wp-meteor-settings --format=json --path=$PATH # confirmar
```
---
## Interface admin — localização e separadores (`backend/views/admin.php`)
`wp-admin` → **Definições → WP Meteor**
(`options-general.php?page=wp-meteor`, capacidade `manage_options`).
Página com 3 separadores fixos (não há mais nenhum, confirmado no template):
| Separador (UI) | Slug interno (`tab`) | Módulo(s) renderizado(s) aqui |
|---|---|---|
| Settings | `ultimate` | `ultimate-reorder` |
| Exclusions | `exclusions` | `exclude`, `gdpr` |
| Elementor | `elementor` | `elementor-animations`, `elementor-pp` |
O módulo interno `Compatibility` não tem `$tab` definido — nunca aparece
em nenhum separador, coerente com não ser configurável pelo utilizador.
---
## Mecanismos de bypass/auto-desactivação automáticos (confirmado em `frontend/Base.php` + `backend/Enqueue.php`)
O plugin desliga-se a si próprio (não faz rewrite de scripts nessa
página/pedido) nas seguintes condições, **sem qualquer intervenção manual
necessária**:
- Query string com `wpmeteordisable` em qualquer posição — desactiva o
plugin nesse pedido (útil para QA sem alterar a option: `?wpmeteordisable=1`).
- Query string com `wpmeteordebug` — mantém o plugin activo mas carrega a
variante `public-debug.js` (não minificada, com logging) em vez de
`public.js`.
- `User-Agent` a corresponder a `/fastpixel/i`.
- `Referer` igual a `https://www.squirrly.co`.
- Constante `NITROPACK_VERSION` definida — **incompatibilidade
declarada**, dispara aviso de erro admin permanente.
- Constante `FASTPIXEL_VERSION` definida — idem.
- WP Rocket activo (`WP_ROCKET_VERSION`) **e** a opção `delay_js` do WP
Rocket está ligada — o WP Meteor desliga-se para não duplicar o
mecanismo, com aviso admin a pedir para desligar um dos dois.
- Pedido não é frontend (admin, ajax, cron, REST) — via `Is_Methods::is_frontend()`.
- Filtro `wpmeteor_enabled` devolve `false` (hook para terceiros/temas
desligarem programaticamente).
- Parâmetros GET/POST de modo de edição de **19 page builders** conhecidos:
`bricks`, `brizy-edit-iframe`, `builder` (Fusion), `ct_builder`
(Oxygen), `elementor-preview`, `et_fb` (Divi), `fb-edit` (Fusion),
`fl_builder` (Beaver Builder), `preview` (Gutenberg/block editor),
`tb-preview` (Themify), `tve` (Thrive), `uxb_iframe` (Flatsome UX
Builder), `vc_action`/`vc_editable`/`vcv-action` (WPBakery), `wyp_mode`/
`wyp_page_type` (YellowPencil), `zionbuilder-preview` (Zion Builder).
- Endpoint AMP activo (`is_amp_endpoint()` ou `ampforwp_is_amp_endpoint()`).
- Modo editor/preview activo detectado directamente via classe do builder
(não só GET/POST): Elementor (`Plugin::$instance->editor`/`preview`),
Beaver Builder (`FLBuilderModel::is_builder_active()`), WPBakery
(`vc_is_inline()`), Divi (`et_core_is_builder_used_on_current_request()`),
Avada/Fusion Builder (`Fusion_App::get_instance()->is_builder`).
- Content-Type da resposta diferente de `text/html` (ex.: JSON, XML,
feeds) — nunca reescreve respostas não-HTML.
- Página `wp-login.php`.
Isto é relevante para diagnóstico: se um script continuar "não atrasado"
mesmo com o módulo `ultimate-reorder` ligado e sem exclusão manual,
confirmar primeiro se a página em causa não cai numa destas condições de
bypass antes de suspeitar de bug/exclusão em falta.
---
## Endpoint REST `wpmeteor/v1/detect` (`rest/Marketing.php`)
`POST /wp-json/wpmeteor/v1/detect` — sem autenticação declarada no
registo da rota (`register_rest_route`, sem `permission_callback`
restritivo visível). Recebe `{"data": [...]}` do JS de admin/frontend,
junta ao array `detected` já gravado na option, corta para os últimos 20
valores (`array_slice(..., -20)`) e grava de volta.
Uso conhecido: o JS reporta strings como `"marketo"` ou `"hubspot"`
quando detecta esses formulários na página; se `detected` contém um destes
valores, `backend/Enqueue.php` mostra um aviso admin permanente sugerindo
contacto com o autor do plugin (`alex@excitingstartup.com`) para
optimizações adicionais específicas dessas ferramentas — **não é uma
funcionalidade que o utilizador configure**, é telemetria/marketing do
próprio plugin.
```bash
# Ler o campo detected (se existir) directamente da option
sudo -u USER /usr/local/bin/wp option get wp-meteor-settings --format=json --path=$PATH | jq '.detected'
```
---
## Comandos WP-CLI próprios — confirmação: NÃO existem
Procurado `WP_CLI::add_command` / `\WP_CLI` em todo o código-fonte do
plugin (`grep -rl 'WP_CLI'` em todos os `.php`) — **zero resultados**. A
única referência a `WP_CLI` no plugin é em `engine/Is_Methods.php`, e é
apenas a verificação `defined('WP_CLI') && WP_CLI` para detectar se o
pedido actual corre em contexto de CLI (usado internamente para decidir
que classes carregar), **não** um comando registado. `wp help meteor` ou
qualquer variante não existe — toda a interacção via linha de comandos é
através dos comandos genéricos `wp option get`/`wp option patch` sobre
`wp-meteor-settings`, como documentado nas secções acima.
---
## Mecânica interna do rewrite de `<script>` (para debugging avançado, `UltimateReorder.php::frontend_rewrite`)
Quando `ultimate-reorder.enabled=true`, o buffer HTML de saída é
reescrito via `ob_start()` (`frontend/Rewrite.php::buffer_start`), e cada
`<script>` sofre uma destas transformações:
- Scripts inline são temporariamente substituídos por um delimitador
único (`WPMETEOR` + password aleatória de 16 chars) para não serem
reprocessados por engano por regex subsequentes que corram sobre JSON
inserido por outros scripts — só no fim são recolocados no buffer.
- `src="..."` passa a `data-wpmeteor-src="..."` (o browser não carrega o
recurso até o JS do plugin trocar o atributo de volta).
- `type="text/javascript"` (ou ausência de `type`, ou `module`) passa a
`type="javascript/blocked" data-wpmeteor-type="..."` — o browser
reconhece `type` desconhecido e não executa o script.
- `onload=`/`onerror=` em `<html|body|img|iframe>` são envolvidos para
disparar `window.dispatchEvent(new CustomEvent('fpo:element-loaded', …))`
antes do handler original, permitindo ao motor do plugin reagir a
carregamento de imagens/iframes mesmo com scripts atrasados.
- Atributo de escape manual por tag: `data-wpmeteor-nooptimize="true"`
num `<script>` específico impede que **esse** script seja atrasado,
independentemente de exclusões globais — mecanismo usado pelo próprio
plugin para o seu script de bootstrap injectado, mas também disponível
para debugging manual (adicionar o atributo directamente no HTML de um
script problemático, via filtro `the_content` ou tema, para testar sem
tocar na option).
- Scripts já marcados por outros plugins como `data-src=`,
`data-wpmeteor-type=`, `data-pmdelayedscript=` ou
`data-rocketlazyloadscript=` são ignorados (evita duplo-delay).
O filtro `wpmeteor_exclude` (booleano, `$exclude, $content` → bool) é o
ponto de extensão central — tanto o `exclude.value` manual como as
exclusões automáticas do `gdpr` e do `Compatibility` são implementadas
como listeners deste mesmo filtro, testados por ordem de prioridade até
um devolver `true`.
---
## Gotchas / erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| Trace mostra CLS igual antes/depois de mudar a config | Cache (WPFC e/ou Cloudflare) ainda a servir HTML/JS antigo | Purgar as duas camadas de cache antes de cada trace, nunca só uma |
| `wp option patch` falha com "No data exists for key" | Caminho de chaves errado — `exclude` tem sub-chave `value`, não é directamente o array | Usar `wp option patch update wp-meteor-settings exclude value '[...]'` (dois níveis) |
| `wp plugin get wp-meteor` devolve vazio noutro site do bundle | Plugin não está instalado nesse site — confirmado em `carstuff.pt` | Nunca assumir presença; confirmar por site antes de qualquer diagnóstico |
| CLS não muda mesmo com `ultimate-reorder` desligado | Causa pode estar noutro módulo (tema, Elementor, imagens sem dimensões) | Não parar na primeira hipótese testada — este é só o próximo suspeito, não a conclusão |
| Analytics/GTM param de disparar nos primeiros segundos | `exclude.value` vazio — GA/GTM ficam sujeitos ao atraso de 2s como qualquer outro script | Adicionar `gtag`/`googletagmanager`/`gtm.js` à lista de exclusões |
| Confundir `elementor-pp` com "compatibilidade Elementor Pro" | O nome sugere isso mas o título real no código é "Emulate Elementor Powerpack Pro menu" — é sobre o plugin de terceiros Elementor PowerPack Pro (Livemesh) | Verificar se o site usa mesmo PowerPack Pro antes de investir tempo a testar este módulo como causa de um problema |
| Script continua a correr "atrasado" mesmo depois de o adicionar a `exclude.value` | Já está coberto por uma exclusão automática embutida (`gdpr` ou `Compatibility`) que corre por regex diferente do valor manual, ou a página cai numa das condições de bypass automático (builder em modo preview, WP Rocket `delay_js`, etc.) | Confirmar primeiro as secções "Exclusões automáticas embutidas" e "Mecanismos de bypass" antes de assumir que a exclusão manual falhou |
| `delay` gravado como `"-1"` e analytics/pixels param de disparar em sessões sem interacção | Comportamento esperado — `-1` significa "só corre na primeira interacção real", nunca por timeout (rdelay=86400000ms) | Se o site precisa de garantia de disparo mesmo sem interacção (bounces), usar um valor positivo, nunca `-1` |
| `wp help meteor` ou comando WP-CLI próprio não existe | O plugin não regista nenhum comando WP-CLI — confirmado por grep a todo o código-fonte | Usar sempre `wp option get/patch` sobre `wp-meteor-settings`, nunca procurar um comando dedicado |
---
## Fonte
`CONFIG-Plugins-Referencia.md` §4 (WP Meteor — mapeado 16-08-2026) +
`BUNDLE-Excelencia-WP.md` §2.2 (Performance) e tabela de pendências (linhas
218 e 233) + verificação SSH ao vivo desta sessão em `emanuelalmeida.pt`
(`wp option get wp-meteor-settings --format=json`, `wp plugin get
wp-meteor --format=json`) + **leitura completa do código-fonte do plugin**
via SSH (`find wp-content/plugins/wp-meteor -iname '*.php' | xargs wc -l`
para mapear as 5718 linhas em todos os `.php`, seguido de leitura integral
de `wp-meteor.php`, `engine/{Initialize,Base,Is_Methods}.php`,
`blocker/{Base,Event}.php`, `blocker/FirstInteraction/{Base,
UltimateReorder}.php`, `blocker/Exclusions/{Exclude,GDPR,Compatibility}.php`,
`blocker/Integration/{ElementorAnimations,ElementorPP}.php`,
`backend/{SettingsPage,SaveSettings,Enqueue,ActDeact,InstUninst}.php`,
`backend/views/admin.php`, `frontend/{Base,Rewrite}.php`,
`functions/functions.php` e `rest/Marketing.php`, na sessão de expansão de
16-08-2026). Catálogo de módulos confirmado como completo e definitivo
para `v3.4.18` — não há funcionalidades por documentar além das listadas
neste ficheiro. Nenhuma alteração foi feita em produção.
+126 -3
View File
@@ -1,7 +1,6 @@
---
name: wp-performance
description: Auditoria e optimização de performance WordPress. Analisa velocidade, caching, base de dados e assets. Usa Lighthouse, métricas Core Web Vitals e checklist sistemático.
layer: wiki
---
# /wp-performance - Auditoria Performance WordPress
@@ -56,7 +55,7 @@ const vitals = await mcp__lighthouse__get_core_web_vitals({ url });
### Passo 2: Verificações Servidor (SSH)
```javascript
// Se site está no servidor CWP (176.9.3.158)
// Se site está no servidor CWP (server.descomplicar.pt)
const server_checks = [
// GZIP activo?
`curl -sI -H 'Accept-Encoding: gzip' ${url} | grep -i 'content-encoding'`,
@@ -215,4 +214,128 @@ wp db optimize
---
## Sites WordPres
## Sites WordPress no Servidor
| Conta | Domínio | Path |
|-------|---------|------|
| ealmeida | emanuelalmeida.pt | `/home/ealmeida/emanuelalmeida.pt/` |
| descomplicar | descomplicar.pt | `/home/descomplicar/public_html/` |
| wtc | watercontrol.pt | `/home/wtc/public_html/` |
---
## Problemas Comuns
### Autoload Grande (>1MB)
```sql
-- Ver maiores autoload options
SELECT option_name, LENGTH(option_value) as size
FROM wp_options
WHERE autoload='yes'
ORDER BY size DESC
LIMIT 20;
```
### Muitas Revisões
```bash
# Limitar revisões futuras (wp-config.php)
define('WP_POST_REVISIONS', 5);
```
### Sem Cache
Plugins recomendados:
- LiteSpeed Cache (se servidor LiteSpeed)
- WP Super Cache (simples)
- W3 Total Cache (avançado)
### Imagens Não Optimizadas
Plugins recomendados:
- ShortPixel (com WebP)
- Imagify
- Smush
---
## Changelog
### v1.0.0 (2026-01-30)
- Versão inicial
- Checklist: Servidor, WordPress, Imagens
- Integração Lighthouse + SSH
- Thresholds definidos
---
## Quando NÃO Usar
- Para tarefas fora do domínio de especialização desta skill
- Quando outra skill mais específica está disponível
- Para operações que requerem aprovação manual obrigatória
- Quando os requisitos não estão claramente definidos
## Protocolo de Execução
1. **Análise Inicial**
- Verificar requisitos e contexto
- Identificar ferramentas necessárias
2. **Preparação**
- Validar acesso a recursos
- Preparar ambiente de trabalho
3. **Execução**
- Executar operações de forma incremental
- Validar cada passo antes de prosseguir
4. **Validação**
- Verificar resultados obtidos
- Confirmar sucesso da operação
5. **Conclusão**
- Documentar alterações realizadas
- Reportar status final e próximos passos
## Exemplos de Uso
### Exemplo 1: Caso Básico
```
User: [requisição simples relacionada com wp-performance]
Skill: [execução directa com validação]
Output: [resultado conciso e accionável]
```
### Exemplo 2: Caso Complexo
```
User: [requisição multi-passo ou complexa]
Skill:
1. Análise dos requisitos
2. Planeamento da abordagem
3. Execução faseada
4. Validação contínua
Output: [resultado detalhado com próximos passos]
```
### Exemplo 3: Caso com Dependências
```
User: [requisição que depende de outros sistemas]
Skill:
1. Verificar dependências disponíveis
2. Coordenar com skills/MCPs necessários
3. Executar workflow integrado
Output: [resultado completo com referências]
```
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
+112 -2
View File
@@ -1,7 +1,6 @@
---
name: wp-translate
description: Tradução de plugins WordPress para PT-PT com DeepL Pro, glossário próprio e scripts de automação. Gere ficheiros .po/.mo/.json, corrige traduções PT-BR e faz deploy para starter.descomplicar.pt.
layer: wiki
---
# /wp-translate — Traduções WordPress PT-PT
@@ -154,4 +153,115 @@ DeepL introduz PT-BR mesmo com `target_lang=PT-PT`. Correr fix_ptbr **depois** d
SSH: mcp__ssh-unified__ssh_execute server:"server"
Path traduções: /home/ealmeida/starter.descomplicar.pt/wp-content/languages/plugins/
Loco Translate: /home/ealmeida/starter.descomplicar.pt/wp-content/languages/loco/plugins/
WP-CLI: wp --path=/hom
WP-CLI: wp --path=/home/ealmeida/starter.descomplicar.pt [...] --allow-root
```
**Permissões obrigatórias após operações root:**
```bash
chown ealmeida:ealmeida /home/ealmeida/starter.descomplicar.pt/wp-content/languages/plugins/PLUGIN-pt_PT.*
```
**Deploy completo:**
```bash
# Copiar .po + .mo + .json
cp /tmp/PLUGIN-pt_PT.{po,mo} /home/.../languages/plugins/
# Se React: copiar também os .json
chown ealmeida:ealmeida /home/.../languages/plugins/PLUGIN-pt_PT*
wp --path=... cache flush --allow-root
```
---
## Verificação de Estado
```bash
# Strings em falta em todos os plugins
python3 -c "
from pathlib import Path; import polib
for f in sorted(Path('languages/plugins').glob('*-pt_PT.po')):
po = polib.pofile(str(f))
empty = len(po.untranslated_entries())
if empty: print(f'{f.name}: {empty} em falta')
"
# PT-BR residual após tradução
grep -rn "atualiz\|ativar\|você\b\|excluir\b" languages/plugins/*.po
```
---
## Problemas Conhecidos
### Python 3.6 no servidor
Scripts locais usam type hints Python 3.9+. Para correr no servidor:
- Remover anotações `-> type` das funções
- Não usar `from __future__ import annotations` (não funciona no 3.6)
- `tuple[str, list]` → usar `Tuple` de typing
### msgfmt vs polib
`msgfmt 0.19.8.1` recusa .po com strings mal formadas. Usar sempre **polib** para compilar:
```python
po = polib.pofile('file.po')
po.save_as_mofile('file.mo')
```
### Glossário Pro vs Free
Glossário criado com chave `:fx` (Free) não funciona na conta Pro e vice-versa.
Fix: `python3 setup_glossary.py` após mudar de chave API.
### XML tag_handling quebra com &
`tag_handling='xml'` falha com strings como "Settings & Tools".
Fix actual: tokens Unicode `⟦0⟧` como placeholders (implementado em translate_missing.py v2.1+).
### Strings hardcoded em bundles JS compilados
Plugins com React que não usam `__()` nas strings de UI (ex: `subtitle:"text"`) não podem ser traduzidos.
Não é possível sem modificar o bundle compilado. Aceitar e documentar.
### Loco Translate "Write protected"
Ficheiros criados por root via SSH ficam inacessíveis ao web server.
Fix: `chown ealmeida:ealmeida` nos ficheiros de tradução.
---
## Compilar .mo após Edições Manuais
```python
import polib
po = polib.pofile('/path/to/file.po')
po.save('/path/to/file.po') # salva o .po actualizado
po.save_as_mofile('/path/to/file.mo')
```
## Re-gerar JSON após Edições ao .po
```bash
wp i18n make-json file-pt_PT.po /path/languages/ --no-purge --allow-root
# Depois sempre copiar com MD5 correcto se necessário
chown ealmeida:ealmeida /path/languages/file-pt_PT-*.json
wp --path=... cache flush --allow-root
```
---
## Estado da Biblioteca (starter.descomplicar.pt — 25-02-2026)
- **21 plugins** com ficheiros .po activos
- **Cobertura:** 100% em todos os plugins (excl. webp-express sem suporte i18n)
- **Strings hardcoded** não traduzíveis: ~7 subtítulos no ai-engine (limitação do plugin)
- **Loco Translate path:** `languages/loco/plugins/` para pro-elements (elementor-pro)
---
*Skill v1.0.0 | 25-02-2026 | Descomplicar®*
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*
+172 -3
View File
@@ -2,12 +2,11 @@
name: wp-update
description: Actualização segura de WordPress core, plugins e temas em todos os sites CWP. Inclui backups pré-actualização, verificação pós-actualização e relatório de estado.
disable-model-invocation: true
layer: wiki
---
# /wp-update - Actualização WordPress
Actualização e verificação de todos os sites WordPress no servidor CWP (176.9.3.158).
Actualização e verificação de todos os sites WordPress no servidor CWP (server.descomplicar.pt).
---
@@ -150,4 +149,174 @@ Após concluir as actualizações e verificação final, publicar relatório com
| Site | Path |
|------|------|
| carstuff.pt | /home/carstuff/public_html |
| descompl
| descomplicar.pt | /home/ealmeida/public_html |
| emanuelalmeida.pt | /home/ealmeida/emanuelalmeida.pt |
| ecommerce.descomplicar.pt | /home/ealmeida/ecommerce.descomplicar.pt |
| ecommerce-demo.descomplicar.pt | /home/ealmeida/ecommerce-demo.descomplicar.pt |
| starter.descomplicar.pt | /home/ealmeida/starter.descomplicar.pt |
| care.descomplicar.pt | /home/ealmeida/care.descomplicar.pt |
| e-commerce.descomplicar.pt | /home/ealmeida/e-commerce.descomplicar.pt |
| espiralsenior.pt | /home/espiral/public_html |
| dev.espiralsenior.pt | /home/espiral/dev.espiralsenior.pt |
| familyclinic.pt | /home/familycl/public_html |
| ignitionvortex.pt | /home/ignition/public_html |
| karateclubedegaia.com | /home/karate/karateclubedegaia.com |
| sintricare.com.pt | /home/sintri/public_html |
| solarfvengenharia.com | /home/solarfv/public_html |
| watercontrol.pt | /home/wtc/public_html |
---
## ERROS COMUNS E SOLUÇÕES
### Erro: "Class not found" (KiviCare)
```bash
# Desactivar plugin problemático
mv /path/wp-content/plugins/PLUGIN /path/wp-content/plugins/PLUGIN.disabled
# Reinstalar do care.descomplicar.pt (tem versões actualizadas)
cp -r /home/ealmeida/care.descomplicar.pt/wp-content/plugins/PLUGIN /path/wp-content/plugins/
chown -R USER:USER /path/wp-content/plugins/PLUGIN
```
### Erro: "Operation not permitted"
```bash
# Adicionar FS_METHOD ao wp-config.php
sed -i "/^define.*WP_DEBUG/a define('FS_METHOD', 'direct');" /path/wp-config.php
```
### Erro: Permissões após update
```bash
chown -R USER:USER /path/wp-content/
```
---
## PLUGINS PREMIUM (Licenças)
Alguns plugins não actualizam automaticamente por falta de licença:
- elementor-pro
- wpforms (versão paga)
- bdthemes-element-pack
Estes requerem actualização manual via CodeCanyon/download.
---
## INTEGRAÇÃO COM /today
Esta skill é invocada automaticamente às **segundas-feiras** pelo comando `/today`.
Workflow segunda-feira:
1. `/today` detecta que é segunda
2. Executa verificação inicial
3. Reporta estado dos sites
4. Propõe actualizações pendentes
5. Após conclusão, publica relatório na Discussão #52 (Projecto #69)
## INTEGRAÇÃO DESK CRM
- **Projecto:** #69 (Manutenção Websites)
- **Discussão:** #52 (Logs de Atualização de Websites)
- **Staff:** 25 (AikTop) para comentários automáticos
- **Tool:** `mcp__desk-crm-v3__add_discussion_comment`
- **Wordfence Alerts:** Conta IMAP `it`, pasta `INBOX.Wordpress.Alertas Wordfence`
---
## CHANGELOG
### v1.2.0 (2026-02-08)
- Adicionado Passo 4: Relatório automático Desk CRM
- Relatório publicado como comentário na Discussão #52 (Projecto #69)
- Template HTML completo com tabelas de actualizações e falhas
- Integração Wordfence: verifica alertas recentes da conta IMAP it@
- Staff AikTop (ID 25) como autor dos relatórios
### v1.1.0 (2026-02-02)
- Script movido para `/media/ealmeida/Dados/Dev/ClaudeDev/Claude-Scripts/wp-update/`
- Removidos domínios expirados da lista (jornadadoheroi, tecoworking, socialboost)
- Adicionada documentação sobre verificação de conteúdo
- README.txt adicionado com tarefa DeskCRM
### v1.0.0 (2026-02-02)
- Criação inicial
- Script de verificação robusta
- Lista de 16 sites activos
- Integração com /today para segundas-feiras
---
## Quando NÃO Usar
- Para tarefas fora do domínio de especialização desta skill
- Quando outra skill mais específica está disponível
- Para operações que requerem aprovação manual obrigatória
- Quando os requisitos não estão claramente definidos
## Protocolo de Execução
1. **Análise Inicial**
- Verificar requisitos e contexto
- Identificar ferramentas necessárias
2. **Preparação**
- Validar acesso a recursos
- Preparar ambiente de trabalho
3. **Execução**
- Executar operações de forma incremental
- Validar cada passo antes de prosseguir
4. **Validação**
- Verificar resultados obtidos
- Confirmar sucesso da operação
5. **Conclusão**
- Documentar alterações realizadas
- Reportar status final e próximos passos
## Exemplos de Uso
### Exemplo 1: Caso Básico
```
User: [requisição simples relacionada com wp-update]
Skill: [execução directa com validação]
Output: [resultado conciso e accionável]
```
### Exemplo 2: Caso Complexo
```
User: [requisição multi-passo ou complexa]
Skill:
1. Análise dos requisitos
2. Planeamento da abordagem
3. Execução faseada
4. Validação contínua
Output: [resultado detalhado com próximos passos]
```
### Exemplo 3: Caso com Dependências
```
User: [requisição que depende de outros sistemas]
Skill:
1. Verificar dependências disponíveis
2. Coordenar com skills/MCPs necessários
3. Executar workflow integrado
Output: [resultado completo com referências]
```
---
## Healing Log
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
```
*Adicionar nova linha após cada erro corrigido.*