feat: enhance machine insights and solidify admin workflows

This commit is contained in:
Esdras Renan 2025-10-16 22:56:57 -03:00
parent ac986410a3
commit 4c228e908a
7 changed files with 286 additions and 35 deletions

View file

@ -138,7 +138,8 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
body: JSON.stringify(payload),
credentials: "include",
})
if (!r.ok) throw new Error("update_failed")
const data = (await r.json().catch(() => ({}))) as { error?: string }
if (!r.ok) throw new Error(data?.error ?? "Falha ao atualizar empresa")
} else {
const r = await fetch(`/api/admin/companies`, {
method: "POST",
@ -146,14 +147,16 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
body: JSON.stringify(payload),
credentials: "include",
})
if (!r.ok) throw new Error("create_failed")
const data = (await r.json().catch(() => ({}))) as { error?: string }
if (!r.ok) throw new Error(data?.error ?? "Falha ao criar empresa")
}
await refresh()
resetForm()
setEditingId(null)
toast.success(editingId ? "Empresa atualizada" : "Empresa criada", { id: "companies" })
} catch {
toast.error("Não foi possível salvar", { id: "companies" })
} catch (error) {
const message = error instanceof Error ? error.message : "Não foi possível salvar"
toast.error(message, { id: "companies" })
}
})
}

View file

@ -43,6 +43,22 @@ type MachineSoftware = {
source?: string
}
type NormalizedSoftwareEntry = {
name: string
version?: string
publisher?: string
installDate?: Date | null
source?: string
}
type MachineAlertEntry = {
id: string
kind: string
message: string
severity: string
createdAt: number
}
type DetailLineProps = { label: string; value?: string | number | null; classNameValue?: string }
type GpuAdapter = {
@ -166,6 +182,13 @@ type MachineInventory = {
collaborator?: { email?: string; name?: string; role?: string }
}
function collectInitials(name: string): string {
const words = name.split(/\s+/).filter(Boolean)
if (words.length === 0) return "?"
if (words.length === 1) return words[0].slice(0, 2).toUpperCase()
return (words[0][0] + words[1][0]).toUpperCase()
}
function toRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object") return null
return value as Record<string, unknown>
@ -278,6 +301,11 @@ type WindowsOsInfo = {
currentBuildNumber?: string
licenseStatus?: number
isActivated?: boolean
licenseStatusText?: string
productId?: string
partialProductKey?: string
computerName?: string
registeredOwner?: string
installDate?: Date | null
experience?: string
}
@ -309,17 +337,35 @@ function parseWindowsOsInfo(raw: unknown): WindowsOsInfo | null {
undefined
const ubrRaw = readFlexible("UBR")
const licenseStatus = readNum("LicenseStatus", "licenseStatus")
const licenseStatusTextRaw = readFlexible(
"LicenseStatusDescription",
"licenseStatusDescription",
"StatusDescription",
"statusDescription",
"Status",
"status"
)
const licenseStatusText = licenseStatusTextRaw ? licenseStatusTextRaw.trim() : undefined
const currentBuildNumber =
baseBuild && ubrRaw && /^\d+$/.test(ubrRaw) ? `${baseBuild}.${ubrRaw}` : baseBuild ?? readFlexible("BuildNumber", "buildNumber")
const currentBuild = baseBuild
const isActivatedRaw = value["IsActivated"] ?? value["isActivated"]
const isLicensedRaw = value["IsLicensed"] ?? value["isLicensed"]
const isActivated =
typeof isActivatedRaw === "boolean"
? isActivatedRaw
: typeof isActivatedRaw === "number"
? isActivatedRaw === 1
: typeof isActivatedRaw === "string"
? isActivatedRaw.toLowerCase() === "true"
: licenseStatus === 1
: typeof isLicensedRaw === "boolean"
? isLicensedRaw
: typeof isLicensedRaw === "number"
? isLicensedRaw === 1
: typeof isLicensedRaw === "string"
? ["1", "true", "licensed", "license", "activated"].includes(isLicensedRaw.toLowerCase())
: licenseStatus === 1 || Boolean(licenseStatusText && /licensed|activated|licenciado/i.test(licenseStatusText))
const installDate =
parseWindowsInstallDate(value["InstallDate"]) ??
@ -331,6 +377,11 @@ function parseWindowsOsInfo(raw: unknown): WindowsOsInfo | null {
readFlexible("Experience", "experience", "FeatureExperiencePack", "featureExperiencePack") ??
(currentBuildNumber ? `OS Build ${currentBuildNumber}` : undefined)
const productId = readFlexible("ProductID", "productID", "ProductId", "productId", "ProductKeyId", "productKeyId")
const partialProductKey = readFlexible("PartialProductKey", "partialProductKey")
const computerName = readFlexible("DeviceName", "deviceName", "ComputerName", "computerName", "CSName", "csName", "HostName", "hostName")
const registeredOwner = readFlexible("RegisteredOwner", "registeredOwner", "RegisteredOrganization", "registeredOrganization")
return {
productName,
editionId,
@ -341,6 +392,11 @@ function parseWindowsOsInfo(raw: unknown): WindowsOsInfo | null {
currentBuildNumber,
licenseStatus,
isActivated,
licenseStatusText,
productId,
partialProductKey,
computerName,
registeredOwner,
installDate,
experience,
}
@ -363,6 +419,39 @@ function parseWindowsOsInfo(raw: unknown): WindowsOsInfo | null {
return parseRecord(record)
}
function normalizeWindowsSoftwareEntry(value: unknown): NormalizedSoftwareEntry | null {
const record = toRecord(value)
if (!record) return null
const name =
readString(record, "DisplayName", "displayName", "Name", "name") ??
readText(record, "Title", "title") ??
""
if (!name) return null
const version = readString(record, "DisplayVersion", "displayVersion", "Version", "version") ?? undefined
const publisher = readString(record, "Publisher", "publisher", "Vendor", "vendor") ?? undefined
const installDate =
parseWindowsInstallDate(record["InstallDate"]) ??
parseWindowsInstallDate(record["InstallDateUTC"]) ??
parseWindowsInstallDate(record["InstallDateTime"]) ??
parseWindowsInstallDate(record["InstallDateFromRegistry"]) ??
parseWindowsInstallDate(record["InstallDateFromRegistryUTC"]) ??
parseWindowsInstallDate(record["InstalledDate"]) ??
parseWindowsInstallDate(record["InstalledOn"]) ??
null
const source = readString(record, "ParentDisplayName", "ParentKeyName", "SystemComponent") ?? undefined
return {
name,
version,
publisher,
installDate,
source,
}
}
function parseBytesLike(value: unknown): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) return value
if (typeof value === "string") {
@ -552,6 +641,12 @@ function formatPostureAlertKind(raw?: string | null): string {
.replace(/\b\w/g, (char) => char.toUpperCase())
}
function postureSeverityClass(severity?: string | null) {
return (severity ?? "warning").toLowerCase() === "critical"
? "border-rose-500/20 bg-rose-500/10"
: "border-amber-500/20 bg-amber-500/10"
}
function formatRelativeTime(date?: Date | null) {
if (!date) return "Nunca"
try {
@ -561,6 +656,11 @@ function formatRelativeTime(date?: Date | null) {
}
}
function formatInstallDate(date?: Date | null) {
if (!date) return null
return `${formatAbsoluteDateTime(date)} (${formatRelativeTime(date)})`
}
function formatDate(date?: Date | null) {
if (!date) return "—"
return format(date, "dd/MM/yyyy HH:mm")
@ -825,6 +925,14 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
api.companies.list,
companyQueryArgs ?? ("skip" as const)
) as Array<{ id: string; name: string; slug?: string }> | undefined
// Convex codegen precisa ser atualizado para tipar `machines.listAlerts`.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const machinesApi: any = api
const alertsHistory = useQuery(
machine && machinesApi?.machines?.listAlerts ? machinesApi.machines.listAlerts : "skip",
machine && machinesApi?.machines?.listAlerts ? { machineId: machine.id as Id<"machines">, limit: 50 } : ("skip" as const)
) as MachineAlertEntry[] | undefined
const machineAlertsHistory = alertsHistory ?? []
const metadata = machine?.inventory ?? null
const metrics = machine?.metrics ?? null
const hardware = metadata?.hardware
@ -878,11 +986,49 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
if (windowsSoftwareRaw && typeof windowsSoftwareRaw === "object") return [windowsSoftwareRaw]
return []
}, [windowsSoftwareRaw])
const normalizedWindowsSoftware = useMemo(() => {
return windowsSoftware
.map((item) => normalizeWindowsSoftwareEntry(item))
.filter((entry): entry is NormalizedSoftwareEntry => Boolean(entry))
.sort((a, b) => {
const aTime = a.installDate ? a.installDate.getTime() : 0
const bTime = b.installDate ? b.installDate.getTime() : 0
if (aTime !== bTime) return bTime - aTime
return a.name.localeCompare(b.name, "pt-BR")
})
}, [windowsSoftware])
const windowsEditionLabel = windowsOsInfo?.productName ?? windowsOsInfo?.editionId ?? null
const windowsVersionLabel = windowsOsInfo?.displayVersion ?? windowsOsInfo?.version ?? windowsOsInfo?.releaseId ?? null
const windowsBuildLabel = windowsOsInfo?.currentBuildNumber ?? windowsOsInfo?.currentBuild ?? null
const windowsInstallDateLabel = windowsOsInfo?.installDate ? formatAbsoluteDateTime(windowsOsInfo.installDate) : null
const windowsExperienceLabel = windowsOsInfo?.experience ?? null
const windowsProductId = windowsOsInfo?.productId ?? null
const windowsPartialProductKey = windowsOsInfo?.partialProductKey ?? null
const windowsComputerName = windowsOsInfo?.computerName ?? hardware?.model ?? machine?.hostname ?? null
const windowsRegisteredOwner = windowsOsInfo?.registeredOwner ?? null
const windowsLicenseStatusLabel = (() => {
if (windowsOsInfo?.licenseStatusText) {
return windowsOsInfo.licenseStatusText
}
switch (windowsOsInfo?.licenseStatus) {
case 0:
return "Sem licença"
case 1:
return "Licenciado"
case 2:
return "Período inicial (OOB Grace)"
case 3:
return "Período exposto (OOT Grace)"
case 4:
return "Período não genuíno"
case 5:
return "Notificação"
case 6:
return "Período estendido"
default:
return null
}
})()
const linuxLsblk = linuxExt?.lsblk ?? []
const linuxSmartEntries = linuxExt?.smart ?? []
const normalizedHardwareGpus = Array.isArray(hardware?.gpus)
@ -1019,8 +1165,8 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
}, [machine?.id])
const displayedWindowsSoftware = useMemo(
() => (showAllWindowsSoftware ? windowsSoftware : windowsSoftware.slice(0, 8)),
[showAllWindowsSoftware, windowsSoftware]
() => (showAllWindowsSoftware ? normalizedWindowsSoftware : normalizedWindowsSoftware.slice(0, 12)),
[showAllWindowsSoftware, normalizedWindowsSoftware]
)
const handleSaveAccess = async () => {
@ -1557,15 +1703,20 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
<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="Nome do dispositivo" value={windowsComputerName ?? "—"} classNameValue="break-words" />
<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="Status da licença" value={windowsLicenseStatusLabel ?? "—"} classNameValue="break-words" />
<DetailLine
label="Ativação"
value={windowsActivationStatus == null ? "—" : windowsActivationStatus ? "Ativado" : "Não ativado"}
/>
<DetailLine label="ID do produto" value={windowsProductId ?? "—"} classNameValue="break-words" />
<DetailLine label="Chave parcial" value={windowsPartialProductKey ?? "—"} />
<DetailLine label="Proprietário registrado" value={windowsRegisteredOwner ?? "—"} classNameValue="break-words" />
<DetailLine label="Número de série" value={windowsSerialNumber ?? "—"} />
</div>
</div>
@ -1639,11 +1790,11 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
</div>
) : null}
{windowsSoftware.length > 0 ? (
{normalizedWindowsSoftware.length > 0 ? (
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
<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 ? (
{normalizedWindowsSoftware.length > 12 ? (
<Button
type="button"
variant="ghost"
@ -1651,21 +1802,33 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
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})`}
{showAllWindowsSoftware ? "Mostrar menos" : `Ver todos (${normalizedWindowsSoftware.length})`}
</Button>
) : null}
</div>
<ul className="mt-2 grid gap-1 text-xs text-muted-foreground">
<ul className="mt-2 space-y-2">
{displayedWindowsSoftware.map((softwareItem, index) => {
const record = toRecord(softwareItem) ?? {}
const name = readString(record, "DisplayName", "name") ?? "—"
const version = readString(record, "DisplayVersion", "version")
const publisher = readString(record, "Publisher")
const initials = collectInitials(softwareItem.name)
const installedAt = formatInstallDate(softwareItem.installDate)
return (
<li key={`sw-${index}`}>
<span className="font-medium text-foreground">{name}</span>
{version ? <span className="ml-1">{version}</span> : null}
{publisher ? <span className="ml-1">· {publisher}</span> : null}
<li key={`sw-${index}`} className="flex items-start gap-3 rounded-md border border-slate-200 bg-white px-3 py-2">
<span className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-slate-200 text-xs font-semibold uppercase text-slate-700">
{initials}
</span>
<div className="flex-1 space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-semibold text-foreground">{softwareItem.name}</span>
{softwareItem.version ? (
<Badge variant="outline" className="border-slate-300 bg-slate-100 text-[11px] text-slate-700">
{softwareItem.version}
</Badge>
) : null}
</div>
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
{softwareItem.publisher ? <span>{softwareItem.publisher}</span> : null}
{installedAt ? <span>Instalado em {installedAt}</span> : null}
</div>
</div>
</li>
)
})}
@ -1848,9 +2011,13 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
<h4 className="text-sm font-semibold">Alertas de postura</h4>
<div className="space-y-2">
{machine?.postureAlerts?.map((a: { kind?: string; message?: string; severity?: string }, i: number) => (
<div key={`alert-${i}`} className={cn("flex items-center justify-between rounded-md border px-3 py-2 text-sm",
(a?.severity ?? "warning").toLowerCase() === "critical" ? "border-rose-500/20 bg-rose-500/10" : "border-amber-500/20 bg-amber-500/10")
}>
<div
key={`alert-${i}`}
className={cn(
"flex items-center justify-between rounded-md border px-3 py-2 text-sm",
postureSeverityClass(a?.severity)
)}
>
<span className="font-medium text-foreground">{a?.message ?? formatPostureAlertKind(a?.kind)}</span>
<Badge variant="outline">{formatPostureAlertKind(a?.kind)}</Badge>
</div>
@ -1862,6 +2029,40 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
</section>
) : null}
<section className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="text-sm font-semibold">Histórico de alertas</h4>
{machineAlertsHistory.length > 0 ? (
<span className="text-xs text-muted-foreground">
Últimos {machineAlertsHistory.length} {machineAlertsHistory.length === 1 ? "evento" : "eventos"}
</span>
) : null}
</div>
{machineAlertsHistory.length > 0 ? (
<div className="relative max-h-64 overflow-y-auto pr-2">
<div className="absolute left-3 top-3 bottom-3 w-px bg-slate-200" />
<ol className="space-y-3 pl-6">
{machineAlertsHistory.map((alert) => {
const date = new Date(alert.createdAt)
return (
<li key={alert.id} className="relative rounded-md border border-slate-200/80 bg-white px-3 py-2 text-xs shadow-sm">
<span className="absolute -left-5 top-3 inline-flex size-3 items-center justify-center rounded-full border border-white bg-slate-200 ring-2 ring-white" />
<div className={cn("flex items-center justify-between", postureSeverityClass(alert.severity))}>
<span className="text-xs font-medium uppercase tracking-wide text-slate-600">{formatPostureAlertKind(alert.kind)}</span>
<span className="text-xs text-slate-500">{formatRelativeTime(date)}</span>
</div>
<p className="mt-1 text-sm text-foreground">{alert.message ?? formatPostureAlertKind(alert.kind)}</p>
<p className="mt-1 text-[11px] text-muted-foreground">{format(date, "dd/MM/yyyy HH:mm:ss")}</p>
</li>
)
})}
</ol>
</div>
) : (
<p className="text-xs text-muted-foreground">Nenhum alerta registrado para esta máquina.</p>
)}
</section>
<div className="flex flex-wrap gap-2 pt-2">
{Array.isArray(software) && software.length > 0 ? (
<Button size="sm" variant="outline" onClick={() => exportCsv(software, "softwares.csv")}>Softwares CSV</Button>