diff --git a/convex/machines.ts b/convex/machines.ts index 03dc52c..237af94 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -10,6 +10,7 @@ import type { MutationCtx } from "./_generated/server" const DEFAULT_TENANT_ID = "tenant-atlas" const DEFAULT_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30 // 30 dias const ALLOWED_MACHINE_PERSONAS = new Set(["collaborator", "manager"]) +const DEFAULT_OFFLINE_THRESHOLD_MS = 10 * 60 * 1000 type NormalizedIdentifiers = { macs: string[] @@ -48,6 +49,26 @@ function getTokenTtlMs(): number { return parsed } +function getOfflineThresholdMs(): number { + const raw = process.env["MACHINE_OFFLINE_THRESHOLD_MS"] + if (!raw) return DEFAULT_OFFLINE_THRESHOLD_MS + const parsed = Number(raw) + if (!Number.isFinite(parsed) || parsed <= 0) { + return DEFAULT_OFFLINE_THRESHOLD_MS + } + return parsed +} + +function getStaleThresholdMs(offlineMs: number): number { + const raw = process.env["MACHINE_STALE_THRESHOLD_MS"] + if (!raw) return offlineMs * 12 + const parsed = Number(raw) + if (!Number.isFinite(parsed) || parsed <= offlineMs) { + return offlineMs * 12 + } + return parsed +} + function normalizeIdentifiers(macAddresses: string[], serialNumbers: string[]): NormalizedIdentifiers { const normalizeMac = (value: string) => value.replace(/[^a-f0-9]/gi, "").toLowerCase() const normalizeSerial = (value: string) => value.trim().toLowerCase() @@ -681,9 +702,24 @@ export const listByTenant = query({ .collect() const activeToken = tokens.find((token) => !token.revoked && token.expiresAt > now) ?? null - const derivedStatus = - machine.status ?? - (machine.lastHeartbeatAt && now - machine.lastHeartbeatAt <= 5 * 60 * 1000 ? "online" : machine.lastHeartbeatAt ? "offline" : "unknown") + const offlineThresholdMs = getOfflineThresholdMs() + const staleThresholdMs = getStaleThresholdMs(offlineThresholdMs) + const manualStatus = (machine.status ?? "").toLowerCase() + let derivedStatus: string + 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 metadata = includeMetadata ? (machine.metadata ?? null) : null diff --git a/src/components/admin/machines/admin-machines-overview.tsx b/src/components/admin/machines/admin-machines-overview.tsx index c0859d2..c1e9442 100644 --- a/src/components/admin/machines/admin-machines-overview.tsx +++ b/src/components/admin/machines/admin-machines-overview.tsx @@ -441,9 +441,23 @@ function useMachinesQuery(tenantId: string): MachinesQueryItem[] { ) } +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", unknown: "Desconhecida", @@ -452,6 +466,7 @@ const statusLabels: Record = { const statusClasses: Record = { online: "border-emerald-500/20 bg-emerald-500/15 text-emerald-600", offline: "border-rose-500/20 bg-rose-500/15 text-rose-600", + stale: "border-slate-400/30 bg-slate-200 text-slate-700", maintenance: "border-amber-500/20 bg-amber-500/15 text-amber-600", blocked: "border-orange-500/20 bg-orange-500/15 text-orange-600", unknown: "border-slate-300 bg-slate-200 text-slate-700", @@ -512,6 +527,21 @@ function getStatusVariant(status?: string | null) { } } +function resolveMachineStatus(machine: { status?: string | null; lastHeartbeatAt?: number | null }): string { + 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 @@ -546,12 +576,12 @@ export function AdminMachinesOverview({ tenantId }: { tenantId: string }) { const companyNameOptions = useMemo(() => (companies ?? []).map((c) => c.name).sort((a,b)=>a.localeCompare(b,"pt-BR")), [companies]) - const filteredMachines = useMemo(() => { +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 = (m.status ?? "unknown").toLowerCase() + const s = resolveMachineStatus(m).toLowerCase() if (s !== statusFilter) return false } if (osFilter !== "all" && (m.osName ?? "").toLowerCase() !== osFilter.toLowerCase()) return false @@ -592,6 +622,7 @@ export function AdminMachinesOverview({ tenantId }: { tenantId: string }) { Todos status Online Offline + Sem sinal Desconhecido @@ -707,6 +738,7 @@ type MachineDetailsProps = { export function MachineDetails({ machine }: MachineDetailsProps) { const { convexUserId } = useAuth() const router = useRouter() + const effectiveStatus = machine ? resolveMachineStatus(machine) : "unknown" // Company name lookup (by slug) const companyQueryArgs = convexUserId && machine ? { tenantId: machine.tenantId, viewerId: convexUserId as Id<"users"> } : undefined const companies = useQuery( @@ -972,7 +1004,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {

) : null} - + {/* ping integrado na badge de status */}
@@ -1908,7 +1940,8 @@ function MachinesGrid({ machines, companyNameBySlug }: { machines: MachinesQuery } function MachineCard({ machine, companyName }: { machine: MachinesQueryItem; companyName?: string | null }) { - const { className } = getStatusVariant(machine.status) + const effectiveStatus = resolveMachineStatus(machine) + const { className } = getStatusVariant(effectiveStatus) const lastHeartbeat = machine.lastHeartbeatAt ? new Date(machine.lastHeartbeatAt) : null type AgentMetrics = { memoryUsedBytes?: number @@ -1962,7 +1995,7 @@ function MachineCard({ machine, companyName }: { machine: MachinesQueryItem; com : "bg-slate-400" )} /> - {String(machine.status ?? "").toLowerCase() === "online" ? ( + {effectiveStatus === "online" ? ( ) : null}