feat: adiciona informacoes de reinicio e melhora SLA global
All checks were successful
CI/CD Web + Desktop / Detect changes (push) Successful in 4s
CI/CD Web + Desktop / Deploy (VPS Linux) (push) Successful in 3m0s
Quality Checks / Lint, Test and Build (push) Successful in 3m29s
CI/CD Web + Desktop / Deploy Convex functions (push) Successful in 1m24s

- Agente Rust: captura LastBootTime, uptime e contagem de boots
- Backend: extrai campos do extended (bootInfo, discos, RAM, etc) antes de salvar
- Frontend /devices: exibe secao de ultimo reinicio
- SLA global: adiciona campos de modo, threshold de alerta e status de pausa
- Corrige acento em "destinatario" -> "destinatario" em automations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rever-tecnologia 2025-12-18 09:38:58 -03:00
parent d32b94c22d
commit f39bd46c2b
7 changed files with 338 additions and 19 deletions

View file

@ -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) {
)}
</div>
{/* Softwares instalados */}
<div className="space-y-3 border-t border-slate-100 pt-5">
<DeviceSoftwareList machineId={device.id as Id<"machines">} />
</div>
{/* Campos personalizados */}
<div className="space-y-3 border-t border-slate-100 pt-5">
<div className="flex flex-wrap items-start justify-between gap-3">
@ -4346,6 +4353,11 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
</div>
) : null}
</div>
{/* Softwares instalados */}
<div className="space-y-3 border-t border-slate-100 pt-5">
<DeviceSoftwareList machineId={device.id as Id<"machines">} />
</div>
</section>
<section className="space-y-3 border-t border-slate-100 pt-6">
@ -5721,6 +5733,33 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
</div>
) : null}
{/* Último reinício */}
{windowsBootInfo ? (
<div className="rounded-md border border-slate-200 bg-slate-50/60 p-3">
<p className="text-xs font-semibold uppercase text-slate-500">Último reinício</p>
<div className="mt-2 grid gap-1 text-sm text-muted-foreground">
<DetailLine
label="Data/Hora"
value={windowsBootInfo.LastBootTime
? format(new Date(windowsBootInfo.LastBootTime), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })
: "—"
}
/>
<DetailLine
label="Tempo ligado"
value={windowsBootInfo.UptimeSeconds
? formatDistanceToNowStrict(Date.now() - (windowsBootInfo.UptimeSeconds * 1000), { locale: ptBR, addSuffix: false })
: "—"
}
/>
<DetailLine
label="Reinícios (30 dias)"
value={windowsBootInfo.BootCountLast30Days?.toString() ?? "—"}
/>
</div>
</div>
) : null}
</div>
) : null}

View file

@ -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<TimeUnit>("hours")
const [responseMode, setResponseMode] = useState<TimeMode>("calendar")
const [resolutionAmount, setResolutionAmount] = useState("")
const [resolutionUnit, setResolutionUnit] = useState<TimeUnit>("hours")
const [solutionMode, setSolutionMode] = useState<TimeMode>("calendar")
const [alertThreshold, setAlertThreshold] = useState(80)
const [pauseStatuses, setPauseStatuses] = useState<string[]>(["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() {
<Card className="border-slate-200">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-neutral-600">
<IconBolt className="size-4" /> Melhor resolucao
<IconBolt className="size-4" /> Melhor resolução
</CardTitle>
<CardDescription>Menor meta para encerrar chamados.</CardDescription>
</CardHeader>
@ -298,9 +352,24 @@ export function SlasManager() {
{policy.description && (
<p className="text-xs text-neutral-500">{policy.description}</p>
)}
<div className="flex gap-4 text-xs text-neutral-600">
<span>Resposta: {formatMinutes(policy.timeToFirstResponse)}</span>
<span>Resolução: {formatMinutes(policy.timeToResolution)}</span>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-neutral-600">
<span>
Resposta: {formatMinutes(policy.timeToFirstResponse)}
{policy.timeToFirstResponse !== null && (
<span className="ml-1 text-neutral-400">
({policy.responseMode === "business" ? "horas úteis" : "corrido"})
</span>
)}
</span>
<span>
Resolução: {formatMinutes(policy.timeToResolution)}
{policy.timeToResolution !== null && (
<span className="ml-1 text-neutral-400">
({policy.solutionMode === "business" ? "horas úteis" : "corrido"})
</span>
)}
</span>
<span>Alerta: {Math.round(policy.alertThreshold * 100)}%</span>
</div>
</div>
<div className="flex items-center gap-2">
@ -382,6 +451,18 @@ export function SlasManager() {
</SelectContent>
</Select>
</div>
<Select value={responseMode} onValueChange={(value) => setResponseMode(value as TimeMode)}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{MODE_OPTIONS.map((mode) => (
<SelectItem key={mode.value} value={mode.value}>
{mode.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Tempo para resolução</Label>
@ -408,6 +489,61 @@ export function SlasManager() {
</SelectContent>
</Select>
</div>
<Select value={solutionMode} onValueChange={(value) => setSolutionMode(value as TimeMode)}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{MODE_OPTIONS.map((mode) => (
<SelectItem key={mode.value} value={mode.value}>
{mode.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Alerta de proximidade</Label>
<p className="text-xs text-neutral-500">
Percentual do tempo em que o sistema emitirá alerta antes do vencimento.
</p>
<div className="flex items-center gap-2">
<Input
type="number"
min={10}
max={95}
value={alertThreshold}
onChange={(event) => setAlertThreshold(Number(event.target.value))}
className="w-20"
/>
<span className="text-sm text-neutral-500">%</span>
</div>
</div>
<div className="space-y-2">
<Label>Status que pausam o SLA</Label>
<p className="text-xs text-neutral-500">
Quando o chamado estiver em um desses status, o cronômetro do SLA será pausado.
</p>
<div className="flex flex-wrap gap-2">
{PAUSE_STATUS_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
onClick={() => togglePauseStatus(option.value)}
className={cn(
"rounded-full px-3 py-1 text-xs font-medium transition-colors",
pauseStatuses.includes(option.value)
? "bg-neutral-900 text-white"
: "bg-slate-100 text-neutral-600 hover:bg-slate-200"
)}
>
{option.label}
</button>
))}
</div>
</div>
</div>
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3">

View file

@ -598,7 +598,7 @@ export function EmailActionConfig({ action, onChange, onRemove, agents }: EmailA
<SelectValue />
</SelectTrigger>
<SelectContent className="rounded-xl">
<SelectItem value="AUTO">Auto (detecta pelo destinatario)</SelectItem>
<SelectItem value="AUTO">Auto (detecta pelo destinatário)</SelectItem>
<SelectItem value="PORTAL">Portal (cliente)</SelectItem>
<SelectItem value="STAFF">Painel (agente)</SelectItem>
</SelectContent>