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_TENANT_ID = "tenant-atlas"
|
||||||
const DEFAULT_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30 // 30 dias
|
const DEFAULT_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30 // 30 dias
|
||||||
const ALLOWED_MACHINE_PERSONAS = new Set(["collaborator", "manager"])
|
const ALLOWED_MACHINE_PERSONAS = new Set(["collaborator", "manager"])
|
||||||
|
const DEFAULT_OFFLINE_THRESHOLD_MS = 10 * 60 * 1000
|
||||||
|
|
||||||
type NormalizedIdentifiers = {
|
type NormalizedIdentifiers = {
|
||||||
macs: string[]
|
macs: string[]
|
||||||
|
|
@ -48,6 +49,26 @@ function getTokenTtlMs(): number {
|
||||||
return parsed
|
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 {
|
function normalizeIdentifiers(macAddresses: string[], serialNumbers: string[]): NormalizedIdentifiers {
|
||||||
const normalizeMac = (value: string) => value.replace(/[^a-f0-9]/gi, "").toLowerCase()
|
const normalizeMac = (value: string) => value.replace(/[^a-f0-9]/gi, "").toLowerCase()
|
||||||
const normalizeSerial = (value: string) => value.trim().toLowerCase()
|
const normalizeSerial = (value: string) => value.trim().toLowerCase()
|
||||||
|
|
@ -681,9 +702,24 @@ export const listByTenant = query({
|
||||||
.collect()
|
.collect()
|
||||||
|
|
||||||
const activeToken = tokens.find((token) => !token.revoked && token.expiresAt > now) ?? null
|
const activeToken = tokens.find((token) => !token.revoked && token.expiresAt > now) ?? null
|
||||||
const derivedStatus =
|
const offlineThresholdMs = getOfflineThresholdMs()
|
||||||
machine.status ??
|
const staleThresholdMs = getStaleThresholdMs(offlineThresholdMs)
|
||||||
(machine.lastHeartbeatAt && now - machine.lastHeartbeatAt <= 5 * 60 * 1000 ? "online" : machine.lastHeartbeatAt ? "offline" : "unknown")
|
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
|
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> = {
|
const statusLabels: Record<string, string> = {
|
||||||
online: "Online",
|
online: "Online",
|
||||||
offline: "Offline",
|
offline: "Offline",
|
||||||
|
stale: "Sem sinal",
|
||||||
maintenance: "Manutenção",
|
maintenance: "Manutenção",
|
||||||
blocked: "Bloqueada",
|
blocked: "Bloqueada",
|
||||||
unknown: "Desconhecida",
|
unknown: "Desconhecida",
|
||||||
|
|
@ -452,6 +466,7 @@ const statusLabels: Record<string, string> = {
|
||||||
const statusClasses: Record<string, string> = {
|
const statusClasses: Record<string, string> = {
|
||||||
online: "border-emerald-500/20 bg-emerald-500/15 text-emerald-600",
|
online: "border-emerald-500/20 bg-emerald-500/15 text-emerald-600",
|
||||||
offline: "border-rose-500/20 bg-rose-500/15 text-rose-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",
|
maintenance: "border-amber-500/20 bg-amber-500/15 text-amber-600",
|
||||||
blocked: "border-orange-500/20 bg-orange-500/15 text-orange-600",
|
blocked: "border-orange-500/20 bg-orange-500/15 text-orange-600",
|
||||||
unknown: "border-slate-300 bg-slate-200 text-slate-700",
|
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 }) {
|
function OsIcon({ osName }: { osName?: string | null }) {
|
||||||
const name = (osName ?? "").toLowerCase()
|
const name = (osName ?? "").toLowerCase()
|
||||||
if (name.includes("mac") || name.includes("darwin") || name.includes("macos")) return <Apple className="size-4 text-black" />
|
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) => {
|
return machines.filter((m) => {
|
||||||
if (onlyAlerts && !(Array.isArray(m.postureAlerts) && m.postureAlerts.length > 0)) return false
|
if (onlyAlerts && !(Array.isArray(m.postureAlerts) && m.postureAlerts.length > 0)) return false
|
||||||
if (statusFilter !== "all") {
|
if (statusFilter !== "all") {
|
||||||
const s = (m.status ?? "unknown").toLowerCase()
|
const s = resolveMachineStatus(m).toLowerCase()
|
||||||
if (s !== statusFilter) return false
|
if (s !== statusFilter) return false
|
||||||
}
|
}
|
||||||
if (osFilter !== "all" && (m.osName ?? "").toLowerCase() !== osFilter.toLowerCase()) 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="all">Todos status</SelectItem>
|
||||||
<SelectItem value="online">Online</SelectItem>
|
<SelectItem value="online">Online</SelectItem>
|
||||||
<SelectItem value="offline">Offline</SelectItem>
|
<SelectItem value="offline">Offline</SelectItem>
|
||||||
|
<SelectItem value="stale">Sem sinal</SelectItem>
|
||||||
<SelectItem value="unknown">Desconhecido</SelectItem>
|
<SelectItem value="unknown">Desconhecido</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
@ -707,6 +738,7 @@ type MachineDetailsProps = {
|
||||||
export function MachineDetails({ machine }: MachineDetailsProps) {
|
export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
const { convexUserId } = useAuth()
|
const { convexUserId } = useAuth()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const effectiveStatus = machine ? resolveMachineStatus(machine) : "unknown"
|
||||||
// Company name lookup (by slug)
|
// Company name lookup (by slug)
|
||||||
const companyQueryArgs = convexUserId && machine ? { tenantId: machine.tenantId, viewerId: convexUserId as Id<"users"> } : undefined
|
const companyQueryArgs = convexUserId && machine ? { tenantId: machine.tenantId, viewerId: convexUserId as Id<"users"> } : undefined
|
||||||
const companies = useQuery(
|
const companies = useQuery(
|
||||||
|
|
@ -972,7 +1004,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<MachineStatusBadge status={machine.status} />
|
<MachineStatusBadge status={effectiveStatus} />
|
||||||
</div>
|
</div>
|
||||||
{/* ping integrado na badge de status */}
|
{/* ping integrado na badge de status */}
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<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 }) {
|
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
|
const lastHeartbeat = machine.lastHeartbeatAt ? new Date(machine.lastHeartbeatAt) : null
|
||||||
type AgentMetrics = {
|
type AgentMetrics = {
|
||||||
memoryUsedBytes?: number
|
memoryUsedBytes?: number
|
||||||
|
|
@ -1962,7 +1995,7 @@ function MachineCard({ machine, companyName }: { machine: MachinesQueryItem; com
|
||||||
: "bg-slate-400"
|
: "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" />
|
<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}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue