# Dashboard Logístico — Auto-instalação de Cron e Feedback de Primeiro Acesso

## Objetivo e Escopo

Eliminar a dependência de configuração manual no provisionamento de novos clientes do **Dashboard Expedição** (`componentes/Missoes/Dashboards/sinais/Expedicao/`). Hoje, ao liberar um cliente, alguém precisa lembrar de cadastrar `grupos_regioes` para o block e aguardar a próxima execução do cron diário (`crons/dashboard_expedicao_diario.php`) — caso contrário o dashboard mostra todos os números como `--`.

Este PRD entrega:

1. **Auto-instalação no primeiro acesso (Task 4)** — quando o cliente abre o Dashboard Expedição pela primeira vez (ou após nunca ter sido configurado), o sistema cria um grupo "Padrão" cobrindo todas as `regioes_administrativas` do block e roda a primeira passagem do cache em background. Zero intervenção manual.
2. **Feedback visual de primeiro acesso (Task 5)** — enquanto o setup roda, o cliente vê uma tela de transição com a mensagem **"Estamos configurando os dados de acesso"**, com polling automático e recarga ao concluir.

**Fora de escopo:**
- Worker formal / fila persistente (Redis, RabbitMQ). O cron CLI já é idempotente — `exec(... &)` é suficiente.
- Re-setup automático quando o cliente altera `regioes_administrativas` depois do primeiro acesso.
- Outros dashboards além de Expedição (este PRD é específico do Sinal `Dashboards/Expedicao`).
- Modificar a infraestrutura do crontab do SO. O cron diário global continua funcionando como hoje em paralelo.

## Requisitos Funcionais

### RF1 — Estado de setup persistente por block
O sistema deve manter um registro explícito do estado de setup do Dashboard Expedição para cada `block` em uma tabela nova `dashboard_expedicao_setup`, com 4 estados possíveis:

- `aguardando` — linha recém-criada, exec ainda não disparado
- `em_progresso` — cron rodando em background
- `pronto` — setup concluído com sucesso, dashboard pode renderizar normal
- `falhou` — última tentativa falhou; UI mostra erro + botão "Tentar novamente"

**Por que estado explícito** (e não inferir do cache vazio): cache vazio é ambíguo — pode significar "cliente sem operação no período" (legítimo) ou "nunca rodou" (precisa setup). A flag remove a ambiguidade.

### RF2 — Detecção e disparo no primeiro acesso
Em `Expedicao::start`, antes do fluxo atual:

- Ler estado do block via `AutoSetupExpedicaoLaboratorio::verificar`.
- Se `pronto` → seguir fluxo atual intacto.
- Se `aguardando` → chamar `iniciar`, que faz `INSERT ON CONFLICT DO NOTHING`, transiciona para `em_progresso`, dispara `exec('php crons/dashboard_expedicao_diario.php --block=N --dias=7 --modo-setup &')`. Render `html/configurando.php`.
- Se `em_progresso` → render `html/configurando.php` (não dispara novo exec).
- Se `falhou` → render `html/configurando.php` com botão de retry.

### RF3 — Criação de grupo "Padrão"
Em `executarSync` (chamado de dentro do cron com `--modo-setup`), antes de agregar o cache:

- Verificar se o block já tem alguma linha em `grupos_regioes`.
- Se não tem, executar:
  ```sql
  SELECT id FROM regioes_administrativas WHERE block = :block;
  INSERT INTO grupos_regioes (block, nome, regioes_ids)
    VALUES (:block, 'Padrão', :ids_jsonb);
  ```
- Se o block não tem nenhuma `regioes_administrativas`, ainda criar grupo "Padrão" com `regioes_ids = []`. Cache cai todo no bucket "Outros" — comportamento já documentado no README do Expedicao.

### RF4 — Endpoint de status (polling)
Novo satélite `StatusSetupSatelite` em `Laboratorios/`. Rota:

```
POST /labs/apis/Dashboards/Expedicao/StatusSetupSatelite/verificar
POST /labs/apis/Dashboards/Expedicao/StatusSetupSatelite/iniciar  (retry após falha)
```

Sem parâmetros — o `block` vem sempre da sessão (`SessionGalaxia::get('galaxiaUserInfo')->customerData->id`). Tenant isolation por design.

Resposta:
```json
{
  "status": "aguardando" | "em_progresso" | "pronto" | "falhou",
  "iniciado_em": "2026-05-04T14:32:01-03:00",
  "finalizado_em": null,
  "erro": null,
  "mensagem": "Processando dados das últimas 7 entregas..."
}
```

### RF5 — View de transição (`html/configurando.php`)
Tela centralizada com:

- Spinner Metronic.
- Título: **"Estamos configurando os dados de acesso"**.
- Subtítulo dinâmico que rotaciona a cada 8s entre 3 mensagens ("Aguardando início...", "Processando dados das últimas 7 entregas...", "Quase lá...").
- JS faz polling em `StatusSetupSatelite/verificar` a cada 4s.
- Em `pronto` → `window.location.reload()`.
- Em `falhou` → mostra mensagem de erro + botão "Tentar novamente" que chama `iniciar` e reinicia polling.
- Após 5 minutos sem mudança de status → mensagem "Demorando mais que o esperado" mas continua tentando.

### RF6 — Suporte a `--modo-setup` no cron
`crons/dashboard_expedicao_diario.php` ganha flag aditiva `--modo-setup`. Quando presente:

- Trap top-level `try/catch` + `register_shutdown_function` garantem que qualquer falha (fatal, exceção, kill) atualize a linha para `falhou` com `erro_mensagem`.
- Início: `UPDATE dashboard_expedicao_setup SET status='em_progresso', iniciado_em=now(), pid=getmypid() WHERE block=:block`.
- Antes de agregar: chama `AutoSetupExpedicaoLaboratorio::garantirGrupoPadrao(block)`.
- Sucesso: `UPDATE status='pronto', finalizado_em=now()`.

Sem `--modo-setup`, o cron mantém comportamento atual (descobre blocks via `grupos_regioes`, recalcula últimos 7 dias). Crontab global do SO não muda.

### RF7 — Watchdog de processo travado
Em `verificar`, se `status='em_progresso'` e `iniciado_em < now() - 10 minutes`, reclassificar para `falhou` com `erro_mensagem='Timeout de execução'`. Permite retry sem intervenção.

### RF8 — Concorrência (múltiplas abas)
`iniciar` usa `INSERT ... ON CONFLICT DO NOTHING` na transição `aguardando → em_progresso`. Apenas a primeira aba dispara o `exec`. Demais abas pegam status atual e fazem polling junto.

### RF9 — i18n
Strings novas em `linguagens/{pt-br,en,es}.json`:
- `dashboard.setup.titulo`
- `dashboard.setup.subtitulo_inicial`
- `dashboard.setup.subtitulo_processando`
- `dashboard.setup.subtitulo_finalizando`
- `dashboard.setup.demorando`
- `dashboard.setup.erro`
- `dashboard.setup.botao_retry`

## Considerações Técnicas e Arquitetura (Padrão Galaxia V2)

### Componentes novos
- **Sonda `DashboardExpedicaoSetupSonda`** (`Sondas/`) — model V2 da tabela `dashboard_expedicao_setup`. Colunas `block_*` obrigatórias sem `DEFAULT 1` (framework injeta).
- **Migration `sql/dashboard_expedicao_setup.sql`** — cria tabela com PK em `block`.
- **Laboratório `AutoSetupExpedicaoLaboratorio`** (`Laboratorios/`) — orquestra estado e disparo. Métodos: `verificar`, `iniciar`, `executarSync`, `garantirGrupoPadrao`.
- **Satélite `StatusSetupSatelite`** (`Laboratorios/`, padrão V2 satélite/validator). Endpoint para o polling do front. Block sempre da sessão.
- **View `html/configurando.php`** + JS pareado.

### Componentes alterados
- **`Expedicao::start`** — branch no início que decide entre `html/configurando` e fluxo atual.
- **`crons/dashboard_expedicao_diario.php`** — flag aditiva `--modo-setup` + handlers de erro top-level.
- **`linguagens/{pt-br,en,es}.json`** — 7 strings novas.

### Background runner
`exec('php ' . PROJECT_ROOT . '/crons/dashboard_expedicao_diario.php --block=N --dias=7 --modo-setup > /var/log/dashboard_expedicao_setup_<block>.log 2>&1 &')`.

A idempotência do cron (DELETE + INSERT em transação) torna o `exec(...&)` seguro mesmo em caso de duplicação rara.

### Multi-tenant / isolamento
- `dashboard_expedicao_setup.block` é PK; toda query filtra por block.
- `StatusSetupSatelite` ignora qualquer `block` em payload — sempre lê da sessão.
- `executarSync` recebe block como parâmetro vindo só do CLI (não da web).

## Critérios de Aceite

- [ ] Ao acessar `/missao/dashboards/Expedicao` pela primeira vez (sem linha em `dashboard_expedicao_setup`), o cliente vê a tela "Estamos configurando os dados de acesso" em vez do dashboard com `--`.
- [ ] O setup em background cria a linha em `dashboard_expedicao_setup` e cria 1 grupo "Padrão" em `grupos_regioes` se o block não tinha nenhum.
- [ ] Quando o setup conclui, o frontend recarrega automaticamente e mostra o dashboard com dados reais (sem intervenção do usuário).
- [ ] Acessar 2 abas simultaneamente em block sem setup → apenas 1 processo de cron é disparado; ambas as abas convergem para o dashboard pronto.
- [ ] Se o cron crasha durante o setup, a linha vira `falhou` com `erro_mensagem` preenchido e a UI mostra botão "Tentar novamente" funcional.
- [ ] Linha presa em `em_progresso` há mais de 10min é reclassificada para `falhou` automaticamente na próxima leitura.
- [ ] Cron diário global (`crons/dashboard_expedicao_diario.php` sem flags) mantém comportamento atual idêntico — comportamento existente não regride.
- [ ] Cron com `--modo-setup --block=N` é idempotente: rodar 3× seguidas não duplica grupo "Padrão" nem sobrescreve cache de forma destrutiva.
- [ ] Cliente em block A acessando o dashboard nunca vê estado de setup nem dados de block B.
- [ ] As 7 strings de i18n estão presentes nos 3 idiomas (`pt-br`, `en`, `es`).
- [ ] Suite de testes integration passa em `dev_gx_goldie` cobrindo: estados do `verificar`, `iniciar` idempotente (race), `executarSync` cria grupo Padrão (e não duplica), watchdog de 10min, multi-tenant isolation, transições de status no cron `--modo-setup`.

## Diagrama de fluxo (primeiro acesso, caminho feliz)

```mermaid
sequenceDiagram
    participant U as Cliente (browser)
    participant E as Expedicao::start
    participant L as AutoSetupLaboratorio
    participant DB as dashboard_expedicao_setup
    participant C as Cron (background)
    participant S as StatusSetupSatelite

    U->>E: GET /missao/dashboards/Expedicao
    E->>L: verificar(block)
    L->>DB: SELECT status WHERE block=N
    DB-->>L: (vazio)
    L-->>E: status=aguardando
    E->>L: iniciar(block)
    L->>DB: INSERT ON CONFLICT DO NOTHING (status=em_progresso)
    L->>C: exec(... --modo-setup &)
    L-->>E: status=em_progresso
    E-->>U: render html/configurando.php

    par Background
        C->>L: garantirGrupoPadrao(block)
        L->>DB: INSERT INTO grupos_regioes (Padrão)
        C->>C: AtualizarCacheExpedicaoLaboratorio
        C->>DB: UPDATE status=pronto, finalizado_em=now()
    and Polling
        loop a cada 4s
            U->>S: POST .../verificar
            S->>L: verificar(block)
            L-->>S: status=em_progresso
            S-->>U: { status: em_progresso }
        end
        U->>S: POST .../verificar
        S-->>U: { status: pronto }
        U->>U: window.location.reload()
        U->>E: GET (de novo)
        E-->>U: dashboard com dados
    end
```
