chore: prep platform improvements

This commit is contained in:
Esdras Renan 2025-11-09 21:09:38 -03:00
parent a62f3d5283
commit c5ddd54a3e
24 changed files with 777 additions and 649 deletions

View file

@ -42,7 +42,7 @@ import {
import { api } from "@/convex/_generated/api"
import { Badge } from "@/components/ui/badge"
import { Button, buttonVariants } from "@/components/ui/button"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Spinner } from "@/components/ui/spinner"
@ -468,67 +468,6 @@ export function normalizeDeviceRemoteAccessList(raw: unknown): DeviceRemoteAcces
return entries
}
const REMOTE_ACCESS_METADATA_IGNORED_KEYS = new Set([
"provider",
"tool",
"vendor",
"name",
"identifier",
"code",
"id",
"accessId",
"username",
"user",
"login",
"email",
"account",
"password",
"pass",
"secret",
"pin",
"url",
"link",
"remoteUrl",
"console",
"viewer",
"notes",
"note",
"description",
"obs",
"lastVerifiedAt",
"verifiedAt",
"checkedAt",
"updatedAt",
])
function extractRemoteAccessMetadataEntries(metadata: Record<string, unknown> | null | undefined) {
if (!metadata) return [] as Array<[string, unknown]>
return Object.entries(metadata).filter(([key, value]) => {
if (REMOTE_ACCESS_METADATA_IGNORED_KEYS.has(key)) return false
if (value === null || value === undefined) return false
if (typeof value === "string" && value.trim().length === 0) return false
return true
})
}
function formatRemoteAccessMetadataKey(key: string) {
return key
.replace(/[_.-]+/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase())
}
function formatRemoteAccessMetadataValue(value: unknown): string {
if (value === null || value === undefined) return ""
if (typeof value === "string") return value
if (typeof value === "number" || typeof value === "boolean") return String(value)
if (value instanceof Date) return formatAbsoluteDateTime(value)
try {
return JSON.stringify(value)
} catch {
return String(value)
}
}
function readText(record: Record<string, unknown>, ...keys: string[]): string | undefined {
const stringValue = readString(record, ...keys)
if (stringValue) return stringValue
@ -3029,7 +2968,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
const [deleteDialog, setDeleteDialog] = useState(false)
const [deleting, setDeleting] = useState(false)
const [accessDialog, setAccessDialog] = useState(false)
const [accessEmail, setAccessEmail] = useState<string>(primaryLinkedUser?.email ?? "")
const [accessEmail, setAccessEmail] = useState<string>("")
const [accessName, setAccessName] = useState<string>(primaryLinkedUser?.name ?? "")
const [accessRole, setAccessRole] = useState<"collaborator" | "manager">(personaRole === "manager" ? "manager" : "collaborator")
const [savingAccess, setSavingAccess] = useState(false)
@ -3091,10 +3030,13 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
// removed copy/export inventory JSON buttons as requested
useEffect(() => {
setAccessEmail(primaryLinkedUser?.email ?? "")
setAccessEmail("")
}, [device?.id])
useEffect(() => {
setAccessName(primaryLinkedUser?.name ?? "")
setAccessRole(personaRole === "manager" ? "manager" : "collaborator")
}, [device?.id, primaryLinkedUser?.email, primaryLinkedUser?.name, personaRole])
}, [device?.id, primaryLinkedUser?.name, personaRole])
useEffect(() => {
setIsActiveLocal(device?.isActive ?? true)
@ -3711,10 +3653,55 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
<InfoChip key={chip.key} label={chip.label} value={chip.value} icon={chip.icon} tone={chip.tone} />
))}
</div>
<div className="rounded-2xl border border-slate-200 bg-white/80 px-4 py-4 shadow-sm">
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">Controles do dispositivo</p>
{device.registeredBy ? (
<span className="text-xs font-medium text-slate-500">
Registrada via <span className="text-slate-800">{device.registeredBy}</span>
</span>
) : null}
</div>
<div className="mt-3 flex flex-wrap gap-2">
<Button size="sm" variant="outline" className="gap-2 border-dashed" onClick={() => { setAccessDialog(true) }}>
<ShieldCheck className="size-4" />
Ajustar acesso
</Button>
{!isManualMobile ? (
<>
<Button
size="sm"
variant="outline"
className="gap-2 border-dashed border-amber-300 text-amber-700 hover:border-amber-400 hover:text-amber-800"
onClick={handleResetAgent}
disabled={isResettingAgent}
>
<RefreshCcw className={cn("size-4", isResettingAgent && "animate-spin")} />
{isResettingAgent ? "Resetando agente..." : "Resetar agente"}
</Button>
<Button
size="sm"
variant={isActiveLocal ? "outline" : "default"}
className={cn(
"gap-2 border-dashed",
!isActiveLocal && "bg-emerald-600 text-white hover:bg-emerald-600/90"
)}
onClick={handleToggleActive}
disabled={togglingActive}
>
{isActiveLocal ? <Power className="size-4" /> : <PlayCircle className="size-4" />}
{isActiveLocal ? (togglingActive ? "Desativando..." : "Desativar") : togglingActive ? "Reativando..." : "Reativar"}
</Button>
</>
) : null}
</div>
</div>
{/* Campos personalizados (posicionado logo após métricas) */}
<div className="space-y-3">
<div className="space-y-3 border-t border-slate-100 pt-5">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<h4 className="text-sm font-semibold text-neutral-900">Campos personalizados</h4>
<Badge variant="outline" className="rounded-full px-2.5 py-0.5 text-[11px] font-semibold">
@ -3816,50 +3803,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
) : null}
</div>
<div className="flex flex-wrap gap-2">
<Button size="sm" variant="outline" className="gap-2 border-dashed" onClick={() => { setAccessDialog(true) }}>
<ShieldCheck className="size-4" />
Ajustar acesso
</Button>
{!isManualMobile ? (
<>
<Button
size="sm"
variant="outline"
className="gap-2 border-dashed border-amber-300 text-amber-700 hover:border-amber-400 hover:text-amber-800"
onClick={handleResetAgent}
disabled={isResettingAgent}
>
<RefreshCcw className={cn("size-4", isResettingAgent && "animate-spin")} />
{isResettingAgent ? "Resetando agente..." : "Resetar agente"}
</Button>
<Button
size="sm"
variant={isActiveLocal ? "outline" : "default"}
className={cn(
"gap-2 border-dashed",
!isActiveLocal && "bg-emerald-600 text-white hover:bg-emerald-600/90"
)}
onClick={handleToggleActive}
disabled={togglingActive}
>
{isActiveLocal ? <Power className="size-4" /> : <PlayCircle className="size-4" />}
{isActiveLocal ? (togglingActive ? "Desativando..." : "Desativar") : togglingActive ? "Reativando..." : "Reativar"}
</Button>
</>
) : null}
{device.registeredBy ? (
<span
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-2 border-dashed border-slate-200 bg-background cursor-default select-text text-neutral-700 hover:bg-background hover:text-neutral-700 focus-visible:outline-none"
)}
>
Registrada via {device.registeredBy}
</span>
) : null}
</div>
<div className="space-y-2">
<div className="space-y-3 border-t border-slate-100 pt-5">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<h4 className="text-sm font-semibold">Acesso remoto</h4>
@ -3889,7 +3833,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
{hasRemoteAccess ? (
<div className="space-y-3">
{remoteAccessEntries.map((entry) => {
const metadataEntries = extractRemoteAccessMetadataEntries(entry.metadata)
const lastVerifiedDate =
entry.lastVerifiedAt && Number.isFinite(entry.lastVerifiedAt)
? new Date(entry.lastVerifiedAt)
@ -3977,12 +3920,12 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
) : null}
{isRustDesk && (entry.identifier || entry.password) ? (
<Button
variant="secondary"
variant="outline"
size="sm"
className="mt-1 inline-flex items-center gap-2 bg-white/80 text-slate-800 hover:bg-white"
className="mt-1 inline-flex items-center gap-2 border-[#00d6eb]/60 bg-white text-slate-800 shadow-sm transition-colors hover:border-[#00d6eb] hover:bg-[#00e8ff]/10 hover:text-slate-900 focus-visible:border-[#00d6eb] focus-visible:ring-[#00e8ff]/30"
onClick={() => handleRustDeskConnect(entry)}
>
<MonitorSmartphone className="size-4" /> Conectar via RustDesk
<MonitorSmartphone className="size-4 text-[#009bb1]" /> Conectar via RustDesk
</Button>
) : null}
{entry.notes ? (
@ -4020,21 +3963,6 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
</div>
) : null}
</div>
{metadataEntries.length ? (
<details className="mt-3 rounded-lg border border-slate-200 bg-white/70 px-3 py-2 text-[11px] text-slate-600">
<summary className="cursor-pointer font-semibold text-slate-700 outline-none transition-colors hover:text-slate-900">
Metadados adicionais
</summary>
<div className="mt-2 grid gap-2 sm:grid-cols-2">
{metadataEntries.map(([key, value]) => (
<div key={`${entry.clientId}-${key}`} className="flex items-center justify-between gap-3 rounded-md border border-slate-200 bg-white px-2 py-1 shadow-sm">
<span className="font-semibold text-slate-700">{formatRemoteAccessMetadataKey(key)}</span>
<span className="truncate text-right text-slate-600">{formatRemoteAccessMetadataValue(value)}</span>
</div>
))}
</div>
</details>
) : null}
</div>
)
})}
@ -4047,7 +3975,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
</div>
</section>
<section className="space-y-2">
<section className="space-y-3 border-t border-slate-100 pt-6">
<h4 className="text-sm font-semibold">Usuários vinculados</h4>
<div className="space-y-2">
{primaryLinkedUser?.email ? (
@ -4339,7 +4267,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
</Dialog>
{!isManualMobile ? (
<section className="space-y-2">
<section className="space-y-3 border-t border-slate-100 pt-6">
<h4 className="text-sm font-semibold">Sincronização</h4>
<div className="grid gap-2 text-sm text-muted-foreground">
<div className="flex justify-between gap-4">
@ -4377,7 +4305,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
) : null}
{!isManualMobile ? (
<section className="space-y-2">
<section className="space-y-3 border-t border-slate-100 pt-6">
<div className="flex flex-wrap items-center gap-2">
<h4 className="text-sm font-semibold">Métricas recentes</h4>
{lastUpdateRelative ? (
@ -4391,7 +4319,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
) : null}
{!isManualMobile && (hardware || network || (labels && labels.length > 0)) ? (
<section className="space-y-3">
<section className="space-y-4 border-t border-slate-100 pt-6">
<div>
<h4 className="text-sm font-semibold">Inventário</h4>
<p className="text-xs text-muted-foreground">
@ -4500,7 +4428,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
{/* Discos (agente) */}
{disks.length > 0 ? (
<section className="space-y-2">
<section className="space-y-3 border-t border-slate-100 pt-6">
<h4 className="text-sm font-semibold">Discos e partições</h4>
<div className="rounded-md border border-slate-200 bg-slate-50/60">
<Table>
@ -4531,7 +4459,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
{/* Inventário estendido por SO */}
{extended ? (
<section className="space-y-3">
<section className="space-y-4 border-t border-slate-100 pt-6">
<div>
<h4 className="text-sm font-semibold">Inventário estendido</h4>
<p className="text-xs text-muted-foreground">Dados ricos coletados pelo agente, variam por sistema operacional.</p>