# Implementação backend — dashboard-logistica-api-v2

---

## Rodada 2 — Correções (2026-05-04)

### Issues corrigidos

#### B1 — Autorização tenant (AcaoMissaoHandler + DashboardLaboratorio)

**Problema**: Qualquer JWT válido conseguia pausar/cancelar/iniciar missão de qualquer tenant.

**Correção**:
- `AcaoMissaoHandler.php`: injeta `$entrada['_tenant'] = ['customer' => $claims['customer']]` antes de chamar `$lab->acaoMissao()`. Sem JWT com `customer`, o campo não é injetado (compatibilidade com contexto de sessão legado).
- `DashboardLaboratorio::acaoMissao()`: quando `_tenant['customer']` está presente, chama o novo método privado `missaoPertenceAoCustomer()` que consulta `MissoesAnaliticoLaboratorio` filtrando por `missao_id + _customer_id`. Lista vazia = missão não pertence ao tenant → retorna `['status'=>'error','code'=>'forbidden',...]`.
- `AcaoMissaoHandler.php`: diferencia `code=forbidden` de erro de domínio — retorna HTTP 403 (não 400).
- `DetalhesMissaoHandler.php`: passa `_customer_id` nos filtros ao chamar `listarMissoes()`, para que o lab filtre tenant no contexto de leitura também.

#### B2 — String mismatch entre laboratório e MissoesStatus

**Problema**: Lab passava `'pausar'` ao `MissoesStatus` que só conhece `'pausarMissao'` — todas as ações caíam no `default => 'Opção inválida'`.

**Correção**: `DashboardLaboratorio::acaoMissao()` agora aplica mapa antes de instanciar `MissoesStatus`:
```php
$mapaOpcoes = ['pausar'=>'pausarMissao','retomar'=>'retomarMissao','cancelar'=>'cancelarMissao','iniciar'=>'iniciarMissao'];
$opcaoSatelite = $mapaOpcoes[$acao];
new MissoesStatus($this->galaxia, $opcaoSatelite, ['missao_id' => $missaoId]);
```

#### M1 — Handlers lendo $_GET direto

**Problema**: 9 handlers faziam `$filtros = $_GET ?? []`, quebrando testabilidade e acoplamento a estado global.

**Correção**:
- `Request.php`: adicionada propriedade `public array $query = []`; `fromGlobals()` a popula com `$_GET ?? []`.
- Todos os 9 handlers GET substituíram `$_GET ?? []` por `$req->query`.

#### M2 — DashboardLaboratorio instanciado com null

**Problema**: `new DashboardLaboratorio(null)` em todos os handlers desligava o pipeline de `block_*` do Galaxia.

**Correção parcial**: O handler de ação (único com mutação) agora passa `_tenant` explicitamente via entrada. Os handlers de leitura passam `_customer_id` nos filtros onde aplicável. A instanciação com `null` permanece porque construir um objeto Galaxia completo a partir de claims exigiria acoplamento com o framework — isso foi documentado como follow-up (nenhum lab de leitura faz write, então o risco é menor).

### Arquivos modificados nesta rodada

| Arquivo | Mudança |
|---|---|
| `componentes/Missoes/Plataforma/sinais/Api/Request.php` | Adicionado `public array $query = []`; `fromGlobals()` popula de `$_GET` |
| `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/AcaoMissaoHandler.php` | Injeta `_tenant` do JWT; diferencia 403 de 400 |
| `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/DetalhesMissaoHandler.php` | Passa `_customer_id` nos filtros; usa `$req->query` (não aplicável aqui pois lê de routeParams) |
| `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/ResumoOperacionalHandler.php` | `$req->query` em vez de `$_GET` |
| `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/ListarMissoesHandler.php` | `$req->query` em vez de `$_GET` |
| `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/DesempenhoHandler.php` | `$req->query` em vez de `$_GET` |
| `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/AlertasHandler.php` | `$req->query` em vez de `$_GET` |
| `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/ExtratoHandler.php` | `$req->query` em vez de `$_GET` |
| `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/VisaoGeralDiaHandler.php` | `$req->query` em vez de `$_GET` |
| `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/VeiculosStatusHandler.php` | `$req->query` em vez de `$_GET` |
| `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/RotasDoDiaHandler.php` | `$req->query` em vez de `$_GET` |
| `componentes/Missoes/Plataforma/sinais/Api/Handlers/Dashboard/FiltrarHandler.php` | `$req->query` em vez de `$_GET` |
| `componentes/Missoes/Logistica/sinais/Dashboard/Laboratorios/DashboardLaboratorio.php` | B2: mapa de ações; B1: `missaoPertenceAoCustomer()`; método privado de verificação de tenancy |

### Arquivos de teste criados/modificados nesta rodada

| Arquivo | O que cobre |
|---|---|
| `Testes/Handlers/AcaoMissaoHandlerTddTest.php` | Atualizado: B1 (403 cross-tenant), M2 (tenant injetado), B2 (ações chegam ao lab sem 422) |
| `Testes/Handlers/FiltrarHandlerTddTest.php` | Atualizado: M1 (req.query usado; $_GET global não contamina) |
| `Testes/Laboratorios/DashboardLaboratorioB2TddTest.php` | Novo: lab real com stub de MissoesStatus — B2 (mapeamento pausar→pausarMissao etc.), B1 (fail-closed cross-tenant) |
| `Testes/Infra/RequestQueryTddTest.php` | Novo: M1 ($query existe, fromGlobals popula, handlers não usam $_GET direto) |

### Comandos de verificação — Rodada 2

- `php -l Request.php` → OK
- `php -l AcaoMissaoHandler.php` → OK
- `php -l DetalhesMissaoHandler.php` → OK
- `php -l FiltrarHandler.php` → OK
- `php -l ResumoOperacionalHandler.php` → OK (+ 6 outros handlers GET) → OK
- `php -l DashboardLaboratorio.php` → OK
- `php -l Testes/Handlers/AcaoMissaoHandlerTddTest.php` → OK
- `php -l Testes/Handlers/FiltrarHandlerTddTest.php` → OK
- `php -l Testes/Laboratorios/DashboardLaboratorioB2TddTest.php` → OK
- `php -l Testes/Infra/RequestQueryTddTest.php` → OK
- `php Testes/Handlers/AcaoMissaoHandlerTddTest.php` → PASS=37 FAIL=0
- `php Testes/Handlers/FiltrarHandlerTddTest.php` → PASS=27 FAIL=0
- `php Testes/Laboratorios/DashboardLaboratorioB2TddTest.php` → PASS=17 FAIL=0
- `php Testes/Infra/RequestQueryTddTest.php` → PASS=9 FAIL=0
- `php Testes/Fluxos/DashboardDispatcherSmokeTddTest.php` → PASS=32 FAIL=0 (sem regressão)
- `php Testes/Validators/AcaoMissaoValidatorTddTest.php` → PASS=20 FAIL=0 (sem regressão)
- `php Testes/Handlers/DetalhesMissaoHandlerTddTest.php` → PASS=14 FAIL=0 (sem regressão)

### Issues não corrigidos nesta rodada (follow-up)

- **M3** (FiltrarHandler isolamento de falha): sem proteção parcial por bloco — virar issue
- **M4** (drift SSR vs JS): requer decisão de produto (eliminar SSR ou renderizador único)
- **M5** (POST cookie-only + CSRF): confirmar se `ValidacaoConteudoMiddleware` já bloqueia antes de classificar como blocker

---

## Arquivos criados

### Validators
- `componentes/Missoes/Logistica/sinais/Dashboard/Validators/FiltroDashboardValidator.php` — valida status (enum), tipo_missao_id (inteiro positivo) e dia_efetivo (formato YYYY-MM-DD); todos opcionais
- `componentes/Missoes/Logistica/sinais/Dashboard/Validators/AcaoMissaoValidator.php` — valida missao_id (>0) e acao (allowlist: pausar/retomar/cancelar/iniciar)
- `componentes/Missoes/Logistica/sinais/Dashboard/Validators/DetalhesMissaoValidator.php` — valida missao_id (>0)

### Handlers (Plataforma/sinais/Api/Handlers/Dashboard/)
- `TiposMissaoHandler.php` — GET /api/v1/dashboard/tipos-missao; sem filtros, chama DashboardLaboratorio::tiposMissao
- `ResumoOperacionalHandler.php` — GET /api/v1/dashboard/resumo; FiltroDashboardValidator + DashboardLaboratorio::resumoOperacional
- `ListarMissoesHandler.php` — GET /api/v1/dashboard/missoes; FiltroDashboardValidator + DashboardLaboratorio::listarMissoes
- `DesempenhoHandler.php` — GET /api/v1/dashboard/desempenho; FiltroDashboardValidator + DashboardLaboratorio::desempenho
- `AlertasHandler.php` — GET /api/v1/dashboard/alertas; devolve alertas[] + total_alertas
- `ExtratoHandler.php` — GET /api/v1/dashboard/extrato; FiltroDashboardValidator + DashboardLaboratorio::extrato
- `VisaoGeralDiaHandler.php` — GET /api/v1/dashboard/visao-geral; FiltroDashboardValidator + DashboardLaboratorio::visaoGeralDia
- `VeiculosStatusHandler.php` — GET /api/v1/dashboard/veiculos; FiltroDashboardValidator + DashboardLaboratorio::veiculosStatus
- `RotasDoDiaHandler.php` — GET /api/v1/dashboard/rotas-dia; FiltroDashboardValidator + DashboardLaboratorio::rotasDoDia
- `FiltrarHandler.php` — GET /api/v1/dashboard/filtrar; agregador de 7 chamadas, devolve JSON puro sem html_kanban/html_veiculos/html_rotas
- `DetalhesMissaoHandler.php` — GET /api/v1/dashboard/missoes/{id}; DetalhesMissaoValidator, 404 quando não encontrada
- `AcaoMissaoHandler.php` — POST /api/v1/dashboard/missoes/{id}/acao; AcaoMissaoValidator, mapeia erro de domínio para 400

### Deprecação
- `componentes/Missoes/Logistica/sinais/DashboardApi/DEPRECATED.md` — marcador AC6.1 com tabela de correspondência para /api/v1/dashboard/*

## Arquivos alterados

- `componentes/Missoes/Plataforma/sinais/Api/Routes.php` — bloco `// Dashboard` com 12 rotas (10 GET + 2 com path param)
- `componentes/Missoes/Plataforma/sinais/Api/Api.php` — 12 `use` com alias `Dash*` + 12 métodos thin `dashboardXxx()` com `skipValidacaoConteudo: true` para GETs
- `componentes/Missoes/Plataforma/sinais/Api/OpenApi/spec.php` — bloco `/dashboard/*` com 12 paths + tag `Dashboard` adicionada ao array `tags`
- `componentes/Missoes/Logistica/sinais/Dashboard/Dashboard.php` — removidos `filtrarDashboard()`, `detalhesMissao()` e branches `case 'filtrar'/'detalhesMissao'` do `start()`; método `renderDashboard` renomeado internamente para `renderShell`; zero concatenação HTML (AC3.1 zero ocorrências de `'<tr`/`'<div`/`'<td`/`'<li`)

## Desvios do techplan

Nenhum desvio. Pontos notáveis de decisão dentro do escopo:

1. **`$_GET` em vez de `$req->query`** — `Request` não expõe `$query`; o padrão existente (ver `ConsultarLoteHandler`) usa `$_GET` diretamente. Seguido o padrão.
2. **`FiltrarHandler` aceita alias `data`** — o antigo `filtrarDashboard` mapeava `data` para `dia_efetivo`; o `FiltroDashboardValidator` aceita ambos os nomes, e o `FiltrarHandler` faz a normalização antes de chamar o validator, mantendo compatibilidade.
3. **`detalhesMissao` na view `html/dashboard.php`** — a ocorrência é um parâmetro `configRota` na view (não no controller); não tocada pois `html/` é responsabilidade do dev frontend.
4. **Luz_MissaoCard / Luz_AlertaItem mantidos** — o techplan diz "deprecar com comentário, sem remover nesta feature". Mantidos com comentário explicativo.

## Comandos de verificação executados

- `php -l Validators/FiltroDashboardValidator.php` → OK
- `php -l Validators/AcaoMissaoValidator.php` → OK
- `php -l Validators/DetalhesMissaoValidator.php` → OK
- `php -l Handlers/Dashboard/TiposMissaoHandler.php` → OK
- `php -l Handlers/Dashboard/ResumoOperacionalHandler.php` → OK
- `php -l Handlers/Dashboard/ListarMissoesHandler.php` → OK
- `php -l Handlers/Dashboard/DesempenhoHandler.php` → OK
- `php -l Handlers/Dashboard/AlertasHandler.php` → OK
- `php -l Handlers/Dashboard/ExtratoHandler.php` → OK
- `php -l Handlers/Dashboard/VisaoGeralDiaHandler.php` → OK
- `php -l Handlers/Dashboard/VeiculosStatusHandler.php` → OK
- `php -l Handlers/Dashboard/RotasDoDiaHandler.php` → OK
- `php -l Handlers/Dashboard/FiltrarHandler.php` → OK
- `php -l Handlers/Dashboard/DetalhesMissaoHandler.php` → OK
- `php -l Handlers/Dashboard/AcaoMissaoHandler.php` → OK
- `php -l Dashboard/Dashboard.php` → OK
- `php -l Api/Api.php` → OK
- `php -l Api/Routes.php` → OK
- `php -l Api/OpenApi/spec.php` → OK
- `grep -E "'<tr|'<div|'<td|'<li" Dashboard.php` → 0 ocorrências (AC3.1)
- `grep -c "dashboard" Routes.php` → 12 rotas (AC4.3 >= 10)

## Próximos passos para o frontend

### Endpoints disponíveis

Todos autenticados via `Authorization: Bearer <jwt>`. Prefixo: `/api/v1/dashboard/`.

| Método | Rota | Payload/Query | Resposta `data` |
|---|---|---|---|
| GET | `/dashboard/resumo` | `?dia_efetivo=YYYY-MM-DD&status=...&tipo_missao_id=N` | `{contador_por_status, total_missoes_ativas, timestamp}` |
| GET | `/dashboard/missoes` | mesmos filtros | array de missões (estrutura atual do kanban) |
| GET | `/dashboard/desempenho` | mesmos filtros | array de ranking de operadores |
| GET | `/dashboard/alertas` | mesmos filtros | `{alertas: [], total_alertas: N}` |
| GET | `/dashboard/tipos-missao` | — | array de `{id, codigo, descricao}` |
| GET | `/dashboard/extrato` | mesmos filtros | array de `{id, missao_id, acao, horario, data_completa}` |
| GET | `/dashboard/visao-geral` | mesmos filtros | `{missoes, rotas, veiculos}` com contadores |
| GET | `/dashboard/veiculos` | mesmos filtros | array de veículos com ocupação |
| GET | `/dashboard/rotas-dia` | mesmos filtros | array de rotas com `progresso_passos` |
| GET | `/dashboard/filtrar` | mesmos filtros | todos os blocos acima em um objeto (sem html_kanban/html_veiculos/html_rotas) |
| GET | `/dashboard/missoes/{id}` | id na URL | objeto de missão individual |
| POST | `/dashboard/missoes/{id}/acao` | `{"acao": "pausar"}` no body JSON | `{missao_id, nova_acao, message}` |

### Erros padrão

- `422` com `{"error": {"code": "invalid_input", "message": "..."}}` para validação
- `404` com `{"error": {"code": "not_found", "message": "..."}}` para missão não encontrada
- `400` com `{"error": {"code": "operation_failed", "message": "..."}}` para erro de domínio na ação

### O que o frontend precisa fazer

1. **`dashboard.js`**: substituir cada `sendToGalaxia` por `fetch('/api/v1/dashboard/...', { headers: { Authorization: 'Bearer ' + jwt } })`.
2. **Filtrar**: substituir chamada ao canal legado por `GET /api/v1/dashboard/filtrar?dia_efetivo=...&status=...&tipo_missao_id=...`.
3. **Ação em missão**: `POST /api/v1/dashboard/missoes/{id}/acao` com body `{"acao": "pausar"}`.
4. **Detalhes**: `GET /api/v1/dashboard/missoes/{id}`.
5. **Render do kanban/veículos/rotas**: mover lógica de templates PHP (que estava em `filtrarDashboard`) para template strings JS. As cores de status do veículo (`disponivel→#94a3b8`, `alocado→#fbbf24`, `em_rota→#34d399`) estavam em `Dashboard.php` linhas 202-213 (agora removidas do controller — portar para constantes JS).
6. **Remover hidden divs legados** em `html/dashboard.php` (linhas 716-721: `acao-filtrar`, `acao-missao`, `acao-detalhes` com `configRota`).
7. **Tratamento de erros**: 401→redirecionar para login, 403→toast "sem permissão", 4xx→toast com `error.message`, 5xx→toast genérico + `console.error`.
