# P4 — Áreas de estatística (métricas privacy-first)

| Campo | Valor |
|---|---|
| Programa | F7+ — operação, estrutura e conteúdo |
| Status | `revisado` |
| Depende de | P0, P3 |
| Habilita | P5 (dashboard), P12 (estatísticas públicas) |
| Tamanho | **G** (4–6 sessões) |
| ADRs | **ADR-047 (aceita)** — métricas privacy-first; ADR-019, ADR-024, ADR-035, ADR-018 |

## 1. Objetivo

Entregar as **áreas de estatística** do painel sem transformar o produto em APM nem em vigilância:
contadores agregados de uso do portal, ranking de páginas, uso por módulo, erros 404 e métricas de
conteúdo — com retenção, expurgo e nenhum dado pessoal.

**Resultado observável:** o painel mostra “notícias publicadas”, “páginas mais visitadas”,
“requisições por módulo” e “404 recentes” no período escolhido; nenhuma tabela guarda IP, e-mail,
user-agent cru ou URL com query string pessoal.

## 2. Estado atual

- `CmsDashboardController` só renderiza usuário e versão (`src/core/Area/Cms/Logic/Controllers/CmsDashboardController.php`).
- GA é opt-in (`SeoSettings::analyticsId()`), carregado por JS same-origin; não há métrica própria.
- `core_audit_logs` é evidência, **não** analytics (ADR-035); não usar para ranking/contadores.
- `RetentionRegistry`/`RetentionTarget` já existem para expurgo por módulo/porta.
- Não há tabela, porta ou middleware de métricas.

## 3. Escopo fechado

### S1 — ADR-047 + contrato + schema de agregados

> **D4:** a métrica é agregada e privacy-first; sem PII/APM; a alternativa mais simples que responde
> “o que foi acessado e quanto” é a escolhida.

- Registrar a **ADR-047 (aceita)**: o que é coletado (rota nomeada, módulo, locale, status, duração,
  `is_bot`), o que **nunca** é coletado (IP cru, e-mail, user-agent cru, query string, corpo,
  cookie de analytics, identificador entre dias), retenção, agregação, acesso e transparência.
- Portas Core:
  - `Core\Contracts\Metrics` (ingestão + consulta de agregados);
  - `Core\Contracts\MetricCards`/`MetricRegistry` (cards contribuídos por módulos);
  - `Core\Contracts\UniqueCounter` (HyperLogLog em Valkey; implementação de array nos testes).
- Tabelas (schema `core`):
  - `core_metrics_daily`: `day date`, `route_name text`, `module text`, `locale text`,
    `status_class text` (`2xx|3xx|4xx|5xx`), `requests bigint`, `duration_sum_ms bigint`,
    `duration_max_ms int`, `bot_requests bigint`; PK composta ou `id` + unique
    `(day, route_name, module, locale, status_class)`.
  - `core_metrics_uniques`? **Não**: únicos ficam no Valkey HLL com TTL; o banco guarda apenas
    agregados. Se um relatório precisar de únicos históricos, o valor é materializado no fechamento
    do dia (`unique_visitors int`) — sem hash persistido.
- Índices por `(day)`, `(module, day)`, `(route_name, day)`; `down()` move para `backup_*`.
- `metrics:expunge` via `RetentionRegistry` (padrão 400 dias para agregados; HLL 90 dias), com
  `--dry-run` e relatório. Nada de apagar auditoria.

### S2 — Ingestão no fim da resposta

- Middleware `CollectRequestMetric` na área **Portal** (e API, com dimensão própria), executado em
  `terminate` (depois do response): não adiciona latência perceptível.
- Coleta: `route()->getName()`, módulo (token do controller/rota), locale resolvido, status class,
  duração (`microtime`), `is_bot` por user-agent (classe, não string).
- URL: apenas o **nome da rota**; nunca path com slug/ID ou query string. Para 404, guardar apenas
  o primeiro segmento normalizado? Decisão: `route_name = 'http.404'` + `module = 'core'`, sem path,
  para não guardar dado pessoal em URL. O diagnóstico de 404 fica no log técnico.
- Unicidade: `UniqueCounter::add(day, hash)` com HMAC de `IP + UA + day` e pepper versionado; só o
  HLL é incrementado; nenhum hash em PG. `DNT`/`Sec-GPC` → não conta único, mas conta request.
- Escrita: `INSERT ... ON CONFLICT DO UPDATE` idempotente (incrementa contadores); falha de métrica
  **nunca** quebra a resposta (fail-open + log). Teste com banco indisponível.
- Amostragem: nenhuma no MVP; se o volume exigir, decisão registrada (não silenciosa).

### S3 — Cards do dashboard e catálogo por módulo

- `MetricRegistry` no Core: um módulo registra `MetricCard` no `register()` com `id`, `label`
  (chave i18n), `icon` (Lucide), `permission` (token), `value(): int|string`, `url` opcional,
  `position`. O Core resolve os cards por request, **checando permissão** (sem vazar número de
  módulo que o usuário não vê).
- Cards Core: usuários ativos, módulos ativos, eventos de auditoria nas últimas 24 h, requisições
  do portal no período.
- Cards dos módulos: Notícias (publicadas/rascunhos), Dados Abertos (datasets), LGPD (DSAR abertos),
  Licitações/Legislação quando existirem. Cada módulo implementa o seu; o Core não importa módulo.
- `/cms` passa a renderizar os cards; sem card = dashboard atual (degradação graciosa). Cache curto
  (ex.: 60 s) por card/período, invalidado por evento de conteúdo quando possível.
- Testes: card de módulo desativado não aparece; card sem permissão não aparece; card que lança
  exceção não derruba o dashboard (mostra “indisponível” + log).

### S4 — Telas de relatório

- Módulo `Infraestrutura/Estatisticas` (token `estatisticas`, grupo `Infraestrutura`,
  permissions `[VIEW]`).
- Telas:
  - `GET /cms/estatisticas` — visão geral: período (7/30/90 dias, custom), cards, série diária
    (tabela; sem gráfico obrigatório), top páginas, top módulos, status.
  - `GET /cms/estatisticas/paginas` — ranking por rota nomeada, requests, únicos (HLL do dia),
    duração média, 4xx/5xx; paginação e ordenação explícita.
  - `GET /cms/estatisticas/modulos` — requests por módulo, participação, tendência.
  - `GET /cms/estatisticas/conteudo` — contadores por módulo via cards (publicados/rascunhos etc.).
- Filtros: período com validação (`from <= to`, máximo 365 dias), locale, módulo. Nenhum filtro
  livre por SQL.
- Export CSV: streaming, `Content-Disposition: attachment`, proteção contra CSV injection
  (prefixo `'` em células iniciadas por `=+-@`), sem PII; ação auditada.
- Acessibilidade: tabelas semânticas, `caption`, cabeçalhos, ordenação por link/`aria-sort`,
  contraste; sem gráfico que seja a única forma de ler o dado.
- Cache: relatórios cacheados por 60 s por período/filtro; `ETag`/304 não é necessário em CMS
  (privado), mas evitar recomputar a cada clique.

### S5 — Retenção, transparência e fechamento

- `metrics:expunge` agendado (diário) + `--dry-run`; relatório de linhas removidas.
- Política de privacidade (`Portal/Lgpd`) ganha um parágrafo factual: métricas agregadas, sem PII,
  sem cookie de analytics; GA só opt-in.
- Página pública de transparência de métricas fica para P12 (agregado institucional).
- E2E: navegar no portal → métrica aparece no relatório; desativar módulo → métrica histórica
  permanece, mas não cresce; retenção remove.
- `make check` 2× + `E2E=1`; axe; cobertura ≥90%.

## 4. Fora de escopo

- APM, tracing de request, profiling, logs de acesso completos (isso é infra/observabilidade).
- Heatmap, gravação de sessão, fingerprint, cookie de analytics, cross-site tracking.
- GA substituindo métrica própria; GA continua opt-in e separado.
- Dashboard público (P12) e relatório da Ouvidoria (P12).
- BI/warehouse/ETL.

## 5. Fatias de sessão

| Sessão | Entrega | Prova | Depende |
|---|---|---|---|
| **P4.S1** | ADR-047 + portas + migrações + `metrics:expunge` | schema/unique; expurgo dry-run; nenhuma coluna de PII | P3 |
| **P4.S2** | middleware de ingestão + HLL + fail-open | request gera agregado; banco off não quebra; sem IP/URL | S1 |
| **P4.S3** | `MetricRegistry` + cards Core/módulo + `/cms` | cards por permissão; módulo desativado; exceção isolada | S1 |
| **P4.S4** | módulo `Estatisticas` + 4 telas + filtros | ranking/período/locale; paginação; axe | S2, S3 |
| **P4.S5** | export CSV + retenção + LGPD + E2E | CSV safe; expurgo; política; 2× check | S4 |

## 6. Contratos e dados

- **Portas:** `Metrics`, `MetricRegistry`, `UniqueCounter`.
- **Tabelas:** `core_metrics_daily` (+ backup no `down()`).
- **Módulo:** `Infraestrutura/Estatisticas` (token `estatisticas`).
- **Rotas:** `estatisticas.cms.*`.
- **Permissões:** `estatisticas.ver`; cards checam a permissão do módulo de origem.
- **Comandos:** `metrics:expunge`.
- **Eventos:** `core.metrics_collected`? Não no hot path (log por request é ruído). Log apenas de
  falha de ingestão e do expurgo. Span não por request; sim no relatório (`estatisticas.report`).

## 7. DoD

- [ ] ADR-047 aceita; nenhuma PII em tabela, cache, log ou export.
- [ ] Ingestão em `terminate`, idempotente, fail-open e sem impacto perceptível.
- [ ] Cards por permissão; módulo desativado/card com erro não derruba o dashboard.
- [ ] Relatórios com período/filtros validados, paginação determinística e export seguro.
- [ ] Retenção/expurgo com `--dry-run` e relatório; política de privacidade atualizada.
- [ ] `make check` 2× + `E2E=1`; cobertura ≥90%; PHPStan max 0.

## Fechamento — ficha de revisão

> **Obrigatório antes de fechar:** criar `_to_revision/P4-estatisticas.md` e seguir
> `_to_revision/README.md` + `_to_revision/PROTOCOLO.md` (v2.0). A ficha não é gate de build; é a
> entrada da fila de revisão profunda.

- Cabeçalho: fase, `kd` épico/tasks/slices, âncoras (este arquivo, ADRs e código) e status
  `⬜ pendente` → `🟡 em revisão` → `✅ revisado` / `🔧 com correções`.
- Conteúdo: o que foi implementado, arquivos/superfícies a revisar, checklist D1–D14 aplicável,
  riscos e observações.
- Fechamento: preencher “Notas da revisão” com data/revisor/resultado e achados (`kd write
  --anchor`), atualizar a fila em `_to_revision/README.md` e `_to_revision/_inventario.md`.
- Achado com correção exige teste vermelho→verde; a fase só é `closed` no `kd` com a ficha
  registrada.

## 8. Riscos

| Risco | Mitigação |
|---|---|
| Métrica virar PII | allowlist de dimensões, HMAC só no HLL, nenhuma URL/IP em PG, revisão LGPD |
| Hot path lento | `terminate` pós-resposta, upsert atômico, fail-open; medir em `make check` |
| Números divergentes (HLL × agregado) | únicos são estimativa; documentar; não usar para decisão financeira |
| Dashboard pesado | cache curto por card/período; query com índice; cards independentes |
| Export CSV injection | prefixo em `=+-@`, `Content-Type` correto, teste |
| Retenção apagar dado necessário | padrão 400 dias + `--dry-run` + backup; auditoria não é tocada |

## 9. Retomada

```bash
kd prime
kd ask "estatísticas métricas dashboard LGPD HLL"
kd task list --ready
```

Ler: ADR-019/024/035/047 → `src/core/Internal/Retention/**` → `src/core/Area/Cms/Logic/Controllers/CmsDashboardController.php`
→ `src/core/Internal/Widgets/**` (padrão de registry) → `sucesu_analise/02_servicos-base/dashboard.md`
(como referência do que **não** repetir: 23 queries sem cache, cards sem permissão).

Referências irmãs (UX/desenho, não código): `../lotep/V2/Modules/Infraestrutura/Estatisticas/**`
(DataSources, KPI cards, dashboards) e `../lotep/V2/Modules/Infraestrutura/Analytics/**` (separação
ingestão × relatório). Rejeitar `BaseWidget`/abstract, heatmap/sessão com PII, consulta sem cache e
card sem permissão.
