Derive machine online status from heartbeat

This commit is contained in:
Esdras Renan 2025-10-13 16:05:18 -03:00
parent 4d8b9a0e39
commit 388ab5feb4
2 changed files with 77 additions and 8 deletions

View file

@ -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

View file

@ -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<string, string> = {
online: "Online",
offline: "Offline",
stale: "Sem sinal",
maintenance: "Manutenção",
blocked: "Bloqueada",
unknown: "Desconhecida",
@ -452,6 +466,7 @@ const statusLabels: Record<string, string> = {
const statusClasses: Record<string, string> = {
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 <Apple className="size-4 text-black" />
@ -551,7 +581,7 @@ export function AdminMachinesOverview({ tenantId }: { tenantId: string }) {
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 }) {
<SelectItem value="all">Todos status</SelectItem>
<SelectItem value="online">Online</SelectItem>
<SelectItem value="offline">Offline</SelectItem>
<SelectItem value="stale">Sem sinal</SelectItem>
<SelectItem value="unknown">Desconhecido</SelectItem>
</SelectContent>
</Select>
@ -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) {
</p>
) : null}
</div>
<MachineStatusBadge status={machine.status} />
<MachineStatusBadge status={effectiveStatus} />
</div>
{/* ping integrado na badge de status */}
<div className="flex flex-wrap items-center gap-2">
@ -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" ? (
<span className="absolute left-1/2 top-1/2 -z-10 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full bg-emerald-400/30 animate-ping" />
) : null}
</div>