From 2fb587b01dc0c0f6409ee9f5dc144601c9745e14 Mon Sep 17 00:00:00 2001 From: codex-bot Date: Fri, 31 Oct 2025 16:09:05 -0300 Subject: [PATCH] Expand machine inventory export with detailed sheets --- src/server/machines/inventory-export.ts | 1448 ++++++++++++++++++++--- 1 file changed, 1313 insertions(+), 135 deletions(-) diff --git a/src/server/machines/inventory-export.ts b/src/server/machines/inventory-export.ts index ec29331..a181db7 100644 --- a/src/server/machines/inventory-export.ts +++ b/src/server/machines/inventory-export.ts @@ -31,6 +31,10 @@ export type MachineInventoryRecord = { updatedAt: number token: { expiresAt: number; usageCount: number; lastUsedAt: number | null } | null inventory: Record | null + metrics?: Record | null + postureAlerts?: Array> | null + lastPostureAt?: number | null + remoteAccess?: unknown linkedUsers?: LinkedUser[] } @@ -50,18 +54,6 @@ type SoftwareEntry = { installedOn: string | null } -type DiskEntry = { - hostname: string - type: string | null - model: string | null - name: string | null - mountPoint: string | null - size: string | null - free: string | null - serial: string | null - smartStatus: string | null -} - const INVENTORY_HEADERS = [ "Hostname", "Empresa", @@ -96,17 +88,77 @@ const INVENTORY_HEADERS = [ "Criada em", "Atualizada em", "Softwares instalados", + "Build SO", + "Licença ativada", + "Experiência SO", + "Domínio", + "Grupo de trabalho", + "Nome do dispositivo", + "Serial placa-mãe", + "Colaborador (nome)", + "Colaborador (e-mail)", + "Acessos remotos", + "Fleet ID", + "Equipe Fleet", + "Fleet atualizado em", ] as const const INVENTORY_COLUMN_WIDTHS = [ 22, 26, 16, 14, 10, 20, 22, 24, 28, 24, 20, 18, 14, 18, 22, 22, 24, 12, 12, 14, 26, 20, 24, 24, 18, 18, 18, 20, 20, 14, 20, 20, 18, + 18, 18, 20, 20, 20, 26, 24, 24, 26, 16, 18, 18, 20, ] as const const SOFTWARE_HEADERS = ["Hostname", "Aplicativo", "Versão", "Origem", "Publicador", "Instalado em"] as const const SOFTWARE_COLUMN_WIDTHS = [22, 36, 18, 18, 22, 20] as const -const DISK_HEADERS = ["Hostname", "Tipo", "Modelo", "Nome", "Montagem", "Capacidade", "Livre", "Serial", "Status SMART"] as const -const DISK_COLUMN_WIDTHS = [22, 14, 24, 18, 18, 16, 16, 22, 18] as const +const PARTITION_HEADERS = [ + "Hostname", + "Nome", + "Montagem", + "FS", + "Capacidade", + "Livre", + "Utilizado", + "Interface", + "Serial", + "Origem", +] as const +const PARTITION_COLUMN_WIDTHS = [22, 24, 18, 12, 16, 16, 16, 16, 20, 18] as const + +const PHYSICAL_DISK_HEADERS = ["Hostname", "Modelo", "Tamanho", "Interface", "Tipo", "Serial", "Origem"] as const +const PHYSICAL_DISK_COLUMN_WIDTHS = [22, 32, 18, 16, 16, 22, 16] as const + +const NETWORK_HEADERS = ["Hostname", "Interface", "MAC", "IP", "Origem"] as const +const NETWORK_COLUMN_WIDTHS = [22, 28, 22, 24, 18] as const + +const REMOTE_ACCESS_HEADERS = ["Hostname", "Provedor", "Identificador", "URL", "Notas", "Última verificação", "Origem", "Metadados"] as const +const REMOTE_ACCESS_COLUMN_WIDTHS = [22, 22, 24, 36, 28, 22, 16, 40] as const + +const SERVICE_HEADERS = ["Hostname", "Nome", "Exibição", "Status", "Origem"] as const +const SERVICE_COLUMN_WIDTHS = [22, 28, 36, 18, 18] as const + +const ALERT_HEADERS = ["Hostname", "Tipo", "Mensagem", "Severidade", "Criado em"] as const +const ALERT_COLUMN_WIDTHS = [22, 18, 44, 14, 22] as const + +const METRICS_HEADERS = [ + "Hostname", + "Capturado em", + "CPU (%)", + "Memória usada", + "Memória total", + "Memória (%)", + "Disco usado", + "Disco total", + "Disco (%)", + "GPU (%)", +] as const +const METRICS_COLUMN_WIDTHS = [22, 24, 14, 20, 20, 14, 20, 20, 14, 14] as const + +const LABEL_HEADERS = ["Hostname", "Label"] as const +const LABEL_COLUMN_WIDTHS = [22, 30] as const + +const SYSTEM_HEADERS = ["Hostname", "Categoria", "Campo", "Valor"] as const +const SYSTEM_COLUMN_WIDTHS = [22, 24, 32, 44] as const const STATUS_LABELS: Record = { online: "Online", @@ -137,48 +189,150 @@ export function buildMachinesInventoryWorkbook( const inventoryRows = machines.map((machine) => flattenMachine(machine)) const linksRows = buildLinkedUsersRows(machines) const softwareRows = buildSoftwareRows(machines) - const diskRows = buildDiskRows(machines) + const partitionRows = buildPartitionRows(machines) + const physicalDiskRows = buildPhysicalDiskRows(machines) + const networkRows = buildNetworkRows(machines) + const remoteAccessRows = buildRemoteAccessRows(machines) + const serviceRows = buildServiceRows(machines) + const alertRows = buildAlertRows(machines) + const metricsRows = buildMetricsRows(machines) + const labelRows = buildLabelRows(machines) + const systemRows = buildSystemRows(machines) - const sheets: WorksheetConfig[] = [ - { - name: "Resumo", - headers: ["Item", "Valor"], - rows: summaryRows, - columnWidths: [28, 48], - }, - { - name: "Inventário", - headers: [...INVENTORY_HEADERS], - rows: inventoryRows, - columnWidths: [...INVENTORY_COLUMN_WIDTHS], + const sheets: WorksheetConfig[] = [] + + sheets.push({ + name: "Resumo", + headers: ["Item", "Valor"], + rows: summaryRows, + columnWidths: [28, 48], + }) + + sheets.push({ + name: "Inventário", + headers: [...INVENTORY_HEADERS], + rows: inventoryRows, + columnWidths: [...INVENTORY_COLUMN_WIDTHS], + freezePane: { rowSplit: 1 }, + autoFilter: true, + }) + + sheets.push({ + name: "Vínculos", + headers: ["Hostname", "Empresa", "Usuário", "E-mail"], + rows: linksRows.length > 0 ? linksRows : [["—", "—", "—", "—"]], + columnWidths: [22, 26, 26, 28], + freezePane: { rowSplit: 1 }, + autoFilter: true, + }) + + sheets.push({ + name: "Softwares", + headers: [...SOFTWARE_HEADERS], + rows: softwareRows.length > 0 ? softwareRows : [["—", "—", "—", "—", "—", "—"]], + columnWidths: [...SOFTWARE_COLUMN_WIDTHS], + freezePane: { rowSplit: 1 }, + autoFilter: softwareRows.length > 0, + }) + + if (partitionRows.length > 0) { + sheets.push({ + name: "Partições", + headers: [...PARTITION_HEADERS], + rows: partitionRows, + columnWidths: [...PARTITION_COLUMN_WIDTHS], freezePane: { rowSplit: 1 }, autoFilter: true, - }, - { - name: "Vínculos", - headers: ["Hostname", "Empresa", "Usuário", "E-mail"], - rows: linksRows.length > 0 ? linksRows : [["—", "—", "—", "—"]], - columnWidths: [22, 26, 26, 28], + }) + } + + if (physicalDiskRows.length > 0) { + sheets.push({ + name: "Discos físicos", + headers: [...PHYSICAL_DISK_HEADERS], + rows: physicalDiskRows, + columnWidths: [...PHYSICAL_DISK_COLUMN_WIDTHS], freezePane: { rowSplit: 1 }, autoFilter: true, - }, - { - name: "Softwares", - headers: [...SOFTWARE_HEADERS], - rows: softwareRows.length > 0 ? softwareRows : [["—", "—", "—", "—", "—", "—"]], - columnWidths: [...SOFTWARE_COLUMN_WIDTHS], + }) + } + + if (networkRows.length > 0) { + sheets.push({ + name: "Rede", + headers: [...NETWORK_HEADERS], + rows: networkRows, + columnWidths: [...NETWORK_COLUMN_WIDTHS], freezePane: { rowSplit: 1 }, - autoFilter: softwareRows.length > 0, - }, - { - name: "Discos", - headers: [...DISK_HEADERS], - rows: diskRows.length > 0 ? diskRows : [Array(DISK_HEADERS.length).fill("—")], - columnWidths: [...DISK_COLUMN_WIDTHS], + autoFilter: true, + }) + } + + if (remoteAccessRows.length > 0) { + sheets.push({ + name: "Acessos remotos", + headers: [...REMOTE_ACCESS_HEADERS], + rows: remoteAccessRows, + columnWidths: [...REMOTE_ACCESS_COLUMN_WIDTHS], freezePane: { rowSplit: 1 }, - autoFilter: diskRows.length > 0, - }, - ] + autoFilter: true, + }) + } + + if (serviceRows.length > 0) { + sheets.push({ + name: "Serviços", + headers: [...SERVICE_HEADERS], + rows: serviceRows, + columnWidths: [...SERVICE_COLUMN_WIDTHS], + freezePane: { rowSplit: 1 }, + autoFilter: true, + }) + } + + if (alertRows.length > 0) { + sheets.push({ + name: "Alertas", + headers: [...ALERT_HEADERS], + rows: alertRows, + columnWidths: [...ALERT_COLUMN_WIDTHS], + freezePane: { rowSplit: 1 }, + autoFilter: true, + }) + } + + if (metricsRows.length > 0) { + sheets.push({ + name: "Métricas", + headers: [...METRICS_HEADERS], + rows: metricsRows, + columnWidths: [...METRICS_COLUMN_WIDTHS], + freezePane: { rowSplit: 1 }, + autoFilter: true, + }) + } + + if (labelRows.length > 0) { + sheets.push({ + name: "Labels", + headers: [...LABEL_HEADERS], + rows: labelRows, + columnWidths: [...LABEL_COLUMN_WIDTHS], + freezePane: { rowSplit: 1 }, + autoFilter: true, + }) + } + + if (systemRows.length > 0) { + sheets.push({ + name: "Sistema", + headers: [...SYSTEM_HEADERS], + rows: systemRows, + columnWidths: [...SYSTEM_COLUMN_WIDTHS], + freezePane: { rowSplit: 1 }, + autoFilter: true, + }) + } return buildXlsxWorkbook(sheets) } @@ -229,6 +383,16 @@ function buildSummaryRows( rows.push(["Empresas no resultado", Array.from(uniqueCompanies).sort((a, b) => a.localeCompare(b, "pt-BR")).join(", ")]) } + const totalRemoteAccess = machines.reduce((acc, machine) => acc + collectRemoteAccessEntries(machine).length, 0) + if (totalRemoteAccess > 0) { + rows.push(["Acessos remotos configurados", totalRemoteAccess]) + } + + const totalAlerts = machines.reduce((acc, machine) => acc + (machine.postureAlerts?.length ?? 0), 0) + if (totalAlerts > 0) { + rows.push(["Alertas de postura ativos", totalAlerts]) + } + return rows } @@ -239,8 +403,12 @@ function flattenMachine(machine: MachineInventoryRecord): WorksheetRow { const labels = extractLabels(inventory) const primaryIp = extractPrimaryIp(inventory) const publicIp = extractPublicIp(inventory) - const softwareCount = countSoftwareEntries(inventory) + const softwareEntries = extractSoftwareEntries(machine.hostname, inventory) const linkedUsers = summarizeLinkedUsers(machine.linkedUsers) + const systemInfo = extractSystemInfo(inventory) + const collaborator = extractCollaborator(machine, inventory) + const remoteAccessCount = collectRemoteAccessEntries(machine).length + const fleetInfo = extractFleetInfo(inventory) return [ machine.hostname, @@ -256,9 +424,9 @@ function flattenMachine(machine: MachineInventoryRecord): WorksheetRow { machine.osName, machine.osVersion ?? "—", machine.architecture ?? "—", - hardware.vendor ?? "—", - hardware.model ?? "—", - hardware.serial ?? "—", + hardware.vendor ?? systemInfo.systemManufacturer ?? "—", + hardware.model ?? systemInfo.systemModel ?? "—", + hardware.serial ?? systemInfo.boardSerial ?? "—", hardware.cpuType ?? "—", hardware.physicalCores ?? "—", hardware.logicalCores ?? "—", @@ -275,7 +443,20 @@ function flattenMachine(machine: MachineInventoryRecord): WorksheetRow { machine.token?.usageCount ?? 0, formatDateTime(machine.createdAt) ?? "—", formatDateTime(machine.updatedAt) ?? "—", - softwareCount ?? 0, + softwareEntries.length, + systemInfo.osBuild ?? "—", + systemInfo.license ?? "—", + systemInfo.experience ?? "—", + systemInfo.domain ?? "—", + systemInfo.workgroup ?? "—", + systemInfo.deviceName ?? "—", + systemInfo.boardSerial ?? hardware.serial ?? "—", + collaborator?.name ?? "—", + collaborator?.email ?? "—", + remoteAccessCount, + fleetInfo?.id ?? "—", + fleetInfo?.team ?? "—", + fleetInfo?.updatedAt ? formatDateTime(fleetInfo.updatedAt) ?? "—" : "—", ] } @@ -290,46 +471,248 @@ function buildLinkedUsersRows(machines: MachineInventoryRecord[]): Array<[string } function buildSoftwareRows(machines: MachineInventoryRecord[]): WorksheetRow[] { - const rows: WorksheetRow[] = [] + const rows = new Map() machines.forEach((machine) => { const inventory = toRecord(machine.inventory) const entries = extractSoftwareEntries(machine.hostname, inventory) entries.forEach((entry) => { + const key = [ + entry.hostname.toLowerCase(), + (entry.name ?? "").toLowerCase(), + (entry.version ?? "").toLowerCase(), + (entry.source ?? "").toLowerCase(), + (entry.publisher ?? "").toLowerCase(), + (entry.installedOn ?? "").toLowerCase(), + ].join("|") + if (!rows.has(key)) { + rows.set(key, [ + entry.hostname, + entry.name, + entry.version ?? "—", + entry.source ?? "—", + entry.publisher ?? "—", + entry.installedOn ?? "—", + ]) + } + }) + }) + return Array.from(rows.values()).sort((a, b) => { + const hostCompare = String(a[0]).localeCompare(String(b[0]), "pt-BR") + if (hostCompare !== 0) return hostCompare + return String(a[1]).localeCompare(String(b[1]), "pt-BR") + }) +} + +function buildPartitionRows(machines: MachineInventoryRecord[]): WorksheetRow[] { + const rows: WorksheetRow[] = [] + machines.forEach((machine) => { + const inventory = toRecord(machine.inventory) + const partitions = extractPartitionEntries(machine.hostname, inventory) + partitions.forEach((partition) => { rows.push([ - entry.hostname, - entry.name, - entry.version ?? "—", - entry.source ?? "—", - entry.publisher ?? "—", - entry.installedOn ?? "—", + partition.hostname, + partition.name ?? "—", + partition.mount ?? "—", + partition.fs ?? "—", + formatBytesValue(partition.capacityBytes), + formatBytesValue(partition.freeBytes), + partition.capacityBytes !== null && partition.freeBytes !== null + ? formatBytesValue(Math.max(0, partition.capacityBytes - partition.freeBytes)) + : "—", + partition.interface ?? "—", + partition.serial ?? "—", + partition.origin, ]) }) }) return rows } -function buildDiskRows(machines: MachineInventoryRecord[]): WorksheetRow[] { +function buildPhysicalDiskRows(machines: MachineInventoryRecord[]): WorksheetRow[] { const rows: WorksheetRow[] = [] machines.forEach((machine) => { const inventory = toRecord(machine.inventory) - const entries = extractDiskEntries(machine.hostname, inventory) - entries.forEach((disk) => { + const disks = extractPhysicalDiskEntries(machine.hostname, inventory) + disks.forEach((disk) => { rows.push([ disk.hostname, - disk.type ?? "—", - disk.model ?? "—", - disk.name ?? "—", - disk.mountPoint ?? "—", - disk.size ?? "—", - disk.free ?? "—", + disk.model ?? disk.serial ?? "—", + formatBytesValue(disk.sizeBytes), + disk.interface ?? "—", + disk.mediaType ?? "—", disk.serial ?? "—", - disk.smartStatus ?? "—", + disk.origin, ]) }) }) return rows } +function buildNetworkRows(machines: MachineInventoryRecord[]): WorksheetRow[] { + const rows: WorksheetRow[] = [] + machines.forEach((machine) => { + const inventory = toRecord(machine.inventory) + const interfaces = extractNetworkEntries(machine.hostname, inventory) + interfaces.forEach((iface) => { + rows.push([iface.hostname, iface.name ?? "—", iface.mac ?? "—", iface.address ?? "—", iface.origin]) + }) + }) + return rows +} + +function buildRemoteAccessRows(machines: MachineInventoryRecord[]): WorksheetRow[] { + const rows: WorksheetRow[] = [] + machines.forEach((machine) => { + collectRemoteAccessEntries(machine).forEach((entry) => { + rows.push([ + machine.hostname, + entry.provider ?? "—", + entry.identifier ?? "—", + entry.url ?? "—", + entry.notes ?? "—", + entry.lastVerifiedAt ? formatDateTime(entry.lastVerifiedAt) ?? "—" : "—", + entry.origin, + stringifyMetadata(entry.metadata), + ]) + }) + }) + return rows +} + +function buildServiceRows(machines: MachineInventoryRecord[]): WorksheetRow[] { + const rows: WorksheetRow[] = [] + machines.forEach((machine) => { + const inventory = toRecord(machine.inventory) + const services = extractServiceEntries(machine.hostname, inventory) + services.forEach((service) => { + rows.push([ + service.hostname, + service.name ?? "—", + service.displayName ?? "—", + service.status ?? "—", + service.origin, + ]) + }) + }) + return rows +} + +function buildAlertRows(machines: MachineInventoryRecord[]): WorksheetRow[] { + const rows: WorksheetRow[] = [] + machines.forEach((machine) => { + const alerts = machine.postureAlerts ?? [] + alerts.forEach((alert) => { + const record = toRecord(alert) + if (!record) return + rows.push([ + machine.hostname, + pickString(record, ["kind", "Kind", "type"]) ?? "—", + pickString(record, ["message", "Message", "description"]) ?? "—", + pickString(record, ["severity", "Severity"]) ?? "—", + formatDateTime( + parseDateish(record["createdAt"]) ?? + parseDateish(record["timestamp"]) ?? + parseDateish(record["created"]) ?? + null, + ) ?? "—", + ]) + }) + }) + return rows +} + +function buildMetricsRows(machines: MachineInventoryRecord[]): WorksheetRow[] { + const rows: WorksheetRow[] = [] + machines.forEach((machine) => { + const inventory = toRecord(machine.inventory) + const metricsRow = deriveMachineMetrics(machine, inventory) + if (metricsRow) { + rows.push(metricsRow) + } + }) + return rows +} + +function buildLabelRows(machines: MachineInventoryRecord[]): WorksheetRow[] { + const rows: WorksheetRow[] = [] + machines.forEach((machine) => { + const inventory = toRecord(machine.inventory) + const labels = extractLabels(inventory) + labels.forEach((label) => rows.push([machine.hostname, label])) + }) + return rows +} + +function buildSystemRows(machines: MachineInventoryRecord[]): WorksheetRow[] { + const rows: WorksheetRow[] = [] + machines.forEach((machine) => { + const inventory = toRecord(machine.inventory) + const hardware = extractHardware(inventory) + const systemInfo = extractSystemInfo(inventory) + const collaborator = extractCollaborator(machine, inventory) + const fleetInfo = extractFleetInfo(inventory) + const push = (category: string, field: string, value: unknown) => { + const display = value === null || value === undefined || value === "" ? "—" : value + rows.push([machine.hostname, category, field, display]) + } + + push("Sistema", "Sistema operacional", machine.osName ?? "—") + push("Sistema", "Versão", machine.osVersion ?? "—") + if (systemInfo.windowsEdition) push("Sistema", "Edição", systemInfo.windowsEdition) + if (systemInfo.osBuild) push("Sistema", "Build", systemInfo.osBuild) + if (systemInfo.experience) push("Sistema", "Experiência", systemInfo.experience) + push("Sistema", "Licença", systemInfo.license ?? "—") + if (systemInfo.installDate) { + push("Sistema", "Instalação", formatDateTime(systemInfo.installDate) ?? "—") + } + + push("Dispositivo", "Nome do dispositivo", systemInfo.deviceName ?? machine.hostname ?? "—") + push("Dispositivo", "Domínio", systemInfo.domain ?? "—") + push("Dispositivo", "Grupo de trabalho", systemInfo.workgroup ?? "—") + push("Dispositivo", "Fabricante", hardware.vendor ?? systemInfo.systemManufacturer ?? "—") + push("Dispositivo", "Modelo", hardware.model ?? systemInfo.systemModel ?? "—") + push("Dispositivo", "Serial hardware", hardware.serial ?? systemInfo.boardSerial ?? "—") + push("Dispositivo", "MACs", machine.macAddresses.join(", ") || "—") + push("Dispositivo", "Seriais", machine.serialNumbers.join(", ") || "—") + + push("Hardware", "Processador", hardware.cpuType ?? "—") + push("Hardware", "Cores físicas", hardware.physicalCores ?? "—") + push("Hardware", "Cores lógicas", hardware.logicalCores ?? "—") + push("Hardware", "Memória", hardware.memoryGiB ?? "—") + push("Hardware", "GPUs", extractGpuNames(inventory).join(", ") || "—") + + push("Acesso", "Persona", describePersona(machine.persona)) + push("Acesso", "Responsável", machine.assignedUserName ?? "—") + push("Acesso", "Responsável (e-mail)", machine.assignedUserEmail ?? "—") + if (collaborator) { + push("Acesso", "Colaborador (nome)", collaborator.name ?? "—") + push("Acesso", "Colaborador (e-mail)", collaborator.email ?? "—") + } + push("Acesso", "Autenticação", machine.authEmail ?? "—") + push("Acesso", "Registrada via", machine.registeredBy ?? "—") + + push("Token", "Expira em", machine.token?.expiresAt ? formatDateTime(machine.token.expiresAt) ?? "—" : "—") + push("Token", "Último uso", machine.token?.lastUsedAt ? formatDateTime(machine.token.lastUsedAt) ?? "—" : "—") + push("Token", "Uso total", machine.token?.usageCount ?? 0) + + push("Status", "Ativa", yesNo(machine.isActive)) + push("Status", "Status atual", describeStatus(machine.status)) + push("Status", "Último heartbeat", formatDateTime(machine.lastHeartbeatAt) ?? "—") + push("Status", "Criada em", formatDateTime(machine.createdAt) ?? "—") + push("Status", "Atualizada em", formatDateTime(machine.updatedAt) ?? "—") + + const remoteCount = collectRemoteAccessEntries(machine).length + push("Acessos remotos", "Total", remoteCount) + + if (fleetInfo) { + push("Fleet", "ID", fleetInfo.id ?? "—") + push("Fleet", "Equipe", fleetInfo.team ?? "—") + push("Fleet", "Atualizado em", fleetInfo.updatedAt ? formatDateTime(fleetInfo.updatedAt) ?? "—" : "—") + } + }) + return rows +} + function toRecord(value: unknown): Record | null { if (value && typeof value === "object" && !Array.isArray(value)) { return value as Record @@ -431,12 +814,6 @@ function formatDateTime(value: number | null | undefined): string | null { return `${yyyy}-${mm}-${dd} ${hh}:${min} UTC` } -function formatBytes(value: number | null | undefined): string | null { - if (typeof value !== "number" || Number.isNaN(value) || value <= 0) return null - const gib = value / 1024 ** 3 - return `${gib.toFixed(2)} GiB` -} - function extractHardware(inventory: Record | null) { if (!inventory) { return { @@ -447,16 +824,24 @@ function extractHardware(inventory: Record | null) { physicalCores: null, logicalCores: null, memoryGiB: null, + memoryBytes: null, } } const hardware = pickRecord(inventory, ["hardware", "Hardware"]) - const vendor = pickString(hardware, ["vendor", "Vendor"]) - const model = pickString(hardware, ["model", "Model"]) + const vendor = + pickString(hardware, ["vendor", "Vendor", "manufacturer", "Manufacturer", "systemManufacturer", "SystemManufacturer"]) ?? null + const model = + pickString(hardware, ["model", "Model", "product", "ProductName", "productName", "SystemProductName"]) ?? null const serial = pickString(hardware, ["serial", "SerialNumber", "Serial"]) const cpuType = pickString(hardware, ["cpuType", "cpu", "processor", "name", "model"]) const physicalCores = pickNumber(hardware, ["physicalCores", "PhysicalCores", "cores", "Cores"]) const logicalCores = pickNumber(hardware, ["logicalCores", "LogicalCores", "threads", "Threads"]) - const memoryGiB = formatBytes(pickNumber(hardware, ["memoryBytes", "MemoryBytes", "totalMemory", "TotalMemory"])) + const memoryBytes = + parseBytesLike(pickValue(hardware, ["memoryBytes", "MemoryBytes", "totalMemory", "TotalMemory"])) ?? + parseBytesLike(pickValue(hardware, ["memory"])) ?? + parseBytesLike(pickValue(hardware, ["ram"])) ?? + null + const memoryGiB = memoryBytes !== null ? formatBytesValue(memoryBytes) : null return { vendor, model, @@ -465,6 +850,7 @@ function extractHardware(inventory: Record | null) { physicalCores, logicalCores, memoryGiB, + memoryBytes, } } @@ -548,44 +934,413 @@ function summarizeLinkedUsers(users: LinkedUser[] | undefined): string | null { return parts.join("; ") } -function countSoftwareEntries(inventory: Record | null): number | null { - if (!inventory) return null - const direct = inventory["software"] - if (Array.isArray(direct)) return direct.length - const extended = pickRecord(inventory, ["extended", "Extended"]) - const windows = pickRecord(extended, ["windows", "Windows"]) - const software = windows ? windows["software"] ?? windows["installedPrograms"] ?? windows["InstalledPrograms"] : undefined - if (Array.isArray(software)) return software.length +function clampPercent(value: number): number { + if (Number.isNaN(value)) return value + return Math.max(0, Math.min(100, value)) +} + +function parseBytesLike(raw: unknown): number | null { + if (raw === null || raw === undefined) return null + if (typeof raw === "number" && Number.isFinite(raw)) { + if (raw > 10_000 && raw < 10_000_000) { + return raw * 1024 + } + return raw + } + if (typeof raw === "string") { + const trimmed = raw.trim() + if (!trimmed) return null + const numeric = Number(trimmed) + if (Number.isFinite(numeric)) { + return numeric + } + const match = trimmed.match(/^([-+]?[0-9]*\.?[0-9]+)\s*([kmgtp]?i?b)?/i) + if (match) { + const value = Number(match[1]) + if (!Number.isFinite(value)) return null + const unit = (match[2] ?? "").toLowerCase() + const multipliers: Record = { + b: 1, + kb: 1000, + kib: 1024, + mb: 1000 ** 2, + mib: 1024 ** 2, + gb: 1000 ** 3, + gib: 1024 ** 3, + tb: 1000 ** 4, + tib: 1024 ** 4, + pb: 1000 ** 5, + pib: 1024 ** 5, + } + const multiplier = multipliers[unit] ?? 1 + return value * multiplier + } + } return null } -function extractSoftwareEntries(hostname: string, inventory: Record | null): SoftwareEntry[] { - if (!inventory) return [] - const entries: SoftwareEntry[] = [] +function formatBytesValue(bytes: number | null | undefined): string { + if (bytes === null || bytes === undefined || Number.isNaN(bytes) || bytes < 0) return "—" + const units = ["B", "KB", "MB", "GB", "TB", "PB"] + let value = bytes + let unitIndex = 0 + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024 + unitIndex += 1 + } + const fixed = value >= 10 || value % 1 === 0 ? value.toFixed(0) : value.toFixed(1) + return `${fixed} ${units[unitIndex]}` +} - const pushEntry = (record: Record | null) => { +function formatPercentValue(value: number | null | undefined): string { + if (value === null || value === undefined || Number.isNaN(value)) return "—" + return `${clampPercent(value).toFixed(0)}%` +} + +function parseDateish(value: unknown): number | null { + if (!value) return null + if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.getTime() + if (typeof value === "number" && Number.isFinite(value)) { + if (value > 10_000_000_000) return value + if (value > 1_000_000_000) return value * 1000 + if (value > 0) return value * 1000 + return null + } + if (typeof value !== "string") return null + const trimmed = value.trim() + if (!trimmed) return null + const numeric = Number(trimmed) + if (Number.isFinite(numeric)) return parseDateish(numeric) + const iso = Date.parse(trimmed) + if (!Number.isNaN(iso)) return iso + const digits = trimmed.replace(/[^0-9]/g, "") + if (digits.length >= 8) { + const yyyy = Number(digits.slice(0, 4)) + const mm = Number(digits.slice(4, 6)) + const dd = Number(digits.slice(6, 8)) + const hh = Number(digits.slice(8, 10) || "0") + const mi = Number(digits.slice(10, 12) || "0") + const ss = Number(digits.slice(12, 14) || "0") + if (yyyy > 1900 && mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31) { + const parsed = Date.UTC(yyyy, mm - 1, dd, hh, mi, ss) + if (!Number.isNaN(parsed)) return parsed + } + } + return null +} + +function stringifyMetadata(metadata: Record | null | undefined): string { + if (!metadata || Object.keys(metadata).length === 0) return "—" + try { + return JSON.stringify(metadata) + } catch { + return String(metadata) + } +} + +type RemoteAccessNormalized = { + provider: string | null + identifier: string | null + url: string | null + notes: string | null + lastVerifiedAt: number | null + origin: string + metadata: Record | null +} + +function normalizeRemoteAccessEntry( + value: unknown, + origin: string, + providerHint?: string, +): RemoteAccessNormalized | null { + if (value === null || value === undefined) return null + if (typeof value === "string") { + const trimmed = value.trim() + if (!trimmed) return null + const isUrl = /^https?:\/\//i.test(trimmed) + return { + provider: providerHint ?? null, + identifier: isUrl ? null : trimmed, + url: isUrl ? trimmed : null, + notes: null, + lastVerifiedAt: null, + origin, + metadata: null, + } + } + const record = toRecord(value) + if (!record) return null + const provider = + ensureString(record["provider"]) ?? + ensureString(record["tool"]) ?? + ensureString(record["vendor"]) ?? + ensureString(record["name"]) ?? + providerHint ?? + null + const identifier = + ensureString(record["identifier"]) ?? + ensureString(record["code"]) ?? + ensureString(record["id"]) ?? + ensureString(record["accessId"]) ?? + ensureString(record["value"]) ?? + ensureString(record["label"]) ?? + null + const url = + ensureString(record["url"]) ?? + ensureString(record["link"]) ?? + ensureString(record["remoteUrl"]) ?? + ensureString(record["console"]) ?? + ensureString(record["viewer"]) ?? + null + const notes = + ensureString(record["notes"]) ?? + ensureString(record["note"]) ?? + ensureString(record["description"]) ?? + ensureString(record["obs"]) ?? + null + const lastVerifiedRaw = + record["lastVerifiedAt"] ?? record["verifiedAt"] ?? record["checkedAt"] ?? record["updatedAt"] ?? record["timestamp"] + const lastVerifiedAt = parseDateish(lastVerifiedRaw) + const metadata = Object.keys(record).length > 0 ? record : null + if (!identifier && !url && !provider) { + return null + } + return { + provider, + identifier, + url, + notes, + lastVerifiedAt, + origin, + metadata, + } +} + +function collectRemoteAccessEntries(machine: MachineInventoryRecord): RemoteAccessNormalized[] { + const raw = machine.remoteAccess + const entries: RemoteAccessNormalized[] = [] + const push = (entry: RemoteAccessNormalized | null) => { + if (entry) entries.push(entry) + } + + const handleValue = (value: unknown, origin: string, providerHint?: string) => { + if (Array.isArray(value)) { + value.forEach((item) => push(normalizeRemoteAccessEntry(item, origin, providerHint))) + return + } + const record = toRecord(value) + if (!record) { + push(normalizeRemoteAccessEntry(value, origin, providerHint)) + return + } + const keys = Object.keys(record) + const looksLikeEntry = ["provider", "identifier", "url", "notes", "id", "value"].some((key) => key in record) + if (looksLikeEntry) { + push(normalizeRemoteAccessEntry(record, origin, providerHint)) + return + } + if ("entries" in record && Array.isArray(record.entries)) { + record.entries.forEach((item) => push(normalizeRemoteAccessEntry(item, `${origin}.entries`, providerHint))) + return + } + keys.forEach((key) => { + handleValue(record[key], `${origin}.${key}`, key) + }) + } + + handleValue(raw, "machine.remoteAccess") + return entries.filter( + (entry, index, array) => + array.findIndex( + (other) => + (other.provider ?? "").toLowerCase() === (entry.provider ?? "").toLowerCase() && + (other.identifier ?? "").toLowerCase() === (entry.identifier ?? "").toLowerCase() && + (other.url ?? "").toLowerCase() === (entry.url ?? "").toLowerCase(), + ) === index, + ) +} + +type CollaboratorInfo = { name: string | null; email: string | null } + +function extractCollaborator(machine: MachineInventoryRecord, inventory: Record | null): CollaboratorInfo | null { + if (machine.assignedUserEmail || machine.assignedUserName) { + return { + name: machine.assignedUserName ?? null, + email: machine.assignedUserEmail ?? null, + } + } + const collab = pickRecord(inventory, ["collaborator", "Collaborator"]) + if (!collab) return null + const email = pickString(collab, ["email", "Email"]) + const name = pickString(collab, ["name", "Name"]) + if (!email && !name) return null + return { + name: name ?? null, + email: email ?? null, + } +} + +type FleetInfo = { id: string | null; team: string | null; updatedAt: number | null } + +function extractFleetInfo(inventory: Record | null): FleetInfo | null { + const fleet = pickRecord(inventory, ["fleet", "Fleet"]) + if (!fleet) return null + const id = ensureString(fleet["id"]) ?? ensureString(fleet["fleetId"]) ?? ensureString(fleet["teamId"]) + const team = ensureString(fleet["teamId"]) ?? ensureString(fleet["team"]) ?? ensureString(fleet["group"]) + const updatedAt = parseDateish(fleet["detailUpdatedAt"] ?? fleet["updatedAt"]) + if (!id && !team && !updatedAt) return null + return { + id: id ?? null, + team: team ?? null, + updatedAt: updatedAt ?? null, + } +} + +type SystemInfo = { + osBuild: string | null + license: string | null + experience: string | null + domain: string | null + workgroup: string | null + deviceName: string | null + systemManufacturer: string | null + systemModel: string | null + boardSerial: string | null + installDate: number | null + windowsEdition: string | null +} + +function extractSystemInfo(inventory: Record | null): SystemInfo { + const extended = pickRecord(inventory, ["extended", "Extended"]) + const windows = pickRecord(extended, ["windows", "Windows"]) + const osInfo = pickRecord(windows, ["osInfo", "OSInfo", "OsInfo"]) + const computerSystem = pickRecord(windows, ["computerSystem", "ComputerSystem"]) + const baseboard = pickRecord(windows, ["baseboard", "Baseboard"]) + + const baseBuild = + pickString(osInfo, ["CurrentBuildNumber", "currentBuildNumber", "OSBuild", "osBuild", "BuildNumber", "buildNumber"]) ?? + pickString(osInfo, ["CurrentBuild", "currentBuild"]) + const ubr = pickString(osInfo, ["UBR", "ubr"]) + const osBuild = baseBuild ? (ubr && /^\d+$/.test(ubr) ? `${baseBuild}.${ubr}` : baseBuild) : null + + const licenseStatusDescription = + pickString(osInfo, ["LicenseStatusDescription", "licenseStatusDescription", "StatusDescription", "statusDescription"]) ?? null + const licenseStatusNumber = pickNumber(osInfo, ["LicenseStatus", "licenseStatus"]) + const licenseBoolRaw = pickValue(osInfo, ["IsActivated", "isActivated", "IsLicensed", "isLicensed"]) + const licenseBool = + typeof licenseBoolRaw === "boolean" + ? licenseBoolRaw + : typeof licenseBoolRaw === "number" + ? licenseBoolRaw === 1 + : typeof licenseBoolRaw === "string" + ? ["1", "true", "licensed", "ativado", "activated"].includes(licenseBoolRaw.toLowerCase()) + : undefined + const license = + licenseBool === true + ? "Ativada" + : licenseBool === false + ? "Não ativada" + : licenseStatusNumber === 1 + ? "Ativada" + : licenseStatusDescription ?? null + + const experience = + pickString(osInfo, ["Experience", "experience"]) ?? + ((() => { + const pack = pickString(osInfo, ["FeatureExperiencePack", "featureExperiencePack"]) + if (pack) return `Feature Experience Pack ${pack}` + return null + })()) + + const domain = pickString(computerSystem, ["Domain", "domain"]) + const workgroup = pickString(computerSystem, ["Workgroup", "workgroup"]) + const deviceName = + pickString(osInfo, ["ComputerName", "computerName"]) ?? + pickString(computerSystem, ["DNSHostName", "dnsHostName", "Name", "name"]) + + const systemManufacturer = + pickString(computerSystem, ["Manufacturer", "manufacturer", "SystemManufacturer", "systemManufacturer"]) ?? + pickString(osInfo, ["Manufacturer", "manufacturer"]) + const systemModel = + pickString(computerSystem, ["Model", "model", "SystemProductName", "systemProductName"]) ?? + pickString(osInfo, ["Model", "model"]) + const boardSerial = + pickString(baseboard, ["SerialNumber", "serialNumber", "Serial"]) ?? + pickString(computerSystem, ["SystemSKUNumber", "systemSKUNumber"]) + const installDate = + parseDateish(osInfo?.["InstallDate"]) ?? + parseDateish(osInfo?.["InstallationDate"]) ?? + parseDateish(osInfo?.["InstalledOn"]) + const edition = + pickString(osInfo, ["ProductName", "productName"]) ?? + pickString(osInfo, ["Caption", "caption"]) ?? + pickString(osInfo, ["EditionID", "editionId"]) + + return { + osBuild, + license, + experience, + domain, + workgroup, + deviceName, + systemManufacturer, + systemModel, + boardSerial, + installDate, + windowsEdition: edition, + } +} + +type SoftwareEntryInternal = SoftwareEntry + +function extractSoftwareEntries(hostname: string, inventory: Record | null): SoftwareEntryInternal[] { + if (!inventory) return [] + const map = new Map() + + const push = (record: Record | null) => { if (!record) return const name = pickString(record, ["DisplayName", "displayName", "Name", "name", "Title", "title"]) if (!name) return const version = pickString(record, ["DisplayVersion", "displayVersion", "Version", "version"]) - const source = pickString(record, ["ParentDisplayName", "parent", "Source", "source", "SystemComponent"]) + const source = + pickString(record, ["ParentDisplayName", "parent", "Source", "source", "SystemComponent", "PublisherURL"]) ?? null const publisher = pickString(record, ["Publisher", "publisher", "Vendor", "vendor"]) - const installed = pickString(record, ["InstalledOn", "installedOn", "InstallDate", "installDate", "InstallDateUTC"]) - entries.push({ + const installedRaw = + pickValue(record, ["InstalledOn", "installedOn", "InstallDateUTC", "InstallDate", "installDate", "InstallTime"]) ?? null + const installedTs = parseDateish(installedRaw) + const installedOn = + installedTs !== null + ? formatDateTime(installedTs) ?? null + : typeof installedRaw === "string" && installedRaw.trim().length > 0 + ? installedRaw.trim() + : null + + const entry: SoftwareEntryInternal = { hostname, name, - version, + version: version ?? null, source, - publisher, - installedOn: installed, - }) + publisher: publisher ?? null, + installedOn, + } + const key = [ + hostname.toLowerCase(), + name.toLowerCase(), + (version ?? "").toLowerCase(), + (source ?? "").toLowerCase(), + (publisher ?? "").toLowerCase(), + (installedOn ?? "").toLowerCase(), + ].join("|") + if (!map.has(key)) { + map.set(key, entry) + } } const direct = inventory["software"] if (Array.isArray(direct)) { - direct.map(toRecord).forEach(pushEntry) + direct.map(toRecord).forEach(push) } else if (direct) { - pushEntry(toRecord(direct)) + push(toRecord(direct)) } const extended = pickRecord(inventory, ["extended", "Extended"]) @@ -593,57 +1348,109 @@ function extractSoftwareEntries(hostname: string, inventory: Record { + if (typeof pkg === "string") { + push({ name: pkg } as Record) + } else { + push(toRecord(pkg)) + } + }) + } + } + + const macos = pickRecord(extended, ["macos", "MacOS", "macOS"]) + if (macos) { + const packages = macos["packages"] + if (Array.isArray(packages)) { + packages.forEach((pkg) => push(toRecord(pkg))) + } + } + + return Array.from(map.values()).sort((a, b) => { + const nameCompare = a.name.localeCompare(b.name, "pt-BR") + if (nameCompare !== 0) return nameCompare + return (a.version ?? "").localeCompare(b.version ?? "", "pt-BR") + }) } -function extractDiskEntries(hostname: string, inventory: Record | null): DiskEntry[] { - if (!inventory) return [] - const entries: DiskEntry[] = [] +type PartitionEntry = { + hostname: string + name: string | null + mount: string | null + fs: string | null + capacityBytes: number | null + freeBytes: number | null + interface: string | null + serial: string | null + origin: string +} - const pushDisk = (record: Record | null) => { +function extractPartitionEntries(hostname: string, inventory: Record | null): PartitionEntry[] { + const entries: PartitionEntry[] = [] + if (!inventory) return entries + + const push = (value: unknown, origin: string) => { + const record = toRecord(value) if (!record) return - const size = formatBytes(pickNumber(record, ["sizeBytes", "Size", "size"])) - const free = formatBytes(pickNumber(record, ["freeBytes", "FreeSpace", "free", "Free"])) - const type = pickString(record, ["type", "Type", "MediaType"]) - const model = pickString(record, ["model", "Model"]) - const name = pickString(record, ["name", "Name", "DeviceID", "Device"]) - const mountPoint = pickString(record, ["mount", "Mount", "mountpoint", "MountPoint", "path"]) + const name = + pickString(record, ["name", "Name", "VolumeName", "label", "Label"]) ?? + pickString(record, ["DeviceID", "deviceId"]) + const mount = + pickString(record, ["mountPoint", "MountPoint", "mount", "Mount", "path", "Path"]) ?? + pickString(record, ["DeviceID", "deviceId"]) + const fs = pickString(record, ["fs", "FS", "filesystem", "FileSystem", "FileSystemType", "type"]) + const capacityBytes = + parseBytesLike(record["totalBytes"]) ?? + parseBytesLike(record["sizeBytes"]) ?? + parseBytesLike(record["Size"]) ?? + parseBytesLike(record["TotalSpace"]) ?? + parseBytesLike(record["Capacity"]) + const freeBytes = + parseBytesLike(record["availableBytes"]) ?? + parseBytesLike(record["freeBytes"]) ?? + parseBytesLike(record["FreeSpace"]) ?? + parseBytesLike(record["available"]) ?? + parseBytesLike(record["Free"]) + const interfaceType = pickString(record, ["interface", "Interface", "interfaceType", "InterfaceType"]) const serial = pickString(record, ["serial", "Serial", "SerialNumber"]) - const smartStatus = pickString(record, ["smartStatus", "SmartStatus", "status", "Status"]) entries.push({ hostname, - type, - model, - name, - mountPoint, - size, - free, - serial, - smartStatus, + name: name ?? mount ?? null, + mount: mount ?? null, + fs: fs ?? null, + capacityBytes: capacityBytes ?? null, + freeBytes: freeBytes ?? null, + interface: interfaceType ?? null, + serial: serial ?? null, + origin, }) } const direct = inventory["disks"] if (Array.isArray(direct)) { - direct.map(toRecord).forEach(pushDisk) - } else { - pushDisk(toRecord(direct)) + direct.forEach((item) => push(item, "inventory.disks")) + } else if (direct) { + push(direct, "inventory.disks") } const extended = pickRecord(inventory, ["extended", "Extended"]) const windows = pickRecord(extended, ["windows", "Windows"]) if (windows) { - const diskDrives = windows["diskDrives"] ?? windows["DiskDrives"] - if (Array.isArray(diskDrives)) { - diskDrives.map(toRecord).forEach(pushDisk) - } else { - pushDisk(toRecord(diskDrives)) + const logical = windows["logicalDisks"] ?? windows["LogicalDisks"] ?? windows["volumes"] ?? windows["Volumes"] + if (Array.isArray(logical)) { + logical.forEach((item) => push(item, "extended.windows.logicalDisks")) + } else if (logical) { + push(logical, "extended.windows.logicalDisks") } } @@ -651,11 +1458,382 @@ function extractDiskEntries(hostname: string, inventory: Record if (linux) { const lsblk = linux["lsblk"] if (Array.isArray(lsblk)) { - lsblk.map(toRecord).forEach(pushDisk) - } else { - pushDisk(toRecord(lsblk)) + lsblk.forEach((item) => push(item, "extended.linux.lsblk")) + } else if (lsblk) { + push(lsblk, "extended.linux.lsblk") } } - return entries + const macos = pickRecord(extended, ["macos", "MacOS", "macOS"]) + if (macos) { + const volumes = macos["volumes"] + if (Array.isArray(volumes)) { + volumes.forEach((item) => push(item, "extended.macos.volumes")) + } else if (volumes) { + push(volumes, "extended.macos.volumes") + } + } + + const dedup = new Map() + entries.forEach((entry) => { + const key = [ + entry.hostname.toLowerCase(), + (entry.name ?? "").toLowerCase(), + (entry.mount ?? "").toLowerCase(), + (entry.fs ?? "").toLowerCase(), + ].join("|") + if (!dedup.has(key)) { + dedup.set(key, entry) + } + }) + return Array.from(dedup.values()) +} + +type PhysicalDiskEntry = { + hostname: string + model: string | null + sizeBytes: number | null + interface: string | null + mediaType: string | null + serial: string | null + origin: string +} + +function extractPhysicalDiskEntries(hostname: string, inventory: Record | null): PhysicalDiskEntry[] { + const entries: PhysicalDiskEntry[] = [] + if (!inventory) return entries + + const push = (value: unknown, origin: string) => { + const record = toRecord(value) + if (!record) return + const model = pickString(record, ["Model", "model", "Caption", "DeviceID"]) + const serial = + pickString(record, ["SerialNumber", "serialNumber", "Serial"]) ?? pickString(record, ["DeviceID", "deviceId"]) + const interfaceType = pickString(record, ["InterfaceType", "interfaceType", "BusType"]) + const mediaType = pickString(record, ["MediaType", "mediaType", "Media"]) ?? pickString(record, ["Type", "type"]) + const sizeBytes = + parseBytesLike(record["Size"]) ?? + parseBytesLike(record["sizeBytes"]) ?? + parseBytesLike(record["TotalSize"]) ?? + parseBytesLike(record["Capacity"]) + entries.push({ + hostname, + model: model ?? null, + sizeBytes: sizeBytes ?? null, + interface: interfaceType ?? null, + mediaType: mediaType ?? null, + serial: serial ?? null, + origin, + }) + } + + const extended = pickRecord(inventory, ["extended", "Extended"]) + const windows = pickRecord(extended, ["windows", "Windows"]) + if (windows) { + const disks = windows["disks"] ?? windows["Disks"] + if (Array.isArray(disks)) { + disks.forEach((item) => push(item, "extended.windows.disks")) + } else if (disks) { + push(disks, "extended.windows.disks") + } + const drives = windows["diskDrives"] ?? windows["DiskDrives"] + if (Array.isArray(drives)) { + drives.forEach((item) => push(item, "extended.windows.diskDrives")) + } else if (drives) { + push(drives, "extended.windows.diskDrives") + } + } + + const hardware = pickRecord(inventory, ["hardware", "Hardware"]) + const storage = pickArray(hardware, ["storage", "Storage"]) + storage.forEach((item) => push(item, "inventory.hardware.storage")) + + const linux = pickRecord(extended, ["linux", "Linux"]) + if (linux) { + const disks = linux["disks"] + if (Array.isArray(disks)) { + disks.forEach((item) => push(item, "extended.linux.disks")) + } else if (disks) { + push(disks, "extended.linux.disks") + } + } + + const dedup = new Map() + entries.forEach((entry) => { + const key = [ + entry.hostname.toLowerCase(), + (entry.serial ?? "").toLowerCase(), + (entry.model ?? "").toLowerCase(), + ].join("|") + if (!dedup.has(key)) { + dedup.set(key, entry) + } + }) + return Array.from(dedup.values()) +} + +type NetworkEntry = { hostname: string; name: string | null; mac: string | null; address: string | null; origin: string } + +function extractNetworkEntries(hostname: string, inventory: Record | null): NetworkEntry[] { + const entries: NetworkEntry[] = [] + if (!inventory) return entries + + const push = (value: unknown, origin: string, nameHint?: string) => { + const record = toRecord(value) + if (!record) return + const mac = + pickString(record, ["mac", "MAC", "MacAddress", "MACAddress", "addressMac"]) ?? + (Array.isArray(record["MACAddress"]) ? record["MACAddress"][0] : null) + const ipValue = + pickString(record, ["ip", "IP", "address", "Address", "ipv4", "IPv4"]) ?? + (Array.isArray(record["IPAddress"]) + ? (record["IPAddress"] as unknown[]).map((addr) => ensureString(addr)).filter(Boolean)?.[0] ?? null + : null) + const name = + pickString(record, ["name", "Name", "Interface", "InterfaceDescription", "AdapterName"]) ?? + nameHint ?? + null + entries.push({ + hostname, + name, + mac: mac ?? null, + address: ipValue ?? null, + origin, + }) + } + + const network = inventory["network"] + if (Array.isArray(network)) { + network.forEach((item) => push(item, "inventory.network")) + } else if (network && typeof network === "object") { + push(network, "inventory.network.summary") + const macAddresses = (network as Record)["macAddresses"] + if (Array.isArray(macAddresses)) { + macAddresses + .map((addr) => ensureString(addr)) + .filter(Boolean) + .forEach((addr) => + entries.push({ + hostname, + name: null, + mac: addr ?? null, + address: null, + origin: "inventory.network.macAddresses", + }), + ) + } + } + + const extended = pickRecord(inventory, ["extended", "Extended"]) + const windows = pickRecord(extended, ["windows", "Windows"]) + if (windows) { + const adapters = windows["networkAdapters"] ?? windows["NetworkAdapters"] ?? windows["networkInterfaces"] + if (Array.isArray(adapters)) { + adapters.forEach((adapter) => push(adapter, "extended.windows.networkAdapters")) + } else if (adapters) { + push(adapters, "extended.windows.networkAdapters") + } + } + + const linux = pickRecord(extended, ["linux", "Linux"]) + if (linux) { + const interfaces = linux["networkInterfaces"] + if (Array.isArray(interfaces)) { + interfaces.forEach((iface) => push(iface, "extended.linux.networkInterfaces")) + } else if (interfaces) { + push(interfaces, "extended.linux.networkInterfaces") + } + } + + const dedup = new Map() + entries.forEach((entry) => { + const key = [ + entry.hostname.toLowerCase(), + (entry.name ?? "").toLowerCase(), + (entry.mac ?? "").toLowerCase(), + (entry.address ?? "").toLowerCase(), + entry.origin, + ].join("|") + if (!dedup.has(key)) { + dedup.set(key, entry) + } + }) + return Array.from(dedup.values()) +} + +type ServiceEntry = { hostname: string; name: string | null; displayName: string | null; status: string | null; origin: string } + +function extractServiceEntries(hostname: string, inventory: Record | null): ServiceEntry[] { + const entries: ServiceEntry[] = [] + if (!inventory) return entries + + const push = (value: unknown, origin: string) => { + const record = toRecord(value) + if (!record) return + const name = pickString(record, ["Name", "name", "ServiceName", "serviceName"]) + const displayName = pickString(record, ["DisplayName", "displayName", "Description"]) + const status = pickString(record, ["Status", "status", "State", "state"]) + if (!name && !displayName) return + entries.push({ + hostname, + name: name ?? displayName ?? null, + displayName: displayName ?? null, + status: status ?? null, + origin, + }) + } + + const direct = inventory["services"] + if (Array.isArray(direct)) { + direct.forEach((svc) => push(svc, "inventory.services")) + } else if (direct) { + push(direct, "inventory.services") + } + + const extended = pickRecord(inventory, ["extended", "Extended"]) + const windows = pickRecord(extended, ["windows", "Windows"]) + if (windows) { + const services = windows["services"] ?? windows["Services"] + if (Array.isArray(services)) { + services.forEach((svc) => push(svc, "extended.windows.services")) + } else if (services) { + push(services, "extended.windows.services") + } + } + + const linux = pickRecord(extended, ["linux", "Linux"]) + if (linux) { + const services = linux["services"] + if (Array.isArray(services)) { + services.forEach((svc) => push(svc, "extended.linux.services")) + } else if (services) { + push(services, "extended.linux.services") + } + } + + const dedup = new Map() + entries.forEach((entry) => { + const key = [ + entry.hostname.toLowerCase(), + (entry.name ?? "").toLowerCase(), + (entry.displayName ?? "").toLowerCase(), + ].join("|") + if (!dedup.has(key)) dedup.set(key, entry) + }) + return Array.from(dedup.values()) +} + +function deriveMachineMetrics( + machine: MachineInventoryRecord, + inventory: Record | null, +): WorksheetRow | null { + const metricsRecord = toRecord(machine.metrics ?? null) + if (!metricsRecord) return null + + const capturedAt = + parseDateish(metricsRecord["capturedAt"]) ?? + parseDateish(metricsRecord["collectedAt"]) ?? + parseDateish(metricsRecord["timestamp"]) ?? + null + + const cpuPercentRaw = Number( + metricsRecord["cpuUsagePercent"] ?? + metricsRecord["cpuUsage"] ?? + metricsRecord["cpu_percent"] ?? + metricsRecord["cpu"], + ) + const cpuPercent = Number.isFinite(cpuPercentRaw) ? clampPercent(cpuPercentRaw) : NaN + + const memoryTotalCandidates = [ + metricsRecord["memoryTotalBytes"], + metricsRecord["memory_total"], + metricsRecord["memory"], + metricsRecord["memoryTotal"], + ] + .map(parseBytesLike) + .filter((value): value is number => value !== null) + const hardware = extractHardware(inventory) + if (hardware.memoryBytes && !memoryTotalCandidates.includes(hardware.memoryBytes)) { + memoryTotalCandidates.push(hardware.memoryBytes) + } + const memoryTotalBytes = memoryTotalCandidates.find((value) => Number.isFinite(value)) ?? null + + const memoryUsedCandidates = [ + metricsRecord["memoryUsedBytes"], + metricsRecord["memory_used"], + metricsRecord["memoryUsed"], + metricsRecord["memoryBytesUsed"], + ] + .map(parseBytesLike) + .filter((value): value is number => value !== null) + const memoryUsedBytes = memoryUsedCandidates.find((value) => Number.isFinite(value)) ?? null + + const memoryPercentCandidates = [ + metricsRecord["memoryUsedPercent"], + metricsRecord["memory_percent"], + metricsRecord["memoryPercent"], + ] + .map((value) => (typeof value === "number" ? clampPercent(value) : NaN)) + .filter((value) => Number.isFinite(value)) + let memoryPercent = memoryPercentCandidates[0] ?? NaN + + let resolvedMemoryUsed = memoryUsedBytes + if ((resolvedMemoryUsed === null || Number.isNaN(resolvedMemoryUsed)) && memoryTotalBytes && Number.isFinite(memoryPercent)) { + resolvedMemoryUsed = (memoryPercent / 100) * memoryTotalBytes + } else if (resolvedMemoryUsed !== null && memoryTotalBytes) { + memoryPercent = clampPercent((resolvedMemoryUsed / memoryTotalBytes) * 100) + } + + const partitions = extractPartitionEntries(machine.hostname, inventory) + let diskTotal = 0 + let diskFree = 0 + partitions.forEach((partition) => { + if (partition.capacityBytes) { + diskTotal += partition.capacityBytes + } + if (partition.freeBytes) { + diskFree += partition.freeBytes + } + }) + const diskUsed = diskTotal > 0 ? Math.max(0, diskTotal - diskFree) : null + const diskPercentCandidates = [ + diskTotal > 0 ? clampPercent((diskUsed ?? 0) / diskTotal * 100) : NaN, + Number(metricsRecord["diskUsagePercent"]), + Number(metricsRecord["diskUsedPercent"]), + Number(metricsRecord["diskUsage"]), + ].filter((value) => Number.isFinite(value)) + const diskPercent = diskPercentCandidates[0] ?? NaN + + const gpuPercentCandidates = [ + Number(metricsRecord["gpuUsagePercent"]), + Number(metricsRecord["gpuUsage"]), + Number(metricsRecord["gpu_percent"]), + Number(metricsRecord["gpu"]), + ].filter((value) => Number.isFinite(value)) + const gpuPercent = gpuPercentCandidates[0] ?? NaN + + if ( + Number.isNaN(cpuPercent) && + resolvedMemoryUsed === null && + memoryTotalBytes === null && + Number.isNaN(memoryPercent) && + diskUsed === null && + diskTotal === 0 && + Number.isNaN(gpuPercent) + ) { + return null + } + + return [ + machine.hostname, + capturedAt ? formatDateTime(capturedAt) ?? "—" : "—", + Number.isNaN(cpuPercent) ? "—" : `${cpuPercent.toFixed(0)}%`, + formatBytesValue(resolvedMemoryUsed), + formatBytesValue(memoryTotalBytes), + Number.isNaN(memoryPercent) ? "—" : `${memoryPercent.toFixed(0)}%`, + formatBytesValue(diskUsed), + formatBytesValue(diskTotal || null), + Number.isNaN(diskPercent) ? "—" : `${diskPercent.toFixed(0)}%`, + Number.isNaN(gpuPercent) ? "—" : `${gpuPercent.toFixed(0)}%`, + ] }