# Implementação frontend

## Views/partials criadas

- `componentes/Missoes/Logistica/sinais/App/html/separacao/endereco_wms.php` (+ `endereco_wms.js` pareado)
  — partial "burra" chamada de dentro de `shared/components/execucao/execucao_card.php`
  (só quando `$tipo === 'separacao'`). Desenha:
  - o badge de endereço real (`A03-P2-N1`, selo `WMS`) para itens `origem_localizacao=wms`;
  - o badge de localização legada (selo `Legado`) para itens `origem_localizacao=legado`;
  - os hidden inputs `#data-origem-localizacao` / `#data-endereco-wms` que alimentam a state
    machine JS;
  - o hidden trigger Galaxia (`#trigger-validar-endereco-wms`, `setConstelacao('Logistica')
    ->setEstrela('App')->setSatelite('ValidarEnderecoSeparacao')->setOpcao('validar')`), mesmo
    padrão de `SolicitarAcessoRetira`/`SolicitarAcessoPesagem`;
  - o bloco i18n (`#data-endereco-wms-i18n`) com as strings de estado/erro traduzidas via `show()`.
  - **Não-regressão (AC8)**: quando o item não tem `dados_adicionais.origem_localizacao` (missão
    sem a variante, ou tipo `carregamento`/`ajustedados`), a função faz early-return e não desenha
    nada — zero hidden input, zero trigger, zero mudança visual/comportamental.
  - `endereco_wms.js` — state machine "endereço-primeiro": intercepta `ExecucaoCard.processarScan`
    (monkey-patch idempotente, guardado por `window.__enderecoWmsGatePatched`) e, enquanto o item
    atual é `origem=wms` e o endereço ainda não foi confirmado, trata o próximo bip como ENDEREÇO
    (não produto). Chama o Laboratório via AJAX direto (não via `SendGalaxiaPost`/`dispararAcaoGalaxia`
    — ver "Decisão: sem `atualizarPagina`" abaixo). Em match, marca `confirmado=true` (persistido em
    `localStorage` por missão+item) e libera o `processarScan` original (mecânica de produto
    intocada — RN11). Em mismatch/erro, dá feedback visual (shake + texto) e mantém o gate.

## Arquivos alterados

- `componentes/Missoes/Logistica/sinais/App/html/shared/components/execucao/execucao_card.php`
  — dentro do bloco já existente `if ($tipo === 'separacao')`: (1) chama
  `separacaoEnderecoWmsBadge(...)` para desenhar o badge; (2) inclui `endereco_wms.js` inline logo
  após `execucao_card.js`; (3) a exibição antiga de "Localização" (texto simples, sem selo) agora
  só desenha quando `dados_adicionais.origem_localizacao` está ausente — evita duplicar com o badge
  novo para itens de missão WMS (wms **e** legado).
- `componentes/Missoes/Logistica/sinais/App/html/separacao/item.php` — doc-comment apontando onde
  vive a lógica da variante (nenhuma mudança de comportamento).
- `componentes/Missoes/Logistica/sinais/Designacoes/html/designar_unificado.php` — no modal de
  confirmação do preset de separação (`#modal-preset-config`), novo toggle "Guiar pelo endereço
  WMS" (`#check-variante-endereco-wms`) com texto de ajuda; bloco i18n
  `#data-cobertura-wms-i18n` para o aviso de cobertura pós-criação (US3).
- `componentes/Missoes/Logistica/sinais/Designacoes/html/designar_unificado.js` —
  `executarSubmissao()` agora popula `#input-variante-separacao` a partir do novo checkbox
  (`'WMS'` quando marcado, `''` caso contrário — default = comportamento atual); novo
  `Controller.hookRespostaCoberturaWms()` (chamado no `init()`) que envolve
  `window.RequestGalaxiaAct` uma única vez para ler a chave extra `cobertura_wms` da resposta de
  criação (chave que o pipeline padrão do framework ignora) e mostrar
  `Controller.exibirCoberturaWms(...)` — um overlay informativo, não bloqueante (AC3.3), com
  contagem e lista de itens sem endereço. Sempre delega para o `RequestGalaxiaAct` original em
  seguida — não altera o comportamento padrão para nenhum outro formulário do site.
- `componentes/Missoes/Logistica/sinais/Designacoes/forms/enviar_unificado.php` — novo hidden
  input `<input type="hidden" name="variante_separacao" id="input-variante-separacao" value="">`.

## i18n

Chaves novas (`show("texto PT literal")`, key = `md5(texto)`) adicionadas em
`linguagens/pt-br.json`, `linguagens/en.json` e `linguagens/es.json` (raiz do repo — mesmo arquivo
usado pela feature irmã `bipagem-cota-minima`, com `solicitacoes: ["separacao-endereco-wms"]`):

- `WMS` → "WMS" / "WMS" / "WMS"
- `Legado` → "Legado" / "Legacy" / "Heredado"
- `Endereço confirmado no WMS` → tooltip do selo WMS
- `Localização legada (sem endereço WMS registrado)` → tooltip do selo Legado
- `Bipe o endereço para liberar o produto` → hint do card enquanto aguarda o bip de endereço
- `Endereço confirmado. Bipe o produto.` → hint após confirmar
- `Endereço inválido ou inativo.` → erro (endereço fora do catálogo/inativo)
- `Endereço não confere com o esperado para este item.` → erro (mismatch)
- `Falha ao validar o endereço. Tente novamente.` → erro de rede
- `Guiar pelo endereço WMS` → rótulo do toggle na criação (Designações)
- `Ordena e guia o separador pela posição física registrada no WMS (ex.: A03-P2-N1). Itens sem
  endereço seguem pela localização atual (andar).` → texto de ajuda do toggle
- `Cobertura de endereço WMS` → título do aviso pós-criação
- `{sem} de {total} itens ficaram sem endereço WMS registrado e seguirão pela localização atual.`
  → corpo do aviso (placeholders substituídos em JS)
- `Todos os itens têm endereço WMS registrado.` → corpo do aviso quando cobertura = 100%
- `Entendi` → botão de fechar do aviso
- `Itens sem endereço WMS:` → cabeçalho da lista de itens sem endereço

Verifiquei as 3 entradas no JSON (chave = `md5` do texto PT) antes de fechar — nenhuma colisão com
chaves pré-existentes.

## Endpoints consumidos

- **Criação da missão** — `POST` do form unificado (`Designacoes` → `GestaoMissoesSeparacao`),
  campo `variante_separacao=WMS` (ou ausente/vazio = andar, default). Resposta inclui
  `cobertura_wms: { total, sem_endereco, itens_sem_endereco: [{produto_id, produto_nome}] }`
  quando a variante rodou — consumida via hook em `RequestGalaxiaAct` (ver acima).
- **Bip de endereço** — Galaxia satélite `setConstelacao('Logistica')->setEstrela('App')
  ->setSatelite('ValidarEnderecoSeparacao')->setOpcao('validar')`, payload `{ item_id, codigo }`.
  Resposta: `{ notification_class, status, notification_html, notification_title,
  endereco_confirmado, item_id, endereco_esperado? }`. **Consumida via AJAX direto** (não via
  `SendGalaxiaPost`) — ver decisão abaixo.

### Decisão: sem `atualizarPagina` / sem `SendGalaxiaPost` no bip de endereço

O ramo `App::start()` para `ValidarEnderecoSeparacao` devolve
`galaxiaResposta($laboratorio->response)->atualizarPagina()->feito()` (mesmo padrão de
`SolicitarAcessoRetira`/`SolicitarAcessoPesagem`). No pipeline padrão do framework
(`SendGalaxiaPost` → `axiosGalaxiaPost` → `RequestGalaxiaAct`), `atualizarPagina: 1` dispara
`location.reload()` **incondicionalmente** (sucesso ou erro) assim que a resposta chega — o que
faria a página recarregar a cada bip de endereço, inclusive nos mismatches que o operador precisa
poder corrigir no mesmo card sem reload. Isso não é compatível com a UX pedida em US5 (bip
endereço → bip produto, fluido, sem reload por scan) nem com AC5.3 (erro não pode travar o card
numa recarga que o tira do fluxo).

Por isso `endereco_wms.js` **não** usa `dispararAcaoGalaxia`/`SendGalaxiaPost` para este endpoint
específico: lê `data-sinalrocket`/`data-sinalroute` do próprio botão-trigger renderizado por
`acaoElemento()` (a mesma rota que o framework computaria) e faz o POST com `$.ajax` direto,
tratando a resposta (`endereco_confirmado`) no cliente sem nunca dar reload. O botão-trigger em si
segue o padrão documentado pelo backend (mesma rota/mesmo Laboratório) — só a forma de dispará-lo é
diferente, por decisão de UX do frontend.

## Limitação conhecida (documentar para o tester/PO)

O gate e o badge só ativam no modo **fila única / Luz Dinâmica** (`shared/components/execucao/
execucao_card.php` + `luz/separacao/item_card.php`), que é o fluxo padrão multi-operador da
separação. No modo **livre legado** (`SeparaçãoV2` em `separacao/item.js`, usado quando a missão
não tem Luz Dinâmica), o card do item é manipulado direto no DOM a partir de `jsItensData`
(`MissaoPresenter::montarJsonFrontendBase()`, em `Constelacoes/.../Presenters/` — fora do escopo
de `html/`/`forms/` do dev frontend) e essa estrutura **ainda não** carrega
`origem_localizacao`/`endereco_wms` por item. Resultado: em modo livre, itens de missão WMS
funcionam exatamente como itens sem a variante (sem badge, sem gate) — **degrada de forma segura**
(nenhuma regressão, apenas a feature nova não aparece nesse modo). Para cobrir o modo livre também,
é preciso um ajuste de 1-2 linhas em `MissaoPresenter::montarJsonFrontendBase()` para repassar essas
duas chaves de `dados_adicionais` — fica registrado aqui como próximo passo, não fiz essa mudança
por estar fora de `html/`/`forms/`.

## Smoke test sugerido

1. **Criação (gestor)** — Designações → Nova missão de Separação → selecionar romaneio/itens/
   operadores → abrir "Método de Operação" (qualquer preset) → no modal, marcar **"Guiar pelo
   endereço WMS"** → Criar Missão.
   - Esperado: se algum item não tem endereço WMS confirmado, aparece o overlay "Cobertura de
     endereço WMS" com a contagem (`N de M itens...`) e a lista de produtos sem endereço; overlay
     fecha em "Entendi" sem bloquear nada.
2. Criar uma segunda missão de separação **sem** marcar o toggle → nenhum overlay de cobertura,
   comportamento idêntico ao atual.
3. **Execução (operador, item `wms`)** — abrir a missão criada no passo 1 no app
   (`/missao/logistica/app/separacao/<id>`), fila única: o primeiro item com endereço confirmado
   mostra o badge azul com o código real (ex.: `A03-P2-N1`) + selo `WMS`, e o hint "Bipe o endereço
   para liberar o produto".
   - Bipar um código de endereço **errado** → shake + mensagem de erro; scanner continua pedindo
     endereço (produto não incrementa).
   - Bipar o endereço **correto** (igual ao badge) → hint muda para "Endereço confirmado. Bipe o
     produto."; bips seguintes vão para a mecânica normal de produto (quantidade incrementa,
     snapshot de códigos/fator intocado).
4. **Execução (item `legado`)** — item sem endereço WMS registrado mostra badge âmbar "Legado" com
   a localização atual (andar) e vai **direto** para a bipagem do produto (sem pedir endereço).
5. Conferir o extrato da missão (`btn-extrato`) → deve haver uma entrada `ENDERECO_VALIDADO`
   correspondente ao bip correto do passo 3.
6. **Não-regressão** — abrir uma missão de separação **por andar** (sem a variante, seja uma já
   existente ou a do passo 2) → fila, card, badge de localização (texto simples, sem selo) e
   bipagem idênticos ao comportamento anterior à feature.

## Correções pós-review (ciclo 1)

Aplicados os 2 majors da parte frontend apontados em `06-review.md` (M2, M3). Nenhum minor/nit foi
tocado (m1-m3, n1-n2 — fora de escopo deste ciclo), e nenhum arquivo de `IntegracaoWms/Tradutores`
ou `Inventariociclico` foi tocado (sessão paralela).

### M2 — `endereco_wms.js` migrado do `$.ajax` manual para o dispatcher padrão Galaxia

Pré-requisito do backend (`03-backend.md` ciclo 1): `App::start()` não manda mais
`->atualizarPagina()` no ramo `ValidarEnderecoSeparacao` — a resposta já chega limpa.

- **Alterado** `componentes/Missoes/Logistica/sinais/App/html/separacao/endereco_wms.js`
  (`_validar()`): trocado o `$.ajax({...})` cru + montagem manual de URL (`systemUrl + '/missao/' +
  sinalRocket + '/' + sinalRoute`) por `SendGalaxiaPost(url, dataObj, tag, $trigger)` — o mesmo
  helper que `ExecucaoCard.dispararAcaoGalaxia()` usa (`shared/components/execucao/
  execucao_card.js`). O parsing de `data-acao-dados` (placeholder `{codigo}`) foi mantido (ainda é
  necessário pra montar o payload antes de despachar), mas o **envio** agora passa pelo pipeline
  oficial (`SendGalaxiaPost` → `sendG` → `axiosGalaxiaPost` → `RequestGalaxiaAct`) em vez de bypassar
  tudo.
- **Novo** `EnderecoWmsGate._instalarHookResposta()`: envolve `window.RequestGalaxiaAct` uma única
  vez (idempotente — mesmo padrão de `designar_unificado.js`/`hookRespostaCoberturaWms()`) pra
  capturar a resposta pela presença da chave `endereco_confirmado` (exclusiva deste endpoint) e
  tratá-la sem reload.
  - **Decisão consciente de não delegar pro `original(...)`** nesse caso (diferente do hook de
    `cobertura_wms`, que sempre delega): `RequestGalaxiaAct` chama `NotificarToastr(data)`
    incondicionalmente pra qualquer resposta com `notification_class` — e a resposta de
    `ValidarEnderecoSeparacao` sempre tem essa chave (sucesso ou erro). Como este endpoint dispara a
    **cada bip de endereço** (fluxo de alta frequência), delegar geraria um toast por scan — regressão
    de UX real (a UX documentada/pedida sempre foi feedback inline: hint do card + shake, nunca
    toast). Por isso a resposta é **consumida** (curto-circuito, com `return` antes de chamar
    `original`) quando tem `endereco_confirmado`; qualquer outra resposta (de outros triggers da
    página) continua caindo no `original.apply(this, arguments)` normalmente — zero impacto em
    outros fluxos.
  - Novo watchdog de 10s (`_timeoutFalha`): como `SendGalaxiaPost` não expõe callback de erro pro
    caller (o `.catch()` interno só faz `console.log`), se nenhuma resposta chegar pelo hook nesse
    prazo o gate destrava com o erro `erro_rede` (mesmo texto/UX que o `.fail()` do `$.ajax` antigo
    dava).
  - `dataObj['item_id']` agora é sempre sobrescrito com `EnderecoWmsGate.itemId` (a propriedade viva
    do gate) em vez de confiar 100% no que veio embutido em `data-acao-dados` — necessário pro modo
    livre (M3, trigger genérico `item_id=0`) e um no-op seguro na fila única (mesmo valor).
- **UX mantida**: erro de endereço não recarrega a página (não havia `atualizarPagina` desde a
  correção do backend, e agora também não há navegação nenhuma disparada pelo pipeline padrão);
  acerto libera a bipagem do produto exatamente como antes (`confirmado=true`, hint muda, próximo
  scan cai no `processarScan` original). Comportamento observável idêntico ao pré-ciclo-1 — só a
  implementação interna do transporte HTTP mudou.
- Comentário desatualizado em `endereco_wms.php` (o texto ao lado do trigger da fila única, que
  descrevia o `$.ajax` manual) foi corrigido pra refletir o `SendGalaxiaPost` — mudança só de
  comentário/HTML-comment, sem efeito em markup/CSS/JS visível.

### M3 — Gate + badge ligados também no modo livre (`SeparaçãoV2`/`separacao/item.js`)

Pré-requisito do backend (`03-backend.md` ciclo 1): `MissaoPresenter::montarJsonFrontendBase()`
expõe `origem_localizacao`/`endereco_wms` no topo de cada item de `jsItensData` (além de dentro de
`dados_adicionais`, que já existia).

- **Alterado** `componentes/Missoes/Logistica/sinais/App/html/separacao/endereco_wms.php`:
  `separacaoEnderecoWmsBadge()` ganhou uma chave nova opcional `modo_dinamico` (default `false`) que,
  quando `true`, delega pra uma função **nova e separada** `separacaoEnderecoWmsSkeletonDinamico()`
  (esqueleto: CSS+i18n+hidden inputs vazios+badge/hint ocultos+trigger genérico `item_id=0`) — e
  retorna, sem tocar em nenhuma linha do branch clássico (que continua 100% igual, byte a byte, pra
  todo caller que não passa `modo_dinamico`). Não-regressão garantida por construção: a fila única
  (`luz/separacao/item_card.php`) e todos os outros usos de `execucaoCard()` (carregamento, pesagem,
  etc.) nunca passam essa chave.
- **Alterado** `componentes/Missoes/Logistica/sinais/App/html/shared/components/execucao/
  execucao_card.php`: a chamada existente a `separacaoEnderecoWmsBadge()` agora repassa
  `'modo_dinamico' => $config['modo_dinamico'] ?? false` — só um pass-through, default `false`
  preserva 100% do comportamento pros ~5 outros callers de `execucaoCard()` no repo.
- **Alterado** `componentes/Missoes/Logistica/sinais/App/html/separacao/item.php`: a chamada de
  `execucaoCard()` do modo livre (card placeholder único, montado uma vez no page load) agora passa
  `'galaxia' => $galaxia` (necessário pro trigger `acaoElemento()`; antes ficava `null` e o
  trigger nunca teria sido emitido) e `'modo_dinamico' => true`. Isso posiciona o esqueleto dinâmico
  dentro do mesmo container visual (`.carga-produto-info-extra`) onde a fila única mostra o badge —
  sem precisar mover nada via JS. Doc-comment do topo do arquivo atualizado (estava desatualizado,
  dizia que o modo livre nunca teria o gate).
- **Alterado** `componentes/Missoes/Logistica/sinais/App/html/separacao/endereco_wms.js`
  (`EnderecoWmsGate`):
  - `_patchScanners()` (wrap de `processarScan`) agora roda **sempre** que o esqueleto existe
    (fila única ou modo livre), não só quando o item atual já é `wms` — o próximo item pode ser.
    Puramente aditivo (só amplia a janela em que o patch é aplicado; nunca deixa de aplicar onde já
    aplicava antes).
  - Novo `_patchModoLivre()`: envolve `SeparaçãoV2.atualizarTelaExecucao()` (chamada por
    `selecionarItem()`/ajustes de quantidade/cancelamentos em `separacao/item.js`) pra, depois do
    render original, chamar `sincronizarItemLivre()`. Idempotente; inofensivo na fila única
    (`SeparaçãoV2` existe no escopo global mas nunca é usada lá — quem roda é `SeparacaoDinamica`).
  - Novo `sincronizarItemLivre(ctrl)`: lê `ctrl.itensData[ctrl.itemAtualId].origem_localizacao` /
    `.endereco_wms` (jsItensData — os 2 campos novos do backend) a cada troca de item, escreve de
    volta nos hidden inputs (`#data-origem-localizacao`/`#data-endereco-wms`, mesmo contrato lido por
    `init()`) e reaplica o gate via `_aplicarEstado()` (extraído de `init()`, agora compartilhado
    pelos dois modos). Item `FINALIZAR` (pseudo-item da tela de finalização) ou sem
    `origem_localizacao` → `ativo=false`, gate inerte — degradação idêntica ao AC8.
  - Novo `_atualizarBadgeDinamico(origem)`: só age quando existe `#sep-endereco-wms-badge`
    (id exclusivo do esqueleto `modo_dinamico` — a fila única, com o badge pronto vindo do PHP, não
    tem esse id, então a função é inerte lá). Alterna classe `--wms`/`--legado`, ícone, código e selo
    reaproveitando as MESMAS classes CSS (`sep-endereco-wms*`) e os textos i18n (`show()`) já usados
    pela fila única — literalmente as mesmas 4 strings de selo/tooltip (`WMS`, `Legado`, `Endereço
    confirmado no WMS`, `Localização legada...`), sem introduzir texto novo.
- **Reuso, não duplicação de lógica**: nenhum arquivo novo de partial/JS foi criado — tudo entra em
  cima do `endereco_wms.php`/`endereco_wms.js` já existentes, mantendo o contrato de hidden
  inputs/trigger/i18n que a fila única já usava.
- **Degradação segura confirmada**: item sem `origem_localizacao` (missão sem a variante, ou
  qualquer item fora do escopo) → `origem === ''` em `_aplicarEstado()`/`_atualizarBadgeDinamico()` →
  `ativo=false`, badge permanece `display:none`, hint permanece oculto, `processarScan` cai direto no
  `original(codigo)` — comportamento idêntico ao pré-ciclo-1 (AC8), tanto na fila única quanto no
  modo livre.

### i18n

Nenhuma chave nova. O badge dinâmico do modo livre reusa literalmente os mesmos 9 textos PT
(`show()`) já traduzidos nos 3 idiomas desde o ciclo 0 (`WMS`, `Legado`, `Endereço confirmado no
WMS`, `Localização legada (sem endereço WMS registrado)`, `Bipe o endereço para liberar o produto`,
`Endereço confirmado. Bipe o produto.`, `Endereço inválido ou inativo.`, `Endereço não confere com o
esperado para este item.`, `Falha ao validar o endereço. Tente novamente.`) — confirmado via `md5()`
de cada string contra `linguagens/{pt-br,en,es}.json` antes de fechar o ciclo.

### Endpoints consumidos (sem mudança de contrato)

Mesmo endpoint do ciclo 0 (`ValidarEnderecoSeparacao`/`validar`, payload `{item_id, codigo}`,
resposta com `endereco_confirmado`) — só a forma de despachar do frontend mudou (M2). Nenhuma
mudança de payload/resposta.

### Comandos de verificação executados (ciclo 1)

- `php -l componentes/Missoes/Logistica/sinais/App/html/separacao/endereco_wms.php` → OK
- `php -l componentes/Missoes/Logistica/sinais/App/html/shared/components/execucao/execucao_card.php` → OK
- `php -l componentes/Missoes/Logistica/sinais/App/html/separacao/item.php` → OK
- `php -l componentes/Missoes/Logistica/sinais/App/App.php` → OK (não tocado neste ciclo, conferido por completude)
- `node --check componentes/Missoes/Logistica/sinais/App/html/separacao/endereco_wms.js` → OK

### Smoke test adicional sugerido (ciclo 1)

1. **M2 — sem reload, sem toast por scan**: repetir o passo 3 do smoke test original (fila única,
   item `wms`) com o DevTools Network aberto. Bipar endereço errado várias vezes seguidas → cada
   tentativa gera 1 requisição POST pro satélite `ValidarEnderecoSeparacao` (Network), a página
   **não** recarrega, e **não** aparece nenhum toast/notificação flutuante — só o hint do card
   (shake + texto de erro) muda. Bipar o endereço correto → hint muda pra "Endereço confirmado. Bipe
   o produto.", sem toast, sem reload.
2. **M2 — fallback de rede**: com o DevTools em "Offline" (ou bloqueando a rota do satélite),
   bipar um endereço → depois de ~10s o hint mostra "Falha ao validar o endereço. Tente novamente."
   e o gate destrava pra nova tentativa (não trava a fila indefinidamente).
3. **M3 — modo livre, item `wms`**: criar/usar uma missão de separação com a variante WMS **sem**
   Luz Dinâmica ativa (modo livre). Abrir a missão no app — ao selecionar/chegar num item com
   endereço WMS confirmado, deve aparecer o badge azul "WMS" com o código (mesmo visual da fila
   única) logo abaixo do nome/código do produto, e o scanner deve pedir o ENDEREÇO antes do produto
   (mesmo hint "Bipe o endereço para liberar o produto"). Bipar errado → shake + erro, produto não
   incrementa. Bipar certo → hint de sucesso, próximos bips vão pra mecânica normal de produto.
4. **M3 — modo livre, item `legado`**: no mesmo modo livre, item sem endereço WMS mostra badge âmbar
   "Legado" e vai direto pra bipagem do produto (sem pedir endereço).
5. **M3 — modo livre, troca de item**: trocar de item várias vezes (via modal de itens ou navegação
   sequencial) sem sair da página — o badge/gate deve atualizar corretamente a cada troca (sem
   precisar de reload), refletindo sempre o item atualmente selecionado.
6. **M3 — não-regressão total**: abrir uma missão de separação **sem** a variante WMS em modo livre
   → nenhum badge novo aparece, nenhum gate de endereço ativa, comportamento idêntico ao
   pré-ciclo-1.

## Correções pós-review (ciclo 2)

Corrigido o único blocker do ciclo 1 (`06-review.md`, "Ciclo 1 — re-review", **B1**). Nenhum
minor/observação (c1-c3) foi tocado (fora de escopo deste ciclo), e nenhum arquivo de
`IntegracaoWms/Tradutores`/`Inventariociclico` foi tocado (sessão paralela).

### B1 — `window.ExecucaoCard`/`window['SeparaçãoV2']` eram sempre `undefined`

**Causa raiz** (diagnóstico do reviewer, confirmado na leitura do código): `ExecucaoCard`
(`shared/components/execucao/execucao_card.js:8`) e `SeparaçãoV2` (`separacao/item.js:42`) são
declarados como `const` no topo de `<script>` clássicos. `const`/`let` de topo entram no *global
lexical environment* do script, **não** viram propriedade de `window` — então todo lookup por
`window['ExecucaoCard']`/`window['SeparaçãoV2']` em `endereco_wms.js` retornava `undefined`,
`_patchScanners()`/`_patchModoLivre()` caíam no `return` cedo, e o gate de endereço nunca
interceptava `processarScan` em nenhum dos dois modos (fila única ou modo livre).

**Fix aplicado** (exatamente o "fix mínimo" sugerido em `06-review.md`, opção 1 — exportar os
controllers ao objeto global):

- `componentes/Missoes/Logistica/sinais/App/html/shared/components/execucao/execucao_card.js` —
  adicionada `window.ExecucaoCard = ExecucaoCard;` logo após o objeto `ExecucaoCard` fechar (antes
  do `$(document).ready(...)` que chama `ExecucaoCard.init()`).
- `componentes/Missoes/Logistica/sinais/App/html/separacao/item.js` — adicionada
  `window.SeparaçãoV2 = SeparaçãoV2;` logo após o objeto `SeparaçãoV2` fechar (antes do bloco
  `VALIDAÇÃO DE INÍCIO - CONTROLLER`/`ValidacaoInicio`).

Nenhuma outra linha de `execucao_card.js`/`item.js` foi tocada — os dois arquivos continuam
funcionalmente idênticos pra tudo que já usava `ExecucaoCard`/`SeparaçãoV2` por nome nu (escopo
léxico do próprio script), que sempre funcionou; só a visibilidade via `window[...]` (usada
exclusivamente por `endereco_wms.js`) estava faltando.

### Verificação executada

- **`node --check`** limpo nos 3 arquivos tocados/consumidos pelo fix:
  `shared/components/execucao/execucao_card.js`, `separacao/item.js`, `separacao/endereco_wms.js`.
- **Match byte-a-byte dos pontos de acesso**: extraí via regex os 3 usos de
  `window['SeparaçãoV2']`/`window.SeparaçãoV2 =`/`const SeparaçãoV2 =` (arquivo `endereco_wms.js`
  linhas 125/147, `item.js` linha nova + declaração original) e comparei os *code points* do nome —
  os 3 são exatamente `SeparaçãoV2` (mesmo `ã`/`ç`, sem divergência de normalização Unicode NFC/NFD
  que poderia fazer um "parecer igual" visualmente mas falhar em runtime). Idem para `ExecucaoCard`
  (nome ASCII, sem risco de normalização).
- **Métodos patcheados existem com os nomes esperados** (grep dedicado, não só a existência do
  objeto):
  - `ExecucaoCard.processarScan` — `execucao_card.js:551`.
  - `SeparaçãoV2.processarScan` — `item.js:2312`.
  - `SeparaçãoV2.atualizarTelaExecucao` — `item.js:1752` (assinatura `function (isVisualizacao =
    false)`, compatível com o wrapper de `_patchModoLivre`).
  - `ExecucaoCard.mostrarFeedbackScan` / `mostrarAlertaErro` / `triggerAnimation` — usados por
    `EnderecoWmsGate._feedback()` via `window.ExecucaoCard` (linha 362) — todos presentes.
  - `SeparaçãoV2.itensData` / `.itemAtualId` / `.missaoId` — usados por
    `sincronizarItemLivre(ctrl)` — todos presentes no objeto.
- **Ordem de inclusão de script conferida** (fila única): `execucao_card.php:1077` inclui
  `execucao_card.js` (define + agora exporta `window.ExecucaoCard`) **antes** de `:1085` incluir
  `endereco_wms.js` (que lê `window.ExecucaoCard` em `init()`/`_feedback()`), ambos inline no mesmo
  `<script>`-block por card — `window.ExecucaoCard` garantidamente existe quando `endereco_wms.js`
  roda, na mesma passada síncrona de parse.

### Risco residual identificado (não corrigido neste ciclo — fora do escopo do fix mínimo)

Ao rastrear a cadeia de carregamento do **modo livre** encontrei um ponto que merece atenção extra
do tester em browser real, mas que **não** é o mesmo bug do B1 e não teria fix de 2 linhas:

- `execucao_card.js`/`endereco_wms.js` são incluídos **inline** (síncrono, durante o parse do HTML)
  também no modo livre — `execucaoCard()` (`execucao_card.php`) inclui os dois sem checar
  `$config['include_js']` (essa chave é passada por `item.php:528` mas não é lida em nenhum lugar
  de `execucao_card.php` — config morta, pré-existente, não introduzida por este ciclo). Então
  `EnderecoWmsGate.init()` roda no `$(document).ready` de `endereco_wms.js`, síncrono.
- Já `separacao/item.js` (dono de `SeparaçãoV2`) é o JS **pareado** de `item.php`, injetado pelo
  framework via `GalaxiaRoute::visual()` como `<div data-novoscript="1"
  data-script=".../item.js?v=...">` — carregado de forma assíncrona por `loadDynamicScripts()`
  (`controladores/_Src/func.js:2199`), que roda dentro de um `setTimeout(..., 200)` antes de criar
  o `<script async>` de fato. O próprio `item.php` (`:740-775`) já lida com essa assincronia
  esperando **1000ms** (`setTimeout(..., 1000)`) antes de checar `typeof SeparaçãoV2 !==
  'undefined'` pra chamar `SeparaçãoV2.init()`.
- `EnderecoWmsGate.init()`, por outro lado, roda em `$(document).ready` **sem** delay nenhum.
  Dependendo de quando exatamente o `DOMContentLoaded`/ready dispara em relação à injeção
  assíncrona de `item.js` (~200ms+tempo de rede), é **teoricamente possível** que
  `_patchModoLivre()` rode antes de `window.SeparaçãoV2` existir — e, nesse caso, **não há retry**:
  `_patchModoLivre()` só reseta o guard (`window.__enderecoWmsGateLivrePatched`) quando o patch
  aplica com sucesso, então uma nova tentativa futura seria possível em teoria, mas **nada no
  código atual chama `_patchModoLivre()` uma segunda vez**. (`_patchScanners()` é ainda mais
  sensível: o guard `window.__enderecoWmsGatePatched` é setado **antes** de checar se
  `SeparaçãoV2` existe, então uma falha no primeiro `SeparaçãoV2.processarScan` nunca seria
  re-tentada mesmo que eu adicionasse uma chamada extra depois.)
- **Não consegui confirmar nem refutar isso com certeza sem um browser real** (a ordem exata de
  execução de `$(document).ready` handlers registrados por scripts diferentes depende de como a
  página do sinal `App` é efetivamente carregada — full page load vs. navegação AJAX dentro do
  shell SPA — e não tenho ambiente de browser neste ciclo para medir o timing real).
- **Não apliquei fix pra isso** por estar fora do "fix mínimo" pedido (a correção robusta exigiria
  reestruturar o guard de `_patchScanners()` pra ser por-controller em vez de global, o que é mais
  que 2 linhas e eu não tenho como validar empiricamente aqui).
- **Ação recomendada para o tester**: no smoke test de modo livre (item 3-5 da seção "Smoke test
  adicional sugerido (ciclo 1)"), rodar o teste também em **carregamento frio** (hard refresh,
  cache desabilitado no DevTools) e, logo após a página abrir (antes de qualquer interação),
  checar no console: `typeof window.SeparaçãoV2` (deve virar `"object"` assim que `item.js`
  carrega) e `window.__enderecoWmsGateLivrePatched` (deve virar `true` depois disso). Se o segundo
  ficar `undefined`/`false` permanentemente mesmo com `SeparaçãoV2` já definido, é esse race — abrir
  um novo achado (não B1) pro próximo ciclo, com o fix sendo tornar `_patchScanners()`/
  `_patchModoLivre()` retentáveis (guard por controller, não global) chamando-as de novo num
  `setTimeout(..., 1200)` em `init()` (depois do prazo de 1000ms que `item.php` já usa pro mesmo
  problema).

### Comandos de verificação executados (ciclo 2)

- `node --check componentes/Missoes/Logistica/sinais/App/html/shared/components/execucao/execucao_card.js` → OK
- `node --check componentes/Missoes/Logistica/sinais/App/html/separacao/item.js` → OK
- `node --check componentes/Missoes/Logistica/sinais/App/html/separacao/endereco_wms.js` → OK
- `grep -n "processarScan\s*:\s*function"` em `execucao_card.js` e `item.js` → 1 match em cada,
  nomes batem com o que `_patchScanners()` espera.
- `grep -n "atualizarTelaExecucao\s*:\s*function"` em `item.js` → 1 match, bate com
  `_patchModoLivre()`.
- Comparação de code points (Python) entre `window['SeparaçãoV2']` (`endereco_wms.js`),
  `window.SeparaçãoV2 =` (novo, `item.js`) e `const SeparaçãoV2 =` (declaração original,
  `item.js`) → os 3 idênticos byte a byte.

### Smoke test adicional sugerido (ciclo 2)

1. **B1 — fila única, verificação de wiring no console**: abrir uma missão de separação com a
   variante WMS em fila única, abrir o DevTools console e checar `typeof window.ExecucaoCard` →
   deve ser `"object"` (antes do fix: `"undefined"`). Repetir os passos 3-5 do smoke test original
   (bip de endereço errado/certo) — devem funcionar como descrito (antes do fix, o bip de endereço
   nunca era interceptado e o primeiro scan já contava como produto).
2. **B1 — modo livre, verificação de wiring no console**: abrir uma missão de separação com a
   variante WMS **sem** Luz Dinâmica (modo livre), checar `typeof window.SeparaçãoV2` → deve ser
   `"object"` (antes do fix: `"undefined"`) e `window.__enderecoWmsGateLivrePatched` → deve ser
   `true`. Repetir os passos 3-5 da seção M3 do smoke test do ciclo 1 (badge WMS/legado, gate de
   endereço, troca de item).
3. **B1 — carregamento frio (ver "Risco residual" acima)**: repetir o item 2 com hard refresh e
   cache desabilitado, checando `window.__enderecoWmsGateLivrePatched` **imediatamente** após o
   carregamento da tela de execução (antes de interagir) — se ficar `false`/`undefined` de forma
   persistente mesmo com `window.SeparaçãoV2` já definido, registrar como novo achado (race
   condition, não B1) para o próximo ciclo.
