- Reestrutura visual do widget de chat (header branco, status emerald) - Adiciona sistema de anexos com upload e drag-and-drop - Substitui select nativo por componente Select do shadcn - Adiciona eventos LIVE_CHAT_STARTED e LIVE_CHAT_ENDED na timeline - Traduz labels de chat para portugues (Chat iniciado/finalizado) - Filtra CHAT_MESSAGE_ADDED da timeline (apenas inicio/fim aparecem) - Restringe inicio de chat a tickets com responsavel atribuido - Exibe duracao da sessao ao finalizar chat 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2109 lines
71 KiB
TypeScript
2109 lines
71 KiB
TypeScript
import type { Id } from "@/convex/_generated/dataModel"
|
|
import { DEVICE_INVENTORY_COLUMN_METADATA, type DeviceInventoryColumnConfig } from "@/lib/device-inventory-columns"
|
|
import { buildXlsxWorkbook, type WorksheetConfig } from "@/lib/xlsx"
|
|
|
|
type DeviceCustomField = {
|
|
fieldId: Id<"deviceFields">
|
|
fieldKey: string
|
|
label: string
|
|
type: string
|
|
value: unknown
|
|
displayValue?: string
|
|
}
|
|
|
|
type LinkedUser = {
|
|
id: string
|
|
email: string | null
|
|
name: string | null
|
|
}
|
|
|
|
export type MachineInventoryRecord = {
|
|
id: Id<"machines">
|
|
tenantId: string
|
|
hostname: string
|
|
displayName: string | null
|
|
deviceType: string | null
|
|
devicePlatform: string | null
|
|
deviceProfile?: Record<string, unknown> | null
|
|
managementMode: string | null
|
|
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
|
|
metrics?: Record<string, unknown> | null
|
|
postureAlerts?: Array<Record<string, unknown>> | null
|
|
lastPostureAt?: number | null
|
|
remoteAccess?: unknown
|
|
linkedUsers?: LinkedUser[]
|
|
customFields?: DeviceCustomField[]
|
|
usbPolicy?: string | null
|
|
usbPolicyStatus?: string | null
|
|
ticketCount?: number | null
|
|
}
|
|
|
|
type WorkbookOptions = {
|
|
tenantId: string
|
|
generatedBy?: string | null
|
|
companyFilterLabel?: string | null
|
|
generatedAt?: Date
|
|
columns?: DeviceInventoryColumnConfig[]
|
|
}
|
|
|
|
type SoftwareEntry = {
|
|
hostname: string
|
|
name: string
|
|
version: string | null
|
|
source: string | null
|
|
publisher: string | null
|
|
installedOn: string | null
|
|
}
|
|
|
|
type InventoryColumnDefinition = {
|
|
key: string
|
|
label: string
|
|
width: number
|
|
getValue: (machine: MachineInventoryRecord, derived: MachineDerivedData) => unknown
|
|
}
|
|
|
|
type MachineDerivedData = {
|
|
inventory: Record<string, unknown>
|
|
hardware: ReturnType<typeof extractHardware>
|
|
gpuNames: string[]
|
|
labels: string[]
|
|
primaryIp: string | null
|
|
publicIp: string | null
|
|
softwareEntries: SoftwareEntry[]
|
|
linkedUsersLabel: string | null
|
|
systemInfo: ReturnType<typeof extractSystemInfo>
|
|
collaborator: ReturnType<typeof extractCollaborator>
|
|
remoteAccessCount: number
|
|
fleetInfo: ReturnType<typeof extractFleetInfo>
|
|
customFieldByKey: Record<string, DeviceCustomField>
|
|
customFieldById: Record<string, DeviceCustomField>
|
|
}
|
|
|
|
const COLUMN_VALUE_RESOLVERS: Record<string, (machine: MachineInventoryRecord, derived: MachineDerivedData) => unknown> = {
|
|
displayName: (machine) => machine.displayName ?? machine.hostname,
|
|
hostname: (machine) => machine.hostname,
|
|
deviceType: (machine) => describeDeviceType(machine.deviceType),
|
|
devicePlatform: (machine) => machine.devicePlatform ?? null,
|
|
company: (machine) => machine.companyName ?? null,
|
|
status: (machine) => describeStatus(machine.status),
|
|
persona: (machine) => describePersona(machine.persona),
|
|
active: (machine) => yesNo(machine.isActive),
|
|
lastHeartbeat: (machine) => formatDateTime(machine.lastHeartbeatAt),
|
|
assignedUser: (machine) => machine.assignedUserName ?? machine.assignedUserEmail ?? null,
|
|
assignedEmail: (machine) => machine.assignedUserEmail ?? null,
|
|
linkedUsers: (_machine, derived) => derived.linkedUsersLabel,
|
|
authEmail: (machine) => machine.authEmail ?? null,
|
|
osName: (machine) => machine.osName,
|
|
osVersion: (machine) => machine.osVersion ?? null,
|
|
architecture: (machine) => machine.architecture ?? null,
|
|
hardwareVendor: (_machine, derived) => derived.hardware.vendor ?? derived.systemInfo.systemManufacturer ?? null,
|
|
hardwareModel: (_machine, derived) => derived.hardware.model ?? derived.systemInfo.systemModel ?? null,
|
|
hardwareSerial: (_machine, derived) => derived.hardware.serial ?? derived.systemInfo.boardSerial ?? null,
|
|
cpu: (_machine, derived) => derived.hardware.cpuType ?? null,
|
|
physicalCores: (_machine, derived) => derived.hardware.physicalCores ?? null,
|
|
logicalCores: (_machine, derived) => derived.hardware.logicalCores ?? null,
|
|
memoryGiB: (_machine, derived) => derived.hardware.memoryGiB ?? null,
|
|
gpus: (_machine, derived) => (derived.gpuNames.length > 0 ? derived.gpuNames.join(", ") : null),
|
|
labels: (_machine, derived) => (derived.labels.length > 0 ? derived.labels.join(", ") : null),
|
|
macs: (machine) => (machine.macAddresses.length > 0 ? machine.macAddresses.join(", ") : null),
|
|
serials: (machine) => (machine.serialNumbers.length > 0 ? machine.serialNumbers.join(", ") : null),
|
|
primaryIp: (_machine, derived) => derived.primaryIp ?? null,
|
|
publicIp: (_machine, derived) => derived.publicIp ?? null,
|
|
registeredBy: (machine) => machine.registeredBy ?? null,
|
|
tokenExpiresAt: (machine) => (machine.token?.expiresAt ? formatDateTime(machine.token.expiresAt) : null),
|
|
tokenLastUsedAt: (machine) => (machine.token?.lastUsedAt ? formatDateTime(machine.token.lastUsedAt) : null),
|
|
tokenUsageCount: (machine) => machine.token?.usageCount ?? 0,
|
|
createdAt: (machine) => formatDateTime(machine.createdAt),
|
|
updatedAt: (machine) => formatDateTime(machine.updatedAt),
|
|
softwareCount: (_machine, derived) => derived.softwareEntries.length,
|
|
osBuild: (_machine, derived) => derived.systemInfo.osBuild ?? null,
|
|
osLicense: (_machine, derived) => derived.systemInfo.license ?? null,
|
|
osExperience: (_machine, derived) => derived.systemInfo.experience ?? null,
|
|
domain: (_machine, derived) => derived.systemInfo.domain ?? null,
|
|
workgroup: (_machine, derived) => derived.systemInfo.workgroup ?? null,
|
|
deviceName: (_machine, derived) => derived.systemInfo.deviceName ?? null,
|
|
boardSerial: (_machine, derived) => derived.systemInfo.boardSerial ?? derived.hardware.serial ?? null,
|
|
collaboratorName: (_machine, derived) => derived.collaborator?.name ?? null,
|
|
collaboratorEmail: (_machine, derived) => derived.collaborator?.email ?? null,
|
|
remoteAccessCount: (_machine, derived) => derived.remoteAccessCount,
|
|
fleetId: (_machine, derived) => derived.fleetInfo?.id ?? null,
|
|
fleetTeam: (_machine, derived) => derived.fleetInfo?.team ?? null,
|
|
fleetUpdatedAt: (_machine, derived) =>
|
|
derived.fleetInfo?.updatedAt ? formatDateTime(derived.fleetInfo.updatedAt) : null,
|
|
managementMode: (machine) => describeManagementMode(machine.managementMode),
|
|
usbPolicy: (machine) => describeUsbPolicy(machine.usbPolicy),
|
|
usbPolicyStatus: (machine) => describeUsbPolicyStatus(machine.usbPolicyStatus),
|
|
ticketCount: (machine) => machine.ticketCount ?? 0,
|
|
}
|
|
|
|
const DEFAULT_COLUMN_CONFIG: DeviceInventoryColumnConfig[] = DEVICE_INVENTORY_COLUMN_METADATA.filter((meta) => meta.default !== false).map(
|
|
(meta) => ({ key: meta.key })
|
|
)
|
|
|
|
const INVENTORY_COLUMN_DEFINITIONS: InventoryColumnDefinition[] = DEVICE_INVENTORY_COLUMN_METADATA.map((meta) => ({
|
|
key: meta.key,
|
|
label: meta.label,
|
|
width: meta.width,
|
|
getValue: COLUMN_VALUE_RESOLVERS[meta.key] ?? (() => null),
|
|
}))
|
|
|
|
const INVENTORY_COLUMN_MAP: Record<string, InventoryColumnDefinition> = Object.fromEntries(
|
|
INVENTORY_COLUMN_DEFINITIONS.map((column) => [column.key, column]),
|
|
)
|
|
|
|
const CUSTOM_FIELD_KEY_PREFIX = "custom:"
|
|
const CUSTOM_FIELD_ID_PREFIX = "custom#"
|
|
|
|
function deriveMachineData(machine: MachineInventoryRecord): MachineDerivedData {
|
|
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 softwareEntries = extractSoftwareEntries(machine.hostname, inventory)
|
|
const linkedUsersLabel = summarizeLinkedUsers(machine.linkedUsers)
|
|
const systemInfo = extractSystemInfo(inventory)
|
|
const collaborator = extractCollaborator(machine, inventory)
|
|
const remoteAccessCount = collectRemoteAccessEntries(machine).length
|
|
const fleetInfo = extractFleetInfo(inventory)
|
|
const customFieldByKey: Record<string, DeviceCustomField> = {}
|
|
const customFieldById: Record<string, DeviceCustomField> = {}
|
|
for (const field of machine.customFields ?? []) {
|
|
customFieldByKey[field.fieldKey] = field
|
|
customFieldById[String(field.fieldId)] = field
|
|
}
|
|
return {
|
|
inventory,
|
|
hardware,
|
|
gpuNames,
|
|
labels,
|
|
primaryIp,
|
|
publicIp,
|
|
softwareEntries,
|
|
linkedUsersLabel,
|
|
systemInfo,
|
|
collaborator,
|
|
remoteAccessCount,
|
|
fleetInfo,
|
|
customFieldByKey,
|
|
customFieldById,
|
|
}
|
|
}
|
|
|
|
export function normalizeInventoryColumnConfig(columns?: DeviceInventoryColumnConfig[]): DeviceInventoryColumnConfig[] {
|
|
if (!columns || columns.length === 0) {
|
|
return [...DEFAULT_COLUMN_CONFIG]
|
|
}
|
|
const seen = new Set<string>()
|
|
const normalized: DeviceInventoryColumnConfig[] = []
|
|
for (const column of columns) {
|
|
const key = column.key.trim()
|
|
if (!key) continue
|
|
if (seen.has(key)) continue
|
|
if (!isCustomFieldKey(key) && !INVENTORY_COLUMN_MAP[key]) continue
|
|
seen.add(key)
|
|
normalized.push({
|
|
key,
|
|
label: column.label?.trim() || undefined,
|
|
})
|
|
}
|
|
return normalized.length > 0 ? normalized : [...DEFAULT_COLUMN_CONFIG]
|
|
}
|
|
|
|
function resolveColumnLabel(column: DeviceInventoryColumnConfig, derivedList: MachineDerivedData[]): string {
|
|
if (column.label) return column.label
|
|
const definition = INVENTORY_COLUMN_MAP[column.key]
|
|
if (definition) return definition.label
|
|
if (isCustomFieldKey(column.key)) {
|
|
for (const derived of derivedList) {
|
|
const field = lookupCustomField(column.key, derived)
|
|
if (field) return field.label
|
|
}
|
|
return "Campo personalizado"
|
|
}
|
|
return column.key
|
|
}
|
|
|
|
function resolveColumnWidth(key: string): number {
|
|
const definition = INVENTORY_COLUMN_MAP[key]
|
|
if (definition) return definition.width
|
|
if (isCustomFieldKey(key)) return 28
|
|
return 20
|
|
}
|
|
|
|
function isCustomFieldKey(key: string): boolean {
|
|
return key.startsWith(CUSTOM_FIELD_KEY_PREFIX) || key.startsWith(CUSTOM_FIELD_ID_PREFIX)
|
|
}
|
|
|
|
function lookupCustomField(key: string, derived: MachineDerivedData): DeviceCustomField | null {
|
|
if (key.startsWith(CUSTOM_FIELD_KEY_PREFIX)) {
|
|
const fieldKey = key.slice(CUSTOM_FIELD_KEY_PREFIX.length)
|
|
return derived.customFieldByKey[fieldKey] ?? null
|
|
}
|
|
if (key.startsWith(CUSTOM_FIELD_ID_PREFIX)) {
|
|
const fieldId = key.slice(CUSTOM_FIELD_ID_PREFIX.length)
|
|
return derived.customFieldById[fieldId] ?? null
|
|
}
|
|
return null
|
|
}
|
|
|
|
function formatCustomFieldValue(field: DeviceCustomField): string {
|
|
if (field.value === null || field.value === undefined) {
|
|
return "—"
|
|
}
|
|
if (field.displayValue !== undefined && field.displayValue !== null) {
|
|
const text = String(field.displayValue).trim()
|
|
if (text.length > 0) return text
|
|
}
|
|
switch (field.type) {
|
|
case "boolean":
|
|
return yesNo(Boolean(field.value))
|
|
case "number": {
|
|
const num = typeof field.value === "number" ? field.value : Number(field.value)
|
|
return Number.isFinite(num) ? String(num) : String(field.value)
|
|
}
|
|
case "date": {
|
|
const date = new Date(field.value as string | number)
|
|
if (Number.isNaN(date.getTime())) return String(field.value)
|
|
return date.toISOString().slice(0, 10)
|
|
}
|
|
default:
|
|
return String(field.value)
|
|
}
|
|
}
|
|
|
|
function resolveColumnValue(
|
|
key: string,
|
|
machine: MachineInventoryRecord,
|
|
derived: MachineDerivedData
|
|
): unknown {
|
|
if (isCustomFieldKey(key)) {
|
|
const field = lookupCustomField(key, derived)
|
|
if (!field) return "—"
|
|
return formatCustomFieldValue(field)
|
|
}
|
|
const definition = INVENTORY_COLUMN_MAP[key]
|
|
if (!definition) return "—"
|
|
return definition.getValue(machine, derived)
|
|
}
|
|
|
|
function formatInventoryCell(value: unknown): unknown {
|
|
if (value === null || value === undefined) return "—"
|
|
if (typeof value === "string") {
|
|
const trimmed = value.trim()
|
|
return trimmed.length > 0 ? trimmed : "—"
|
|
}
|
|
return value
|
|
}
|
|
|
|
function describeDeviceType(type: string | null | undefined): string {
|
|
const normalized = (type ?? "").toLowerCase()
|
|
switch (normalized) {
|
|
case "desktop":
|
|
return "Desktop"
|
|
case "mobile":
|
|
return "Celular"
|
|
case "tablet":
|
|
return "Tablet"
|
|
default:
|
|
return "Desconhecido"
|
|
}
|
|
}
|
|
|
|
function describeManagementMode(mode: string | null | undefined): string {
|
|
const normalized = (mode ?? "").toLowerCase()
|
|
switch (normalized) {
|
|
case "agent":
|
|
return "Agente"
|
|
case "manual":
|
|
return "Manual"
|
|
default:
|
|
return "—"
|
|
}
|
|
}
|
|
|
|
function describeUsbPolicy(policy: string | null | undefined): string {
|
|
if (!policy) return "—"
|
|
const normalized = policy.toUpperCase()
|
|
switch (normalized) {
|
|
case "ALLOW":
|
|
return "Permitido"
|
|
case "BLOCK_ALL":
|
|
return "Bloqueado"
|
|
case "READONLY":
|
|
return "Somente leitura"
|
|
default:
|
|
return policy
|
|
}
|
|
}
|
|
|
|
function describeUsbPolicyStatus(status: string | null | undefined): string {
|
|
if (!status) return "—"
|
|
const normalized = status.toUpperCase()
|
|
switch (normalized) {
|
|
case "PENDING":
|
|
return "Pendente"
|
|
case "APPLYING":
|
|
return "Aplicando"
|
|
case "APPLIED":
|
|
return "Aplicado"
|
|
case "FAILED":
|
|
return "Falhou"
|
|
default:
|
|
return status
|
|
}
|
|
}
|
|
|
|
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 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",
|
|
"Usuário",
|
|
"Senha",
|
|
"URL",
|
|
"Notas",
|
|
"Última verificação",
|
|
"Origem",
|
|
"Metadados",
|
|
] as const
|
|
const REMOTE_ACCESS_COLUMN_WIDTHS = [22, 22, 24, 24, 20, 32, 28, 22, 16, 36] 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<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: "Dispositivo",
|
|
}
|
|
|
|
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 inventorySheet = buildInventoryWorksheet(machines, options.columns)
|
|
const linksRows = buildLinkedUsersRows(machines)
|
|
const softwareRows = buildSoftwareRows(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[] = []
|
|
|
|
sheets.push({
|
|
name: "Resumo",
|
|
headers: ["Item", "Valor"],
|
|
rows: summaryRows,
|
|
columnWidths: [28, 48],
|
|
})
|
|
|
|
sheets.push(inventorySheet)
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
if (networkRows.length > 0) {
|
|
sheets.push({
|
|
name: "Rede",
|
|
headers: [...NETWORK_HEADERS],
|
|
rows: networkRows,
|
|
columnWidths: [...NETWORK_COLUMN_WIDTHS],
|
|
freezePane: { rowSplit: 1 },
|
|
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: 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)
|
|
}
|
|
|
|
export function buildInventoryWorksheet(
|
|
machines: MachineInventoryRecord[],
|
|
columns?: DeviceInventoryColumnConfig[],
|
|
sheetName = "Inventário",
|
|
): WorksheetConfig {
|
|
const columnConfig = normalizeInventoryColumnConfig(columns)
|
|
const derivedList = machines.map((machine) => deriveMachineData(machine))
|
|
const headers = columnConfig.map((column) => resolveColumnLabel(column, derivedList))
|
|
const columnWidths = columnConfig.map((column) => resolveColumnWidth(column.key))
|
|
const inventoryRows = machines.map((machine, index) => {
|
|
const derived = derivedList[index]
|
|
return columnConfig.map((column) => formatInventoryCell(resolveColumnValue(column.key, machine, derived)))
|
|
})
|
|
const fallbackRows =
|
|
inventoryRows.length > 0 ? inventoryRows : [columnConfig.map(() => "—")]
|
|
|
|
return {
|
|
name: sheetName,
|
|
headers,
|
|
rows: fallbackRows,
|
|
columnWidths,
|
|
freezePane: { rowSplit: 1 },
|
|
autoFilter: inventoryRows.length > 0,
|
|
}
|
|
}
|
|
|
|
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 dispositivos", machines.length])
|
|
|
|
const activeCount = machines.filter((machine) => machine.isActive).length
|
|
rows.push(["Dispositivos ativos", activeCount])
|
|
rows.push(["Dispositivos inativos", machines.length - activeCount])
|
|
|
|
const typeCounts = new Map<string, number>()
|
|
machines.forEach((machine) => {
|
|
const label = describeDeviceType(machine.deviceType)
|
|
typeCounts.set(label, (typeCounts.get(label) ?? 0) + 1)
|
|
})
|
|
const DEVICE_TYPE_ORDER = ["Desktop", "Celular", "Tablet", "Desconhecido"]
|
|
Array.from(typeCounts.entries())
|
|
.sort((a, b) => {
|
|
const idxA = DEVICE_TYPE_ORDER.indexOf(a[0])
|
|
const idxB = DEVICE_TYPE_ORDER.indexOf(b[0])
|
|
if (idxA === -1 && idxB === -1) return a[0].localeCompare(b[0], "pt-BR")
|
|
if (idxA === -1) return 1
|
|
if (idxB === -1) return -1
|
|
return idxA - idxB
|
|
})
|
|
.forEach(([label, total]) => rows.push([`Tipo: ${label}`, total]))
|
|
|
|
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(", ")])
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 = new Map<string, WorksheetRow>()
|
|
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([
|
|
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 buildPhysicalDiskRows(machines: MachineInventoryRecord[]): WorksheetRow[] {
|
|
const rows: WorksheetRow[] = []
|
|
machines.forEach((machine) => {
|
|
const inventory = toRecord(machine.inventory)
|
|
const disks = extractPhysicalDiskEntries(machine.hostname, inventory)
|
|
disks.forEach((disk) => {
|
|
rows.push([
|
|
disk.hostname,
|
|
disk.model ?? disk.serial ?? "—",
|
|
formatBytesValue(disk.sizeBytes),
|
|
disk.interface ?? "—",
|
|
disk.mediaType ?? "—",
|
|
disk.serial ?? "—",
|
|
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.username ?? "—",
|
|
entry.password ?? "—",
|
|
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<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 extractHardware(inventory: Record<string, unknown> | null) {
|
|
if (!inventory) {
|
|
return {
|
|
vendor: null,
|
|
model: null,
|
|
serial: null,
|
|
cpuType: null,
|
|
physicalCores: null,
|
|
logicalCores: null,
|
|
memoryGiB: null,
|
|
memoryBytes: null,
|
|
}
|
|
}
|
|
const hardware = pickRecord(inventory, ["hardware", "Hardware"])
|
|
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 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,
|
|
serial,
|
|
cpuType,
|
|
physicalCores,
|
|
logicalCores,
|
|
memoryGiB,
|
|
memoryBytes,
|
|
}
|
|
}
|
|
|
|
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 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<string, number> = {
|
|
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 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]}`
|
|
}
|
|
|
|
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<string, unknown> | 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
|
|
username: string | null
|
|
password: string | null
|
|
url: string | null
|
|
notes: string | null
|
|
lastVerifiedAt: number | null
|
|
origin: string
|
|
metadata: Record<string, unknown> | 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,
|
|
username: null,
|
|
password: null,
|
|
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 username =
|
|
ensureString(record["username"]) ??
|
|
ensureString(record["user"]) ??
|
|
ensureString(record["login"]) ??
|
|
ensureString(record["email"]) ??
|
|
ensureString(record["account"]) ??
|
|
null
|
|
const password =
|
|
ensureString(record["password"]) ??
|
|
ensureString(record["pass"]) ??
|
|
ensureString(record["secret"]) ??
|
|
ensureString(record["pin"]) ??
|
|
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,
|
|
username,
|
|
password,
|
|
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<string, unknown> | 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<string, unknown> | 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<string, unknown> | 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<string, unknown> | null): SoftwareEntryInternal[] {
|
|
if (!inventory) return []
|
|
const map = new Map<string, SoftwareEntryInternal>()
|
|
|
|
const push = (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", "PublisherURL"]) ?? null
|
|
const publisher = pickString(record, ["Publisher", "publisher", "Vendor", "vendor"])
|
|
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 ?? null,
|
|
source,
|
|
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(push)
|
|
} else if (direct) {
|
|
push(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(push)
|
|
} else {
|
|
push(toRecord(software))
|
|
}
|
|
}
|
|
|
|
const linux = pickRecord(extended, ["linux", "Linux"])
|
|
if (linux) {
|
|
const packages = linux["packages"]
|
|
if (Array.isArray(packages)) {
|
|
packages.forEach((pkg) => {
|
|
if (typeof pkg === "string") {
|
|
push({ name: pkg } as Record<string, unknown>)
|
|
} 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")
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
function extractPartitionEntries(hostname: string, inventory: Record<string, unknown> | null): PartitionEntry[] {
|
|
const entries: PartitionEntry[] = []
|
|
if (!inventory) return entries
|
|
|
|
const push = (value: unknown, origin: string) => {
|
|
const record = toRecord(value)
|
|
if (!record) return
|
|
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"])
|
|
entries.push({
|
|
hostname,
|
|
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.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 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")
|
|
}
|
|
}
|
|
|
|
const linux = pickRecord(extended, ["linux", "Linux"])
|
|
if (linux) {
|
|
const lsblk = linux["lsblk"]
|
|
if (Array.isArray(lsblk)) {
|
|
lsblk.forEach((item) => push(item, "extended.linux.lsblk"))
|
|
} else if (lsblk) {
|
|
push(lsblk, "extended.linux.lsblk")
|
|
}
|
|
}
|
|
|
|
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<string, PartitionEntry>()
|
|
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<string, unknown> | 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<string, PhysicalDiskEntry>()
|
|
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<string, unknown> | 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<string, unknown>)["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<string, NetworkEntry>()
|
|
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<string, unknown> | 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<string, ServiceEntry>()
|
|
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<string, unknown> | 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)}%`,
|
|
]
|
|
}
|