docs: registrar fluxo do updater e atualizar chaves

This commit is contained in:
Esdras Renan 2025-10-12 04:06:29 -03:00
parent 206d00700e
commit b5fd920efd
50 changed files with 980 additions and 93 deletions

View file

@ -119,7 +119,7 @@ type MachineInventory = {
disks?: Array<{ name?: string; mountPoint?: string; fs?: string; interface?: string | null; serial?: string | null; totalBytes?: number; availableBytes?: number }>
extended?: { linux?: LinuxExtended; windows?: WindowsExtended; macos?: MacExtended }
services?: Array<{ name?: string; status?: string; displayName?: string }>
collaborator?: { email?: string; name?: string }
collaborator?: { email?: string; name?: string; role?: string }
}
export type MachinesQueryItem = {
@ -135,6 +135,11 @@ export type MachinesQueryItem = {
serialNumbers: string[]
authUserId: string | null
authEmail: string | null
persona: string | null
assignedUserId: string | null
assignedUserEmail: string | null
assignedUserName: string | null
assignedUserRole: string | null
status: string | null
lastHeartbeatAt: number | null
heartbeatAgeMs: number | null
@ -209,12 +214,6 @@ function formatPercent(value?: number | null) {
return `${normalized.toFixed(0)}%`
}
function fmtBool(value: unknown) {
if (value === true) return "Sim"
if (value === false) return "Não"
return "—"
}
function readBool(source: unknown, key: string): boolean | undefined {
if (!source || typeof source !== "object") return undefined
const value = (source as Record<string, unknown>)[key]
@ -490,15 +489,31 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
}
}
// collaborator (from inventory metadata, when provided by onboarding)
type Collaborator = { email?: string; name?: string }
const collaborator: Collaborator | null = (() => {
// collaborator (from machine assignment or metadata)
type Collaborator = { email?: string; name?: string; role?: string }
const collaborator: Collaborator | null = useMemo(() => {
if (machine?.assignedUserEmail) {
return {
email: machine.assignedUserEmail ?? undefined,
name: machine.assignedUserName ?? undefined,
role: machine.persona ?? machine.assignedUserRole ?? undefined,
}
}
if (!metadata || typeof metadata !== "object") return null
const inv = metadata as Record<string, unknown>
const c = inv["collaborator"]
if (c && typeof c === "object") return c as Collaborator
if (c && typeof c === "object") {
const base = c as Record<string, unknown>
return {
email: typeof base.email === "string" ? base.email : undefined,
name: typeof base.name === "string" ? base.name : undefined,
role: typeof base.role === "string" ? (base.role as string) : undefined,
}
}
return null
})()
}, [machine?.assignedUserEmail, machine?.assignedUserName, machine?.persona, machine?.assignedUserRole, metadata])
const personaLabel = collaborator?.role === "manager" ? "Gestor" : "Colaborador"
const companyName = (() => {
if (!companies || !machine?.companySlug) return machine?.companySlug ?? null
@ -513,6 +528,13 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
const [dialogQuery, setDialogQuery] = useState("")
const [deleteDialog, setDeleteDialog] = useState(false)
const [deleting, setDeleting] = useState(false)
const [accessDialog, setAccessDialog] = useState(false)
const [accessEmail, setAccessEmail] = useState<string>(collaborator?.email ?? "")
const [accessName, setAccessName] = useState<string>(collaborator?.name ?? "")
const [accessRole, setAccessRole] = useState<"collaborator" | "manager">(
(machine?.persona === "manager" || collaborator?.role === "manager") ? "manager" : "collaborator"
)
const [savingAccess, setSavingAccess] = useState(false)
const jsonText = useMemo(() => {
const payload = {
id: machine?.id,
@ -535,6 +557,42 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
}, [jsonText, dialogQuery])
// removed copy/export inventory JSON buttons as requested
useEffect(() => {
setAccessEmail(collaborator?.email ?? "")
setAccessName(collaborator?.name ?? "")
setAccessRole((machine?.persona === "manager" || collaborator?.role === "manager") ? "manager" : "collaborator")
}, [machine?.id, machine?.persona, collaborator?.email, collaborator?.name, collaborator?.role])
const handleSaveAccess = async () => {
if (!machine) return
if (!accessEmail.trim()) {
toast.error("Informe o e-mail do colaborador ou gestor.")
return
}
setSavingAccess(true)
try {
const response = await fetch("/api/admin/machines/access", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
machineId: machine.id,
persona: accessRole,
email: accessEmail.trim(),
name: accessName.trim() || undefined,
}),
})
if (!response.ok) {
throw new Error(await response.text())
}
toast.success("Perfil de acesso atualizado.")
setAccessDialog(false)
} catch (error) {
console.error(error)
toast.error("Falha ao atualizar acesso da máquina.")
} finally {
setSavingAccess(false)
}
}
return (
<Card className="border-slate-200">
@ -594,17 +652,21 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
) : null}
{collaborator?.email ? (
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
Colaborador: {collaborator?.name ? `${collaborator.name} · ` : ""}{collaborator.email}
{personaLabel}: {collaborator?.name ? `${collaborator.name} · ` : ""}{collaborator.email}
</Badge>
) : null}
</div>
<div className="flex flex-wrap gap-2">
{machine.authEmail ? (
<Button size="sm" variant="outline" onClick={copyEmail} className="gap-2 border-dashed">
<ClipboardCopy className="size-4" />
Copiar e-mail
<div className="flex flex-wrap gap-2">
{machine.authEmail ? (
<Button size="sm" variant="outline" onClick={copyEmail} className="gap-2 border-dashed">
<ClipboardCopy className="size-4" />
Copiar e-mail
</Button>
) : null}
<Button size="sm" variant="outline" className="gap-2 border-dashed" onClick={() => { setAccessDialog(true) }}>
<ShieldCheck className="size-4" />
Ajustar acesso
</Button>
{machine.registeredBy ? (
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
Registrada via {machine.registeredBy}
@ -653,6 +715,42 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
</DialogContent>
</Dialog>
<Dialog open={accessDialog} onOpenChange={setAccessDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Ajustar acesso da máquina</DialogTitle>
</DialogHeader>
<div className="grid gap-3 py-2">
<div className="grid gap-2">
<label className="text-sm font-medium">Perfil</label>
<Select value={accessRole} onValueChange={(value) => setAccessRole((value as "collaborator" | "manager") ?? "collaborator")}>
<SelectTrigger>
<SelectValue placeholder="Selecione o perfil" />
</SelectTrigger>
<SelectContent>
<SelectItem value="collaborator">Colaborador (portal)</SelectItem>
<SelectItem value="manager">Gestor (painel completo)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<label className="text-sm font-medium">E-mail</label>
<Input type="email" value={accessEmail} onChange={(e) => setAccessEmail(e.target.value)} placeholder="colaborador@empresa.com" />
</div>
<div className="grid gap-2">
<label className="text-sm font-medium">Nome (opcional)</label>
<Input value={accessName} onChange={(e) => setAccessName(e.target.value)} placeholder="Nome completo" />
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setAccessDialog(false)} disabled={savingAccess}>Cancelar</Button>
<Button onClick={handleSaveAccess} disabled={savingAccess || !accessEmail.trim()}>
{savingAccess ? "Salvando..." : "Salvar"}
</Button>
</div>
</DialogContent>
</Dialog>
<section className="space-y-2">
<h4 className="text-sm font-semibold">Sincronização</h4>
<div className="grid gap-2 text-sm text-muted-foreground">
@ -1372,16 +1470,27 @@ function MachineCard({ machine, companyName }: { machine: MachinesQueryItem; com
const memPct = mm?.memoryUsedPercent ?? (Number.isFinite(memUsed) && Number.isFinite(memTotal) ? (Number(memUsed) / Number(memTotal)) * 100 : NaN)
const cpuPct = mm?.cpuUsagePercent ?? NaN
const collaborator = (() => {
if (machine.assignedUserEmail) {
return {
email: machine.assignedUserEmail ?? undefined,
name: machine.assignedUserName ?? undefined,
role: machine.persona ?? machine.assignedUserRole ?? undefined,
}
}
const inv = machine.inventory as unknown
if (!inv || typeof inv !== "object") return null
const raw = (inv as Record<string, unknown>).collaborator
if (!raw || typeof raw !== "object") return null
const obj = raw as Record<string, unknown>
const email = typeof obj.email === "string" ? obj.email : undefined
const name = typeof obj.name === "string" ? obj.name : undefined
if (!email) return null
return { email, name }
return {
email,
name: typeof obj.name === "string" ? obj.name : undefined,
role: typeof obj.role === "string" ? (obj.role as string) : undefined,
}
})()
const persona = collaborator?.role === "manager" ? "Gestor" : "Colaborador"
const companyLabel = companyName ?? machine.companySlug ?? null
return (
@ -1427,7 +1536,7 @@ function MachineCard({ machine, companyName }: { machine: MachinesQueryItem; com
</div>
{collaborator?.email ? (
<p className="text-[11px] text-muted-foreground">
{collaborator.name ? `${collaborator.name} · ` : ""}
{persona}: {collaborator.name ? `${collaborator.name} · ` : ""}
{collaborator.email}
</p>
) : null}

View file

@ -23,17 +23,21 @@ const navItems = [
export function PortalShell({ children }: PortalShellProps) {
const pathname = usePathname()
const router = useRouter()
const { session } = useAuth()
const { session, machineContext } = useAuth()
const [isSigningOut, setIsSigningOut] = useState(false)
const displayName = machineContext?.assignedUserName ?? session?.user.name ?? session?.user.email ?? "Cliente"
const displayEmail = machineContext?.assignedUserEmail ?? session?.user.email ?? ""
const personaLabel = machineContext?.persona === "manager" ? "Gestor" : "Colaborador"
const initials = useMemo(() => {
const name = session?.user.name || session?.user.email || "Cliente"
const name = displayName || displayEmail || "Cliente"
return name
.split(" ")
.slice(0, 2)
.map((part) => part.charAt(0).toUpperCase())
.join("")
}, [session?.user.name, session?.user.email])
}, [displayName, displayEmail])
async function handleSignOut() {
if (isSigningOut) return
@ -85,12 +89,15 @@ export function PortalShell({ children }: PortalShellProps) {
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 text-sm">
<Avatar className="size-9 border border-slate-200">
<AvatarImage src={session?.user.avatarUrl ?? undefined} alt={session?.user.name ?? ""} />
<AvatarImage src={session?.user.avatarUrl ?? undefined} alt={displayName ?? ""} />
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<div className="flex flex-col leading-tight">
<span className="font-semibold text-neutral-900">{session?.user.name ?? "Cliente"}</span>
<span className="text-xs text-neutral-500">{session?.user.email ?? ""}</span>
<span className="font-semibold text-neutral-900">{displayName}</span>
<span className="text-xs text-neutral-500">{displayEmail}</span>
{machineContext ? (
<span className="text-[10px] uppercase tracking-wide text-neutral-400">{personaLabel}</span>
) : null}
</div>
</div>
<Button