# Review — dashboard-logistica-api-v2

**Base**: main
**HEAD**: 06a82fa0 (feature/dashboard-logistica-api-v2)
**Commits da feature**: 5 (spec, techplan, backend, frontend, tests)
**Arquivos tocados (feature)**: 23 (excluindo PRD)
**Linhas (feature)**: ~3.000

## Sumário

A refatoração entrega o que o techplan promete: 12 handlers em `Plataforma/sinais/Api/Handlers/Dashboard/`, 12 rotas registradas, controller `Dashboard.php` reduzido a dispatcher fino sem string-HTML, `dashboard.js` falando exclusivamente `/api/v1/dashboard/*`, validators dedicados e suíte de testes TDD por handler/validator. A aderência ao padrão V2 (espelha `EdicaoRota/CriarHandler` e o bloco de rotas do EdicaoRota) é boa em forma, mas há **dois blockers reais**: (1) `AcaoMissaoHandler` não enforce tenancy — qualquer JWT válido aciona `pausar/cancelar/iniciar` em qualquer `missao_id` de qualquer cliente, contrariando AC4.5 e o próprio risco #3 do techplan; (2) o `acao` mandado para `MissoesStatus` é `'pausar'` mas o satélite só mata-cha `'pausarMissao'` — todo POST de ação cai no `default => Message::error('Opção inválida')` (bug funcional silencioso, mascarado pelos testes que mockam o laboratório). Recomendação: **voltar pro dev** para corrigir esses dois pontos antes do merge.

Há ainda majors de qualidade: handlers leem de `$_GET` direto em vez do Request (quebra testabilidade), `DashboardLaboratorio` é instanciado com `null` (perdendo o contexto multi-tenant que o Galaxia injetaria), `dashboard.php` continua com KPIs SSR e re-cálculo client-side do mesmo dado (drift), e nenhum handler propaga `jwtClaims` para o Laboratório. Pontos fortes: `dashboard.js` está limpo e bem organizado; smoke test de gates (regex sobre o source) é uma ideia boa para travar regressão de AC; DEPRECATED.md cumpre AC6.1.

## Issues

### 🔴 Blocker (2)

#### B1 — `AcaoMissaoHandler` não valida tenancy do `missao_id`
**Onde**: `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/AcaoMissaoHandler.php:36-37`
**Problema**: O handler recebe `{id}` da URL, valida só formato (positivo) e chama `(new DashboardLaboratorio(null))->acaoMissao(['missao_id' => $id, 'acao' => $acao])`. Não há nenhuma checagem de que essa missão pertence ao `customer`/`empresa` do JWT. `DashboardLaboratorio::acaoMissao` (`Logistica/sinais/Dashboard/Laboratorios/DashboardLaboratorio.php:270-307`) também não checa tenancy — repassa direto pra `MissoesStatus($galaxia=null, $acao, ['missao_id'=>$id])`. Resultado: qualquer usuário autenticado em qualquer tenant consegue pausar/cancelar/iniciar missão de qualquer outro tenant via `POST /api/v1/dashboard/missoes/{id}/acao`.
**Por que é blocker**:
- Viola **AC4.5** literalmente ("handlers consideram customerControl/companyControl e colunas block_* como o restante do código V2").
- Viola **RN 4** ("Cada handler... respeita as regras multi-tenant").
- Viola o risco já mapeado no **techplan §Riscos #3** ("handler valida customerControl/companyControl ao buscar a missão antes de aplicar a ação (não confiar só no JWT)") — o plano previa, a implementação não fez.
- Contraria a memória `feedback_block_columns` (multi-tenant é hard requirement).
**Como corrigir**:
1. Antes de chamar `acaoMissao`, carregar a missão e checar `block_empresa_id`/`block_cliente` contra `$req->jwtClaims['customer']`. Mais simples: passar o claim ao laboratório e fazer a checagem dentro de `MissoesStatus` (ou no laboratório).
2. Esquema mínimo:
   ```php
   $claims = $req->jwtClaims ?? [];
   $entrada['_tenant'] = ['customer' => $claims['customer'] ?? null];
   $resultado = $lab->acaoMissao($entrada);
   ```
   E `DashboardLaboratorio::acaoMissao` carrega via Sonda da missão filtrando `block_cliente`/`block_empresa_id` antes de delegar ao satélite.
3. Mesmo defeito existe (em menor grau, leitura) em `DetalhesMissaoHandler.php:28-29` — `listarMissoes(['missao_id'=>$id])` precisa filtrar tenant. Se `DashboardApi/Laboratorios/MissoesAnaliticoLaboratorio` já aplica `block_*`, documentar; senão B1 estende a esse handler.

#### B2 — `acao` enviada ao `MissoesStatus` não bate com o `match` do satélite
**Onde**: `componentes/Missoes/Logistica/sinais/Dashboard/Laboratorios/DashboardLaboratorio.php:285` (interage com `Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesStatus.php:51-63`)
**Problema**: O laboratório passa `$acao` cru (`'pausar'`, `'retomar'`, `'cancelar'`, `'iniciar'`) como `$opcao` para `new MissoesStatus($galaxia, $acao, [...])`. O `match` em `MissoesStatus` só conhece `'pausarMissao'`, `'retomarMissao'`, `'cancelarMissao'`, `'iniciarMissao'`. Resultado: cai no `default => (new Message())->error('Opção inválida')`. Como `MissoesStatus->response` retorna o envelope de erro, `acaoMissao` devolve `status=>error` e o handler responde 400 — **nenhuma ação em missão funciona pelo dashboard**.
**Por que é blocker**: Quebra US1 AC1.3 ("executar ação em missão produz mesmo efeito de domínio do legado"). A feature jura paridade funcional zero-regressão. Os testes (`AcaoMissaoHandlerTddTest.php:70-97`) **mockam** o laboratório, então passam — o bug só aparece em integração real.
**Nota**: Pode ser bug pré-existente do `DashboardLaboratorio`, mas está sendo herdado e exposto via API V2 sem teste de integração — owner agora é a feature.
**Como corrigir**:
- Mapear no laboratório:
  ```php
  $opMap = ['pausar'=>'pausarMissao','retomar'=>'retomarMissao','cancelar'=>'cancelarMissao','iniciar'=>'iniciarMissao'];
  $opcao = $opMap[$acao] ?? null;
  if (!$opcao) return ['status'=>'error', ...];
  $satelite = new MissoesStatus($this->galaxia, $opcao, ['missao_id'=>$missaoId]);
  ```
- Adicionar 1 teste de integração real (não-mock) cobrindo `pausar` e `retomar` em `dev_gx_goldie` — exatamente o `AcaoMissaoE2ETest` previsto no techplan §Plano de teste, que **não foi entregue** (só o smoke do dispatcher).

### 🟡 Major (5)

#### M1 — Handlers leem `$_GET` direto, ignorando o `Request`
**Onde**: `Handlers/Dashboard/{Resumo,Listar,Filtrar,Alertas,Desempenho,Extrato,Veiculos,Rotas,VisaoGeral}Handler.php` — todos linha 20-25
**Problema**: Cada handler faz `$filtros = $_GET ?? []`. O `Request` (que existe e é o ponto canônico) não é consultado. Isso (a) impede tests passarem query params via stub do Request (os TDDs atuais só conseguem testar 422 e mockar o lab — não dá pra exercitar combinações de filtro), (b) acopla handler a estado global, (c) diverge do `EdicaoRota/CriarHandler` (que usa `$req->body`).
**Como corrigir**: Adicionar `public array $query = [];` em `Request.php` populado em `fromGlobals()` a partir de `$_GET`, e nos handlers usar `$req->query` em vez de `$_GET`. Bônus: `FiltrarHandler.php:25-30` (alias `data → dia_efetivo`) sai do handler e vai pro Validator.

#### M2 — `DashboardLaboratorio` instanciado com `$galaxia = null` em todo handler
**Onde**: 12 handlers, padrão `new DashboardLaboratorio(null)` (ex.: `AcaoMissaoHandler.php:36`, `FiltrarHandler.php:39`)
**Problema**: O construtor aceita `null` (`Laboratorios/DashboardLaboratorio.php:38`), e o satélite `MissoesStatus` recebe esse `null` adiante. Em V2, `$galaxia` é o objeto Galaxia que carrega `customerData`/`empresaData` para o pipeline injetar `block_*` automaticamente. Passar `null` desliga esse pipeline. Combinado com B1, é a raíz do problema de tenancy. Em endpoints só-leitura ainda funciona porque os labs da `DashboardApi` provavelmente lêem do contexto de sessão atual (frontend mesma origem) — mas isso é frágil e não é "respeito a multi-tenant"; é "depender de um efeito colateral de sessão".
**Como corrigir**: Construir um shim Galaxia mínimo a partir de `$req->jwtClaims` (`customer`, `user`) e passar ao laboratório, ou expor um construtor `DashboardLaboratorio::fromClaims(array $claims)`. O techplan §Riscos #5 falou em "validar no smoke test"; o smoke test entregue (`DashboardDispatcherSmokeTddTest.php`) é só estático sobre o source — não valida isso.

#### M3 — `FiltrarHandler` agrega 7 chamadas em série + perde cache
**Onde**: `Handlers/Dashboard/FiltrarHandler.php:39-48`
**Problema**: Faz `new DashboardLaboratorio(null)` (instância única, ok) e chama 7 métodos do mesmo objeto — bom porque `$cacheRespostas` interno é reaproveitado para `rotasVeiculos` (veiculosStatus + rotasDoDia). Mas:
- O comentário do PHPdoc fala em "chamadas paralelas" e "cache por requisição"; o código é serial. Atualizar a doc ou paralelizar (curl_multi/promises) — como é só PHP-FPM síncrono, manter serial e remover a promessa de paralelo do comentário.
- Não há proteção contra exceção parcial: se 1 dos 7 labs lança, o handler explode 500 e perde os outros 6 blocos. Considerar `try/catch` por bloco devolvendo `null` no campo correspondente (consistente com US1 AC1.5 — não deixar tela travada).

#### M4 — SSR + JS pintam o mesmo dado, com risco de drift
**Onde**: `componentes/Missoes/Logistica/sinais/Dashboard/Dashboard.php:64-74` + `html/dashboard.php:332-433` + `html/dashboard.js:99-302`
**Problema**: O techplan §Arquivos alterados decidiu manter SSR atual (pra não regredir TTI). Mas o JS reproduz o mesmo HTML em `DashboardTemplates.missaoCard/linhaVeiculo/cardRota` com strings inline (estilos hardcoded e classes diferentes do PHP em alguns pontos — ex.: PHP usa `<?= $galaxia->luz('MissaoCard',...) ?>` dentro de `g-kanban-coluna-cards`, JS substitui por template `<div class="g-missao-card">`). Drift é só uma questão de tempo. AC1.1 (regressão visual zero) depende de paridade pixel-a-pixel entre os dois caminhos, que ninguém testou.
**Como corrigir** (escolher um):
- (a) Eliminar SSR dos 9 blocos — `dashboard.php` vira só skeleton + JS hidrata tudo. Aceito pelo RN 9.
- (b) Manter SSR e usar **um único renderizador** (ex.: o `Luz_MissaoCard` continua sendo o card único; o JS só atualiza dados via `data-*` attributes em vez de re-renderizar HTML). Mais trabalho.
- (c) Aceitar drift e adicionar QA visual no checklist de release (registrar em "Dúvidas em aberto").

#### M5 — `acaoMissao` é POST com mutação mas `dashboard.js` usa `confirm()` simples e nenhum CSRF
**Onde**: `componentes/Missoes/Logistica/sinais/Dashboard/html/dashboard.js:419-431`
**Problema**: `executarAcaoMissao` faz `POST /api/v1/dashboard/missoes/{id}/acao` via `credentials: 'same-origin'` (cookie de sessão Galaxia) sem CSRF token e sem header customizado. Mesma origem mitiga, mas o `AuthJwtMiddleware` aceita sessão como auth (linhas 56-68). Endpoints idempotentes (GET) ok; um POST mutante deveria exigir token explícito (Bearer JWT do storage local) ou um header custom (`X-Requested-With`) que browser não envia em form-POST cross-site. Verificar se `ValidacaoConteudoMiddleware` já bloqueia (skipValidacaoConteudo está como `false` no `dashboardAcaoMissao`, então parece sim — confirmar). Caso confirmado, virar nit; caso não, é major de segurança.

### 🟢 Minor (4)

#### m1 — Strings hardcoded em PT-BR no `dashboard.js`
**Onde**: `dashboard.js:83,363,366,420,424,605,634-636,...`
**Problema**: `'Sem permissão...'`, `'Erro ao carregar...'`, `'Deseja '+acao+'...'`, `'Pendente'/'Em Andamento'/...` (`DASH_LABELS_STATUS_MISSAO`). Memória `feedback_views_i18n`/spec não exige migrar tudo agora, mas as **chaves novas em `linguagens/pt-br.json` (`dashboard_erro_carregar`)** previstas no techplan §Arquivos alterados não foram criadas e o JS escreve em PT direto. Diff de `linguagens/*.json` mostra só strings de outra feature (Expedicao) — nada novo p/ Dashboard. Não bloqueia, mas perde a chance prevista.

#### m2 — `cssvar`s e cores hex hardcoded em todo o JS
**Onde**: `dashboard.js:12-38, 133-186, 244-289`
**Problema**: Paleta inteira (`#a78bfa`, `#34d399`, `rgba(...)`) duplicada vs. CSS do `dashboard.php`. Migrar pra CSS variables (`var(--g-kpi-purple)`) reduz drift e facilita white-label futuro.

#### m3 — `confirm()` nativo do browser
**Onde**: `dashboard.js:420`
**Problema**: O resto do dashboard usa `DashboardToast` premium; `confirm()` quebra a estética. Substituir por modal custom (mesmo estilo do `_renderDetalhesMissao`).

#### m4 — Test naming não-padrão dentro do diretório `Testes/Validators/`
**Onde**: `Testes/Validators/AcaoMissaoValidatorTddTest.php` etc.
**Problema**: Convenção V2 (`feedback_referencias_v2`) é `<NomeDaClasse>TddTest.php` — ok, batem. Mas os testes de Handler estão sob `Logistica/sinais/Dashboard/Testes/Handlers/` enquanto o handler vive em `Plataforma/sinais/Api/Handlers/Dashboard/`. Espelhamento quebrado — o tester sênior não acha o teste a partir do handler. Recomendado: mover os Handler tests para `Plataforma/sinais/Api/Testes/Handlers/Dashboard/` (espelhando a fonte) ou pelo menos adicionar README explicando a convenção.

### ⚪ Nit (3)

#### n1 — Imports com aliases agressivos em `Api.php`
**Onde**: `Api.php:24-35` (12× `use ... as Dash...Handler`)
**Problema**: Todos os handlers Dashboard ganham alias `Dash...`. Funciona, mas suja o topo. Alternativa: importar pelo namespace `use ...Handlers\Dashboard;` e referenciar `Dashboard\AcaoMissaoHandler::class`.

#### n2 — `Luz_MissaoCard` e `Luz_AlertaItem` mantidos no controller "para o caso de"
**Onde**: `Dashboard.php:90-106`
**Problema**: Comentário diz "Deprecar quando o JS assumir integralmente." A view ainda chama `$galaxia->luz('MissaoCard',...)` (`html/dashboard.php:591, 673`), então não dá pra remover hoje. Manter, mas adicionar `@deprecated` PHPdoc com data alvo, senão vira código órfão.

#### n3 — `$visão_geral` (variável com acento) em `dashboard.php:16`
**Onde**: `componentes/Missoes/Logistica/sinais/Dashboard/html/dashboard.php:16, 409, 416, 423, 430`
**Problema**: PHP aceita, mas é caçapelo de bug — se algum lugar referenciar `$visao_geral` sem acento, fica `undefined`. Padronizar `$visaoGeral` ou `$visao_geral`.

## Pontos fortes

- **Dispatcher fino entregue de verdade**: `Dashboard.php` foi de 283 linhas para 107 e o smoke test (`DashboardDispatcherSmokeTddTest.php`) trava AC3.1/AC3.2 com regex sobre o próprio source — bom mecanismo defensivo contra regressão.
- **Bloco de rotas e métodos `dashboardXxx` no `Api.php` espelham EdicaoRota com fidelidade** (`Routes.php:44-56`, `Api.php:186-269`). Mantém uma referência V2 só, sem inventar nada.
- **`DashboardApi.get/post` no JS é coeso**: uma função única para o transport, tratamento centralizado em `_tratar`, redirect 401 → `/login`, throw com `{code,message,status}`. Bem encapsulado.
- **`DEPRECATED.md` cumpre AC6.1 com tabela de mapeamento velho→novo** — facilita o próximo PRD que de fato apagar o `DashboardApi`.
- **Validators são puros e testáveis isoladamente** (sem dependência de banco) — `AcaoMissaoValidator`, `FiltroDashboardValidator` e `DetalhesMissaoValidator` estão exatamente no formato Otp/Edicaorota.
- **Toast premium em vez de `alert`** no JS, com fade in/out e cores por tipo.

## Recomendação

🛑 **Voltar pro dev** antes do merge.

- B1 (autorização ausente) é hard requirement multi-tenant da própria spec/AC4.5 — sem isso o endpoint `POST /dashboard/missoes/{id}/acao` é uma escalação cross-tenant pronta.
- B2 (mismatch de string da `acao`) inutiliza completamente o fluxo principal de US1 AC1.3 — o feature está "verde" só porque o teste mocka o laboratório.

Resolvendo B1+B2 e idealmente M1+M2 (que são a infraestrutura para B1 ficar limpo), o PR pode entrar com os majors restantes (M3-M5) virando issues de follow-up. A entrega arquitetural em si é sólida e segue padrão V2 — falta apenas fechar o loop de tenancy e adicionar **um** teste de integração real (não-mock) para `AcaoMissaoHandler` que teria pego B2 sozinho.

---

## Rodada 2

**Base**: df5364ac
**HEAD**: 9af1cc15
**Commits revisados**: 2 (`9a82f5a1` fix, `9af1cc15` test)
**Arquivos tocados (excluindo PRD)**: 17 (12 src + 5 testes novos/atualizados)
**Linhas**: +1.715 / -41

### Sumário

Os 4 issues abertos na rodada 1 foram **endereçados**. B1 e B2 ganharam tratamento de código no laboratório + handler **e** prova de integração real em `dev_gx_goldie` (não-mock), exatamente o que faltava. M1 ficou cirurgicamente resolvido com `Request::$query` populado em `fromGlobals()` e os 9 handlers GET migrados em bloco. M2 foi resolvido de forma **parcial mas suficiente**: em vez de injetar um shim Galaxia (que exigiria acoplamento com framework), o contexto multi-tenant agora viaja como `_tenant['customer']` / `_customer_id` no array de entrada — funcionalmente correto e o trade-off está documentado em `03-backend.md` como follow-up. Nenhum blocker novo foi introduzido pelas correções.

### Verificação por issue

#### B1 — Tenancy em AcaoMissaoHandler — ✅ Resolvido

- `AcaoMissaoHandler.php:39-42` injeta `_tenant['customer']` a partir de `$req->jwtClaims`.
- `DashboardLaboratorio::acaoMissao` (lab.php:291-301) checa `missaoPertenceAoCustomer()` antes de delegar; **fail-closed** se a consulta lançar (`return false` no catch — `lab.php:355-358`).
- `missaoPertenceAoCustomer()` reusa `MissoesAnaliticoLaboratorio` (que já filtra `block_*`) — não duplica regra de tenancy. Boa decisão.
- HTTP 403 para `code=forbidden`, 400 para erro de domínio (handler:48-52). Diferenciação correta.
- `DetalhesMissaoHandler.php:28-32` propaga `_customer_id` no canal de leitura — extensão prevista no original B1.
- **Integration test real** em `Testes/Integracao/TenancyB1IntegrationTest.php` cobre: (a) sem `_tenant` não dispara forbidden, (b) `_tenant.customer=42` + `missao_id=888888888` ⇒ `code=forbidden`, (c) `_tenant` sem `customer` não dispara verificação. 8 asserts pass.

#### B2 — String mismatch pausar/pausarMissao — ✅ Resolvido

- `DashboardLaboratorio::acaoMissao` (lab.php:304-313) aplica `$mapaOpcoes = ['pausar'=>'pausarMissao', ...]` antes de instanciar `MissoesStatus`. `MissoesStatus` agora recebe a opção que seu próprio `match` reconhece.
- **Integration test real** em `Testes/Integracao/AcaoMissaoB2IntegrationTest.php` exercita pausar contra missão real `em_andamento` em `dev_gx_goldie` e afirma explicitamente `mensagem NÃO contém "Opção inválida"` — exatamente o teste que faltava. 15 asserts pass.

#### M1 — Handlers lendo $_GET direto — ✅ Resolvido

- `Request.php:12,30` adiciona `public array $query = []` populado em `fromGlobals()`.
- 9 handlers GET trocados para `$req->query`: Alertas, Desempenho, Extrato, Filtrar, ListarMissoes, ResumoOperacional, RotasDoDia, VeiculosStatus, VisaoGeralDia. Confirmado por grep — nenhum handler ainda lê `$_GET` direto.
- Novo `Testes/Infra/RequestQueryTddTest.php` trava regressão (regex sobre source proibindo `$_GET` em handlers). 9 asserts pass.

#### M2 — DashboardLaboratorio(null) — ✅ Resolvido (parcial, com trade-off documentado)

- `new DashboardLaboratorio(null)` permanece em todos os 12 handlers (verificado via grep).
- O trade-off é explícito: em vez de construir um Galaxia shim, o contexto trafega como dado (`_tenant`/`_customer_id`). Para writes (única ação mutante = `acaoMissao`) o tenancy é re-verificado dentro do lab via `missaoPertenceAoCustomer`. Para reads, `_customer_id` é propagado em `DetalhesMissaoHandler`.
- Documentado em `03-backend.md` Rodada 2 como follow-up.
- Aceitável: o objetivo do M2 era fechar o gap de B1, e isso está fechado por outra via igualmente válida. Não bloqueia merge.

### Issues novos introduzidos pela rodada 2

Nenhum blocker. Notas menores (não escalam):

- **n4** (nit) — `_tenant` é injetado **só** se `!empty($claims['customer'])` (handler:40). Isso significa que uma chamada autenticada apenas por sessão (sem JWT.customer) ainda passa direto pro lab sem verificação de tenant — depende do `DashboardLaboratorio` confiar no contexto de sessão Galaxia para esse caminho. Ok hoje (mesma origem, AuthJwtMiddleware aceita sessão), mas convém sentinela: idealmente o middleware sempre devolveria `customer` em `jwtClaims` (mesmo na auth por sessão) e o handler poderia tratar ausência como 401. Follow-up.
- **n5** (nit) — `missaoPertenceAoCustomer` chama `MissoesAnaliticoLaboratorio` com `_customer_id` mas `MissoesAnaliticoLaboratorio` precisa de fato honrar esse filtro. Os testes de integração de B1 confirmam o efeito (forbidden corretamente disparado em cross-tenant), então tá OK na prática, mas o contrato `_customer_id → filtro WHERE` não está documentado no MissoesAnalitico. Adicionar PHPdoc lá fortaleceria o contrato.
- **n6** (nit) — Dois integration tests pré-existentes tinham caminho de autoload errado (10x `..` em vez de 7) e foram corrigidos nesta rodada. Bom catch — não bloqueia.

### Pontos fortes da rodada 2

- **Integration tests reais em `dev_gx_goldie`** (B1+B2) com guarda `SELECT current_database()` que faz SKIP gracioso fora do banco esperado — exatamente o padrão `feedback_integration_tests` da memória.
- **Fail-closed em `missaoPertenceAoCustomer`** (catch retorna `false`) — postura defensiva correta para verificação de autorização.
- **Reuso de `MissoesAnaliticoLaboratorio`** para checar tenancy em vez de reimplementar regra `block_*` — DRY e consistente.
- **Documentação da rodada 2 dentro de `03-backend.md` e `05-testes.md`** com PASS counts, trade-offs explícitos e issues que viraram follow-up — bom rastro para o próximo reviewer.
- **Nenhum raw SQL em código de produção** introduzido (raw SELECT só nos integration tests, que é aceitável).

### Veredito

✅ **Aprovado para merge.**

Os 2 blockers e 2 majors da rodada 1 estão resolvidos com cobertura de teste apropriada (unit + integration real). M3, M4, M5 da rodada 1 permanecem como follow-up planejado, conforme acordado. Nenhum blocker novo foi introduzido. A entrega cumpre AC4.5 (multi-tenant), US1 AC1.3 (paridade funcional do `acaoMissao`) e o risco #3 do techplan original.
