# Liberação Realtime — Scaledrone Push — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Atualizar automaticamente os cards de liberação pendente em `estrutura.php` quando o admin aprova ou rejeita, via Scaledrone WebSocket push, sem recarregar a página.

**Architecture:** Fix no REST publish do Scaledrone → novo `LiberacaoSocketNotifier` chamado pelos labs de aprovação/rejeição → `estrutura.php` renderiza templates ocultos + lerSocket + JS que assina o room e faz swap do card ao receber o evento.

**Tech Stack:** PHP 8+, Scaledrone REST API (publish), Scaledrone WebSocket SDK (subscribe), jQuery (já presente)

## Global Constraints

- Channel Scaledrone: `paKUSNCLRO6yHHZ8` / secret `pIe8XnOQbMcHWmT8BAnMDpmMuRXldmYG`
- Room naming: `galaxia_logistica_liberacoes_user_{userId}` (isolamento por usuário)
- Scaledrone SDK CDN: `https://cdn.scaledrone.com/scaledrone.min.js`
- Notifier é best-effort: falhas vão para `error_log`, nunca propagam exceção ao caller
- Tipos cobertos: `ACESSO_PESAGEM`, `ACESSO_ALOCACAO_WMS`, `ACESSO_RETIRA`
- `SAIDA_OPERACAO` está fora do escopo (não tem card pendente em `estrutura.php`)
- Nunca usar SQL raw; não introduzir dependências novas além do SDK Scaledrone já em uso

---

## Mapa de arquivos

| Arquivo | Ação |
|---|---|
| `controladores/_Controladores/GalaxiaRoute.php` | Modificar `enviarSocket()` — fix HTTP 500 |
| `componentes/Missoes/Logistica/sinais/Edicaorota/Eventos/EdicaoRotaSocketNotifier.php` | Modificar `publicar()` — mesma correção para consistência |
| `componentes/Missoes/Logistica/sinais/Solicitacoes/Eventos/LiberacaoSocketNotifier.php` | **Criar** |
| `componentes/Missoes/Logistica/sinais/Solicitacoes/Laboratorios/AprovarSolicitacaoLaboratorio.php` | Modificar — chamar notifier |
| `componentes/Missoes/Logistica/sinais/Solicitacoes/Laboratorios/RejeitarSolicitacaoLaboratorio.php` | Modificar — chamar notifier |
| `componentes/Missoes/Logistica/sinais/App/html/estrutura.php` | Modificar — lerSocket + templates ocultos + SDK + JS IIFE |

---

## Task 1: Diagnosticar e corrigir o Scaledrone REST publish

**Files:**
- Modify: `controladores/_Controladores/GalaxiaRoute.php` (método `enviarSocket`, linha ~1436)
- Modify: `componentes/Missoes/Logistica/sinais/Edicaorota/Eventos/EdicaoRotaSocketNotifier.php` (método `publicar`)

**Interfaces:**
- Produz: `GalaxiaRoute::enviarSocket(string $room, array $dados)` retorna `['status' => 'ok']` quando o publish tem sucesso

- [ ] **Step 1: Diagnosticar o erro real com cURL manual**

No terminal do servidor (ou em um script PHP temporário), rode:

```bash
curl -s -X POST \
  "https://api2.scaledrone.com/paKUSNCLRO6yHHZ8/galaxia_logistica_teste/publish" \
  -H "Content-Type: application/json" \
  -H "Authorization: Basic $(echo -n 'paKUSNCLRO6yHHZ8:pIe8XnOQbMcHWmT8BAnMDpmMuRXldmYG' | base64 -w0)" \
  -d '{"test":"ping"}' \
  -w "\nHTTP %{http_code}\n"
```

Anote o body da resposta e o HTTP status. Se retornar algo como `{"error": "..."}`, esse é o motivo real. Ajuste os steps seguintes conforme o erro encontrado.

- [ ] **Step 2: Verificar se a conta Scaledrone suporta REST publish**

Acesse o painel Scaledrone (https://scaledrone.com) com as credenciais do canal `paKUSNCLRO6yHHZ8` e confirme:
- O plano ativo permite REST publish (planos free podem ter restrições)
- O canal está ativo e as credenciais são as corretas

Se o plano não suporta REST publish, o canal precisa ser atualizado ou substituído. Neste caso, interrompa e reporte antes de continuar.

- [ ] **Step 3: Atualizar `enviarSocket()` para usar cURL com leitura de resposta de erro**

No arquivo `controladores/_Controladores/GalaxiaRoute.php`, substitua o método `enviarSocket` (linha ~1436):

```php
public function enviarSocket($room, $dados, $debug = false)
{
    $channel_id     = 'paKUSNCLRO6yHHZ8';
    $channel_secret = 'pIe8XnOQbMcHWmT8BAnMDpmMuRXldmYG';
    $auth           = base64_encode("{$channel_id}:{$channel_secret}");
    $url            = "https://api2.scaledrone.com/{$channel_id}/{$room}/publish";
    $body           = json_encode($dados);

    if (function_exists('curl_init')) {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $body,
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'Authorization: Basic ' . $auth,
            ],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 5,
            CURLOPT_CONNECTTIMEOUT => 3,
        ]);
        $result   = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($result === false || $httpCode >= 400) {
            error_log("[enviarSocket] Falha room={$room} HTTP={$httpCode} body={$result}");
            return ['status' => 'nao'];
        }
        return ['status' => 'ok'];
    }

    // Fallback: file_get_contents
    $context = stream_context_create([
        'http' => [
            'header'        => [
                'Content-Type: application/json',
                'Authorization: Basic ' . $auth,
            ],
            'method'        => 'POST',
            'content'       => $body,
            'timeout'       => 5,
            'ignore_errors' => true,
        ],
    ]);
    $result = @file_get_contents($url, false, $context);
    $httpCode = 0;
    if (isset($http_response_header)) {
        preg_match('/HTTP\/\S+\s+(\d+)/', $http_response_header[0] ?? '', $m);
        $httpCode = (int)($m[1] ?? 0);
    }

    if ($result === false || $httpCode >= 400) {
        error_log("[enviarSocket] Falha room={$room} HTTP={$httpCode} body={$result}");
        return ['status' => 'nao'];
    }
    return ['status' => 'ok'];
}
```

- [ ] **Step 4: Aplicar o mesmo fix em `EdicaoRotaSocketNotifier::publicar()`**

No arquivo `componentes/Missoes/Logistica/sinais/Edicaorota/Eventos/EdicaoRotaSocketNotifier.php`, substitua o método `publicar`:

```php
private static function publicar(string $room, array $payload): void
{
    try {
        $channel_id     = self::SCALEDRONE_CHANNEL_ID;
        $channel_secret = self::SCALEDRONE_CHANNEL_SECRET;
        $auth           = base64_encode("{$channel_id}:{$channel_secret}");
        $url            = "https://api2.scaledrone.com/{$channel_id}/{$room}/publish";
        $body           = json_encode($payload);

        if (function_exists('curl_init')) {
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_POST           => true,
                CURLOPT_POSTFIELDS     => $body,
                CURLOPT_HTTPHEADER     => [
                    'Content-Type: application/json',
                    'Authorization: Basic ' . $auth,
                ],
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT        => 5,
                CURLOPT_CONNECTTIMEOUT => 3,
            ]);
            $result   = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            if ($result === false || $httpCode >= 400) {
                error_log("[EdicaoRotaSocketNotifier] Falha room={$room} HTTP={$httpCode} body={$result}");
            }
            return;
        }

        $context = stream_context_create([
            'http' => [
                'header'        => [
                    'Content-Type: application/json',
                    'Authorization: Basic ' . $auth,
                ],
                'method'        => 'POST',
                'content'       => $body,
                'timeout'       => 5,
                'ignore_errors' => true,
            ],
        ]);
        $result = @file_get_contents($url, false, $context);
        $httpCode = 0;
        if (isset($http_response_header)) {
            preg_match('/HTTP\/\S+\s+(\d+)/', $http_response_header[0] ?? '', $m);
            $httpCode = (int)($m[1] ?? 0);
        }
        if ($result === false || $httpCode >= 400) {
            error_log("[EdicaoRotaSocketNotifier] Falha room={$room} HTTP={$httpCode} body={$result}");
        }
    } catch (\Throwable $e) {
        error_log('[EdicaoRotaSocketNotifier::publicar] ' . $e->getMessage());
    }
}
```

- [ ] **Step 5: Validar que o publish funciona**

Num script PHP temporário (ou via Tinker se disponível), execute:

```php
$galaxiaRoute = new \GalaxiaRoute();  // ou instanciar conforme padrão do projeto
$resultado = $galaxiaRoute->enviarSocket('galaxia_logistica_teste_fix', ['test' => 'ping']);
var_dump($resultado); // deve ser ['status' => 'ok']
```

Abra o DevTools de um browser, conecte ao Scaledrone channel `paKUSNCLRO6yHHZ8` no room `galaxia_logistica_teste_fix` e confirme que o evento chega. Se o status ainda for `nao`, leia o `error_log` do PHP para o body de erro retornado pelo Scaledrone e corrija antes de continuar.

- [ ] **Step 6: Commit**

```bash
git add controladores/_Controladores/GalaxiaRoute.php \
        componentes/Missoes/Logistica/sinais/Edicaorota/Eventos/EdicaoRotaSocketNotifier.php
git commit -m "fix: corrigir REST publish do Scaledrone (cURL + log do erro real)"
```

---

## Task 2: Criar LiberacaoSocketNotifier

**Files:**
- Create: `componentes/Missoes/Logistica/sinais/Solicitacoes/Eventos/LiberacaoSocketNotifier.php`

**Interfaces:**
- Consome: `GalaxiaRoute::enviarSocket()` (Task 1)
- Produz: `LiberacaoSocketNotifier::notificar(int $solicitanteId, string $tipo, string $status): void`

- [ ] **Step 1: Criar o diretório `Eventos/` se não existir**

```bash
mkdir -p componentes/Missoes/Logistica/sinais/Solicitacoes/Eventos
```

- [ ] **Step 2: Criar `LiberacaoSocketNotifier.php`**

```php
<?php

namespace GalaxiaMissoes\Logistica\sinais\Solicitacoes\Eventos;

/**
 * Publica no room Scaledrone do usuário quando uma solicitação de acesso
 * é aprovada ou rejeitada. Best-effort: falhas não propagam ao caller.
 */
class LiberacaoSocketNotifier
{
    private const CHANNEL_ID     = 'paKUSNCLRO6yHHZ8';
    private const CHANNEL_SECRET = 'pIe8XnOQbMcHWmT8BAnMDpmMuRXldmYG';

    public static function notificar(int $solicitanteId, string $tipo, string $status): void
    {
        try {
            $room    = 'galaxia_logistica_liberacoes_user_' . $solicitanteId;
            $payload = [
                'action'        => 'liberacao_atualizada',
                'tipo'          => $tipo,
                'status'        => $status,
                'solicitante_id' => $solicitanteId,
            ];

            self::publicar($room, $payload);
        } catch (\Throwable $e) {
            error_log('[LiberacaoSocketNotifier::notificar] ' . $e->getMessage());
        }
    }

    private static function publicar(string $room, array $payload): void
    {
        $auth = base64_encode(self::CHANNEL_ID . ':' . self::CHANNEL_SECRET);
        $url  = 'https://api2.scaledrone.com/' . self::CHANNEL_ID . '/' . $room . '/publish';
        $body = json_encode($payload);

        if (function_exists('curl_init')) {
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_POST           => true,
                CURLOPT_POSTFIELDS     => $body,
                CURLOPT_HTTPHEADER     => [
                    'Content-Type: application/json',
                    'Authorization: Basic ' . $auth,
                ],
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT        => 5,
                CURLOPT_CONNECTTIMEOUT => 3,
            ]);
            $result   = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            if ($result === false || $httpCode >= 400) {
                error_log("[LiberacaoSocketNotifier] Falha room={$room} HTTP={$httpCode} body={$result}");
            }
            return;
        }

        $context = stream_context_create([
            'http' => [
                'header'        => [
                    'Content-Type: application/json',
                    'Authorization: Basic ' . $auth,
                ],
                'method'        => 'POST',
                'content'       => $body,
                'timeout'       => 5,
                'ignore_errors' => true,
            ],
        ]);
        $result = @file_get_contents($url, false, $context);
        $httpCode = 0;
        if (isset($http_response_header)) {
            preg_match('/HTTP\/\S+\s+(\d+)/', $http_response_header[0] ?? '', $m);
            $httpCode = (int)($m[1] ?? 0);
        }
        if ($result === false || $httpCode >= 400) {
            error_log("[LiberacaoSocketNotifier] Falha room={$room} HTTP={$httpCode} body={$result}");
        }
    }
}
```

- [ ] **Step 3: Verificar autoload**

Confirme que o namespace `GalaxiaMissoes\Logistica\sinais\Solicitacoes\Eventos\` está coberto pelo autoloader (PSR-4). Verifique se existe um padrão equivalente em outros `Eventos/` do projeto:

```bash
find componentes/Missoes/Logistica/sinais -type d -name "Eventos"
```

Se nenhum existir, confira o `composer.json` ou o autoloader do framework para garantir que o diretório é descoberto.

- [ ] **Step 4: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Solicitacoes/Eventos/LiberacaoSocketNotifier.php
git commit -m "feat: criar LiberacaoSocketNotifier para push Scaledrone de liberações"
```

---

## Task 3: Chamar o notifier nos laboratórios de aprovação e rejeição

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Solicitacoes/Laboratorios/AprovarSolicitacaoLaboratorio.php`
- Modify: `componentes/Missoes/Logistica/sinais/Solicitacoes/Laboratorios/RejeitarSolicitacaoLaboratorio.php`

**Interfaces:**
- Consome: `LiberacaoSocketNotifier::notificar(int, string, string)` (Task 2)
- Consome: `SolicitacaoTipo::ACESSO_PESAGEM`, `::ACESSO_ALOCACAO_WMS`, `::ACESSO_RETIRA`

- [ ] **Step 1: Adicionar use + chamada em `AprovarSolicitacaoLaboratorio`**

No topo do arquivo (após os `use` existentes), adicione:

```php
use GalaxiaMissoes\Logistica\sinais\Solicitacoes\Eventos\LiberacaoSocketNotifier;
```

No método `executar()`, logo após `return $resposta;` (linha ~102) mas antes do `catch`, adicione a chamada ao notifier. O local correto é **após** `$respostaStrategy` e **antes** do `return $resposta`:

```php
            // [SOCKET] Notificar operador em tempo real (best-effort)
            $tiposComCard = [
                SolicitacaoTipo::ACESSO_PESAGEM,
                SolicitacaoTipo::ACESSO_ALOCACAO_WMS,
                SolicitacaoTipo::ACESSO_RETIRA,
            ];
            if (in_array($solicitacaoEncontrada->tipo, $tiposComCard, true)) {
                LiberacaoSocketNotifier::notificar(
                    (int) $solicitacaoEncontrada->solicitante_id,
                    $solicitacaoEncontrada->tipo,
                    'aprovada'
                );
            }

            return $resposta;
```

O trecho completo do método `executar()` a partir da linha onde está o strategy fica assim:

```php
            $respostaStrategy = $this->executarStrategySideEffect($solicitacaoEncontrada);

            /** @var array */
            $resposta = $this->message->success("Solicitação aprovada com sucesso.")->render();

            if (is_array($respostaStrategy) && !empty($respostaStrategy['refresh'])) {
                $resposta['refresh'] = $respostaStrategy['refresh'];
            }

            // [SOCKET] Notificar operador em tempo real (best-effort)
            $tiposComCard = [
                SolicitacaoTipo::ACESSO_PESAGEM,
                SolicitacaoTipo::ACESSO_ALOCACAO_WMS,
                SolicitacaoTipo::ACESSO_RETIRA,
            ];
            if (in_array($solicitacaoEncontrada->tipo, $tiposComCard, true)) {
                LiberacaoSocketNotifier::notificar(
                    (int) $solicitacaoEncontrada->solicitante_id,
                    $solicitacaoEncontrada->tipo,
                    'aprovada'
                );
            }

            return $resposta;
```

- [ ] **Step 2: Adicionar use + chamada em `RejeitarSolicitacaoLaboratorio`**

No topo do arquivo (após os `use` existentes), adicione:

```php
use GalaxiaMissoes\Logistica\sinais\Solicitacoes\Eventos\LiberacaoSocketNotifier;
```

No método `executar()`, após `$this->executarStrategySideEffectRejeicao($solicitacaoEncontrada);` (dentro do `if ($rejeicaoPersistida)`), adicione:

```php
            if ($rejeicaoPersistida) {
                $this->executarStrategySideEffectRejeicao($solicitacaoEncontrada);

                // [SOCKET] Notificar operador em tempo real (best-effort)
                $tiposComCard = [
                    SolicitacaoTipo::ACESSO_PESAGEM,
                    SolicitacaoTipo::ACESSO_ALOCACAO_WMS,
                    SolicitacaoTipo::ACESSO_RETIRA,
                ];
                if (in_array($solicitacaoEncontrada->tipo, $tiposComCard, true)) {
                    LiberacaoSocketNotifier::notificar(
                        (int) $solicitacaoEncontrada->solicitante_id,
                        $solicitacaoEncontrada->tipo,
                        'rejeitada'
                    );
                }
            }
```

- [ ] **Step 3: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/Solicitacoes/Laboratorios/AprovarSolicitacaoLaboratorio.php \
        componentes/Missoes/Logistica/sinais/Solicitacoes/Laboratorios/RejeitarSolicitacaoLaboratorio.php
git commit -m "feat: notificar operador via Scaledrone após aprovar/rejeitar liberação"
```

---

## Task 4: estrutura.php — templates ocultos, lerSocket e JS de swap

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/App/html/estrutura.php`

**Interfaces:**
- Consome: `$galaxia->lerSocket(string $room)` → renderiza `<div class="rooms-socket hidden" data-channel="..." data-room="...">`
- Consome: Scaledrone SDK (`new Scaledrone(channel)`, `.subscribe(room, opts)`)
- Consome: evento payload `{ action: 'liberacao_atualizada', tipo, status }`

### 4a — Adicionar `data-tipo` + `lerSocket` + SDK

- [ ] **Step 1: Adicionar `data-tipo` nos 3 cards pendentes**

Nos 3 blocos de card pendente (Alocação WMS, Pesagem, Retira), a `<div>` externa do card precisa receber `data-tipo`. Localize cada bloco pelo comentário `elseif ($solicitacaoXxxPendente):` e adicione o atributo.

**Card Alocação WMS** (buscar por `btn-alocacaowms-bloqueado`):
```php
<div id="btn-alocacaowms-bloqueado" data-tipo="ACESSO_ALOCACAO_WMS" class="action-card" ...>
```

**Card Pesagem** (buscar por `btn-pesagem-bloqueado` ou equivalente):
```php
<div id="btn-pesagem-bloqueado" data-tipo="ACESSO_PESAGEM" class="action-card" ...>
```

**Card Retira** (buscar por `btn-pedidos-bloqueado`):
```php
<div id="btn-pedidos-bloqueado" data-tipo="ACESSO_RETIRA" class="action-card" ...>
```

- [ ] **Step 2: Adicionar os templates ocultos após cada card pendente**

Para cada card que está em estado pendente no PHP, renderizar dois `<template>` imediatamente após o `</div>` do card pendente (mas dentro do mesmo `<?php elseif (...)?>` ... `<?php else: ?>` não — deve ficar fora, disponível sempre).

O melhor lugar é **fora** do `if/elseif/else`, logo após o `</div>` que fecha o `col-6`. Assim o PHP sempre renderiza os templates independente do estado atual.

**Após o `</div>` do col-6 de Alocação WMS** (depois do `<?php endif; ?>` do bloco WMS):

```php
<?php if (!$acessoWmsLiberado): ?>
<template data-tipo-aprovado="ACESSO_ALOCACAO_WMS">
    <div class="col-6">
        <a <?= $galaxia->proximaRota("logistica/app/alocacaowms"); ?> href="#" id="btn-alocacaowms" class="action-card" style="background: linear-gradient(135deg, #4f46e5 0%, #3730a3 100%);">
            <div class="icon-wrapper"><i class="bi bi-geo-alt-fill"></i></div>
            <div>
                <h3 class="text-white">Alocação WMS</h3>
                <p>Endereçar produtos no armazém.</p>
            </div>
        </a>
    </div>
</template>
<template data-tipo-rejeitado="ACESSO_ALOCACAO_WMS">
    <div class="col-6">
        <div id="btn-solicitar-alocacaowms" class="action-card" style="background: linear-gradient(135deg, #374151 0%, #1f2937 100%); position: relative; cursor: pointer;">
            <button id="btn-solicitar-alocacaowms-real" style="display:none;"
                <?= $galaxia->setVersao(2)
                    ->setConstelacao('Logistica')
                    ->setEstrela('App')
                    ->setSatelite('SolicitarAcessoAlocacaoWms')
                    ->setOpcao('solicitar')
                    ->acaoElemento(''); ?>></button>
            <div class="icon-wrapper" style="background-color: rgba(239, 68, 68, 0.2);"><i class="bi bi-geo-alt-fill" style="color: #f87171;"></i></div>
            <div>
                <h3 class="text-white" style="font-size: 1rem;">Alocação WMS</h3>
                <p style="color: #f87171;">Toque para solicitar acesso</p>
            </div>
        </div>
        <script>
            (function() {
                var btn = document.getElementById('btn-solicitar-alocacaowms');
                if (!btn) return;
                btn.addEventListener('click', function() {
                    if (btn.dataset.processando === '1') return;
                    window.pendenteSolicitacaoAlocacaoWms = true;
                    $('#confirmationModalLabel').text('Solicitar Liberação?');
                    $('#modal-body-text').text('Deseja realmente solicitar a liberação de Alocação WMS?');
                    $('#btn-confirm-action').text('Solicitar').removeClass('btn-danger').addClass('btn-primary');
                    var modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('confirmationModal'));
                    modal.show();
                });
                window.processarUISolicitacaoAlocacaoWms = function() {
                    if (!btn || btn.dataset.processando === '1') return;
                    btn.dataset.processando = '1';
                    btn.style.pointerEvents = 'none';
                    btn.style.cursor = 'not-allowed';
                    btn.style.opacity = '0.85';
                    var iw = btn.querySelector('.icon-wrapper');
                    if (iw) { iw.style.backgroundColor = 'rgba(245, 158, 11, 0.2)'; iw.innerHTML = '<i class="bi bi-hourglass-split icon-pulse" style="color: #fbbf24;"></i>'; }
                    var p = btn.querySelector('p');
                    if (p) { p.textContent = 'Solicitando acesso...'; p.style.color = '#fbbf24'; }
                    var badge = document.createElement('span'); badge.className = 'badge-em-analise'; badge.textContent = 'Em análise'; btn.appendChild(badge);
                    var btnReal = document.getElementById('btn-solicitar-alocacaowms-real');
                    if (btnReal) btnReal.click();
                };
            }());
        </script>
    </div>
</template>
<?php endif; ?>
```

Repetir o mesmo padrão para **Pesagem** (templates `data-tipo-aprovado="ACESSO_PESAGEM"` e `data-tipo-rejeitado="ACESSO_PESAGEM"`) usando o HTML de cada estado correspondente dos blocos if/else já existentes.

Repetir para **Retira** (templates `data-tipo-aprovado="ACESSO_RETIRA"` e `data-tipo-rejeitado="ACESSO_RETIRA"`).

> **Nota:** O conteúdo dos templates é cópia fiel do HTML que o PHP já renderiza para o estado "liberado" e "não-solicitado". Não inventar novo HTML — copiar o existente dos blocos `<?php if ($acessoXxxLiberado):?>` e `<?php else:?>`.

- [ ] **Step 3: Adicionar `lerSocket` e SDK, condicionalmente (só quando há card pendente)**

Identifique se há pelo menos um card pendente. Logo antes do `<footer>` (linha ~729), adicione:

```php
<?php
$haCardPendente = ($solicitacaoWmsPendente && !$acessoWmsLiberado)
               || ($solicitacaoPesagemPendente && !$acessoPesagemLiberado)
               || ($solicitacaoRetiraPendente && !$acessoRetiraLiberado);
$uidParaSocket  = $usuarioLogado->id ?? ($usuarioLogadoPesagem->id ?? ($usuarioLogadoWms->id ?? 0));
?>
<?php if ($haCardPendente): ?>
<div class="hidden socket-container">
    <?= $galaxia->lerSocket('galaxia_logistica_liberacoes_user_' . $uidParaSocket); ?>
</div>
<script type='text/javascript' src='https://cdn.scaledrone.com/scaledrone.min.js'></script>
<?php endif; ?>
```

### 4b — JS de subscrição e swap

- [ ] **Step 4: Adicionar o JS IIFE de subscrição Scaledrone**

Imediatamente após o bloco do Step 3 (ainda dentro do `<?php if ($haCardPendente): ?>`), adicione o script:

```php
<script>
(function () {
    var socketEl = document.querySelector('.rooms-socket[data-room^="galaxia_logistica_liberacoes_user_"]');
    if (!socketEl || typeof Scaledrone === 'undefined') return;

    var channel  = socketEl.dataset.channel;
    var roomName = socketEl.dataset.room;

    var drone;
    try {
        drone = new Scaledrone(channel);
    } catch (e) {
        console.error('[LiberacaoSocket] Erro ao inicializar Scaledrone:', e);
        return;
    }

    drone.on('open', function () {
        var room = drone.subscribe(roomName);

        room.on('message', function (message) {
            try {
                var data = message.data;
                if (!data || data.action !== 'liberacao_atualizada') return;

                var tipo   = data.tipo;   // ex: 'ACESSO_PESAGEM'
                var status = data.status; // 'aprovada' | 'rejeitada'

                // Localizar o card pendente pelo data-tipo
                var cardPendente = document.querySelector('[data-tipo="' + tipo + '"]');
                if (!cardPendente) return;

                // Localizar o template correto
                var attrTemplate = status === 'aprovada'
                    ? '[data-tipo-aprovado="' + tipo + '"]'
                    : '[data-tipo-rejeitado="' + tipo + '"]';
                var tpl = document.querySelector('template' + attrTemplate);
                if (!tpl) return;

                // Encontrar o col-6 pai do card pendente para fazer o swap correto
                var col = cardPendente.closest('.col-6') || cardPendente;
                var novoConteudo = tpl.content.cloneNode(true);
                col.replaceWith(novoConteudo);

                // Desconectar apenas quando não houver mais cards pendentes
                var restantes = document.querySelectorAll('[data-tipo]');
                if (restantes.length === 0) {
                    try { drone.close(); } catch (_) {}
                }
            } catch (e) {
                console.error('[LiberacaoSocket] Erro ao processar mensagem:', e);
            }
        });

        room.on('error', function (err) {
            console.error('[LiberacaoSocket] Erro no room:', err);
        });
    });

    drone.on('error', function (err) {
        console.error('[LiberacaoSocket] Erro de conexão:', err);
    });
}());
</script>
<?php endif; ?>
```

- [ ] **Step 5: Verificar que o `col-6` pai do card pendente tem a classe correta**

O JS usa `.closest('.col-6')` para encontrar o wrapper. Verifique que os 3 cards pendentes estão envolvidos em `<div class="col-6">` (já confirmado na leitura do código). Se algum não estiver, adicione o wrapper.

- [ ] **Step 6: Commit**

```bash
git add componentes/Missoes/Logistica/sinais/App/html/estrutura.php
git commit -m "feat: atualizar card de liberação em tempo real via Scaledrone WebSocket"
```

---

## Task 5: Teste manual de ponta a ponta

- [ ] **Step 1: Abrir o app como operador**

Acesse o `estrutura.php` como usuário que não tem acesso a Pesagem/WMS/Retira. Confirme que os cards mostram "Toque para solicitar acesso".

- [ ] **Step 2: Solicitar uma liberação e abrir DevTools**

No app, solicite acesso à Pesagem (ou qualquer tipo). O card deve mudar para "Em análise / Aguardando aprovação...". Abra o DevTools → Network/Console e confirme que o WebSocket Scaledrone conectou (deve aparecer conexão WebSocket para `scaledrone.com`).

- [ ] **Step 3: Aprovar a solicitação como admin**

Em outra aba/dispositivo (como admin), acesse a gestão de solicitações e aprove. Confirme que o `error_log` do PHP não registrou falha do `LiberacaoSocketNotifier`.

- [ ] **Step 4: Verificar atualização do card no app**

Sem recarregar a página do operador, o card deve automaticamente mudar para o estado liberado (clicável) em ~1 segundo. Se não mudar, cheque o console do browser por erros do `[LiberacaoSocket]`.

- [ ] **Step 5: Testar rejeição**

Repita o fluxo (solicitar → rejeitar como admin) e confirme que o card volta para "Toque para solicitar acesso".

- [ ] **Step 6: Testar os 3 tipos**

Repetir Steps 2–5 para `ACESSO_ALOCACAO_WMS` e `ACESSO_RETIRA`.

- [ ] **Step 7: Commit final (se ajustes menores foram feitos)**

```bash
git add -p  # staging seletivo dos ajustes
git commit -m "fix: ajustes pós-teste manual do swap de cards de liberação"
```
