Derive machine online status from heartbeat
This commit is contained in:
parent
4d8b9a0e39
commit
388ab5feb4
2 changed files with 77 additions and 8 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
@ -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 }) {
|
|||
<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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue