From 388ab5feb43d228a8e9eb95c2c47db46c870f3fb Mon Sep 17 00:00:00 2001
From: Esdras Renan
Date: Mon, 13 Oct 2025 16:05:18 -0300
Subject: [PATCH] Derive machine online status from heartbeat
---
convex/machines.ts | 42 ++++++++++++++++--
.../machines/admin-machines-overview.tsx | 43 ++++++++++++++++---
2 files changed, 77 insertions(+), 8 deletions(-)
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}