feat: enhance machine insights and solidify admin workflows
This commit is contained in:
parent
ac986410a3
commit
4c228e908a
7 changed files with 286 additions and 35 deletions
|
|
@ -24,6 +24,7 @@
|
|||
- **React / React DOM**: `18.2.0`.
|
||||
- **Trilha de testes**: Vitest (`pnpm test`) sem modo watch por padrão (`--run --passWithNoTests`).
|
||||
- **CI**: workflow `Quality Checks` (`.github/workflows/quality-checks.yml`) roda `pnpm install`, `prisma:generate`, `lint`, `test`, `build`. Variáveis críticas (`BETTER_AUTH_SECRET`, `NEXT_PUBLIC_APP_URL`, etc.) são definidas apenas no runner — não afetam a VPS.
|
||||
- **Disciplina pós-mudanças**: sempre que fizer alterações locais, rode **obrigatoriamente** `pnpm lint`, `pnpm build` e `pnpm test` antes de entregar ou abrir PR. Esses comandos são mandatórios também para os agentes/automations, garantindo que o projeto continua íntegro.
|
||||
- **Deploy**: pipeline `ci-cd-web-desktop.yml` (runner self-hosted). Build roda com pnpm 9, Node 20. Web é publicado em `/home/renan/apps/sistema` e o Swarm aponta `sistema_web` para essa pasta.
|
||||
|
||||
## Setup local (atualizado)
|
||||
|
|
|
|||
|
|
@ -349,6 +349,20 @@ async function evaluatePostureAndMaybeRaise(
|
|||
const lastAtPrev = ensureFiniteNumber(prevMeta?.["lastPostureAt"]) ?? 0
|
||||
await ctx.db.patch(machine._id, { metadata: mergeMetadata(machine.metadata, record), updatedAt: now })
|
||||
|
||||
await Promise.all(
|
||||
findings.map((finding) =>
|
||||
ctx.db.insert("machineAlerts", {
|
||||
tenantId: machine.tenantId,
|
||||
machineId: machine._id,
|
||||
companyId: machine.companyId ?? undefined,
|
||||
kind: finding.kind,
|
||||
message: finding.message,
|
||||
severity: finding.severity,
|
||||
createdAt: now,
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
if ((process.env["MACHINE_ALERTS_CREATE_TICKETS"] ?? "false").toLowerCase() !== "true") return
|
||||
if (lastAtPrev && now - lastAtPrev < 30 * 60 * 1000) return
|
||||
|
||||
|
|
@ -818,6 +832,32 @@ export const listByTenant = query({
|
|||
},
|
||||
})
|
||||
|
||||
export const listAlerts = query({
|
||||
args: {
|
||||
machineId: v.id("machines"),
|
||||
limit: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const limit = Math.max(1, Math.min(args.limit ?? 50, 200))
|
||||
const alerts = await ctx.db
|
||||
.query("machineAlerts")
|
||||
.withIndex("by_machine_created", (q) => q.eq("machineId", args.machineId))
|
||||
.order("desc")
|
||||
.take(limit)
|
||||
|
||||
return alerts.map((alert) => ({
|
||||
id: alert._id,
|
||||
machineId: alert.machineId,
|
||||
tenantId: alert.tenantId,
|
||||
companyId: alert.companyId ?? null,
|
||||
kind: alert.kind,
|
||||
message: alert.message,
|
||||
severity: alert.severity,
|
||||
createdAt: alert.createdAt,
|
||||
}))
|
||||
},
|
||||
})
|
||||
|
||||
export const updatePersona = mutation({
|
||||
args: {
|
||||
machineId: v.id("machines"),
|
||||
|
|
|
|||
|
|
@ -274,6 +274,19 @@ export default defineSchema({
|
|||
.index("by_tenant_assigned_email", ["tenantId", "assignedUserEmail"])
|
||||
.index("by_auth_email", ["authEmail"]),
|
||||
|
||||
machineAlerts: defineTable({
|
||||
tenantId: v.string(),
|
||||
machineId: v.id("machines"),
|
||||
companyId: v.optional(v.id("companies")),
|
||||
kind: v.string(),
|
||||
message: v.string(),
|
||||
severity: v.string(),
|
||||
createdAt: v.number(),
|
||||
})
|
||||
.index("by_machine_created", ["machineId", "createdAt"])
|
||||
.index("by_tenant_created", ["tenantId", "createdAt"])
|
||||
.index("by_tenant_machine", ["tenantId", "machineId"]),
|
||||
|
||||
machineTokens: defineTable({
|
||||
tenantId: v.string(),
|
||||
machineId: v.id("machines"),
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
"better-auth": "^1.3.26",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"convex": "^1.27.3",
|
||||
"convex": "^1.28.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"lucide-react": "^0.544.0",
|
||||
|
|
|
|||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
|
|
@ -105,8 +105,8 @@ importers:
|
|||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
convex:
|
||||
specifier: ^1.27.3
|
||||
version: 1.27.3(react@18.2.0)
|
||||
specifier: ^1.28.0
|
||||
version: 1.28.0(react@18.2.0)
|
||||
date-fns:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
|
|
@ -2631,8 +2631,8 @@ packages:
|
|||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
convex@1.27.3:
|
||||
resolution: {integrity: sha512-Ebr9lPgXkL7JO5IFr3bG+gYvHskyJjc96Fx0BBNkJUDXrR/bd9/uI4q8QszbglK75XfDu068vR0d/HK2T7tB9Q==}
|
||||
convex@1.28.0:
|
||||
resolution: {integrity: sha512-40FgeJ/LxP9TxnkDDztU/A5gcGTdq1klcTT5mM0Ak+kSlQiDktMpjNX1TfkWLxXaE3lI4qvawKH95v2RiYgFxA==}
|
||||
engines: {node: '>=18.0.0', npm: '>=7.0.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
|
|
@ -3401,10 +3401,6 @@ packages:
|
|||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
jwt-decode@4.0.0:
|
||||
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
|
|
@ -6992,10 +6988,9 @@ snapshots:
|
|||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
convex@1.27.3(react@18.2.0):
|
||||
convex@1.28.0(react@18.2.0):
|
||||
dependencies:
|
||||
esbuild: 0.25.4
|
||||
jwt-decode: 4.0.0
|
||||
prettier: 3.6.2
|
||||
optionalDependencies:
|
||||
react: 18.2.0
|
||||
|
|
@ -7926,8 +7921,6 @@ snapshots:
|
|||
object.assign: 4.1.7
|
||||
object.values: 1.2.1
|
||||
|
||||
jwt-decode@4.0.0: {}
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
|
|
|||
|
|
@ -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" })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue