"use client" import { useCallback, useEffect, useMemo, useState } from "react" import type { ReactNode } from "react" import { useQuery } from "convex/react" import { format, formatDistanceToNowStrict } from "date-fns" import { ptBR } from "date-fns/locale" import { toast } from "sonner" import { ClipboardCopy, ServerCog, Cpu, MemoryStick, Monitor, HardDrive, Pencil, ShieldCheck, ShieldAlert, Shield, ShieldOff, ShieldQuestion, Lock, Cloud, RefreshCcw, AlertTriangle, Key, Globe, Apple, Terminal, Power, PlayCircle, Download, } from "lucide-react" import { api } from "@/convex/_generated/api" import { Badge } from "@/components/ui/badge" import { Button, buttonVariants } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Spinner } from "@/components/ui/spinner" import { Progress } from "@/components/ui/progress" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Checkbox } from "@/components/ui/checkbox" import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Separator } from "@/components/ui/separator" import { ChartContainer } from "@/components/ui/chart" import { cn } from "@/lib/utils" import { RadialBarChart, RadialBar, PolarAngleAxis } from "recharts" import Link from "next/link" import { useRouter } from "next/navigation" import { useAuth } from "@/lib/auth-client" import type { Id } from "@/convex/_generated/dataModel" import { TicketStatusBadge } from "@/components/tickets/status-badge" import type { TicketPriority, TicketStatus } from "@/lib/schemas/ticket" type MachineMetrics = Record | null type MachineLabel = { id?: number | string name?: string } type MachineSoftware = { name?: string version?: string source?: string } type NormalizedSoftwareEntry = { name: string version?: string publisher?: string installDate?: Date | null source?: string } type MachineAlertEntry = { id: string kind: string message: string severity: string createdAt: number } type MachineTicketSummary = { id: string reference: number subject: string status: TicketStatus priority: TicketPriority updatedAt: number createdAt: number machine: { id: string | null; hostname: string | null } | null assignee: { name: string | null; email: string | null } | null } type MachineOpenTicketsSummary = { totalOpen: number hasMore: boolean tickets: MachineTicketSummary[] } type DetailLineProps = { label: string value?: string | number | null classNameValue?: string layout?: "spread" | "compact" } type GpuAdapter = { name?: string vendor?: string driver?: string memoryBytes?: number } type LinuxLsblkEntry = { name?: string mountPoint?: string mountpoint?: string fs?: string fstype?: string sizeBytes?: number size?: number } type LinuxSmartEntry = { smart_status?: { passed?: boolean } model_name?: string model_family?: string serial_number?: string device?: { name?: string } } type LinuxExtended = { lsblk?: LinuxLsblkEntry[] lspci?: string lsusb?: string pciList?: Array<{ text: string }> usbList?: Array<{ text: string }> smart?: LinuxSmartEntry[] } type WindowsCpuInfo = { Name?: string Manufacturer?: string SocketDesignation?: string NumberOfCores?: number NumberOfLogicalProcessors?: number L2CacheSize?: number L3CacheSize?: number MaxClockSpeed?: number } type WindowsMemoryModule = { BankLabel?: string Capacity?: number Manufacturer?: string PartNumber?: string SerialNumber?: string ConfiguredClockSpeed?: number Speed?: number ConfiguredVoltage?: number } type WindowsVideoController = { Name?: string AdapterRAM?: number DriverVersion?: string PNPDeviceID?: string } type WindowsDiskEntry = { Model?: string SerialNumber?: string Size?: number InterfaceType?: string MediaType?: string } type WindowsExtended = { software?: MachineSoftware[] services?: Array<{ name?: string; status?: string; displayName?: string }> defender?: Record hotfix?: Array> cpu?: WindowsCpuInfo | WindowsCpuInfo[] baseboard?: Record | Array> bios?: Record | Array> memoryModules?: WindowsMemoryModule[] videoControllers?: WindowsVideoController[] disks?: WindowsDiskEntry[] osInfo?: WindowsOsInfo bitLocker?: Array> | Record bitlocker?: Array> | Record tpm?: Record secureBoot?: Record deviceGuard?: Array> | Record firewallProfiles?: Array> | Record windowsUpdate?: Record computerSystem?: Record azureAdStatus?: Record } type MacExtended = { systemProfiler?: Record packages?: string[] launchctl?: string } type NetworkInterface = { name?: string; mac?: string; ip?: string } type MachineInventory = { hardware?: { vendor?: string model?: string serial?: string cpuType?: string physicalCores?: number logicalCores?: number memoryBytes?: number memory?: number primaryGpu?: GpuAdapter gpus?: GpuAdapter[] } network?: { primaryIp?: string; publicIp?: string; macAddresses?: string[] } | NetworkInterface[] software?: MachineSoftware[] labels?: MachineLabel[] fleet?: { id?: number | string teamId?: number | string detailUpdatedAt?: string osqueryVersion?: string } disks?: Array<{ name?: string; mountPoint?: string; fs?: string; interface?: string | null; serial?: string | null; totalBytes?: number; availableBytes?: number }> extended?: { linux?: LinuxExtended; windows?: WindowsExtended; macos?: MacExtended } services?: Array<{ name?: string; status?: string; displayName?: string }> collaborator?: { email?: string; name?: string; role?: string } } type MachineRemoteAccessEntry = { id: string | null clientId: string provider: string | null identifier: string | null url: string | null notes: string | null lastVerifiedAt: number | null metadata: Record | null } export type MachineRemoteAccess = { provider: string | null identifier: string | null url: string | null notes: string | null lastVerifiedAt: number | null metadata: Record | null } function collectInitials(name: string): string { const words = name.split(/\s+/).filter(Boolean) if (words.length === 0) return "?" if (words.length === 1) return words[0].slice(0, 2).toUpperCase() return (words[0][0] + words[1][0]).toUpperCase() } function toRecord(value: unknown): Record | null { if (!value || typeof value !== "object") return null return value as Record } function readString(record: Record, ...keys: string[]): string | undefined { for (const key of keys) { const raw = record[key] if (typeof raw === "string" && raw.trim().length > 0) { return raw } } return undefined } function readNumber(record: Record, ...keys: string[]): number | undefined { for (const key of keys) { const raw = record[key] if (typeof raw === "number" && Number.isFinite(raw)) { return raw } if (typeof raw === "string") { const trimmed = raw.trim() if (!trimmed) continue const parsed = Number(trimmed) if (!Number.isNaN(parsed)) return parsed const digits = trimmed.replace(/[^0-9.]/g, "") if (digits) { const fallback = Number(digits) if (!Number.isNaN(fallback)) return fallback } } } return undefined } function createRemoteAccessClientId() { if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { return crypto.randomUUID() } return `ra-${Math.random().toString(36).slice(2, 8)}-${Date.now().toString(36)}` } function normalizeMachineRemoteAccessEntry(raw: unknown): MachineRemoteAccessEntry | null { if (!raw) return null if (typeof raw === "string") { const trimmed = raw.trim() if (!trimmed) return null const isUrl = /^https?:\/\//i.test(trimmed) return { id: null, clientId: createRemoteAccessClientId(), provider: null, identifier: isUrl ? null : trimmed, url: isUrl ? trimmed : null, notes: null, lastVerifiedAt: null, metadata: null, } } const record = toRecord(raw) if (!record) return null const provider = readString(record, "provider", "tool", "vendor", "name") ?? null const identifier = readString(record, "identifier", "code", "id", "accessId") ?? readString(record, "value", "label") const url = readString(record, "url", "link", "remoteUrl", "console", "viewer") ?? null const notes = readString(record, "notes", "note", "description", "obs") ?? null const timestampCandidate = readNumber(record, "lastVerifiedAt", "verifiedAt", "checkedAt", "updatedAt") ?? parseDateish(record["lastVerifiedAt"] ?? record["verifiedAt"] ?? record["checkedAt"] ?? record["updatedAt"]) const lastVerifiedAt = timestampCandidate instanceof Date ? timestampCandidate.getTime() : timestampCandidate ?? null const id = readString(record, "id") ?? null return { id, clientId: id ?? createRemoteAccessClientId(), provider, identifier: identifier ?? url ?? null, url, notes, lastVerifiedAt, metadata: record, } } export function normalizeMachineRemoteAccess(raw: unknown): MachineRemoteAccess | null { const entry = normalizeMachineRemoteAccessEntry(raw) if (!entry) return null const { provider, identifier, url, notes, lastVerifiedAt, metadata } = entry return { provider, identifier, url, notes, lastVerifiedAt, metadata } } export function normalizeMachineRemoteAccessList(raw: unknown): MachineRemoteAccessEntry[] { if (!raw) return [] const source = Array.isArray(raw) ? raw : [raw] const seen = new Set() const entries: MachineRemoteAccessEntry[] = [] for (const item of source) { const entry = normalizeMachineRemoteAccessEntry(item) if (!entry) continue let clientId = entry.clientId while (seen.has(clientId)) { clientId = createRemoteAccessClientId() } seen.add(clientId) entries.push(clientId === entry.clientId ? entry : { ...entry, clientId }) } return entries } const REMOTE_ACCESS_METADATA_IGNORED_KEYS = new Set([ "provider", "tool", "vendor", "name", "identifier", "code", "id", "accessId", "url", "link", "remoteUrl", "console", "viewer", "notes", "note", "description", "obs", "lastVerifiedAt", "verifiedAt", "checkedAt", "updatedAt", ]) function extractRemoteAccessMetadataEntries(metadata: Record | null | undefined) { if (!metadata) return [] as Array<[string, unknown]> return Object.entries(metadata).filter(([key, value]) => { if (REMOTE_ACCESS_METADATA_IGNORED_KEYS.has(key)) return false if (value === null || value === undefined) return false if (typeof value === "string" && value.trim().length === 0) return false return true }) } function formatRemoteAccessMetadataKey(key: string) { return key .replace(/[_.-]+/g, " ") .replace(/\b\w/g, (char) => char.toUpperCase()) } function formatRemoteAccessMetadataValue(value: unknown): string { if (value === null || value === undefined) return "" if (typeof value === "string") return value if (typeof value === "number" || typeof value === "boolean") return String(value) if (value instanceof Date) return formatAbsoluteDateTime(value) try { return JSON.stringify(value) } catch { return String(value) } } function readText(record: Record, ...keys: string[]): string | undefined { const stringValue = readString(record, ...keys) if (stringValue) return stringValue const numberValue = readNumber(record, ...keys) if (typeof numberValue === "number") return String(numberValue) return undefined } function parseWindowsInstallDate(value: unknown): Date | null { if (!value) return null if (value instanceof Date) return value if (typeof value === "number" && Number.isFinite(value)) { if (value > 10_000_000_000) { return new Date(value) } if (value > 1_000_000_000) { return new Date(value * 1000) } return new Date(value * 1000) } if (typeof value !== "string") return null const trimmed = value.trim() if (!trimmed) return null const wmiMatch = trimmed.match(/Date\((\d+)\)/) if (wmiMatch) { const timestamp = Number(wmiMatch[1]) return Number.isFinite(timestamp) ? new Date(timestamp) : null } const isoValue = Date.parse(trimmed) if (!Number.isNaN(isoValue)) return new Date(isoValue) const digitsOnly = trimmed.replace(/[^0-9]/g, "") if (digitsOnly.length >= 8) { const yyyy = Number(digitsOnly.slice(0, 4)) const mm = Number(digitsOnly.slice(4, 6)) const dd = Number(digitsOnly.slice(6, 8)) const hh = Number(digitsOnly.slice(8, 10) || "0") const mi = Number(digitsOnly.slice(10, 12) || "0") const ss = Number(digitsOnly.slice(12, 14) || "0") if (yyyy > 1900 && mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31) { const parsed = new Date(Date.UTC(yyyy, mm - 1, dd, hh, mi, ss)) if (!Number.isNaN(parsed.getTime())) return parsed } const dd2 = Number(digitsOnly.slice(0, 2)) const mm2 = Number(digitsOnly.slice(2, 4)) const yyyy2 = Number(digitsOnly.slice(4, 8)) if (yyyy2 > 1900 && mm2 >= 1 && mm2 <= 12 && dd2 >= 1 && dd2 <= 31) { const parsed = new Date(Date.UTC(yyyy2, mm2 - 1, dd2)) if (!Number.isNaN(parsed.getTime())) return parsed } } const localMatch = trimmed.match( /^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/ ) if (localMatch) { const dd = Number(localMatch[1]) const mm = Number(localMatch[2]) const yyyy = Number(localMatch[3]) const hh = Number(localMatch[4] || "0") const mi = Number(localMatch[5] || "0") const ss = Number(localMatch[6] || "0") if (yyyy > 1900 && mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31) { const parsed = new Date(Date.UTC(yyyy, mm - 1, dd, hh, mi, ss)) if (!Number.isNaN(parsed.getTime())) return parsed } } return null } type WindowsOsInfo = { productName?: string caption?: string editionId?: string displayVersion?: string releaseId?: string version?: string currentBuild?: string currentBuildNumber?: string licenseStatus?: number isActivated?: boolean licenseStatusText?: string productId?: string partialProductKey?: string computerName?: string registeredOwner?: string installDate?: Date | null experience?: string } function formatOsVersionDisplay(osName: string | null | undefined, osVersion: string | null | undefined) { const name = (osName ?? "").trim() const version = (osVersion ?? "").trim() if (!version) return "" // If Windows and version redundantly starts with the same major (e.g., "11 (26100)"), drop leading major const winMatch = name.match(/^windows\s+(\d+)\b/i) if (winMatch) { const major = winMatch[1] const re = new RegExp(`^\\s*${major}(?:\\b|\\.|-_|\\s)+(.*)$`, "i") const m = version.match(re) if (m) { const rest = (m[1] ?? "").trim() return rest } } return version } function parseWindowsOsInfo(raw: unknown): WindowsOsInfo | null { if (!raw) return null const parseRecord = (value: Record) => { const read = (k: string, ...alts: string[]) => readString(value, k, ...alts) const readNum = (...keys: string[]) => readNumber(value, ...keys) const readFlexible = (...keys: string[]) => readText(value, ...keys) const family = read("Family") const edition = read("Edition") const captionRaw = readFlexible("Caption", "caption") const captionNormalized = captionRaw ? captionRaw.replace(/^Microsoft\s+/i, "").trim() : undefined let productName = readFlexible("ProductName", "productName", "Name", "name") ?? (family && edition ? `${family} ${edition}` : family ?? edition ?? undefined) if (captionNormalized && /windows/i.test(captionNormalized)) { productName = captionNormalized } const editionId = readFlexible("EditionID", "editionId", "Edition", "edition", "SkuEdition", "skuEdition", "CompositionEditionID", "compositionEditionId") ?? undefined const displayVersion = readFlexible("DisplayVersion", "displayVersion") const versionValue = readFlexible("Version", "version", "ReleaseId", "releaseId") ?? displayVersion const baseBuild = readFlexible("CurrentBuildNumber", "currentBuildNumber", "CurrentBuild", "currentBuild", "OSBuild", "osBuild", "BuildNumber", "buildNumber") ?? undefined const ubrRaw = readFlexible("UBR") const licenseStatus = readNum("LicenseStatus", "licenseStatus") const licenseStatusTextRaw = readFlexible( "LicenseStatusDescription", "licenseStatusDescription", "StatusDescription", "statusDescription", "Status", "status" ) const licenseStatusText = licenseStatusTextRaw ? licenseStatusTextRaw.trim() : undefined const currentBuildNumber = baseBuild && ubrRaw && /^\d+$/.test(ubrRaw) ? `${baseBuild}.${ubrRaw}` : baseBuild ?? readFlexible("BuildNumber", "buildNumber") const currentBuild = baseBuild const isActivatedRaw = value["IsActivated"] ?? value["isActivated"] const isLicensedRaw = value["IsLicensed"] ?? value["isLicensed"] const isActivated = typeof isActivatedRaw === "boolean" ? isActivatedRaw : typeof isActivatedRaw === "number" ? isActivatedRaw === 1 : typeof isActivatedRaw === "string" ? isActivatedRaw.toLowerCase() === "true" : typeof isLicensedRaw === "boolean" ? isLicensedRaw : typeof isLicensedRaw === "number" ? isLicensedRaw === 1 : typeof isLicensedRaw === "string" ? ["1", "true", "licensed", "license", "activated"].includes(isLicensedRaw.toLowerCase()) : licenseStatus === 1 || Boolean(licenseStatusText && /licensed|activated|licenciado/i.test(licenseStatusText)) const installDate = parseWindowsInstallDate(value["InstallDate"]) ?? parseWindowsInstallDate(value["InstallationDate"]) ?? parseWindowsInstallDate(value["InstallDateTime"]) ?? parseWindowsInstallDate(value["InstalledOn"]) const experience = (() => { const exp = readFlexible("Experience", "experience") if (exp) return exp const pack = readFlexible("FeatureExperiencePack", "featureExperiencePack") if (pack) { const trimmed = pack.trim() if (trimmed) return `Pacote de experiência ${trimmed}` } return currentBuildNumber ? `OS Build ${currentBuildNumber}` : undefined })() const productId = readFlexible("ProductID", "productID", "ProductId", "productId", "ProductKeyId", "productKeyId") const partialProductKey = readFlexible("PartialProductKey", "partialProductKey") const computerName = readFlexible("DeviceName", "deviceName", "ComputerName", "computerName", "CSName", "csName", "HostName", "hostName") const registeredOwner = readFlexible("RegisteredOwner", "registeredOwner", "RegisteredOrganization", "registeredOrganization") return { productName, editionId, displayVersion, version: versionValue, releaseId: read("ReleaseId", "releaseId"), currentBuild, currentBuildNumber, licenseStatus, isActivated, licenseStatusText, productId, partialProductKey, computerName, registeredOwner, installDate, experience, caption: captionNormalized ?? captionRaw ?? undefined, } } if (Array.isArray(raw)) { for (const entry of raw) { const record = toRecord(entry) if (record) { return parseRecord(record) } } return null } if (typeof raw === "string") { return { productName: raw } } const record = toRecord(raw) if (!record) return null return parseRecord(record) } function normalizeWindowsSoftwareEntry(value: unknown): NormalizedSoftwareEntry | null { const record = toRecord(value) if (!record) return null const name = readString(record, "DisplayName", "displayName", "Name", "name") ?? readText(record, "Title", "title") ?? "" if (!name) return null const version = readString(record, "DisplayVersion", "displayVersion", "Version", "version") ?? undefined const publisher = readString(record, "Publisher", "publisher", "Vendor", "vendor") ?? undefined const installDate = parseWindowsInstallDate(record["InstallDate"]) ?? parseWindowsInstallDate(record["InstallDateUTC"]) ?? parseWindowsInstallDate(record["InstallDateTime"]) ?? parseWindowsInstallDate(record["InstallDateFromRegistry"]) ?? parseWindowsInstallDate(record["InstallDateFromRegistryUTC"]) ?? parseWindowsInstallDate(record["InstalledDate"]) ?? parseWindowsInstallDate(record["InstalledOn"]) ?? null const source = readString(record, "ParentDisplayName", "ParentKeyName", "SystemComponent") ?? undefined return { name, version, publisher, installDate, source, } } function parseBytesLike(value: unknown): number | undefined { if (typeof value === "number" && Number.isFinite(value)) return value if (typeof value === "string") { const trimmed = value.trim() if (!trimmed) return undefined const normalized = trimmed.replace(",", ".") const match = normalized.match(/^([\d.]+)\s*(ti|tb|tib|gb|gib|mb|mib|kb|kib|b)?$/i) if (match) { const amount = Number(match[1]) if (Number.isNaN(amount)) return undefined const unit = match[2]?.toLowerCase() const base = 1024 const unitMap: Record = { b: 1, kb: base, kib: base, mb: base ** 2, mib: base ** 2, gb: base ** 3, gib: base ** 3, tb: base ** 4, tib: base ** 4, ti: base ** 4, } if (unit) { const multiplier = unitMap[unit] if (multiplier) { return amount * multiplier } } return amount } const digits = normalized.replace(/[^0-9.]/g, "") if (digits) { const fallback = Number(digits) if (!Number.isNaN(fallback)) return fallback } } return undefined } function deriveVendor(record: Record): string | undefined { const direct = readString(record, "vendor", "Vendor", "AdapterCompatibility") if (direct) return direct const pnp = readString(record, "PNPDeviceID") if (!pnp) return undefined const match = pnp.match(/VEN_([0-9A-F]{4})/i) if (match) { const vendorCode = match[1].toUpperCase() const vendorMap: Record = { "10DE": "NVIDIA", "1002": "AMD", "1022": "AMD", "8086": "Intel", "8087": "Intel", "1AF4": "Red Hat", } return vendorMap[vendorCode] ?? `VEN_${vendorCode}` } const segments = pnp.split("\\") const last = segments.pop() return last && last.trim().length > 0 ? last : pnp } function normalizeGpuSource(value: unknown): GpuAdapter | null { if (typeof value === "string") { const name = value.trim() return name ? { name } : null } const record = toRecord(value) if (!record) return null const name = readString(record, "name", "Name", "_name", "AdapterCompatibility") const vendor = deriveVendor(record) const driver = readString(record, "driver", "DriverVersion", "driverVersion") const memoryBytes = readNumber(record, "memoryBytes", "MemoryBytes", "AdapterRAM", "VRAM", "vramBytes") ?? parseBytesLike(record["AdapterRAM"] ?? record["VRAM"] ?? record["vram"]) if (!name && !vendor && !driver && memoryBytes === undefined) { return null } return { name, vendor, driver, memoryBytes } } function uniqueBy(items: T[], keyFn: (item: T) => string): T[] { const seen = new Set() const result: T[] = [] items.forEach((item) => { const key = keyFn(item) if (key && !seen.has(key)) { seen.add(key) result.push(item) } }) return result } export type MachinesQueryItem = { id: string tenantId: string hostname: string companyId: string | null companySlug: string | null companyName: string | null osName: string | null osVersion: string | null architecture: string | null macAddresses: string[] serialNumbers: string[] authUserId: string | null authEmail: string | null persona: string | null assignedUserId: string | null assignedUserEmail: string | null assignedUserName: string | null assignedUserRole: string | null status: string | null isActive: boolean lastHeartbeatAt: number | null heartbeatAgeMs: number | null registeredBy: string | null createdAt: number updatedAt: number token: { expiresAt: number lastUsedAt: number | null usageCount: number } | null metrics: MachineMetrics inventory: MachineInventory | null postureAlerts?: Array> | null lastPostureAt?: number | null linkedUsers?: Array<{ id: string; email: string; name: string }> remoteAccessEntries: MachineRemoteAccessEntry[] } export function normalizeMachineItem(raw: Record): MachinesQueryItem { const { remoteAccess, ...rest } = raw as Record & { remoteAccess?: unknown } return { ...(rest as MachinesQueryItem), remoteAccessEntries: normalizeMachineRemoteAccessList(remoteAccess), } } function useMachinesQuery(tenantId: string): { machines: MachinesQueryItem[]; isLoading: boolean } { const result = useQuery(api.machines.listByTenant, { tenantId, includeMetadata: true, }) as Array> | undefined const machines = useMemo(() => (result ?? []).map((item) => normalizeMachineItem(item)), [result]) return { machines, isLoading: result === undefined, } } const DEFAULT_OFFLINE_THRESHOLD_MS = 10 * 60 * 1000 const DEFAULT_STALE_THRESHOLD_MS = DEFAULT_OFFLINE_THRESHOLD_MS * 12 function parseThreshold(raw: string | undefined, fallback: number) { if (!raw) return fallback const parsed = Number(raw) if (!Number.isFinite(parsed) || parsed <= 0) return fallback return parsed } const MACHINE_OFFLINE_THRESHOLD_MS = parseThreshold(process.env.NEXT_PUBLIC_MACHINE_OFFLINE_THRESHOLD_MS, DEFAULT_OFFLINE_THRESHOLD_MS) const MACHINE_STALE_THRESHOLD_MS = parseThreshold(process.env.NEXT_PUBLIC_MACHINE_STALE_THRESHOLD_MS, DEFAULT_STALE_THRESHOLD_MS) const statusLabels: Record = { online: "Online", offline: "Offline", stale: "Sem sinal", maintenance: "Manutenção", blocked: "Bloqueada", deactivated: "Desativada", unknown: "Desconhecida", } const TICKET_PRIORITY_META: Record = { LOW: { label: "Baixa", badgeClass: "border border-slate-200 bg-slate-100 text-slate-600" }, MEDIUM: { label: "Média", badgeClass: "border border-sky-200 bg-sky-100 text-sky-700" }, HIGH: { label: "Alta", badgeClass: "border border-amber-200 bg-amber-50 text-amber-700" }, URGENT: { label: "Urgente", badgeClass: "border border-rose-200 bg-rose-50 text-rose-700" }, } function getTicketPriorityMeta(priority: TicketPriority | string | null | undefined) { if (!priority) { return { label: "Sem prioridade", badgeClass: "border border-slate-200 bg-slate-100 text-neutral-600" } } const normalized = priority.toUpperCase() return ( TICKET_PRIORITY_META[normalized] ?? { label: priority.charAt(0).toUpperCase() + priority.slice(1).toLowerCase(), badgeClass: "border border-slate-200 bg-slate-100 text-neutral-600", } ) } const statusClasses: Record = { online: "border-emerald-200 text-emerald-600", offline: "border-rose-200 text-rose-600", stale: "border-amber-200 text-amber-600", maintenance: "border-amber-300 text-amber-700", blocked: "border-orange-200 text-orange-600", deactivated: "border-slate-200 bg-slate-50 text-slate-500", unknown: "border-slate-200 text-slate-600", } const REMOTE_ACCESS_PROVIDERS = [ { value: "TEAMVIEWER", label: "TeamViewer" }, { value: "ANYDESK", label: "AnyDesk" }, { value: "SUPREMO", label: "Supremo" }, { value: "RUSTDESK", label: "RustDesk" }, { value: "QUICKSUPPORT", label: "TeamViewer QS" }, { value: "CHROME_REMOTE_DESKTOP", label: "Chrome Remote Desktop" }, { value: "DW_SERVICE", label: "DWService" }, { value: "OTHER", label: "Outro" }, ] as const type RemoteAccessProviderValue = (typeof REMOTE_ACCESS_PROVIDERS)[number]["value"] const POSTURE_ALERT_LABELS: Record = { CPU_HIGH: "CPU alta", SERVICE_DOWN: "Serviço indisponível", SMART_FAIL: "Falha SMART", } function formatPostureAlertKind(raw?: string | null): string { if (!raw) return "Alerta" const normalized = raw.toUpperCase() if (POSTURE_ALERT_LABELS[normalized]) { return POSTURE_ALERT_LABELS[normalized] } return raw .toLowerCase() .replace(/_/g, " ") .replace(/\b\w/g, (char) => char.toUpperCase()) } function postureSeverityClass(severity?: string | null) { return (severity ?? "warning").toLowerCase() === "critical" ? "border-rose-500/20 bg-rose-500/10" : "border-amber-500/20 bg-amber-500/10" } function formatRelativeTime(date?: Date | null) { if (!date) return "Nunca" try { return formatDistanceToNowStrict(date, { addSuffix: true, locale: ptBR }) } catch { return "—" } } function formatInstallDate(date?: Date | null) { if (!date) return null return `${formatAbsoluteDateTime(date)} (${formatRelativeTime(date)})` } function formatDate(date?: Date | null) { if (!date) return "—" return format(date, "dd/MM/yyyy HH:mm") } function formatAbsoluteDateTime(date?: Date | null) { if (!date) return "—" return new Intl.DateTimeFormat("pt-BR", { dateStyle: "long", timeStyle: "short", }).format(date) } function parseDateish(value: unknown): Date | null { if (!value) return null if (value instanceof Date && !Number.isNaN(value.getTime())) return value if (typeof value === "number" && Number.isFinite(value)) { const numeric = value > 1e12 ? value : value > 1e9 ? value * 1000 : value const date = new Date(numeric) return Number.isNaN(date.getTime()) ? null : date } if (typeof value === "string") { const trimmed = value.trim() if (!trimmed) return null const numericValue = Number(trimmed) if (Number.isFinite(numericValue)) { const asMs = trimmed.length >= 13 ? numericValue : numericValue * 1000 const date = new Date(asMs) if (!Number.isNaN(date.getTime())) return date } const date = new Date(trimmed) if (!Number.isNaN(date.getTime())) return date } return null } function getMetricsTimestamp(metrics: MachineMetrics): Date | null { if (!metrics || typeof metrics !== "object") return null const data = metrics as Record const candidates = [ data["collectedAt"], data["collected_at"], data["collected_at_iso"], data["collected_at_ms"], data["timestamp"], data["updatedAt"], data["updated_at"], data["createdAt"], data["created_at"], ] for (const candidate of candidates) { const parsed = parseDateish(candidate) if (parsed) return parsed } return null } function formatBytes(bytes?: number | null) { if (!bytes || Number.isNaN(bytes)) return "—" const units = ["B", "KB", "MB", "GB", "TB"] let value = bytes let unitIndex = 0 while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024 unitIndex += 1 } return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}` } function formatPercent(value?: number | null) { if (value === null || value === undefined || Number.isNaN(value)) return "—" const normalized = value > 1 ? value : value * 100 return `${normalized.toFixed(0)}%` } const BADGE_POSITIVE = "gap-1 border-emerald-500/20 bg-emerald-500/15 text-emerald-700" const BADGE_WARNING = "gap-1 border-amber-500/20 bg-amber-100 text-amber-700" const BADGE_NEUTRAL = "gap-1 border-slate-300 bg-slate-100 text-slate-600" function parseBooleanLike(value: unknown): boolean | undefined { if (typeof value === "boolean") return value if (typeof value === "number") { if (value === 1) return true if (value === 0) return false } if (typeof value === "string") { const normalized = value.trim().toLowerCase() if (["yes", "sim", "true", "enabled", "on", "ativo", "active", "y", "1"].includes(normalized)) return true if (["no", "não", "nao", "false", "disabled", "off", "inativo", "not joined", "n", "0"].includes(normalized)) return false } return undefined } function toNumberArray(value: unknown): number[] { if (!value) return [] if (Array.isArray(value)) { return value .map((item) => { if (typeof item === "number") return item if (typeof item === "string") { const parsed = Number(item) return Number.isNaN(parsed) ? null : parsed } return null }) .filter((item): item is number => item !== null) } if (typeof value === "number") return [value] if (typeof value === "string" && value.trim().length > 0) { return value .replace(/[^\d,]/g, " ") .split(/[,\s]+/) .map((part) => Number(part)) .filter((num) => !Number.isNaN(num)) } return [] } function describeDomainRole(role?: number | null): string | null { if (role === null || role === undefined) return null const map: Record = { 0: "Estação isolada", 1: "Estação em domínio", 2: "Servidor isolado", 3: "Servidor em domínio", 4: "Controlador de domínio (backup)", 5: "Controlador de domínio (primário)", } return map[role] ?? `Função ${role}` } function describePcSystemType(code?: number | null): string | null { if (code === null || code === undefined) return null const map: Record = { 0: "Não especificado", 1: "Desktop", 2: "Portátil / Laptop", 3: "Workstation", 4: "Servidor corporativo", 5: "Servidor SOHO", 8: "Tablet / Slate", 9: "Conversível", 10: "Sistema baseado em detecção", } return map[code] ?? `Tipo ${code}` } function describeDeviceGuardService(code: number): string { const map: Record = { 1: "Credential Guard", 2: "HVCI (Kernel Integrity)", 3: "Secure Boot com DMA", 4: "Hypervisor com Device Guard", 5: "Aplicação protegida", } return map[code] ?? `Serviço ${code}` } function describeVbsStatus(code?: number | null): string | null { switch (code) { case 0: return "VBS desabilitado" case 1: return "VBS habilitado (inativo)" case 2: return "VBS ativo (sem serviços)" case 3: return "VBS ativo com proteções" default: return code != null ? `VBS status ${code}` : null } } function describeAuOption(value?: number | null): string | null { switch (value ?? -1) { case 1: return "Não configurado" case 2: return "Notificar antes de baixar" case 3: return "Baixar automático e notificar" case 4: return "Baixar e instalar automaticamente" case 5: return "Administrador local define" default: return value != null ? `Opção ${value}` : null } } function describeScheduledDay(value?: number | null): string | null { if (value === null || value === undefined) return null if (value === 0) return "Todos os dias" const map = ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado"] return map[value - 1] ?? `Dia ${value}` } function getStatusVariant(status?: string | null) { if (!status) return { label: statusLabels.unknown, className: statusClasses.unknown } const normalized = status.toLowerCase() return { label: statusLabels[normalized] ?? status, className: statusClasses[normalized] ?? statusClasses.unknown, } } function resolveMachineStatus(machine: { status?: string | null; lastHeartbeatAt?: number | null; isActive?: boolean | null }): string { if (machine.isActive === false) return "deactivated" const manualStatus = (machine.status ?? "").toLowerCase() if (["maintenance", "blocked"].includes(manualStatus)) { return manualStatus } const heartbeat = machine.lastHeartbeatAt if (typeof heartbeat === "number" && Number.isFinite(heartbeat) && heartbeat > 0) { const age = Date.now() - heartbeat if (age <= MACHINE_OFFLINE_THRESHOLD_MS) return "online" if (age <= MACHINE_STALE_THRESHOLD_MS) return "offline" return "stale" } return machine.status ?? "unknown" } function OsIcon({ osName }: { osName?: string | null }) { const name = (osName ?? "").toLowerCase() if (name.includes("mac") || name.includes("darwin") || name.includes("macos")) return if (name.includes("linux")) return // fallback para Windows/outros como monitor genérico return } export function AdminMachinesOverview({ tenantId, initialCompanyFilterSlug = "all" }: { tenantId: string; initialCompanyFilterSlug?: string }) { const { machines, isLoading } = useMachinesQuery(tenantId) const [q, setQ] = useState("") const [statusFilter, setStatusFilter] = useState("all") const [companyFilterSlug, setCompanyFilterSlug] = useState(initialCompanyFilterSlug) const [companySearch, setCompanySearch] = useState("") const [isCompanyPopoverOpen, setIsCompanyPopoverOpen] = useState(false) const [onlyAlerts, setOnlyAlerts] = useState(false) const [isExportDialogOpen, setIsExportDialogOpen] = useState(false) const [exportSelection, setExportSelection] = useState([]) const [isExporting, setIsExporting] = useState(false) const [exportProgress, setExportProgress] = useState(0) const [exportError, setExportError] = useState(null) const { convexUserId } = useAuth() const companies = useQuery( convexUserId ? api.companies.list : undefined, convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : undefined ) as Array<{ id: string; name: string; slug?: string }> | undefined const companyNameBySlug = useMemo(() => { const map = new Map() machines.forEach((m) => { if (m.companySlug && m.companyName) { map.set(m.companySlug, m.companyName) } }) ;(companies ?? []).forEach((c) => { if (c.slug) { map.set(c.slug, c.name) } }) return map }, [machines, companies]) const companyOptions = useMemo(() => { if (companies && companies.length > 0) { return companies .map((c) => ({ slug: c.slug ?? c.id, name: c.name })) .sort((a, b) => a.name.localeCompare(b.name, "pt-BR")) } const fallback = new Map() machines.forEach((m) => { if (m.companySlug) { fallback.set(m.companySlug, m.companyName ?? m.companySlug) } }) return Array.from(fallback.entries()) .map(([slug, name]) => ({ slug, name })) .sort((a, b) => a.name.localeCompare(b.name, "pt-BR")) }, [companies, machines]) const filteredMachines = useMemo(() => { const text = q.trim().toLowerCase() return machines.filter((m) => { if (onlyAlerts && !(Array.isArray(m.postureAlerts) && m.postureAlerts.length > 0)) return false if (statusFilter !== "all") { const s = resolveMachineStatus(m).toLowerCase() if (s !== statusFilter) return false } if (companyFilterSlug !== "all" && (m.companySlug ?? "") !== companyFilterSlug) return false if (!text) return true const hay = [ m.hostname, m.authEmail ?? "", (m.macAddresses ?? []).join(" "), (m.serialNumbers ?? []).join(" "), ] .join(" ") .toLowerCase() return hay.includes(text) }) }, [machines, q, statusFilter, companyFilterSlug, onlyAlerts]) const handleOpenExportDialog = useCallback(() => { if (filteredMachines.length === 0) { toast.info("Não há máquinas para exportar com os filtros atuais.") return } setExportSelection(filteredMachines.map((m) => m.id)) setExportProgress(0) setExportError(null) setIsExporting(false) setIsExportDialogOpen(true) }, [filteredMachines]) const handleExportDialogOpenChange = useCallback((open: boolean) => { if (!open && isExporting) return setIsExportDialogOpen(open) }, [isExporting]) useEffect(() => { if (!isExportDialogOpen) { setExportSelection([]) setExportProgress(0) setExportError(null) setIsExporting(false) } }, [isExportDialogOpen]) useEffect(() => { if (!isExportDialogOpen) return const allowed = new Set(filteredMachines.map((m) => m.id)) setExportSelection((prev) => { const next = prev.filter((id) => allowed.has(id)) return next.length === prev.length ? prev : next }) }, [filteredMachines, isExportDialogOpen]) const handleToggleMachineSelection = useCallback((machineId: string, checked: boolean) => { setExportSelection((prev) => { if (checked) { if (prev.includes(machineId)) return prev return [...prev, machineId] } return prev.filter((id) => id !== machineId) }) }, []) const handleSelectAllMachines = useCallback((checked: boolean) => { if (checked) { setExportSelection(filteredMachines.map((m) => m.id)) } else { setExportSelection([]) } }, [filteredMachines]) const handleConfirmExport = useCallback(async () => { const orderedSelection = filteredMachines.map((m) => m.id).filter((id) => exportSelection.includes(id)) if (orderedSelection.length === 0) { toast.info("Selecione ao menos uma máquina para exportar.") return } setIsExporting(true) setExportError(null) setExportProgress(5) try { const params = new URLSearchParams() if (companyFilterSlug !== "all") { params.set("companyId", companyFilterSlug) } orderedSelection.forEach((id) => params.append("machineId", id)) const qs = params.toString() const url = `/api/reports/machines-inventory.xlsx${qs ? `?${qs}` : ""}` const response = await fetch(url) if (!response.ok) { throw new Error(`Export failed with status ${response.status}`) } const contentLengthHeader = response.headers.get("Content-Length") const totalBytes = contentLengthHeader ? parseInt(contentLengthHeader, 10) : Number.NaN const hasLength = Number.isFinite(totalBytes) && totalBytes > 0 const disposition = response.headers.get("Content-Disposition") const filenameMatch = disposition?.match(/filename="?([^";]+)"?/i) const filename = filenameMatch?.[1] ?? `machines-inventory-${new Date().toISOString().slice(0, 10)}.xlsx` let blob: Blob if (!response.body || typeof response.body.getReader !== "function") { blob = await response.blob() setExportProgress(100) } else { const reader = response.body.getReader() const chunks: ArrayBuffer[] = [] let received = 0 while (true) { const { value, done } = await reader.read() if (done) break if (value) { chunks.push(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength)) received += value.length if (hasLength) { const percent = Math.min(99, Math.round((received / totalBytes) * 100)) setExportProgress(percent) } else { setExportProgress((prev) => (prev >= 95 ? prev : prev + 5)) } } } blob = new Blob(chunks, { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }) setExportProgress(100) } const downloadUrl = window.URL.createObjectURL(blob) const link = document.createElement("a") link.href = downloadUrl link.download = filename document.body.appendChild(link) link.click() link.remove() window.URL.revokeObjectURL(downloadUrl) toast.success(`Exportação gerada para ${orderedSelection.length} máquina${orderedSelection.length === 1 ? "" : "s"}.`) setIsExporting(false) setIsExportDialogOpen(false) } catch (error) { console.error("Failed to export machines inventory", error) setIsExporting(false) setExportProgress(0) setExportError("Não foi possível gerar o arquivo. Tente novamente.") } }, [companyFilterSlug, exportSelection, filteredMachines]) const exportableCount = filteredMachines.length const selectedCount = exportSelection.length const selectAllState: boolean | "indeterminate" = exportableCount === 0 ? false : selectedCount === exportableCount ? true : selectedCount > 0 ? "indeterminate" : false return (
Máquinas registradas Sincronizadas via agente local ou Fleet. Atualiza em tempo real.
setQ(e.target.value)} placeholder="Buscar hostname, e-mail, MAC, serial..." />
setCompanySearch(e.target.value)} placeholder="Buscar empresa..." />
{companyOptions .filter((c) => c.name.toLowerCase().includes(companySearch.toLowerCase())) .map((c) => ( ))}
{isLoading ? ( ) : machines.length === 0 ? ( ) : ( )}
Exportar inventário Revise as máquinas antes de gerar o XLSX. {filteredMachines.length === 0 ? (
Nenhuma máquina disponível para exportar com os filtros atuais.
) : (
{selectedCount} de {filteredMachines.length} selecionadas
    {filteredMachines.map((machine) => { const statusKey = resolveMachineStatus(machine) const statusLabel = statusLabels[statusKey] ?? statusKey const isChecked = exportSelection.includes(machine.id) const osParts = [machine.osName ?? "", machine.osVersion ?? ""].filter(Boolean) const osLabel = osParts.join(" ") return (
  • ) })}
)} {isExporting ? (
Gerando planilha... {Math.min(100, Math.max(0, Math.round(exportProgress)))}%
) : null} {exportError ?

{exportError}

: null}
) } function MachineStatusBadge({ status }: { status?: string | null }) { const { label, className } = getStatusVariant(status) const s = String(status ?? "").toLowerCase() const colorClass = s === "online" ? "bg-emerald-500" : s === "offline" ? "bg-rose-500" : s === "stale" ? "bg-amber-500" : s === "maintenance" ? "bg-amber-500" : s === "blocked" ? "bg-orange-500" : s === "deactivated" ? "bg-slate-500" : "bg-slate-400" const ringClass = s === "online" ? "bg-emerald-400/30" : s === "offline" ? "bg-rose-400/30" : s === "stale" ? "bg-amber-400/30" : s === "maintenance" ? "bg-amber-400/30" : s === "blocked" ? "bg-orange-400/30" : s === "deactivated" ? "bg-slate-400/40" : "bg-slate-300/30" const isOnline = s === "online" return ( {isOnline ? ( ) : null} {label} ) } function EmptyState() { return (

Nenhuma máquina registrada ainda

Execute o agente local ou o webhook do Fleet para registrar as máquinas do tenant.

) } function LoadingState() { return (

Carregando máquinas...

Sincronizando o inventário em tempo real. Isso leva apenas alguns instantes.

) } type MachineDetailsProps = { machine: MachinesQueryItem | null } export function MachineDetails({ machine }: MachineDetailsProps) { const router = useRouter() const { role: viewerRole } = useAuth() const normalizedViewerRole = (viewerRole ?? "").toLowerCase() const canManageRemoteAccess = normalizedViewerRole === "admin" || normalizedViewerRole === "agent" const effectiveStatus = machine ? resolveMachineStatus(machine) : "unknown" const [isActiveLocal, setIsActiveLocal] = useState(machine?.isActive ?? true) const isDeactivated = !isActiveLocal || effectiveStatus === "deactivated" const alertsHistory = useQuery( machine ? api.machines.listAlerts : undefined, machine ? { machineId: machine.id as Id<"machines">, limit: 50 } : undefined ) as MachineAlertEntry[] | undefined const machineAlertsHistory = alertsHistory ?? [] const openTickets = useQuery( machine ? api.machines.listOpenTickets : undefined, machine ? { machineId: machine.id as Id<"machines">, limit: 6 } : undefined ) as MachineOpenTicketsSummary | undefined const machineTickets = openTickets?.tickets ?? [] const totalOpenTickets = openTickets?.totalOpen ?? machineTickets.length const displayLimit = 3 const displayedMachineTickets = machineTickets.slice(0, displayLimit) const hasAdditionalOpenTickets = totalOpenTickets > displayedMachineTickets.length const machineTicketsHref = machine ? `/admin/machines/${machine.id}/tickets` : null const metadata = machine?.inventory ?? null const metrics = machine?.metrics ?? null const metricsCapturedAt = useMemo(() => getMetricsTimestamp(metrics), [metrics]) // Live refresh the relative time label every second when we have a capture timestamp const [, setRelativeTick] = useState(0) useEffect(() => { if (!metricsCapturedAt) return const id = setInterval(() => setRelativeTick((t) => t + 1), 1000) return () => clearInterval(id) }, [metricsCapturedAt]) const lastUpdateRelative = metricsCapturedAt ? formatRelativeTime(metricsCapturedAt) : null const hardware = metadata?.hardware const network = metadata?.network ?? null const networkInterfaces = Array.isArray(network) ? network : null const networkSummary = !Array.isArray(network) && network ? network : null const software = metadata?.software ?? null const labels = metadata?.labels ?? null const fleet = metadata?.fleet ?? null const disks = Array.isArray(metadata?.disks) ? metadata.disks : [] const extended = metadata?.extended ?? null const linuxExt = extended?.linux ?? null const windowsExt = extended?.windows ?? null const macosExt = extended?.macos ?? null const windowsOsInfo = parseWindowsOsInfo(windowsExt?.osInfo) const windowsActivationStatus = windowsOsInfo?.isActivated ?? (typeof windowsOsInfo?.licenseStatus === "number" ? windowsOsInfo.licenseStatus === 1 : null) const windowsMemoryModulesRaw = windowsExt?.memoryModules const windowsVideoControllersRaw = windowsExt?.videoControllers const windowsDiskEntriesRaw = windowsExt?.disks const windowsServicesRaw = windowsExt?.services const windowsSoftwareRaw = windowsExt?.software const windowsBaseboardRaw = windowsExt?.baseboard const windowsBaseboard = Array.isArray(windowsBaseboardRaw) ? windowsBaseboardRaw[0] : windowsBaseboardRaw && typeof windowsBaseboardRaw === "object" ? windowsBaseboardRaw : null const windowsSerialNumber = windowsBaseboard ? readString(toRecord(windowsBaseboard) ?? {}, "SerialNumber", "serialNumber") : undefined const windowsMemoryModules = useMemo(() => { if (Array.isArray(windowsMemoryModulesRaw)) return windowsMemoryModulesRaw if (windowsMemoryModulesRaw && typeof windowsMemoryModulesRaw === "object") return [windowsMemoryModulesRaw] return [] }, [windowsMemoryModulesRaw]) const windowsVideoControllers = useMemo(() => { if (Array.isArray(windowsVideoControllersRaw)) return windowsVideoControllersRaw if (windowsVideoControllersRaw && typeof windowsVideoControllersRaw === "object") return [windowsVideoControllersRaw] return [] }, [windowsVideoControllersRaw]) const windowsDiskEntries = useMemo(() => { if (Array.isArray(windowsDiskEntriesRaw)) return windowsDiskEntriesRaw if (windowsDiskEntriesRaw && typeof windowsDiskEntriesRaw === "object") return [windowsDiskEntriesRaw] return [] }, [windowsDiskEntriesRaw]) const windowsServices = useMemo(() => { if (Array.isArray(windowsServicesRaw)) return windowsServicesRaw if (windowsServicesRaw && typeof windowsServicesRaw === "object") return [windowsServicesRaw] return [] }, [windowsServicesRaw]) const windowsSoftware = useMemo(() => { if (Array.isArray(windowsSoftwareRaw)) return windowsSoftwareRaw if (windowsSoftwareRaw && typeof windowsSoftwareRaw === "object") return [windowsSoftwareRaw] return [] }, [windowsSoftwareRaw]) const normalizedWindowsSoftware = useMemo(() => { return windowsSoftware .map((item) => normalizeWindowsSoftwareEntry(item)) .filter((entry): entry is NormalizedSoftwareEntry => Boolean(entry)) .sort((a, b) => { const aTime = a.installDate ? a.installDate.getTime() : 0 const bTime = b.installDate ? b.installDate.getTime() : 0 if (aTime !== bTime) return bTime - aTime return a.name.localeCompare(b.name, "pt-BR") }) }, [windowsSoftware]) const windowsEditionLabel = useMemo(() => { const raw = windowsOsInfo?.productName ?? windowsOsInfo?.caption ?? windowsOsInfo?.editionId ?? null if (!raw) return null return raw.replace(/^Microsoft\s+/i, "").trim() }, [windowsOsInfo?.productName, windowsOsInfo?.caption, windowsOsInfo?.editionId]) const windowsVersionLabel = windowsOsInfo?.displayVersion ?? windowsOsInfo?.version ?? windowsOsInfo?.releaseId ?? null const windowsBuildLabel = windowsOsInfo?.currentBuildNumber ?? windowsOsInfo?.currentBuild ?? null const windowsInstallDateLabel = windowsOsInfo?.installDate ? formatAbsoluteDateTime(windowsOsInfo.installDate) : null const windowsExperienceLabel = windowsOsInfo?.experience ?? null const windowsProductId = windowsOsInfo?.productId ?? null const windowsPartialProductKey = windowsOsInfo?.partialProductKey ?? null const windowsComputerName = windowsOsInfo?.computerName ?? hardware?.model ?? machine?.hostname ?? null const windowsRegisteredOwner = windowsOsInfo?.registeredOwner ?? null const windowsLicenseStatusLabel = (() => { if (windowsOsInfo?.licenseStatusText) { return windowsOsInfo.licenseStatusText } switch (windowsOsInfo?.licenseStatus) { case 0: return "Sem licença" case 1: return "Licenciado" case 2: return "Período inicial (OOB Grace)" case 3: return "Período exposto (OOT Grace)" case 4: return "Período não genuíno" case 5: return "Notificação" case 6: return "Período estendido" default: return null } })() const windowsBitLockerRaw = windowsExt?.bitLocker ?? windowsExt?.bitlocker ?? null const windowsBitLockerVolumes = useMemo(() => { if (Array.isArray(windowsBitLockerRaw)) return windowsBitLockerRaw if (windowsBitLockerRaw && typeof windowsBitLockerRaw === "object") return [windowsBitLockerRaw] return [] }, [windowsBitLockerRaw]) const windowsBitLockerSummary = useMemo(() => { if (windowsBitLockerVolumes.length === 0) return null let protectedCount = 0 let lockedCount = 0 windowsBitLockerVolumes.forEach((volume) => { const record = toRecord(volume) ?? {} const protection = readString(record, "ProtectionStatus", "protectionStatus")?.toLowerCase() if (protection && (protection.includes("on") || protection.includes("ativo"))) { protectedCount += 1 } const lockStatus = readString(record, "LockStatus", "lockStatus")?.toLowerCase() if (lockStatus && (lockStatus.includes("locked") || lockStatus.includes("bloqueado"))) { lockedCount += 1 } }) return { total: windowsBitLockerVolumes.length, protectedCount, lockedCount } }, [windowsBitLockerVolumes]) const windowsTpm = toRecord(windowsExt?.tpm ?? (windowsExt as Record | undefined)?.["TPM"]) const windowsSecureBoot = toRecord(windowsExt?.secureBoot ?? (windowsExt as Record | undefined)?.["secureboot"]) const windowsDeviceGuardRaw = windowsExt?.deviceGuard ?? (windowsExt as Record | undefined)?.["deviceguard"] ?? null const windowsDeviceGuard = useMemo(() => { if (Array.isArray(windowsDeviceGuardRaw)) return windowsDeviceGuardRaw if (windowsDeviceGuardRaw && typeof windowsDeviceGuardRaw === "object") return [windowsDeviceGuardRaw] return [] }, [windowsDeviceGuardRaw]) const windowsDeviceGuardDetails = useMemo(() => { if (!windowsDeviceGuard.length) return null const primary = toRecord(windowsDeviceGuard[0]) ?? {} const configured = toNumberArray(primary?.["SecurityServicesConfigured"]) const running = toNumberArray(primary?.["SecurityServicesRunning"]) const required = toNumberArray(primary?.["RequiredSecurityProperties"]) const available = toNumberArray(primary?.["AvailableSecurityProperties"]) const vbsRaw = primary?.["VirtualizationBasedSecurityStatus"] const vbs = typeof vbsRaw === "number" ? vbsRaw : typeof vbsRaw === "string" && vbsRaw.trim().length > 0 ? Number(vbsRaw) : undefined return { primary, configured, running, required, available, vbs } }, [windowsDeviceGuard]) const deviceGuardConfiguredLabels = windowsDeviceGuardDetails?.configured?.map((code) => describeDeviceGuardService(code)) ?? [] const deviceGuardRunningLabels = windowsDeviceGuardDetails?.running?.map((code) => describeDeviceGuardService(code)) ?? [] const windowsFirewallProfilesRaw = windowsExt?.firewallProfiles ?? (windowsExt as Record | undefined)?.["firewallprofiles"] ?? null const windowsFirewallProfiles = useMemo(() => { if (Array.isArray(windowsFirewallProfilesRaw)) return windowsFirewallProfilesRaw if (windowsFirewallProfilesRaw && typeof windowsFirewallProfilesRaw === "object") return [windowsFirewallProfilesRaw] return [] }, [windowsFirewallProfilesRaw]) const windowsUpdateSettings = toRecord(windowsExt?.windowsUpdate ?? (windowsExt as Record | undefined)?.["windowsupdate"]) const windowsUpdateLastSuccess = useMemo(() => { if (!windowsUpdateSettings) return null const candidates = [ windowsUpdateSettings["LastSuccessTime"], windowsUpdateSettings["lastSuccessTime"], windowsUpdateSettings["LastSuccessTimeUtc"], windowsUpdateSettings["lastSuccessTimeUtc"], ] for (const candidate of candidates) { const parsed = parseDateish(candidate) if (parsed) return parsed if (typeof candidate === "string" && candidate.trim().length > 0) { const parsedIso = new Date(candidate) if (!Number.isNaN(parsedIso.getTime())) return parsedIso } } return null }, [windowsUpdateSettings]) const windowsUpdateLastSuccessLabel = windowsUpdateLastSuccess ? formatAbsoluteDateTime(windowsUpdateLastSuccess) : null const windowsComputerSystem = toRecord( windowsExt?.computerSystem ?? (windowsExt as Record | undefined)?.["computersystem"] ) const windowsAzureAdStatusRaw = toRecord( windowsExt?.azureAdStatus ?? (windowsExt as Record | undefined)?.["azureadstatus"] ) const windowsAzureAdStatus = useMemo(() => { if (!windowsAzureAdStatusRaw) return null return Object.entries(windowsAzureAdStatusRaw).reduce>>((acc, [section, value]) => { acc[section] = toRecord(value) ?? {} return acc }, {}) }, [windowsAzureAdStatusRaw]) const secureBootSupported = windowsSecureBoot ? parseBooleanLike(windowsSecureBoot["Supported"] ?? windowsSecureBoot["supported"]) : undefined const secureBootEnabled = windowsSecureBoot ? parseBooleanLike(windowsSecureBoot["Enabled"] ?? windowsSecureBoot["enabled"]) : undefined const secureBootError = windowsSecureBoot ? readString(windowsSecureBoot, "Error", "error") : undefined const tpmPresent = windowsTpm ? parseBooleanLike(windowsTpm["TpmPresent"] ?? windowsTpm["tpmPresent"]) : undefined const tpmReady = windowsTpm ? parseBooleanLike(windowsTpm["TpmReady"] ?? windowsTpm["tpmReady"]) : undefined const tpmEnabled = windowsTpm ? parseBooleanLike(windowsTpm["TpmEnabled"] ?? windowsTpm["tpmEnabled"]) : undefined const tpmActivated = windowsTpm ? parseBooleanLike(windowsTpm["TpmActivated"] ?? windowsTpm["tpmActivated"]) : undefined const tpmManufacturer = windowsTpm ? readString(windowsTpm, "ManufacturerIdTxt", "manufacturerIdTxt", "ManufacturerId", "manufacturerId") : undefined const tpmVersion = windowsTpm ? readString(windowsTpm, "ManufacturerVersionFull20", "manufacturerVersionFull20", "ManufacturerVersion", "manufacturerVersion", "SpecVersion") : undefined const windowsFirewallNormalized = useMemo(() => { return windowsFirewallProfiles.map((profile) => { const record = toRecord(profile) ?? {} return { name: readString(record, "Name", "name") ?? "Perfil", enabled: parseBooleanLike(record["Enabled"] ?? record["enabled"]), inboundAction: readString(record, "DefaultInboundAction", "defaultInboundAction"), outboundAction: readString(record, "DefaultOutboundAction", "defaultOutboundAction"), notifyOnListen: parseBooleanLike(record["NotifyOnListen"] ?? record["notifyOnListen"]), } }) }, [windowsFirewallProfiles]) const firewallEnabledCount = windowsFirewallNormalized.filter((profile) => profile.enabled !== false).length const windowsUpdateMode = windowsUpdateSettings ? describeAuOption(readNumber(windowsUpdateSettings, "AUOptions", "auOptions")) : null const windowsUpdateDay = windowsUpdateSettings ? describeScheduledDay(readNumber(windowsUpdateSettings, "ScheduledInstallDay", "scheduledInstallDay")) : null const windowsUpdateHourRaw = windowsUpdateSettings ? readNumber(windowsUpdateSettings, "ScheduledInstallTime", "scheduledInstallTime") : undefined const windowsUpdateHour = windowsUpdateHourRaw != null ? `${windowsUpdateHourRaw.toString().padStart(2, "0")}h` : null const windowsUpdateDisabled = windowsUpdateSettings ? parseBooleanLike(windowsUpdateSettings["NoAutoUpdate"] ?? windowsUpdateSettings["noAutoUpdate"]) : undefined const windowsUpdateDetectionEnabled = windowsUpdateSettings ? parseBooleanLike( windowsUpdateSettings["DetectionFrequency"] ?? windowsUpdateSettings["DetectionFrequencyEnabled"] ?? windowsUpdateSettings["detectionFrequencyEnabled"], ) : undefined const windowsUpdateScheduleLabel = windowsUpdateDay || windowsUpdateHour ? `${windowsUpdateDay ?? ""}${windowsUpdateDay && windowsUpdateHour ? ` · ${windowsUpdateHour}` : windowsUpdateHour ?? ""}`.trim() : null const computerDomain = windowsComputerSystem ? readString(windowsComputerSystem, "Domain", "domain") : undefined const computerWorkgroup = windowsComputerSystem ? readString(windowsComputerSystem, "Workgroup", "workgroup") : undefined const computerPartOfDomain = windowsComputerSystem ? parseBooleanLike(windowsComputerSystem["PartOfDomain"] ?? windowsComputerSystem["partOfDomain"]) : undefined const computerDomainRole = windowsComputerSystem ? readNumber(windowsComputerSystem, "DomainRole", "domainRole") : undefined const computerManufacturer = windowsComputerSystem ? readString(windowsComputerSystem, "Manufacturer", "manufacturer") : undefined const computerModel = windowsComputerSystem ? readString(windowsComputerSystem, "Model", "model") : undefined const computerPcType = windowsComputerSystem ? readNumber(windowsComputerSystem, "PCSystemType", "pcSystemType", "PCSystemTypeEx", "pcSystemTypeEx") : undefined const computerTotalMemory = windowsComputerSystem ? readNumber(windowsComputerSystem, "TotalPhysicalMemory", "totalPhysicalMemory") : undefined const computerDomainRoleLabel = describeDomainRole(computerDomainRole) const computerPcTypeLabel = describePcSystemType(computerPcType) const computerTotalMemoryLabel = typeof computerTotalMemory === "number" ? formatBytes(computerTotalMemory) : null const azureDeviceState = windowsAzureAdStatus ? windowsAzureAdStatus["Device State"] ?? null : null const azureTenantDetails = windowsAzureAdStatus ? windowsAzureAdStatus["Tenant Details"] ?? null : null const azureUserState = windowsAzureAdStatus ? windowsAzureAdStatus["User State"] ?? null : null const azureAdJoined = azureDeviceState ? parseBooleanLike(azureDeviceState["AzureAdJoined"]) : undefined const azureDomainJoined = azureDeviceState ? parseBooleanLike(azureDeviceState["DomainJoined"]) : undefined const azureEnterpriseJoined = azureDeviceState ? parseBooleanLike(azureDeviceState["EnterpriseJoined"]) : undefined const azureTenantName = (azureDeviceState ? readString(azureDeviceState, "TenantName") : undefined) ?? (azureTenantDetails ? readString(azureTenantDetails, "TenantName") : undefined) const azureDeviceId = (azureDeviceState ? readString(azureDeviceState, "DeviceId") : undefined) ?? (azureTenantDetails ? readString(azureTenantDetails, "DeviceId") : undefined) const azureUserSso = azureUserState ? readString(azureUserState, "AzureAdPrt") ?? (azureUserState["AzureAdPrt"] ? String(azureUserState["AzureAdPrt"]) : undefined) : undefined const windowsDefender = windowsExt?.defender ?? null const defenderAntivirus = windowsDefender ? parseBooleanLike(windowsDefender["AntivirusEnabled"] ?? windowsDefender["antivirusEnabled"]) : undefined const defenderRealtime = windowsDefender ? parseBooleanLike(windowsDefender["RealTimeProtectionEnabled"] ?? windowsDefender["realTimeProtectionEnabled"]) : undefined const defenderMode = windowsDefender ? readString(windowsDefender, "AMRunningMode", "amRunningMode") : undefined const windowsHotfixes = useMemo(() => { if (!Array.isArray(windowsExt?.hotfix)) return [] return windowsExt.hotfix .map((entry) => { const record = toRecord(entry) ?? {} const id = readString(record, "HotFixID", "hotFixId", "HotfixId", "Id", "id") ?? "Atualização" const installedAt = parseDateish(record["InstalledOn"] ?? record["installedOn"]) const installedLabel = installedAt ? formatAbsoluteDateTime(installedAt) : readString(record, "InstalledOn", "installedOn") ?? "—" return { id, installedAt, installedLabel } }) .sort((a, b) => { if (a.installedAt && b.installedAt) { return b.installedAt.getTime() - a.installedAt.getTime() } if (a.installedAt) return -1 if (b.installedAt) return 1 return a.id.localeCompare(b.id) }) }, [windowsExt?.hotfix]) const osNameDisplay = useMemo(() => { const base = machine?.osName?.trim() const edition = windowsEditionLabel?.trim() if (edition) { if (!base) return edition const baseLower = base.toLowerCase() const editionLower = edition.toLowerCase() if (editionLower.includes(baseLower) || baseLower.includes(editionLower)) { return edition } if (baseLower.startsWith("windows") && editionLower.startsWith("windows")) { return edition } return `${base} ${edition}`.replace(/\s+/g, " ").trim() } return base ?? "" }, [machine?.osName, windowsEditionLabel]) const linuxLsblk = linuxExt?.lsblk ?? [] const linuxSmartEntries = linuxExt?.smart ?? [] const normalizedHardwareGpus = Array.isArray(hardware?.gpus) ? hardware.gpus.map((gpu) => normalizeGpuSource(gpu)).filter((gpu): gpu is GpuAdapter => Boolean(gpu)) : [] const hardwarePrimaryGpu = hardware?.primaryGpu ? normalizeGpuSource(hardware.primaryGpu) : null const windowsCpuRaw = windowsExt?.cpu const winCpu = windowsCpuRaw ? (Array.isArray(windowsCpuRaw) ? windowsCpuRaw[0] ?? null : windowsCpuRaw) : null const winMemTotal = windowsMemoryModules.reduce((acc, module) => acc + (parseBytesLike(module?.Capacity) ?? 0), 0) const normalizedWindowsGpus = windowsVideoControllers .map((controller) => normalizeGpuSource(controller)) .filter((gpu): gpu is GpuAdapter => Boolean(gpu)) const combinedGpus = uniqueBy( [ ...(hardwarePrimaryGpu ? [hardwarePrimaryGpu] : []), ...normalizedHardwareGpus, ...normalizedWindowsGpus, ], (gpu) => `${gpu.name ?? ""}|${gpu.vendor ?? ""}|${gpu.driver ?? ""}` ) const displayGpus = [...combinedGpus].sort( (a, b) => (b.memoryBytes ?? 0) - (a.memoryBytes ?? 0) ) const primaryGpu = hardwarePrimaryGpu ?? displayGpus[0] ?? null const windowsPrimaryGpu = [...normalizedWindowsGpus].sort( (a, b) => (b.memoryBytes ?? 0) - (a.memoryBytes ?? 0) )[0] ?? null const windowsCpuDetails = windowsCpuRaw ? Array.isArray(windowsCpuRaw) ? windowsCpuRaw : [windowsCpuRaw] : [] const winDiskStats = windowsDiskEntries.length > 0 ? { count: windowsDiskEntries.length, total: windowsDiskEntries.reduce((acc, disk) => acc + (parseBytesLike(disk?.Size) ?? 0), 0), } : { count: 0, total: 0 } const lastHeartbeatDate = machine?.lastHeartbeatAt ? new Date(machine.lastHeartbeatAt) : null const tokenExpiry = machine?.token?.expiresAt ? new Date(machine.token.expiresAt) : null const tokenLastUsed = machine?.token?.lastUsedAt ? new Date(machine.token.lastUsedAt) : null const copyEmail = async () => { if (!machine?.authEmail) return try { await navigator.clipboard.writeText(machine.authEmail) toast.success("E-mail da máquina copiado.") } catch { toast.error("Não foi possível copiar o e-mail da máquina.") } } // collaborator (from machine assignment or metadata) type Collaborator = { email?: string; name?: string; role?: string } const collaborator: Collaborator | null = useMemo(() => { if (machine?.assignedUserEmail) { return { email: machine.assignedUserEmail ?? undefined, name: machine.assignedUserName ?? undefined, role: machine.persona ?? machine.assignedUserRole ?? undefined, } } if (!metadata || typeof metadata !== "object") return null const inv = metadata as Record const c = inv["collaborator"] if (c && typeof c === "object") { const base = c as Record return { email: typeof base.email === "string" ? base.email : undefined, name: typeof base.name === "string" ? base.name : undefined, role: typeof base.role === "string" ? (base.role as string) : undefined, } } return null }, [machine?.assignedUserEmail, machine?.assignedUserName, machine?.persona, machine?.assignedUserRole, metadata]) const primaryLinkedUser: Collaborator | null = useMemo(() => { const firstLinked = machine?.linkedUsers?.find((user) => typeof user?.email === "string" && user.email.trim().length > 0) if (firstLinked) { return { email: firstLinked.email, name: firstLinked.name ?? undefined, role: collaborator?.role ?? machine?.persona ?? undefined, } } if (collaborator?.email) { return collaborator } if (machine?.authEmail) { return { email: machine.authEmail ?? undefined, name: undefined, role: machine?.persona ?? undefined, } } return null }, [collaborator, machine?.authEmail, machine?.linkedUsers, machine?.persona]) const personaRole = (primaryLinkedUser?.role ?? collaborator?.role ?? machine?.persona ?? "").toLowerCase() const personaLabel = personaRole === "manager" ? "Gestor" : "Colaborador" const remoteAccessEntries = useMemo(() => machine?.remoteAccessEntries ?? [], [machine?.remoteAccessEntries]) const hasRemoteAccess = remoteAccessEntries.length > 0 const summaryChips = useMemo(() => { const chips: Array<{ key: string; label: string; value: string; icon: ReactNode; tone?: "warning" | "muted" }> = [] const osName = osNameDisplay || "Sistema desconhecido" const osVersionRaw = machine?.osVersion ?? windowsVersionLabel ?? "" const osVersion = formatOsVersionDisplay(osNameDisplay, osVersionRaw) chips.push({ key: "os", label: "Sistema", value: [osName, osVersion].filter(Boolean).join(" ").trim(), icon: , }) if (machine?.architecture) { chips.push({ key: "arch", label: "Arquitetura", value: machine.architecture.toUpperCase(), icon: , }) } if (windowsBuildLabel) { chips.push({ key: "build", label: "Build", value: windowsBuildLabel, icon: , }) } if (windowsActivationStatus !== null && windowsActivationStatus !== undefined) { chips.push({ key: "activation", label: "Licença", value: windowsActivationStatus ? "Ativada" : "Não ativada", icon: windowsActivationStatus ? : , tone: windowsActivationStatus ? undefined : "warning", }) } if (primaryLinkedUser?.email) { const collaboratorValue = primaryLinkedUser.name ? `${primaryLinkedUser.name} · ${primaryLinkedUser.email}` : primaryLinkedUser.email chips.push({ key: "collaborator", label: personaLabel, value: collaboratorValue, icon: , }) } const primaryRemoteAccess = remoteAccessEntries[0] if (primaryRemoteAccess && (primaryRemoteAccess.identifier || primaryRemoteAccess.url)) { const value = primaryRemoteAccess.identifier ?? primaryRemoteAccess.url ?? "—" const label = primaryRemoteAccess.provider ? `Acesso (${primaryRemoteAccess.provider})` : "Acesso remoto" chips.push({ key: "remote-access", label, value, icon: , }) } return chips }, [ osNameDisplay, machine?.osVersion, machine?.architecture, windowsVersionLabel, windowsBuildLabel, windowsActivationStatus, primaryLinkedUser?.email, primaryLinkedUser?.name, personaLabel, machine?.osName, remoteAccessEntries, ]) const companyName = machine?.companyName ?? machine?.companySlug ?? null const [renaming, setRenaming] = useState(false) const [newName, setNewName] = useState(machine?.hostname ?? "") const [openDialog, setOpenDialog] = useState(false) const [dialogQuery, setDialogQuery] = useState("") const [deleteDialog, setDeleteDialog] = useState(false) const [deleting, setDeleting] = useState(false) const [accessDialog, setAccessDialog] = useState(false) const [accessEmail, setAccessEmail] = useState(primaryLinkedUser?.email ?? "") const [accessName, setAccessName] = useState(primaryLinkedUser?.name ?? "") const [accessRole, setAccessRole] = useState<"collaborator" | "manager">(personaRole === "manager" ? "manager" : "collaborator") const [savingAccess, setSavingAccess] = useState(false) const [remoteAccessDialog, setRemoteAccessDialog] = useState(false) const [editingRemoteAccessClientId, setEditingRemoteAccessClientId] = useState(null) const [remoteAccessProviderOption, setRemoteAccessProviderOption] = useState( REMOTE_ACCESS_PROVIDERS[0].value, ) const [remoteAccessCustomProvider, setRemoteAccessCustomProvider] = useState("") const [remoteAccessIdentifierInput, setRemoteAccessIdentifierInput] = useState("") const [remoteAccessUrlInput, setRemoteAccessUrlInput] = useState("") const [remoteAccessNotesInput, setRemoteAccessNotesInput] = useState("") const [remoteAccessSaving, setRemoteAccessSaving] = useState(false) const editingRemoteAccess = useMemo( () => remoteAccessEntries.find((entry) => entry.clientId === editingRemoteAccessClientId) ?? null, [editingRemoteAccessClientId, remoteAccessEntries] ) const [togglingActive, setTogglingActive] = useState(false) const [showAllWindowsSoftware, setShowAllWindowsSoftware] = useState(false) const jsonText = useMemo(() => { const payload = { id: machine?.id, hostname: machine?.hostname, status: machine?.status, lastHeartbeatAt: machine?.lastHeartbeatAt, metrics, inventory: metadata, postureAlerts: machine?.postureAlerts ?? null, lastPostureAt: machine?.lastPostureAt ?? null, } return JSON.stringify(payload, null, 2) }, [machine, metrics, metadata]) const handleDownloadInventoryJson = useCallback(() => { if (!machine) return const safeHostname = machine.hostname.replace(/[^a-z0-9_-]/gi, "-").replace(/-{2,}/g, "-").toLowerCase() const fileName = `${safeHostname || "machine"}_${machine.id}.json` const blob = new Blob([jsonText], { type: "application/json" }) const url = URL.createObjectURL(blob) const link = document.createElement("a") link.href = url link.download = fileName document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) }, [jsonText, machine]) const filteredJsonHtml = useMemo(() => { if (!dialogQuery.trim()) return jsonText const q = dialogQuery.trim().toLowerCase() // highlight simples return jsonText.replace(new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"), (m) => `__HIGHLIGHT__${m}__END__`) }, [jsonText, dialogQuery]) // removed copy/export inventory JSON buttons as requested useEffect(() => { setAccessEmail(primaryLinkedUser?.email ?? "") setAccessName(primaryLinkedUser?.name ?? "") setAccessRole(personaRole === "manager" ? "manager" : "collaborator") }, [machine?.id, primaryLinkedUser?.email, primaryLinkedUser?.name, personaRole]) useEffect(() => { setIsActiveLocal(machine?.isActive ?? true) }, [machine?.isActive]) useEffect(() => { if (!remoteAccessDialog) return const providerName = editingRemoteAccess?.provider ?? "" const matched = REMOTE_ACCESS_PROVIDERS.find( (option) => option.value !== "OTHER" && option.label.toLowerCase() === providerName.toLowerCase(), ) if (matched) { setRemoteAccessProviderOption(matched.value) setRemoteAccessCustomProvider("") } else { setRemoteAccessProviderOption(providerName ? "OTHER" : REMOTE_ACCESS_PROVIDERS[0].value) setRemoteAccessCustomProvider(providerName ?? "") } setRemoteAccessIdentifierInput(editingRemoteAccess?.identifier ?? "") setRemoteAccessUrlInput(editingRemoteAccess?.url ?? "") setRemoteAccessNotesInput(editingRemoteAccess?.notes ?? "") }, [remoteAccessDialog, editingRemoteAccess]) useEffect(() => { if (remoteAccessDialog) return if (!editingRemoteAccessClientId) { setRemoteAccessProviderOption(REMOTE_ACCESS_PROVIDERS[0].value) setRemoteAccessCustomProvider("") setRemoteAccessIdentifierInput("") setRemoteAccessUrlInput("") setRemoteAccessNotesInput("") } }, [editingRemoteAccessClientId, remoteAccessDialog]) useEffect(() => { setShowAllWindowsSoftware(false) }, [machine?.id]) const displayedWindowsSoftware = useMemo( () => (showAllWindowsSoftware ? normalizedWindowsSoftware : normalizedWindowsSoftware.slice(0, 12)), [showAllWindowsSoftware, normalizedWindowsSoftware] ) const handleSaveAccess = async () => { if (!machine) return if (!accessEmail.trim()) { toast.error("Informe o e-mail do colaborador ou gestor.") return } setSavingAccess(true) try { const response = await fetch("/api/admin/machines/access", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ machineId: machine.id, persona: accessRole, email: accessEmail.trim(), name: accessName.trim() || undefined, }), }) if (!response.ok) { throw new Error(await response.text()) } toast.success("Perfil de acesso atualizado.") setAccessDialog(false) } catch (error) { console.error(error) toast.error("Falha ao atualizar acesso da máquina.") } finally { setSavingAccess(false) } } const handleSaveRemoteAccess = useCallback(async () => { if (!machine) return if (!canManageRemoteAccess) { toast.error("Você não tem permissão para ajustar o acesso remoto desta máquina.") return } const providerOption = REMOTE_ACCESS_PROVIDERS.find((option) => option.value === remoteAccessProviderOption) const providerName = remoteAccessProviderOption === "OTHER" ? remoteAccessCustomProvider.trim() : providerOption?.label ?? "" if (!providerName) { toast.error("Informe a ferramenta de acesso remoto.") return } const identifier = remoteAccessIdentifierInput.trim() if (!identifier) { toast.error("Informe o ID ou código do acesso remoto.") return } let normalizedUrl: string | undefined const rawUrl = remoteAccessUrlInput.trim() if (rawUrl.length > 0) { const candidate = /^https?:\/\//i.test(rawUrl) ? rawUrl : `https://${rawUrl}` try { new URL(candidate) normalizedUrl = candidate } catch { toast.error("Informe uma URL válida (ex: https://example.com).") return } } const notes = remoteAccessNotesInput.trim() toast.dismiss("remote-access") toast.loading("Salvando acesso remoto...", { id: "remote-access" }) setRemoteAccessSaving(true) try { if (editingRemoteAccess && !editingRemoteAccess.id) { const cleanupPayload: Record = { machineId: machine.id, action: "delete", } if (editingRemoteAccess.provider) cleanupPayload.provider = editingRemoteAccess.provider if (editingRemoteAccess.identifier) cleanupPayload.identifier = editingRemoteAccess.identifier if (editingRemoteAccess.clientId) cleanupPayload.entryId = editingRemoteAccess.clientId await fetch("/api/admin/machines/remote-access", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(cleanupPayload), }).catch(() => null) } const response = await fetch("/api/admin/machines/remote-access", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ machineId: machine.id, provider: providerName, identifier, url: normalizedUrl, notes: notes.length ? notes : undefined, action: "upsert", entryId: editingRemoteAccess?.id ?? undefined, }), }) const responsePayload = await response.json().catch(() => null) if (!response.ok) { const message = typeof responsePayload?.error === "string" ? responsePayload.error : "Falha ao atualizar acesso remoto." const detailMessage = typeof responsePayload?.detail === "string" ? responsePayload.detail : null throw new Error(detailMessage ? `${message}. ${detailMessage}` : message) } toast.success("Acesso remoto atualizado.", { id: "remote-access" }) setRemoteAccessDialog(false) setEditingRemoteAccessClientId(null) } catch (error) { const message = error instanceof Error ? error.message : "Falha ao atualizar acesso remoto." toast.error(message, { id: "remote-access" }) } finally { setRemoteAccessSaving(false) } }, [ machine, canManageRemoteAccess, remoteAccessProviderOption, remoteAccessCustomProvider, remoteAccessIdentifierInput, remoteAccessUrlInput, remoteAccessNotesInput, editingRemoteAccess, ]) const handleRemoveRemoteAccess = useCallback(async (entry: MachineRemoteAccessEntry) => { if (!machine) return if (!canManageRemoteAccess) { toast.error("Você não tem permissão para ajustar o acesso remoto desta máquina.") return } toast.dismiss("remote-access") toast.loading("Removendo acesso remoto...", { id: "remote-access" }) setRemoteAccessSaving(true) try { const requestPayload: Record = { machineId: machine.id, action: "delete", } if (entry.id) { requestPayload.entryId = entry.id } else if (entry.clientId) { requestPayload.entryId = entry.clientId } if (entry.provider) { requestPayload.provider = entry.provider } if (entry.identifier) { requestPayload.identifier = entry.identifier } const response = await fetch("/api/admin/machines/remote-access", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestPayload), }) const responsePayload = await response.json().catch(() => null) if (!response.ok) { const message = typeof responsePayload?.error === "string" ? responsePayload.error : "Falha ao remover acesso remoto." const detailMessage = typeof responsePayload?.detail === "string" ? responsePayload.detail : null throw new Error(detailMessage ? `${message}. ${detailMessage}` : message) } toast.success("Acesso remoto removido.", { id: "remote-access" }) setRemoteAccessDialog(false) setEditingRemoteAccessClientId(null) } catch (error) { const message = error instanceof Error ? error.message : "Falha ao remover acesso remoto." toast.error(message, { id: "remote-access" }) } finally { setRemoteAccessSaving(false) } }, [machine, canManageRemoteAccess]) const handleToggleActive = async () => { if (!machine) return const nextActive = !isActiveLocal setIsActiveLocal(nextActive) setTogglingActive(true) try { const response = await fetch("/api/admin/machines/toggle-active", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ machineId: machine.id, active: nextActive }), credentials: "include", }) if (!response.ok) { const payload = (await response.json().catch(() => ({}))) as { error?: string } throw new Error(payload?.error ?? "Falha ao atualizar status") } toast.success(nextActive ? "Máquina reativada" : "Máquina desativada") } catch (error) { console.error(error) setIsActiveLocal(!nextActive) toast.error("Não foi possível atualizar o status da máquina.") } finally { setTogglingActive(false) } } const handleCopyRemoteIdentifier = useCallback(async (identifier: string | null | undefined) => { if (!identifier) return try { await navigator.clipboard.writeText(identifier) toast.success("Identificador de acesso remoto copiado.") } catch (error) { console.error(error) toast.error("Não foi possível copiar o identificador.") } }, []) return ( Detalhes Resumo da máquina selecionada {machine ? (
{companyName ? (
{companyName}
) : null} {!isDeactivated ? : null} {!isActiveLocal ? ( Máquina desativada ) : null}
) : null}
{!machine ? (

Selecione uma máquina para visualizar detalhes.

) : (

{machine.hostname}

{machine.authEmail ?? "E-mail não definido"}

{/* ping integrado na badge de status */}
{summaryChips.map((chip) => ( ))}

Tickets abertos por esta máquina

{machineTicketsHref ? ( Ver todos ) : null}
{totalOpenTickets === 0 ? (

Nenhum chamado em aberto registrado diretamente por esta máquina.

) : (
{hasAdditionalOpenTickets ? (

Mostrando últimos {Math.min(displayLimit, totalOpenTickets)} de {totalOpenTickets} chamados em aberto

) : null}
    {displayedMachineTickets.map((ticket) => { const priorityMeta = getTicketPriorityMeta(ticket.priority) return (
  • #{ticket.reference} · {ticket.subject}

    Atualizado {formatRelativeTime(new Date(ticket.updatedAt))}

    {priorityMeta.label}
  • ) })}
)}
{totalOpenTickets}
{machine.authEmail ? ( ) : null} {machine.registeredBy ? ( Registrada via {machine.registeredBy} ) : null}

Acesso remoto

{hasRemoteAccess ? ( {remoteAccessEntries.length === 1 ? remoteAccessEntries[0].provider ?? "Configuração única" : `${remoteAccessEntries.length} acessos`} ) : null}
{canManageRemoteAccess ? ( ) : null}
{hasRemoteAccess ? (
{remoteAccessEntries.map((entry) => { const metadataEntries = extractRemoteAccessMetadataEntries(entry.metadata) const lastVerifiedDate = entry.lastVerifiedAt && Number.isFinite(entry.lastVerifiedAt) ? new Date(entry.lastVerifiedAt) : null return (
{entry.provider ? ( {entry.provider} ) : null} {entry.identifier ? ( {entry.identifier} ) : null} {entry.identifier ? ( ) : null}
{entry.url ? ( Abrir console remoto ) : null} {entry.notes ? (

{entry.notes}

) : null} {lastVerifiedDate ? (

Atualizado {formatRelativeTime(lastVerifiedDate)}{" "} ({formatAbsoluteDateTime(lastVerifiedDate)})

) : null}
{canManageRemoteAccess ? (
) : null}
{metadataEntries.length ? (
Metadados adicionais
{metadataEntries.map(([key, value]) => (
{formatRemoteAccessMetadataKey(key)} {formatRemoteAccessMetadataValue(value)}
))}
) : null}
) })}
) : (
Nenhum identificador de acesso remoto cadastrado. Registre o ID do TeamViewer, AnyDesk ou outra ferramenta para agilizar o suporte.
)}

Usuários vinculados

{primaryLinkedUser?.email ? (
{primaryLinkedUser.name || primaryLinkedUser.email} {primaryLinkedUser.name ? `· ${primaryLinkedUser.email}` : ""} Principal
) : null} {Array.isArray(machine.linkedUsers) && machine.linkedUsers.length > 0 ? (
    {machine.linkedUsers.map((u) => (
  • {u.name || u.email} {u.name ? `· ${u.email}` : ''}
  • ))}
) : null} {!primaryLinkedUser?.email && (!machine.linkedUsers || machine.linkedUsers.length === 0) ? (

Nenhum usuário vinculado.

) : null}
setAccessEmail(e.target.value)} placeholder="e-mail do usuário para vincular" className="max-w-xs" type="email" /> Somente colaboradores/gestores. Gerenciar usuários
{/* Renomear máquina */} Renomear máquina
setNewName(e.target.value)} placeholder="Novo hostname" />
Ajustar acesso da máquina
setAccessEmail(e.target.value)} placeholder="colaborador@empresa.com" />
setAccessName(e.target.value)} placeholder="Nome completo" />
{ setRemoteAccessDialog(open) if (!open) { setRemoteAccessSaving(false) setEditingRemoteAccessClientId(null) } }} > {editingRemoteAccess ? "Editar acesso remoto" : "Adicionar acesso remoto"} Registre os detalhes do acesso remoto utilizado por esta máquina.
{ event.preventDefault() void handleSaveRemoteAccess() }} className="space-y-4" >
{remoteAccessProviderOption === "OTHER" ? (
setRemoteAccessCustomProvider(event.target.value)} placeholder="Ex: Supremo, Zoho Assist..." autoFocus />
) : null}
setRemoteAccessIdentifierInput(event.target.value)} placeholder="Ex: 123 456 789" required />
setRemoteAccessUrlInput(event.target.value)} placeholder="https://" />