Expose detailed Windows OS info in machine inventory
This commit is contained in:
parent
3d89c5fd32
commit
64e4e02a9a
1 changed files with 216 additions and 35 deletions
|
|
@ -116,17 +116,6 @@ type WindowsDiskEntry = {
|
|||
MediaType?: string
|
||||
}
|
||||
|
||||
type WindowsOsInfo = {
|
||||
ProductName?: string
|
||||
CurrentBuild?: string | number
|
||||
CurrentBuildNumber?: string | number
|
||||
DisplayVersion?: string
|
||||
ReleaseId?: string
|
||||
EditionID?: string
|
||||
LicenseStatus?: number
|
||||
IsActivated?: boolean
|
||||
}
|
||||
|
||||
type WindowsExtended = {
|
||||
software?: MachineSoftware[]
|
||||
services?: Array<{ name?: string; status?: string; displayName?: string }>
|
||||
|
|
@ -213,6 +202,105 @@ function readNumber(record: Record<string, unknown>, ...keys: string[]): number
|
|||
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 {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value
|
||||
if (typeof value === "string") {
|
||||
|
|
@ -383,6 +471,14 @@ function formatDate(date?: Date | null) {
|
|||
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) {
|
||||
if (!bytes || Number.isNaN(bytes)) return "—"
|
||||
const units = ["B", "KB", "MB", "GB", "TB"]
|
||||
|
|
@ -631,24 +727,50 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
const linuxExt = extended?.linux ?? null
|
||||
const windowsExt = extended?.windows ?? 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 windowsVideoControllersRaw = windowsExt?.videoControllers
|
||||
const windowsDiskEntriesRaw = windowsExt?.disks
|
||||
const windowsMemoryModules = Array.isArray(windowsMemoryModulesRaw)
|
||||
? windowsMemoryModulesRaw
|
||||
: windowsMemoryModulesRaw && typeof windowsMemoryModulesRaw === "object"
|
||||
? [windowsMemoryModulesRaw]
|
||||
: []
|
||||
const windowsVideoControllers = Array.isArray(windowsVideoControllersRaw)
|
||||
? windowsVideoControllersRaw
|
||||
: windowsVideoControllersRaw && typeof windowsVideoControllersRaw === "object"
|
||||
? [windowsVideoControllersRaw]
|
||||
: []
|
||||
const windowsDiskEntries = Array.isArray(windowsDiskEntriesRaw)
|
||||
? windowsDiskEntriesRaw
|
||||
: windowsDiskEntriesRaw && typeof windowsDiskEntriesRaw === "object"
|
||||
? [windowsDiskEntriesRaw]
|
||||
: []
|
||||
const windowsServicesRaw = windowsExt?.services
|
||||
const windowsSoftwareRaw = windowsExt?.software
|
||||
const windowsBaseboardRaw = windowsExt?.baseboard
|
||||
const windowsBaseboard = Array.isArray(windowsBaseboardRaw)
|
||||
? windowsBaseboardRaw[0]
|
||||
: windowsBaseboardRaw && typeof windowsBaseboardRaw === "object"
|
||||
? windowsBaseboardRaw
|
||||
: null
|
||||
const windowsSerialNumber = windowsBaseboard ? readString(toRecord(windowsBaseboard) ?? {}, "SerialNumber", "serialNumber") : undefined
|
||||
const windowsMemoryModules = useMemo(() => {
|
||||
if (Array.isArray(windowsMemoryModulesRaw)) return windowsMemoryModulesRaw
|
||||
if (windowsMemoryModulesRaw && typeof windowsMemoryModulesRaw === "object") return [windowsMemoryModulesRaw]
|
||||
return []
|
||||
}, [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 linuxSmartEntries = linuxExt?.smart ?? []
|
||||
const normalizedHardwareGpus = Array.isArray(hardware?.gpus)
|
||||
|
|
@ -684,8 +806,6 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
? windowsCpuRaw
|
||||
: [windowsCpuRaw]
|
||||
: []
|
||||
const windowsServices = windowsExt?.services ?? []
|
||||
const windowsSoftware = windowsExt?.software ?? []
|
||||
const winDiskStats = windowsDiskEntries.length > 0
|
||||
? {
|
||||
count: windowsDiskEntries.length,
|
||||
|
|
@ -753,6 +873,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
(machine?.persona === "manager" || collaborator?.role === "manager") ? "manager" : "collaborator"
|
||||
)
|
||||
const [savingAccess, setSavingAccess] = useState(false)
|
||||
const [showAllWindowsSoftware, setShowAllWindowsSoftware] = useState(false)
|
||||
const jsonText = useMemo(() => {
|
||||
const payload = {
|
||||
id: machine?.id,
|
||||
|
|
@ -781,6 +902,15 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
setAccessRole((machine?.persona === "manager" || collaborator?.role === "manager") ? "manager" : "collaborator")
|
||||
}, [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 () => {
|
||||
if (!machine) return
|
||||
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">
|
||||
{machine.architecture?.toUpperCase() ?? "Arquitetura indefinida"}
|
||||
</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">
|
||||
Build: {String(windowsExt.osInfo?.CurrentBuildNumber ?? windowsExt.osInfo?.CurrentBuild ?? "—")}
|
||||
Build: {windowsBuildLabel ?? "—"}
|
||||
</Badge>
|
||||
) : 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">
|
||||
Ativado: {windowsExt.osInfo?.IsActivated === true ? "Sim" : "Não"}
|
||||
Ativado: {
|
||||
windowsActivationStatus == null
|
||||
? "—"
|
||||
: windowsActivationStatus
|
||||
? "Sim"
|
||||
: "Não"
|
||||
}
|
||||
</Badge>
|
||||
) : null}
|
||||
{primaryGpu?.name ? (
|
||||
|
|
@ -1251,7 +1387,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
{windowsExt ? (
|
||||
<div className="space-y-3">
|
||||
{/* 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">
|
||||
<CardContent className="flex items-center gap-3 py-3">
|
||||
<Cpu className="size-5 text-slate-500" />
|
||||
|
|
@ -1270,6 +1406,20 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
</div>
|
||||
</CardContent>
|
||||
</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">
|
||||
<CardContent className="flex items-center gap-3 py-3">
|
||||
<Monitor className="size-5 text-slate-500" />
|
||||
|
|
@ -1289,6 +1439,24 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
</CardContent>
|
||||
</Card>
|
||||
</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 ? (
|
||||
<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>
|
||||
|
|
@ -1359,9 +1527,22 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
|
||||
{windowsSoftware.length > 0 ? (
|
||||
<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">
|
||||
{windowsSoftware.slice(0, 8).map((softwareItem, index) => {
|
||||
{displayedWindowsSoftware.map((softwareItem, index) => {
|
||||
const record = toRecord(softwareItem) ?? {}
|
||||
const name = readString(record, "DisplayName", "name") ?? "—"
|
||||
const version = readString(record, "DisplayVersion", "version")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue