# Escalar futuramente — saindo do polling

Hoje a central do gestor (`Logistica/sinais/Central/html/central.php`) usa **polling** pra detectar mudanças nas edições de rota:

- Tick a cada 10s (sobe pra 20s em backoff quando nada muda em 3 ticks).
- Aba escondida pausa o tick (Page Visibility).
- Cada tick é 1 request batched: `GET /api/v1/edicao-rota?ids=1,2,3`.

Isso resolve **bem** até a faixa de **dezenas de gestores × dezenas de edições simultâneas**. Quando o tráfego ou a latência percebida começar a doer, troque por algo *push-based*. Este doc descreve as duas opções (SSE e WebSocket) com os detalhes de implementação que importam neste código.

## Quando trocar

Sinais concretos pra começar a planejar a migração:

| Sintoma | Threshold sugerido |
|---|---|
| RPS no endpoint de polling | > 50 req/s sustentados |
| Latência percebida do gestor pra ver mudança | usuário reclamando dos 10s |
| Carga PHP-FPM atribuível ao polling | > 15% do tempo total dos workers |
| Edições ativas simultâneas (todos os gestores) | > ~200 |

Antes desses pontos, polling com batch + visibility + backoff é mais barato de manter.

## SSE (Server-Sent Events) — recomendação primária

Caso de uso aqui é **unidirecional** (servidor → cliente, gestor só recebe atualizações). SSE encaixa perfeito e é mais simples que WebSocket.

### Lado cliente

Trocar o `setInterval` por `EventSource`:

```js
const es = new EventSource('/api/v1/edicao-rota/stream');
es.addEventListener('edicao_atualizada', (ev) => {
    const ed = JSON.parse(ev.data);
    const rotaId = encontrarRotaIdPorEdicaoId(ed.id);
    if (!rotaId) return;
    Object.assign(edicoesPorRota[rotaId], {
        status: ed.status, expirado: ed.expirado, expira_em: ed.expira_em,
        proposta_em_andamento: ed.rota_em_andamento, rota_proposta: ed.rota_proposta,
    });
    atualizarIconeStatus(rotaId);
});
es.addEventListener('keepalive', () => {});  // heartbeat, ignora
es.onerror = () => { /* navegador faz reconnect automático */ };
```

`EventSource` tem reconnect nativo. Sem fallback necessário em browsers modernos.

### Lado servidor — o problema do PHP-FPM

PHP-FPM é o gargalo: cada conexão SSE prende um worker enquanto estiver aberta. 50 gestores online = 50 workers parados em `sleep`/`stream_select`. Não escala.

Três caminhos viáveis, em ordem de complexidade:

**1. Servidor PHP de longa-vida — Swoole/RoadRunner/ReactPHP/Workerman**

- Sobe um processo separado escutando numa porta (ex: 9501).
- nginx faz `proxy_pass` de `/api/v1/edicao-rota/stream` pra ele, **fora do FPM**.
- Loop de eventos: pra cada conexão, segura aberta e empurra eventos quando recebe sinais (Redis pub/sub, fila, ou polling do banco — *aí o polling vira só do servidor pro DB, 1 query independente do número de clientes*).

Custo: novo processo no deploy, supervisor (systemd/supervisord) pra mantê-lo vivo, monitoramento separado. Vale quando a aplicação começa a fazer outras coisas que pedem long-lived (chat, notificações em tempo real, etc.).

**2. Node/Go sidecar só pro stream**

- Microserviço dedicado em Node ou Go, escutando Redis pub/sub.
- O PHP de escrita (handlers de aprovar/rejeitar/submeter) faz `redis->publish('edicao_rota:42', json_encode([...]))` depois do INSERT/UPDATE.
- O sidecar faz fan-out pra clients conectados.
- nginx roteia `/api/v1/edicao-rota/stream` pro sidecar.

Custo similar ao 1, mas em outra stack. Bom se o time já tem afinidade com Node/Go.

**3. SSE direto via PHP-FPM com `fastcgi_finish_request` + tail no banco** *(NÃO RECOMENDO)*

Tecnicamente possível, mas presta atenção: ainda prende worker. Só funciona em volume baixíssimo (5-10 conexões), e nesse caso polling já basta.

### Schema de eventos

Sugestão de payload (envelope SSE):

```
event: edicao_atualizada
id: <edicao_id>:<updated_at_micros>
data: {"id":42,"status":"pendente_aprovacao","expira_em":"...","rota_em_andamento":[...],"rota_proposta":[...]}

event: keepalive
data: {}
```

`id` permite o `EventSource` mandar `Last-Event-ID` no reconnect — o servidor então pode replayar eventos perdidos (precisa de buffer/log de eventos).

### Auth

Headers customizados não funcionam em `EventSource`. Soluções:

- **Cookie de sessão** (mais simples): `EventSource('/api/v1/edicao-rota/stream', { withCredentials: true })`. Hoje a API REST aceita auth por cookie/sessão; manter.
- **Token via query string**: `?access_token=...`. Funciona mas vaza em logs do nginx/proxy. Aceitável em curto-prazo.

### Filtro de escopo

Gestor não deve receber eventos de empresas/clientes que não são dele. Sem `customerControl`/`companyControl` na sonda (desligamos antes), o filtro vira responsabilidade do **stream service**:

- Lê `customer_id`/`company_id` da sessão na hora do connect.
- Se mensagens vêm via Redis pub/sub, assinar canal `edicao_rota:customer_<id>:company_<id>` em vez de canal global.
- Publisher (handler PHP) constrói o canal com base nas colunas `block`/`block_empresa_id` da edição.

## WebSocket — overkill aqui

WebSocket faz sentido quando há **comunicação bidirecional**. Casos:

- Motorista marcando posição em tempo real (gestor vê arrastando no mapa).
- Chat entre gestor e motorista durante a edição.
- Edição colaborativa multi-gestor.

**Pra simples "gestor recebe atualização de status", SSE é suficiente e mais barato.** Sai mais caro em:

- Complexidade do servidor (ping/pong, frames, close handshake).
- Cliente: precisa de wrapper em volta de `WebSocket` pra reconnect (não tem nativo).
- Auth no upgrade (mesmo dilema do SSE — cookie ou query).
- Proxies/firewalls corporativos: SSE = HTTP normal, WS pode ser bloqueado por proxies que não fazem upgrade.

Se o projeto crescer pra ter os casos bidirecionais acima, aí sim. Stack natural seria a mesma do SSE (Swoole/RoadRunner/Node sidecar) só que com biblioteca de WS no lugar do streaming HTTP.

## Migração incremental

Plano sugerido pra não fazer big-bang:

1. **Manter polling com batch + visibility + backoff** *(estado atual)*.
2. Quando dor surgir, instalar Redis (se já não tiver) e expor pub/sub interno.
3. Subir o stream service (Swoole ou Node sidecar) num endpoint novo `/api/v1/edicao-rota/stream`.
4. **Feature flag no front**: se `EventSource` conectar e receber `keepalive` em < 5s, desliga o polling. Senão, fallback pra polling. Convive os dois.
5. Depois de uma semana estável com 100% dos gestores no SSE, remover o polling do front.
6. O endpoint REST `GET /edicao-rota?ids=...` pode continuar existindo (útil pra apps mobile, jobs, debug).

## Checklist mínimo antes de virar pra SSE

- [ ] Redis (ou outra fila) disponível e parte do deploy.
- [ ] Processo dedicado no deploy (systemd unit ou container).
- [ ] Métricas: nº de conexões abertas, taxa de eventos publicados, latência publish→deliver.
- [ ] Estratégia de filtro por `customer_id`/`company_id` no canal.
- [ ] Auth: definida (cookie ou query token).
- [ ] Reconnect testado: derrubar o stream service e ver se `EventSource` retoma sozinho.
