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`.
|
- **React / React DOM**: `18.2.0`.
|
||||||
- **Trilha de testes**: Vitest (`pnpm test`) sem modo watch por padrão (`--run --passWithNoTests`).
|
- **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.
|
- **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.
|
- **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)
|
## Setup local (atualizado)
|
||||||
|
|
|
||||||
|
|
@ -349,6 +349,20 @@ async function evaluatePostureAndMaybeRaise(
|
||||||
const lastAtPrev = ensureFiniteNumber(prevMeta?.["lastPostureAt"]) ?? 0
|
const lastAtPrev = ensureFiniteNumber(prevMeta?.["lastPostureAt"]) ?? 0
|
||||||
await ctx.db.patch(machine._id, { metadata: mergeMetadata(machine.metadata, record), updatedAt: now })
|
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 ((process.env["MACHINE_ALERTS_CREATE_TICKETS"] ?? "false").toLowerCase() !== "true") return
|
||||||
if (lastAtPrev && now - lastAtPrev < 30 * 60 * 1000) 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({
|
export const updatePersona = mutation({
|
||||||
args: {
|
args: {
|
||||||
machineId: v.id("machines"),
|
machineId: v.id("machines"),
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,19 @@ export default defineSchema({
|
||||||
.index("by_tenant_assigned_email", ["tenantId", "assignedUserEmail"])
|
.index("by_tenant_assigned_email", ["tenantId", "assignedUserEmail"])
|
||||||
.index("by_auth_email", ["authEmail"]),
|
.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({
|
machineTokens: defineTable({
|
||||||
tenantId: v.string(),
|
tenantId: v.string(),
|
||||||
machineId: v.id("machines"),
|
machineId: v.id("machines"),
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
"better-auth": "^1.3.26",
|
"better-auth": "^1.3.26",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"convex": "^1.27.3",
|
"convex": "^1.28.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
|
|
|
||||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
|
|
@ -105,8 +105,8 @@ importers:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
convex:
|
convex:
|
||||||
specifier: ^1.27.3
|
specifier: ^1.28.0
|
||||||
version: 1.27.3(react@18.2.0)
|
version: 1.28.0(react@18.2.0)
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
|
|
@ -2631,8 +2631,8 @@ packages:
|
||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
convex@1.27.3:
|
convex@1.28.0:
|
||||||
resolution: {integrity: sha512-Ebr9lPgXkL7JO5IFr3bG+gYvHskyJjc96Fx0BBNkJUDXrR/bd9/uI4q8QszbglK75XfDu068vR0d/HK2T7tB9Q==}
|
resolution: {integrity: sha512-40FgeJ/LxP9TxnkDDztU/A5gcGTdq1klcTT5mM0Ak+kSlQiDktMpjNX1TfkWLxXaE3lI4qvawKH95v2RiYgFxA==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=7.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=7.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -3401,10 +3401,6 @@ packages:
|
||||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
jwt-decode@4.0.0:
|
|
||||||
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
|
|
||||||
|
|
@ -6992,10 +6988,9 @@ snapshots:
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
convex@1.27.3(react@18.2.0):
|
convex@1.28.0(react@18.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.4
|
esbuild: 0.25.4
|
||||||
jwt-decode: 4.0.0
|
|
||||||
prettier: 3.6.2
|
prettier: 3.6.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
|
|
@ -7926,8 +7921,6 @@ snapshots:
|
||||||
object.assign: 4.1.7
|
object.assign: 4.1.7
|
||||||
object.values: 1.2.1
|
object.values: 1.2.1
|
||||||
|
|
||||||
jwt-decode@4.0.0: {}
|
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,8 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
credentials: "include",
|
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 {
|
} else {
|
||||||
const r = await fetch(`/api/admin/companies`, {
|
const r = await fetch(`/api/admin/companies`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -146,14 +147,16 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
credentials: "include",
|
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()
|
await refresh()
|
||||||
resetForm()
|
resetForm()
|
||||||
setEditingId(null)
|
setEditingId(null)
|
||||||
toast.success(editingId ? "Empresa atualizada" : "Empresa criada", { id: "companies" })
|
toast.success(editingId ? "Empresa atualizada" : "Empresa criada", { id: "companies" })
|
||||||
} catch {
|
} catch (error) {
|
||||||
toast.error("Não foi possível salvar", { id: "companies" })
|
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
|
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 DetailLineProps = { label: string; value?: string | number | null; classNameValue?: string }
|
||||||
|
|
||||||
type GpuAdapter = {
|
type GpuAdapter = {
|
||||||
|
|
@ -166,6 +182,13 @@ type MachineInventory = {
|
||||||
collaborator?: { email?: string; name?: string; role?: string }
|
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 {
|
function toRecord(value: unknown): Record<string, unknown> | null {
|
||||||
if (!value || typeof value !== "object") return null
|
if (!value || typeof value !== "object") return null
|
||||||
return value as Record<string, unknown>
|
return value as Record<string, unknown>
|
||||||
|
|
@ -278,6 +301,11 @@ type WindowsOsInfo = {
|
||||||
currentBuildNumber?: string
|
currentBuildNumber?: string
|
||||||
licenseStatus?: number
|
licenseStatus?: number
|
||||||
isActivated?: boolean
|
isActivated?: boolean
|
||||||
|
licenseStatusText?: string
|
||||||
|
productId?: string
|
||||||
|
partialProductKey?: string
|
||||||
|
computerName?: string
|
||||||
|
registeredOwner?: string
|
||||||
installDate?: Date | null
|
installDate?: Date | null
|
||||||
experience?: string
|
experience?: string
|
||||||
}
|
}
|
||||||
|
|
@ -309,17 +337,35 @@ function parseWindowsOsInfo(raw: unknown): WindowsOsInfo | null {
|
||||||
undefined
|
undefined
|
||||||
const ubrRaw = readFlexible("UBR")
|
const ubrRaw = readFlexible("UBR")
|
||||||
const licenseStatus = readNum("LicenseStatus", "licenseStatus")
|
const licenseStatus = readNum("LicenseStatus", "licenseStatus")
|
||||||
|
const licenseStatusTextRaw = readFlexible(
|
||||||
|
"LicenseStatusDescription",
|
||||||
|
"licenseStatusDescription",
|
||||||
|
"StatusDescription",
|
||||||
|
"statusDescription",
|
||||||
|
"Status",
|
||||||
|
"status"
|
||||||
|
)
|
||||||
|
const licenseStatusText = licenseStatusTextRaw ? licenseStatusTextRaw.trim() : undefined
|
||||||
const currentBuildNumber =
|
const currentBuildNumber =
|
||||||
baseBuild && ubrRaw && /^\d+$/.test(ubrRaw) ? `${baseBuild}.${ubrRaw}` : baseBuild ?? readFlexible("BuildNumber", "buildNumber")
|
baseBuild && ubrRaw && /^\d+$/.test(ubrRaw) ? `${baseBuild}.${ubrRaw}` : baseBuild ?? readFlexible("BuildNumber", "buildNumber")
|
||||||
const currentBuild = baseBuild
|
const currentBuild = baseBuild
|
||||||
|
|
||||||
const isActivatedRaw = value["IsActivated"] ?? value["isActivated"]
|
const isActivatedRaw = value["IsActivated"] ?? value["isActivated"]
|
||||||
|
const isLicensedRaw = value["IsLicensed"] ?? value["isLicensed"]
|
||||||
const isActivated =
|
const isActivated =
|
||||||
typeof isActivatedRaw === "boolean"
|
typeof isActivatedRaw === "boolean"
|
||||||
? isActivatedRaw
|
? isActivatedRaw
|
||||||
|
: typeof isActivatedRaw === "number"
|
||||||
|
? isActivatedRaw === 1
|
||||||
: typeof isActivatedRaw === "string"
|
: typeof isActivatedRaw === "string"
|
||||||
? isActivatedRaw.toLowerCase() === "true"
|
? 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 =
|
const installDate =
|
||||||
parseWindowsInstallDate(value["InstallDate"]) ??
|
parseWindowsInstallDate(value["InstallDate"]) ??
|
||||||
|
|
@ -331,6 +377,11 @@ function parseWindowsOsInfo(raw: unknown): WindowsOsInfo | null {
|
||||||
readFlexible("Experience", "experience", "FeatureExperiencePack", "featureExperiencePack") ??
|
readFlexible("Experience", "experience", "FeatureExperiencePack", "featureExperiencePack") ??
|
||||||
(currentBuildNumber ? `OS Build ${currentBuildNumber}` : undefined)
|
(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 {
|
return {
|
||||||
productName,
|
productName,
|
||||||
editionId,
|
editionId,
|
||||||
|
|
@ -341,6 +392,11 @@ function parseWindowsOsInfo(raw: unknown): WindowsOsInfo | null {
|
||||||
currentBuildNumber,
|
currentBuildNumber,
|
||||||
licenseStatus,
|
licenseStatus,
|
||||||
isActivated,
|
isActivated,
|
||||||
|
licenseStatusText,
|
||||||
|
productId,
|
||||||
|
partialProductKey,
|
||||||
|
computerName,
|
||||||
|
registeredOwner,
|
||||||
installDate,
|
installDate,
|
||||||
experience,
|
experience,
|
||||||
}
|
}
|
||||||
|
|
@ -363,6 +419,39 @@ function parseWindowsOsInfo(raw: unknown): WindowsOsInfo | null {
|
||||||
return parseRecord(record)
|
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 {
|
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") {
|
||||||
|
|
@ -552,6 +641,12 @@ function formatPostureAlertKind(raw?: string | null): string {
|
||||||
.replace(/\b\w/g, (char) => char.toUpperCase())
|
.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) {
|
function formatRelativeTime(date?: Date | null) {
|
||||||
if (!date) return "Nunca"
|
if (!date) return "Nunca"
|
||||||
try {
|
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) {
|
function formatDate(date?: Date | null) {
|
||||||
if (!date) return "—"
|
if (!date) return "—"
|
||||||
return format(date, "dd/MM/yyyy HH:mm")
|
return format(date, "dd/MM/yyyy HH:mm")
|
||||||
|
|
@ -825,6 +925,14 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
api.companies.list,
|
api.companies.list,
|
||||||
companyQueryArgs ?? ("skip" as const)
|
companyQueryArgs ?? ("skip" as const)
|
||||||
) as Array<{ id: string; name: string; slug?: string }> | undefined
|
) 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 metadata = machine?.inventory ?? null
|
||||||
const metrics = machine?.metrics ?? null
|
const metrics = machine?.metrics ?? null
|
||||||
const hardware = metadata?.hardware
|
const hardware = metadata?.hardware
|
||||||
|
|
@ -878,11 +986,49 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
if (windowsSoftwareRaw && typeof windowsSoftwareRaw === "object") return [windowsSoftwareRaw]
|
if (windowsSoftwareRaw && typeof windowsSoftwareRaw === "object") return [windowsSoftwareRaw]
|
||||||
return []
|
return []
|
||||||
}, [windowsSoftwareRaw])
|
}, [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 windowsEditionLabel = windowsOsInfo?.productName ?? windowsOsInfo?.editionId ?? null
|
||||||
const windowsVersionLabel = windowsOsInfo?.displayVersion ?? windowsOsInfo?.version ?? windowsOsInfo?.releaseId ?? null
|
const windowsVersionLabel = windowsOsInfo?.displayVersion ?? windowsOsInfo?.version ?? windowsOsInfo?.releaseId ?? null
|
||||||
const windowsBuildLabel = windowsOsInfo?.currentBuildNumber ?? windowsOsInfo?.currentBuild ?? null
|
const windowsBuildLabel = windowsOsInfo?.currentBuildNumber ?? windowsOsInfo?.currentBuild ?? null
|
||||||
const windowsInstallDateLabel = windowsOsInfo?.installDate ? formatAbsoluteDateTime(windowsOsInfo.installDate) : null
|
const windowsInstallDateLabel = windowsOsInfo?.installDate ? formatAbsoluteDateTime(windowsOsInfo.installDate) : null
|
||||||
const windowsExperienceLabel = windowsOsInfo?.experience ?? 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 linuxLsblk = linuxExt?.lsblk ?? []
|
||||||
const linuxSmartEntries = linuxExt?.smart ?? []
|
const linuxSmartEntries = linuxExt?.smart ?? []
|
||||||
const normalizedHardwareGpus = Array.isArray(hardware?.gpus)
|
const normalizedHardwareGpus = Array.isArray(hardware?.gpus)
|
||||||
|
|
@ -1019,8 +1165,8 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
}, [machine?.id])
|
}, [machine?.id])
|
||||||
|
|
||||||
const displayedWindowsSoftware = useMemo(
|
const displayedWindowsSoftware = useMemo(
|
||||||
() => (showAllWindowsSoftware ? windowsSoftware : windowsSoftware.slice(0, 8)),
|
() => (showAllWindowsSoftware ? normalizedWindowsSoftware : normalizedWindowsSoftware.slice(0, 12)),
|
||||||
[showAllWindowsSoftware, windowsSoftware]
|
[showAllWindowsSoftware, normalizedWindowsSoftware]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSaveAccess = async () => {
|
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">
|
<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>
|
<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">
|
<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="Edição" value={windowsEditionLabel ?? "—"} classNameValue="break-words" />
|
||||||
<DetailLine label="Versão" value={windowsVersionLabel ?? "—"} />
|
<DetailLine label="Versão" value={windowsVersionLabel ?? "—"} />
|
||||||
<DetailLine label="Compilação do SO" value={windowsBuildLabel ?? "—"} />
|
<DetailLine label="Compilação do SO" value={windowsBuildLabel ?? "—"} />
|
||||||
<DetailLine label="Instalado em" value={windowsInstallDateLabel ?? "—"} />
|
<DetailLine label="Instalado em" value={windowsInstallDateLabel ?? "—"} />
|
||||||
<DetailLine label="Experiência" value={windowsExperienceLabel ?? "—"} />
|
<DetailLine label="Experiência" value={windowsExperienceLabel ?? "—"} />
|
||||||
|
<DetailLine label="Status da licença" value={windowsLicenseStatusLabel ?? "—"} classNameValue="break-words" />
|
||||||
<DetailLine
|
<DetailLine
|
||||||
label="Ativação"
|
label="Ativação"
|
||||||
value={windowsActivationStatus == null ? "—" : windowsActivationStatus ? "Ativado" : "Não ativado"}
|
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 ?? "—"} />
|
<DetailLine label="Número de série" value={windowsSerialNumber ?? "—"} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1639,11 +1790,11 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{windowsSoftware.length > 0 ? (
|
{normalizedWindowsSoftware.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">
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
<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>
|
<p className="text-xs font-semibold uppercase text-slate-500">Aplicativos instalados</p>
|
||||||
{windowsSoftware.length > 8 ? (
|
{normalizedWindowsSoftware.length > 12 ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
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"
|
className="h-7 px-2 text-xs text-slate-600 hover:bg-slate-200/60"
|
||||||
onClick={() => setShowAllWindowsSoftware((prev) => !prev)}
|
onClick={() => setShowAllWindowsSoftware((prev) => !prev)}
|
||||||
>
|
>
|
||||||
{showAllWindowsSoftware ? "Mostrar menos" : `Ver todos (${windowsSoftware.length})`}
|
{showAllWindowsSoftware ? "Mostrar menos" : `Ver todos (${normalizedWindowsSoftware.length})`}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<ul className="mt-2 grid gap-1 text-xs text-muted-foreground">
|
<ul className="mt-2 space-y-2">
|
||||||
{displayedWindowsSoftware.map((softwareItem, index) => {
|
{displayedWindowsSoftware.map((softwareItem, index) => {
|
||||||
const record = toRecord(softwareItem) ?? {}
|
const initials = collectInitials(softwareItem.name)
|
||||||
const name = readString(record, "DisplayName", "name") ?? "—"
|
const installedAt = formatInstallDate(softwareItem.installDate)
|
||||||
const version = readString(record, "DisplayVersion", "version")
|
|
||||||
const publisher = readString(record, "Publisher")
|
|
||||||
return (
|
return (
|
||||||
<li key={`sw-${index}`}>
|
<li key={`sw-${index}`} className="flex items-start gap-3 rounded-md border border-slate-200 bg-white px-3 py-2">
|
||||||
<span className="font-medium text-foreground">{name}</span>
|
<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">
|
||||||
{version ? <span className="ml-1">{version}</span> : null}
|
{initials}
|
||||||
{publisher ? <span className="ml-1">· {publisher}</span> : null}
|
</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>
|
</li>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
@ -1848,9 +2011,13 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
<h4 className="text-sm font-semibold">Alertas de postura</h4>
|
<h4 className="text-sm font-semibold">Alertas de postura</h4>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{machine?.postureAlerts?.map((a: { kind?: string; message?: string; severity?: string }, i: number) => (
|
{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",
|
<div
|
||||||
(a?.severity ?? "warning").toLowerCase() === "critical" ? "border-rose-500/20 bg-rose-500/10" : "border-amber-500/20 bg-amber-500/10")
|
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>
|
<span className="font-medium text-foreground">{a?.message ?? formatPostureAlertKind(a?.kind)}</span>
|
||||||
<Badge variant="outline">{formatPostureAlertKind(a?.kind)}</Badge>
|
<Badge variant="outline">{formatPostureAlertKind(a?.kind)}</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1862,6 +2029,40 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : 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">
|
<div className="flex flex-wrap gap-2 pt-2">
|
||||||
{Array.isArray(software) && software.length > 0 ? (
|
{Array.isArray(software) && software.length > 0 ? (
|
||||||
<Button size="sm" variant="outline" onClick={() => exportCsv(software, "softwares.csv")}>Softwares CSV</Button>
|
<Button size="sm" variant="outline" onClick={() => exportCsv(software, "softwares.csv")}>Softwares CSV</Button>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue