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). +

+ )} ); }