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:
parent
c5150fee8f
commit
23e7cf58ae
11 changed files with 863 additions and 441 deletions
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue