From 9d3c84064f3a0fdc531f501dcfaa607f1a6bc8ed Mon Sep 17 00:00:00 2001 From: Matheus Date: Mon, 31 Aug 2026 13:11:03 -0300 Subject: [PATCH] =?UTF-8?q?fix(ui):=20bot=C3=A3o=20de=20copiar=20senha=20n?= =?UTF-8?q?=C3=A3o=20fazia=20nada=20em=20HTTP=20puro=20(fora=20de=20localh?= =?UTF-8?q?ost)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit navigator.clipboard só existe em contexto seguro (HTTPS ou localhost) — este ambiente é HTTP puro, acessado pelo IP real da VM. SecretReveal (usado na criação de ramal e em "ver senha atual") chamava navigator.clipboard.writeText sem tratamento de erro, então em contexto inseguro o botão simplesmente não fazia nada, sem nenhum aviso. Fix: fallback pra document.execCommand("copy") (textarea temporário) quando a Clipboard API moderna não existe ou falha, funciona em qualquer contexto. Se as duas formas falharem, mostra aviso pra copiar manualmente em vez de falhar em silêncio. Testado com Playwright acessando o IP real da VM (não localhost, pra reproduzir o contexto inseguro de verdade): confirmado navigator.clipboard.writeText undefined nesse contexto (reproduz o sintoma exato), e os dois fluxos de senha mostram sucesso via o fallback. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8 --- TODO.md | 25 +++++++ .../src/components/ui/secret-reveal.tsx | 68 +++++++++++++++++-- 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index 9ea57c2..daf3c8b 100644 --- a/TODO.md +++ b/TODO.md @@ -2573,6 +2573,31 @@ teste01 — "so fica cinza") 200 OK observado no tráfego WebSocket real, botão do widget muda de cinza pra verde (conectado). +## PHASE 71 — Botão de copiar senha SIP não fazia nada (pedido do +usuário: "o botão de copiar a senha dentro do cadastro do ramal está com +problema") +- [x] **Achado real**: `navigator.clipboard` só existe em "contexto + seguro" (HTTPS ou `localhost`) — este ambiente é HTTP puro, + acessado pelo IP da VM (nunca localhost pro usuário de verdade). + `SecretReveal` (usado tanto na criação de ramal quanto em "ver + senha atual") chamava `navigator.clipboard.writeText` direto, sem + try/catch — em contexto inseguro isso derruba/rejeita sem erro + visível nenhum, o botão simplesmente não fazia nada. Confirmado + com Playwright acessando `http://10.10.32.138:3001` (IP real, não + localhost): `navigator.clipboard.writeText` de fato `undefined` + nesse contexto — reproduz o sintoma exato. +- [x] Fix: fallback pra `document.execCommand("copy")` (textarea + temporário fora da tela) quando a Clipboard API moderna não existe + ou falha — funciona em qualquer contexto, inclusive HTTP. Se as + duas formas falharem, mostra aviso pra selecionar e copiar + manualmente (Ctrl+C) em vez de falhar em silêncio — o campo já é + `select-all`. +- [x] Testado ponta a ponta com Playwright acessando o IP real da VM + (não localhost, pra genuinamente cair no contexto inseguro): os + dois fluxos (senha mostrada na criação do ramal, e "ver senha + atual" de um ramal já existente) mostram o ícone verde de sucesso + via o fallback. + --- ## Riscos conhecidos diff --git a/apps/frontend/src/components/ui/secret-reveal.tsx b/apps/frontend/src/components/ui/secret-reveal.tsx index 46fd316..5d63ea9 100644 --- a/apps/frontend/src/components/ui/secret-reveal.tsx +++ b/apps/frontend/src/components/ui/secret-reveal.tsx @@ -1,9 +1,36 @@ "use client"; import { useState } from "react"; -import { Copy, Check, TriangleAlert } from "lucide-react"; +import { Copy, Check, TriangleAlert, X } from "lucide-react"; import { cn } from "@/lib/utils"; +/** + * `navigator.clipboard` só existe em "contexto seguro" (HTTPS ou + * localhost) — achado real reportado pelo usuário: o botão de copiar não + * fazia nada (sem erro nenhum, silencioso) quando acessado pelo IP da VM + * em HTTP puro (agente.md: ambiente de laboratório sem HTTPS ainda). + * `execCommand("copy")` sobre um textarea temporário fora da tela é o + * fallback clássico que funciona em qualquer contexto, inclusive HTTP. + */ +function legacyCopy(value: string): boolean { + const textarea = document.createElement("textarea"); + textarea.value = value; + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + textarea.style.pointerEvents = "none"; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + let ok = false; + try { + ok = document.execCommand("copy"); + } catch { + ok = false; + } + document.body.removeChild(textarea); + return ok; +} + /** * Mostra um segredo em texto puro UMA vez (senha SIP recém-gerada, * agente.md secao 39: "nunca mostrar novamente a senha inteira" depois @@ -12,11 +39,31 @@ import { cn } from "@/lib/utils"; */ export function SecretReveal({ label, value }: { label: string; value: string }) { const [copied, setCopied] = useState(false); + const [error, setError] = useState(false); async function onCopy() { - await navigator.clipboard.writeText(value); - setCopied(true); - setTimeout(() => setCopied(false), 2000); + setError(false); + let ok = false; + if (navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(value); + ok = true; + } catch { + ok = false; + } + } + if (!ok) ok = legacyCopy(value); + + if (ok) { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } else { + // Nenhuma das duas formas funcionou — o texto já está selecionável + // (`select-all` no abaixo), avisa pra copiar manualmente + // (Ctrl+C) em vez de falhar em silêncio. + setError(true); + setTimeout(() => setError(false), 4000); + } } return ( @@ -35,9 +82,20 @@ export function SecretReveal({ label, value }: { label: string; value: string }) className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" aria-label="Copiar senha" > - {copied ? : } + {copied ? ( + + ) : error ? ( + + ) : ( + + )} + {error && ( +

+ Não deu pra copiar automaticamente — selecione o texto acima e copie com Ctrl+C (ou Cmd+C). +

+ )} ); }