# Dashboard Logístico — Plano técnico

## Resumo de decisão arquitetural

A feature **estende o Sinal existente** `componentes/Missoes/Dashboards/sinais/Expedicao/` — não cria sinal novo. O escopo é ortogonal ao cache (que continua intacto): adicionamos uma camada de "estado de provisionamento por block" com 1 Sonda nova, 1 Laboratório orquestrador, 1 Satélite de polling e 1 view de transição. O cron diário existente (`crons/dashboard_expedicao_diario.php`) ganha **uma flag aditiva** (`--modo-setup`) — sem fork de cron novo, sem worker, sem fila. O padrão V2 dominante é **Sonda + Laboratório + Satélite + Validator**, mesmo modelo do `componentes/Missoes/Mensageria/sinais/Otp/`. Nenhum Adapter/Registry é necessário: só existe um fluxo, um canal de execução (CLI), um destinatário (o próprio block).

## Sinal de referência

- **Imitar**: `componentes/Missoes/Mensageria/sinais/Otp/` — estrutura `Sondas/ + Laboratorios/ + Satelites/ + Validators/ + Migrations/NNN_*.sql + Testes/`. Cada Laboratório recebe `array $entrada`, monta `Message`, retorna `response`. Satélite faz `match($opcao)` e despacha — exatamente o que precisamos para `verificar` / `iniciar`.
- **Imitar parcialmente**: `componentes/Missoes/Dashboards/sinais/Expedicao/Laboratorios/AtualizarCacheExpedicaoLaboratorio.php` — já é chamado pelo cron com `(object)['routeId'=>'cron_'.$block]`; o novo Laboratório de orquestração reutiliza essa mecânica de "executar a partir do CLI sem GalaxiaRoute completo".
- **Imitar**: `componentes/Missoes/Logistica/sinais/DashboardApi/Laboratorios/DashboardApiSatelite.php` — confirma que satélite roteado via `POST /labs/apis/<Constelacao>/<Sinal>/<NomeSatelite>/<opcao>` é padrão estabelecido.
- **Não imitar**: `componentes/Constelacoes/Designacoes/`, `componentes/Constelacoes/Pessoas/` (legado V1, `ModelInteligencia`, sem `Migrations/NNN_`). Também não copiar `componentes/Missoes/Logistica/sinais/Edicaorota/Callbacks/Registry/` — não há multi-consumer aqui, Registry seria over-engineering.

## Estrutura proposta

### Novos arquivos

```
componentes/Missoes/Dashboards/sinais/Expedicao/
├── Migrations/
│   └── 001_dashboard_expedicao_setup.sql              # tabela nova
├── Sondas/
│   └── DashboardExpedicaoSetupSonda.php               # model V2
├── Enums/
│   └── DashboardExpedicaoSetupStatus.php              # aguardando|em_progresso|pronto|falhou
├── Laboratorios/
│   ├── AutoSetupExpedicaoLaboratorio.php              # orquestrador (verificar/iniciar/executarSync/garantirGrupoPadrao)
│   └── (existente) AtualizarCacheExpedicaoLaboratorio.php
├── Satelites/
│   └── StatusSetupSatelite.php                        # despacha verificar/iniciar para o front
├── Validators/
│   └── (nenhum — verificar/iniciar não recebem payload do front)
├── html/
│   ├── (existente) estrutura.php
│   ├── configurando.php                               # view de transição
│   └── configurando.js                                # polling 4s + retry
└── Testes/Laboratorios/
    └── AutoSetupExpedicaoTddTest.php
```

### Arquivos alterados

- `componentes/Missoes/Dashboards/sinais/Expedicao/Expedicao.php` — `start()` ganha branch no topo: chama `AutoSetupExpedicaoLaboratorio::verificar`, decide entre `html/configurando` e fluxo atual.
- `crons/dashboard_expedicao_diario.php` — parser de argv ganha `--modo-setup`; adiciona `register_shutdown_function` + `try/catch` top-level que escreve `falhou` na Sonda; quando `--modo-setup` está presente e `--block=N` está presente, chama `AutoSetupExpedicaoLaboratorio::executarSync($block)` antes do loop de cache; ao final transiciona `pronto`. Comportamento sem flag não muda.
- `linguagens/pt-br.json`, `linguagens/en.json`, `linguagens/es.json` — 7 chaves novas (RF9).

## Sondas e schema

### `dashboard_expedicao_setup` (Sonda: `DashboardExpedicaoSetupSonda`)

```sql
-- Migrations/001_dashboard_expedicao_setup.sql
BEGIN;

CREATE TABLE IF NOT EXISTS dashboard_expedicao_setup (
    id              SERIAL PRIMARY KEY,
    status          VARCHAR(20) NOT NULL DEFAULT 'aguardando',
                    -- aguardando | em_progresso | pronto | falhou
    iniciado_em     TIMESTAMP WITHOUT TIME ZONE,
    finalizado_em   TIMESTAMP WITHOUT TIME ZONE,
    pid             INTEGER,
    erro_mensagem   TEXT,
    tentativas      INTEGER NOT NULL DEFAULT 0,

    -- Block columns obrigatórias (sem DEFAULT — framework injeta via customerControl/companyControl)
    block                  INTEGER NOT NULL,
    block_empresa_id       INTEGER,
    block_filial_id        INTEGER,
    block_usuario_id       INTEGER,
    block_setor_id         INTEGER,
    block_quem             VARCHAR(255),
    block_data             TIMESTAMP WITHOUT TIME ZONE,
    data_cadastro          TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
    data_atualizacao       TIMESTAMP WITHOUT TIME ZONE,
    usuario_cadastro       VARCHAR(255),
    usuario_atualizacao    VARCHAR(255),

    CONSTRAINT dashboard_expedicao_setup_block_unq UNIQUE (block)
);

CREATE INDEX IF NOT EXISTS dashboard_expedicao_setup_status_idx
    ON dashboard_expedicao_setup (status, iniciado_em);

COMMIT;
```

**Notas de schema**:
- `UNIQUE(block)` é o que viabiliza o `INSERT ... ON CONFLICT DO NOTHING` do RF8 (concorrência multi-aba).
- A spec menciona "PK em block"; mantemos `id SERIAL PK + UNIQUE(block)` para alinhar com o padrão de todas as outras Sondas do projeto (todas têm `id SERIAL PK`). Funcionalmente idêntico para o `ON CONFLICT`.
- Granularidade: 1 linha por block. Não é histórico — o mesmo registro é mutado ao longo das transições.

**`$propeties` da Sonda**:
```php
'customerControl' => true,   // cliente acessa o próprio setup
'db'              => 'gx_goldie',
'companyControl'  => true,
'userControl'     => false,  // setup é por block, não por usuário
```
Justificativa: `customerControl=true` porque a feature inteira é multi-tenant por block (RF garante "cliente em block A nunca vê B"). `userControl=false` porque qualquer usuário do block disparando o setup compartilha o mesmo registro — não é por sessão de usuário.

## Endpoints / fluxo

| Método | Rota | Handler | Laboratório chamado | Auth |
|---|---|---|---|---|
| GET  | `/missao/dashboards/Expedicao` | `Expedicao::start` | `AutoSetupExpedicaoLaboratorio::verificar` (+ `iniciar` se `aguardando`) | Sessão Galaxia |
| POST | `/labs/apis/Dashboards/Expedicao/StatusSetupSatelite/verificar` | `StatusSetupSatelite('verificar')` | `AutoSetupExpedicaoLaboratorio::verificar` | Sessão Galaxia |
| POST | `/labs/apis/Dashboards/Expedicao/StatusSetupSatelite/iniciar`   | `StatusSetupSatelite('iniciar')`   | `AutoSetupExpedicaoLaboratorio::iniciar`   | Sessão Galaxia |
| CLI  | `php crons/dashboard_expedicao_diario.php --block=N --dias=7 --modo-setup` | (cron) | `AutoSetupExpedicaoLaboratorio::executarSync` → `garantirGrupoPadrao` → `AtualizarCacheExpedicaoLaboratorio` | CLI-only (`php_sapi_name() === 'cli'`) |

**Contrato do Satélite** (RF4) — sem parâmetros do payload; `block` lido **somente** de `SessionGalaxia::get('galaxiaUserInfo')->customerData->id`. Qualquer `block` em `$data` é ignorado (tenant safety).

Resposta JSON do `verificar` segue exatamente o RF4 do spec.

## API interna do `AutoSetupExpedicaoLaboratorio`

| Método | Caller | Recebe | Faz |
|---|---|---|---|
| `verificar(int $block): array` | `Expedicao::start`, `StatusSetupSatelite` | block da sessão | SELECT na Sonda; aplica watchdog (RF7: `em_progresso` + `iniciado_em < now()-10min` → muta para `falhou`); se nada existir, retorna `aguardando` (sem inserir). Resposta no formato RF4. |
| `iniciar(int $block): array` | `Expedicao::start`, `StatusSetupSatelite` | block da sessão | `INSERT ... ON CONFLICT (block) DO NOTHING` com `status='em_progresso'`; se já estava `em_progresso` válido, retorna estado atual sem disparar; caso contrário UPDATE para `em_progresso` + `pid=null` (set pelo cron) e `exec(php ... --block=N --dias=7 --modo-setup > /var/log/... 2>&1 &)`. |
| `executarSync(int $block): array` | cron `--modo-setup` | block via CLI | UPDATE `status='em_progresso', pid=getmypid(), iniciado_em=now()`; chama `garantirGrupoPadrao`; retorna estado p/ o cron prosseguir. Em caso de erro, exception sobe — handler top-level do cron marca `falhou`. |
| `garantirGrupoPadrao(int $block): void` | `executarSync` | block | RF3: SELECT em `grupos_regioes` filtrado por block; se vazio, SELECT ids de `regioes_administrativas WHERE block=N`; INSERT 1 linha em `grupos_regioes` (`nome='Padrão'`, `regioes_ids` = jsonb dos ids ou `[]`). Idempotente. |

## Padrões de extensibilidade

**N/A.** Não há Registry, Adapter ou Interface aqui:
- Não há múltiplos "tipos de setup" — só um (Expedicao).
- Não há múltiplos canais de execução — só CLI via `exec`.
- Não há plugin de terceiros chamando este sinal.

Introduzir Registry/Adapter agora seria over-engineering. Quando vier um `Dashboards/Recebimento` com a mesma necessidade, refatoramos extraindo uma `AbstractAutoSetupLaboratorio` ou subindo para `componentes/Missoes/Dashboards/sinais/Comum/`.

## Ordem de implementação (sequencial)

1. **Migration** `Migrations/001_dashboard_expedicao_setup.sql` → roda manualmente em `dev_gx_goldie`; valida com `php tests/integration/dashboard_expedicao_setup_migration_test.php` (smoke `INFORMATION_SCHEMA`).
2. **Enum** `DashboardExpedicaoSetupStatus` (constantes string).
3. **Sonda** `DashboardExpedicaoSetupSonda` (clone de `DashboardExpedicaoCacheSonda`, ajustar `$entity`, `$required`, `$propeties`).
4. **Laboratório** `AutoSetupExpedicaoLaboratorio` na ordem dos métodos: `verificar` → `garantirGrupoPadrao` → `executarSync` → `iniciar` (este último depende de tudo já estar pronto).
5. **Cron** alterado: parser `--modo-setup`, `try/catch` + `register_shutdown_function`, branch que invoca `executarSync` antes do loop, transição `pronto` ao final. Validar com `php crons/dashboard_expedicao_diario.php` (sem flags) que comportamento legacy não muda.
6. **Satélite** `StatusSetupSatelite` (despacha `verificar`/`iniciar` lendo block da sessão).
7. **`Expedicao::start`** — branch no início (`switch (status)`).
8. **Views**: `html/configurando.php` + `html/configurando.js` (polling 4s, recarrega em `pronto`, retry em `falhou`, mensagem de "demorando" após 5min).
9. **i18n** — 7 chaves nos 3 arquivos.
10. **Testes TDD** em `Testes/Laboratorios/AutoSetupExpedicaoTddTest.php`.

## Riscos e decisões não-óbvias

1. **Race condition entre `iniciar` e o cron começando** — se `iniciar` faz INSERT e dispara `exec` quase simultaneamente, o cron pode começar a rodar antes de o INSERT efetivar (`exec(&)` é assíncrono mas rápido). **Mitigação**: o `INSERT ON CONFLICT DO NOTHING` é commitado em transação curta antes do `exec`; o cron `executarSync` faz `UPDATE ... WHERE block=:block` que será no-op se a linha não existir e retornará 0 rows — nesse caso o cron aborta com `falhou`. Adicional: o cron começa com `sleep(0)` e lê o estado; se não achar linha, faz `INSERT` defensivo (também `ON CONFLICT DO NOTHING`). Idempotência total.

2. **`exec(... &)` em ambiente sem permissão de fork (PHP-FPM com restrições)** — se `disable_functions` bloquear `exec`, o `iniciar` falha silenciosamente e o usuário fica preso em `em_progresso` até o watchdog. **Mitigação**: `iniciar` valida `function_exists('exec')` antes; se ausente, retorna `falhou` com `erro_mensagem='exec_indisponivel'` para o front mostrar ação clara. Documentar requisito de `exec` no README do sinal.

3. **`grupos_regioes.regioes_ids` formato** — a spec assume JSONB. Antes de implementar `garantirGrupoPadrao`, **confirmar via `\d grupos_regioes` em `dev_gx_goldie`** se a coluna é `jsonb`, `text` ou `int[]`. Se for `text`, ajustar para `json_encode($ids)`. Esse é o único ponto da migration/laboratório que depende de schema externo ao Sinal.

## Breaking changes

**Nenhum.** Todas as mudanças são aditivas:
- Tabela nova (não toca em `dashboard_expedicao_cache` nem `grupos_regioes` exceto INSERT).
- Cron mantém semântica padrão sem `--modo-setup`.
- `Expedicao::start` só adiciona um branch antecedente — caminho atual é executado quando `status='pronto'` (eventual estado de todo block já configurado).
- **Atenção de compatibilidade**: blocks **já em produção** com `grupos_regioes` cadastrados precisam de uma linha `dashboard_expedicao_setup (block, status='pronto')` para não cair na tela de transição no próximo acesso. **Mitigação**: a migration faz `INSERT ... SELECT DISTINCT block, 'pronto' FROM grupos_regioes` no final, marcando todos os blocks pré-existentes como já configurados.

## Plano de teste

**Unit (TDD em `Testes/Laboratorios/AutoSetupExpedicaoTddTest.php`)**:
- `verificar` retorna `aguardando` quando não há linha.
- `verificar` aplica watchdog: linha `em_progresso` com `iniciado_em` há 11min vira `falhou`.
- `iniciar` é idempotente: 2 chamadas concorrentes resultam em 1 só `INSERT`/`exec` (mock de `exec`).
- `garantirGrupoPadrao` cria 1 grupo "Padrão" na primeira call, no-op na segunda.
- `garantirGrupoPadrao` com block sem `regioes_administrativas` cria grupo com `regioes_ids=[]`.

**Integration (em `dev_gx_goldie`)**:
- Fluxo end-to-end: limpar `dashboard_expedicao_setup` + `grupos_regioes` para um block teste; chamar cron `--modo-setup --block=N`; validar grupo criado e linha `pronto`.
- Cron `--modo-setup` rodado 3× seguidas: 1 grupo "Padrão" único, cache não-destrutivo.
- Cron sem flags (regressão): comportamento idêntico ao atual.
- Multi-tenant: setup do block A não cria/altera nada no block B.
- Crash forçado (`kill -9` no PID do cron mid-flight) → próxima chamada de `verificar` reporta `falhou`/`em_progresso` conforme tempo decorrido.

**Manual (smoke do humano)**:
1. Criar block novo sem `grupos_regioes` em `dev_gx_goldie`.
2. Logar como user desse block, abrir `/missao/dashboards/Expedicao`.
3. Ver tela "Estamos configurando os dados de acesso" com spinner e mensagens rotacionando.
4. Esperar ~30s — página deve recarregar sozinha mostrando dashboard com dados.
5. Abrir 2 abas em paralelo no início — só uma execução de cron deve aparecer em `ps aux | grep dashboard_expedicao_diario`.

## Out of scope (confirmando spec)

- Worker formal / fila persistente (Redis, RabbitMQ).
- Re-setup automático quando o cliente altera `regioes_administrativas` depois.
- Outros dashboards (Recebimento, Coleta, etc.) — só Expedicao.
- Modificar o crontab do SO. Cron diário global continua igual.
- **Acréscimos técnicos out-of-scope** explicitados aqui:
  - UI de admin para resetar `dashboard_expedicao_setup` manualmente (DBA roda SQL direto se precisar).
  - Métricas/observabilidade (Prometheus, etc.) — log em `/var/log/dashboard_expedicao_setup_<block>.log` é suficiente.
  - Notificação ao usuário por e-mail/push quando o setup terminar — polling do front cobre.
  - Locking pessimista no Postgres (`pg_advisory_lock`) — `UNIQUE(block)` + `ON CONFLICT` resolve sem essa complexidade.
