# Plano de Implementação — Diferenciais SEO/IA + Fundação de Observabilidade

> **Última atualização:** 05/07/2026 — Migração visual completa (paleta do novo design em todas as páginas) + Fase 3 (Search Console) implementada e testada + núcleo da Fase 4 (indexação automática + reindex manual) + backend da Fase 6 pronto.
> **Status geral:** Em implementação. Fases 0 e 3 concluídas e verificadas; 4 e 6 parciais. Próximo passo: aplicar em produção (migração + credenciais Google) e seguir para as Fases 1/2.

## Nota para quem (ou qual IA) ler isso depois

Este arquivo existe pra que qualquer pessoa — ou qualquer sessão nova de IA sem memória da conversa que gerou este plano — consiga entender **o que é este projeto, o que foi decidido, por que foi decidido assim, o que já foi feito e o que falta**, sem precisar reconstruir o raciocínio do zero.

Regras de uso deste documento:
1. Leia primeiro a seção **"Status atual"** (final do arquivo) — ela diz exatamente onde a implementação parou.
2. As decisões na seção **"Decisões já fechadas"** são para não serem re-discutidas, a menos que o usuário explicitamente peça pra revisitar alguma.
3. Sempre que uma fase for concluída, atualize o status dela aqui (⬜ → 🔄 → ✅) e registre em `ALTERACOES.md` (convenção que já existe no projeto para o changelog de mudanças entregues) — este arquivo é o **plano**, `ALTERACOES.md` é o **histórico do que já foi ao ar**.
4. Se durante a implementação você descobrir que algo aqui estava errado ou incompleto (aconteceu pelo menos uma vez durante este planejamento — ver seção de Erratas), corrija o texto deste arquivo e registre a correção, não apague o histórico do engano.

---

## 1. O que é este projeto

**Content Manager AI** é uma plataforma de gestão de conteúdo com IA integrada ao WordPress, hoje operada pela agência como ferramenta interna (não é ainda um produto comercializado). Fluxo central: cadastro de sites WordPress → geração de conteúdo/imagem via IA (Claude/Gemini/OpenAI, com fallback) → validação SEO → aprovação do cliente via link público → publicação/agendamento no WordPress → fila assíncrona de jobs processada via cron.

Stack: PHP 8 puro (sem framework), MySQL, Alpine.js no front-end admin, ORM caseiro (Active Record simples), JWT próprio com 2FA. Pensado pra hospedagem compartilhada (cPanel/LiteSpeed).

## 2. Diagnóstico da arquitetura atual (resumo — feito antes deste plano)

Pontos fortes: tratamento de erro central e cuidadoso, proteção contra IDOR, fila com lock atômico e retry, fallback entre providers de IA, cache com driver plugável.

Gaps identificados para o objetivo de virar SaaS multi-tenant no futuro (**não faz parte do escopo deste plano — ver Fase 8**):
- Não existe tabela `accounts`/`tenants`; isolamento é só por `user_id`, espalhado manualmente em cada endpoint.
- `Database::getInstance()` é um singleton único, sem camada central de escopo por tenant.
- Roteamento é um arquivo PHP por recurso com cascata de `if/preg_match`, sem framework/router de verdade.
- Cache usa `FileDriver` por padrão (não escala horizontalmente).
- `encrypt()`/`decrypt()` reaproveita `JWT_SECRET` como chave — deveria ter segredo próprio.

## 3. Visão comercial de longo prazo (decisão consciente: NÃO AGORA)

Foi discutido como transformar isso num produto comercializável (cliente roda no próprio servidor, mas depende da plataforma central pra funcionar/ser cobrado). Três modelos foram mapeados:

- **Modelo A — Licença clássica**: `bootstrap.php` valida uma chave contra API central periodicamente. Fácil de burlar tecnicamente (PHP não compilado), funciona mais como atrito + base contratual do que proteção real.
- **Modelo B — Gateway de IA obrigatório**: em vez do cliente usar a própria chave de IA, toda geração passa pela plataforma central (que detém as chaves de verdade, fatura por uso). Muito mais forte que licença, porque não é um `if` comentável — é a entrega de valor em si.
- **Modelo C — Híbrido (recomendado quando chegar a hora)**: licença simples liga/desliga o painel administrativo; gateway de IA obrigatório protege o motor de geração de conteúdo; EULA cobre o resto.

**Decisão explícita do usuário**: essa fase fica pra depois. Prioridade agora é rodar em produção real (uso interno da agência), testar bastante, resolver bugs, e só then avaliar comercialização. Nada deste plano (Fases 0–7) depende de decidir isso agora — a arquitetura foi desenhada pra não fechar portas nessa direção, mas não implementa nada disso ainda.

## 4. Fluxograma — fluxo de publicação, checklist e notificações

Este é o fluxo mais refinado do planejamento (Fases 2, 4 e 5 combinadas), depois de várias rodadas de ajuste:

```mermaid
flowchart TD
    A["Cliente aprova o post<br/>(link público de aprovação)"] --> B["Cria job PublishContentJob<br/>NÃO publica na hora, só entra na fila"]
    B --> C{"Idempotência:<br/>content já tem wp_post_url salvo?"}
    C -->|"Sim, já foi publicado antes<br/>(retry de tentativa anterior)"| H
    C -->|"Não"| D["Chama REST API do WordPress"]
    D --> E{"Sucesso?<br/>HTTP 200/201"}
    E -->|"Não"| F["Checklist mostra:<br/>'Publicação não realizada.<br/>Item em fila de processamento.'<br/>(tentativa X de 3)"]
    F -->|"Retry automático<br/>backoff 5 min"| D
    F -->|"Esgotou 3 tentativas"| X["Checklist: Falha ao publicar<br/>Notifica REDATOR por e-mail<br/>Ação manual necessária"]
    E -->|"Sim"| H["Grava published_at = hora real<br/>Checklist: Publicado"]
    H --> P1["Job: solicitar indexação<br/>Search Console Indexing API"]
    H --> P2["Job: reenviar sitemap"]
    H --> P3["Job: notificar CLIENTE<br/>e-mail/WhatsApp conforme configurado"]
    P1 --> R1{"Sucesso?"}
    R1 -->|"Não, esgotou tentativas"| N1["Checklist: falha na indexação<br/>Notifica REDATOR<br/>Botão manual Reindexar aparece"]
    R1 -->|"Sim"| S1["Checklist: indexação solicitada"]
    P2 --> S2["Checklist: sitemap reindexado<br/>sucesso ou falha, best effort"]
    P3 --> S3["Cliente recebe aviso:<br/>Post publicado + URL"]
```

Regra de ouro do fluxo: **o cliente só sabe do que é dele** (post está no ar, aqui está a URL). **O redator sabe de tudo**, inclusive falhas técnicas de indexação, porque pode agir manualmente. A notificação do cliente nunca espera indexação/sitemap terminarem — dispara em paralelo, assim que a publicação é confirmada.

## 5. Mapa mental — visão geral do roadmap

```mermaid
mindmap
  root((Roadmap Content Manager AI))
    Fase 0 Fundacao
      Log central system_logs
      Painel de logs no admin
      Estados aberto resolvido limpo
    Fase 1 Padrao de integracao
      IntegrationResult
      Aplicar primeiro em IA e WordPress
    Fase 2 Publicacao
      Fila controlada pela plataforma
      Retry automatico com backoff
      Idempotencia via wp_post_url
    Fase 3 Search Console
      OAuth por site
      Campo de sitemap
      Deteccao Yoast RankMath
    Fase 4 Indexacao e Checklist
      Indexacao best effort
      Checklist com quatro estados
      Botao manual reindexar
    Fase 5 Notificacoes
      Cliente somente apos publicar
      Canal configuravel por site
      Redator sempre por email
    Fase 6 Performance
      Botao ver desempenho no post
      Search Analytics API
    Fase 7 Relatorios
      HTML por email sem PDF
      Palavras e tempo de leitura
      SEO GEO AEO ja existentes no editor
      Retrabalho via content approvals
      Pagina Relatorios com historico de envio
    Fase 8 Futuro pos testes
      Multi tenant real
      Licenciamento
      Gateway de IA obrigatorio
```

## 6. Princípios de engenharia (todo código novo destas fases segue isso)

1. **Uma implementação, dois gatilhos.** Toda ação que existe automática (via fila) e manual (via botão) chama a **mesma** função de serviço — nunca duas implementações paralelas que podem divergir com o tempo. Já decidido para: reindexação manual (Fase 4) e reenvio de relatório (Fase 7).
2. **Checklist nunca é binário.** Todo item de status tem no mínimo 4 estados: `pendente/fila`, `tentando novamente` (com contador de tentativa), `sucesso` (com timestamp real), `falhou` (com erro real, exigindo ação humana).
3. **Log central, não espalhado.** Toda comunicação externa (IA, WordPress, Search Console, e-mail, WhatsApp) loga em um único lugar (`system_logs`, Fase 0), com o erro exato — nunca "deu erro" genérico.
4. **Idempotência em tudo que pode ser reprocessado.** Antes de repetir uma ação com efeito colateral (publicar, enviar), checar se ela já aconteceu.
5. **Cliente vs. Redator: visibilidade diferente por design.** Cliente vê resultado final relevante a ele. Redator/equipe vê tudo, incluindo falhas técnicas.
6. **Cópia congelada em vez de recálculo.** Quando algo é "enviado" (relatório, notificação), guardar o conteúdo exato enviado naquele momento — não regenerar depois com dados que podem ter mudado.
7. **Nunca reinventar o que já existe.** Antes de criar uma função/validador novo, checar se já existe algo equivalente no projeto (aconteceu com SEOService, com os validadores GEO/AEO do editor, com `content_events`, com `NotificationService` — todos reaproveitados em vez de recriados).

## 7. Passo a passo por fase

### Fase 0 — Fundação: Log Central de Eventos e Erros
**Status:** ✅ Implementado (código pronto e lintado; ainda não testado contra um MySQL real — depende de rodar a migração no servidor)
**Por quê primeiro:** todas as fases seguintes (especialmente 2, 3, 4, 5, 7) precisam logar erro de integração externa; sem isso pronto, cada uma cria seu próprio log espalhado.

O que foi criado/alterado nesta rodada:
- `database/migrate_system_logs.sql` (novo) + `database/schema.sql` atualizado — tabela `system_logs`.
- `core/models/SystemLog.php` (novo) — model ActiveRecord, com `markAs()` e `contextArray()`.
- `core/logging/SystemLogger.php` (novo) — mesma interface pública do `Logger` (`info/warning/error/debug`) + `critical()` novo; usa `Logger` internamente para o arquivo (não duplica lógica), grava em `system_logs` para warning+.
- 18 arquivos trocados de `new Logger(` para `new SystemLogger(`: `cron.php`, `core/providers/ProviderManager.php`, `core/providers/adapters/{Claude,OpenAI,Gemini}Provider.php`, `core/auth/AuthManager.php`, `core/ContentService.php`, `core/services/{SiteManager,NotificationService,ApprovalService,AiImageLimiter,ImageGenerationService,RateLimiter,PublishService,QuotaManager,ImageService,TokenService}.php`, `core/queue/Queue.php`.
- `api/v1/logs.php` (novo) — `GET /logs` (filtros: channel/level/status/date_from/date_to + paginação + contagens), `GET /logs/channels`, `PATCH /logs/{id}` (resolver/limpar um), `POST /logs/clear-all` (limpar em lote respeitando filtros). Acesso: admin ou gerente.
- `admin/logs.html` (novo) — painel com badges de contagem, filtros, polling de 8s (só com a aba em foco), expandir linha para ver contexto JSON completo.
- `admin/topbar.js` — adicionada entrada "Logs" ao menu (a mesma correção que faltava para "Relatórios", ver Fase 7).
- `cron.php` — purga definitiva (DELETE real) de logs `cleared` há mais de 30 dias, dentro do bloco de limpeza das 3h já existente.

**Achados no caminho (sem ação necessária agora):**
- `core/services/ContentService.php` era uma cópia idêntica e **morta** de `core/ContentService.php` (o autoload sempre carregava a da raiz primeiro — ver ordem de busca em `bootstrap.php`). Removida nesta rodada.
- A tabela `audit_logs` já existe em `schema.sql` mas **não é escrita por nenhum lugar do código hoje** — é uma tabela órfã, com propósito diferente do log central (guardaria "quem mudou o quê", não "o que falhou tecnicamente"). Não mexida, só registrada aqui para não esquecer que existe.

**Validação feita:** `php -l` em 100% dos `.php` do projeto (zero erros) + `node --check` no JS inline de `admin/logs.html` e em `admin/topbar.js` (zero erros) + **suite funcional real** contra MariaDB de teste + servidor PHP embutido real (não simulação): ~25 cenários via HTTP de verdade (login, warning real disparado por tentativa de login falha, validação de filtros, paginação com 35 registros reais, clear-all incluindo casos de borda, extração de source_ref, filtro por data, purga do cron, e testes isolados do `stringify()` com mensagens não-string). Todos passando.

**Bugs reais encontrados e corrigidos numa rodada de auto-revisão após a entrega inicial (registro completo, nada escondido):**
1. `persist()` tinha type hint estrito (`string $message`) que causaria `TypeError` se algum código futuro passasse `null`/array/objeto por engano — hoje nenhuma das ~24 chamadas existentes faz isso, mas era um risco desnecessário. Corrigido com normalização segura (`stringify()`).
2. `critical()` concatenava `'[CRITICAL] ' . $message` **antes** de qualquer proteção — se `$message` fosse um objeto sem `__toString`, isso lançava um `Error` fatal não capturado. Corrigido movendo a normalização para o início de cada método público.
3. `json_encode()` do contexto podia retornar `false` silenciosamente (dado não serializável) e gravar isso como se fosse o contexto real. Corrigido com fallback explícito.
4. **Bug mais sério, encontrado só ao testar contra banco real:** `mb_substr()` (extensão `mbstring`) não estava disponível no ambiente de teste — e o `README.md` do projeto não listava essa extensão como requisito, apesar de `SEOService.php` já depender dela. Sem isso, `persist()` falhava silenciosamente em TODO warning/error/critical do sistema (caía no catch, só ia pro arquivo) — ou seja, em qualquer hospedagem sem `mbstring`, o painel de logs ficaria **sempre vazio, sem nenhum erro visível**, o que seria muito confuso de diagnosticar. Corrigido com fallback (`substr()` puro se `mb_substr` não existir) e `mbstring` adicionado ao `README.md`.
5. `$_GET`/corpo de requisição não validavam que os filtros eram valores escalares — `?channel[]=x` quebraria o bind do PDO. Corrigido com uma função `scalarParam()` que ignora valores não-escalares.
6. Faltava validação de `level`/`status` inválidos no `GET /logs` — um valor fora da lista simplesmente não batia com nada, sem avisar por quê. Adicionada validação com erro 422 claro.
7. A contagem de badges usava busca de substring (`strpos($w, 'status =')`) pra excluir a cláusula de status — funcionava, mas era frágil. Refeito com um array próprio, sem depender de coincidência de texto.
8. **Bug de lógica real no `clear-all`:** se o corpo pedisse `status=cleared` explicitamente, a query combinava `status != 'cleared' AND status = 'cleared'` — uma contradição que nunca batia com nada, silenciosamente. Corrigido: os dois casos (status explícito vs. padrão "tudo que não estiver limpo") agora são mutuamente exclusivos.
9. O botão "Limpar tudo" continuava clicável mesmo com o filtro na visão "limpos" (não faz sentido limpar o que já está limpo). Corrigido com `:disabled` + guarda explícita na função.
10. Mensagem vazia (ex: alguém loga `null`) virava uma linha em branco ilegível no painel. Corrigido com um marcador `(mensagem vazia)`.

**Limitação residual conhecida e aceita conscientemente (não é bug, é trade-off):** o fallback de truncamento sem `mbstring` (item 4) usa `substr()` por bytes, não por caractere — em tese, um texto com acentuação que ultrapasse exatamente 500 bytes no ponto de corte poderia truncar um caractere UTF-8 no meio. Como isso só ocorre na ausência de uma extensão praticamente universal em hospedagem PHP real, e o impacto é cosmético (não perda de dado, não erro), não foi tratado com mais complexidade — mencionado aqui por transparência, não por ser um problema prático esperado.

**Critério de pronto:** ✅ atingido e verificado com teste real (não só revisão de código) — um erro real (login com senha errada) aparece no painel com canal/nível/status/contexto corretos, e todas as ações (resolver, limpar, limpar em lote, filtrar, paginar) funcionam contra um banco de verdade.

---

### Fase 1 — Contrato padrão de integração (`IntegrationResult`)
**Status:** ⬜ Não iniciado
Objeto de valor `IntegrationResult::success($data)` / `IntegrationResult::failure($message, $raw = null)` usado por toda chamada externa. Toda falha aciona automaticamente o `SystemLogger` (Fase 0) com o canal certo.

Aplicar primeiro em (validar o padrão antes de estender):
- `core/providers/ProviderManager.php` (IA — Claude/Gemini/OpenAI)
- `core/services/SiteManager.php` (WordPress)

Depois reutilizar em: Search Console (Fase 3/4), WhatsApp/e-mail (Fase 5), envio de relatório (Fase 7).

**Critério de pronto:** uma falha real de IA ou WordPress hoje aparece em `admin/logs.html` com a resposta de erro exata da API externa, não só "erro interno do servidor".

---

### Fase 2 — Publicação e agendamento controlados pela plataforma
**Status:** ⬜ Não iniciado
Hoje `core/services/PublishService.php::publish()` é chamado **de forma síncrona** direto no endpoint (`api/v1/posts.php`, rota `/posts/{id}/publish`) — se a chamada ao WordPress falhar naquele segundo, não há retry. E `schedule()` hoje manda status `future` pro WordPress e deixa o WP-Cron do próprio site publicar — a plataforma perde a visão do que acontece depois.

O que precisa mudar:
- Publicação (e agendamento) passam a ser um job `PublishContentJob` na fila já existente (`core/queue/Queue.php` + `core/models/JobModel.php`), usando `available_at` para agendamento exato e o retry com backoff **que já existe no mecanismo da fila** (3 tentativas, 5 min) — não é preciso construir fila nova.
- Checar idempotência antes de tentar publicar de novo: se `content->wp_post_url` já estiver preenchido, não republica, só segue pro próximo passo (evita posts duplicados no WordPress em caso de retry após falha parcial de rede).
- Se um job agendado passar do horário programado sem executar (cron caiu, etc.), isso deve gerar um `critical` em `system_logs` — alerta para a própria operação, não só o cliente.
- Ver fluxograma (seção 4) para a sequência completa.

**Critério de pronto:** aprovar um post não publica na hora — cria o job; o post fica de fato publicado (`published_at` gravado) mesmo que a primeira tentativa falhe, sem duplicar no WordPress.

---

### Fase 3 — Conexão Google Search Console + Sitemap
**Status:** ✅ Implementada e testada (suite funcional de 14 cenários contra MariaDB + servidor PHP reais — detalhes no ALTERACOES.md)
- Fluxo OAuth2 do Google (escopos Search Console + Indexing API + userinfo.email) por site, guardando `access_token` + `refresh_token` criptografados com o mesmo `encrypt()`/`decrypt()` de `sites.wp_password`. `state` assinado por HMAC (JWT_SECRET) com validade de 30 min contra CSRF. Popup + postMessage no front (`admin/sites.html`, modal de edição).
- **Decisão de modelagem FECHADA:** tabela dedicada `site_google_connections` (não a genérica `site_credentials`) — token OAuth tem formato e ciclo de vida próprios (expiração ~1h + refresh + escopos + status `connected/revoked/error` + `last_error`). Migração em `database/migrate_search_console.sql` (testada contra banco existente) + `schema.sql`.
- Campo `sites.sitemap_url` com detecção automática (`sitemap_index.xml` → `sitemap.xml` → `wp-sitemap.xml` → `post-sitemap.xml`) e correção manual.
- Renovação automática do access token com margem de 2 min; `invalid_grant` marca a conexão `revoked` para a UI oferecer "Reconectar"; sem `refresh_token` na troca inicial, falha com instrução clara (revogar acesso em myaccount.google.com/permissions e reconectar).
- **Atenção de prazo (segue valendo):** o Google exige verificação de app para escopos sensíveis — pode levar semanas. Enquanto isso, modo "Testing" no Cloud Console aceita as contas dos clientes como test users. Falta em produção: criar credenciais OAuth no Google Cloud Console, cadastrar o redirect URI `{APP_URL}/api/v1/google/callback`, preencher `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET` no `.env` e rodar a migração.

**Critério de pronto:** ✅ atingido — site conecta via OAuth (com todos os casos de falha tratados e visíveis no log central, canal `search_console`) e tem sitemap configurado (detectado ou manual).

---

### Fase 4 — Indexação automática + Checklist transparente
**Status:** 🟨 Parcial — o núcleo (indexação automática + botão manual) foi implementado e testado junto com a Fase 3; o checklist visual nos três lugares fica para quando a Fase 2 (fila própria) existir
- ✅ **Feito:** ao confirmar publicação, `PublishService::requestSearchConsoleIndexing()` dispara indexação (Indexing API, `URL_UPDATED`) + resubmissão do sitemap — best effort, nunca desfaz/atrasa a publicação, silencioso se o site não tem conexão. Resultado de cada passo em `content_events` (`gsc_indexing`, `gsc_sitemap`) e no log central.
- ✅ **Feito:** botão manual de reindexação (`POST /api/v1/google/reindex`): mesma função de serviço do fluxo automático (princípio "uma implementação, dois gatilhos"), síncrono, exige `wp_post_url` (409 para rascunho), grava evento `manual_reindex` com autor e resultado. **Nota de UI pendente:** o botão ainda não foi posicionado na lista de posts recentes com a regra "só aparece quando failed" — o endpoint está pronto; a exposição visual entra quando o checklist (abaixo) for construído.
- ⬜ **Pendente (depende da Fase 2):** checklist em três lugares consumindo o mesmo `GET /posts/{id}/checklist` (histórico, lista de posts recentes, dentro do post). A observação sobre a Indexing API ser oficialmente só para `JobPosting`/`BroadcastEvent` segue valendo — já tratada como best effort no código e na copy.

**Critério de pronto:** parcialmente atingido — publicar um post já dispara indexação/sitemap com rastro auditável; falta o checklist visível nos três lugares.

---

### Fase 5 — Notificações (cliente e redator)
**Status:** ⬜ Não iniciado
- Reaproveita `core/services/NotificationService.php` (já tem `sendEmail()` via SMTP/`mail()` nativo, e `sendWhatsapp()` via API HTTP genérica compatível com Z-API/Evolution).
- **Cliente**: recebe só depois que a publicação é confirmada (nunca no clique de aprovação) — via e-mail e/ou WhatsApp, conforme configurado. Precisa de campo novo em `sites` (hoje só tem `email`/`whatsapp` como dados de contato, sem indicar qual canal usar): `notification_channel` (`email`/`whatsapp`/`both`/`none`).
- **Redator**: recebe **sempre por e-mail, nunca WhatsApp**, usando o e-mail da própria conta de usuário cadastrada na plataforma (`users.email`) — inclusive quando algo falha (indexação, por exemplo) e precisa de ação manual.
- Notificação ao cliente dispara em paralelo aos jobs de indexação/sitemap (Fase 4), nunca espera eles terminarem.

**Critério de pronto:** cliente recebe aviso da publicação (URL incluída) só quando ela de fato aconteceu; redator recebe aviso sempre que algo precisar de atenção manual.

---

### Fase 6 — "Ver desempenho" no post
**Status:** 🟨 Backend pronto (junto com a Fase 3); falta só a UI no editor
Botão acima de "Análise de Conteúdo" no editor (`admin/editor.html`), visível quando o site tem Search Console conectado. **O endpoint já existe e foi testado:** `GET /api/v1/google/performance?content_id=&days=` (7–90 dias, padrão 28) devolve cliques, impressões, posição média ponderada e as 20 principais queries daquela URL específica (Search Analytics API). Falta apenas o botão + painel no editor consumindo esse endpoint.

**Critério de pronto:** dentro de um post publicado e indexado, dá pra ver dados reais de desempenho de busca daquela URL específica.

---

### Fase 7 — Relatórios para clientes
**Status:** ⬜ Não iniciado

**O que já existe (reaproveitar, não recriar):**
- `api/v1/report.php` + `admin/relatorio.html` já calculam, por site/mês: total de posts, publicados, contagem de palavras por post, tempo médio de leitura (fórmula 200 palavras/min), quantos pediram ajuste, quantos foram rejeitados.
- `contents.created_at` e `contents.published_at` já existem no schema — período criação→publicação é subtração direta, sem migração.
- `content_approvals` já guarda uma linha por rodada de aprovação (pendente/aprovado/ajustes/rejeitado, com comentário do cliente e data) — retrabalho é contagem de linhas, dado já existe.
- `contents.faq_data` (JSON) já existe — contagem de perguntas de FAQ é direta.
- **Validadores GEO e AEO completos já existem** em `admin/editor.html` (funções `validateGEO()` e `validateAEO()`, por volta das linhas 2475–2598) — cada um com ~5–6 critérios reais e pontuação 0–100 (GEO: headings em pergunta, parágrafo introdutório direto, listas estruturadas, dados concretos, tamanho do conteúdo, citações; AEO: título orientado a resposta, parágrafo de resposta direta após H2, lista numerada, FAQ estruturada, conclusão/resumo). **Ver Errata na seção 9 — isso foi descoberto tarde no planejamento e corrige uma afirmação anterior errada.**

**O que é trabalho novo:**
- **Persistir o resultado GEO/AEO no backend.** Hoje `validateGEO()`/`validateAEO()` rodam só no navegador (Alpine.js) enquanto alguém edita o post — o resultado nunca é salvo. `seo_validation_results` hoje só guarda o score de SEO tradicional (não tem colunas para GEO/AEO). Para o relatório poder mostrar isso depois, o resultado (score + itens) precisa ser enviado e salvo no momento de salvar/publicar o post — **reaproveitando exatamente a lógica que já existe no editor**, não reimplementando os mesmos critérios em PHP (evita duas implementações divergentes, ver princípio 7).
- Formato de entrega: **HTML bem montado por e-mail, não PDF** (decisão explícita — remove complexidade inteira de geração de PDF de servidor). Atenção técnica: e-mail HTML precisa de CSS inline (sem `<style>` no head, sem JS) e estrutura simples, pois clientes de e-mail (Gmail/Outlook) ignoram isso.
- Destinatários do relatório: vivem no **Site** (= cliente, não no `User` interno da agência) — nova tabela `report_recipients` (`site_id, email, label opcional`), permitindo mais de um e-mail por cliente (em vez de um campo único).
- Agendamento de envio automático: novo job recorrente (mesma infraestrutura de fila) que checa diariamente quem tem relatório programado pra aquela data.
- Validação de entrega: **cópia (BCC) prova envio, não prova recebimento** — a validação real é logar o resultado de cada tentativa de envio (sucesso/falha com a resposta real do servidor de e-mail), seguindo o mesmo padrão de log central da Fase 0. Rastreamento de abertura (pixel) fica como possível melhoria futura, não é escopo agora.
- **Nova página "Relatórios" no menu administrativo** — hoje `admin/relatorio.html` existe mas **não está linkado** em `admin/topbar.js` (array `LINKS`), por isso não aparece — correção trivial (uma linha), mas a página em si precisa crescer:
  - Filtro por cliente (site) — reaproveita o mesmo padrão de filtro por `site_id` que `report.php` já usa.
  - Nova tabela `report_sends` (`id, site_id, period, status_geral, error_detail, html_snapshot, sent_at, created_at`) guardando **a cópia congelada exata** do que foi enviado (não regenerar depois com dados que podem ter mudado).
  - Status de sucesso/falha **por destinatário**, não um booleano único geral (um relatório pode ter 2 de 3 destinatários entregues e 1 falho — isso precisa aparecer separado).
  - Botão "Reenviar": reenvia a **cópia congelada original** (não gera um relatório novo), reaproveitando a mesma função de envio usada pelo job automático (mesmo princípio do botão de reindexação da Fase 4).

**Critério de pronto:** um cliente recebe, na data programada, um e-mail HTML com métricas reais do mês (incluindo GEO/AEO e retrabalho); a página "Relatórios" mostra o histórico de envios por cliente com status exato e permite reenviar.

---

### Fase 8 — Comercialização (visão futura, fora do escopo atual)
**Status:** ⬜ Não iniciado — **não começar sem decisão explícita do usuário**
Ver seção 3. Só volta à mesa depois de um período real de uso/testes das Fases 0–7.

## 8. Decisões já fechadas (não re-discutir sem motivo novo)

- Redator recebe notificações **só por e-mail**, nunca WhatsApp, no e-mail da própria conta.
- Cliente só é avisado **depois** da publicação confirmada — nunca no clique de aprovar/publicar.
- Falha de indexação/sitemap **não bloqueia nem atrasa** o aviso ao cliente; só o redator é avisado nesse caso.
- Checklist tem 4 estados (pendente/fila, tentando de novo com contador, sucesso com hora exata, falhou com erro real). Copy padrão do estado "em fila": *"Publicação não realizada. Item em fila de processamento."*
- Botão de reindexação manual: síncrono, mesma função do job automático, some quando já há sucesso, desabilita durante a chamada, loga em `system_logs` + `content_events`.
- Relatório é HTML por e-mail, não PDF.
- Destinatários de relatório ficam no `Site` (cliente), como lista (suporta múltiplos e-mails), não no `User` interno.
- BCC ≠ prova de entrega — validação real é log do resultado de envio.
- AEO/GEO: os validadores **já existem** no editor; o trabalho novo é persistir o resultado, não criar os critérios do zero.
- Comercialização/licenciamento: conscientemente adiado, fora do escopo das Fases 0–7.

## 9. Erratas e observações registradas ao longo da implementação (não apagar)

- **AEO/GEO (corrigido na consolidação final do planejamento):** durante a conversa, foi dito que "AEO e GEO não existem no código, só uma sugestão de placeholder e o FAQ". Isso estava **incompleto/errado** — uma busca mais rasa (truncada por limite de resultados) não alcançou as funções reais. Ao inspecionar `ALTERACOES.md` (que já mencionava "3 validadores SEO/GEO/AEO") e depois `admin/editor.html` diretamente, confirmou-se que `validateGEO()` e `validateAEO()` são implementações completas e bem pensadas, com scoring 0–100 cada. A conclusão prática mudou: o trabalho da Fase 7 não é criar checklist de AEO/GEO, é **persistir** o que já é calculado no navegador.
- **Análise arquitetural completa do projeto (fluxograma geral):** o usuário pediu explicitamente que isso fique para o **final/entrega do projeto**, não agora — os fluxogramas/mapa mental deste documento cobrem só o escopo das Fases 0–7, não o projeto inteiro (que já existia antes deste plano e tem módulos não tocados aqui: quotas de imagem, 2FA, MCP server, etc.). Anotado aqui para não esquecer que essa entrega fica pendente para o encerramento.
- **`core/services/ContentService.php` era duplicata morta** de `core/ContentService.php` (autoload sempre carregava a da raiz primeiro) — removida durante a implementação da Fase 0.
- **Tabela `audit_logs`** já existe em `schema.sql` mas não é escrita por nenhum lugar do código — órfã, propósito diferente do log central (guardaria "quem mudou o quê", não falha técnica). Não mexida.

## 10. Status atual

- **Fase 0 (Log Central): ✅ implementada, revisada (3 rodadas de auto-correção) e testada de ponta a ponta** contra MariaDB real + servidor PHP embutido real, incluindo o caso real de um warning disparado por um login com senha errada e capturado corretamente no painel. 10 bugs reais foram encontrados e corrigidos durante a revisão (detalhes na seção da Fase 0 acima) — o mais sério foi uma dependência não documentada da extensão `mbstring`, que faria o log central falhar silenciosamente em qualquer hospedagem sem ela.
- **Migração visual: ✅ concluída** — paleta OKLCH do novo design (referência Lovable) aplicada em todas as 13 páginas nos dois temas, dark mode preservado, fonte Inter carregada de verdade via topbar.js, `admin/sites.html` reescrito (cards + busca) com 100% da lógica preservada. **Decisão registrada:** o front novo é replicado direto no projeto PHP/Alpine (menos risco de incompatibilidade); o projeto Lovable (`c955bba3-8a22-4275-a342-bcd0e06f47ee`) fica só como referência visual.
- **Fase 3 (Search Console): ✅ implementada e testada** (14 cenários funcionais). **Fase 4: 🟨 núcleo pronto** (indexação automática pós-publicação via gancho no `PublishService` + reindex manual auditado); checklist visual pendente da Fase 2. **Fase 6: 🟨 backend pronto** (`GET /google/performance`); falta só o botão no editor.
- **Fases 1, 2, 5, 7, 8:** ⬜ não iniciadas.
- **Pendências de produção desta rodada:** rodar `database/migrate_search_console.sql`; criar as credenciais OAuth no Google Cloud Console (redirect URI `{APP_URL}/api/v1/google/callback`) e preencher `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET` no `.env`; **iniciar o processo de verificação do app no Google o quanto antes (leva semanas)** — enquanto isso, usar modo Testing com as contas dos clientes como test users.
- **Próximo passo natural:** Fase 1 (`IntegrationResult`) e Fase 2 (publicação via fila própria com idempotência), que destravam o checklist da Fase 4.

