# Canal OTP "Exibir em tela" 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.
>
> **User policy:** Per `feedback_no_autocommit.md`, **do not run `git commit`/`git push` without the user explicitly approving each commit**. Commit steps below are breakpoints — show the diff to the user and wait for "ok"/"sim"/"commit" before running.

**Goal:** Renomear o canal OTP `teste` → `exibir_tela`, remover o gate `Config::isDev`, fixar o modal "Liberar rota" que sumia, e adicionar botões de copiar pra código/token/link.

**Architecture:** Refator backend (rename de classe + namespace stay, atualizar registry/handler/OpenAPI), seguido de frontend (option HTML, condicional JS, fix do `setTimeout` que fechava o modal independente do canal, copy buttons no padrão Metronic do projeto).

**Tech Stack:** PHP 8.1+ (Galaxia framework), jQuery / Tailwind no frontend (`html/central.php`), test runner CLI próprio com `AssertCT`.

**Spec:** `docs/superpowers/specs/2026-05-08-canal-otp-exibir-tela-design.md`

---

## File Structure

**Renomear:**
- `componentes/Missoes/Mensageria/sinais/Otp/Canais/TesteCanal.php` → `ExibirTelaCanal.php`
- `componentes/Missoes/Plataforma/sinais/Api/Testes/Handlers/CanalTesteIntegrationTest.php` → `CanalExibirTelaIntegrationTest.php`

**Modificar:**
- `componentes/Missoes/Plataforma/sinais/Api/Bootstrap.php` (use + registrar)
- `componentes/Missoes/Plataforma/sinais/Api/Handlers/EdicaoRota/GerarOtpHandler.php` (use + condição + chaves de resposta)
- `componentes/Missoes/Plataforma/sinais/Api/OpenApi/spec.php` (enum + descrição)
- `componentes/Missoes/Logistica/sinais/Central/html/central.php` (option + JS handler + render do painel + auto-close)

---

## Task 1: Atualizar test de integração pra nova semântica (red)

**Files:**
- Move: `componentes/Missoes/Plataforma/sinais/Api/Testes/Handlers/CanalTesteIntegrationTest.php` → `CanalExibirTelaIntegrationTest.php`
- Modify: novo arquivo (substituir conteúdo)

- [ ] **Step 1: Renomear arquivo do teste**

```bash
git mv componentes/Missoes/Plataforma/sinais/Api/Testes/Handlers/CanalTesteIntegrationTest.php \
       componentes/Missoes/Plataforma/sinais/Api/Testes/Handlers/CanalExibirTelaIntegrationTest.php
```

- [ ] **Step 2: Reescrever conteúdo do teste**

Substituir o conteúdo de `CanalExibirTelaIntegrationTest.php` por:

```php
<?php

/**
 * Teste de integração — canal "exibir_tela" no GerarOtpHandler
 *
 * Cobre:
 *   - canal=exibir_tela + user dev (id=1)    → response devolve codigo_exibido
 *   - canal=exibir_tela + user não-dev (id=99) → response devolve codigo_exibido
 *     (gate Config::isDev foi removido — qualquer JWT autenticado vê o código)
 *   - canal=log + user dev → response NÃO devolve codigo_exibido
 *   - Persistência: canal salvo em verificacoes_otp.canal == 'exibir_tela'
 */

if (php_sapi_name() !== 'cli') {
    die("Apenas via CLI\n");
}

$_SERVER['HTTP_HOST']      = 'dev-genesis.goldieapp.com.br';
$_SERVER['REQUEST_METHOD'] = 'POST';

require_once __DIR__ . '/../../../../../../../autoload.php';

if (session_status() === PHP_SESSION_NONE) {
    @session_start();
}
$info = new stdClass();
$info->userData         = (object) ['id' => 1, 'username' => 'integ_test'];
$info->customerData     = (object) ['id' => 7];
$info->principalEmpresa = (object) ['empresa_id' => 3];
$info->sectorData       = (object) ['id' => 1, 'inteligencia' => 'dev_gx_goldie'];
$_SESSION['galaxiaUserInfo']              = $info;
$_SESSION['dashboard']['galaxiaUserInfo'] = $info;

if (!defined('GENESIS_API_DEV_USERS')) {
    define('GENESIS_API_DEV_USERS', '1');
}

use Galaxia\_Controladores\Connect;
use GalaxiaMissoes\Logistica\sinais\Edicaorota\Callbacks\EdicaoRotaCallbackInterface;
use GalaxiaMissoes\Logistica\sinais\Edicaorota\Callbacks\EdicaoRotaCallbackRegistry;
use GalaxiaMissoes\Plataforma\sinais\Api\Bootstrap as ApiBootstrap;
use GalaxiaMissoes\Plataforma\sinais\Api\Handlers\EdicaoRota\CriarHandler as ErCriarHandler;
use GalaxiaMissoes\Plataforma\sinais\Api\Handlers\EdicaoRota\GerarOtpHandler;
use GalaxiaMissoes\Plataforma\sinais\Api\Request as ApiRequest;
use GalaxiaMissoes\Plataforma\sinais\Api\Response as ApiResponse;

class CallbackNopExibirTela implements EdicaoRotaCallbackInterface
{
    public function aoAprovar(object $edicao): bool { return true; }
    public function aoRejeitar(object $edicao): bool { return true; }
}

class AssertCT
{
    public static int $passou = 0;
    public static int $falhas = 0;
    public static function true(bool $cond, string $msg): void
    {
        if ($cond) { self::$passou++; echo "[PASS] {$msg}\n"; }
        else        { self::$falhas++; echo "[FAIL] {$msg}\n"; }
    }
}

$pdo = (new Connect)->getInstance(null, null, 'dev_gx_goldie');
if ($pdo->query("SELECT current_database()")->fetchColumn() !== 'dev_gx_goldie') {
    fwrite(STDERR, "❌ Não conectou em dev_gx_goldie\n"); exit(1);
}

$callbackModulo = 'integ_canal_exibir_tela';
$pdo->prepare("DELETE FROM edicoes_rota WHERE callback_modulo = :m")->execute([':m' => $callbackModulo]);
$pdo->prepare("DELETE FROM verificacoes_otp_eventos WHERE verificacao_otp_id IN (SELECT id FROM verificacoes_otp WHERE contexto = 'edicao_rota')")->execute();
$pdo->prepare("DELETE FROM verificacoes_otp WHERE contexto = 'edicao_rota'")->execute();

EdicaoRotaCallbackRegistry::reset();
EdicaoRotaCallbackRegistry::registrar($callbackModulo, CallbackNopExibirTela::class);
ApiBootstrap::ensure(); // garante que canal 'exibir_tela' está registrado

$buildReq = function (string $method, string $pattern, array $body, array $params, string $userId): ApiRequest {
    $req = new ApiRequest();
    $req->method       = $method;
    $req->routePattern = $pattern;
    $req->body         = $body;
    $req->routeParams  = $params;
    $req->ip           = '127.0.0.1';
    $req->userAgent    = 'integ-test';
    $req->requestId    = 'req-' . uniqid();
    $req->jwtClaims    = ['user' => $userId, 'customer' => '7'];
    return $req;
};
$exec = function (string $handlerClass, ApiRequest $req): array {
    $res = new ApiResponse();
    (new $handlerClass())->handle($req, $res);
    return [$res->status, $res->body];
};

echo "====================================================\n";
echo "INTEGRATION — Canal 'exibir_tela' (sem gate dev)\n";
echo "====================================================\n\n";

$rotaAtual = [['id' => 'p1'], ['id' => 'p2']];

try {
    [, $b1] = $exec(ErCriarHandler::class, $buildReq(
        'POST', '/api/v1/edicao-rota',
        ['callback_modulo' => $callbackModulo, 'callback_referencia_id' => 1, 'telefone_motorista' => '11111111111', 'rota_atual' => $rotaAtual],
        [], '1'
    ));
    $token1 = $b1['data']['token'];

    [, $b2] = $exec(ErCriarHandler::class, $buildReq(
        'POST', '/api/v1/edicao-rota',
        ['callback_modulo' => $callbackModulo, 'callback_referencia_id' => 2, 'telefone_motorista' => '22222222222', 'rota_atual' => $rotaAtual],
        [], '1'
    ));
    $token2 = $b2['data']['token'];

    [, $b3] = $exec(ErCriarHandler::class, $buildReq(
        'POST', '/api/v1/edicao-rota',
        ['callback_modulo' => $callbackModulo, 'callback_referencia_id' => 3, 'telefone_motorista' => '33333333333', 'rota_atual' => $rotaAtual],
        [], '1'
    ));
    $token3 = $b3['data']['token'];

    // ---------- 1. canal=exibir_tela + user dev (id=1) → expõe codigo_exibido ----------
    [$st, $body] = $exec(GerarOtpHandler::class, $buildReq(
        'POST', '/api/v1/edicao-rota/{token}/otp/gerar',
        ['canal' => 'exibir_tela', 'destinatario' => '11111111111'],
        ['token' => $token1], '1'
    ));
    AssertCT::true($st === 200, 'canal=exibir_tela + dev: HTTP 200');
    AssertCT::true(
        isset($body['data']['codigo_exibido']) && preg_match('/^\d{6}$/', (string) $body['data']['codigo_exibido']) === 1,
        'canal=exibir_tela + dev: data.codigo_exibido tem 6 dígitos'
    );
    AssertCT::true(
        isset($body['data']['codigo_exibido_dest']) && $body['data']['codigo_exibido_dest'] === '11111111111',
        'canal=exibir_tela + dev: codigo_exibido_dest bate com destinatário'
    );

    // ---------- 2. canal=exibir_tela + user NÃO dev (id=99) → expõe igual ----------
    [$st, $body] = $exec(GerarOtpHandler::class, $buildReq(
        'POST', '/api/v1/edicao-rota/{token}/otp/gerar',
        ['canal' => 'exibir_tela', 'destinatario' => '22222222222'],
        ['token' => $token2], '99'
    ));
    AssertCT::true($st === 200, 'canal=exibir_tela + não-dev: HTTP 200');
    AssertCT::true(
        isset($body['data']['codigo_exibido']) && preg_match('/^\d{6}$/', (string) $body['data']['codigo_exibido']) === 1,
        'canal=exibir_tela + não-dev: data.codigo_exibido EXPOSTO (gate removido)'
    );

    // ---------- 3. canal=log + user dev → não expõe ----------
    [$st, $body] = $exec(GerarOtpHandler::class, $buildReq(
        'POST', '/api/v1/edicao-rota/{token}/otp/gerar',
        ['canal' => 'log', 'destinatario' => '33333333333'],
        ['token' => $token3], '1'
    ));
    AssertCT::true($st === 200, 'canal=log + dev: HTTP 200');
    AssertCT::true(
        !isset($body['data']['codigo_exibido']),
        'canal=log + dev: codigo_exibido NÃO exposto (canal != exibir_tela)'
    );

    // ---------- 4. Persistência: canal salvo como 'exibir_tela' ----------
    $stmt = $pdo->prepare("SELECT canal FROM verificacoes_otp WHERE referencia_externa = :tk ORDER BY id DESC LIMIT 1");
    $stmt->execute([':tk' => $token1]);
    $canalPersistido = $stmt->fetchColumn();
    AssertCT::true($canalPersistido === 'exibir_tela', "OTP persistido com canal=exibir_tela no banco (got '{$canalPersistido}')");

} finally {
    $pdo->prepare("DELETE FROM edicoes_rota WHERE callback_modulo = :m")->execute([':m' => $callbackModulo]);
    $pdo->prepare("DELETE FROM verificacoes_otp_eventos WHERE verificacao_otp_id IN (SELECT id FROM verificacoes_otp WHERE contexto = 'edicao_rota')")->execute();
    $pdo->prepare("DELETE FROM verificacoes_otp WHERE contexto = 'edicao_rota'")->execute();
    EdicaoRotaCallbackRegistry::reset();
}

echo "\n====================================================\n";
echo "RESULTADO: PASS=" . AssertCT::$passou . "  FAIL=" . AssertCT::$falhas . "\n";
if (AssertCT::$falhas > 0) exit(1);
echo "TODOS OS TESTES PASSARAM\n";
```

- [ ] **Step 3: Rodar o teste pra confirmar que falha (red)**

Run:
```bash
php componentes/Missoes/Plataforma/sinais/Api/Testes/Handlers/CanalExibirTelaIntegrationTest.php
```

Expected: FAIL — porque o canal `'exibir_tela'` ainda não está registrado (`OtpCanalRegistry` só conhece `'teste'`). O `CriarOtpLaboratorio` deve lançar erro ou retornar status != 200, e/ou os asserts de `codigo_exibido` falham.

Output esperado terá `[FAIL]` em vários asserts. Capturar a saída pra confirmar que está vermelho pelo motivo certo (canal não registrado), não por outro bug.

- [ ] **Step 4: Pause — aguardar usuário aprovar antes de commitar**

Mostrar diff do test:
```bash
git diff --staged componentes/Missoes/Plataforma/sinais/Api/Testes/Handlers/
```

Aguardar "ok" do usuário. Quando aprovado:

```bash
git add componentes/Missoes/Plataforma/sinais/Api/Testes/Handlers/CanalExibirTelaIntegrationTest.php
git rm componentes/Missoes/Plataforma/sinais/Api/Testes/Handlers/CanalTesteIntegrationTest.php 2>/dev/null || true
git commit -m "test(api/edicao-rota): expect canal=exibir_tela without dev gate (red)"
```

---

## Task 2: Renomear canal no backend (PHP) — fica verde

**Files:**
- Move: `componentes/Missoes/Mensageria/sinais/Otp/Canais/TesteCanal.php` → `ExibirTelaCanal.php`
- Modify: `componentes/Missoes/Plataforma/sinais/Api/Bootstrap.php` (L.12, L.63-69)
- Modify: `componentes/Missoes/Plataforma/sinais/Api/Handlers/EdicaoRota/GerarOtpHandler.php` (use + handler)
- Modify: `componentes/Missoes/Plataforma/sinais/Api/OpenApi/spec.php` (L.299, L.305, L.314)

- [ ] **Step 1: Mover e renomear classe `TesteCanal` → `ExibirTelaCanal`**

```bash
git mv componentes/Missoes/Mensageria/sinais/Otp/Canais/TesteCanal.php \
       componentes/Missoes/Mensageria/sinais/Otp/Canais/ExibirTelaCanal.php
```

Substituir conteúdo de `ExibirTelaCanal.php`:

```php
<?php

namespace GalaxiaMissoes\Mensageria\sinais\Otp\Canais;

/**
 * Canal "exibir em tela".
 *
 * Não envia o OTP por nenhum provedor externo — captura o código em uma
 * variável estática (acessível pelo handler da API dentro da mesma requisição)
 * pra que o caller exiba na UI e o atendente repasse manualmente ao destinatário
 * (ex.: motorista chegando no balcão sem celular acessível na hora).
 *
 * Uso na API: o consumidor opta explicitamente passando `canal='exibir_tela'`
 * no body. Não é canal default — só vaza código quando explicitamente pedido.
 */
class ExibirTelaCanal implements OtpCanalInterface
{
    public static ?string $ultimoCodigo       = null;
    public static ?string $ultimoDestinatario = null;
    public static array   $ultimoContexto     = [];

    public function nome(): string
    {
        return 'exibir_tela';
    }

    public function validaDestinatario(string $destinatario): bool
    {
        return trim($destinatario) !== '';
    }

    public function enviar(string $destinatario, string $codigo, array $contextoExtra = []): bool
    {
        self::$ultimoCodigo       = $codigo;
        self::$ultimoDestinatario = $destinatario;
        self::$ultimoContexto     = $contextoExtra;
        error_log("[Otp][ExibirTelaCanal] codigo capturado para dest={$destinatario}");
        return true;
    }

    public static function reset(): void
    {
        self::$ultimoCodigo = null;
        self::$ultimoDestinatario = null;
        self::$ultimoContexto = [];
    }
}
```

- [ ] **Step 2: Atualizar `Bootstrap.php`**

Em `componentes/Missoes/Plataforma/sinais/Api/Bootstrap.php`:

Linha 12 — trocar:
```php
use GalaxiaMissoes\Mensageria\sinais\Otp\Canais\TesteCanal;
```
por:
```php
use GalaxiaMissoes\Mensageria\sinais\Otp\Canais\ExibirTelaCanal;
```

Linhas 63-69 — substituir o bloco:
```php
        // Canal "teste" — captura o código em estática pra surfacing na UI
        // (gated por Config::isDev no handler).
        if (class_exists(OtpCanalRegistry::class) && class_exists(TesteCanal::class)) {
            if (!OtpCanalRegistry::temRegistro('teste')) {
                OtpCanalRegistry::registrar('teste', TesteCanal::class);
            }
        }
```
por:
```php
        // Canal "exibir_tela" — captura o código em estática pra surfacing
        // na UI; o atendente repassa manualmente ao destinatário.
        if (class_exists(OtpCanalRegistry::class) && class_exists(ExibirTelaCanal::class)) {
            if (!OtpCanalRegistry::temRegistro('exibir_tela')) {
                OtpCanalRegistry::registrar('exibir_tela', ExibirTelaCanal::class);
            }
        }
```

- [ ] **Step 3: Atualizar `GerarOtpHandler.php` — rename, remove dev gate, rename response keys**

Substituir o conteúdo completo de `componentes/Missoes/Plataforma/sinais/Api/Handlers/EdicaoRota/GerarOtpHandler.php` por:

```php
<?php

namespace GalaxiaMissoes\Plataforma\sinais\Api\Handlers\EdicaoRota;

use GalaxiaMissoes\Logistica\sinais\Edicaorota\Laboratorios\GerarOtpLaboratorio;
use GalaxiaMissoes\Mensageria\sinais\Otp\Canais\ExibirTelaCanal;
use GalaxiaMissoes\Plataforma\sinais\Api\Handlers\BaseHandler;
use GalaxiaMissoes\Plataforma\sinais\Api\Request;
use GalaxiaMissoes\Plataforma\sinais\Api\Response;

/**
 * POST /api/v1/edicao-rota/{token}/otp/gerar
 *
 * body opcional: canal (whatsapp|email|sms|log|exibir_tela). Quando omitido,
 * usa o canal_padrao do contexto 'edicao_rota'.
 *
 * Quando canal=exibir_tela, a resposta inclui `codigo_exibido` em claro
 * (capturado por ExibirTelaCanal) pra que o atendente repasse manualmente
 * ao destinatário. Não há gate de role — basta JWT válido. Quem não quer
 * vazamento, simplesmente não passa canal=exibir_tela.
 */
class GerarOtpHandler extends BaseHandler
{
    public function handle(Request $req, Response $res): void
    {
        $token = (string) ($req->routeParams['token'] ?? '');
        if ($token === '') {
            [$body, $status] = $this->erro($req, 'invalid_input', 'Token público obrigatório.', 422);
            $res->json($body, $status);
            return;
        }

        $canal = isset($req->body['canal']) ? (string) $req->body['canal'] : null;

        ExibirTelaCanal::reset();
        $resposta = (new GerarOtpLaboratorio($token, $req->ip, $req->userAgent, $canal))->response;

        // Surfacing do código quando o consumidor escolheu explicitamente
        // o canal exibir_tela (caller é responsável por garantir que faz sentido
        // expor — qualquer JWT válido recebe).
        if ($canal === 'exibir_tela' && ExibirTelaCanal::$ultimoCodigo !== null) {
            $resposta['codigo_exibido']      = ExibirTelaCanal::$ultimoCodigo;
            $resposta['codigo_exibido_dest'] = ExibirTelaCanal::$ultimoDestinatario;
        }

        [$body, $status] = $this->adaptarRespostaLegacy($resposta, $req);
        $res->json($body, $status);
    }
}
```

- [ ] **Step 4: Atualizar `OpenApi/spec.php`**

Em `componentes/Missoes/Plataforma/sinais/Api/OpenApi/spec.php`:

Linha 299 — trocar:
```php
'enum'        => ['whatsapp', 'email', 'sms', 'log', 'teste'],
```
por:
```php
'enum'        => ['whatsapp', 'email', 'sms', 'log', 'exibir_tela'],
```

Linhas 300-305 — substituir a `description` por:
```php
'description' => 'Provedor de envio. '
    . '`whatsapp` usa Evolution API quando configurada — destinatário vem de telefone_motorista (cai em `log` se Evolution não configurada). '
    . '`email` usa SMTP por customer/company quando habilitado — destinatário vem de identificador_motorista, que precisa ser email válido (a edição falha aqui se o gestor não preencheu o email no momento da criação). '
    . '`sms` ainda é stub `log`. '
    . '`log` escreve em error_log (dev). '
    . '`exibir_tela` captura o código em estática — resposta inclui `codigo_exibido` em claro pra qualquer JWT autenticado, pra que o atendente repasse manualmente ao destinatário.',
```

Linha 314 — trocar:
```php
'description' => 'OTP enviado. Quando canal=teste e dev autorizado, resposta inclui `codigo_teste` em claro.',
```
por:
```php
'description' => 'OTP enviado. Quando canal=exibir_tela, resposta inclui `codigo_exibido` (e `codigo_exibido_dest`) em claro.',
```

- [ ] **Step 5: Rodar test de integração — deve passar**

Run:
```bash
php componentes/Missoes/Plataforma/sinais/Api/Testes/Handlers/CanalExibirTelaIntegrationTest.php
```

Expected output:
```
[PASS] canal=exibir_tela + dev: HTTP 200
[PASS] canal=exibir_tela + dev: data.codigo_exibido tem 6 dígitos
[PASS] canal=exibir_tela + dev: codigo_exibido_dest bate com destinatário
[PASS] canal=exibir_tela + não-dev: HTTP 200
[PASS] canal=exibir_tela + não-dev: data.codigo_exibido EXPOSTO (gate removido)
[PASS] canal=log + dev: HTTP 200
[PASS] canal=log + dev: codigo_exibido NÃO exposto (canal != exibir_tela)
[PASS] OTP persistido com canal=exibir_tela no banco (got 'exibir_tela')
RESULTADO: PASS=8  FAIL=0
TODOS OS TESTES PASSARAM
```

Se algum FAIL — investigar antes de continuar (não pular).

- [ ] **Step 6: Sondar grep de qualquer referência residual a `TesteCanal` ou `'teste'` (canal)**

```bash
grep -rn "TesteCanal" componentes/Missoes/Mensageria componentes/Missoes/Plataforma 2>/dev/null
grep -rn "'teste'" componentes/Missoes/Plataforma/sinais/Api 2>/dev/null
```

Expected: zero ocorrências em código de produção (só em tasks/*.md de histórico, que ficam como registro). Se aparecer algo em código `.php` que não seja Testes/, atualizar antes de prosseguir.

- [ ] **Step 7: Pause — aguardar usuário aprovar antes de commitar**

Mostrar diff:
```bash
git status
git diff --staged
git diff
```

Aguardar "ok". Quando aprovado:

```bash
git add componentes/Missoes/Mensageria/sinais/Otp/Canais/ExibirTelaCanal.php
git rm componentes/Missoes/Mensageria/sinais/Otp/Canais/TesteCanal.php 2>/dev/null || true
git add componentes/Missoes/Plataforma/sinais/Api/Bootstrap.php
git add componentes/Missoes/Plataforma/sinais/Api/Handlers/EdicaoRota/GerarOtpHandler.php
git add componentes/Missoes/Plataforma/sinais/Api/OpenApi/spec.php
git commit -m "refactor(otp): rename canal teste -> exibir_tela, drop Config::isDev gate"
```

---

## Task 3: Atualizar `central.php` — option, JS handler, kill dev pre-fill

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Central/html/central.php` (L.2494, L.3998-4007, L.4067)

- [ ] **Step 1: Trocar `<option>` no select de canal**

Em `componentes/Missoes/Logistica/sinais/Central/html/central.php`, linha 2494:

Trocar:
```html
                        <option value="teste">Teste — mostra o código (dev)</option>
```
por:
```html
                        <option value="exibir_tela">Exibir em tela — mostra código, token e link</option>
```

- [ ] **Step 2: Trocar handler do `change` no select e remover o pre-fill de dev**

Em `central.php`, linhas 3998-4007 (bloco `$(document).on('change', '#liberar-motorista-canal', ...)`).

Trocar:
```js
    // Quando o canal muda para "teste", pré-preenche dados fictícios pra dev
    // simular a chamada sem ter que digitar nada.
    $(document).on('change', '#liberar-motorista-canal', function () {
        if ($(this).val() === 'teste') {
            const $tel = $('#liberar-motorista-telefone');
            const $email = $('#liberar-motorista-email');
            if (!$tel.val()) $tel.val('+5511999990000');
            if (!$email.val()) $email.val('motorista.teste@example.com');
        }
    });
```
por (deletar o handler — não há mais pre-fill útil; o atendente preenche dados reais do motorista):
```js
    // Canal 'exibir_tela' não tem pre-fill: o atendente preenche os dados
    // reais do motorista normalmente. O efeito do canal é só na exibição
    // do código/token/link após o submit (ver branch dentro do confirmar).
```

- [ ] **Step 3: Trocar a condicional do painel de exibição (`canal === 'teste'`) e ler `codigo_exibido`**

Em `central.php`, linhas 4064-4088. Trocar o bloco:

```js
            // Modo teste: o backend devolve o código em claro pra desenvolvedores.
            // Mostramos token + código + link da tela do motorista no próprio modal.
            const codigoTeste = gerarResp?.data?.codigo_teste;
            if (canal === 'teste' && codigoTeste) {
                const linkMotorista = `${window.location.origin}/missao/Logistica/edicaorota/ver_status?token=${encodeURIComponent(token)}`;
                $feedback.removeClass('hidden bg-green-900/30 text-green-300 bg-red-900/30 text-red-300')
                    .addClass('bg-green-900/30 text-green-300')
                    .html(`
                        <div class="space-y-2">
                            <div><strong>Edição #${edicaoId}</strong> — modo teste</div>
                            <div class="text-xs">
                                <div><span class="opacity-70">Código:</span> <code class="bg-black/30 px-2 py-0.5 rounded text-yellow-300">${codigoTeste}</code></div>
                                <div class="mt-1"><span class="opacity-70">Token:</span> <code class="bg-black/30 px-2 py-0.5 rounded break-all">${token}</code></div>
                                <div class="mt-1">
                                    <span class="opacity-70">Link do motorista:</span><br>
                                    <a href="${linkMotorista}" target="_blank" class="underline text-blue-300 break-all">${linkMotorista}</a>
                                </div>
                                <div class="mt-2 opacity-70 text-[11px]">
                                    Abra o link em outra aba pra simular o motorista. (A tela do motorista
                                    será conectada à API na próxima fase — por enquanto pode usar /api/v1/docs
                                    com o token e o código acima.)
                                </div>
                            </div>
                        </div>
                    `);
            } else {
                mostrarFeedback($feedback, 'ok', `Solicitação enviada (edição #${edicaoId}).`);
            }
```

por (Task 5 vai adicionar copy buttons depois — esse step só renomeia):

```js
            // Canal 'exibir_tela': backend devolve código em claro pra que o
            // atendente repasse manualmente ao destinatário. Mostramos código,
            // token e link no próprio modal.
            const codigoExibido = gerarResp?.data?.codigo_exibido;
            if (canal === 'exibir_tela' && codigoExibido) {
                const linkMotorista = `${window.location.origin}/missao/Logistica/edicaorota/ver_status?token=${encodeURIComponent(token)}`;
                $feedback.removeClass('hidden bg-green-900/30 text-green-300 bg-red-900/30 text-red-300')
                    .addClass('bg-green-900/30 text-green-300')
                    .html(`
                        <div class="space-y-2">
                            <div><strong>Edição #${edicaoId}</strong> — exibir em tela</div>
                            <div class="text-xs">
                                <div><span class="opacity-70">Código:</span> <code class="bg-black/30 px-2 py-0.5 rounded text-yellow-300">${codigoExibido}</code></div>
                                <div class="mt-1"><span class="opacity-70">Token:</span> <code class="bg-black/30 px-2 py-0.5 rounded break-all">${token}</code></div>
                                <div class="mt-1">
                                    <span class="opacity-70">Link do motorista:</span><br>
                                    <a href="${linkMotorista}" target="_blank" class="underline text-blue-300 break-all">${linkMotorista}</a>
                                </div>
                                <div class="mt-2 opacity-70 text-[11px]">
                                    Copie e repasse os dados ao motorista. O modal não fecha sozinho — feche em "Cancelar"/×.
                                </div>
                            </div>
                        </div>
                    `);
            } else {
                mostrarFeedback($feedback, 'ok', `Solicitação enviada (edição #${edicaoId}).`);
            }
```

- [ ] **Step 4: Smoke manual no browser**

Subir o ambiente local (ou apontar pra dev), abrir a Central, abrir o modal "Liberar rota":
- Verificar que o select agora mostra "Exibir em tela — mostra código, token e link"
- Selecionar "Exibir em tela" — confirmar que **não** pré-preenche telefone/email
- Selecionar outros canais (whatsapp/email/sms) — comportamento normal preservado

Se algo quebrar, voltar e ajustar antes de seguir.

- [ ] **Step 5: Pause — aguardar usuário aprovar antes de commitar**

Mostrar diff:
```bash
git diff componentes/Missoes/Logistica/sinais/Central/html/central.php
```

Aguardar "ok". Quando aprovado:

```bash
git add componentes/Missoes/Logistica/sinais/Central/html/central.php
git commit -m "refactor(central): rename canal teste -> exibir_tela in liberar-motorista modal"
```

---

## Task 4: Fix do auto-close + copy buttons no painel "Exibir em tela"

**Files:**
- Modify: `componentes/Missoes/Logistica/sinais/Central/html/central.php` (render do painel + L.4099)

- [ ] **Step 1: Adicionar copy buttons no HTML do painel**

No mesmo bloco que foi tocado no Task 3 step 3, substituir o `.html(...)` pra incluir botões "Copiar":

```js
            const codigoExibido = gerarResp?.data?.codigo_exibido;
            if (canal === 'exibir_tela' && codigoExibido) {
                const linkMotorista = `${window.location.origin}/missao/Logistica/edicaorota/ver_status?token=${encodeURIComponent(token)}`;
                $feedback.removeClass('hidden bg-green-900/30 text-green-300 bg-red-900/30 text-red-300')
                    .addClass('bg-green-900/30 text-green-300')
                    .html(`
                        <div class="space-y-2">
                            <div><strong>Edição #${edicaoId}</strong> — exibir em tela</div>
                            <div class="text-xs space-y-2">
                                <div class="flex items-center gap-2">
                                    <span class="opacity-70 w-16 shrink-0">Código:</span>
                                    <code class="bg-black/30 px-2 py-0.5 rounded text-yellow-300 flex-1">${codigoExibido}</code>
                                    <button type="button"
                                            class="g-botao g-botao-contorno text-[11px] py-0.5 px-2 copiar-exibir-tela"
                                            data-copiar-valor="${codigoExibido}"
                                            data-copiar-rotulo="Código">
                                        <i class="fas fa-copy mr-1"></i>Copiar
                                    </button>
                                </div>
                                <div class="flex items-center gap-2">
                                    <span class="opacity-70 w-16 shrink-0">Token:</span>
                                    <code class="bg-black/30 px-2 py-0.5 rounded break-all flex-1">${token}</code>
                                    <button type="button"
                                            class="g-botao g-botao-contorno text-[11px] py-0.5 px-2 copiar-exibir-tela"
                                            data-copiar-valor="${token}"
                                            data-copiar-rotulo="Token">
                                        <i class="fas fa-copy mr-1"></i>Copiar
                                    </button>
                                </div>
                                <div class="flex items-start gap-2">
                                    <span class="opacity-70 w-16 shrink-0 pt-0.5">Link:</span>
                                    <a href="${linkMotorista}" target="_blank" class="underline text-blue-300 break-all flex-1">${linkMotorista}</a>
                                    <button type="button"
                                            class="g-botao g-botao-contorno text-[11px] py-0.5 px-2 copiar-exibir-tela"
                                            data-copiar-valor="${linkMotorista}"
                                            data-copiar-rotulo="Link">
                                        <i class="fas fa-copy mr-1"></i>Copiar
                                    </button>
                                </div>
                                <div class="opacity-70 text-[11px] pt-1">
                                    Copie e repasse os dados ao motorista. O modal não fecha sozinho — feche em "Cancelar"/×.
                                </div>
                            </div>
                        </div>
                    `);
            } else {
                mostrarFeedback($feedback, 'ok', `Solicitação enviada (edição #${edicaoId}).`);
            }
```

- [ ] **Step 2: Adicionar handler global pro botão `.copiar-exibir-tela`**

Logo após o handler `mostrarFeedback` (ou junto dos outros `$(document).on(...)` da seção, ~L.4111), adicionar:

```js
    // Handler dos botões "Copiar" do painel exibir_tela.
    // Usa Clipboard API moderna; fallback pra execCommand em browsers antigos.
    $(document).on('click', '.copiar-exibir-tela', async function () {
        const $btn   = $(this);
        const valor  = $btn.attr('data-copiar-valor') || '';
        const rotulo = $btn.attr('data-copiar-rotulo') || '';
        if (!valor) return;

        let ok = false;
        try {
            if (navigator.clipboard && window.isSecureContext) {
                await navigator.clipboard.writeText(valor);
                ok = true;
            } else {
                const ta = document.createElement('textarea');
                ta.value = valor;
                ta.setAttribute('readonly', '');
                ta.style.position = 'absolute';
                ta.style.left = '-9999px';
                document.body.appendChild(ta);
                ta.select();
                ok = document.execCommand('copy');
                document.body.removeChild(ta);
            }
        } catch (e) {
            console.warn('[exibir-tela] copiar falhou:', e);
            ok = false;
        }

        const htmlOriginal = $btn.html();
        $btn.html(ok
            ? `<i class="fas fa-check mr-1"></i>${rotulo} copiado`
            : `<i class="fas fa-times mr-1"></i>Falhou`);
        setTimeout(() => $btn.html(htmlOriginal), 1500);
    });
```

- [ ] **Step 3: Fix do auto-close — só fecha quando canal != 'exibir_tela'**

Em `central.php`, linha 4099. Trocar:

```js
            setTimeout(() => $('#liberar-motorista-modal').addClass('hidden'), 1200);
```

por:

```js
            // No modo 'exibir_tela' o atendente precisa ler/copiar código,
            // token e link antes de fechar. Não auto-close nesse caso —
            // o usuário fecha em "Cancelar"/×.
            if (canal !== 'exibir_tela') {
                setTimeout(() => $('#liberar-motorista-modal').addClass('hidden'), 1200);
            }
```

- [ ] **Step 4: Smoke manual no browser**

Cenário 1 — exibir_tela:
- Abrir modal "Liberar rota", selecionar "Exibir em tela", preencher dados, confirmar.
- Verificar que aparece o painel verde com Código (6 dígitos) / Token / Link, cada um com botão "Copiar".
- Clicar em cada "Copiar" — confirmar que: (a) o conteúdo vai pro clipboard, (b) o botão muda pra "Código copiado" / "Token copiado" / "Link copiado" por ~1.5s.
- **Confirmar que o modal NÃO fecha sozinho** depois de 1.2s. Modal só fecha clicando em × ou "Cancelar".

Cenário 2 — outros canais:
- Trocar pra "WhatsApp" (ou outro), confirmar.
- Modal mostra feedback breve "Solicitação enviada (edição #N)." e fecha em ~1.2s. Comportamento atual preservado.

Se quebrar, voltar e ajustar.

- [ ] **Step 5: Pause — aguardar usuário aprovar antes de commitar**

Mostrar diff:
```bash
git diff componentes/Missoes/Logistica/sinais/Central/html/central.php
```

Aguardar "ok". Quando aprovado:

```bash
git add componentes/Missoes/Logistica/sinais/Central/html/central.php
git commit -m "feat(central): keep liberar-motorista modal open on exibir_tela + copy buttons"
```

---

## Task 5: Verificação final + cleanup

**Files:** nenhum direto — sondagem.

- [ ] **Step 1: Re-rodar test de integração**

```bash
php componentes/Missoes/Plataforma/sinais/Api/Testes/Handlers/CanalExibirTelaIntegrationTest.php
```

Expected: `RESULTADO: PASS=8  FAIL=0`.

- [ ] **Step 2: Sondar referências esquecidas**

```bash
grep -rn "TesteCanal\|codigo_teste" componentes/Missoes/Mensageria componentes/Missoes/Plataforma componentes/Missoes/Logistica/sinais/Edicaorota componentes/Missoes/Logistica/sinais/Central 2>/dev/null | grep -v "tasks/" | grep -v "\.md:"
```

Expected: zero linhas em código `.php`/`.js`. Se aparecer alguma:
- Em `tasks/*.md` ou `PRD/`: ignorar (histórico).
- Em código vivo: voltar e atualizar antes de fechar.

```bash
grep -rn "'teste'" componentes/Missoes/Plataforma/sinais/Api 2>/dev/null | grep -v "tasks/"
```

Expected: zero linhas.

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

Abrir `/api/v1/docs` (com user dev autenticado) e conferir:
- Endpoint `POST /api/v1/edicao-rota/{token}/otp/gerar` mostra enum com `exibir_tela` (não `teste`).
- Descrição da response 200 menciona `codigo_exibido`.

Se a página não estiver acessível em dev local, basta validar visualmente o conteúdo gerado por `OpenApi/spec.php`.

- [ ] **Step 4: Smoke final no browser — fluxo completo**

- Abrir Central, selecionar uma rota, abrir "Liberar rota para motorista".
- Selecionar "Exibir em tela", preencher, confirmar.
- Copiar código e token, abrir o link em outra aba.
- Confirmar que a tela do motorista carrega com o token (a integração além disso já existe — só estamos validando que o fluxo de geração + repasse manual funciona).
- Voltar pro modal, fechar manualmente.

- [ ] **Step 5: Atualizar memory do projeto se necessário**

Não esperado — esse é um fix/refactor pontual, não introduz padrão novo. Se durante implementação surgir algo surpreendente (ex.: descobrir uma convenção do canal que não estava documentada), salvar via memory conforme regras de auto-memory.

---

## Self-Review (notas do plano)

- **Spec coverage**: cada decisão do spec mapeia pra uma task:
  - Rename canal/classe → Task 2
  - Remoção do gate dev → Task 2 step 3
  - Rename de chaves resposta → Task 2 step 3
  - OpenAPI → Task 2 step 4
  - Fix auto-close → Task 4 step 3
  - Copy buttons → Task 4 steps 1-2
  - Test refactor → Task 1
  - Critérios de aceite → cobertos pelos smoke tests (Task 3 step 4, Task 4 step 4) e Task 5
- **Placeholder scan**: nenhum TBD/TODO/"add error handling" sem código.
- **Type consistency**: `ExibirTelaCanal::$ultimoCodigo` é a mesma propriedade estática que `TesteCanal::$ultimoCodigo` — só renomeada. Não há divergência entre tasks. `codigo_exibido` / `codigo_exibido_dest` consistente em backend + test + frontend.
- **i18n**: spec deixou em aberto "verificar antes de adicionar". Plano mantém label hardcoded em PT-BR (consistente com o resto do modal `liberar-motorista` que também é hardcoded). Se i18n for necessário, é escopo separado.
