diff --git a/apps/desktop/src/components/MachineStateMonitor.tsx b/apps/desktop/src/components/MachineStateMonitor.tsx new file mode 100644 index 0000000..0ed9e96 --- /dev/null +++ b/apps/desktop/src/components/MachineStateMonitor.tsx @@ -0,0 +1,78 @@ +/** + * MachineStateMonitor - Componente para monitorar o estado da máquina em tempo real + * + * Este componente usa uma subscription Convex para detectar mudanças no estado da máquina: + * - Quando isActive muda para false: máquina foi desativada + * - Quando hasValidToken muda para false: máquina foi resetada (tokens revogados) + * + * O componente não renderiza nada, apenas monitora e chama callbacks quando detecta mudanças. + */ + +import { useEffect, useRef } from "react" +import { useQuery, ConvexProvider } from "convex/react" +import type { ConvexReactClient } from "convex/react" +import { api } from "../convex/_generated/api" +import type { Id } from "../convex/_generated/dataModel" + +type MachineStateMonitorProps = { + machineId: string + onDeactivated?: () => void + onTokenRevoked?: () => void +} + +function MachineStateMonitorInner({ machineId, onDeactivated, onTokenRevoked }: MachineStateMonitorProps) { + const machineState = useQuery(api.machines.getMachineState, { + machineId: machineId as Id<"machines">, + }) + + // Refs para rastrear o estado anterior e evitar chamadas duplicadas + const previousIsActive = useRef(null) + const previousHasValidToken = useRef(null) + const initialLoadDone = useRef(false) + + useEffect(() => { + if (!machineState) return + + // Na primeira carga, apenas armazena os valores iniciais + if (!initialLoadDone.current) { + 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("[MachineStateMonitor] Máquina foi desativada") + onDeactivated?.() + } + + // Detecta mudança de token válido para inválido + if (previousHasValidToken.current === true && machineState.hasValidToken === false) { + console.log("[MachineStateMonitor] Token foi revogado (reset)") + onTokenRevoked?.() + } + + // Atualiza refs + previousIsActive.current = machineState.isActive + previousHasValidToken.current = machineState.hasValidToken + }, [machineState, onDeactivated, onTokenRevoked]) + + // Este componente nao renderiza nada + return null +} + +type MachineStateMonitorWithClientProps = MachineStateMonitorProps & { + client: ConvexReactClient +} + +/** + * Wrapper que recebe o cliente Convex e envolve o monitor com o provider + */ +export function MachineStateMonitor({ client, ...props }: MachineStateMonitorWithClientProps) { + return ( + + + + ) +} diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx index c9dae68..2d0448c 100644 --- a/apps/desktop/src/main.tsx +++ b/apps/desktop/src/main.tsx @@ -6,12 +6,19 @@ import { listen } from "@tauri-apps/api/event" import { Store } from "@tauri-apps/plugin-store" import { appLocalDataDir, join } from "@tauri-apps/api/path" import { ExternalLink, Eye, EyeOff, Loader2, RefreshCw } from "lucide-react" +import { ConvexReactClient } from "convex/react" import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs" import { cn } from "./lib/utils" import { ChatApp } from "./chat" import { DeactivationScreen } from "./components/DeactivationScreen" +import { MachineStateMonitor } from "./components/MachineStateMonitor" import type { SessionStartedEvent, UnreadUpdateEvent, NewMessageEvent, SessionEndedEvent } from "./chat/types" +// URL do Convex para subscription em tempo real +const CONVEX_URL = import.meta.env.MODE === "production" + ? "https://convex.esdrasrenan.com.br" + : (import.meta.env.VITE_CONVEX_URL ?? "https://convex.esdrasrenan.com.br") + type MachineOs = { name: string version?: string | null @@ -321,6 +328,9 @@ function App() { const selfHealPromiseRef = useRef | null>(null) const lastHealAtRef = useRef(0) + // Cliente Convex para monitoramento em tempo real do estado da maquina + const [convexClient, setConvexClient] = useState(null) + const [provisioningCode, setProvisioningCode] = useState("") const [validatedCompany, setValidatedCompany] = useState<{ id: string; name: string; slug: string; tenantId: string } | null>(null) const [companyName, setCompanyName] = useState("") @@ -693,6 +703,56 @@ useEffect(() => { rustdeskInfoRef.current = rustdeskInfo }, [rustdeskInfo]) +// Cria/destrói cliente Convex quando o token muda +useEffect(() => { + if (!token) { + if (convexClient) { + convexClient.close() + setConvexClient(null) + } + return + } + + // Cria novo cliente Convex para monitoramento em tempo real + const client = new ConvexReactClient(CONVEX_URL, { + unsavedChangesWarning: false, + }) + setConvexClient(client) + + return () => { + client.close() + } +}, [token]) // eslint-disable-line react-hooks/exhaustive-deps + +// Callbacks para quando a máquina for desativada ou resetada +const handleMachineDeactivated = useCallback(() => { + console.log("[App] Máquina foi desativada - mostrando tela de bloqueio") + setIsMachineActive(false) +}, []) + +const handleTokenRevoked = useCallback(async () => { + console.log("[App] Token foi revogado - voltando para tela de registro") + if (store) { + try { + await store.delete("token") + await store.delete("config") + await store.save() + } catch (err) { + console.error("Falha ao limpar store", err) + } + } + tokenVerifiedRef.current = false + autoLaunchRef.current = false + setToken(null) + setConfig(null) + setStatus(null) + setIsMachineActive(true) + setError("Este dispositivo foi resetado. Informe o código de provisionamento para reconectar.") + try { + const p = await invoke("collect_machine_profile") + setProfile(p) + } catch {} +}, [store]) useEffect(() => { if (!store || !config) return @@ -1514,6 +1574,15 @@ const resolvedAppUrl = useMemo(() => { return (
+ {/* Monitor de estado da maquina em tempo real via Convex */} + {token && config?.machineId && convexClient && ( + + )} {token && !isMachineActive ? ( ) : ( diff --git a/convex/machines.ts b/convex/machines.ts index 3428759..ab21fcf 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -2317,6 +2317,44 @@ export const resetAgent = mutation({ }, }) +/** + * Query para o desktop monitorar o estado da máquina em tempo real. + * O desktop faz subscribe nessa query e reage imediatamente quando: + * - isActive muda para false (desativação) + * - hasValidToken muda para false (reset/revogação de tokens) + */ +export const getMachineState = query({ + args: { + machineId: v.id("machines"), + }, + handler: async (ctx, { machineId }) => { + const machine = await ctx.db.get(machineId) + if (!machine) { + return { found: false, isActive: false, hasValidToken: false, status: "unknown" as const } + } + + // Verifica se existe algum token válido (não revogado e não expirado) + const now = Date.now() + const tokens = await ctx.db + .query("machineTokens") + .withIndex("by_machine", (q) => q.eq("machineId", machineId)) + .take(10) + + const hasValidToken = tokens.some((token) => { + if (token.revoked) return false + if (token.expiresAt && token.expiresAt < now) return false + return true + }) + + return { + found: true, + isActive: machine.isActive ?? true, + hasValidToken, + status: machine.status ?? "unknown", + } + }, +}) + type RemoteAccessEntry = { id: string provider: string diff --git a/src/components/admin/devices/admin-devices-overview.tsx b/src/components/admin/devices/admin-devices-overview.tsx index a6177ff..41670c3 100644 --- a/src/components/admin/devices/admin-devices-overview.tsx +++ b/src/components/admin/devices/admin-devices-overview.tsx @@ -3275,6 +3275,8 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { ) const [togglingActive, setTogglingActive] = useState(false) const [isResettingAgent, setIsResettingAgent] = useState(false) + const [resetConfirmOpen, setResetConfirmOpen] = useState(false) + const [deactivateConfirmOpen, setDeactivateConfirmOpen] = useState(false) const [showAllWindowsSoftware, setShowAllWindowsSoftware] = useState(false) const [isUsbModalOpen, setIsUsbModalOpen] = useState(false) const jsonText = useMemo(() => { @@ -4001,7 +4003,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { size="sm" variant="outline" className="gap-2 border-dashed" - onClick={handleResetAgent} + onClick={() => setResetConfirmOpen(true)} disabled={isResettingAgent} > @@ -4014,7 +4016,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { "gap-2 border-dashed", !isActiveLocal && "bg-emerald-600 text-white hover:bg-emerald-600/90" )} - onClick={handleToggleActive} + onClick={() => isActiveLocal ? setDeactivateConfirmOpen(true) : handleToggleActive()} disabled={togglingActive} > {isActiveLocal ? : } @@ -5970,6 +5972,80 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
+ + {/* Modal de confirmação - Resetar */} + + + + + + Resetar dispositivo + + + Esta ação revogará todos os tokens de acesso do dispositivo. + + +
+

+ O dispositivo {device?.displayName ?? device?.hostname} será desconectado imediatamente e precisará ser reprovisionado para voltar a funcionar. +

+
+ + + + +
+
+ + {/* Modal de confirmação - Desativar */} + + + + + + Desativar dispositivo + + + O dispositivo será bloqueado e não poderá mais acessar o sistema. + + +
+

+ O dispositivo {device?.displayName ?? device?.hostname} será bloqueado imediatamente. O usuário verá uma tela de desativação e não poderá mais abrir chamados ou usar o chat. +

+
+ + + + +
+
)}