diff --git a/src/hooks/use-machine-state-monitor.ts b/src/hooks/use-machine-state-monitor.ts
new file mode 100644
index 0000000..6a6df48
--- /dev/null
+++ b/src/hooks/use-machine-state-monitor.ts
@@ -0,0 +1,117 @@
+/**
+ * Hook para monitorar o estado da máquina em tempo real via Convex subscription.
+ *
+ * Usado no portal para detectar instantaneamente quando uma máquina é:
+ * - Desativada (isActive = false)
+ * - Resetada (tokens revogados)
+ *
+ * Diferente do polling de 15s do AuthProvider, isso é verdadeiramente real-time.
+ */
+
+import { useEffect, useRef, useCallback } from "react"
+import { useQuery } from "convex/react"
+import { api } from "@/convex/_generated/api"
+import type { Id } from "@/convex/_generated/dataModel"
+
+type UseMachineStateMonitorOptions = {
+ machineId: string | null | undefined
+ onDeactivated?: () => void
+ onTokenRevoked?: () => void
+ enabled?: boolean
+}
+
+type MachineStateResult = {
+ isActive: boolean
+ hasValidToken: boolean
+ isLoading: boolean
+ found: boolean
+}
+
+export function useMachineStateMonitor({
+ machineId,
+ onDeactivated,
+ onTokenRevoked,
+ enabled = true,
+}: UseMachineStateMonitorOptions): MachineStateResult {
+ // Refs para rastrear estado anterior e evitar chamadas duplicadas
+ const previousIsActive = useRef
(null)
+ const previousHasValidToken = useRef(null)
+ const initialLoadDone = useRef(false)
+
+ // Subscription Convex - só ativa se tiver machineId válido
+ const machineState = useQuery(
+ api.machines.getMachineState,
+ enabled && machineId ? { machineId: machineId as Id<"machines"> } : "skip"
+ )
+
+ // Callbacks estáveis
+ const handleDeactivated = useCallback(() => {
+ onDeactivated?.()
+ }, [onDeactivated])
+
+ const handleTokenRevoked = useCallback(() => {
+ onTokenRevoked?.()
+ }, [onTokenRevoked])
+
+ useEffect(() => {
+ if (!machineState) return
+
+ // Na primeira carga, verifica estado inicial E armazena valores
+ if (!initialLoadDone.current) {
+ console.log("[useMachineStateMonitor] Carga inicial", {
+ isActive: machineState.isActive,
+ hasValidToken: machineState.hasValidToken,
+ found: machineState.found,
+ })
+
+ // Se já estiver desativado na carga inicial, chama callback
+ if (machineState.isActive === false) {
+ console.log("[useMachineStateMonitor] Máquina já estava desativada")
+ handleDeactivated()
+ }
+
+ // Se token já estiver inválido na carga inicial, chama callback
+ if (machineState.hasValidToken === false) {
+ console.log("[useMachineStateMonitor] Token já estava revogado")
+ handleTokenRevoked()
+ }
+
+ previousIsActive.current = machineState.isActive
+ previousHasValidToken.current = machineState.hasValidToken
+ initialLoadDone.current = true
+ return
+ }
+
+ // Detecta mudança de ativo para inativo
+ if (previousIsActive.current === true && machineState.isActive === false) {
+ console.log("[useMachineStateMonitor] Máquina foi desativada")
+ handleDeactivated()
+ }
+
+ // Detecta mudança de token válido para inválido
+ if (previousHasValidToken.current === true && machineState.hasValidToken === false) {
+ console.log("[useMachineStateMonitor] Token foi revogado (reset)")
+ handleTokenRevoked()
+ }
+
+ // Atualiza refs
+ previousIsActive.current = machineState.isActive
+ previousHasValidToken.current = machineState.hasValidToken
+ }, [machineState, handleDeactivated, handleTokenRevoked])
+
+ // Reset refs quando machineId muda
+ useEffect(() => {
+ if (!machineId) {
+ previousIsActive.current = null
+ previousHasValidToken.current = null
+ initialLoadDone.current = false
+ }
+ }, [machineId])
+
+ return {
+ isActive: machineState?.isActive ?? true,
+ hasValidToken: machineState?.hasValidToken ?? true,
+ isLoading: machineState === undefined,
+ found: machineState?.found ?? false,
+ }
+}
diff --git a/src/lib/auth-client.tsx b/src/lib/auth-client.tsx
index e661020..33c108a 100644
--- a/src/lib/auth-client.tsx
+++ b/src/lib/auth-client.tsx
@@ -68,6 +68,7 @@ type AuthContextValue = {
machineContext: MachineContext | null
machineContextLoading: boolean
machineContextError: MachineContextError | null
+ refreshMachineContext: (() => Promise) | null
}
const AuthContext = createContext({
@@ -81,6 +82,7 @@ const AuthContext = createContext({
machineContext: null,
machineContextLoading: false,
machineContextError: null,
+ refreshMachineContext: null,
})
export function useAuth() {
@@ -345,6 +347,35 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const effectiveConvexUserId = baseRole === "machine" ? (machineContext?.assignedUserId ?? null) : convexUserId
+ // Função para forçar atualização do contexto de máquina
+ const refreshMachineContext = useCallback(async () => {
+ try {
+ const response = await fetch("/api/machines/session", { credentials: "include" })
+ if (!response.ok) {
+ console.warn("[refreshMachineContext] Falha ao buscar sessão:", response.status)
+ return
+ }
+ const data = (await response.json()) as { machine?: MachineContext & { id: string } }
+ const mc = data?.machine
+ if (mc && typeof mc === "object") {
+ setMachineContext({
+ machineId: mc.id,
+ tenantId: mc.tenantId,
+ persona: mc.persona ?? null,
+ assignedUserId: mc.assignedUserId ?? null,
+ assignedUserEmail: mc.assignedUserEmail ?? null,
+ assignedUserName: mc.assignedUserName ?? null,
+ assignedUserRole: mc.assignedUserRole ?? null,
+ companyId: mc.companyId ?? null,
+ isActive: mc.isActive ?? true,
+ })
+ setMachineContextError(null)
+ }
+ } catch (error) {
+ console.error("[refreshMachineContext] Erro:", error)
+ }
+ }, [])
+
const value = useMemo(
() => ({
session: session ?? null,
@@ -357,8 +388,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
machineContext,
machineContextLoading,
machineContextError,
+ refreshMachineContext,
}),
- [session, isPending, effectiveConvexUserId, normalizedRole, machineContext, machineContextLoading, machineContextError]
+ [session, isPending, effectiveConvexUserId, normalizedRole, machineContext, machineContextLoading, machineContextError, refreshMachineContext]
)
return {children}