diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 75bc20b..29480fa 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -10,6 +10,7 @@ "tauri": "tauri" }, "dependencies": { + "@radix-ui/react-tabs": "^1.1.13", "@tauri-apps/api": "^2", "@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-store": "^2", diff --git a/apps/desktop/src-tauri/src/agent.rs b/apps/desktop/src-tauri/src/agent.rs index d7f6a7b..996a8c6 100644 --- a/apps/desktop/src-tauri/src/agent.rs +++ b/apps/desktop/src-tauri/src/agent.rs @@ -490,6 +490,22 @@ fn collect_windows_extended() -> serde_json::Value { let defender = ps("Get-MpComputerStatus | Select-Object AMRunningMode,AntivirusEnabled,RealTimeProtectionEnabled,AntispywareEnabled").unwrap_or_else(|| json!({})); let hotfix = ps("Get-HotFix | Select-Object HotFixID,InstalledOn").unwrap_or_else(|| json!([])); + // Informações de build/edição e ativação + let os_info = ps(r#" + $cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'; + $ls = (Get-CimInstance -Query "SELECT LicenseStatus FROM SoftwareLicensingProduct WHERE PartialProductKey IS NOT NULL" | Select-Object -First 1).LicenseStatus; + [PSCustomObject]@{ + ProductName = $cv.ProductName + CurrentBuild = $cv.CurrentBuild + CurrentBuildNumber = $cv.CurrentBuildNumber + DisplayVersion = $cv.DisplayVersion + ReleaseId = $cv.ReleaseId + EditionID = $cv.EditionID + LicenseStatus = $ls + IsActivated = ($ls -eq 1) + } + "#).unwrap_or_else(|| json!({})); + // Hardware detalhado (CPU/Board/BIOS/Memória/Vídeo/Discos) let cpu = ps("Get-CimInstance Win32_Processor | Select-Object Name,Manufacturer,SocketDesignation,NumberOfCores,NumberOfLogicalProcessors,L2CacheSize,L3CacheSize,MaxClockSpeed").unwrap_or_else(|| json!({})); let baseboard = ps("Get-CimInstance Win32_BaseBoard | Select-Object Product,Manufacturer,SerialNumber,Version").unwrap_or_else(|| json!({})); @@ -504,6 +520,7 @@ fn collect_windows_extended() -> serde_json::Value { "services": services, "defender": defender, "hotfix": hotfix, + "osInfo": os_info, "cpu": cpu, "baseboard": baseboard, "bios": bios, diff --git a/apps/desktop/src/components/ui/tabs.tsx b/apps/desktop/src/components/ui/tabs.tsx new file mode 100644 index 0000000..c617697 --- /dev/null +++ b/apps/desktop/src/components/ui/tabs.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "../../lib/utils" + +export function Tabs({ className, ...props }: React.ComponentProps) { + return +} + +export function TabsList({ className, ...props }: React.ComponentProps) { + return ( + + ) +} + +export function TabsTrigger({ className, ...props }: React.ComponentProps) { + return ( + + ) +} + +export function TabsContent({ className, ...props }: React.ComponentProps) { + return +} + diff --git a/apps/desktop/src/index.css b/apps/desktop/src/index.css index e39e270..879898e 100644 --- a/apps/desktop/src/index.css +++ b/apps/desktop/src/index.css @@ -12,3 +12,50 @@ body { @apply bg-slate-50 text-slate-900; } +.badge-status { + @apply inline-flex h-8 items-center gap-3 rounded-full border border-slate-200 px-3 text-sm font-semibold; +} + +.card { + @apply w-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm; +} + +.btn { + @apply inline-flex items-center justify-center rounded-lg border px-3 py-2 text-sm font-semibold transition; +} + +.btn-primary { + @apply border-black bg-black text-white hover:bg-black/90; +} + +.btn-outline { + @apply border-slate-300 bg-white text-slate-800 hover:bg-slate-50; +} + +.input { + @apply w-full rounded-lg border border-slate-300 px-3 py-2 text-sm; +} + +.label { + @apply text-sm font-medium; +} + +.tabs { + @apply mt-4 flex flex-col gap-3; +} + +.tab-list { + @apply flex flex-wrap gap-2 border-b border-slate-200 pb-2; +} + +.tab-btn { + @apply rounded-lg border border-transparent bg-transparent px-3 py-1.5 text-sm font-medium text-slate-700 hover:bg-slate-100; +} + +.tab-btn.active { + @apply border-slate-300 bg-slate-100 text-slate-900; +} + +.stat-card { + @apply flex items-center gap-3 rounded-md border border-slate-200 bg-slate-50/80 px-3 py-2; +} diff --git a/apps/desktop/src/lib/utils.ts b/apps/desktop/src/lib/utils.ts new file mode 100644 index 0000000..24be1cd --- /dev/null +++ b/apps/desktop/src/lib/utils.ts @@ -0,0 +1,4 @@ +export function cn(...classes: Array) { + return classes.filter(Boolean).join(" ") +} + diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts deleted file mode 100644 index e52f4ab..0000000 --- a/apps/desktop/src/main.ts +++ /dev/null @@ -1,758 +0,0 @@ -import { invoke } from "@tauri-apps/api/core" -import { Store } from "@tauri-apps/plugin-store" -import { openUrl } from "@tauri-apps/plugin-opener" - -type MachineOs = { - name: string - version?: string | null - architecture?: string | null -} - -type MachineMetrics = { - collectedAt: string - cpuLogicalCores: number - cpuPhysicalCores?: number | null - cpuUsagePercent: number - loadAverageOne?: number | null - loadAverageFive?: number | null - loadAverageFifteen?: number | null - memoryTotalBytes: number - memoryUsedBytes: number - memoryUsedPercent: number - uptimeSeconds: number -} - -type MachineInventory = { - cpuBrand?: string | null - hostIdentifier?: string | null -} - -type MachineProfile = { - hostname: string - os: MachineOs - macAddresses: string[] - serialNumbers: string[] - inventory: MachineInventory - metrics: MachineMetrics -} - -type MachineRegisterResponse = { - machineId: string - tenantId?: string | null - companyId?: string | null - companySlug?: string | null - machineToken: string - machineEmail?: string | null - expiresAt?: number | null -} - -type AgentConfig = { - machineId: string - tenantId?: string | null - companySlug?: string | null - machineEmail?: string | null - apiBaseUrl: string - appUrl: string - createdAt: number - lastSyncedAt?: number | null - expiresAt?: number | null - heartbeatIntervalSec?: number | null -} - -declare global { - interface ImportMetaEnv { - readonly VITE_APP_URL?: string - readonly VITE_API_BASE_URL?: string - } - - interface ImportMeta { - readonly env: ImportMetaEnv - } -} - -const STORE_FILENAME = "machine-agent.json" -// Defaults: em produção, apontamos para o domínio público; em dev, localhost -const DEFAULT_APP_URL = - import.meta.env.MODE === "production" - ? "https://tickets.esdrasrenan.com.br" - : "http://localhost:3000" - -function normalizeUrl(value?: string | null, fallback = DEFAULT_APP_URL) { - const trimmed = (value ?? fallback).trim() - if (!trimmed.startsWith("http")) { - return fallback - } - return trimmed.replace(/\/+$/, "") -} - -const appUrl = normalizeUrl(import.meta.env.VITE_APP_URL, DEFAULT_APP_URL) -const apiBaseUrl = normalizeUrl( - import.meta.env.VITE_API_BASE_URL, - appUrl -) - -const alertElement = document.getElementById("alert-container") as HTMLDivElement | null -const contentElement = document.getElementById("content") as HTMLDivElement | null -const statusElement = document.getElementById("status-text") as HTMLParagraphElement | null - -function setAlert(message: string | null, variant: "info" | "error" | "success" = "info") { - if (!alertElement) return - if (!message) { - alertElement.textContent = "" - alertElement.className = "alert" - return - } - alertElement.textContent = message - const extra = variant === "info" ? "" : ` ${variant}` - alertElement.className = `alert visible${extra}` -} - -function setStatus(message: string) { - if (statusElement) { - statusElement.textContent = message - } -} - -let storeInstance: Store | null = null - -async function ensureStoreLoaded(): Promise { - if (!storeInstance) { - try { - storeInstance = await Store.load(STORE_FILENAME) - } catch (error) { - console.error("[agent] Falha ao carregar store", error) - throw error - } - } - return storeInstance -} - -async function loadConfig(): Promise { - try { - const store = await ensureStoreLoaded() - const record = await store.get("config") - if (!record) return null - return record - } catch (error) { - console.error("[agent] Falha ao recuperar configuração", error) - return null - } -} - -async function saveConfig(config: AgentConfig) { - const store = await ensureStoreLoaded() - await store.set("config", config) - await store.save() -} - -async function clearConfig() { - const store = await ensureStoreLoaded() - await store.delete("config") - await store.save() - await store.delete("token"); - await store.save() -} - -async function getMachineToken(): Promise { - const store = await ensureStoreLoaded() - const token = await store.get("token") - return token ?? null -} - -async function setMachineToken(token: string) { - const store = await ensureStoreLoaded() - await store.set("token", token) - await store.save() -} - -async function collectMachineProfile(): Promise { - return await invoke("collect_machine_profile") -} - -async function collectMachineInventory(): Promise> { - return await invoke>("collect_machine_inventory") -} - -async function startHeartbeat(config: AgentConfig) { - const token = await getMachineToken() - if (!token) throw new Error("Token da máquina ausente no cofre seguro") - await invoke("start_machine_agent", { - baseUrl: config.apiBaseUrl, - token, - status: "online", - intervalSeconds: Math.max(60, Number(config.heartbeatIntervalSec ?? 300)), - }) -} - -async function stopHeartbeat() { - await invoke("stop_machine_agent") -} - -function renderTabsShell() { - if (!contentElement) return - contentElement.innerHTML = ` -
-
- - - - -
-
-
-
-
-
- ` - - const buttons = contentElement.querySelectorAll(".tab-list button") - buttons.forEach((btn) => { - btn.addEventListener("click", () => { - buttons.forEach((b) => b.classList.remove("active")) - btn.classList.add("active") - const tab = btn.getAttribute("data-tab") - const panels = contentElement.querySelectorAll(".tab-panel") - panels.forEach((p) => p.classList.remove("active")) - const panel = contentElement.querySelector(`#tab-${tab}`) - if (panel) panel.classList.add("active") - }) - }) -} - -function renderOverviewPanel(profile: MachineProfile) { - const panel = document.getElementById("tab-overview") - if (!panel) return - const summary = renderMachineSummary(profile) ?? "" - panel.innerHTML = ` -

Máquina provisionada e com heartbeat ativo.

- ${summary} - ` -} - -function renderInventoryPanel(inv: Record, profile: MachineProfile) { - const panel = document.getElementById("tab-inventory") - if (!panel) return - const disks = Array.isArray((inv as any).disks) ? ((inv as any).disks as any[]) : [] - const network = (inv as any).network - const mem = profile.metrics - const totalDisk = disks.reduce((acc, d) => acc + Number(d?.totalBytes ?? 0), 0) - const disksCount = disks.length - - let networkHtml = "" - if (Array.isArray(network)) { - networkHtml = ` -
-
Rede (interfaces)
- ${(network as any[]) - .map((iface) => `
${iface.name ?? "iface"} · ${iface.mac ?? "—"} · ${iface.ip ?? "—"}
`) - .join("")} -
- ` - } - - let disksHtml = "" - if (disks.length > 0) { - disksHtml = ` -
-
Discos e partições
- ${disks - .map( - (d) => - `
${d.name ?? "disco"} · ${d.mountPoint ?? "—"} · ${d.fs ?? "?"} · ${formatBytes(Number(d.totalBytes))} (${formatBytes(Number(d.availableBytes))} livre)
` - ) - .join("")} -
- ` - } - - const software: any[] = Array.isArray((inv as any).software) ? ((inv as any).software as any[]) : [] - const services: any[] = Array.isArray((inv as any).services) ? ((inv as any).services as any[]) : [] - const softwareHtml = software.length - ? `
Softwares (amostra)
${software - .slice(0, 8) - .map((s, i) => `
${s.name ?? s.DisplayName ?? `Software ${i + 1}`} ${s.version ?? s.DisplayVersion ?? ""}
`) - .join("")}${software.length > 8 ? `
+${software.length - 8} itens
` : ""}
` - : "" - const servicesHtml = services.length - ? `
Serviços (amostra)
${services - .slice(0, 8) - .map((svc) => `
${svc.name ?? svc.Name ?? "serviço"} · ${svc.status ?? svc.Status ?? "?"}
`) - .join("")}${services.length > 8 ? `
+${services.length - 8} itens
` : ""}
` - : "" - - panel.innerHTML = ` -
-
🧠
CPU
${formatPercent(mem.cpuUsagePercent)}
-
🧩
Memória
${formatBytes(mem.memoryUsedBytes)} / ${formatBytes(mem.memoryTotalBytes)}
-
🖥️
Sistema
${profile.os.name}
-
💾
Discos
${disksCount} · ${formatBytes(totalDisk)}
-
- ${networkHtml} - ${disksHtml} - ${softwareHtml} - ${servicesHtml} -
- ` - - const refresh = document.getElementById("refresh-inventory") - refresh?.addEventListener("click", async () => { - setStatus("Atualizando inventário…") - try { - const [p, i] = await Promise.all([collectMachineProfile(), collectMachineInventory()]) - renderOverviewPanel(p) - renderInventoryPanel(i, p) - renderDiagnosticsPanel(p) - setStatus("Inventário atualizado.") - } catch { - setAlert("Falha ao atualizar inventário.", "error") - } - }) -} - -function renderDiagnosticsPanel(profile: MachineProfile) { - const panel = document.getElementById("tab-diagnostics") - if (!panel) return - const m = profile.metrics - panel.innerHTML = ` -
-
CPU ${formatPercent(m.cpuUsagePercent)}
-
Memória ${formatBytes(m.memoryUsedBytes)} / ${formatBytes(m.memoryTotalBytes)} (${formatPercent(m.memoryUsedPercent)})
-
Uptime ${m.uptimeSeconds}s
-
Load ${[m.loadAverageOne, m.loadAverageFive, m.loadAverageFifteen] - .map((v) => (v ? v.toFixed(2) : "—")) - .join(" / ")}
-
-
-
Histórico (curto)
- - -

Amostras locais a cada ~3s, mantemos ~60 pontos.

-
- ` - - const cpuCanvas = panel.querySelector("#diag-cpu") - const memCanvas = panel.querySelector("#diag-mem") - const cpuData: number[] = [] - const memData: number[] = [] - - function draw(canvas: HTMLCanvasElement, series: number[], maxValue: number, color: string) { - const ctx = canvas.getContext("2d") - if (!ctx) return - ctx.clearRect(0, 0, canvas.width, canvas.height) - const w = canvas.width - const h = canvas.height - const n = Math.max(1, series.length) - ctx.strokeStyle = color - ctx.lineWidth = 2 - ctx.beginPath() - for (let i = 0; i < n; i++) { - const x = (i / (n - 1)) * (w - 6) + 3 - const v = Math.max(0, Math.min(maxValue, series[i] ?? 0)) - const y = h - 4 - (v / maxValue) * (h - 8) - if (i === 0) ctx.moveTo(x, y) - else ctx.lineTo(x, y) - } - ctx.stroke() - } - - let stop = false - async function pump() { - if (stop) return - try { - const p = await collectMachineProfile() - cpuData.push(Math.max(0, Math.min(100, p.metrics.cpuUsagePercent))) - const memPct = (p.metrics.memoryUsedPercent) - memData.push(Math.max(0, Math.min(100, memPct))) - while (cpuData.length > 60) cpuData.shift() - while (memData.length > 60) memData.shift() - if (cpuCanvas) draw(cpuCanvas, cpuData, 100, "#2563eb") - if (memCanvas) draw(memCanvas, memData, 100, "#10b981") - } catch { - // ignore - } finally { - setTimeout(pump, 3000) - } - } - pump() - panel.addEventListener("DOMNodeRemoved", () => { stop = true }) -} - -function renderSettingsPanel(config: AgentConfig) { - const panel = document.getElementById("tab-settings") - if (!panel) return - panel.innerHTML = ` -
-
Ambiente ${config.appUrl}
-
API ${config.apiBaseUrl}
-
Criado em ${formatDate(config.createdAt)}
-
Última sync ${formatDate(config.lastSyncedAt ?? null)}
-
-
- - -
-
- -
-
-
Intervalo do heartbeat (segundos)
-
- - -
-

Mínimo 60s. Salvar reinicia o processo de heartbeat.

-
- ` - - document.getElementById("open-app-settings")?.addEventListener("click", () => redirectToApp(config)) - document.getElementById("reset-agent-settings")?.addEventListener("click", async () => { - await stopHeartbeat().catch(() => undefined) - await clearConfig() - setAlert("Configuração removida. Reiniciando fluxo de provisionamento.", "success") - setTimeout(() => window.location.reload(), 600) - }) - document.getElementById("send-inventory")?.addEventListener("click", async () => { - setStatus("Enviando inventário…") - try { - const token = await getMachineToken() - if (!token) throw new Error("Token ausente no cofre do SO") - const [p, inv] = await Promise.all([collectMachineProfile(), collectMachineInventory()]) - const payload = { - machineToken: token, - hostname: p.hostname, - os: p.os, - metrics: p.metrics as any, - inventory: inv, - } - const response = await fetch(`${apiBaseUrl}/api/machines/inventory`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }) - if (!response.ok) throw new Error(`Falha HTTP ${response.status}`) - setAlert("Inventário enviado com sucesso.", "success") - setStatus("Inventário sincronizado.") - } catch (error) { - console.error("[agent] Falha ao enviar inventário", error) - setAlert("Falha ao enviar inventário agora.", "error") - } - }) - - document.getElementById("save-hb-interval")?.addEventListener("click", async () => { - try { - const input = document.getElementById("hb-interval") as HTMLInputElement | null - const value = Math.max(60, Number(input?.value ?? 300)) - const updated: AgentConfig = { ...config, heartbeatIntervalSec: value, lastSyncedAt: Date.now() } - await saveConfig(updated) - await stopHeartbeat().catch(() => undefined) - await startHeartbeat(updated) - setAlert("Intervalo do heartbeat atualizado.", "success") - } catch (error) { - console.error("[agent] Falha ao salvar intervalo", error) - setAlert("Falha ao salvar intervalo do heartbeat.", "error") - } - }) -} - -function formatBytes(bytes: number) { - if (!bytes || Number.isNaN(bytes)) return "—" - const units = ["B", "KB", "MB", "GB", "TB"] - let value = bytes - let index = 0 - while (value >= 1024 && index < units.length - 1) { - value /= 1024 - index += 1 - } - return `${value.toFixed(value >= 10 || index === 0 ? 0 : 1)} ${units[index]}` -} - -function formatPercent(value: number) { - if (Number.isNaN(value)) return "—" - return `${value.toFixed(1)}%` -} - -function formatDate(timestamp?: number | null) { - if (!timestamp) return "—" - try { - return new Date(timestamp).toLocaleString() - } catch { - return "—" - } -} - -function renderMachineSummary(profile: MachineProfile) { - if (!contentElement) return - const macs = profile.macAddresses.length > 0 ? profile.macAddresses.join(", ") : "—" - const serials = profile.serialNumbers.length > 0 ? profile.serialNumbers.join(", ") : "—" - const metrics = profile.metrics - const lastCollection = metrics.collectedAt ? new Date(metrics.collectedAt).toLocaleString() : "—" - - return ` -
-
Hostname: ${profile.hostname}
-
Sistema: ${profile.os.name}${profile.os.version ? ` ${profile.os.version}` : ""} (${profile.os.architecture ?? "?"})
-
Endereços MAC: ${macs}
-
Identificadores: ${serials}
-
CPU: ${metrics.cpuPhysicalCores ?? metrics.cpuLogicalCores} núcleos · uso ${formatPercent(metrics.cpuUsagePercent)}
-
Memória: ${formatBytes(metrics.memoryUsedBytes)} / ${formatBytes(metrics.memoryTotalBytes)} (${formatPercent(metrics.memoryUsedPercent)})
-
Coletado em: ${lastCollection}
-
- ` -} - -async function renderRegistered(config: AgentConfig) { - if (!contentElement) return - renderTabsShell() - try { - const [profile, inv] = await Promise.all([collectMachineProfile(), collectMachineInventory()]) - renderOverviewPanel(profile) - renderInventoryPanel(inv, profile) - renderDiagnosticsPanel(profile) - renderSettingsPanel(config) - setStatus("Máquina provisionada.") - } catch (error) { - console.error("[agent] Falha ao preparar as abas", error) - setAlert("Não foi possível preparar a interface de inventário.", "error") - } -} - -function renderProvisionForm(profile: MachineProfile) { - if (!contentElement) return - - const summary = renderMachineSummary(profile) ?? "" - - contentElement.innerHTML = ` -
-
- -
- - -
- Segredo fornecido pelo servidor -
- -
- - Use apenas se houver múltiplos ambientes - -
- -
- - Informe para vincular à empresa correta - -
- -
- - -
-
- ${summary} - ` - - const form = document.getElementById("provision-form") as HTMLFormElement | null - const refreshButton = document.getElementById("refresh-profile") - - form?.addEventListener("submit", (event) => { - event.preventDefault() - handleRegister(profile, form) - }) - - refreshButton?.addEventListener("click", async () => { - setStatus("Recolhendo informações atualizadas da máquina…") - try { - const updatedProfile = await collectMachineProfile() - renderProvisionForm(updatedProfile) - setStatus("Dados atualizados. Revise e confirme o provisionamento.") - } catch (error) { - console.error("[agent] Falha ao atualizar perfil da máquina", error) - setAlert("Não foi possível atualizar as informações da máquina.", "error") - } - }) - - // Olhinho: mostrar/ocultar segredo - const toggleBtn = document.getElementById("toggle-secret") as HTMLButtonElement | null - const secretInput = document.getElementById("provisioningSecret") as HTMLInputElement | null - toggleBtn?.addEventListener("click", () => { - if (!secretInput) return - const isPw = secretInput.type === "password" - secretInput.type = isPw ? "text" : "password" - // alterna ícone - toggleBtn.innerHTML = isPw - ? '' - : '' - }) -} - -async function handleRegister(profile: MachineProfile, form: HTMLFormElement) { - const submitButton = form.querySelector("button[type=submit]") as HTMLButtonElement | null - const formData = new FormData(form) - const provisioningSecret = (formData.get("provisioningSecret") as string | null)?.trim() - const tenantId = (formData.get("tenantId") as string | null)?.trim() - const companySlug = (formData.get("companySlug") as string | null)?.trim() - - if (!provisioningSecret) { - setAlert("Informe o código de provisionamento.", "error") - return - } - - try { - if (submitButton) { - submitButton.disabled = true - } - setAlert(null) - setStatus("Enviando dados de registro da máquina…") - - const payload = { - provisioningSecret, - tenantId: tenantId && tenantId.length > 0 ? tenantId : undefined, - companySlug: companySlug && companySlug.length > 0 ? companySlug : undefined, - hostname: profile.hostname, - os: profile.os, - macAddresses: profile.macAddresses, - serialNumbers: profile.serialNumbers, - metadata: { - inventory: profile.inventory, - metrics: profile.metrics, - }, - registeredBy: "desktop-agent", - } - - const response = await fetch(`${apiBaseUrl}/api/machines/register`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }) - - if (!response.ok) { - const status = response.status - let message = `Falha ao registrar máquina (${status})` - try { - // tenta JSON - const ct = response.headers.get("content-type") || "" - if (ct.includes("application/json")) { - const errorBody = await response.json() - if (errorBody?.error) message = `${message}: ${String(errorBody.error)}` - if (errorBody?.details) message += ` — ${String(errorBody.details)}` - } else { - const text = await response.text() - if (text && text.length < 500) message += ` — ${text}` - } - } catch { - // ignore - } - throw new Error(message) - } - - const data = (await response.json()) as MachineRegisterResponse - // Guarda token localmente (Store). Em produção, podemos trocar por keyring do SO. - await setMachineToken(data.machineToken) - const config: AgentConfig = { - machineId: data.machineId, - tenantId: data.tenantId ?? null, - companySlug: data.companySlug ?? null, - machineEmail: data.machineEmail ?? null, - apiBaseUrl, - appUrl, - createdAt: Date.now(), - lastSyncedAt: Date.now(), - expiresAt: data.expiresAt ?? null, - } - - await saveConfig(config) - await startHeartbeat(config) - - setAlert("Máquina registrada com sucesso! Abrindo a interface web…", "success") - setStatus("Autenticando dispositivo e abrindo o Sistema de Chamados.") - - setTimeout(() => redirectToApp(config), 800) - } catch (error) { - console.error("[agent] Erro no registro da máquina", error) - const message = error instanceof Error ? error.message : "Erro desconhecido ao registrar a máquina." - const normalized = message.toLowerCase() - if (normalized.includes("failed to fetch") || normalized.includes("load failed") || normalized.includes("network")) { - setAlert("Não foi possível se conectar ao servidor. Verifique a conexão e o endereço configurado.", "error") - } else if (normalized.includes("401") || normalized.includes("403") || normalized.includes("código de provisionamento inválido")) { - setAlert("Código de provisionamento inválido. Confirme o segredo configurado no servidor e tente novamente.", "error") - } else { - setAlert(message, "error") - } - setStatus("Revise os dados e tente novamente.") - if (submitButton) { - submitButton.disabled = false - } - } -} - -function redirectToApp(config: AgentConfig) { - const perform = async () => { - const token = await getMachineToken() - if (!token) { - setAlert("Token da máquina não encontrado. Reprovisione a máquina.", "error") - return - } - const url = `${config.appUrl}/machines/handshake?token=${encodeURIComponent(token)}` - try { - await openUrl(url) - setAlert("Abrindo o Sistema de Chamados no navegador padrão…", "success") - } catch { - // fallback: navegar dentro da WebView - window.location.replace(url) - } - } - void perform() -} - -async function ensureHeartbeat(config: AgentConfig): Promise { - const adjustedConfig = { - ...config, - apiBaseUrl, - appUrl, - lastSyncedAt: Date.now(), - } - - await saveConfig(adjustedConfig) - await startHeartbeat(adjustedConfig) - - return adjustedConfig -} - -async function bootstrap() { - setStatus("Iniciando agente desktop…") - setAlert(null) - - try { - const stored = await loadConfig() - const token = await getMachineToken() - if (stored && token) { - const updated = await ensureHeartbeat(stored) - await renderRegistered(updated) - return - } - } catch (error) { - console.error("[agent] Falha ao iniciar com configuração existente", error) - setAlert("Não foi possível carregar a configuração armazenada. Você poderá reprovisionar abaixo.", "error") - } - - try { - setStatus("Coletando informações básicas da máquina…") - const profile = await collectMachineProfile() - renderProvisionForm(profile) - setStatus("Informe o código de provisionamento para registrar esta máquina.") - } catch (error) { - console.error("[agent] Falha ao coletar dados da máquina", error) - setAlert("Não foi possível coletar dados da máquina. Verifique permissões do sistema e tente novamente.", "error") - setStatus("Interação necessária para continuar.") - } -} - -document.addEventListener("DOMContentLoaded", () => { - void bootstrap() -}) diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx index 1b70305..717122c 100644 --- a/apps/desktop/src/main.tsx +++ b/apps/desktop/src/main.tsx @@ -1,8 +1,10 @@ -import React, { useEffect, useMemo, useState } from "react" +import { useEffect, useState } from "react" import { createRoot } from "react-dom/client" import { invoke } from "@tauri-apps/api/core" import { Store } from "@tauri-apps/plugin-store" import { ExternalLink, Eye, EyeOff } from "lucide-react" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs" +import { cn } from "./lib/utils" type MachineOs = { name: string @@ -117,6 +119,7 @@ function App() { const [profile, setProfile] = useState(null) const [error, setError] = useState(null) const [busy, setBusy] = useState(false) + const [status, setStatus] = useState(null) const [showSecret, setShowSecret] = useState(false) const [provisioningSecret, setProvisioningSecret] = useState("") @@ -138,6 +141,7 @@ function App() { const p = await invoke("collect_machine_profile") setProfile(p) } + setStatus(t ? "online" : null) } catch (err) { setError("Falha ao carregar estado do agente.") } @@ -182,6 +186,7 @@ function App() { await writeConfig(store, cfg) setConfig(cfg); setToken(data.machineToken) await invoke("start_machine_agent", { baseUrl: apiBaseUrl, token: data.machineToken, status: "online", intervalSeconds: 300 }) + setStatus("online") } catch (err) { setError(err instanceof Error ? err.message : String(err)) } finally { @@ -204,15 +209,45 @@ function App() { async function reprovision() { if (!store) return await store.delete("token"); await store.delete("config"); await store.save() - setToken(null); setConfig(null) + setToken(null); setConfig(null); setStatus(null) const p = await invoke("collect_machine_profile") setProfile(p) } + async function sendInventoryNow() { + if (!token || !profile) return + setBusy(true); setError(null) + try { + const payload = { + machineToken: token, + hostname: profile.hostname, + os: profile.os, + metrics: profile.metrics, + inventory: profile.inventory, + } + const res = await fetch(`${apiBaseUrl}/api/machines/inventory`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }) + if (!res.ok) { + const text = await res.text() + throw new Error(`Falha ao enviar inventário (${res.status}): ${text.slice(0, 200)}`) + } + } catch (err) { + setError(err instanceof Error ? err.message : String(err)) + } finally { + setBusy(false) + } + } + return (
-
-

Sistema de Chamados — Agente Desktop

+
+
+

Sistema de Chamados — Agente Desktop

+ +
{error ?

{error}

: null} {!token ? (
@@ -267,14 +302,66 @@ function App() {
) : ( -
-

Máquina provisionada.

-
- - -
+
+ + + Resumo + Inventário + Diagnóstico + Configurações + + +
+
+
CPU
+
{profile ? pct(profile.metrics.cpuUsagePercent) : "—"}
+
+
+
Memória
+
{profile ? `${bytes(profile.metrics.memoryUsedBytes)} / ${bytes(profile.metrics.memoryTotalBytes)}` : "—"}
+
+
+
+ + +
+
+ +

Inventário básico coletado localmente. Envie para sincronizar com o servidor.

+
+
+
Hostname
+
{profile?.hostname ?? "—"}
+
+
+
Sistema
+
{profile?.os?.name ?? "—"} {profile?.os?.version ?? ""}
+
+
+
+ + +
+
+ +
+

Token armazenado: {token?.slice(0, 6)}…{token?.slice(-6)}

+

Base URL: {apiBaseUrl}

+
+
+ +
+ + setCollabEmail(e.target.value)} /> +
+
+ + setCollabName(e.target.value)} /> +
+
+
)}
@@ -282,6 +369,22 @@ function App() { ) } +function StatusBadge({ status }: { status: string | null }) { + const s = (status ?? "").toLowerCase() + const label = s === "online" ? "Online" : s === "offline" ? "Offline" : s === "maintenance" ? "Manutenção" : "Sem status" + const dot = s === "online" ? "bg-emerald-500" : s === "offline" ? "bg-rose-500" : s === "maintenance" ? "bg-amber-500" : "bg-slate-400" + const ring = s === "online" ? "bg-emerald-400/30" : s === "offline" ? "bg-rose-400/30" : s === "maintenance" ? "bg-amber-400/30" : "bg-slate-300/30" + const isOnline = s === "online" + return ( + + + + {isOnline ? : null} + + {label} + + ) +} + const root = document.getElementById("root") || (() => { const el = document.createElement("div"); el.id = "root"; document.body.appendChild(el); return el })() createRoot(root).render() - diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css deleted file mode 100644 index 322173f..0000000 --- a/apps/desktop/src/styles.css +++ /dev/null @@ -1,266 +0,0 @@ -:root { - color-scheme: light; /* força tema claro para máxima legibilidade */ - font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - font-size: 16px; - line-height: 1.5; - background-color: #f8fafc; /* slate-50 */ - color: #0f172a; /* slate-900 */ - margin: 0; -} - -body { - margin: 0; -} - -.app-root { - min-height: 100vh; - display: grid; - place-items: center; - padding: 24px; - background: radial-gradient(circle at top, rgba(59, 130, 246, 0.12), transparent 60%), - radial-gradient(circle at bottom, rgba(16, 185, 129, 0.12), transparent 55%); -} - -.card { - width: min(520px, 100%); - background-color: #ffffff; - border-radius: 16px; - box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12); - padding: 28px; -} - -.card header h1 { - margin: 0; - font-size: 1.75rem; -} - -.subtitle { - margin: 4px 0 0; - color: #475569; - font-size: 0.95rem; -} - -.alert { - margin-top: 16px; - font-size: 0.95rem; - color: #0f172a; - background-color: #e0f2fe; - border-radius: 12px; - padding: 12px 14px; - display: none; -} - -.alert.visible { - display: block; -} - -.alert.error { - background-color: #fee2e2; - color: #b91c1c; -} - -.alert.success { - background-color: #dcfce7; - color: #166534; -} - -form { - display: grid; - gap: 12px; - margin-top: 18px; -} - -label { - display: block; - font-weight: 600; - color: #0f172a; -} - -label span.optional { - font-weight: 400; - color: #64748b; - font-size: 0.85rem; -} - -.field { display: grid; gap: 6px; } -.label { font-size: 0.9rem; } - -.input-group { position: relative; } -.input-group input { width: 100%; padding-right: 40px; } -.icon-button { - position: absolute; - top: 50%; - right: 8px; - transform: translateY(-50%); - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; height: 28px; - border-radius: 6px; - border: 1px solid transparent; - background: #f1f5f9; - color: #334155; - cursor: pointer; -} -.icon-button:hover { background: #e2e8f0; } -.icon-eye, .icon-eye-off { width: 18px; height: 18px; } - -input, -select { - padding: 10px 12px; - border-radius: 10px; - border: 1px solid #cbd5e1; /* slate-300 */ - font-size: 1rem; - background-color: #ffffff; - color: #0f172a; - transition: border-color 0.2s ease, box-shadow 0.2s ease; -} - -input:focus, -select:focus { - outline: none; - border-color: #2563eb; - box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.25); -} - -button { - padding: 12px 16px; - border-radius: 12px; - border: none; - background-color: #2563eb; - color: #ffffff; - font-weight: 600; - font-size: 1rem; - cursor: pointer; - transition: background-color 0.2s ease, transform 0.2s ease; -} - -button.secondary { - background: none; - color: #2563eb; -} - -button:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -button:hover:not(:disabled) { - background-color: #1d4ed8; - transform: translateY(-1px); -} - -.machine-summary { - margin-top: 18px; - padding: 14px; - border-radius: 12px; - background-color: rgba(15, 23, 42, 0.05); - display: grid; - gap: 8px; - font-size: 0.95rem; -} - -.machine-summary strong { - font-weight: 600; -} - -.actions { - display: flex; - justify-content: space-between; - align-items: center; - gap: 12px; - margin-top: 20px; -} - -.actions button { - flex: 1; -} - -.status-text { - margin-top: 16px; - font-size: 0.95rem; - color: #334155; -} - -/* Tabs (desktop app shell) */ -.tabs { - margin-top: 18px; -} -.tab-list { - display: flex; - gap: 8px; - border-bottom: 1px solid rgba(148, 163, 184, 0.4); - padding-bottom: 8px; -} -.tab-list button { - background: none; - color: #334155; - border: 1px solid transparent; - border-bottom: 2px solid transparent; - padding: 8px 12px; - border-radius: 10px 10px 0 0; -} -.tab-list button.active { - background-color: rgba(241, 245, 249, 0.8); - border-color: rgba(148, 163, 184, 0.6); - border-bottom-color: #2563eb; - color: #0f172a; -} -.tab-panel { - display: none; - padding-top: 12px; -} -.tab-panel.active { - display: block; -} - -/* Summary cards */ -.cards-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); - gap: 12px; - margin-top: 8px; -} -.card-item { - display: flex; - align-items: center; - gap: 10px; - border: 1px solid rgba(148, 163, 184, 0.5); - background-color: rgba(248, 250, 252, 0.85); - padding: 10px 12px; - border-radius: 12px; -} -.card-icon { - font-size: 18px; -} - -.spinner { - width: 18px; - height: 18px; - border-radius: 50%; - border: 3px solid rgba(37, 99, 235, 0.14); - border-top-color: #2563eb; - animation: spin 0.8s linear infinite; - display: inline-block; - vertical-align: middle; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -@media (prefers-color-scheme: dark) { - /* forçamos visual claro também em modo escuro do sistema */ - :root { background-color: #f8fafc; color: #0f172a; } - .card { background-color: #ffffff; color: #0f172a; box-shadow: 0 10px 30px rgba(15,23,42,0.12); } - .subtitle { color: #475569; } - .alert { background-color: #e0f2fe; color: #0f172a; } - .alert.error { background-color: #fee2e2; color: #b91c1c; } - .alert.success { background-color: #dcfce7; color: #166534; } - input, select { background-color: #ffffff; border-color: #cbd5e1; } - button.secondary { color: #2563eb; } - .machine-summary { background-color: rgba(15, 23, 42, 0.05); } - .status-text { color: #334155; } -} diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 75abdef..0a3d9c7 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -17,7 +17,9 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "jsx": "react-jsx", + "types": ["vite/client"] }, "include": ["src"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2acfa54..c8dff5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -216,6 +216,9 @@ importers: apps/desktop: dependencies: + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.13(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tauri-apps/api': specifier: ^2 version: 2.8.0 @@ -225,10 +228,22 @@ importers: '@tauri-apps/plugin-store': specifier: ^2 version: 2.4.0 + lucide-react: + specifier: ^0.544.0 + version: 0.544.0(react@19.2.0) + react: + specifier: ^19.0.0 + version: 19.2.0 + react-dom: + specifier: ^19.0.0 + version: 19.2.0(react@19.2.0) devDependencies: '@tauri-apps/cli': specifier: ^2 version: 2.8.4 + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@6.3.6(@types/node@20.19.19)(jiti@2.6.1)(lightningcss@1.30.1)) typescript: specifier: ~5.6.2 version: 5.6.3 @@ -242,10 +257,93 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.28.4': resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + '@better-auth/core@1.3.26': resolution: {integrity: sha512-S5ooXaOcn9eLV3/JayfbMsAB5PkfoTRaRrtpb5djwvI/UAJOgLyjqhd+rObsBycovQ/nPQvMKjzyM/G1oBKngA==} @@ -1443,6 +1541,9 @@ packages: '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/rollup-android-arm-eabi@4.52.4': resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} cpu: [arm] @@ -1919,6 +2020,18 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -2157,6 +2270,12 @@ packages: cpu: [x64] os: [win32] + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -2286,6 +2405,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.8.16: + resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==} + hasBin: true + better-auth@1.3.26: resolution: {integrity: sha512-umaOGmv29yF4sD6o2zlW6B0Oayko5yD/A8mKJOFDDEIoVP/pR7nJ/2KsqKy+xvBpnDsKdrZseqA8fmczPviUHw==} peerDependencies: @@ -2331,6 +2454,11 @@ packages: brotli@1.3.3: resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + browserslist@4.26.3: + resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -2419,6 +2547,9 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + convex@1.27.3: resolution: {integrity: sha512-Ebr9lPgXkL7JO5IFr3bG+gYvHskyJjc96Fx0BBNkJUDXrR/bd9/uI4q8QszbglK75XfDu068vR0d/HK2T7tB9Q==} engines: {node: '>=18.0.0', npm: '>=7.0.0'} @@ -2600,6 +2731,9 @@ packages: effect@3.16.12: resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==} + electron-to-chromium@1.5.234: + resolution: {integrity: sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==} + emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -2660,6 +2794,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2878,6 +3016,10 @@ packages: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -3123,6 +3265,11 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -3136,6 +3283,11 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -3252,6 +3404,9 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react@0.544.0: resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==} peerDependencies: @@ -3350,6 +3505,9 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-releases@2.0.23: + resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} + nypm@0.6.2: resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==} engines: {node: ^14.16.0 || >=16.10.0} @@ -3602,6 +3760,10 @@ packages: peerDependencies: react: ^19.0.0 + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -4012,6 +4174,12 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -4183,6 +4351,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -4216,8 +4387,120 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/runtime@7.28.4': {} + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@better-auth/core@1.3.26': dependencies: better-call: 1.0.19 @@ -5283,6 +5566,8 @@ snapshots: '@remirror/core-constants@3.0.0': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/rollup-android-arm-eabi@4.52.4': optional: true @@ -5704,6 +5989,27 @@ snapshots: tslib: 2.8.1 optional: true + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.4 + '@types/d3-array@3.2.2': {} '@types/d3-color@3.1.3': {} @@ -5939,6 +6245,18 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vitejs/plugin-react@4.7.0(vite@6.3.6(@types/node@20.19.19)(jiti@2.6.1)(lightningcss@1.30.1))': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.3.6(@types/node@20.19.19)(jiti@2.6.1)(lightningcss@1.30.1) + transitivePeerDependencies: + - supports-color + '@vitest/expect@2.1.9': dependencies: '@vitest/spy': 2.1.9 @@ -6099,6 +6417,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.8.16: {} + better-auth@1.3.26(next@15.5.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@better-auth/core': 1.3.26 @@ -6144,6 +6464,14 @@ snapshots: dependencies: base64-js: 1.5.1 + browserslist@4.26.3: + dependencies: + baseline-browser-mapping: 2.8.16 + caniuse-lite: 1.0.30001747 + electron-to-chromium: 1.5.234 + node-releases: 2.0.23 + update-browserslist-db: 1.1.3(browserslist@4.26.3) + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -6234,6 +6562,8 @@ snapshots: consola@3.4.2: {} + convert-source-map@2.0.0: {} + convex@1.27.3(react@19.2.0): dependencies: esbuild: 0.25.4 @@ -6394,6 +6724,8 @@ snapshots: '@standard-schema/spec': 1.0.0 fast-check: 3.23.2 + electron-to-chromium@1.5.234: {} + emoji-regex@9.2.2: {} empathic@2.0.0: {} @@ -6562,6 +6894,8 @@ snapshots: '@esbuild/win32-ia32': 0.25.4 '@esbuild/win32-x64': 0.25.4 + escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} eslint-config-next@15.5.4(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): @@ -6863,6 +7197,8 @@ snapshots: generator-function@2.0.1: {} + gensync@1.0.0-beta.2: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -7119,6 +7455,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsesc@3.1.0: {} + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} @@ -7129,6 +7467,8 @@ snapshots: dependencies: minimist: 1.2.8 + json5@2.2.3: {} + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -7225,6 +7565,10 @@ snapshots: loupe@3.2.1: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + lucide-react@0.544.0(react@19.2.0): dependencies: react: 19.2.0 @@ -7311,6 +7655,8 @@ snapshots: node-fetch-native@1.6.7: {} + node-releases@2.0.23: {} + nypm@0.6.2: dependencies: citty: 0.1.6 @@ -7605,6 +7951,8 @@ snapshots: react: 19.2.0 scheduler: 0.25.0 + react-refresh@0.17.0: {} + react-remove-scroll-bar@2.3.8(@types/react@19.2.0)(react@19.2.0): dependencies: react: 19.2.0 @@ -8124,6 +8472,12 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + update-browserslist-db@1.1.3(browserslist@4.26.3): + dependencies: + browserslist: 4.26.3 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -8304,6 +8658,8 @@ snapshots: word-wrap@1.2.5: {} + yallist@3.1.1: {} + yallist@5.0.0: {} yocto-queue@0.1.0: {} diff --git a/src/app/admin/machines/[id]/page.tsx b/src/app/admin/machines/[id]/page.tsx index d5a6809..7799167 100644 --- a/src/app/admin/machines/[id]/page.tsx +++ b/src/app/admin/machines/[id]/page.tsx @@ -1,40 +1,22 @@ "use client" import Link from "next/link" -import { use, useMemo } from "react" -import { useQuery } from "convex/react" -import type { Id } from "@/convex/_generated/dataModel" +import { use } from "react" import { AppShell } from "@/components/app-shell" import { SiteHeader } from "@/components/site-header" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { AdminMachineDetailsClient } from "@/components/admin/machines/admin-machine-details.client" -import { api } from "@/convex/_generated/api" -import { useAuth } from "@/lib/auth-client" +import { MachineBreadcrumbs } from "@/components/admin/machines/machine-breadcrumbs.client" export const runtime = "nodejs" export const dynamic = "force-dynamic" export default function AdminMachineDetailsPage({ params }: { params: Promise<{ id: string }> }) { const { id } = use(params) - const { convexUserId } = useAuth() - const machines = useQuery( - convexUserId ? api.machines.listByTenant : "skip", - convexUserId ? { tenantId: DEFAULT_TENANT_ID, includeMetadata: false } : ("skip" as const) - ) as Array<{ _id: Id<"machines">; hostname: string }> | undefined - const hostname = useMemo(() => machines?.find((m) => m._id === (id as unknown as Id<"machines">))?.hostname ?? "Hostname", [machines, id]) - return ( }>
- +
diff --git a/src/components/admin/machines/admin-machines-overview.tsx b/src/components/admin/machines/admin-machines-overview.tsx index a1784f7..5fe284f 100644 --- a/src/components/admin/machines/admin-machines-overview.tsx +++ b/src/components/admin/machines/admin-machines-overview.tsx @@ -5,7 +5,7 @@ import { useQuery } from "convex/react" import { format, formatDistanceToNowStrict } from "date-fns" import { ptBR } from "date-fns/locale" import { toast } from "sonner" -import { ClipboardCopy, ServerCog, Cpu, MemoryStick, Monitor, HardDrive, Pencil, ShieldCheck, ShieldAlert } from "lucide-react" +import { ClipboardCopy, ServerCog, Cpu, MemoryStick, Monitor, HardDrive, Pencil, ShieldCheck, ShieldAlert, Apple, Terminal } from "lucide-react" import { api } from "@/convex/_generated/api" import { Badge } from "@/components/ui/badge" @@ -42,6 +42,10 @@ type MachineSoftware = { source?: string } +// Forward declaration to ensure props are known before JSX usage +type DetailLineProps = { label: string; value?: string | number | null; classNameValue?: string } +declare function DetailLine(props: DetailLineProps): JSX.Element + type LinuxExtended = { lsblk?: unknown lspci?: string @@ -71,6 +75,16 @@ type WindowsExtended = { }> videoControllers?: Array<{ Name?: string; AdapterRAM?: number; DriverVersion?: string; PNPDeviceID?: string }> disks?: Array<{ Model?: string; SerialNumber?: string; Size?: number; InterfaceType?: string; MediaType?: string }> + osInfo?: { + ProductName?: string + CurrentBuild?: string | number + CurrentBuildNumber?: string | number + DisplayVersion?: string + ReleaseId?: string + EditionID?: string + LicenseStatus?: number + IsActivated?: boolean + } } type MacExtended = { @@ -213,12 +227,12 @@ function getStatusVariant(status?: string | null) { } } -function osIcon(osName?: string | null) { +function OsIcon({ osName }: { osName?: string | null }) { const name = (osName ?? "").toLowerCase() - if (name.includes("windows")) return "🪟" - if (name.includes("mac") || name.includes("darwin") || name.includes("macos")) return "" - if (name.includes("linux")) return "🐧" - return "🖥️" + if (name.includes("mac") || name.includes("darwin") || name.includes("macos")) return + if (name.includes("linux")) return + // fallback para Windows/outros como monitor genérico + return } export function AdminMachinesOverview({ tenantId }: { tenantId: string }) { @@ -375,11 +389,11 @@ function MachineStatusBadge({ status }: { status?: string | null }) { const isOnline = s === "online" return ( - + {isOnline ? ( - + ) : null} {label} @@ -407,6 +421,11 @@ type MachineDetailsProps = { export function MachineDetails({ machine }: MachineDetailsProps) { const { convexUserId } = useAuth() + // Company name lookup (by slug) + const companies = useQuery( + convexUserId && machine ? api.companies.list : "skip", + convexUserId && machine ? { tenantId: machine.tenantId, viewerId: convexUserId as any } : ("skip" as const) + ) as Array<{ id: string; name: string; slug?: string }> | undefined const metadata = machine?.inventory ?? null const metrics = machine?.metrics ?? null const hardware = metadata?.hardware ?? null @@ -463,6 +482,22 @@ export function MachineDetails({ machine }: MachineDetailsProps) { } } + // collaborator (from inventory metadata, when provided by onboarding) + type Collaborator = { email?: string; name?: string } + const collaborator: Collaborator | null = (() => { + if (!metadata || typeof metadata !== "object") return null + const inv = metadata as Record + const c = inv["collaborator"] + if (c && typeof c === "object") return c as Collaborator + return null + })() + + const companyName = (() => { + if (!companies || !machine?.companySlug) return machine?.companySlug ?? null + const found = companies.find((c) => c.slug === machine.companySlug) + return found?.name ?? machine.companySlug + })() + const [renaming, setRenaming] = useState(false) const [newName, setNewName] = useState(machine?.hostname ?? "") @@ -489,48 +524,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) { return jsonText.replace(new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"), (m) => `__HIGHLIGHT__${m}__END__`) }, [jsonText, dialogQuery]) - const exportInventoryJson = () => { - if (!machine) return - const payload = { - id: machine.id, - hostname: machine.hostname, - status: machine.status, - lastHeartbeatAt: machine.lastHeartbeatAt, - metrics, - inventory: metadata, - postureAlerts: machine.postureAlerts ?? null, - lastPostureAt: machine.lastPostureAt ?? null, - } - const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }) - const url = URL.createObjectURL(blob) - const a = document.createElement("a") - a.href = url - a.download = `inventario-${machine.hostname}.json` - document.body.appendChild(a) - a.click() - a.remove() - URL.revokeObjectURL(url) - } - - const copyInventoryJson = async () => { - if (!machine) return - const payload = { - id: machine.id, - hostname: machine.hostname, - status: machine.status, - lastHeartbeatAt: machine.lastHeartbeatAt, - metrics, - inventory: metadata, - postureAlerts: machine.postureAlerts ?? null, - lastPostureAt: machine.lastPostureAt ?? null, - } - try { - await navigator.clipboard.writeText(JSON.stringify(payload, null, 2)) - toast.success("Inventário copiado para a área de transferência.") - } catch { - toast.error("Não foi possível copiar o inventário.") - } - } + // removed copy/export inventory JSON buttons as requested return ( @@ -567,12 +561,32 @@ export function MachineDetails({ machine }: MachineDetailsProps) { {/* ping integrado na badge de status */}
- {osIcon(machine.osName)} + {machine.osName ?? "SO desconhecido"} {machine.osVersion ?? ""} {machine.architecture?.toUpperCase() ?? "Arquitetura indefinida"} + {windowsExt?.osInfo ? ( + + Build: {String((windowsExt.osInfo as any)?.CurrentBuildNumber ?? (windowsExt.osInfo as any)?.CurrentBuild ?? "—")} + + ) : null} + {windowsExt?.osInfo ? ( + + Ativado: {((windowsExt.osInfo as any)?.IsActivated === true) ? "Sim" : "Não"} + + ) : null} + {companyName ? ( + + Empresa: {companyName} + + ) : null} + {collaborator?.email ? ( + + Colaborador: {collaborator?.name ? `${collaborator.name} · ` : ""}{collaborator.email} + + ) : null}
{machine.authEmail ? ( @@ -581,11 +595,10 @@ export function MachineDetails({ machine }: MachineDetailsProps) { Copiar e-mail ) : null} - {machine.registeredBy ? ( - Registrada via {machine.registeredBy} + + Registrada via {machine.registeredBy} + ) : null}
@@ -1141,8 +1154,6 @@ export function MachineDetails({ machine }: MachineDetailsProps) { ) : null}
- - {Array.isArray(software) && software.length > 0 ? ( ) : null} @@ -1329,7 +1340,7 @@ function MachineCard({ machine }: { machine: MachinesQueryItem }) { ) } -function DetailLine({ label, value }: { label: string; value?: string | number | null }) { +function DetailLine({ label, value, classNameValue }: { label: string; value?: string | number | null; classNameValue?: string }) { if (value === null || value === undefined) return null if (typeof value === "string" && (value.trim() === "" || value === "undefined" || value === "null")) { return null @@ -1337,7 +1348,7 @@ function DetailLine({ label, value }: { label: string; value?: string | number | return (
{label} - {value} + {value}
) } diff --git a/src/components/admin/machines/machine-breadcrumbs.client.tsx b/src/components/admin/machines/machine-breadcrumbs.client.tsx new file mode 100644 index 0000000..990206a --- /dev/null +++ b/src/components/admin/machines/machine-breadcrumbs.client.tsx @@ -0,0 +1,30 @@ +"use client" + +import Link from "next/link" +import { useMemo } from "react" +import { useQuery } from "convex/react" +import type { Id } from "@/convex/_generated/dataModel" +import { api } from "@/convex/_generated/api" +import { useAuth } from "@/lib/auth-client" + +export function MachineBreadcrumbs({ tenantId, machineId }: { tenantId: string; machineId: string }) { + const { convexUserId } = useAuth() + const list = useQuery( + convexUserId ? api.machines.listByTenant : "skip", + convexUserId ? { tenantId, includeMetadata: false } : ("skip" as const) + ) as Array<{ id: Id<"machines">; hostname: string }> | undefined + const hostname = useMemo(() => list?.find((m) => m.id === (machineId as unknown as Id<"machines">))?.hostname ?? "Detalhe", [list, machineId]) + + return ( + + ) +} +