# WebSocket no Genesis — Arquitetura Completa

> Extração técnica total do mecanismo de realtime do framework. Para receitas práticas, veja [`01-usabilidade.md`](01-usabilidade.md).

## Índice

1. [Visão geral e fluxo](#1-visão-geral-e-fluxo)
2. [Camada de transporte (Scaledrone)](#2-camada-de-transporte-scaledrone)
3. [Backend — publicação](#3-backend--publicação)
4. [Frontend — assinatura e despacho](#4-frontend--assinatura-e-despacho)
5. [Vocabulário de ações (RequestGalaxiaAct)](#vocabulário-de-ações-requestgalaxiaact)
6. [Consumidores especializados](#5-consumidores-especializados)
7. [Convenções de room](#6-convenções-de-room)
8. [Decisões de design e armadilhas](#7-decisões-de-design-e-armadilhas)
9. [Mapa de arquivos](#8-mapa-de-arquivos)

---

## 1. Visão geral e fluxo

O Genesis usa **Scaledrone** (serviço SaaS de WebSocket) como transporte. A comunicação é **unidirecional na prática**: o servidor publica, os navegadores recebem. O navegador não publica de volta — quando o usuário age, é um request HTTP normal; é o backend que decide publicar.

O diferencial do mecanismo é o **protocolo declarativo**: o payload publicado é uma "ação Galaxia" — um JSON que descreve *o que fazer no DOM*, não dados crus. O mesmo despachante (`RequestGalaxiaAct`) que processa respostas HTTP de formulários também processa mensagens de socket. Socket e AJAX compartilham o mesmo vocabulário.

```
┌─────────────────────────────────────────────────────────────────┐
│ BACKEND (Satélite / Laboratório / Evento)                        │
│   $galaxia->galaxiaResposta([])->chamarFuncao('x', $d)->feito()  │
│                          │                                       │
│                          ▼                                       │
│   $galaxia->enviarSocket('galaxia_logistica_central', $payload)  │
└──────────────────────────┬──────────────────────────────────────┘
                           │ POST REST (Basic auth)
                           ▼
┌─────────────────────────────────────────────────────────────────┐
│ SCALEDRONE  —  channel uzJPCX4DfT380eoN                          │
│   roteia o payload a todos inscritos no room                     │
└──────────────────────────┬──────────────────────────────────────┘
                           │ WebSocket push
                           ▼
┌─────────────────────────────────────────────────────────────────┐
│ FRONTEND                                                          │
│   lerSocket() já renderizou <div class="rooms-socket"            │
│                              data-channel data-room>             │
│   verificarSockets() (func.js, ~500ms após ready) conectou       │
│   message → RequestGalaxiaAct(message.data) → aplica a ação      │
└─────────────────────────────────────────────────────────────────┘
```

**Princípio de isolamento:** o que separa audiências é o **room**, não o canal. Há um único canal global; rooms por-usuário/por-missão (`..._user_42`, `..._missao_123`) entregam só a quem deve receber.

---

## 2. Camada de transporte (Scaledrone)

| Item | Valor |
|---|---|
| Provedor | Scaledrone (`api2.scaledrone.com`) |
| `channel_id` | `uzJPCX4DfT380eoN` |
| `channel_secret` | `YD3NkepLpR9dFS6aupLQDHajEphEf9xs` |
| URL de publish | `https://api2.scaledrone.com/{channel_id}/{room}/publish` |
| Autenticação | `Authorization: Basic base64(channel_id:channel_secret)` |
| Content-Type | `application/json` |
| Timeout (publish) | 5s total / 3s conexão |
| SDK no navegador | `https://cdn.scaledrone.com/scaledrone.min.js` |

O `channel_id` é **hardcoded em três lugares** que precisam permanecer em sincronia: `GalaxiaRoute::enviarSocket()` (publish), `GalaxiaRoute::lerSocket()` (o `data-channel` que o front escuta) e `EdicaoRotaSocketNotifier` (POST direto). Trocar o canal exige atualizar os três.

O SDK é carregado pelo tema `genezes/_theme.php` (linha ~2597), dentro de `if (empty($disableGenAssets))` — ou seja, **incondicionalmente** para qualquer tela que use os assets do tema. Não deve ser reincluído nas views.

---

## 3. Backend — publicação

### 3.1 `GalaxiaRoute::enviarSocket($room, $dados, $debug = false)`

`controladores/_Controladores/GalaxiaRoute.php` (~linha 1436)

Publica `$dados` (serializado como JSON) no `$room`. Retorna `['status' => 'ok']` em sucesso (HTTP < 400) ou `['status' => 'nao']` em falha (json_encode falhou, HTTP ≥ 400, ou erro de transporte). Tenta cURL primeiro; faz fallback para `file_get_contents`. Falhas vão para `error_log` e nunca lançam exceção — publicar é best-effort.

```php
$this->galaxia->enviarSocket('galaxia_logistica_board', [
    'action' => 'route_update',
    'data'   => [ /* ... */ ],
    'timestamp' => time(),
]);
```

### 3.2 `GalaxiaRoute::lerSocket(string $room)`

`controladores/_Controladores/GalaxiaRoute.php` (~linha 1500)

Renderiza o "container de escuta" que o front descobre e conecta:

```php
return '<div class="rooms-socket hidden" data-channel="uzJPCX4DfT380eoN" data-room="' . $room . '"></div>';
```

O `data-channel` **deve** ser o mesmo canal de `enviarSocket()` — senão o front escuta um canal onde o servidor nunca publica.

### 3.3 Builder `galaxiaResposta()` → `GalaxiaRouteRespostas`

`controladores/_Controladores/GalaxiaRoute.php` (`galaxiaResposta()`, ~linha 1523) e `controladores/_Controladores/GalaxiaRouteRespostas.php`

Forma idiomática de montar uma ação Galaxia. Fluent API — todos os métodos retornam `$this`, exceto `feito()` que devolve o array final. **O mesmo payload serve tanto para resposta HTTP quanto para `enviarSocket()`** — daí a unificação com AJAX.

| Método | Chave gerada no payload |
|---|---|
| `chamarFuncao(string $funcao, ?array $dados = [])` | `chamarFuncoes[] = ['funcao'=>…, 'dados'=>…]` |
| `substituirConteudo(string $tag, string $valor)` | `elementsHtml[] = ['tag'=>…, 'html'=>…]` |
| `inserirConteudo(string $tag, string $valor)` | `inserirConteudo[] = ['tag'=>…, 'html'=>…]` |
| `preencherCampo(string $tag, string $valor)` | `preencherCampo[] = ['tag'=>…, 'html'=>…]` |
| `removerElemento(string $valor)` | `removerElemento[] = $valor` |
| `adicionarClassesElemento(string $el, string $classe)` | `adicionarClasseElemento[] = ['tag'=>…, 'classe'=>…]` |
| `removerClassesElemento(string $el, string $classe)` | `removerClassesElemento[] = ['tag'=>…, 'classe'=>…]` |
| `trocarUrl(string $url)` | `trocarUrl = ['url'=>…]` |
| `proximaRota(string $url)` | `proximaRota = ['url'=>…]` |
| `novaModal($dados)` | `novaModal = $dados` |
| `fecharModal()` / `naoFecharModal()` | `closeModal = 1` / `doNotCloseModal = 1` |
| `voltarPagina()` / `atualizarPagina()` | `voltarPagina = 1` / `atualizarPagina = 1` |
| `limparFormulario()` / `limparLocalStorage()` | `limparFormulario = 1` / `limparLocalStorage = true` |
| `atualizarGraficos(?int $v = 0)` | `atualizarGraficos = $v` |
| `pararRequisicao()` | `pararRequisicao = true` |
| `feito()` | **finaliza** e retorna o array |

```php
$payload = $this->galaxia->galaxiaResposta([])
    ->chamarFuncao('atualizarCardLiberacao', ['tipo' => 'ACESSO_PESAGEM', 'status' => 'aprovada'])
    ->feito();
$this->galaxia->enviarSocket("galaxia_logistica_liberacoes_user_{$uid}", $payload);
```

### 3.4 `goToUpdateMissaoSinal($sinal, $sentData, $existedResponse, $option)`

`controladores/_Controladores/GalaxiaRoute.php` (~linha 1060)

Renderiza um Sinal (sub-componente) e embrulha seu HTML + tags de auto-refresh em `galaxiaMissaoUpdate[]`, pronto para `enviarSocket()`. Usado quando se quer **re-renderizar um bloco inteiro server-side** e empurrar para os clientes:

```php
$enviarSocket = $this->galaxia->goToUpdateMissaoSinal('ConteudoListagemDeRomaneios', [], $response);
$this->galaxia->enviarSocket('galaxia_logistica_central', $enviarSocket);
```

### 3.5 `doNotification($response, $option, $message, $title)`

`controladores/_Controladores/GalaxiaRoute.php` (~linha 1528). Monta `data.notifications[]` (toast) dentro de uma resposta.

### 3.6 Notifiers diretos (sem `$galaxia`)

Laboratórios, Strategies e Eventos não recebem `$galaxia`. Dois padrões:

- **`LiberacaoSocketNotifier`** (`sinais/Solicitacoes/Eventos/`): recebe `$galaxiaRoute` por parâmetro e delega a `$galaxiaRoute->enviarSocket()`. **Preferido** — reusa o transporte central. Payload é ação Galaxia (`chamarFuncoes` → `atualizarCardLiberacao`).
- **`EdicaoRotaSocketNotifier`** (`sinais/Edicaorota/Eventos/`): faz **POST direto** ao Scaledrone (replica URL/auth/cURL), porque também é acionado por callback de API REST onde não há `$galaxiaRoute`. Best-effort, log silencioso. Use só quando não há `$galaxiaRoute` algum — duplica a lógica de transporte.

### 3.7 `LogisticaSocketTrait`

`componentes/Constelacoes/Logistica/Traits/LogisticaSocketTrait.php` — encapsula broadcasts recorrentes de rota:
- `broadcastRouteUpdates($rotas, $normalizar)` → publica `{action:'route_update', data, timestamp}` em **board e central**.
- `broadcastMissaoAction($missaoId, $action, $data)` → publica em `galaxia_logistica_missao_{id}`.

---

## 4. Frontend — assinatura e despacho

### 4.1 `verificarSockets()`

`controladores/_Src/func.js` (~linha 651). Chamada por `main.js:14` dentro de `$(document).ready` com `setTimeout(..., 500)`.

Lógica:
1. Se não há `.rooms-socket` no DOM → loga `"nao encontramos sockets"` e retorna.
2. Para **cada** `.rooms-socket`: lê `data-channel` e `data-room`; instancia `new Scaledrone(channel)`; em `open`, faz `drone.subscribe(room)`.
3. No `room.on('message')`, despacha:

```js
if (message && message.data && message.data.array) {
    RequestGalaxiaAct(message.data.array);
} else {
    RequestGalaxiaAct(message.data);
}
```

Pontos importantes:
- **Roda uma única vez** (500ms após ready). Não re-executa em navegação AJAX. Como a maioria das telas é full page load (navegação via `proximaRota` → `location.href`), o `.rooms-socket` está no DOM quando ela roda.
- **Não é idempotente** — chamar de novo abre conexões duplicadas para os mesmos rooms.
- Múltiplos `.rooms-socket` → múltiplas conexões paralelas (uma por room).
- Erros de conexão são logados no console; não derrubam a página.

### 4.2 O formato `message.data`

O que `enviarSocket($room, $payload)` envia chega ao cliente como `message.data` — ou seja, `message.data === $payload`. O ramo `message.data.array` existe para um envelope legado opcional (`{array: <ação>}`); na prática, publicar a ação diretamente (`{chamarFuncoes: […]}`) cai no `else` e funciona.

### Vocabulário de ações (RequestGalaxiaAct)

`controladores/_Src/func.js` (~linha 1927). É o coração do protocolo declarativo. Recebe `data` (a ação) e aplica, na ordem abaixo. **Toda chave é opcional** — um payload pode combinar várias.

| Chave | O que faz |
|---|---|
| `abrirPagina` / `abrirPaginaWindow` | `window.open(url, '_blank')` |
| `galaxiaRotas` | Append de resultados de busca em `#search-results-area` |
| `modalLoad` | Carrega HTML em modal (`modalGalaxiaResponse`); reinicializa charts se houver |
| `limparFiltroDeHistoria` | Limpa `#search` e `#suggestions` |
| `limparLocalStorage` | `localStorage.clear()` |
| `notifications` | Array de toasts via `doNotification(d)` |
| `salvarElementoEmCache` | Salva HTML de elementos no IndexedDB |
| `cardNotifications` | Cards de notificação (Toastify) |
| `galaxiaMissaoUpdate` | Atualiza blocos de missão (criação/edição/remoção via `gxautoacao`) |
| `hideElements` | `$(sel).fadeOut('fast')` para cada seletor |
| `removerElemento` | `$(sel).remove()` |
| `removerClasseElemento` / `removerClassesElemento` | `$(tag).removeClass(classe)` |
| `proximaRota` | `location.href = systemUrl + '/' + url` |
| `trocarUrl` | Atualiza a URL sem reload (`atualizarURL`) + `verificarEditors()` |
| `adicionarClasseElemento` / `adicionarClassesElemento` | `$(tag).addClass(classe)` |
| **`elementsHtml`** | `$(tag).html(html)` — substitui conteúdo |
| `inserirConteudo` | `$(tag).append(html)` |
| `preencherCampo` | `$(tag).val(html)` |
| `copiarElemento` | Clona o último e faz append no destino |
| **`chamarFuncoes`** | `window[funcao](dados)` para cada `{funcao, dados}` — ponto de extensão principal |
| `multiSelectReload` | Reinstancia `[data-multi-select]` |
| `preenchimentoDeNecessidadesDoGestor` | Renderiza sugestões em `#suggestions` |
| `voltarPagina` | `history.back()` se `== 1` |
| `atualizarPagina` | `location.reload()` se `== 1` |
| `closeModal` | Fecha o modal se `== 1` |
| `novaModal` | Abre modal e carrega conteúdo (`openGalaxiaModal`) |
| `limparFormulario` | Limpa inputs/selects/textareas (exceto hidden) |
| `status` | Se `!= 'ok'`, notifica erro e interrompe ações seguintes |
| `nxtact` / `doNotCloseModal` | Controle de fechamento de modal pós-ação |
| `google` | `notify(data.google)` (integração) |

As duas chaves mais usadas para realtime são **`chamarFuncoes`** (delega a uma função JS da tela — máxima flexibilidade) e **`elementsHtml`** (troca de HTML por seletor — sem JS próprio).

---

## 5. Consumidores especializados

Nem todo socket passa pelo `verificarSockets()` genérico. Há dois padrões paralelos, ambos no **mesmo canal**, mas com handlers próprios para lógica que o despacho declarativo não cobre bem.

### 5.1 CentralSocket (`maps.js`)

`controladores/_Src/maps.js` (~linhas 3–241). `window.CentralSocketHandler` instancia seu próprio `Scaledrone`, assina só `galaxia_logistica_central`, e processa ações de domínio:
- `route_update` — atualiza métricas (separado/carregado/faturamento) e re-renderiza a tabela de rotas.
- `route_sequence_updated` — recebe nova ordem de pedidos e recalcula o trajeto no Google Maps.

Existe à parte porque mexe em **estado global do Board** (textarea `.rotasGeradas`, `rotasState`) e integra com o mapa — coisas fora do escopo do despacho declarativo de DOM.

### 5.2 Apps mobile com socket próprio

`sinais/App/html/{pesagem,separacao,carregamento,retorno}/item.js`. Cada controller assina `galaxia_logistica_missao_{id}` (room dinâmico por missão, renderizado por `lerSocket()` no `.php` pareado). Processam principalmente `mission_cancelled` (bloqueiam a UI). Mantêm estado local pesado em `localStorage`; o socket é mais sinal de controle do que fonte de verdade.

### 5.3 Quando usar genérico vs próprio

| Use o genérico (`lerSocket` + `RequestGalaxiaAct`) | Use handler próprio |
|---|---|
| Atualização de DOM / troca de card / chamar função | Mexe em estado JS global complexo |
| Notificação, toast, badge | Integra com biblioteca externa (mapa, chart com estado) |
| Maioria dos casos | Lógica de domínio que ramifica por `action` |

Na dúvida, comece pelo genérico — é menos código e segue o padrão.

---

## 6. Convenções de room

| Room | Publisher | Subscriber | Escopo |
|---|---|---|---|
| `galaxia_logistica_board` | `Central.php`, `LogisticaSocketTrait` | Board (`maps.js`) | Global do módulo |
| `galaxia_logistica_central` | `Central.php`, `goToUpdateMissaoSinal`, Trait | CentralSocket (`maps.js`) | Global do módulo |
| `galaxia_logistica_missao_{id}` | `MissoesPersistencia`, Trait | Apps mobile (pesagem/separação/…) | Por missão |
| `galaxia_logistica_separacao_usuario_{id}` | `MissoesExecucao`, `MissoesPersistencia` | Operador (`estrutura.php`) | Por usuário |
| `galaxia_logistica_liberacoes_user_{id}` | `LiberacaoSocketNotifier` | Operador (`estrutura.php`) | Por usuário |
| `goldie`, `goldieCliente{id}` | tema `genezes/_theme.php` | tema | **Dev only** (username matheus/felipe) |

Os rooms `goldie`/`goldieCliente` são gateados por username de desenvolvedor — são canais de debug, **não** fazem parte do mecanismo de produção.

---

## 7. Decisões de design e armadilhas

### Por que declarativo (e não comandos imperativos)
Reaproveita o `RequestGalaxiaAct` que já processa respostas de formulário. Backend não precisa saber se a resposta vai por HTTP ou socket — monta a mesma ação Galaxia. O front tem um único ponto de aplicação.

### Mesmo canal, rooms diferentes
A central e o app de um operador conectam ao **mesmo canal** (`uzJPCX4DfT380eoN`), em **rooms diferentes**. A entrega a um operador específico não depende de a central estar no room dele: quando o gestor decide, o **backend** publica no room do operador (`..._user_{id}`). O navegador da central nunca fala direto com o do operador.

### O incidente do canal quebrado
Um commit ("Melhorias do board") trocou o canal de `uzJPCX4DfT380eoN` para `paKUSNCLRO6yHHZ8`. O canal novo retornava **HTTP 500 "Unable to send message"** no publish REST (problema de conta/plano, não de código), o que quebrou silenciosamente todo o realtime server→cliente — daí o uso de fallbacks `refresh` via HTTP em vários pontos. A correção foi voltar ao canal funcional. **Diagnóstico rápido:**

```bash
curl -s -X POST "https://api2.scaledrone.com/<channel>/teste/publish" \
  -H "Content-Type: application/json" \
  -H "Authorization: Basic $(echo -n '<channel>:<secret>' | base64 -w0)" \
  -d '{"hello":"world"}' -w "\nHTTP %{http_code}\n"
```
`HTTP 200` = canal saudável; `HTTP 500 Unable to send message` = canal/conta com problema; `HTTP 401 Channel does not exist` = channel_id errado.

### `lerSocket` deve ser incondicional
Se a view só renderiza `lerSocket()` quando já existe estado pendente, o socket nunca conecta a tempo de receber a **transição** para esse estado (o usuário cria o estado na mesma sessão, sem reload). Renderize a escuta sempre que a tela puder receber eventos.

### Não reincluir o SDK
O SDK vem do tema. Reincluí-lo numa view (especialmente carregada via AJAX) cria corrida: o `<script src>` resolve async e o código que usa `Scaledrone` pode rodar antes de ele existir.

### Best-effort sempre
Publicar nunca pode derrubar a operação principal. Toda publicação fica em `try/catch` com `error_log`; o fluxo de negócio (aprovar, salvar) já está persistido antes de publicar (write-before-fire).

### Gotcha de Sonda relacionado (não é socket, mas afeta o fluxo)
`SolicitacaoHistoricoSonda::make()` retorna `null` (não `$this`), então encadear `->make([...])->save()` estoura `"save() on null"`. Sempre separe `make()` e `save()` em linhas distintas — um crash aqui aborta o Laboratório **antes** de chegar à publicação do socket, e o sintoma aparenta ser "o realtime não funciona".

---

## 8. Mapa de arquivos

| Arquivo | Papel |
|---|---|
| `controladores/_Controladores/GalaxiaRoute.php` | `enviarSocket`, `lerSocket`, `galaxiaResposta`, `goToUpdateMissaoSinal`, `doNotification` |
| `controladores/_Controladores/GalaxiaRouteRespostas.php` | Builder fluente de ações Galaxia |
| `controladores/_Src/func.js` | `verificarSockets` (~651), `RequestGalaxiaAct` (~1927) |
| `controladores/_Src/main.js` | Dispara `verificarSockets()` 500ms após ready (~14) |
| `controladores/_Src/maps.js` | `CentralSocketHandler` (handler próprio do Board/Central) |
| `controladores/_Src/galaxia_themes/genezes/_theme.php` | Carrega o SDK (~2597); rooms de dev `goldie*` (~2881) |
| `componentes/Constelacoes/Logistica/Traits/LogisticaSocketTrait.php` | Broadcasts de rota (board/central/missão) |
| `…/sinais/Edicaorota/Eventos/EdicaoRotaSocketNotifier.php` | Notifier com POST direto (sem `$galaxia`) |
| `…/sinais/Solicitacoes/Eventos/LiberacaoSocketNotifier.php` | Notifier via `$galaxiaRoute->enviarSocket` (ação Galaxia) |
| `…/sinais/App/html/{pesagem,separacao,carregamento,retorno}/item.js` | Sockets próprios por missão |
