Expose detailed Windows OS info in machine inventory

This commit is contained in:
Esdras Renan 2025-10-13 15:45:24 -03:00
parent 3d89c5fd32
commit 64e4e02a9a

View file

@ -116,17 +116,6 @@ type WindowsDiskEntry = {
MediaType?: string MediaType?: string
} }
type WindowsOsInfo = {
ProductName?: string
CurrentBuild?: string | number
CurrentBuildNumber?: string | number
DisplayVersion?: string
ReleaseId?: string
EditionID?: string
LicenseStatus?: number
IsActivated?: boolean
}
type WindowsExtended = { type WindowsExtended = {
software?: MachineSoftware[] software?: MachineSoftware[]
services?: Array<{ name?: string; status?: string; displayName?: string }> services?: Array<{ name?: string; status?: string; displayName?: string }>
@ -213,6 +202,105 @@ function readNumber(record: Record<string, unknown>, ...keys: string[]): number
return undefined return undefined
} }
function parseWindowsInstallDate(value: unknown): Date | null {
if (!value) return null
if (value instanceof Date) return value
if (typeof value === "number" && Number.isFinite(value)) {
return new Date(value)
}
if (typeof value !== "string") return null
const trimmed = value.trim()
if (!trimmed) return null
const wmiMatch = trimmed.match(/Date\((\d+)\)/)
if (wmiMatch) {
const timestamp = Number(wmiMatch[1])
if (Number.isFinite(timestamp)) {
return new Date(timestamp)
}
}
const digitsOnly = trimmed.replace(/[^0-9]/g, "")
if (digitsOnly.length >= 8 && digitsOnly.length <= 14) {
const year = Number(digitsOnly.slice(0, 4))
const month = Number(digitsOnly.slice(4, 6)) - 1
const day = Number(digitsOnly.slice(6, 8))
const hours = Number(digitsOnly.slice(8, 10) || "0")
const minutes = Number(digitsOnly.slice(10, 12) || "0")
const seconds = Number(digitsOnly.slice(12, 14) || "0")
const parsed = new Date(Date.UTC(year, month, day, hours, minutes, seconds))
if (!Number.isNaN(parsed.getTime())) {
return parsed
}
}
const parsed = new Date(trimmed)
return Number.isNaN(parsed.getTime()) ? null : parsed
}
type WindowsOsInfo = {
productName?: string
editionId?: string
displayVersion?: string
releaseId?: string
currentBuild?: string
currentBuildNumber?: string
licenseStatus?: number
isActivated?: boolean
installDate?: Date | null
experience?: string
}
function parseWindowsOsInfo(raw: unknown): WindowsOsInfo | null {
if (!raw) return null
const parseRecord = (value: Record<string, unknown>) => {
const productName = readString(value, "ProductName", "productName")
const editionId = readString(value, "EditionID", "editionId")
const displayVersion = readString(value, "DisplayVersion", "displayVersion")
const releaseId = readString(value, "ReleaseId", "releaseId")
const currentBuild = readString(value, "CurrentBuild", "currentBuild")
const currentBuildNumber = readString(value, "CurrentBuildNumber", "currentBuildNumber")
const licenseStatus = readNumber(value, "LicenseStatus", "licenseStatus")
const isActivatedRaw = value["IsActivated"]
const isActivated =
typeof isActivatedRaw === "boolean"
? isActivatedRaw
: typeof isActivatedRaw === "string"
? isActivatedRaw.toLowerCase() === "true"
: undefined
const installDate =
parseWindowsInstallDate(value["InstallDate"]) ??
parseWindowsInstallDate(value["InstallationDate"]) ??
parseWindowsInstallDate(value["InstallDateTime"])
const experience = readString(value, "Experience", "experience", "UBR")
return {
productName,
editionId,
displayVersion,
releaseId,
currentBuild,
currentBuildNumber,
licenseStatus,
isActivated,
installDate,
experience,
}
}
if (Array.isArray(raw)) {
for (const entry of raw) {
const record = toRecord(entry)
if (record) {
return parseRecord(record)
}
}
return null
}
if (typeof raw === "string") {
return { productName: raw }
}
const record = toRecord(raw)
if (!record) return null
return parseRecord(record)
}
function parseBytesLike(value: unknown): number | undefined { function parseBytesLike(value: unknown): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) return value if (typeof value === "number" && Number.isFinite(value)) return value
if (typeof value === "string") { if (typeof value === "string") {
@ -383,6 +471,14 @@ function formatDate(date?: Date | null) {
return format(date, "dd/MM/yyyy HH:mm") return format(date, "dd/MM/yyyy HH:mm")
} }
function formatAbsoluteDateTime(date?: Date | null) {
if (!date) return "—"
return new Intl.DateTimeFormat("pt-BR", {
dateStyle: "long",
timeStyle: "short",
}).format(date)
}
function formatBytes(bytes?: number | null) { function formatBytes(bytes?: number | null) {
if (!bytes || Number.isNaN(bytes)) return "—" if (!bytes || Number.isNaN(bytes)) return "—"
const units = ["B", "KB", "MB", "GB", "TB"] const units = ["B", "KB", "MB", "GB", "TB"]
@ -631,24 +727,50 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
const linuxExt = extended?.linux ?? null const linuxExt = extended?.linux ?? null
const windowsExt = extended?.windows ?? null const windowsExt = extended?.windows ?? null
const macosExt = extended?.macos ?? null const macosExt = extended?.macos ?? null
const windowsOsInfo = parseWindowsOsInfo(windowsExt?.osInfo)
const windowsActivationStatus = windowsOsInfo?.isActivated ?? (typeof windowsOsInfo?.licenseStatus === "number" ? windowsOsInfo.licenseStatus === 1 : null)
const windowsMemoryModulesRaw = windowsExt?.memoryModules const windowsMemoryModulesRaw = windowsExt?.memoryModules
const windowsVideoControllersRaw = windowsExt?.videoControllers const windowsVideoControllersRaw = windowsExt?.videoControllers
const windowsDiskEntriesRaw = windowsExt?.disks const windowsDiskEntriesRaw = windowsExt?.disks
const windowsMemoryModules = Array.isArray(windowsMemoryModulesRaw) const windowsServicesRaw = windowsExt?.services
? windowsMemoryModulesRaw const windowsSoftwareRaw = windowsExt?.software
: windowsMemoryModulesRaw && typeof windowsMemoryModulesRaw === "object" const windowsBaseboardRaw = windowsExt?.baseboard
? [windowsMemoryModulesRaw] const windowsBaseboard = Array.isArray(windowsBaseboardRaw)
: [] ? windowsBaseboardRaw[0]
const windowsVideoControllers = Array.isArray(windowsVideoControllersRaw) : windowsBaseboardRaw && typeof windowsBaseboardRaw === "object"
? windowsVideoControllersRaw ? windowsBaseboardRaw
: windowsVideoControllersRaw && typeof windowsVideoControllersRaw === "object" : null
? [windowsVideoControllersRaw] const windowsSerialNumber = windowsBaseboard ? readString(toRecord(windowsBaseboard) ?? {}, "SerialNumber", "serialNumber") : undefined
: [] const windowsMemoryModules = useMemo(() => {
const windowsDiskEntries = Array.isArray(windowsDiskEntriesRaw) if (Array.isArray(windowsMemoryModulesRaw)) return windowsMemoryModulesRaw
? windowsDiskEntriesRaw if (windowsMemoryModulesRaw && typeof windowsMemoryModulesRaw === "object") return [windowsMemoryModulesRaw]
: windowsDiskEntriesRaw && typeof windowsDiskEntriesRaw === "object" return []
? [windowsDiskEntriesRaw] }, [windowsMemoryModulesRaw])
: [] const windowsVideoControllers = useMemo(() => {
if (Array.isArray(windowsVideoControllersRaw)) return windowsVideoControllersRaw
if (windowsVideoControllersRaw && typeof windowsVideoControllersRaw === "object") return [windowsVideoControllersRaw]
return []
}, [windowsVideoControllersRaw])
const windowsDiskEntries = useMemo(() => {
if (Array.isArray(windowsDiskEntriesRaw)) return windowsDiskEntriesRaw
if (windowsDiskEntriesRaw && typeof windowsDiskEntriesRaw === "object") return [windowsDiskEntriesRaw]
return []
}, [windowsDiskEntriesRaw])
const windowsServices = useMemo(() => {
if (Array.isArray(windowsServicesRaw)) return windowsServicesRaw
if (windowsServicesRaw && typeof windowsServicesRaw === "object") return [windowsServicesRaw]
return []
}, [windowsServicesRaw])
const windowsSoftware = useMemo(() => {
if (Array.isArray(windowsSoftwareRaw)) return windowsSoftwareRaw
if (windowsSoftwareRaw && typeof windowsSoftwareRaw === "object") return [windowsSoftwareRaw]
return []
}, [windowsSoftwareRaw])
const windowsEditionLabel = windowsOsInfo?.productName ?? windowsOsInfo?.editionId ?? null
const windowsVersionLabel = windowsOsInfo?.displayVersion ?? windowsOsInfo?.releaseId ?? null
const windowsBuildLabel = windowsOsInfo?.currentBuildNumber ?? windowsOsInfo?.currentBuild ?? null
const windowsInstallDateLabel = windowsOsInfo?.installDate ? formatAbsoluteDateTime(windowsOsInfo.installDate) : null
const windowsExperienceLabel = windowsOsInfo?.experience ?? null
const linuxLsblk = linuxExt?.lsblk ?? [] const linuxLsblk = linuxExt?.lsblk ?? []
const linuxSmartEntries = linuxExt?.smart ?? [] const linuxSmartEntries = linuxExt?.smart ?? []
const normalizedHardwareGpus = Array.isArray(hardware?.gpus) const normalizedHardwareGpus = Array.isArray(hardware?.gpus)
@ -684,8 +806,6 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
? windowsCpuRaw ? windowsCpuRaw
: [windowsCpuRaw] : [windowsCpuRaw]
: [] : []
const windowsServices = windowsExt?.services ?? []
const windowsSoftware = windowsExt?.software ?? []
const winDiskStats = windowsDiskEntries.length > 0 const winDiskStats = windowsDiskEntries.length > 0
? { ? {
count: windowsDiskEntries.length, count: windowsDiskEntries.length,
@ -753,6 +873,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
(machine?.persona === "manager" || collaborator?.role === "manager") ? "manager" : "collaborator" (machine?.persona === "manager" || collaborator?.role === "manager") ? "manager" : "collaborator"
) )
const [savingAccess, setSavingAccess] = useState(false) const [savingAccess, setSavingAccess] = useState(false)
const [showAllWindowsSoftware, setShowAllWindowsSoftware] = useState(false)
const jsonText = useMemo(() => { const jsonText = useMemo(() => {
const payload = { const payload = {
id: machine?.id, id: machine?.id,
@ -781,6 +902,15 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
setAccessRole((machine?.persona === "manager" || collaborator?.role === "manager") ? "manager" : "collaborator") setAccessRole((machine?.persona === "manager" || collaborator?.role === "manager") ? "manager" : "collaborator")
}, [machine?.id, machine?.persona, collaborator?.email, collaborator?.name, collaborator?.role]) }, [machine?.id, machine?.persona, collaborator?.email, collaborator?.name, collaborator?.role])
useEffect(() => {
setShowAllWindowsSoftware(false)
}, [machine?.id])
const displayedWindowsSoftware = useMemo(
() => (showAllWindowsSoftware ? windowsSoftware : windowsSoftware.slice(0, 8)),
[showAllWindowsSoftware, windowsSoftware]
)
const handleSaveAccess = async () => { const handleSaveAccess = async () => {
if (!machine) return if (!machine) return
if (!accessEmail.trim()) { if (!accessEmail.trim()) {
@ -853,14 +983,20 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700"> <Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
{machine.architecture?.toUpperCase() ?? "Arquitetura indefinida"} {machine.architecture?.toUpperCase() ?? "Arquitetura indefinida"}
</Badge> </Badge>
{windowsExt?.osInfo ? ( {windowsOsInfo ? (
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700"> <Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
Build: {String(windowsExt.osInfo?.CurrentBuildNumber ?? windowsExt.osInfo?.CurrentBuild ?? "—")} Build: {windowsBuildLabel ?? "—"}
</Badge> </Badge>
) : null} ) : null}
{windowsExt?.osInfo ? ( {windowsOsInfo ? (
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700"> <Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
Ativado: {windowsExt.osInfo?.IsActivated === true ? "Sim" : "Não"} Ativado: {
windowsActivationStatus == null
? "—"
: windowsActivationStatus
? "Sim"
: "Não"
}
</Badge> </Badge>
) : null} ) : null}
{primaryGpu?.name ? ( {primaryGpu?.name ? (
@ -1251,7 +1387,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
{windowsExt ? ( {windowsExt ? (
<div className="space-y-3"> <div className="space-y-3">
{/* Cards resumidos: CPU / RAM / GPU / Discos */} {/* Cards resumidos: CPU / RAM / GPU / Discos */}
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4"> <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-5">
<Card className="border-slate-200"> <Card className="border-slate-200">
<CardContent className="flex items-center gap-3 py-3"> <CardContent className="flex items-center gap-3 py-3">
<Cpu className="size-5 text-slate-500" /> <Cpu className="size-5 text-slate-500" />
@ -1270,6 +1406,20 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="border-slate-200">
<CardContent className="flex items-center gap-3 py-3">
<Terminal className="size-5 text-slate-500" />
<div className="min-w-0">
<p className="truncate text-xs text-muted-foreground">Windows</p>
<p className="truncate text-sm font-semibold text-foreground">
{windowsEditionLabel ?? machine?.osName ?? "—"}
</p>
<p className="truncate text-xs text-muted-foreground">
{windowsVersionLabel ?? windowsBuildLabel ?? "Versão desconhecida"}
</p>
</div>
</CardContent>
</Card>
<Card className="border-slate-200"> <Card className="border-slate-200">
<CardContent className="flex items-center gap-3 py-3"> <CardContent className="flex items-center gap-3 py-3">
<Monitor className="size-5 text-slate-500" /> <Monitor className="size-5 text-slate-500" />
@ -1289,6 +1439,24 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
{windowsOsInfo ? (
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
<p className="text-xs font-semibold uppercase text-slate-500">Informações do Windows</p>
<div className="mt-2 grid gap-1 text-sm text-muted-foreground">
<DetailLine label="Edição" value={windowsEditionLabel ?? "—"} classNameValue="break-words" />
<DetailLine label="Versão" value={windowsVersionLabel ?? "—"} />
<DetailLine label="Compilação do SO" value={windowsBuildLabel ?? "—"} />
<DetailLine label="Instalado em" value={windowsInstallDateLabel ?? "—"} />
<DetailLine label="Experiência" value={windowsExperienceLabel ?? "—"} />
<DetailLine
label="Ativação"
value={windowsActivationStatus == null ? "—" : windowsActivationStatus ? "Ativado" : "Não ativado"}
/>
<DetailLine label="Número de série" value={windowsSerialNumber ?? "—"} />
</div>
</div>
) : null}
{windowsCpuDetails.length > 0 ? ( {windowsCpuDetails.length > 0 ? (
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3"> <div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
<p className="text-xs font-semibold uppercase text-slate-500">CPU</p> <p className="text-xs font-semibold uppercase text-slate-500">CPU</p>
@ -1359,9 +1527,22 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
{windowsSoftware.length > 0 ? ( {windowsSoftware.length > 0 ? (
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3"> <div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
<p className="text-xs font-semibold uppercase text-slate-500">Softwares (amostra)</p> <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<p className="text-xs font-semibold uppercase text-slate-500">Aplicativos instalados</p>
{windowsSoftware.length > 8 ? (
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 px-2 text-xs text-slate-600 hover:bg-slate-200/60"
onClick={() => setShowAllWindowsSoftware((prev) => !prev)}
>
{showAllWindowsSoftware ? "Mostrar menos" : `Ver todos (${windowsSoftware.length})`}
</Button>
) : null}
</div>
<ul className="mt-2 grid gap-1 text-xs text-muted-foreground"> <ul className="mt-2 grid gap-1 text-xs text-muted-foreground">
{windowsSoftware.slice(0, 8).map((softwareItem, index) => { {displayedWindowsSoftware.map((softwareItem, index) => {
const record = toRecord(softwareItem) ?? {} const record = toRecord(softwareItem) ?? {}
const name = readString(record, "DisplayName", "name") ?? "—" const name = readString(record, "DisplayName", "name") ?? "—"
const version = readString(record, "DisplayVersion", "version") const version = readString(record, "DisplayVersion", "version")