diff --git a/convex/machines.ts b/convex/machines.ts index bb22df6..e1957a4 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -850,6 +850,118 @@ export const listByTenant = query({ }, }) +export const getById = query({ + args: { + id: v.id("machines"), + includeMetadata: v.optional(v.boolean()), + }, + handler: async (ctx, args) => { + const includeMetadata = Boolean(args.includeMetadata) + const now = Date.now() + + const machine = await ctx.db.get(args.id) + if (!machine) return null + + const tokens = await ctx.db + .query("machineTokens") + .withIndex("by_machine", (q) => q.eq("machineId", machine._id)) + .collect() + + const activeToken = tokens.find((token) => !token.revoked && token.expiresAt > now) ?? null + + const offlineThresholdMs = getOfflineThresholdMs() + const staleThresholdMs = getStaleThresholdMs(offlineThresholdMs) + const manualStatus = (machine.status ?? "").toLowerCase() + let derivedStatus: string + if (machine.isActive === false) { + derivedStatus = "deactivated" + } else if (["maintenance", "blocked"].includes(manualStatus)) { + derivedStatus = manualStatus + } else if (machine.lastHeartbeatAt) { + const age = now - machine.lastHeartbeatAt + if (age <= offlineThresholdMs) { + derivedStatus = "online" + } else if (age <= staleThresholdMs) { + derivedStatus = "offline" + } else { + derivedStatus = "stale" + } + } else { + derivedStatus = machine.status ?? "unknown" + } + + const meta = includeMetadata ? (machine.metadata ?? null) : null + let metrics: Record | null = null + let inventory: Record | null = null + let postureAlerts: Array> | null = null + let lastPostureAt: number | null = null + if (meta && typeof meta === "object") { + const metaRecord = meta as Record + if (metaRecord.metrics && typeof metaRecord.metrics === "object") { + metrics = metaRecord.metrics as Record + } + if (metaRecord.inventory && typeof metaRecord.inventory === "object") { + inventory = metaRecord.inventory as Record + } + if (Array.isArray(metaRecord.postureAlerts)) { + postureAlerts = metaRecord.postureAlerts as Array> + } + if (typeof metaRecord.lastPostureAt === "number") { + lastPostureAt = metaRecord.lastPostureAt as number + } + } + + const linkedUserIds = machine.linkedUserIds ?? [] + const linkedUsers = await Promise.all( + linkedUserIds.map(async (id) => { + const u = await ctx.db.get(id) + if (!u) return null + return { id: u._id, email: u.email, name: u.name } + }) + ).then((arr) => arr.filter(Boolean) as Array<{ id: string; email: string; name: string }>) + + return { + id: machine._id, + tenantId: machine.tenantId, + hostname: machine.hostname, + companyId: machine.companyId ?? null, + companySlug: machine.companySlug ?? null, + osName: machine.osName, + osVersion: machine.osVersion ?? null, + architecture: machine.architecture ?? null, + macAddresses: machine.macAddresses, + serialNumbers: machine.serialNumbers, + authUserId: machine.authUserId ?? null, + authEmail: machine.authEmail ?? null, + persona: machine.persona ?? null, + assignedUserId: machine.assignedUserId ?? null, + assignedUserEmail: machine.assignedUserEmail ?? null, + assignedUserName: machine.assignedUserName ?? null, + assignedUserRole: machine.assignedUserRole ?? null, + linkedUsers, + status: derivedStatus, + isActive: machine.isActive ?? true, + lastHeartbeatAt: machine.lastHeartbeatAt ?? null, + heartbeatAgeMs: machine.lastHeartbeatAt ? now - machine.lastHeartbeatAt : null, + registeredBy: machine.registeredBy ?? null, + createdAt: machine.createdAt, + updatedAt: machine.updatedAt, + token: activeToken + ? { + expiresAt: activeToken.expiresAt, + lastUsedAt: activeToken.lastUsedAt ?? null, + usageCount: activeToken.usageCount ?? 0, + } + : null, + metrics, + inventory, + postureAlerts, + lastPostureAt, + remoteAccess: machine.remoteAccess ?? null, + } + }, +}) + export const listAlerts = query({ args: { machineId: v.id("machines"), diff --git a/src/components/admin/machines/admin-machine-details.client.tsx b/src/components/admin/machines/admin-machine-details.client.tsx index 919c9e1..de6bb86 100644 --- a/src/components/admin/machines/admin-machine-details.client.tsx +++ b/src/components/admin/machines/admin-machine-details.client.tsx @@ -10,18 +10,18 @@ import { } from "@/components/admin/machines/admin-machines-overview" import { Card, CardContent } from "@/components/ui/card" import { Skeleton } from "@/components/ui/skeleton" +import type { Id } from "@/convex/_generated/dataModel" export function AdminMachineDetailsClient({ tenantId, machineId }: { tenantId: string; machineId: string }) { - const rawResult = useQuery(api.machines.listByTenant, { tenantId, includeMetadata: true }) as Array> | undefined - const machines: MachinesQueryItem[] | undefined = useMemo(() => { - if (!rawResult) return undefined - return rawResult.map((item) => normalizeMachineItem(item)) - }, [rawResult]) - const isLoading = rawResult === undefined - const machine = useMemo(() => { - if (!machines) return null - return machines.find((m) => m.id === machineId) ?? null - }, [machines, machineId]) + const single = useQuery((api as any).machines.getById, { id: machineId as Id<"machines">, includeMetadata: true }) as + | Record + | null + | undefined + const machine: MachinesQueryItem | null = useMemo(() => { + if (single === undefined || single === null) return single as null + return normalizeMachineItem(single) + }, [single]) + const isLoading = single === undefined if (isLoading) { return ( diff --git a/src/components/admin/machines/machine-breadcrumbs.client.tsx b/src/components/admin/machines/machine-breadcrumbs.client.tsx index 990206a..9736d02 100644 --- a/src/components/admin/machines/machine-breadcrumbs.client.tsx +++ b/src/components/admin/machines/machine-breadcrumbs.client.tsx @@ -9,11 +9,11 @@ import { useAuth } from "@/lib/auth-client" export function MachineBreadcrumbs({ tenantId, machineId }: { tenantId: string; machineId: string }) { const { convexUserId } = useAuth() - const list = useQuery( - convexUserId ? api.machines.listByTenant : "skip", - convexUserId ? { tenantId, includeMetadata: false } : ("skip" as const) - ) as Array<{ id: Id<"machines">; hostname: string }> | undefined - const hostname = useMemo(() => list?.find((m) => m.id === (machineId as unknown as Id<"machines">))?.hostname ?? "Detalhe", [list, machineId]) + const item = useQuery((api as any).machines.getById, { id: machineId as Id<"machines">, includeMetadata: false }) as + | { hostname: string } + | null + | undefined + const hostname = useMemo(() => item?.hostname ?? "Detalhe", [item]) return ( ) } -