feat(portal): adiciona monitoramento em tempo real de desativação de máquina
Some checks failed
CI/CD Web + Desktop / Deploy Convex functions (push) Blocked by required conditions
CI/CD Web + Desktop / Detect changes (push) Successful in 5s
Quality Checks / Lint, Test and Build (push) Has been cancelled
CI/CD Web + Desktop / Deploy (VPS Linux) (push) Has been cancelled

- Cria hook useMachineStateMonitor com subscription Convex
- Cria MachineDeactivationOverlay para bloquear acesso
- Integra no PortalShell para exibir overlay quando máquina é desativada
- Adiciona refreshMachineContext ao AuthProvider para retry manual
- Funciona tanto via Raven quanto via navegador web

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rever-tecnologia 2025-12-17 18:02:23 -03:00
parent 413749d999
commit ae4fd7f890
4 changed files with 274 additions and 3 deletions

View file

@ -0,0 +1,73 @@
"use client"
import { ShieldAlert, Mail, RefreshCw } from "lucide-react"
import { Button } from "@/components/ui/button"
type MachineDeactivationOverlayProps = {
companyName?: string | null
onRetry?: () => void
isRetrying?: boolean
}
/**
* Overlay de bloqueio exibido quando a máquina é desativada.
* Cobre toda a tela impedindo qualquer interação até que a máquina seja reativada.
*/
export function MachineDeactivationOverlay({
companyName,
onRetry,
isRetrying,
}: MachineDeactivationOverlayProps) {
return (
<div className="fixed inset-0 z-[100] grid place-items-center bg-neutral-950/95 p-6 backdrop-blur-sm">
<div className="flex w-full max-w-[520px] flex-col items-center gap-6 rounded-2xl border border-slate-200 bg-white px-8 py-10 shadow-xl animate-in fade-in zoom-in-95 duration-300">
<div className="flex flex-col items-center gap-3 text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-rose-200 bg-rose-50 px-3 py-1 text-xs font-semibold text-rose-700">
<ShieldAlert className="size-4" /> Acesso bloqueado
</span>
<h1 className="text-2xl font-semibold text-neutral-900">Dispositivo desativado</h1>
<p className="max-w-md text-sm text-neutral-600">
Este dispositivo foi desativado temporariamente pelos administradores. O acesso ao portal
e o envio de informações ficam indisponíveis até a reativação.
</p>
{companyName ? (
<span className="rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-semibold text-neutral-700">
{companyName}
</span>
) : null}
</div>
<div className="w-full space-y-4">
<div className="rounded-xl border border-slate-200 bg-slate-50 p-4 text-sm text-neutral-700">
<p className="font-medium text-neutral-800">Como regularizar</p>
<ul className="mt-2 list-disc space-y-1 pl-5 text-neutral-600">
<li>Entre em contato com o suporte e solicite a reativação.</li>
<li>Informe o nome do computador e seus dados de contato.</li>
</ul>
</div>
<div className="flex flex-col items-center gap-3 sm:flex-row sm:justify-center">
<a
href="mailto:suporte@rever.com.br"
className="inline-flex items-center gap-2 rounded-full border border-black bg-black px-4 py-2 text-sm font-semibold text-white transition hover:bg-black/90"
>
<Mail className="size-4" /> Falar com o suporte
</a>
{onRetry ? (
<Button
variant="outline"
size="sm"
onClick={onRetry}
disabled={isRetrying}
className="gap-2 rounded-full"
>
<RefreshCw className={`size-4 ${isRetrying ? "animate-spin" : ""}`} />
{isRetrying ? "Verificando..." : "Verificar novamente"}
</Button>
) : null}
</div>
</div>
</div>
</div>
)
}

View file

@ -1,6 +1,6 @@
"use client"
import { type ReactNode, useMemo, useState } from "react"
import { type ReactNode, useMemo, useState, useCallback } from "react"
import Image from "next/image"
import Link from "next/link"
import { usePathname, useRouter } from "next/navigation"
@ -12,6 +12,8 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Skeleton } from "@/components/ui/skeleton"
import { cn } from "@/lib/utils"
import { useAuth, signOut } from "@/lib/auth-client"
import { useMachineStateMonitor } from "@/hooks/use-machine-state-monitor"
import { MachineDeactivationOverlay } from "./machine-deactivation-overlay"
interface PortalShellProps {
children: ReactNode
@ -25,10 +27,50 @@ const navItems = [
export function PortalShell({ children }: PortalShellProps) {
const pathname = usePathname()
const router = useRouter()
const { session, machineContext, machineContextError, machineContextLoading } = useAuth()
const { session, machineContext, machineContextError, machineContextLoading, refreshMachineContext } = useAuth()
const [isSigningOut, setIsSigningOut] = useState(false)
const [showDeactivationOverlay, setShowDeactivationOverlay] = useState(false)
const [isRetryingActivation, setIsRetryingActivation] = useState(false)
const isMachineSession = session?.user.role === "machine" || Boolean(machineContext)
// Monitor de estado da máquina em tempo real via Convex
const handleMachineDeactivated = useCallback(() => {
console.log("[PortalShell] Máquina foi desativada - exibindo overlay de bloqueio")
setShowDeactivationOverlay(true)
}, [])
const handleTokenRevoked = useCallback(() => {
console.log("[PortalShell] Token foi revogado - redirecionando para login")
toast.error("Este dispositivo foi resetado. Faça login novamente.")
router.replace("/login")
}, [router])
const { isActive: machineIsActive } = useMachineStateMonitor({
machineId: machineContext?.machineId,
onDeactivated: handleMachineDeactivated,
onTokenRevoked: handleTokenRevoked,
enabled: isMachineSession && !!machineContext?.machineId,
})
// Verifica também o estado vindo do contexto (polling) como fallback
const effectivelyDeactivated = showDeactivationOverlay || machineContext?.isActive === false || !machineIsActive
// Função para verificar novamente o estado
const handleRetryActivation = useCallback(async () => {
setIsRetryingActivation(true)
try {
await refreshMachineContext?.()
// Se ainda está ativo após refresh, esconde overlay
if (machineContext?.isActive !== false && machineIsActive) {
setShowDeactivationOverlay(false)
}
} catch (error) {
console.error("Erro ao verificar estado:", error)
} finally {
setIsRetryingActivation(false)
}
}, [refreshMachineContext, machineContext?.isActive, machineIsActive])
const personaValue = machineContext?.persona ?? session?.user.machinePersona ?? null
const collaboratorName = machineContext?.assignedUserName?.trim() ?? ""
const collaboratorEmail = machineContext?.assignedUserEmail?.trim() ?? ""
@ -74,6 +116,13 @@ export function PortalShell({ children }: PortalShellProps) {
return (
<div className="flex min-h-screen flex-col bg-gradient-to-b from-slate-50 via-slate-50 to-white">
{/* Overlay de bloqueio quando máquina é desativada */}
{isMachineSession && effectivelyDeactivated && (
<MachineDeactivationOverlay
onRetry={handleRetryActivation}
isRetrying={isRetryingActivation}
/>
)}
<header className="border-b border-slate-200 bg-white/90 backdrop-blur">
<div className="mx-auto flex w-full max-w-6xl flex-wrap items-center justify-between gap-4 px-6 py-4">
<div className="flex items-center gap-3">