From 38b46f32ceee81b2f2d4e0641e2460ca841eeef7 Mon Sep 17 00:00:00 2001 From: codex-bot Date: Thu, 30 Oct 2025 16:09:06 -0300 Subject: [PATCH] feat: improve machines inventory exports --- .../machines/[id]/inventory.xlsx/route.ts | 69 ++ .../reports/machines-inventory.xlsx/route.ts | 211 +----- .../machines/admin-machines-overview.tsx | 38 +- src/lib/xlsx.ts | 101 ++- src/server/machines/inventory-export.ts | 661 ++++++++++++++++++ 5 files changed, 858 insertions(+), 222 deletions(-) create mode 100644 src/app/api/admin/machines/[id]/inventory.xlsx/route.ts create mode 100644 src/server/machines/inventory-export.ts diff --git a/src/app/api/admin/machines/[id]/inventory.xlsx/route.ts b/src/app/api/admin/machines/[id]/inventory.xlsx/route.ts new file mode 100644 index 0000000..ccfbf27 --- /dev/null +++ b/src/app/api/admin/machines/[id]/inventory.xlsx/route.ts @@ -0,0 +1,69 @@ +import { NextResponse } from "next/server" +import { ConvexHttpClient } from "convex/browser" + +import { api } from "@/convex/_generated/api" +import type { Id } from "@/convex/_generated/dataModel" +import { env } from "@/lib/env" +import { assertAuthenticatedSession } from "@/lib/auth-server" +import { DEFAULT_TENANT_ID } from "@/lib/constants" +import { buildMachinesInventoryWorkbook, type MachineInventoryRecord } from "@/server/machines/inventory-export" + +export const runtime = "nodejs" + +type RouteContext = { + params: Promise<{ + id: string + }> +} + +function sanitizeFilename(hostname: string, fallback: string): string { + const safe = hostname.replace(/[^a-z0-9_-]/gi, "-").replace(/-{2,}/g, "-").toLowerCase() + return safe || fallback +} + +export async function GET(_request: Request, context: RouteContext) { + const session = await assertAuthenticatedSession() + if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 }) + + const convexUrl = env.NEXT_PUBLIC_CONVEX_URL + if (!convexUrl) { + return NextResponse.json({ error: "Convex não configurado" }, { status: 500 }) + } + + const { id } = await context.params + const machineId = id as Id<"machines"> + const client = new ConvexHttpClient(convexUrl) + const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID + + try { + const machine = (await client.query(api.machines.getById, { + id: machineId, + includeMetadata: true, + })) as MachineInventoryRecord | null + + if (!machine || machine.tenantId !== tenantId) { + return NextResponse.json({ error: "Máquina não encontrada" }, { status: 404 }) + } + + const workbook = buildMachinesInventoryWorkbook([machine], { + tenantId, + generatedBy: session.user.name ?? session.user.email, + companyFilterLabel: machine.companyName ?? machine.companySlug ?? null, + generatedAt: new Date(), + }) + + const hostnameSafe = sanitizeFilename(machine.hostname, "machine") + const body = new Uint8Array(workbook) + + return new NextResponse(body, { + headers: { + "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Content-Disposition": `attachment; filename="machine-inventory-${hostnameSafe}.xlsx"`, + "Cache-Control": "no-store", + }, + }) + } catch (error) { + console.error("Failed to export machine inventory", error) + return NextResponse.json({ error: "Falha ao gerar planilha da máquina" }, { status: 500 }) + } +} diff --git a/src/app/api/reports/machines-inventory.xlsx/route.ts b/src/app/api/reports/machines-inventory.xlsx/route.ts index 942dd35..5efa357 100644 --- a/src/app/api/reports/machines-inventory.xlsx/route.ts +++ b/src/app/api/reports/machines-inventory.xlsx/route.ts @@ -2,96 +2,13 @@ import { NextResponse } from "next/server" import { ConvexHttpClient } from "convex/browser" import { api } from "@/convex/_generated/api" -import type { Id } from "@/convex/_generated/dataModel" import { env } from "@/lib/env" import { assertAuthenticatedSession } from "@/lib/auth-server" import { DEFAULT_TENANT_ID } from "@/lib/constants" -import { buildXlsxWorkbook } from "@/lib/xlsx" +import { buildMachinesInventoryWorkbook, type MachineInventoryRecord } from "@/server/machines/inventory-export" export const runtime = "nodejs" -type MachineListEntry = { - 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 | null - linkedUsers?: Array<{ id: string; email: string; name: string }> -} - -function formatIso(value: number | null | undefined): string | null { - if (typeof value !== "number") return null - try { - return new Date(value).toISOString() - } catch { - return null - } -} - -function formatMemory(bytes: unknown): number | null { - if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes <= 0) return null - const gib = bytes / (1024 ** 3) - return Number(gib.toFixed(2)) -} - -function extractPrimaryIp(inventory: Record | null): string | null { - if (!inventory) return null - const network = inventory.network - if (!network) return null - if (Array.isArray(network)) { - for (const entry of network) { - if (entry && typeof entry === "object") { - const candidate = (entry as { ip?: unknown }).ip - if (typeof candidate === "string" && candidate.trim().length > 0) return candidate.trim() - } - } - } else if (typeof network === "object") { - const record = network as Record - const ip = - typeof record.primaryIp === "string" - ? record.primaryIp - : typeof record.publicIp === "string" - ? record.publicIp - : null - if (ip && ip.trim().length > 0) return ip.trim() - } - return null -} - -function extractHardware(inventory: Record | null) { - if (!inventory) return {} - const hardware = inventory.hardware - if (!hardware || typeof hardware !== "object") return {} - const hw = hardware as Record - return { - vendor: typeof hw.vendor === "string" ? hw.vendor : null, - model: typeof hw.model === "string" ? hw.model : null, - serial: typeof hw.serial === "string" ? hw.serial : null, - cpuType: typeof hw.cpuType === "string" ? hw.cpuType : null, - physicalCores: typeof hw.physicalCores === "number" ? hw.physicalCores : null, - logicalCores: typeof hw.logicalCores === "number" ? hw.logicalCores : null, - memoryBytes: typeof hw.memoryBytes === "number" ? hw.memoryBytes : null, - } -} - export async function GET(request: Request) { const session = await assertAuthenticatedSession() if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 }) @@ -130,126 +47,28 @@ export async function GET(request: Request) { const machines = (await client.query(api.machines.listByTenant, { tenantId, includeMetadata: true, - })) as MachineListEntry[] + })) as MachineInventoryRecord[] const filtered = machines.filter((machine) => { if (!companyId) return true return String(machine.companyId ?? "") === companyId || machine.companySlug === companyId }) + const companyFilterLabel = (() => { + if (!companyId) return null + const matchById = filtered.find((machine) => machine.companyId && String(machine.companyId) === companyId) + if (matchById?.companyName) return matchById.companyName + const matchBySlug = filtered.find((machine) => machine.companySlug === companyId) + if (matchBySlug?.companyName) return matchBySlug.companyName + return companyId + })() - const statusCounts = filtered.reduce>((acc, machine) => { - const key = machine.status ?? "unknown" - acc[key] = (acc[key] ?? 0) + 1 - return acc - }, {}) - - const summaryRows: Array> = [ - ["Tenant", tenantId], - ["Total de máquinas", filtered.length], - ] - if (companyId) summaryRows.push(["Filtro de empresa", companyId]) - Object.entries(statusCounts).forEach(([status, total]) => { - summaryRows.push([`Status: ${status}`, total]) + const workbook = buildMachinesInventoryWorkbook(filtered, { + tenantId, + generatedBy: session.user.name ?? session.user.email, + companyFilterLabel, + generatedAt: new Date(), }) - const inventorySheetRows = filtered.map((machine) => { - const inventory = - machine.inventory && typeof machine.inventory === "object" - ? (machine.inventory as Record) - : null - const hardware = extractHardware(inventory) - const primaryIp = extractPrimaryIp(inventory) - const memoryGiB = formatMemory(hardware.memoryBytes) - return [ - machine.hostname, - machine.companyName ?? "—", - machine.status ?? "unknown", - machine.isActive ? "Sim" : "Não", - formatIso(machine.lastHeartbeatAt), - machine.persona ?? null, - machine.assignedUserName ?? null, - machine.assignedUserEmail ?? null, - machine.authEmail ?? null, - machine.osName, - machine.osVersion ?? null, - machine.architecture ?? null, - machine.macAddresses.join(", "), - machine.serialNumbers.join(", "), - machine.registeredBy ?? null, - formatIso(machine.createdAt), - formatIso(machine.updatedAt), - hardware.vendor, - hardware.model, - hardware.serial, - hardware.cpuType, - hardware.physicalCores, - hardware.logicalCores, - memoryGiB, - primaryIp, - machine.token?.expiresAt ? formatIso(machine.token.expiresAt) : null, - machine.token?.usageCount ?? null, - ] - }) - - const linksSheetRows: Array> = [] - filtered.forEach((machine) => { - if (!machine.linkedUsers || machine.linkedUsers.length === 0) return - machine.linkedUsers.forEach((user) => { - linksSheetRows.push([ - machine.hostname, - machine.companyName ?? "—", - user.name ?? user.email ?? "—", - user.email ?? "—", - ]) - }) - }) - - const workbook = buildXlsxWorkbook([ - { - name: "Resumo", - headers: ["Item", "Valor"], - rows: summaryRows, - }, - { - name: "Máquinas", - headers: [ - "Hostname", - "Empresa", - "Status", - "Ativa", - "Último heartbeat", - "Persona", - "Responsável", - "E-mail responsável", - "E-mail autenticado", - "Sistema operacional", - "Versão SO", - "Arquitetura", - "Endereços MAC", - "Seriais", - "Registrada via", - "Criada em", - "Atualizada em", - "Fabricante", - "Modelo", - "Serial hardware", - "Processador", - "Cores físicas", - "Cores lógicas", - "Memória (GiB)", - "IP principal", - "Token expira em", - "Uso do token", - ], - rows: inventorySheetRows.length > 0 ? inventorySheetRows : [["—"]], - }, - { - name: "Vínculos", - headers: ["Hostname", "Empresa", "Usuário", "E-mail"], - rows: linksSheetRows.length > 0 ? linksSheetRows : [["—", "—", "—", "—"]], - }, - ]) - const body = new Uint8Array(workbook) return new NextResponse(body, { diff --git a/src/components/admin/machines/admin-machines-overview.tsx b/src/components/admin/machines/admin-machines-overview.tsx index 49c7936..71773c0 100644 --- a/src/components/admin/machines/admin-machines-overview.tsx +++ b/src/components/admin/machines/admin-machines-overview.tsx @@ -2004,7 +2004,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) { } return JSON.stringify(payload, null, 2) }, [machine, metrics, metadata]) - const handleDownloadInventory = useCallback(() => { + const handleDownloadInventoryJson = useCallback(() => { if (!machine) return const safeHostname = machine.hostname.replace(/[^a-z0-9_-]/gi, "-").replace(/-{2,}/g, "-").toLowerCase() const fileName = `${safeHostname || "machine"}_${machine.id}.json` @@ -3915,7 +3915,14 @@ export function MachineDetails({ machine }: MachineDetailsProps) { ) : null} -
+
+ {machine ? ( + + ) : null} @@ -3932,15 +3939,24 @@ export function MachineDetails({ machine }: MachineDetailsProps) { onChange={(e) => setDialogQuery(e.target.value)} className="sm:flex-1" /> - +
+ + {machine ? ( + + ) : null} +
+  freezePane?: {
+    rowSplit?: number
+    columnSplit?: number
+  }
+  autoFilter?: boolean
 }
 
 type ZipEntry = {
@@ -38,22 +44,23 @@ function columnRef(index: number): string {
   return col
 }
 
-function formatCell(value: unknown, colIndex: number, rowIndex: number): string {
-  const ref = `${columnRef(colIndex)}${rowIndex + 1}`
+function formatCell(value: unknown, colIndex: number, rowNumber: number, styleIndex?: number): string {
+  const ref = `${columnRef(colIndex)}${rowNumber}`
+  const styleAttr = styleIndex !== undefined ? ` s="${styleIndex}"` : ""
   if (value === null || value === undefined || value === "") {
-    return ``
+    return ``
   }
 
   if (value instanceof Date) {
-    return `${escapeXml(value.toISOString())}`
+    return `${escapeXml(value.toISOString())}`
   }
 
   if (typeof value === "number" && Number.isFinite(value)) {
-    return `${value}`
+    return `${value}`
   }
 
   if (typeof value === "boolean") {
-    return `${value ? 1 : 0}`
+    return `${value ? 1 : 0}`
   }
 
   let text: string
@@ -62,25 +69,75 @@ function formatCell(value: unknown, colIndex: number, rowIndex: number): string
   } else {
     text = JSON.stringify(value)
   }
-  return `${escapeXml(text)}`
+  return `${escapeXml(text)}`
 }
 
-function buildWorksheetXml(config: WorksheetConfig): string {
+type WorksheetStyles = {
+  header: number
+  body: number
+}
+
+function buildWorksheetXml(config: WorksheetConfig, styles: WorksheetStyles): string {
+  const totalRows = config.rows.length + 1
   const rows: string[] = []
-  const headerRow = config.headers.map((header, idx) => formatCell(header, idx, 0)).join("")
+  const headerRow = config.headers.map((header, idx) => formatCell(header, idx, 1, styles.header)).join("")
   rows.push(`${headerRow}`)
 
   config.rows.forEach((rowData, rowIdx) => {
-    const cells = config.headers.map((_, colIdx) => formatCell(rowData[colIdx], colIdx, rowIdx + 1)).join("")
-    rows.push(`${cells}`)
+    const actualRow = rowIdx + 2
+    const cells = config.headers
+      .map((_, colIdx) => formatCell(rowData[colIdx], colIdx, actualRow, styles.body))
+      .join("")
+    rows.push(`${cells}`)
   })
 
+  const hasCustomWidths = Array.isArray(config.columnWidths) && config.columnWidths.some((width) => typeof width === "number")
+  const colsXml = hasCustomWidths
+    ? `${config.headers
+        .map((_, idx) => {
+          const width = config.columnWidths?.[idx]
+          if (typeof width !== "number" || Number.isNaN(width)) {
+            return ``
+          }
+          return ``
+        })
+        .join("")}`
+    : ""
+
+  let sheetViews = ""
+  const rowSplit = config.freezePane?.rowSplit ?? 0
+  const columnSplit = config.freezePane?.columnSplit ?? 0
+  if (rowSplit > 0 || columnSplit > 0) {
+    const attributes: string[] = []
+    if (columnSplit > 0) attributes.push(`xSplit="${columnSplit}"`)
+    if (rowSplit > 0) attributes.push(`ySplit="${rowSplit}"`)
+    const topLeftColumn = columnSplit > 0 ? columnRef(columnSplit) : "A"
+    const topLeftRow = rowSplit > 0 ? rowSplit + 1 : 1
+    const activePane =
+      rowSplit > 0 && columnSplit > 0
+        ? "bottomRight"
+        : rowSplit > 0
+          ? "bottomLeft"
+          : "topRight"
+    const pane = ``
+    sheetViews = `${pane}`
+  }
+
+  const autoFilter =
+    config.autoFilter && config.headers.length > 0 && totalRows > 1
+      ? ``
+      : ""
+
   return [
     XML_DECLARATION,
     '',
+    sheetViews,
+    colsXml,
+    '  ',
     "  ",
     rows.map((row) => `    ${row}`).join("\n"),
     "  ",
+    autoFilter,
     "",
   ].join("\n")
 }
@@ -206,6 +263,10 @@ export function buildXlsxWorkbook(sheets: WorksheetConfig[]): Buffer {
     throw new Error("Workbook requires at least one sheet")
   }
 
+  const styles: WorksheetStyles = {
+    body: 0,
+    header: 1,
+  }
   const now = new Date()
   const timestamp = now.toISOString()
   const workbookRels: string[] = []
@@ -214,7 +275,7 @@ export function buildXlsxWorkbook(sheets: WorksheetConfig[]): Buffer {
   const sheetRefs = sheets.map((sheet, index) => {
     const sheetId = index + 1
     const relId = `rId${sheetId}`
-    const worksheetXml = buildWorksheetXml(sheet)
+    const worksheetXml = buildWorksheetXml(sheet, styles)
     sheetEntries.push({
       path: `xl/worksheets/sheet${sheetId}.xml`,
       data: Buffer.from(worksheetXml, "utf8"),
@@ -248,11 +309,21 @@ export function buildXlsxWorkbook(sheets: WorksheetConfig[]): Buffer {
   const stylesXml = [
     XML_DECLARATION,
     '',
-    '  ',
-    '  ',
+    '  ',
+    '    ',
+    '    ',
+    "  ",
+    '  ',
+    '    ',
+    '    ',
+    '    ',
+    "  ",
     '  ',
     '  ',
-    '  ',
+    '  ',
+    '    ',
+    '    ',
+    "  ",
     '  ',
     "",
   ].join("\n")
diff --git a/src/server/machines/inventory-export.ts b/src/server/machines/inventory-export.ts
new file mode 100644
index 0000000..ec29331
--- /dev/null
+++ b/src/server/machines/inventory-export.ts
@@ -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 | 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 = {
+  online: "Online",
+  offline: "Offline",
+  stale: "Sem sinal",
+  maintenance: "Manutenção",
+  blocked: "Bloqueada",
+  deactivated: "Desativada",
+  unknown: "Desconhecido",
+}
+
+const PERSONA_LABELS: Record = {
+  collaborator: "Colaborador",
+  manager: "Gestor",
+  machine: "Máquina",
+}
+
+const SUMMARY_STATUS_ORDER = ["Online", "Sem sinal", "Offline", "Manutenção", "Bloqueada", "Desativada", "Desconhecido"]
+
+type WorksheetRow = Array
+
+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()
+  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 | null {
+  if (value && typeof value === "object" && !Array.isArray(value)) {
+    return value as Record
+  }
+  return null
+}
+
+function toRecordArray(value: unknown): Record[] {
+  if (Array.isArray(value)) {
+    return value.map(toRecord).filter((item): item is Record => 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 | 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 | null | undefined, keys: string[]): string | null {
+  return ensureString(pickValue(record, keys))
+}
+
+function pickNumber(record: Record | null | undefined, keys: string[]): number | null {
+  return ensureNumber(pickValue(record, keys))
+}
+
+function pickRecord(record: Record | null | undefined, keys: string[]): Record | null {
+  return toRecord(pickValue(record, keys))
+}
+
+function pickRecordArray(record: Record | null | undefined, keys: string[]): Record[] {
+  return toRecordArray(pickValue(record, keys))
+}
+
+function pickArray(record: Record | 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 | 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 | 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 | null): string | null {
+  if (!inventory) return null
+  const network = pickRecord(inventory, ["network", "Network"])
+  return pickString(network, ["publicIp", "PublicIp", "externalIp", "ExternalIp"])
+}
+
+function extractGpuNames(inventory: Record | null): string[] {
+  if (!inventory) return []
+  const names = new Set()
+  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 | 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 | 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 | null): SoftwareEntry[] {
+  if (!inventory) return []
+  const entries: SoftwareEntry[] = []
+
+  const pushEntry = (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 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 | null): DiskEntry[] {
+  if (!inventory) return []
+  const entries: DiskEntry[] = []
+
+  const pushDisk = (record: Record | 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
+}