Redesenho da UI de dispositivos e correcao de VRAM

- Reorganiza layout da tela de dispositivos admin
- Renomeia secao "Controles do dispositivo" para "Atalhos"
- Adiciona botao de Tickets com badge de quantidade
- Simplifica textos de botoes (Acesso, Resetar)
- Remove email da maquina do cabecalho
- Move empresa e status para mesma linha
- Remove chip de Build do resumo
- Corrige deteccao de VRAM para GPUs >4GB usando nvidia-smi
- Adiciona prefixo "VRAM" na exibicao de memoria da GPU
- Documenta sincronizacao RustDesk

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
esdrasrenan 2025-12-06 17:01:40 -03:00
parent c5150fee8f
commit 23e7cf58ae
11 changed files with 863 additions and 441 deletions

View file

@ -117,7 +117,6 @@ const apiBaseUrl = normalizeUrl(import.meta.env.VITE_API_BASE_URL, appUrl)
const RUSTDESK_CONFIG_STRING = import.meta.env.VITE_RUSTDESK_CONFIG_STRING?.trim() || null
const RUSTDESK_DEFAULT_PASSWORD = import.meta.env.VITE_RUSTDESK_DEFAULT_PASSWORD?.trim() || null
const RUSTDESK_SYNC_INTERVAL_MS = 60 * 60 * 1000 // 1h
const TOKEN_SELF_HEAL_DEBOUNCE_MS = 30 * 1000
function sanitizeEmail(value: string | null | undefined) {
@ -244,7 +243,10 @@ async function writeRustdeskInfo(store: Store, info: RustdeskInfo): Promise<void
function logDesktop(message: string, data?: Record<string, unknown>) {
const enriched = data ? `${message} ${JSON.stringify(data)}` : message
console.log(`[raven] ${enriched}`)
const line = `[raven] ${enriched}`
console.log(line)
// Persiste em arquivo local para facilitar debugging fora do console
invoke("log_app_event", { message: line }).catch(() => {})
}
function bytes(n?: number) {
@ -782,99 +784,68 @@ const resolvedAppUrl = useMemo(() => {
return normalized
}, [config?.appUrl])
const syncRemoteAccessNow = useCallback(
async (info: RustdeskInfo, allowRetry = true) => {
if (!store) return
if (!config?.machineId) {
logDesktop("remoteAccess:sync:skipped", { reason: "unregistered" })
return
}
const payload = buildRemoteAccessPayload(info)
if (!payload) return
// Funcao simplificada de sync - sempre le do disco para evitar race conditions
const syncRemoteAccessDirect = useCallback(
async (info: RustdeskInfo, allowRetry = true): Promise<boolean> => {
try {
// Sempre le do disco para evitar race conditions com state React
const freshStore = await loadStore()
const freshConfig = await readConfig(freshStore)
const freshToken = await readToken(freshStore)
const resolveToken = async (allowHeal: boolean): Promise<string | null> => {
let currentToken = token
if (!currentToken) {
currentToken = (await readToken(store)) ?? null
if (currentToken) {
setToken(currentToken)
}
if (!freshConfig?.machineId || !freshToken) {
logDesktop("remoteAccess:sync:skip", {
hasMachineId: !!freshConfig?.machineId,
hasToken: !!freshToken,
})
return false
}
if (!currentToken && allowHeal) {
const healed = await attemptSelfHeal("remote-access")
if (healed) {
currentToken = (await readToken(store)) ?? null
if (currentToken) {
setToken(currentToken)
}
}
}
return currentToken
}
const sendRequest = async (machineToken: string, retryAllowed: boolean): Promise<void> => {
const payload = buildRemoteAccessPayload(info)
if (!payload) return false
logDesktop("remoteAccess:sync:start", { id: info.id })
const response = await fetch(`${apiBaseUrl}/api/machines/remote-access`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": `${config?.machineId ?? "unknown"}:RustDesk:${info.id}`,
"Idempotency-Key": `${freshConfig.machineId}:RustDesk:${info.id}`,
},
body: JSON.stringify({ machineToken, ...payload }),
body: JSON.stringify({ machineToken: freshToken, ...payload }),
})
if (!response.ok) {
logDesktop("remoteAccess:sync:error", { status: response.status })
const text = await response.text()
if (retryAllowed && (response.status === 401 || isTokenRevokedMessage(text))) {
const healed = await attemptSelfHeal("remote-access")
if (healed) {
const refreshedToken = await resolveToken(false)
if (refreshedToken) {
return sendRequest(refreshedToken, false)
}
}
}
throw new Error(text.slice(0, 300) || "Falha ao registrar acesso remoto")
if (response.ok) {
const nextInfo: RustdeskInfo = { ...info, lastSyncedAt: Date.now(), lastError: null }
await writeRustdeskInfo(freshStore, nextInfo)
setRustdeskInfo(nextInfo)
logDesktop("remoteAccess:sync:success", { id: info.id })
return true
}
const nextInfo: RustdeskInfo = { ...info, lastSyncedAt: Date.now(), lastError: null }
await writeRustdeskInfo(store, nextInfo)
setRustdeskInfo(nextInfo)
logDesktop("remoteAccess:sync:success", { id: info.id })
}
const errorText = await response.text()
logDesktop("remoteAccess:sync:error", { status: response.status, error: errorText.slice(0, 200) })
try {
const machineToken = await resolveToken(true)
if (!machineToken) {
const failedInfo: RustdeskInfo = {
...info,
lastError: "Token indisponível para sincronizar acesso remoto",
}
await writeRustdeskInfo(store, failedInfo)
setRustdeskInfo(failedInfo)
logDesktop("remoteAccess:sync:skipped", { reason: "missing-token" })
return
}
await sendRequest(machineToken, allowRetry)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error("Falha ao sincronizar acesso remoto com a plataforma", error)
const failedInfo: RustdeskInfo = { ...info, lastError: message }
await writeRustdeskInfo(store, failedInfo)
setRustdeskInfo(failedInfo)
if (allowRetry && isTokenRevokedMessage(message)) {
// Se token invalido, tenta self-heal uma vez
if (allowRetry && (response.status === 401 || isTokenRevokedMessage(errorText))) {
const healed = await attemptSelfHeal("remote-access")
if (healed) {
const refreshedToken = await resolveToken(false)
if (refreshedToken) {
return syncRemoteAccessNow(failedInfo, false)
}
return syncRemoteAccessDirect(info, false)
}
}
logDesktop("remoteAccess:sync:failed", { id: info.id, error: message })
// Salva erro no store
const failedInfo: RustdeskInfo = { ...info, lastError: errorText.slice(0, 200) }
await writeRustdeskInfo(freshStore, failedInfo)
setRustdeskInfo(failedInfo)
return false
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
logDesktop("remoteAccess:sync:exception", { error: message })
return false
}
},
[store, token, config?.machineId, attemptSelfHeal, setToken]
[attemptSelfHeal]
)
const handleRustdeskProvision = useCallback(
@ -1007,23 +978,58 @@ const resolvedAppUrl = useMemo(() => {
}
}, [store, handleRustdeskProvision])
// Bootstrap do RustDesk + retry simplificado (60s)
useEffect(() => {
if (!store || !config?.machineId) return
if (!rustdeskInfo && !isRustdeskProvisioning && !rustdeskBootstrapRef.current) {
rustdeskBootstrapRef.current = true
ensureRustdesk().finally(() => {
rustdeskBootstrapRef.current = false
})
return
}
if (rustdeskInfo && !isRustdeskProvisioning) {
const lastSync = rustdeskInfo.lastSyncedAt ?? 0
const needsSync = Date.now() - lastSync > RUSTDESK_SYNC_INTERVAL_MS
if (needsSync) {
syncRemoteAccessNow(rustdeskInfo)
let disposed = false
async function bootstrap() {
// Se nao tem rustdeskInfo, provisiona primeiro
if (!rustdeskInfo && !isRustdeskProvisioning && !rustdeskBootstrapRef.current) {
rustdeskBootstrapRef.current = true
try {
await ensureRustdesk()
} finally {
rustdeskBootstrapRef.current = false
}
return // handleRustdeskProvision fara o sync
}
// Se ja tem rustdeskInfo mas nunca sincronizou, tenta sync
if (rustdeskInfo && !rustdeskInfo.lastSyncedAt) {
logDesktop("remoteAccess:sync:bootstrap", { id: rustdeskInfo.id })
await syncRemoteAccessDirect(rustdeskInfo)
}
}
}, [store, config?.machineId, rustdeskInfo, ensureRustdesk, syncRemoteAccessNow, isRustdeskProvisioning])
bootstrap()
// Retry a cada 30s se nunca sincronizou (o Rust faz o sync automaticamente)
const interval = setInterval(async () => {
if (disposed) return
try {
const freshStore = await loadStore()
const freshRustdesk = await readRustdeskInfo(freshStore)
if (freshRustdesk && !freshRustdesk.lastSyncedAt) {
logDesktop("remoteAccess:sync:retry:fallback", { id: freshRustdesk.id })
// Re-invoca o Rust para tentar sync novamente
await invoke("ensure_rustdesk_and_emit", {
configString: RUSTDESK_CONFIG_STRING || null,
password: RUSTDESK_DEFAULT_PASSWORD || null,
machineId: config?.machineId,
})
}
} catch (err) {
logDesktop("remoteAccess:sync:retry:error", { error: String(err) })
}
}, 30_000)
return () => {
disposed = true
clearInterval(interval)
}
}, [store, config?.machineId, rustdeskInfo, isRustdeskProvisioning, ensureRustdesk, syncRemoteAccessDirect])
async function register() {
if (!profile) return
@ -1100,10 +1106,23 @@ const resolvedAppUrl = useMemo(() => {
},
})
await ensureRustdesk()
logDesktop("register:rustdesk:done", { machineId: data.machineId })
// Provisiona RustDesk em background (fire-and-forget)
// O Rust faz o sync com o backend automaticamente, sem passar pelo CSP do webview
logDesktop("register:rustdesk:start", { machineId: data.machineId })
invoke<RustdeskProvisioningResult>("ensure_rustdesk_and_emit", {
configString: RUSTDESK_CONFIG_STRING || null,
password: RUSTDESK_DEFAULT_PASSWORD || null,
machineId: data.machineId,
}).then((result) => {
logDesktop("register:rustdesk:done", { machineId: data.machineId, id: result.id })
}).catch((err) => {
const msg = err instanceof Error ? err.message : String(err)
if (!msg.toLowerCase().includes("apenas no windows")) {
logDesktop("register:rustdesk:error", { error: msg })
}
})
// Abre o sistema imediatamente após registrar (evita ficar com token inválido no fluxo antigo)
// Redireciona imediatamente (nao espera RustDesk)
try {
await fetch(`${apiBaseUrl}/api/machines/sessions`, {
method: "POST",