docs: registrar fluxo do updater e atualizar chaves
This commit is contained in:
parent
206d00700e
commit
b5fd920efd
50 changed files with 980 additions and 93 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useMemo, useState } from "react"
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import { Store } from "@tauri-apps/plugin-store"
|
||||
|
|
@ -45,6 +46,12 @@ type MachineRegisterResponse = {
|
|||
machineToken: string
|
||||
machineEmail?: string | null
|
||||
expiresAt?: number | null
|
||||
persona?: string | null
|
||||
assignedUserId?: string | null
|
||||
collaborator?: {
|
||||
email: string
|
||||
name?: string | null
|
||||
} | null
|
||||
}
|
||||
|
||||
type AgentConfig = {
|
||||
|
|
@ -54,6 +61,10 @@ type AgentConfig = {
|
|||
machineEmail?: string | null
|
||||
collaboratorEmail?: string | null
|
||||
collaboratorName?: string | null
|
||||
accessRole: "collaborator" | "manager"
|
||||
assignedUserId?: string | null
|
||||
assignedUserEmail?: string | null
|
||||
assignedUserName?: string | null
|
||||
apiBaseUrl: string
|
||||
appUrl: string
|
||||
createdAt: number
|
||||
|
|
@ -129,7 +140,14 @@ function App() {
|
|||
const [company, setCompany] = useState("")
|
||||
const [collabEmail, setCollabEmail] = useState("")
|
||||
const [collabName, setCollabName] = useState("")
|
||||
const [accessRole, setAccessRole] = useState<"collaborator" | "manager">("collaborator")
|
||||
const [updating, setUpdating] = useState(false)
|
||||
const [updateInfo, setUpdateInfo] = useState<{ message: string; tone: "info" | "success" | "error" } | null>({
|
||||
message: "Atualizações automáticas são verificadas a cada inicialização.",
|
||||
tone: "info",
|
||||
})
|
||||
const autoLaunchRef = useRef(false)
|
||||
const autoUpdateRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
|
@ -140,6 +158,7 @@ function App() {
|
|||
setToken(t)
|
||||
const cfg = await readConfig(s)
|
||||
setConfig(cfg)
|
||||
setAccessRole(cfg?.accessRole ?? "collaborator")
|
||||
if (cfg?.collaboratorEmail) setCollabEmail(cfg.collaboratorEmail)
|
||||
if (cfg?.collaboratorName) setCollabName(cfg.collaboratorName)
|
||||
if (!t) {
|
||||
|
|
@ -147,7 +166,7 @@ function App() {
|
|||
setProfile(p)
|
||||
}
|
||||
setStatus(t ? "online" : null)
|
||||
} catch (err) {
|
||||
} catch {
|
||||
setError("Falha ao carregar estado do agente.")
|
||||
}
|
||||
})()
|
||||
|
|
@ -161,7 +180,8 @@ function App() {
|
|||
const normalizedName = name.length > 0 ? name : null
|
||||
if (
|
||||
config.collaboratorEmail === normalizedEmail &&
|
||||
config.collaboratorName === normalizedName
|
||||
config.collaboratorName === normalizedName &&
|
||||
config.accessRole === accessRole
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
|
@ -169,10 +189,11 @@ function App() {
|
|||
...config,
|
||||
collaboratorEmail: normalizedEmail,
|
||||
collaboratorName: normalizedName,
|
||||
accessRole,
|
||||
}
|
||||
setConfig(nextConfig)
|
||||
writeConfig(store, nextConfig).catch((err) => console.error("Falha ao atualizar colaborador", err))
|
||||
}, [store, config?.machineId, config?.collaboratorEmail, config?.collaboratorName, collabEmail, collabName])
|
||||
}, [store, config, config?.collaboratorEmail, config?.collaboratorName, config?.accessRole, collabEmail, collabName, accessRole])
|
||||
|
||||
useEffect(() => {
|
||||
if (!store || !config) return
|
||||
|
|
@ -195,7 +216,7 @@ function App() {
|
|||
return appUrl
|
||||
}
|
||||
return normalized
|
||||
}, [config?.appUrl, appUrl])
|
||||
}, [config?.appUrl])
|
||||
|
||||
async function register() {
|
||||
if (!profile) return
|
||||
|
|
@ -205,6 +226,16 @@ function App() {
|
|||
const collaboratorPayload = collabEmail.trim()
|
||||
? { email: collabEmail.trim(), name: collabName.trim() || undefined }
|
||||
: undefined
|
||||
const collaboratorMetadata = collaboratorPayload
|
||||
? { ...collaboratorPayload, role: accessRole }
|
||||
: undefined
|
||||
const metadataPayload: Record<string, unknown> = {
|
||||
inventory: profile.inventory,
|
||||
metrics: profile.metrics,
|
||||
}
|
||||
if (collaboratorMetadata) {
|
||||
metadataPayload.collaborator = collaboratorMetadata
|
||||
}
|
||||
const payload = {
|
||||
provisioningSecret: provisioningSecret.trim(),
|
||||
tenantId: tenantId.trim() || undefined,
|
||||
|
|
@ -213,7 +244,9 @@ function App() {
|
|||
os: profile.os,
|
||||
macAddresses: profile.macAddresses,
|
||||
serialNumbers: profile.serialNumbers,
|
||||
metadata: { inventory: profile.inventory, metrics: profile.metrics, collaborator: collaboratorPayload },
|
||||
metadata: metadataPayload,
|
||||
accessRole,
|
||||
collaborator: collaboratorPayload,
|
||||
registeredBy: "desktop-agent",
|
||||
}
|
||||
const res = await fetch(`${apiBaseUrl}/api/machines/register`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) })
|
||||
|
|
@ -231,6 +264,10 @@ function App() {
|
|||
machineEmail: data.machineEmail ?? null,
|
||||
collaboratorEmail: collaboratorPayload?.email ?? null,
|
||||
collaboratorName: collaboratorPayload?.name ?? null,
|
||||
accessRole,
|
||||
assignedUserId: data.assignedUserId ?? null,
|
||||
assignedUserEmail: data.collaborator?.email ?? collaboratorPayload?.email ?? null,
|
||||
assignedUserName: data.collaborator?.name ?? collaboratorPayload?.name ?? null,
|
||||
apiBaseUrl,
|
||||
appUrl,
|
||||
createdAt: Date.now(),
|
||||
|
|
@ -248,16 +285,19 @@ function App() {
|
|||
}
|
||||
}
|
||||
|
||||
async function openSystem() {
|
||||
if (!token || !config) return
|
||||
const url = `${resolvedAppUrl}/machines/handshake?token=${encodeURIComponent(token)}`
|
||||
const openSystem = useCallback(() => {
|
||||
if (!token) return
|
||||
const persona = (config?.accessRole ?? accessRole) === "manager" ? "manager" : "collaborator"
|
||||
const redirectTarget = persona === "manager" ? "/dashboard" : "/portal"
|
||||
const url = `${resolvedAppUrl}/machines/handshake?token=${encodeURIComponent(token)}&redirect=${encodeURIComponent(redirectTarget)}`
|
||||
window.location.href = url
|
||||
}
|
||||
}, [token, config?.accessRole, accessRole, resolvedAppUrl])
|
||||
|
||||
async function reprovision() {
|
||||
if (!store) return
|
||||
await store.delete("token"); await store.delete("config"); await store.save()
|
||||
setToken(null); setConfig(null); setStatus(null)
|
||||
autoLaunchRef.current = false
|
||||
setToken(null); setConfig(null); setStatus(null); setAccessRole("collaborator")
|
||||
const p = await invoke<MachineProfile>("collect_machine_profile")
|
||||
setProfile(p)
|
||||
}
|
||||
|
|
@ -269,9 +309,12 @@ function App() {
|
|||
const collaboratorPayload = collabEmail.trim()
|
||||
? { email: collabEmail.trim(), name: collabName.trim() || undefined }
|
||||
: undefined
|
||||
const collaboratorInventory = collaboratorPayload
|
||||
? { ...collaboratorPayload, role: accessRole }
|
||||
: undefined
|
||||
const inventoryPayload: Record<string, unknown> = { ...profile.inventory }
|
||||
if (collaboratorPayload) {
|
||||
inventoryPayload.collaborator = collaboratorPayload
|
||||
if (collaboratorInventory) {
|
||||
inventoryPayload.collaborator = collaboratorInventory
|
||||
}
|
||||
const payload = {
|
||||
machineToken: token,
|
||||
|
|
@ -296,27 +339,61 @@ function App() {
|
|||
}
|
||||
}
|
||||
|
||||
async function checkForUpdates() {
|
||||
async function checkForUpdates(auto = false) {
|
||||
try {
|
||||
setUpdating(true)
|
||||
if (!auto) {
|
||||
setUpdating(true)
|
||||
setUpdateInfo({ tone: "info", message: "Procurando por atualizações..." })
|
||||
}
|
||||
const { check } = await import("@tauri-apps/plugin-updater")
|
||||
const update = await check()
|
||||
if (update && (update as any).available) {
|
||||
// download and install then relaunch
|
||||
await (update as any).downloadAndInstall()
|
||||
const { relaunch } = await import("@tauri-apps/plugin-process")
|
||||
await relaunch()
|
||||
} else {
|
||||
alert("Nenhuma atualização disponível.")
|
||||
type UpdateResult = {
|
||||
available?: boolean
|
||||
version?: string
|
||||
downloadAndInstall?: () => Promise<void>
|
||||
}
|
||||
const update = (await check()) as UpdateResult | null
|
||||
if (update?.available) {
|
||||
setUpdateInfo({
|
||||
tone: "info",
|
||||
message: `Atualização ${update.version} disponível. Baixando e aplicando...`,
|
||||
})
|
||||
if (typeof update.downloadAndInstall === "function") {
|
||||
await update.downloadAndInstall()
|
||||
const { relaunch } = await import("@tauri-apps/plugin-process")
|
||||
await relaunch()
|
||||
}
|
||||
} else if (!auto) {
|
||||
setUpdateInfo({ tone: "info", message: "Nenhuma atualização disponível no momento." })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Falha ao verificar atualizações", error)
|
||||
alert("Falha ao verificar atualizações.")
|
||||
if (!auto) {
|
||||
setUpdateInfo({
|
||||
tone: "error",
|
||||
message: "Falha ao verificar atualizações. Tente novamente mais tarde.",
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
setUpdating(false)
|
||||
if (!auto) setUpdating(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (import.meta.env.DEV) return
|
||||
if (autoUpdateRef.current) return
|
||||
autoUpdateRef.current = true
|
||||
checkForUpdates(true).catch((err: unknown) => {
|
||||
console.error("Falha ao executar atualização automática", err)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) return
|
||||
if (autoLaunchRef.current) return
|
||||
autoLaunchRef.current = true
|
||||
openSystem()
|
||||
}, [token, config?.accessRole, openSystem])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen grid place-items-center p-6">
|
||||
<div className="w-full max-w-[720px] rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
|
||||
|
|
@ -345,6 +422,20 @@ function App() {
|
|||
<label className="text-sm font-medium">Empresa (slug opcional)</label>
|
||||
<input className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm" placeholder="ex.: atlas-engenharia" value={company} onChange={(e)=>setCompany(e.target.value)} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm font-medium">Perfil de acesso</label>
|
||||
<select
|
||||
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm"
|
||||
value={accessRole}
|
||||
onChange={(e) => setAccessRole((e.target.value as "collaborator" | "manager") ?? "collaborator")}
|
||||
>
|
||||
<option value="collaborator">Colaborador (portal)</option>
|
||||
<option value="manager">Gestor (painel completo)</option>
|
||||
</select>
|
||||
<p className="text-xs text-slate-500">
|
||||
Colaboradores veem apenas seus chamados. Gestores acompanham todos os tickets da empresa.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm font-medium">Colaborador (e-mail)</label>
|
||||
<input className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm" placeholder="colaborador@empresa.com" value={collabEmail} onChange={(e)=>setCollabEmail(e.target.value)} />
|
||||
|
|
@ -442,9 +533,25 @@ function App() {
|
|||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="label">Atualizações</label>
|
||||
<button onClick={checkForUpdates} disabled={updating} className={cn("btn btn-outline inline-flex items-center gap-2", updating && "opacity-60")}>
|
||||
<RefreshCw className="size-4" /> Verificar atualizações
|
||||
<button
|
||||
onClick={() => checkForUpdates(false)}
|
||||
disabled={updating}
|
||||
className={cn("btn btn-outline inline-flex items-center gap-2", updating && "opacity-60")}
|
||||
>
|
||||
<RefreshCw className={cn("size-4", updating && "animate-spin")} /> Verificar atualizações
|
||||
</button>
|
||||
{updateInfo ? (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border px-3 py-2 text-sm leading-snug",
|
||||
updateInfo.tone === "success" && "border-emerald-200 bg-emerald-50 text-emerald-700",
|
||||
updateInfo.tone === "error" && "border-rose-200 bg-rose-50 text-rose-700",
|
||||
updateInfo.tone === "info" && "border-slate-200 bg-slate-50 text-slate-700"
|
||||
)}
|
||||
>
|
||||
{updateInfo.message}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
|
@ -452,7 +559,7 @@ function App() {
|
|||
)}
|
||||
</div>
|
||||
<div className="mt-6 flex justify-center">
|
||||
<img src={`${appUrl}/rever-8.png`} alt="Logotipo Rever Tecnologia" width={110} height={110} className="h-[3.45rem] w-auto" />
|
||||
<img src={`${appUrl}/raven.png`} alt="Logotipo Raven" width={110} height={110} className="h-[3.45rem] w-auto" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue