feat: improve machines inventory exports

This commit is contained in:
codex-bot 2025-10-30 16:09:06 -03:00
parent d92c817e7b
commit 38b46f32ce
5 changed files with 858 additions and 222 deletions

View file

@ -0,0 +1,661 @@
import { buildXlsxWorkbook, type WorksheetConfig } from "@/lib/xlsx"
import type { Id } from "@/convex/_generated/dataModel"
type LinkedUser = {
id: string
email: string | null
name: string | null
}
export type MachineInventoryRecord = {
id: Id<"machines">
tenantId: string
hostname: string
companyId: Id<"companies"> | null
companySlug: string | null
companyName: string | null
status: string | null
isActive: boolean
lastHeartbeatAt: number | null
persona: string | null
assignedUserName: string | null
assignedUserEmail: string | null
authEmail: string | null
osName: string
osVersion: string | null
architecture: string | null
macAddresses: string[]
serialNumbers: string[]
registeredBy: string | null
createdAt: number
updatedAt: number
token: { expiresAt: number; usageCount: number; lastUsedAt: number | null } | null
inventory: Record<string, unknown> | null
linkedUsers?: LinkedUser[]
}
type WorkbookOptions = {
tenantId: string
generatedBy?: string | null
companyFilterLabel?: string | null
generatedAt?: Date
}
type SoftwareEntry = {
hostname: string
name: string
version: string | null
source: string | null
publisher: string | null
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",
"Status",
"Persona",
"Ativa",
"Último heartbeat",
"Responsável",
"E-mail responsável",
"Usuários vinculados",
"E-mail autenticado",
"Sistema operacional",
"Versão SO",
"Arquitetura",
"Fabricante",
"Modelo",
"Serial hardware",
"Processador",
"Cores físicas",
"Cores lógicas",
"Memória (GiB)",
"GPUs",
"Labels",
"MACs",
"Seriais",
"IP principal",
"IP público",
"Registrada via",
"Token expira em",
"Token último uso",
"Uso do token",
"Criada em",
"Atualizada em",
"Softwares instalados",
] 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,
] 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 STATUS_LABELS: Record<string, string> = {
online: "Online",
offline: "Offline",
stale: "Sem sinal",
maintenance: "Manutenção",
blocked: "Bloqueada",
deactivated: "Desativada",
unknown: "Desconhecido",
}
const PERSONA_LABELS: Record<string, string> = {
collaborator: "Colaborador",
manager: "Gestor",
machine: "Máquina",
}
const SUMMARY_STATUS_ORDER = ["Online", "Sem sinal", "Offline", "Manutenção", "Bloqueada", "Desativada", "Desconhecido"]
type WorksheetRow = Array<unknown>
export function buildMachinesInventoryWorkbook(
machines: MachineInventoryRecord[],
options: WorkbookOptions,
): Buffer {
const generatedAt = options.generatedAt ?? new Date()
const summaryRows = buildSummaryRows(machines, options, generatedAt)
const inventoryRows = machines.map((machine) => flattenMachine(machine))
const linksRows = buildLinkedUsersRows(machines)
const softwareRows = buildSoftwareRows(machines)
const diskRows = buildDiskRows(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],
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],
freezePane: { rowSplit: 1 },
autoFilter: true,
},
{
name: "Softwares",
headers: [...SOFTWARE_HEADERS],
rows: softwareRows.length > 0 ? softwareRows : [["—", "—", "—", "—", "—", "—"]],
columnWidths: [...SOFTWARE_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],
freezePane: { rowSplit: 1 },
autoFilter: diskRows.length > 0,
},
]
return buildXlsxWorkbook(sheets)
}
function buildSummaryRows(
machines: MachineInventoryRecord[],
options: WorkbookOptions,
generatedAt: Date,
): Array<[string, unknown]> {
const rows: Array<[string, unknown]> = [
["Tenant", options.tenantId],
["Gerado em", formatDateTime(generatedAt.getTime()) ?? generatedAt.toISOString()],
]
if (options.generatedBy) {
rows.push(["Solicitado por", options.generatedBy])
}
if (options.companyFilterLabel) {
rows.push(["Filtro de empresa", options.companyFilterLabel])
}
rows.push(["Total de máquinas", machines.length])
const activeCount = machines.filter((machine) => machine.isActive).length
rows.push(["Máquinas ativas", activeCount])
rows.push(["Máquinas inativas", machines.length - activeCount])
const statusCounts = new Map<string, number>()
machines.forEach((machine) => {
const label = describeStatus(machine.status)
statusCounts.set(label, (statusCounts.get(label) ?? 0) + 1)
})
const sortedStatuses = Array.from(statusCounts.entries()).sort((a, b) => {
const indexA = SUMMARY_STATUS_ORDER.indexOf(a[0])
const indexB = SUMMARY_STATUS_ORDER.indexOf(b[0])
if (indexA === -1 && indexB === -1) return a[0].localeCompare(b[0], "pt-BR")
if (indexA === -1) return 1
if (indexB === -1) return -1
return indexA - indexB
})
sortedStatuses.forEach(([status, total]) => {
rows.push([`Status: ${status}`, total])
})
const uniqueCompanies = new Set(machines.map((machine) => machine.companyName).filter(Boolean) as string[])
if (uniqueCompanies.size > 0) {
rows.push(["Empresas no resultado", Array.from(uniqueCompanies).sort((a, b) => a.localeCompare(b, "pt-BR")).join(", ")])
}
return rows
}
function flattenMachine(machine: MachineInventoryRecord): WorksheetRow {
const inventory = toRecord(machine.inventory)
const hardware = extractHardware(inventory)
const gpuNames = extractGpuNames(inventory)
const labels = extractLabels(inventory)
const primaryIp = extractPrimaryIp(inventory)
const publicIp = extractPublicIp(inventory)
const softwareCount = countSoftwareEntries(inventory)
const linkedUsers = summarizeLinkedUsers(machine.linkedUsers)
return [
machine.hostname,
machine.companyName ?? "—",
describeStatus(machine.status),
describePersona(machine.persona),
yesNo(machine.isActive),
formatDateTime(machine.lastHeartbeatAt),
machine.assignedUserName ?? machine.assignedUserEmail ?? "—",
machine.assignedUserEmail ?? "—",
linkedUsers ?? "—",
machine.authEmail ?? "—",
machine.osName,
machine.osVersion ?? "—",
machine.architecture ?? "—",
hardware.vendor ?? "—",
hardware.model ?? "—",
hardware.serial ?? "—",
hardware.cpuType ?? "—",
hardware.physicalCores ?? "—",
hardware.logicalCores ?? "—",
hardware.memoryGiB ?? "—",
gpuNames.length > 0 ? gpuNames.join(", ") : "—",
labels.length > 0 ? labels.join(", ") : "—",
machine.macAddresses.length > 0 ? machine.macAddresses.join(", ") : "—",
machine.serialNumbers.length > 0 ? machine.serialNumbers.join(", ") : "—",
primaryIp ?? "—",
publicIp ?? "—",
machine.registeredBy ?? "—",
machine.token?.expiresAt ? formatDateTime(machine.token.expiresAt) ?? "—" : "—",
machine.token?.lastUsedAt ? formatDateTime(machine.token.lastUsedAt) ?? "—" : "—",
machine.token?.usageCount ?? 0,
formatDateTime(machine.createdAt) ?? "—",
formatDateTime(machine.updatedAt) ?? "—",
softwareCount ?? 0,
]
}
function buildLinkedUsersRows(machines: MachineInventoryRecord[]): Array<[string, string | null, string | null, string | null]> {
const rows: Array<[string, string | null, string | null, string | null]> = []
machines.forEach((machine) => {
machine.linkedUsers?.forEach((user) => {
rows.push([machine.hostname, machine.companyName ?? null, user.name ?? user.email ?? null, user.email ?? null])
})
})
return rows
}
function buildSoftwareRows(machines: MachineInventoryRecord[]): WorksheetRow[] {
const rows: WorksheetRow[] = []
machines.forEach((machine) => {
const inventory = toRecord(machine.inventory)
const entries = extractSoftwareEntries(machine.hostname, inventory)
entries.forEach((entry) => {
rows.push([
entry.hostname,
entry.name,
entry.version ?? "—",
entry.source ?? "—",
entry.publisher ?? "—",
entry.installedOn ?? "—",
])
})
})
return rows
}
function buildDiskRows(machines: MachineInventoryRecord[]): WorksheetRow[] {
const rows: WorksheetRow[] = []
machines.forEach((machine) => {
const inventory = toRecord(machine.inventory)
const entries = extractDiskEntries(machine.hostname, inventory)
entries.forEach((disk) => {
rows.push([
disk.hostname,
disk.type ?? "—",
disk.model ?? "—",
disk.name ?? "—",
disk.mountPoint ?? "—",
disk.size ?? "—",
disk.free ?? "—",
disk.serial ?? "—",
disk.smartStatus ?? "—",
])
})
})
return rows
}
function toRecord(value: unknown): Record<string, unknown> | null {
if (value && typeof value === "object" && !Array.isArray(value)) {
return value as Record<string, unknown>
}
return null
}
function toRecordArray(value: unknown): Record<string, unknown>[] {
if (Array.isArray(value)) {
return value.map(toRecord).filter((item): item is Record<string, unknown> => Boolean(item))
}
const record = toRecord(value)
return record ? [record] : []
}
function ensureString(value: unknown): string | null {
if (typeof value === "string") {
const trimmed = value.trim()
return trimmed.length ? trimmed : null
}
if (typeof value === "number" && Number.isFinite(value)) {
return String(value)
}
return null
}
function ensureNumber(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) {
return value
}
if (typeof value === "string") {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : null
}
return null
}
function pickValue(record: Record<string, unknown> | null | undefined, keys: string[]): unknown {
if (!record) return undefined
for (const key of keys) {
if (key in record) {
const value = record[key]
if (value !== undefined && value !== null && value !== "") {
return value
}
}
}
return undefined
}
function pickString(record: Record<string, unknown> | null | undefined, keys: string[]): string | null {
return ensureString(pickValue(record, keys))
}
function pickNumber(record: Record<string, unknown> | null | undefined, keys: string[]): number | null {
return ensureNumber(pickValue(record, keys))
}
function pickRecord(record: Record<string, unknown> | null | undefined, keys: string[]): Record<string, unknown> | null {
return toRecord(pickValue(record, keys))
}
function pickRecordArray(record: Record<string, unknown> | null | undefined, keys: string[]): Record<string, unknown>[] {
return toRecordArray(pickValue(record, keys))
}
function pickArray(record: Record<string, unknown> | null | undefined, keys: string[]): unknown[] {
const value = pickValue(record, keys)
if (Array.isArray(value)) return value
if (value === undefined || value === null) return []
return [value]
}
function describeStatus(status: string | null | undefined): string {
if (!status) return STATUS_LABELS.unknown
const normalized = status.toLowerCase()
return STATUS_LABELS[normalized] ?? status
}
function describePersona(persona: string | null | undefined): string {
if (!persona) return "—"
const normalized = persona.toLowerCase()
return PERSONA_LABELS[normalized] ?? persona
}
function yesNo(value: boolean | null | undefined): string {
if (value === undefined || value === null) return "—"
return value ? "Sim" : "Não"
}
function formatDateTime(value: number | null | undefined): string | null {
if (typeof value !== "number" || Number.isNaN(value)) return null
const date = new Date(value)
const yyyy = date.getUTCFullYear()
const mm = `${date.getUTCMonth() + 1}`.padStart(2, "0")
const dd = `${date.getUTCDate()}`.padStart(2, "0")
const hh = `${date.getUTCHours()}`.padStart(2, "0")
const min = `${date.getUTCMinutes()}`.padStart(2, "0")
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<string, unknown> | null) {
if (!inventory) {
return {
vendor: null,
model: null,
serial: null,
cpuType: null,
physicalCores: null,
logicalCores: null,
memoryGiB: null,
}
}
const hardware = pickRecord(inventory, ["hardware", "Hardware"])
const vendor = pickString(hardware, ["vendor", "Vendor"])
const model = pickString(hardware, ["model", "Model"])
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"]))
return {
vendor,
model,
serial,
cpuType,
physicalCores,
logicalCores,
memoryGiB,
}
}
function extractPrimaryIp(inventory: Record<string, unknown> | null): string | null {
if (!inventory) return null
const network = pickRecord(inventory, ["network", "Network"])
const direct = pickString(network, ["primaryIp", "PrimaryIp", "ip", "address", "PrimaryAddress"])
if (direct) return direct
const networkArray = pickRecordArray(inventory, ["network", "Network"])
for (const entry of networkArray) {
const ip = pickString(entry, ["ip", "IP", "address", "primaryIp", "PrimaryIp"])
if (ip) return ip
}
return null
}
function extractPublicIp(inventory: Record<string, unknown> | null): string | null {
if (!inventory) return null
const network = pickRecord(inventory, ["network", "Network"])
return pickString(network, ["publicIp", "PublicIp", "externalIp", "ExternalIp"])
}
function extractGpuNames(inventory: Record<string, unknown> | null): string[] {
if (!inventory) return []
const names = new Set<string>()
const hardware = pickRecord(inventory, ["hardware", "Hardware"])
const primaryGpu = pickRecord(hardware, ["primaryGpu", "primarygpu", "PrimaryGpu"])
if (primaryGpu) {
const name = pickString(primaryGpu, ["name", "Name", "model", "Model", "GPUName", "gpuName"])
if (name) names.add(name)
}
const gpuArray = pickArray(hardware, ["gpus", "GPUs"])
if (gpuArray.length > 0) {
gpuArray.forEach((gpu) => {
const record = toRecord(gpu)
const name = pickString(record, ["name", "Name", "model", "Model", "GPUName", "gpuName"])
if (name) names.add(name)
})
}
const extended = pickRecord(inventory, ["extended", "Extended"])
const windows = pickRecord(extended, ["windows", "Windows"])
const videoControllers = pickRecordArray(windows, ["videoControllers", "VideoControllers"])
videoControllers.forEach((controller) => {
const name = pickString(controller, ["Name", "name", "Caption", "Model"])
if (name) names.add(name)
})
return Array.from(names)
}
function extractLabels(inventory: Record<string, unknown> | null): string[] {
if (!inventory) return []
const labels = inventory["labels"]
if (!labels) return []
if (Array.isArray(labels)) {
return labels
.map((label) => {
if (typeof label === "string") return label.trim()
const record = toRecord(label)
if (!record) return null
return pickString(record, ["name", "value", "label", "Name"])
})
.filter((item): item is string => Boolean(item))
}
const record = toRecord(labels)
const name = pickString(record, ["name", "label", "value"])
return name ? [name] : []
}
function summarizeLinkedUsers(users: LinkedUser[] | undefined): string | null {
if (!users || users.length === 0) return null
const parts = users.map((user) => {
const name = user.name ?? user.email ?? ""
if (user.email && user.email !== name) {
return `${name} <${user.email}>`
}
return name
})
return parts.join("; ")
}
function countSoftwareEntries(inventory: Record<string, unknown> | 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
return null
}
function extractSoftwareEntries(hostname: string, inventory: Record<string, unknown> | null): SoftwareEntry[] {
if (!inventory) return []
const entries: SoftwareEntry[] = []
const pushEntry = (record: Record<string, unknown> | 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 publisher = pickString(record, ["Publisher", "publisher", "Vendor", "vendor"])
const installed = pickString(record, ["InstalledOn", "installedOn", "InstallDate", "installDate", "InstallDateUTC"])
entries.push({
hostname,
name,
version,
source,
publisher,
installedOn: installed,
})
}
const direct = inventory["software"]
if (Array.isArray(direct)) {
direct.map(toRecord).forEach(pushEntry)
} else if (direct) {
pushEntry(toRecord(direct))
}
const extended = pickRecord(inventory, ["extended", "Extended"])
const windows = pickRecord(extended, ["windows", "Windows"])
if (windows) {
const software = windows["software"] ?? windows["installedPrograms"] ?? windows["InstalledPrograms"]
if (Array.isArray(software)) {
software.map(toRecord).forEach(pushEntry)
} else {
pushEntry(toRecord(software))
}
}
return entries
}
function extractDiskEntries(hostname: string, inventory: Record<string, unknown> | null): DiskEntry[] {
if (!inventory) return []
const entries: DiskEntry[] = []
const pushDisk = (record: Record<string, unknown> | null) => {
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 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,
})
}
const direct = inventory["disks"]
if (Array.isArray(direct)) {
direct.map(toRecord).forEach(pushDisk)
} else {
pushDisk(toRecord(direct))
}
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 linux = pickRecord(extended, ["linux", "Linux"])
if (linux) {
const lsblk = linux["lsblk"]
if (Array.isArray(lsblk)) {
lsblk.map(toRecord).forEach(pushDisk)
} else {
pushDisk(toRecord(lsblk))
}
}
return entries
}