diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx index 256bbde..a4311e5 100644 --- a/apps/desktop/src/main.tsx +++ b/apps/desktop/src/main.tsx @@ -188,6 +188,7 @@ function App() { const [isMachineActive, setIsMachineActive] = useState(true) const [showSecret, setShowSecret] = useState(false) const [isLaunchingSystem, setIsLaunchingSystem] = useState(false) + const [tokenValidationTick, setTokenValidationTick] = useState(0) const [, setIsValidatingToken] = useState(false) const tokenVerifiedRef = useRef(false) @@ -246,6 +247,7 @@ function App() { if (res.ok) { tokenVerifiedRef.current = true setStatus("online") + setTokenValidationTick((tick) => tick + 1) try { await invoke("start_machine_agent", { baseUrl: apiBaseUrl, @@ -290,9 +292,15 @@ function App() { } else { // Não limpa token em falhas genéricas (ex.: rede); apenas informa setError("Falha ao validar sessão da máquina. Tente novamente.") + tokenVerifiedRef.current = true + setTokenValidationTick((tick) => tick + 1) } } catch (err) { - if (!cancelled) console.error("Falha ao validar token (rede)", err) + if (!cancelled) { + console.error("Falha ao validar token (rede)", err) + tokenVerifiedRef.current = true + setTokenValidationTick((tick) => tick + 1) + } } finally { if (!cancelled) setIsValidatingToken(false) } @@ -731,7 +739,7 @@ function App() { autoLaunchRef.current = true setIsLaunchingSystem(true) openSystem() - }, [token, status, config?.accessRole, openSystem]) + }, [token, status, config?.accessRole, openSystem, tokenValidationTick]) if (isLaunchingSystem && token) { return ( diff --git a/src/components/admin/machines/admin-machines-overview.tsx b/src/components/admin/machines/admin-machines-overview.tsx index 696702b..7372d4d 100644 --- a/src/components/admin/machines/admin-machines-overview.tsx +++ b/src/components/admin/machines/admin-machines-overview.tsx @@ -680,6 +680,50 @@ function formatAbsoluteDateTime(date?: Date | null) { }).format(date) } +function parseDateish(value: unknown): Date | null { + if (!value) return null + if (value instanceof Date && !Number.isNaN(value.getTime())) return value + if (typeof value === "number" && Number.isFinite(value)) { + const numeric = value > 1e12 ? value : value > 1e9 ? value * 1000 : value + const date = new Date(numeric) + return Number.isNaN(date.getTime()) ? null : date + } + if (typeof value === "string") { + const trimmed = value.trim() + if (!trimmed) return null + const numericValue = Number(trimmed) + if (Number.isFinite(numericValue)) { + const asMs = trimmed.length >= 13 ? numericValue : numericValue * 1000 + const date = new Date(asMs) + if (!Number.isNaN(date.getTime())) return date + } + const date = new Date(trimmed) + if (!Number.isNaN(date.getTime())) return date + } + return null +} + +function getMetricsTimestamp(metrics: MachineMetrics): Date | null { + if (!metrics || typeof metrics !== "object") return null + const data = metrics as Record + const candidates = [ + data["collectedAt"], + data["collected_at"], + data["collected_at_iso"], + data["collected_at_ms"], + data["timestamp"], + data["updatedAt"], + data["updated_at"], + data["createdAt"], + data["created_at"], + ] + for (const candidate of candidates) { + const parsed = parseDateish(candidate) + if (parsed) return parsed + } + return null +} + function formatBytes(bytes?: number | null) { if (!bytes || Number.isNaN(bytes)) return "—" const units = ["B", "KB", "MB", "GB", "TB"] @@ -930,6 +974,8 @@ export function MachineDetails({ machine }: MachineDetailsProps) { const { convexUserId } = useAuth() const router = useRouter() const effectiveStatus = machine ? resolveMachineStatus(machine) : "unknown" + const isActive = machine?.isActive ?? true + const isDeactivated = !isActive || effectiveStatus === "deactivated" // Company name lookup (by slug) const companyQueryArgs = convexUserId && machine ? { tenantId: machine.tenantId, viewerId: convexUserId as Id<"users"> } : undefined const companies = useQuery( @@ -946,6 +992,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) { const machineAlertsHistory = alertsHistory ?? [] const metadata = machine?.inventory ?? null const metrics = machine?.metrics ?? null + const metricsCapturedAt = useMemo(() => getMetricsTimestamp(metrics), [metrics]) const hardware = metadata?.hardware const network = metadata?.network ?? null const networkInterfaces = Array.isArray(network) ? network : null @@ -972,7 +1019,6 @@ export function MachineDetails({ machine }: MachineDetailsProps) { ? windowsBaseboardRaw : null const windowsSerialNumber = windowsBaseboard ? readString(toRecord(windowsBaseboard) ?? {}, "SerialNumber", "serialNumber") : undefined - const isActive = machine?.isActive ?? true const windowsMemoryModules = useMemo(() => { if (Array.isArray(windowsMemoryModulesRaw)) return windowsMemoryModulesRaw if (windowsMemoryModulesRaw && typeof windowsMemoryModulesRaw === "object") return [windowsMemoryModulesRaw] @@ -1307,9 +1353,9 @@ export function MachineDetails({ machine }: MachineDetailsProps) { return ( - + Detalhes - Resumo da máquina selecionada. + Resumo da máquina selecionada {machine ? (
@@ -1318,7 +1364,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) { {companyName}
) : null} - + {!isDeactivated ? : null} {!isActive ? ( Máquina desativada @@ -1499,7 +1545,14 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
-

Métricas recentes

+
+

Métricas recentes

+ {metricsCapturedAt ? ( + + Última atualização {formatRelativeTime(metricsCapturedAt)} + + ) : null} +
diff --git a/src/components/portal/portal-ticket-detail.tsx b/src/components/portal/portal-ticket-detail.tsx index ec60848..b0679a6 100644 --- a/src/components/portal/portal-ticket-detail.tsx +++ b/src/components/portal/portal-ticket-detail.tsx @@ -328,14 +328,14 @@ export function PortalTicketDetail({ ticketId }: PortalTicketDetailProps) { ) : null}
-
@@ -610,6 +610,8 @@ function PortalCommentAttachmentCard({ const handleDownload = useCallback(async () => { const target = await ensureUrl() if (!target) return + const toastId = `portal-attachment-download-${attachment.id}` + toast.loading("Baixando anexo...", { id: toastId }) try { const response = await fetch(target, { credentials: "include" }) if (!response.ok) { @@ -627,9 +629,11 @@ function PortalCommentAttachmentCard({ window.setTimeout(() => { window.URL.revokeObjectURL(blobUrl) }, 1000) + toast.success("Download concluído!", { id: toastId }) } catch (error) { console.error("Falha ao iniciar download do anexo", error) window.open(target, "_blank", "noopener,noreferrer") + toast.error("Não foi possível baixar o anexo automaticamente.", { id: toastId }) } }, [attachment.name, ensureUrl]) diff --git a/src/components/tickets/ticket-comments.rich.tsx b/src/components/tickets/ticket-comments.rich.tsx index 23f2bc3..8eafed7 100644 --- a/src/components/tickets/ticket-comments.rich.tsx +++ b/src/components/tickets/ticket-comments.rich.tsx @@ -613,6 +613,8 @@ function CommentAttachmentCard({ const handleDownload = useCallback(async () => { const target = url ?? (await ensureUrl()) if (!target) return + const toastId = `attachment-download-${attachment.id}` + toast.loading("Baixando anexo...", { id: toastId }) try { const response = await fetch(target, { credentials: "include" }) if (!response.ok) { @@ -630,9 +632,11 @@ function CommentAttachmentCard({ window.setTimeout(() => { window.URL.revokeObjectURL(blobUrl) }, 1000) + toast.success("Download concluído!", { id: toastId }) } catch (error) { console.error("Failed to download attachment", error) window.open(target, "_blank", "noopener,noreferrer") + toast.error("Não foi possível baixar o anexo automaticamente.", { id: toastId }) } }, [attachment.name, ensureUrl, url]) diff --git a/src/components/tickets/ticket-details-panel.tsx b/src/components/tickets/ticket-details-panel.tsx index 5c8a052..4b40294 100644 --- a/src/components/tickets/ticket-details-panel.tsx +++ b/src/components/tickets/ticket-details-panel.tsx @@ -16,6 +16,9 @@ const tagBadgeClass = "inline-flex items-center gap-1 rounded-full border border const iconAccentClass = "size-3 text-neutral-700" export function TicketDetailsPanel({ ticket }: TicketDetailsPanelProps) { + const isAvulso = Boolean(ticket.company?.isAvulso) + const companyLabel = ticket.company?.name ?? (isAvulso ? "Cliente avulso" : "Sem empresa vinculada") + return ( @@ -27,6 +30,11 @@ export function TicketDetailsPanel({ ticket }: TicketDetailsPanelProps) { {ticket.queue ?? "Sem fila"} +
+

Empresa

+ {companyLabel} +
+

SLA

{ticket.slaPolicy ? ( diff --git a/src/components/tickets/ticket-summary-header.tsx b/src/components/tickets/ticket-summary-header.tsx index 5de8bd6..46736af 100644 --- a/src/components/tickets/ticket-summary-header.tsx +++ b/src/components/tickets/ticket-summary-header.tsx @@ -165,7 +165,12 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { const queueDirty = useMemo(() => queueSelection !== currentQueueName, [queueSelection, currentQueueName]) const assigneeDirty = useMemo(() => assigneeSelection !== currentAssigneeId, [assigneeSelection, currentAssigneeId]) const formDirty = dirty || categoryDirty || queueDirty || assigneeDirty - + const companyLabel = useMemo(() => { + if (ticket.company?.name) return ticket.company.name + if (isAvulso) return "Cliente avulso" + return "Sem empresa vinculada" + }, [ticket.company?.name, isAvulso]) + const activeCategory = useMemo( () => categories.find((category) => category.id === selectedCategoryId) ?? null, [categories, selectedCategoryId] @@ -726,6 +731,10 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { Solicitante {ticket.requester.name}
+
+ Empresa + {companyLabel} +
Responsável {editing ? (