diff --git a/apps/desktop/src-tauri/src/agent.rs b/apps/desktop/src-tauri/src/agent.rs index a49292b..1d42d23 100644 --- a/apps/desktop/src-tauri/src/agent.rs +++ b/apps/desktop/src-tauri/src/agent.rs @@ -1099,6 +1099,41 @@ fn collect_windows_extended() -> serde_json::Value { } "#).unwrap_or_else(|| json!({})); + // Último reinício e contagem de boots + let boot_info = ps(r#" + $os = Get-CimInstance Win32_OperatingSystem | Select-Object LastBootUpTime + $lastBoot = $os.LastBootUpTime + + # Calcula uptime + $uptime = if ($lastBoot) { (New-TimeSpan -Start $lastBoot -End (Get-Date)).TotalSeconds } else { 0 } + + # Conta eventos de boot (ID 6005) - últimos 30 dias para performance + $startDate = (Get-Date).AddDays(-30) + $bootEvents = @() + $bootCount = 0 + try { + $events = Get-WinEvent -FilterHashtable @{ + LogName = 'System' + ID = 6005 + StartTime = $startDate + } -MaxEvents 50 -ErrorAction SilentlyContinue + $bootCount = @($events).Count + $bootEvents = @($events | Select-Object -First 10 | ForEach-Object { + @{ + TimeCreated = $_.TimeCreated.ToString('o') + Computer = $_.MachineName + } + }) + } catch {} + + [PSCustomObject]@{ + LastBootTime = if ($lastBoot) { $lastBoot.ToString('o') } else { $null } + UptimeSeconds = [math]::Round($uptime) + BootCountLast30Days = $bootCount + RecentBoots = $bootEvents + } + "#).unwrap_or_else(|| json!({ "LastBootTime": null, "UptimeSeconds": 0, "BootCountLast30Days": 0, "RecentBoots": [] })); + json!({ "windows": { "software": software, @@ -1125,6 +1160,7 @@ fn collect_windows_extended() -> serde_json::Value { "networkAdapters": network_adapters, "monitors": monitors, "chassis": power_supply, + "bootInfo": boot_info, } }) } diff --git a/convex/machines.ts b/convex/machines.ts index 69c8b61..091eca1 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -331,9 +331,59 @@ async function getMachineLastHeartbeat( return hb?.lastHeartbeatAt ?? fallback ?? null } -// Campos do inventory que sao muito grandes e nao devem ser persistidos -// para evitar OOM no Convex (documentos de ~100KB cada) -const INVENTORY_BLOCKLIST = new Set(["software", "extended"]) +// Campo software é muito grande e é tratado separadamente via machineSoftware + +// Extrai campos importantes do extended antes de bloqueá-lo +function extractFromExtended(extended: unknown): JsonRecord { + const result: JsonRecord = {} + const sanitizedExtended = sanitizeRecord(extended) + if (!sanitizedExtended) return result + + // Extrair dados do Windows + const windows = sanitizeRecord(sanitizedExtended["windows"]) + if (windows) { + const windowsFields: JsonRecord = {} + // bootInfo - informacoes de reinicio + if (windows["bootInfo"]) { + windowsFields["bootInfo"] = windows["bootInfo"] as JsonValue + } + // osInfo - informacoes do sistema operacional + if (windows["osInfo"]) { + windowsFields["osInfo"] = windows["osInfo"] as JsonValue + } + // cpu, baseboard, bios, memoryModules, videoControllers, disks + for (const key of ["cpu", "baseboard", "bios", "memoryModules", "videoControllers", "disks", "bitLocker", "tpm", "secureBoot", "deviceGuard", "firewallProfiles", "windowsUpdate", "computerSystem", "azureAdStatus", "battery", "thermal", "networkAdapters", "monitors", "chassis", "defender", "hotfix"]) { + if (windows[key]) { + windowsFields[key] = windows[key] as JsonValue + } + } + if (Object.keys(windowsFields).length > 0) { + result["windows"] = windowsFields + } + } + + // Extrair dados do Linux + const linux = sanitizeRecord(sanitizedExtended["linux"]) + if (linux) { + const linuxFields: JsonRecord = {} + for (const key of ["lsblk", "smart", "lspci", "lsusb", "dmidecode"]) { + if (linux[key]) { + linuxFields[key] = linux[key] as JsonValue + } + } + if (Object.keys(linuxFields).length > 0) { + result["linux"] = linuxFields + } + } + + // Extrair dados do macOS + const macos = sanitizeRecord(sanitizedExtended["macos"]) + if (macos) { + result["macos"] = macos as JsonValue + } + + return result +} function mergeInventory(current: JsonRecord | null | undefined, patch: Record): JsonRecord { const sanitizedPatch = sanitizeRecord(patch) @@ -341,9 +391,10 @@ function mergeInventory(current: JsonRecord | null | undefined, patch: Record) @@ -393,9 +444,20 @@ function ensureString(value: unknown): string | null { function sanitizeInventoryPayload(value: unknown): JsonRecord | null { const record = sanitizeRecord(value) if (!record) return null - for (const blocked of INVENTORY_BLOCKLIST) { - delete record[blocked] + + // Extrair campos importantes do extended antes de deletá-lo + if (record["extended"]) { + const extractedExtended = extractFromExtended(record["extended"]) + if (Object.keys(extractedExtended).length > 0) { + record["extended"] = extractedExtended + } else { + delete record["extended"] + } } + + // Deletar apenas software (extended já foi processado acima) + delete record["software"] + return record } diff --git a/convex/schema.ts b/convex/schema.ts index c8f9099..9e3502a 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -200,7 +200,11 @@ export default defineSchema({ name: v.string(), description: v.optional(v.string()), timeToFirstResponse: v.optional(v.number()), // minutes + responseMode: v.optional(v.string()), // "business" | "calendar" timeToResolution: v.optional(v.number()), // minutes + solutionMode: v.optional(v.string()), // "business" | "calendar" + alertThreshold: v.optional(v.number()), // 0.1 a 0.95 + pauseStatuses: v.optional(v.array(v.string())), // Status que pausam SLA }).index("by_tenant_name", ["tenantId", "name"]), tickets: defineTable({ diff --git a/convex/slas.ts b/convex/slas.ts index 32ab0a5..27f6645 100644 --- a/convex/slas.ts +++ b/convex/slas.ts @@ -9,6 +9,26 @@ function normalizeName(value: string) { return value.trim(); } +function normalizeMode(value?: string): "business" | "calendar" { + if (value === "business") return "business"; + return "calendar"; +} + +function normalizeThreshold(value?: number): number { + if (value === undefined || value === null) return 0.8; + if (value < 0.1) return 0.1; + if (value > 0.95) return 0.95; + return value; +} + +const VALID_PAUSE_STATUSES = ["PAUSED", "PENDING", "AWAITING_ATTENDANCE"] as const; + +function normalizePauseStatuses(statuses?: string[]): string[] { + if (!statuses || statuses.length === 0) return ["PAUSED"]; + const filtered = statuses.filter((s) => VALID_PAUSE_STATUSES.includes(s as typeof VALID_PAUSE_STATUSES[number])); + return filtered.length > 0 ? filtered : ["PAUSED"]; +} + type AnyCtx = QueryCtx | MutationCtx; async function ensureUniqueName(ctx: AnyCtx, tenantId: string, name: string, excludeId?: Id<"slaPolicies">) { @@ -35,7 +55,11 @@ export const list = query({ name: policy.name, description: policy.description ?? "", timeToFirstResponse: policy.timeToFirstResponse ?? null, + responseMode: policy.responseMode ?? "calendar", timeToResolution: policy.timeToResolution ?? null, + solutionMode: policy.solutionMode ?? "calendar", + alertThreshold: policy.alertThreshold ?? 0.8, + pauseStatuses: policy.pauseStatuses ?? ["PAUSED"], })); }, }); @@ -47,9 +71,14 @@ export const create = mutation({ name: v.string(), description: v.optional(v.string()), timeToFirstResponse: v.optional(v.number()), + responseMode: v.optional(v.string()), timeToResolution: v.optional(v.number()), + solutionMode: v.optional(v.string()), + alertThreshold: v.optional(v.number()), + pauseStatuses: v.optional(v.array(v.string())), }, - handler: async (ctx, { tenantId, actorId, name, description, timeToFirstResponse, timeToResolution }) => { + handler: async (ctx, args) => { + const { tenantId, actorId, name, description, timeToFirstResponse, responseMode, timeToResolution, solutionMode, alertThreshold, pauseStatuses } = args; await requireAdmin(ctx, actorId, tenantId); const trimmed = normalizeName(name); if (trimmed.length < 2) { @@ -68,7 +97,11 @@ export const create = mutation({ name: trimmed, description, timeToFirstResponse, + responseMode: normalizeMode(responseMode), timeToResolution, + solutionMode: normalizeMode(solutionMode), + alertThreshold: normalizeThreshold(alertThreshold), + pauseStatuses: normalizePauseStatuses(pauseStatuses), }); return id; }, @@ -82,9 +115,14 @@ export const update = mutation({ name: v.string(), description: v.optional(v.string()), timeToFirstResponse: v.optional(v.number()), + responseMode: v.optional(v.string()), timeToResolution: v.optional(v.number()), + solutionMode: v.optional(v.string()), + alertThreshold: v.optional(v.number()), + pauseStatuses: v.optional(v.array(v.string())), }, - handler: async (ctx, { policyId, tenantId, actorId, name, description, timeToFirstResponse, timeToResolution }) => { + handler: async (ctx, args) => { + const { policyId, tenantId, actorId, name, description, timeToFirstResponse, responseMode, timeToResolution, solutionMode, alertThreshold, pauseStatuses } = args; await requireAdmin(ctx, actorId, tenantId); const policy = await ctx.db.get(policyId); if (!policy || policy.tenantId !== tenantId) { @@ -106,7 +144,11 @@ export const update = mutation({ name: trimmed, description, timeToFirstResponse, + responseMode: normalizeMode(responseMode), timeToResolution, + solutionMode: normalizeMode(solutionMode), + alertThreshold: normalizeThreshold(alertThreshold), + pauseStatuses: normalizePauseStatuses(pauseStatuses), }); }, }); diff --git a/src/components/admin/devices/admin-devices-overview.tsx b/src/components/admin/devices/admin-devices-overview.tsx index 65435bf..ac187b5 100644 --- a/src/components/admin/devices/admin-devices-overview.tsx +++ b/src/components/admin/devices/admin-devices-overview.tsx @@ -329,6 +329,16 @@ type WindowsChassis = { SMBIOSAssetTag?: string } +type WindowsBootInfo = { + LastBootTime?: string + UptimeSeconds?: number + BootCountLast30Days?: number + RecentBoots?: Array<{ + TimeCreated?: string + Computer?: string + }> +} + type WindowsExtended = { software?: DeviceSoftware[] services?: Array<{ name?: string; status?: string; displayName?: string }> @@ -355,6 +365,7 @@ type WindowsExtended = { networkAdapters?: WindowsNetworkAdapter[] monitors?: WindowsMonitor[] chassis?: WindowsChassis + bootInfo?: WindowsBootInfo } type MacExtended = { @@ -3093,6 +3104,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { return [] }, [windowsExt?.monitors]) const windowsChassisInfo = windowsExt?.chassis ?? null + const windowsBootInfo = windowsExt?.bootInfo ?? null const osNameDisplay = useMemo(() => { const base = device?.osName?.trim() const edition = windowsEditionLabel?.trim() @@ -4302,11 +4314,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { )} - {/* Softwares instalados */} -
- } /> -
- {/* Campos personalizados */}
@@ -4346,6 +4353,11 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
) : null}
+ + {/* Softwares instalados */} +
+ } /> +
@@ -5721,6 +5733,33 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { ) : null} + {/* Último reinício */} + {windowsBootInfo ? ( +
+

Último reinício

+
+ + + +
+
+ ) : null} + ) : null} diff --git a/src/components/admin/slas/slas-manager.tsx b/src/components/admin/slas/slas-manager.tsx index cfb7fe1..d3b2e6b 100644 --- a/src/components/admin/slas/slas-manager.tsx +++ b/src/components/admin/slas/slas-manager.tsx @@ -17,6 +17,7 @@ import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Skeleton } from "@/components/ui/skeleton" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { cn } from "@/lib/utils" import { CategorySlaManager } from "./category-sla-manager" import { CompanySlaManager } from "./company-sla-manager" @@ -26,10 +27,15 @@ type SlaPolicy = { name: string description: string timeToFirstResponse: number | null + responseMode: "business" | "calendar" timeToResolution: number | null + solutionMode: "business" | "calendar" + alertThreshold: number + pauseStatuses: string[] } type TimeUnit = "minutes" | "hours" | "days" +type TimeMode = "business" | "calendar" const TIME_UNITS: Array<{ value: TimeUnit; label: string; factor: number }> = [ { value: "minutes", label: "Minutos", factor: 1 }, @@ -37,6 +43,17 @@ const TIME_UNITS: Array<{ value: TimeUnit; label: string; factor: number }> = [ { value: "days", label: "Dias", factor: 1440 }, ] +const MODE_OPTIONS: Array<{ value: TimeMode; label: string }> = [ + { value: "calendar", label: "Horas corridas" }, + { value: "business", label: "Horas úteis" }, +] + +const PAUSE_STATUS_OPTIONS = [ + { value: "PENDING", label: "Pendente" }, + { value: "AWAITING_ATTENDANCE", label: "Em atendimento" }, + { value: "PAUSED", label: "Pausado" }, +] as const + function formatMinutes(value: number | null) { if (value === null) return "—" if (value < 60) return `${Math.round(value)} min` @@ -82,8 +99,12 @@ export function SlasManager() { const [description, setDescription] = useState("") const [responseAmount, setResponseAmount] = useState("") const [responseUnit, setResponseUnit] = useState("hours") + const [responseMode, setResponseMode] = useState("calendar") const [resolutionAmount, setResolutionAmount] = useState("") const [resolutionUnit, setResolutionUnit] = useState("hours") + const [solutionMode, setSolutionMode] = useState("calendar") + const [alertThreshold, setAlertThreshold] = useState(80) + const [pauseStatuses, setPauseStatuses] = useState(["PAUSED"]) const [saving, setSaving] = useState(false) const { bestFirstResponse, bestResolution } = useMemo(() => { @@ -107,8 +128,12 @@ export function SlasManager() { setDescription("") setResponseAmount("") setResponseUnit("hours") + setResponseMode("calendar") setResolutionAmount("") setResolutionUnit("hours") + setSolutionMode("calendar") + setAlertThreshold(80) + setPauseStatuses(["PAUSED"]) } const openCreateDialog = () => { @@ -125,11 +150,30 @@ export function SlasManager() { setDescription(policy.description) setResponseAmount(response.amount) setResponseUnit(response.unit) + setResponseMode(policy.responseMode ?? "calendar") setResolutionAmount(resolution.amount) setResolutionUnit(resolution.unit) + setSolutionMode(policy.solutionMode ?? "calendar") + setAlertThreshold(Math.round((policy.alertThreshold ?? 0.8) * 100)) + setPauseStatuses(policy.pauseStatuses ?? ["PAUSED"]) setDialogOpen(true) } + const togglePauseStatus = (status: string) => { + setPauseStatuses((current) => { + const selected = new Set(current) + if (selected.has(status)) { + selected.delete(status) + } else { + selected.add(status) + } + if (selected.size === 0) { + selected.add("PAUSED") + } + return Array.from(selected) + }) + } + const closeDialog = () => { setDialogOpen(false) setEditingSla(null) @@ -153,6 +197,8 @@ export function SlasManager() { const toastId = editingSla ? "sla-edit" : "sla-create" toast.loading(editingSla ? "Salvando alterações..." : "Criando política...", { id: toastId }) + const normalizedThreshold = Math.min(Math.max(alertThreshold, 10), 95) / 100 + try { if (editingSla) { await updateSla({ @@ -162,7 +208,11 @@ export function SlasManager() { name: name.trim(), description: description.trim() || undefined, timeToFirstResponse, + responseMode, timeToResolution, + solutionMode, + alertThreshold: normalizedThreshold, + pauseStatuses, }) toast.success("Política atualizada", { id: toastId }) } else { @@ -172,7 +222,11 @@ export function SlasManager() { name: name.trim(), description: description.trim() || undefined, timeToFirstResponse, + responseMode, timeToResolution, + solutionMode, + alertThreshold: normalizedThreshold, + pauseStatuses, }) toast.success("Política criada", { id: toastId }) } @@ -235,7 +289,7 @@ export function SlasManager() { - Melhor resolucao + Melhor resolução Menor meta para encerrar chamados. @@ -298,9 +352,24 @@ export function SlasManager() { {policy.description && (

{policy.description}

)} -
- Resposta: {formatMinutes(policy.timeToFirstResponse)} - Resolução: {formatMinutes(policy.timeToResolution)} +
+ + Resposta: {formatMinutes(policy.timeToFirstResponse)} + {policy.timeToFirstResponse !== null && ( + + ({policy.responseMode === "business" ? "horas úteis" : "corrido"}) + + )} + + + Resolução: {formatMinutes(policy.timeToResolution)} + {policy.timeToResolution !== null && ( + + ({policy.solutionMode === "business" ? "horas úteis" : "corrido"}) + + )} + + Alerta: {Math.round(policy.alertThreshold * 100)}%
@@ -382,6 +451,18 @@ export function SlasManager() {
+
@@ -408,6 +489,61 @@ export function SlasManager() {
+ + + + +
+
+ +

+ Percentual do tempo em que o sistema emitirá alerta antes do vencimento. +

+
+ setAlertThreshold(Number(event.target.value))} + className="w-20" + /> + % +
+
+
+ +

+ Quando o chamado estiver em um desses status, o cronômetro do SLA será pausado. +

+
+ {PAUSE_STATUS_OPTIONS.map((option) => ( + + ))} +
diff --git a/src/components/automations/email-action-config.tsx b/src/components/automations/email-action-config.tsx index 9f09210..d968302 100644 --- a/src/components/automations/email-action-config.tsx +++ b/src/components/automations/email-action-config.tsx @@ -598,7 +598,7 @@ export function EmailActionConfig({ action, onChange, onRemove, agents }: EmailA - Auto (detecta pelo destinatario) + Auto (detecta pelo destinatário) Portal (cliente) Painel (agente)