# Review — dashboard-operacional

**Base**: `dev_team` (`63bd2fc5`)
**HEAD**: `34013c51`
**Commits**: 8 (spec → techplan → techplan R3 → wip backend → backend completo → frontend → testes + fix bug)
**Arquivos tocados**: 88
**Linhas**: +14.526 / −1.641

## Sumário

A feature substitui o kanban legado pelo painel operacional novo, reusando o sinal `Logistica/sinais/Dashboard/` em padrão V2 desacoplado. A arquitetura macro está **correta e bem alinhada** com Otp/Edicaorota: dispatcher fino, 8 Laboratórios temáticos, 2 Sondas V2 reusadas + 1 nova (`ErrosMotoristaSonda`), 14 handlers REST `/api/v1/dashboard/*`, frontend vanilla com `fetch` (zero `sendToGalaxia`), shell HTML sem SSR de dados. Cobertura de testes é alta (275 unit + 105 integration pass).

Há **2 blockers**: (1) a fórmula de score nos rankings diverge da spec (RN10/RN11 — pesos errados, variáveis erradas), e (2) o spec OpenAPI (`Api/OpenApi/spec.php`) ficou stale — documenta 11 endpoints removidos e omite os 14 novos. Há também o **bug #1 corrigido de forma frágil**: a correção contorna o sintoma mas mantém inconsistência multi-tenant entre `ErrosMotoristaSonda` e as outras 2 Sondas. Recomendação: **voltar pro dev** para corrigir esses 3 pontos antes do merge — o resto é major/nit absorvível.

## Issues

### 🔴 Blocker (3 issues)

#### B1 — `ScoreCalculator` não implementa as fórmulas da spec
**Onde**:
- `componentes/Missoes/Logistica/sinais/Dashboard/Laboratorios/ScoreCalculator.php:37-66` (`scoreSeparador`)
- `componentes/Missoes/Logistica/sinais/Dashboard/Laboratorios/ScoreCalculator.php:76-93` (`scoreConferente`)

**Problema**:
- **Spec RN10**: separador = `0.5·assertividade + 0.3·tempo⁻¹ + 0.12·itens + 0.08·cubagem` (50/30/12/8). **Implementado**: `40·assert + 40·vel + 15·volume + 5·liderança` — pesos diferentes, **`cubagem` ausente** e **`liderança` adicionada** (não está na spec).
- **Spec RN11**: conferente = `0.35·tempo⁻¹ + 0.25·cubagem + 0.40·assertividade` (35/25/40). **Implementado**: `60·acurácia + 40·(1-retrabalho)` — usa `erros/cargas` em vez de tempo, **`cubagem` ausente**, **assertividade ausente** como variável independente.

**Por que é blocker**: invalida critério de aceite das User Stories US5/US6 (rankings mostram score errado) e da regra de negócio explícita; os testes unit (`ScoreCalculatorTddTest` 38/38 PASS) cobriram só a implementação atual, não a spec — então o teste não pegou. Métrica de adoção (gestor confiar no ranking) cai por terra se o score não bate com o que a spec promete.

**Como corrigir**:
1. Reescrever `scoreSeparador` para exatamente: `0.5·norm(assert,100) + 0.3·norm(tempoRef/tempo,1)·100 + 0.12·norm(itens,refItens)·100 + 0.08·norm(cubagem,refCubagem)·100`. Adicionar parâmetro `cubagem` no construtor da chamada em `RankingSeparadoresLaboratorio.php:99`. Remover componente "liderança" (vai para coluna informativa, não pra score).
2. Reescrever `scoreConferente` para exatamente: `0.35·norm(tempoRef/tempo,1)·100 + 0.25·norm(cubagem,refCubagem)·100 + 0.40·norm(assert,100)`. Atualizar `RankingConferentesLaboratorio.php:89` para passar `tempoSegMedio`, `cubagem`, `assertividade_pct` (calcular com base em `cargas - erros`).
3. Atualizar `ScoreCalculatorTddTest.php` com asserts numéricos derivados das fórmulas da spec, não da implementação.
4. Conversar com PO sobre eventual desvio justificado e atualizar a spec se aceito.

---

#### B2 — `OpenApi/spec.php` fora de sincronia: documenta 11 rotas removidas e omite 14 novas
**Onde**: `componentes/Missoes/Plataforma/sinais/Api/OpenApi/spec.php:510-686`

**Problema**: O spec OpenAPI ainda declara `/dashboard/desempenho`, `/dashboard/alertas`, `/dashboard/extrato`, `/dashboard/visao-geral`, `/dashboard/veiculos`, `/dashboard/rotas-dia`, `/dashboard/filtrar`, `/dashboard/missoes`, `/dashboard/missoes/{id}`, `/dashboard/missoes/{id}/acao`, `/dashboard/resumo` — **todos handlers deletados nesta feature**. E **não declara nenhum dos 14 endpoints novos** (`/kpis`, `/fluxo-diario`, `/overnight-serie`, `/ranking-separadores`, `/ranking-conferentes`, 6 endpoints de erros, 2 de tempo médio, `/retrabalho`).

**Por que é blocker**: `/api/v1/docs` (Swagger UI gateado por `DEV_USER_IDS`) é a referência canônica da API interna (memória `project_api_interna.md`). Mantê-lo mentindo significa devs futuros tentando chamar endpoints inexistentes (404) e ignorando endpoints reais. O techplan inclusive prevê em "Refactor de Routes.php + Api.php" — o passo equivalente para o spec foi esquecido.

**Como corrigir**: remover blocos das 11 rotas mortas e adicionar entradas para as 14 novas (mesmo nível de detalhe do que existe em `/dashboard/tipos-missao`). Modelo: copiar o estilo `/edicao-rota/{id}` que já está em conformidade. Mínimo aceitável: path + parâmetros obrigatórios (`from`, `to`) + descrição do `data` retornado (referenciar `03-backend.md` linhas 165-180).

---

#### B3 — Fix do bug #1 (`ErrosMotoristaSonda::customerId()`) é frágil e cria inconsistência tenant
**Onde**: `componentes/Missoes/Logistica/sinais/Dashboard/Sondas/ErrosMotoristaSonda.php:140-147`

**Problema**: A correção em `34013c51` apenas **removeu** a linha `$this->customerId() ??` do fallback. Mas a causa real é estrutural: `MissaoLogisticaSonda:33-39` e `MissaoLogisticaItemSonda` têm um método mágico `__call($method, $arguments)` que re-executa `init()` antes de chamar métodos da classe pai — é esse `__call` que faz `customerId()` funcionar lá. `ErrosMotoristaSonda` (linhas 33-51) **não tem `__call`**, daí o erro.

Consequências da correção atual:
- `MissaoLogisticaSonda::getEmpresaId()` (linha 197) ainda usa `$this->customerId() ??` como **primeira opção** do fallback. Em multi-tenant com impersonation (admin acessando como outra empresa, ou JWT do `/api/v1` com `customer` diferente do `$_SESSION['empresa_id']`), `customerId()` retorna o tenant correto do contexto — `$_SESSION` não.
- `ErrosMotoristaSonda` agora pula o `customerId()` e vai direto pro `$_SESSION` — em produção, **lê dados do tenant errado** quando há divergência entre o contexto da Sonda e a sessão. Isso é um vazamento cross-tenant silencioso (memória `feedback_block_columns` aponta tenancy como obrigatória).

**Por que é blocker**: pode parecer inofensivo no `dev_gx_goldie` (1 tenant), mas em prod multi-empresa fica latente. Risco 3 do techplan (multi-tenant em queries com JOIN) explicitamente alerta para esse tipo de erro. O `TenancyB1IntegrationTest.php` foi escrito mas roda só no banco mono-tenant atual.

**Como corrigir**: replicar o `__call` de `MissaoLogisticaSonda:33-39` em `ErrosMotoristaSonda` e restaurar `$this->customerId() ??` no fallback. Ficar igual à Sonda canônica:

```php
public function __call($method, $arguments)
{
    if (method_exists($this, $method)) {
        $this->init();
        return call_user_func_array([$this, $method], $arguments);
    }
}

private function getEmpresaId(): int
{
    return (int) (
        $this->customerId()
        ?? $_SESSION['empresa_id']
        ?? $_SESSION['block_empresa_id']
        ?? 0
    );
}
```

Validar com teste de integração que exercita JWT com `customer` ≠ `$_SESSION` (extensão do `TenancyB1IntegrationTest`).

---

### 🟡 Major (5)

#### M1 — Testes legados com `require_once` para arquivos deletados
**Onde**:
- `Testes/Handlers/AcaoMissaoHandlerTddTest.php:124` (require de `Validators/AcaoMissaoValidator.php` deletado)
- `Testes/Handlers/DetalhesMissaoHandlerTddTest.php:88` (require de `Validators/DetalhesMissaoValidator.php` deletado)
- `Testes/Handlers/FiltrarHandlerTddTest.php` (idem; testa handler deletado)
- `Testes/Validators/AcaoMissaoValidatorTddTest.php`, `DetalhesMissaoValidatorTddTest.php`, `FiltroDashboardValidatorTddTest.php` (testam Validators deletados)
- `Testes/Laboratorios/DashboardLaboratorioB2TddTest.php` (testa `DashboardLaboratorio` deletado)

**Problema**: O `03-backend.md:130-134` explicitamente reconhece o débito ("remoção fica para o Tester") mas o commit de testes não removeu. Esses arquivos vão **fatalizar** se o CI rodar `find Testes -name "*TddTest.php" -exec php {} \;`.

**Como corrigir**: `git rm` os 7 arquivos. Verificar `php -l` em todos os `Testes/**/*.php` no CI script (ou roda em `pre-commit`).

---

#### M2 — Boilerplate `getEmpresaId()` + `getConn()` + `rawSql()` duplicado em 4 Labs
**Onde**:
- `RankingSeparadoresLaboratorio.php:177-225`
- `RankingConferentesLaboratorio.php:151-192`
- `RetrabalhoLaboratorio.php:109-135`
- (Sondas também; padrão herdado mas Lab não deveria conhecer `\PDO`/`block_empresa_id`)

**Problema**: O techplan Risco 5 admitia raw SQL no Lab via `$this->conn` injetado quando JOIN cross-tabela, mas o que aconteceu foi pior: cada Lab carrega seu próprio `getEmpresaId`/`getConn`/`rawSql` (cópias byte-a-byte). Viola DRY e cria 3 lugares para corrigir o B3 acima ou qualquer mudança de estratégia tenant. Conceitualmente, o Lab está fazendo trabalho de Sonda (conhecer schema, parametrizar tenant).

**Por que é major (não blocker)**: funciona, está coberto por testes, e o techplan deixou a porta aberta para essa abordagem. Mas vai sangrar quando aparecer mais 1 Lab com JOIN.

**Como corrigir** (caminho mais barato): extrair `Laboratorios/Util/SqlAdHoc.php` (trait ou classe estática) com `getEmpresaId()`, `rawSql(\PDO, $sql, $params)`, `rawSqlFirst()`. Os 3 Labs passam a usar trait. Ou (caminho V2-puro): adicionar métodos novos nas Sondas existentes (ex.: `MissaoLogisticaItemSonda::agregarPorConcluidoComJoinUsuarios()`) e voltar os Labs a chamar só Sondas — alinhado ao "imitar Otp/Edicaorota" da memória `feedback_referencias_v2`.

---

#### M3 — `TiposMissaoHandler` lê de tabela que não é entity da Sonda
**Onde**: `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/TiposMissaoHandler.php:20-42`

**Problema**: O handler instancia `MissaoLogisticaSonda` (entity = `missao_logistica`), pega o `\PDO` por `->conn()`, e executa `SELECT id, codigo, descricao FROM tipos_missao_logistica`. Está lendo de **outra tabela** usando a Sonda só para roubar a conexão. O `03-backend.md:130` reconhece o desvio ("desvio justificado pela remoção do Lab legado"). O padrão V2 manda 1 Sonda fina = 1 tabela.

**Como corrigir**: criar `Sondas/TipoMissaoLogisticaSonda.php` (1 arquivo, ~50 linhas, espelhando o contrato V2 de `MissaoLogisticaSonda`) com método `listarTodos(): array`. Handler passa a chamar `(new TipoMissaoLogisticaSonda())->listarTodos()`. Mais limpo, alinhado com `feedback_sondas_v2`. Pode ficar para PR de polimento se prazo apertar — mas ficar como está abre precedente ruim.

---

#### M4 — `ApexCharts` carregado via CDN externo
**Onde**: `componentes/Missoes/Logistica/sinais/Dashboard/html/dashboard.php:20`

**Problema**: `<script src="https://cdn.jsdelivr.net/npm/apexcharts">`. O techplan dizia "ApexCharts já presente em `node_modules/apexcharts`; zero nova dependência npm" (techplan linha 99). A implementação puxa do CDN — viola o plano e abre dependência externa em runtime do Genesis (que tradicionalmente serve assets locais). O `04-frontend.md:135` justifica como "mesmo padrão de `Designacoes/html/designacoes_lista.php`" mas Designacoes não é referência V2 (memória `feedback_referencias_v2` reforça Otp/Edicaorota).

**Por que é major (não blocker)**: funciona, mas adiciona ponto de falha externo (offline, CDN down, CSP futura) e faz Genesis depender de jsdelivr. A fix é trivial.

**Como corrigir**: trocar a tag para `<script src="/node_modules/apexcharts/dist/apexcharts.min.js">` (mesmo estilo da `flowbite-datepicker` na linha 24 acima). Validar visualmente no smoke. Se Designacoes também precisar mudar, abrir issue separada — não escopo desta feature.

---

#### M5 — `ScoreCalculator::semaforoRetrabalho` confunde semântica
**Onde**: `componentes/Missoes/Logistica/sinais/Dashboard/Laboratorios/ScoreCalculator.php:129-137`

**Problema**: A spec RN15/AC1.5 fala de "Pendências Overnight: 0 verde, 1–3 amarelo, ≥4 vermelho" e RN14 fala de eficiência (≥95/85). **Não há regra explícita de retrabalho** — o método inverte (`acerto = 100 - pct`) e aplica faixas de assertividade. Resultado: retrabalho de 5% → `acerto=95` → `success`. Razoável, mas acopla retrabalho ao threshold de assertividade e não está na spec. Pior: o método é usado em `ranking-conferentes` para colorir `semaforo_retrab` por conferente (linha do ranking) sem critério escrito.

**Como corrigir**: ou (a) adicionar regra explícita à spec ("Retrabalho ≤5% verde, ≤15% amarelo, >15% vermelho", validar com PO) e implementar separadamente, ou (b) remover `semaforo_retrab` do payload e o frontend deixa neutro. Atual é zona cinza testada mas não especificada.

---

### 🟢 Minor (4)

#### m1 — `Sondas/ErrosMotoristaSonda` duplica código Sonda canônica
Após corrigir B3, restará boilerplate `rawSql`/`getConn`/`getEmpresaId` igual nas 3 Sondas. Vale extrair trait `Sondas/Util/SondaSql.php` no próximo PR — fora de escopo para a feature, mas anotar.

#### m2 — `dashboard.php` tem CSS inline gigante (~250 linhas de `<style>`)
**Onde**: `componentes/Missoes/Logistica/sinais/Dashboard/html/dashboard.php:26-280` aprox.
O techplan listava `dashboard.css` separado ("`html/dashboard.css` (novo) — tokens semafóricos"). Implementado inline. Não bloqueia mas dificulta cache, lint e A/B futuro.

#### m3 — `_isAgregadoPorSemana()` heurístico no JS
**Onde**: `dashboard.js`, comentado em `04-frontend.md:141`.
Detecta agregação por intervalo entre pontos. Frágil se backend retornar série esparsa por outro motivo (gap de dados). Melhor: backend mandar `agregacao: "dia"|"semana"` no envelope. Anotar para iteração 2.

#### m4 — `RetrabalhoLaboratorio::respostaVazia()` retorna `semaforo: 'success'` por padrão
**Onde**: `RetrabalhoLaboratorio.php:104`.
Quando não há dados, mostra verde — pode enganar gestor ("zerado = ótimo!"). Melhor: retornar `semaforo: 'neutral'` ou `null` e o frontend renderizar borda neutra. Caso degenerado, acho que cabe minor.

---

### ⚪ Nit (3)

#### n1 — Comentários de "ORIGEM" datados
`KpisLaboratorio.php:14`, `FluxoLaboratorio.php:13` mantêm "Origem: conteúdo migrado de Sondas/KpisSonda.php". Útil só durante revisão; remover depois do merge.

#### n2 — `Dashboard.php:38-44` `Observatorio()` retorna sempre `[]`
Hook morto. Pode deletar ou comentar `// hook futuro`.

#### n3 — Cor hardcoded `#22c55e/#f59e0b/#ef4444` no JS (`DASH_COR_SEMAFORO`)
`dashboard.js:48-53`. As `var(--bg-*)` do Genesis já têm tokens semafóricos? Verificar e usar `getComputedStyle` se sim, para coerência visual com light/dark mode.

---

## Pontos fortes

- **Padrão V2 desacoplado bem aplicado no fluxo principal**: Handler → Validator → Lab → Sonda. `KpisHandler.php` está em paridade com Otp/Edicaorota e serve de modelo dentro do sinal.
- **Refactor `Dashboard.php` (controller fino)** ficou exemplar — `renderShell()` com 25 linhas, zero chamada a Lab no boot, atende AC8.5 com folga.
- **Frontend cumpre AC8.4 e AC8.5**: zero `sendToGalaxia` no `dashboard.js`, 100% via `fetch /api/v1/dashboard/*`. `dashboard.js` tem 1.000 linhas mas modular (DashboardApi/State/Router/Filtro/Kpis/Abas/Toast).
- **i18n completo** nas 3 línguas (58 chaves novas). PHP usa `show()` em todos os labels visíveis (verificado: 63 ocorrências).
- **Cobertura de teste alta**: 275 unit + 105 integration pass. ScoreCalculator/Validators 100%; Labs com Sondas stubadas; integration em `dev_gx_goldie` validando contra dados reais. Excelente disciplina TDD.
- **Auditoria dos 12 handlers cumprida**: 11 deletados, 1 sobreviveu (`TiposMissaoHandler`). Grep externo (`grep "/api/v1/dashboard/" outros sinais`) confirmou zero callers órfãos.
- **Sondas existentes reusadas** em vez de recriadas (R3 do techplan honrado). `MissaoLogisticaSonda`/`MissaoLogisticaItemSonda` permaneceram intactas e ganharam consumidores legítimos.
- **`ErrosCarregamentoLaboratorio` graceful** quando não há dados — alinhado com decisão "cron pulado" (frontend renderiza "Sem registros no período" em vez de quebrar).
- **`DEPRECATED.md` em `DashboardApi/`** documenta a poda — boa cidadania.

---

## Recomendação

**Voltar pro dev** — 3 blockers tornam o merge prematuro, mas todos têm fix bem-delimitado:

1. **B1**: corrigir `ScoreCalculator` para bater com RN10/RN11 da spec (≈ 1h dev + ajustar ScoreCalculatorTddTest).
2. **B2**: limpar e expandir `OpenApi/spec.php` (≈ 1h dev, mecânico).
3. **B3**: replicar `__call` em `ErrosMotoristaSonda` e restaurar `$this->customerId()` (≈ 15 min + smoke em integration).

Após esses 3 fixes (≈ 3h totais), majors M1 (deletar testes legados, ≈ 5 min) e M4 (CDN → local, ≈ 5 min) podem entrar no mesmo commit. M2/M3/M5 e os minors/nits podem ir para PR de polimento pós-merge ou ficar como TODO documentado.

Padrão V2 da feature está saudável — o que falta é correção de fórmula, sincronia de spec OpenAPI e robustez do fix multi-tenant. Não há red flag arquitetural.
