feat(desktop): migra abas do Tauri para shadcn/Radix Tabs, adiciona status badge e botão 'Enviar inventário agora'\n\nfix(web): corrige tipo do DetailLine (classNameValue) para build no CI\n\nchore(prisma): padroniza fluxo local DEV com DATABASE_URL=file:./prisma/db.dev.sqlite (db push + seed)\n\nchore: atualiza pnpm-lock.yaml após dependências do desktop
This commit is contained in:
parent
ce4b935e0c
commit
e3d6fea412
13 changed files with 683 additions and 1118 deletions
|
|
@ -10,6 +10,7 @@
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@tauri-apps/plugin-store": "^2",
|
"@tauri-apps/plugin-store": "^2",
|
||||||
|
|
|
||||||
|
|
@ -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 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!([]));
|
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)
|
// 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 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!({}));
|
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,
|
"services": services,
|
||||||
"defender": defender,
|
"defender": defender,
|
||||||
"hotfix": hotfix,
|
"hotfix": hotfix,
|
||||||
|
"osInfo": os_info,
|
||||||
"cpu": cpu,
|
"cpu": cpu,
|
||||||
"baseboard": baseboard,
|
"baseboard": baseboard,
|
||||||
"bios": bios,
|
"bios": bios,
|
||||||
|
|
|
||||||
36
apps/desktop/src/components/ui/tabs.tsx
Normal file
36
apps/desktop/src/components/ui/tabs.tsx
Normal file
|
|
@ -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<typeof TabsPrimitive.Root>) {
|
||||||
|
return <TabsPrimitive.Root data-slot="tabs" className={cn("flex flex-col gap-2", className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
data-slot="tabs-list"
|
||||||
|
className={cn("inline-flex h-9 w-fit items-center justify-center rounded-lg bg-slate-100 p-[3px] text-slate-600", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
data-slot="tabs-trigger"
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 py-1 text-sm font-medium text-slate-800 transition-[color,box-shadow] data-[state=active]:bg-white data-[state=active]:shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
|
return <TabsPrimitive.Content data-slot="tabs-content" className={cn("flex-1 outline-none", className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -12,3 +12,50 @@ body {
|
||||||
@apply bg-slate-50 text-slate-900;
|
@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;
|
||||||
|
}
|
||||||
|
|
|
||||||
4
apps/desktop/src/lib/utils.ts
Normal file
4
apps/desktop/src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export function cn(...classes: Array<string | false | null | undefined>) {
|
||||||
|
return classes.filter(Boolean).join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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<Store> {
|
|
||||||
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<AgentConfig | null> {
|
|
||||||
try {
|
|
||||||
const store = await ensureStoreLoaded()
|
|
||||||
const record = await store.get<AgentConfig>("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<string | null> {
|
|
||||||
const store = await ensureStoreLoaded()
|
|
||||||
const token = await store.get<string>("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<MachineProfile> {
|
|
||||||
return await invoke<MachineProfile>("collect_machine_profile")
|
|
||||||
}
|
|
||||||
|
|
||||||
async function collectMachineInventory(): Promise<Record<string, unknown>> {
|
|
||||||
return await invoke<Record<string, unknown>>("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 = `
|
|
||||||
<div class="tabs">
|
|
||||||
<div class="tab-list">
|
|
||||||
<button class="active" data-tab="overview">Resumo</button>
|
|
||||||
<button data-tab="inventory">Inventário</button>
|
|
||||||
<button data-tab="diagnostics">Diagnóstico</button>
|
|
||||||
<button data-tab="settings">Configurações</button>
|
|
||||||
</div>
|
|
||||||
<section id="tab-overview" class="tab-panel active"></section>
|
|
||||||
<section id="tab-inventory" class="tab-panel"></section>
|
|
||||||
<section id="tab-diagnostics" class="tab-panel"></section>
|
|
||||||
<section id="tab-settings" class="tab-panel"></section>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
const buttons = contentElement.querySelectorAll<HTMLButtonElement>(".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<HTMLElement>(".tab-panel")
|
|
||||||
panels.forEach((p) => p.classList.remove("active"))
|
|
||||||
const panel = contentElement.querySelector<HTMLElement>(`#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 = `
|
|
||||||
<p>Máquina provisionada e com heartbeat ativo.</p>
|
|
||||||
${summary}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderInventoryPanel(inv: Record<string, unknown>, 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 = `
|
|
||||||
<div class="machine-summary">
|
|
||||||
<div><strong>Rede (interfaces)</strong></div>
|
|
||||||
${(network as any[])
|
|
||||||
.map((iface) => `<div>${iface.name ?? "iface"} · ${iface.mac ?? "—"} · ${iface.ip ?? "—"}</div>`)
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
let disksHtml = ""
|
|
||||||
if (disks.length > 0) {
|
|
||||||
disksHtml = `
|
|
||||||
<div class="machine-summary">
|
|
||||||
<div><strong>Discos e partições</strong></div>
|
|
||||||
${disks
|
|
||||||
.map(
|
|
||||||
(d) =>
|
|
||||||
`<div>${d.name ?? "disco"} · ${d.mountPoint ?? "—"} · ${d.fs ?? "?"} · ${formatBytes(Number(d.totalBytes))} (${formatBytes(Number(d.availableBytes))} livre)</div>`
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
? `<div class="machine-summary"><div><strong>Softwares (amostra)</strong></div>${software
|
|
||||||
.slice(0, 8)
|
|
||||||
.map((s, i) => `<div>${s.name ?? s.DisplayName ?? `Software ${i + 1}`} ${s.version ?? s.DisplayVersion ?? ""}</div>`)
|
|
||||||
.join("")}${software.length > 8 ? `<div class="text-xs">+${software.length - 8} itens</div>` : ""}</div>`
|
|
||||||
: ""
|
|
||||||
const servicesHtml = services.length
|
|
||||||
? `<div class="machine-summary"><div><strong>Serviços (amostra)</strong></div>${services
|
|
||||||
.slice(0, 8)
|
|
||||||
.map((svc) => `<div>${svc.name ?? svc.Name ?? "serviço"} · ${svc.status ?? svc.Status ?? "?"}</div>`)
|
|
||||||
.join("")}${services.length > 8 ? `<div class="text-xs">+${services.length - 8} itens</div>` : ""}</div>`
|
|
||||||
: ""
|
|
||||||
|
|
||||||
panel.innerHTML = `
|
|
||||||
<div class="cards-grid">
|
|
||||||
<div class="card-item"><span class="card-icon">🧠</span><div><div class="text-xs">CPU</div><div class="text-sm"><strong>${formatPercent(mem.cpuUsagePercent)}</strong></div></div></div>
|
|
||||||
<div class="card-item"><span class="card-icon">🧩</span><div><div class="text-xs">Memória</div><div class="text-sm"><strong>${formatBytes(mem.memoryUsedBytes)}</strong> / ${formatBytes(mem.memoryTotalBytes)}</div></div></div>
|
|
||||||
<div class="card-item"><span class="card-icon">🖥️</span><div><div class="text-xs">Sistema</div><div class="text-sm"><strong>${profile.os.name}</strong></div></div></div>
|
|
||||||
<div class="card-item"><span class="card-icon">💾</span><div><div class="text-xs">Discos</div><div class="text-sm"><strong>${disksCount}</strong> · ${formatBytes(totalDisk)}</div></div></div>
|
|
||||||
</div>
|
|
||||||
${networkHtml}
|
|
||||||
${disksHtml}
|
|
||||||
${softwareHtml}
|
|
||||||
${servicesHtml}
|
|
||||||
<div class="actions"><button id="refresh-inventory" class="secondary">Atualizar inventário</button></div>
|
|
||||||
`
|
|
||||||
|
|
||||||
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 = `
|
|
||||||
<div class="machine-summary">
|
|
||||||
<div><strong>CPU</strong> ${formatPercent(m.cpuUsagePercent)}</div>
|
|
||||||
<div><strong>Memória</strong> ${formatBytes(m.memoryUsedBytes)} / ${formatBytes(m.memoryTotalBytes)} (${formatPercent(m.memoryUsedPercent)})</div>
|
|
||||||
<div><strong>Uptime</strong> ${m.uptimeSeconds}s</div>
|
|
||||||
<div><strong>Load</strong> ${[m.loadAverageOne, m.loadAverageFive, m.loadAverageFifteen]
|
|
||||||
.map((v) => (v ? v.toFixed(2) : "—"))
|
|
||||||
.join(" / ")}</div>
|
|
||||||
</div>
|
|
||||||
<div class="machine-summary">
|
|
||||||
<div><strong>Histórico (curto)</strong></div>
|
|
||||||
<canvas id="diag-cpu" width="520" height="100" style="background:rgba(148,163,184,0.15); border-radius:8px;"></canvas>
|
|
||||||
<canvas id="diag-mem" width="520" height="100" style="background:rgba(148,163,184,0.15); border-radius:8px;"></canvas>
|
|
||||||
<p class="text-xs">Amostras locais a cada ~3s, mantemos ~60 pontos.</p>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
const cpuCanvas = panel.querySelector<HTMLCanvasElement>("#diag-cpu")
|
|
||||||
const memCanvas = panel.querySelector<HTMLCanvasElement>("#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 = `
|
|
||||||
<div class="machine-summary">
|
|
||||||
<div><strong>Ambiente</strong> ${config.appUrl}</div>
|
|
||||||
<div><strong>API</strong> ${config.apiBaseUrl}</div>
|
|
||||||
<div><strong>Criado em</strong> ${formatDate(config.createdAt)}</div>
|
|
||||||
<div><strong>Última sync</strong> ${formatDate(config.lastSyncedAt ?? null)}</div>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<button id="send-inventory">Enviar inventário agora</button>
|
|
||||||
<button id="open-app-settings" class="secondary">Abrir sistema</button>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<button id="reset-agent-settings" class="secondary">Reprovisionar</button>
|
|
||||||
</div>
|
|
||||||
<div class="machine-summary">
|
|
||||||
<div><strong>Intervalo do heartbeat (segundos)</strong></div>
|
|
||||||
<div style="display:flex; gap:8px; align-items:center;">
|
|
||||||
<input id="hb-interval" type="number" min="60" step="30" value="${String(config.heartbeatIntervalSec ?? 300)}" style="max-width:140px;" />
|
|
||||||
<button id="save-hb-interval">Salvar</button>
|
|
||||||
</div>
|
|
||||||
<p class="text-xs">Mínimo 60s. Salvar reinicia o processo de heartbeat.</p>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
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 `
|
|
||||||
<div class="machine-summary">
|
|
||||||
<div><strong>Hostname:</strong> ${profile.hostname}</div>
|
|
||||||
<div><strong>Sistema:</strong> ${profile.os.name}${profile.os.version ? ` ${profile.os.version}` : ""} (${profile.os.architecture ?? "?"})</div>
|
|
||||||
<div><strong>Endereços MAC:</strong> ${macs}</div>
|
|
||||||
<div><strong>Identificadores:</strong> ${serials}</div>
|
|
||||||
<div><strong>CPU:</strong> ${metrics.cpuPhysicalCores ?? metrics.cpuLogicalCores} núcleos · uso ${formatPercent(metrics.cpuUsagePercent)}</div>
|
|
||||||
<div><strong>Memória:</strong> ${formatBytes(metrics.memoryUsedBytes)} / ${formatBytes(metrics.memoryTotalBytes)} (${formatPercent(metrics.memoryUsedPercent)})</div>
|
|
||||||
<div><strong>Coletado em:</strong> ${lastCollection}</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = `
|
|
||||||
<form id="provision-form">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Código de provisionamento</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input id="provisioningSecret" type="password" name="provisioningSecret" placeholder="Insira o código fornecido" required autocomplete="one-time-code" />
|
|
||||||
<button type="button" id="toggle-secret" class="icon-button" aria-label="Mostrar/ocultar">
|
|
||||||
<svg class="icon-eye" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M12 5C7 5 2.73 8.11 1 12c1.73 3.89 6 7 11 7s9.27-3.11 11-7c-1.73-3.89-6-7-11-7Z" stroke="currentColor" stroke-width="1.8"/>
|
|
||||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span class="optional">Segredo fornecido pelo servidor</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Tenant (opcional)</label>
|
|
||||||
<span class="optional">Use apenas se houver múltiplos ambientes</span>
|
|
||||||
<input type="text" name="tenantId" placeholder="Ex.: tenant-atlas" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Empresa (slug opcional)</label>
|
|
||||||
<span class="optional">Informe para vincular à empresa correta</span>
|
|
||||||
<input type="text" name="companySlug" placeholder="Ex.: empresa-exemplo" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<button type="submit">Registrar máquina</button>
|
|
||||||
<button type="button" class="secondary" id="refresh-profile">Atualizar dados</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
${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
|
|
||||||
? '<svg class="icon-eye-off" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 3l18 18" stroke="currentColor" stroke-width="1.8"/><path d="M10.58 10.58A3 3 0 0012 15a3 3 0 001.42-.38M9.9 4.24A10.94 10.94 0 0112 4c5 0 9.27 3.11 11 7a11.65 11.65 0 01-4.31 5.22M6.35 6.35C4 7.76 2.27 9.7 1 12c1.73 3.89 6 7 11 7 1.53 0 2.99-.29 4.31-.82" stroke="currentColor" stroke-width="1.8"/></svg>'
|
|
||||||
: '<svg class="icon-eye" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 5C7 5 2.73 8.11 1 12c1.73 3.89 6 7 11 7s9.27-3.11 11-7c-1.73-3.89-6-7-11-7Z" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/></svg>'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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<AgentConfig> {
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { useEffect, useMemo, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
import { invoke } from "@tauri-apps/api/core"
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
import { Store } from "@tauri-apps/plugin-store"
|
import { Store } from "@tauri-apps/plugin-store"
|
||||||
import { ExternalLink, Eye, EyeOff } from "lucide-react"
|
import { ExternalLink, Eye, EyeOff } from "lucide-react"
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"
|
||||||
|
import { cn } from "./lib/utils"
|
||||||
|
|
||||||
type MachineOs = {
|
type MachineOs = {
|
||||||
name: string
|
name: string
|
||||||
|
|
@ -117,6 +119,7 @@ function App() {
|
||||||
const [profile, setProfile] = useState<MachineProfile | null>(null)
|
const [profile, setProfile] = useState<MachineProfile | null>(null)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
|
const [status, setStatus] = useState<string | null>(null)
|
||||||
const [showSecret, setShowSecret] = useState(false)
|
const [showSecret, setShowSecret] = useState(false)
|
||||||
|
|
||||||
const [provisioningSecret, setProvisioningSecret] = useState("")
|
const [provisioningSecret, setProvisioningSecret] = useState("")
|
||||||
|
|
@ -138,6 +141,7 @@ function App() {
|
||||||
const p = await invoke<MachineProfile>("collect_machine_profile")
|
const p = await invoke<MachineProfile>("collect_machine_profile")
|
||||||
setProfile(p)
|
setProfile(p)
|
||||||
}
|
}
|
||||||
|
setStatus(t ? "online" : null)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("Falha ao carregar estado do agente.")
|
setError("Falha ao carregar estado do agente.")
|
||||||
}
|
}
|
||||||
|
|
@ -182,6 +186,7 @@ function App() {
|
||||||
await writeConfig(store, cfg)
|
await writeConfig(store, cfg)
|
||||||
setConfig(cfg); setToken(data.machineToken)
|
setConfig(cfg); setToken(data.machineToken)
|
||||||
await invoke("start_machine_agent", { baseUrl: apiBaseUrl, token: data.machineToken, status: "online", intervalSeconds: 300 })
|
await invoke("start_machine_agent", { baseUrl: apiBaseUrl, token: data.machineToken, status: "online", intervalSeconds: 300 })
|
||||||
|
setStatus("online")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : String(err))
|
setError(err instanceof Error ? err.message : String(err))
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -204,15 +209,45 @@ function App() {
|
||||||
async function reprovision() {
|
async function reprovision() {
|
||||||
if (!store) return
|
if (!store) return
|
||||||
await store.delete("token"); await store.delete("config"); await store.save()
|
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<MachineProfile>("collect_machine_profile")
|
const p = await invoke<MachineProfile>("collect_machine_profile")
|
||||||
setProfile(p)
|
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 (
|
return (
|
||||||
<div className="min-h-screen grid place-items-center p-6">
|
<div className="min-h-screen grid place-items-center p-6">
|
||||||
<div className="w-full max-w-[560px] rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
|
<div className="w-full max-w-[720px] rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
<h1 className="text-xl font-semibold">Sistema de Chamados — Agente Desktop</h1>
|
<h1 className="text-xl font-semibold">Sistema de Chamados — Agente Desktop</h1>
|
||||||
|
<StatusBadge status={status} />
|
||||||
|
</div>
|
||||||
{error ? <p className="mt-3 rounded-md bg-rose-50 p-2 text-sm text-rose-700">{error}</p> : null}
|
{error ? <p className="mt-3 rounded-md bg-rose-50 p-2 text-sm text-rose-700">{error}</p> : null}
|
||||||
{!token ? (
|
{!token ? (
|
||||||
<div className="mt-4 space-y-3">
|
<div className="mt-4 space-y-3">
|
||||||
|
|
@ -267,14 +302,66 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-4 space-y-3">
|
<div className="mt-4">
|
||||||
<p className="text-sm text-slate-700">Máquina provisionada.</p>
|
<Tabs defaultValue="resumo" className="w-full">
|
||||||
<div className="grid gap-2">
|
<TabsList className="h-10">
|
||||||
<button onClick={openSystem} className="inline-flex items-center gap-2 rounded-lg border border-black bg-black px-3 py-2 text-sm font-semibold text-white hover:bg-black/90">
|
<TabsTrigger value="resumo" className="rounded-lg px-3">Resumo</TabsTrigger>
|
||||||
|
<TabsTrigger value="inventario" className="rounded-lg px-3">Inventário</TabsTrigger>
|
||||||
|
<TabsTrigger value="diagnostico" className="rounded-lg px-3">Diagnóstico</TabsTrigger>
|
||||||
|
<TabsTrigger value="config" className="rounded-lg px-3">Configurações</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="resumo" className="mt-4">
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="text-xs text-slate-500">CPU</div>
|
||||||
|
<div className="text-sm font-semibold text-slate-900">{profile ? pct(profile.metrics.cpuUsagePercent) : "—"}</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="text-xs text-slate-500">Memória</div>
|
||||||
|
<div className="text-sm font-semibold text-slate-900">{profile ? `${bytes(profile.metrics.memoryUsedBytes)} / ${bytes(profile.metrics.memoryTotalBytes)}` : "—"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex gap-2">
|
||||||
|
<button onClick={openSystem} className="btn btn-primary inline-flex items-center gap-2">
|
||||||
<ExternalLink className="size-4"/> Abrir sistema
|
<ExternalLink className="size-4"/> Abrir sistema
|
||||||
</button>
|
</button>
|
||||||
<button onClick={reprovision} className="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-semibold text-slate-800 hover:bg-slate-50">Reprovisionar</button>
|
<button onClick={reprovision} className="btn btn-outline">Reprovisionar</button>
|
||||||
</div>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="inventario" className="mt-4 space-y-3">
|
||||||
|
<p className="text-sm text-slate-600">Inventário básico coletado localmente. Envie para sincronizar com o servidor.</p>
|
||||||
|
<div className="grid gap-2 sm:grid-cols-2">
|
||||||
|
<div className="card">
|
||||||
|
<div className="text-xs text-slate-500">Hostname</div>
|
||||||
|
<div className="text-sm font-semibold text-slate-900">{profile?.hostname ?? "—"}</div>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<div className="text-xs text-slate-500">Sistema</div>
|
||||||
|
<div className="text-sm font-semibold text-slate-900">{profile?.os?.name ?? "—"} {profile?.os?.version ?? ""}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button disabled={busy} onClick={sendInventoryNow} className={cn("btn btn-primary", busy && "opacity-60")}>Enviar inventário agora</button>
|
||||||
|
<button onClick={openSystem} className="btn btn-outline">Ver no sistema</button>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="diagnostico" className="mt-4 space-y-2">
|
||||||
|
<div className="card">
|
||||||
|
<p className="text-sm text-slate-700">Token armazenado: <span className="font-mono break-all text-xs">{token?.slice(0, 6)}…{token?.slice(-6)}</span></p>
|
||||||
|
<p className="text-sm text-slate-700">Base URL: <span className="font-mono text-xs">{apiBaseUrl}</span></p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="config" className="mt-4 space-y-3">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<label className="label">E-mail do colaborador (opcional)</label>
|
||||||
|
<input className="input" placeholder="colaborador@empresa.com" value={collabEmail} onChange={(e)=>setCollabEmail(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<label className="label">Nome do colaborador (opcional)</label>
|
||||||
|
<input className="input" placeholder="Nome completo" value={collabName} onChange={(e)=>setCollabName(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -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 (
|
||||||
|
<span className="inline-flex h-8 items-center gap-4 rounded-full border border-slate-200 px-3 text-sm font-semibold">
|
||||||
|
<span className="relative inline-flex items-center">
|
||||||
|
<span className={cn("size-2 rounded-full", dot)} />
|
||||||
|
{isOnline ? <span className={cn("absolute left-1/2 top-1/2 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full animate-ping [animation-duration:2s]", ring)} /> : null}
|
||||||
|
</span>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const root = document.getElementById("root") || (() => { const el = document.createElement("div"); el.id = "root"; document.body.appendChild(el); return el })()
|
const root = document.getElementById("root") || (() => { const el = document.createElement("div"); el.id = "root"; document.body.appendChild(el); return el })()
|
||||||
createRoot(root).render(<App />)
|
createRoot(root).render(<App />)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
|
|
@ -17,7 +17,9 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"types": ["vite/client"]
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
356
pnpm-lock.yaml
generated
356
pnpm-lock.yaml
generated
|
|
@ -216,6 +216,9 @@ importers:
|
||||||
|
|
||||||
apps/desktop:
|
apps/desktop:
|
||||||
dependencies:
|
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':
|
'@tauri-apps/api':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.8.0
|
version: 2.8.0
|
||||||
|
|
@ -225,10 +228,22 @@ importers:
|
||||||
'@tauri-apps/plugin-store':
|
'@tauri-apps/plugin-store':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.4.0
|
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:
|
devDependencies:
|
||||||
'@tauri-apps/cli':
|
'@tauri-apps/cli':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.8.4
|
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:
|
typescript:
|
||||||
specifier: ~5.6.2
|
specifier: ~5.6.2
|
||||||
version: 5.6.3
|
version: 5.6.3
|
||||||
|
|
@ -242,10 +257,93 @@ packages:
|
||||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
engines: {node: '>=10'}
|
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':
|
'@babel/runtime@7.28.4':
|
||||||
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
|
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
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':
|
'@better-auth/core@1.3.26':
|
||||||
resolution: {integrity: sha512-S5ooXaOcn9eLV3/JayfbMsAB5PkfoTRaRrtpb5djwvI/UAJOgLyjqhd+rObsBycovQ/nPQvMKjzyM/G1oBKngA==}
|
resolution: {integrity: sha512-S5ooXaOcn9eLV3/JayfbMsAB5PkfoTRaRrtpb5djwvI/UAJOgLyjqhd+rObsBycovQ/nPQvMKjzyM/G1oBKngA==}
|
||||||
|
|
||||||
|
|
@ -1443,6 +1541,9 @@ packages:
|
||||||
'@remirror/core-constants@3.0.0':
|
'@remirror/core-constants@3.0.0':
|
||||||
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
|
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':
|
'@rollup/rollup-android-arm-eabi@4.52.4':
|
||||||
resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==}
|
resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
|
|
@ -1919,6 +2020,18 @@ packages:
|
||||||
'@tybys/wasm-util@0.10.1':
|
'@tybys/wasm-util@0.10.1':
|
||||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
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':
|
'@types/d3-array@3.2.2':
|
||||||
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
|
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
|
||||||
|
|
||||||
|
|
@ -2157,6 +2270,12 @@ packages:
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
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':
|
'@vitest/expect@2.1.9':
|
||||||
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
|
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
|
||||||
|
|
||||||
|
|
@ -2286,6 +2405,10 @@ packages:
|
||||||
base64-js@1.5.1:
|
base64-js@1.5.1:
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
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:
|
better-auth@1.3.26:
|
||||||
resolution: {integrity: sha512-umaOGmv29yF4sD6o2zlW6B0Oayko5yD/A8mKJOFDDEIoVP/pR7nJ/2KsqKy+xvBpnDsKdrZseqA8fmczPviUHw==}
|
resolution: {integrity: sha512-umaOGmv29yF4sD6o2zlW6B0Oayko5yD/A8mKJOFDDEIoVP/pR7nJ/2KsqKy+xvBpnDsKdrZseqA8fmczPviUHw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -2331,6 +2454,11 @@ packages:
|
||||||
brotli@1.3.3:
|
brotli@1.3.3:
|
||||||
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
|
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:
|
buffer@6.0.3:
|
||||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||||
|
|
||||||
|
|
@ -2419,6 +2547,9 @@ packages:
|
||||||
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
|
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
|
||||||
engines: {node: ^14.18.0 || >=16.10.0}
|
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:
|
convex@1.27.3:
|
||||||
resolution: {integrity: sha512-Ebr9lPgXkL7JO5IFr3bG+gYvHskyJjc96Fx0BBNkJUDXrR/bd9/uI4q8QszbglK75XfDu068vR0d/HK2T7tB9Q==}
|
resolution: {integrity: sha512-Ebr9lPgXkL7JO5IFr3bG+gYvHskyJjc96Fx0BBNkJUDXrR/bd9/uI4q8QszbglK75XfDu068vR0d/HK2T7tB9Q==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=7.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=7.0.0'}
|
||||||
|
|
@ -2600,6 +2731,9 @@ packages:
|
||||||
effect@3.16.12:
|
effect@3.16.12:
|
||||||
resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==}
|
resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==}
|
||||||
|
|
||||||
|
electron-to-chromium@1.5.234:
|
||||||
|
resolution: {integrity: sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==}
|
||||||
|
|
||||||
emoji-regex@9.2.2:
|
emoji-regex@9.2.2:
|
||||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||||
|
|
||||||
|
|
@ -2660,6 +2794,10 @@ packages:
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
escalade@3.2.0:
|
||||||
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
escape-string-regexp@4.0.0:
|
escape-string-regexp@4.0.0:
|
||||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -2878,6 +3016,10 @@ packages:
|
||||||
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
|
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
|
||||||
engines: {node: '>= 0.4'}
|
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:
|
get-intrinsic@1.3.0:
|
||||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -3123,6 +3265,11 @@ packages:
|
||||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jsesc@3.1.0:
|
||||||
|
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
json-buffer@3.0.1:
|
json-buffer@3.0.1:
|
||||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||||
|
|
||||||
|
|
@ -3136,6 +3283,11 @@ packages:
|
||||||
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
json5@2.2.3:
|
||||||
|
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
jsx-ast-utils@3.3.5:
|
jsx-ast-utils@3.3.5:
|
||||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
|
|
@ -3252,6 +3404,9 @@ packages:
|
||||||
loupe@3.2.1:
|
loupe@3.2.1:
|
||||||
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
||||||
|
|
||||||
|
lru-cache@5.1.1:
|
||||||
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
lucide-react@0.544.0:
|
lucide-react@0.544.0:
|
||||||
resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==}
|
resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -3350,6 +3505,9 @@ packages:
|
||||||
node-fetch-native@1.6.7:
|
node-fetch-native@1.6.7:
|
||||||
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
|
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
|
||||||
|
|
||||||
|
node-releases@2.0.23:
|
||||||
|
resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
|
||||||
|
|
||||||
nypm@0.6.2:
|
nypm@0.6.2:
|
||||||
resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==}
|
resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==}
|
||||||
engines: {node: ^14.16.0 || >=16.10.0}
|
engines: {node: ^14.16.0 || >=16.10.0}
|
||||||
|
|
@ -3602,6 +3760,10 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.0.0
|
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:
|
react-remove-scroll-bar@2.3.8:
|
||||||
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
|
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -4012,6 +4174,12 @@ packages:
|
||||||
unrs-resolver@1.11.1:
|
unrs-resolver@1.11.1:
|
||||||
resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==}
|
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:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
|
@ -4183,6 +4351,9 @@ packages:
|
||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
yallist@3.1.1:
|
||||||
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
yallist@5.0.0:
|
yallist@5.0.0:
|
||||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -4216,8 +4387,120 @@ snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@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/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':
|
'@better-auth/core@1.3.26':
|
||||||
dependencies:
|
dependencies:
|
||||||
better-call: 1.0.19
|
better-call: 1.0.19
|
||||||
|
|
@ -5283,6 +5566,8 @@ snapshots:
|
||||||
|
|
||||||
'@remirror/core-constants@3.0.0': {}
|
'@remirror/core-constants@3.0.0': {}
|
||||||
|
|
||||||
|
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.52.4':
|
'@rollup/rollup-android-arm-eabi@4.52.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -5704,6 +5989,27 @@ snapshots:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
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-array@3.2.2': {}
|
||||||
|
|
||||||
'@types/d3-color@3.1.3': {}
|
'@types/d3-color@3.1.3': {}
|
||||||
|
|
@ -5939,6 +6245,18 @@ snapshots:
|
||||||
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
||||||
optional: true
|
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':
|
'@vitest/expect@2.1.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 2.1.9
|
'@vitest/spy': 2.1.9
|
||||||
|
|
@ -6099,6 +6417,8 @@ snapshots:
|
||||||
|
|
||||||
base64-js@1.5.1: {}
|
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):
|
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:
|
dependencies:
|
||||||
'@better-auth/core': 1.3.26
|
'@better-auth/core': 1.3.26
|
||||||
|
|
@ -6144,6 +6464,14 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
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:
|
buffer@6.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
|
|
@ -6234,6 +6562,8 @@ snapshots:
|
||||||
|
|
||||||
consola@3.4.2: {}
|
consola@3.4.2: {}
|
||||||
|
|
||||||
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
convex@1.27.3(react@19.2.0):
|
convex@1.27.3(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.4
|
esbuild: 0.25.4
|
||||||
|
|
@ -6394,6 +6724,8 @@ snapshots:
|
||||||
'@standard-schema/spec': 1.0.0
|
'@standard-schema/spec': 1.0.0
|
||||||
fast-check: 3.23.2
|
fast-check: 3.23.2
|
||||||
|
|
||||||
|
electron-to-chromium@1.5.234: {}
|
||||||
|
|
||||||
emoji-regex@9.2.2: {}
|
emoji-regex@9.2.2: {}
|
||||||
|
|
||||||
empathic@2.0.0: {}
|
empathic@2.0.0: {}
|
||||||
|
|
@ -6562,6 +6894,8 @@ snapshots:
|
||||||
'@esbuild/win32-ia32': 0.25.4
|
'@esbuild/win32-ia32': 0.25.4
|
||||||
'@esbuild/win32-x64': 0.25.4
|
'@esbuild/win32-x64': 0.25.4
|
||||||
|
|
||||||
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
escape-string-regexp@4.0.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):
|
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: {}
|
generator-function@2.0.1: {}
|
||||||
|
|
||||||
|
gensync@1.0.0-beta.2: {}
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
get-intrinsic@1.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
|
@ -7119,6 +7455,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
argparse: 2.0.1
|
argparse: 2.0.1
|
||||||
|
|
||||||
|
jsesc@3.1.0: {}
|
||||||
|
|
||||||
json-buffer@3.0.1: {}
|
json-buffer@3.0.1: {}
|
||||||
|
|
||||||
json-schema-traverse@0.4.1: {}
|
json-schema-traverse@0.4.1: {}
|
||||||
|
|
@ -7129,6 +7467,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
|
|
||||||
|
json5@2.2.3: {}
|
||||||
|
|
||||||
jsx-ast-utils@3.3.5:
|
jsx-ast-utils@3.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
|
|
@ -7225,6 +7565,10 @@ snapshots:
|
||||||
|
|
||||||
loupe@3.2.1: {}
|
loupe@3.2.1: {}
|
||||||
|
|
||||||
|
lru-cache@5.1.1:
|
||||||
|
dependencies:
|
||||||
|
yallist: 3.1.1
|
||||||
|
|
||||||
lucide-react@0.544.0(react@19.2.0):
|
lucide-react@0.544.0(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
|
|
@ -7311,6 +7655,8 @@ snapshots:
|
||||||
|
|
||||||
node-fetch-native@1.6.7: {}
|
node-fetch-native@1.6.7: {}
|
||||||
|
|
||||||
|
node-releases@2.0.23: {}
|
||||||
|
|
||||||
nypm@0.6.2:
|
nypm@0.6.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
citty: 0.1.6
|
citty: 0.1.6
|
||||||
|
|
@ -7605,6 +7951,8 @@ snapshots:
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
scheduler: 0.25.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):
|
react-remove-scroll-bar@2.3.8(@types/react@19.2.0)(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
|
|
@ -8124,6 +8472,12 @@ snapshots:
|
||||||
'@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
|
'@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
|
||||||
'@unrs/resolver-binding-win32-x64-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:
|
uri-js@4.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
@ -8304,6 +8658,8 @@ snapshots:
|
||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
yallist@5.0.0: {}
|
yallist@5.0.0: {}
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,22 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { use, useMemo } from "react"
|
import { use } from "react"
|
||||||
import { useQuery } from "convex/react"
|
|
||||||
import type { Id } from "@/convex/_generated/dataModel"
|
|
||||||
import { AppShell } from "@/components/app-shell"
|
import { AppShell } from "@/components/app-shell"
|
||||||
import { SiteHeader } from "@/components/site-header"
|
import { SiteHeader } from "@/components/site-header"
|
||||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||||
import { AdminMachineDetailsClient } from "@/components/admin/machines/admin-machine-details.client"
|
import { AdminMachineDetailsClient } from "@/components/admin/machines/admin-machine-details.client"
|
||||||
import { api } from "@/convex/_generated/api"
|
import { MachineBreadcrumbs } from "@/components/admin/machines/machine-breadcrumbs.client"
|
||||||
import { useAuth } from "@/lib/auth-client"
|
|
||||||
|
|
||||||
export const runtime = "nodejs"
|
export const runtime = "nodejs"
|
||||||
export const dynamic = "force-dynamic"
|
export const dynamic = "force-dynamic"
|
||||||
|
|
||||||
export default function AdminMachineDetailsPage({ params }: { params: Promise<{ id: string }> }) {
|
export default function AdminMachineDetailsPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = use(params)
|
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 (
|
return (
|
||||||
<AppShell header={<SiteHeader title="Detalhe da máquina" lead="Inventário e métricas da máquina selecionada." />}>
|
<AppShell header={<SiteHeader title="Detalhe da máquina" lead="Inventário e métricas da máquina selecionada." />}>
|
||||||
<div className="mx-auto w-full max-w-6xl px-4 pb-12 lg:px-6">
|
<div className="mx-auto w-full max-w-6xl px-4 pb-12 lg:px-6">
|
||||||
<nav className="mb-4 text-sm text-neutral-600">
|
<MachineBreadcrumbs tenantId={DEFAULT_TENANT_ID} machineId={id} />
|
||||||
<ol className="flex items-center gap-2">
|
|
||||||
<li>
|
|
||||||
<Link href="/admin/machines" className="underline-offset-4 hover:underline">Máquinas</Link>
|
|
||||||
</li>
|
|
||||||
<li className="text-neutral-400">/</li>
|
|
||||||
<li className="text-neutral-800">{hostname}</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
<AdminMachineDetailsClient tenantId={DEFAULT_TENANT_ID} machineId={id} />
|
<AdminMachineDetailsClient tenantId={DEFAULT_TENANT_ID} machineId={id} />
|
||||||
</div>
|
</div>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { useQuery } from "convex/react"
|
||||||
import { format, formatDistanceToNowStrict } from "date-fns"
|
import { format, formatDistanceToNowStrict } from "date-fns"
|
||||||
import { ptBR } from "date-fns/locale"
|
import { ptBR } from "date-fns/locale"
|
||||||
import { toast } from "sonner"
|
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 { api } from "@/convex/_generated/api"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
|
@ -42,6 +42,10 @@ type MachineSoftware = {
|
||||||
source?: string
|
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 = {
|
type LinuxExtended = {
|
||||||
lsblk?: unknown
|
lsblk?: unknown
|
||||||
lspci?: string
|
lspci?: string
|
||||||
|
|
@ -71,6 +75,16 @@ type WindowsExtended = {
|
||||||
}>
|
}>
|
||||||
videoControllers?: Array<{ Name?: string; AdapterRAM?: number; DriverVersion?: string; PNPDeviceID?: string }>
|
videoControllers?: Array<{ Name?: string; AdapterRAM?: number; DriverVersion?: string; PNPDeviceID?: string }>
|
||||||
disks?: Array<{ Model?: string; SerialNumber?: string; Size?: number; InterfaceType?: string; MediaType?: 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 = {
|
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()
|
const name = (osName ?? "").toLowerCase()
|
||||||
if (name.includes("windows")) return "🪟"
|
if (name.includes("mac") || name.includes("darwin") || name.includes("macos")) return <Apple className="size-4 text-black" />
|
||||||
if (name.includes("mac") || name.includes("darwin") || name.includes("macos")) return ""
|
if (name.includes("linux")) return <Terminal className="size-4 text-black" />
|
||||||
if (name.includes("linux")) return "🐧"
|
// fallback para Windows/outros como monitor genérico
|
||||||
return "🖥️"
|
return <Monitor className="size-4 text-black" />
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AdminMachinesOverview({ tenantId }: { tenantId: string }) {
|
export function AdminMachinesOverview({ tenantId }: { tenantId: string }) {
|
||||||
|
|
@ -375,11 +389,11 @@ function MachineStatusBadge({ status }: { status?: string | null }) {
|
||||||
|
|
||||||
const isOnline = s === "online"
|
const isOnline = s === "online"
|
||||||
return (
|
return (
|
||||||
<Badge className={cn("inline-flex h-9 items-center gap-3 rounded-full border border-slate-200 px-3 text-sm font-semibold", className)}>
|
<Badge className={cn("inline-flex h-9 items-center gap-5 rounded-full border border-slate-200 px-3 text-sm font-semibold", className)}>
|
||||||
<span className="relative inline-flex items-center">
|
<span className="relative inline-flex items-center">
|
||||||
<span className={cn("size-2 rounded-full", colorClass)} />
|
<span className={cn("size-2 rounded-full", colorClass)} />
|
||||||
{isOnline ? (
|
{isOnline ? (
|
||||||
<span className={cn("absolute left-1/2 top-1/2 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full animate-ping", ringClass)} />
|
<span className={cn("absolute left-1/2 top-1/2 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full animate-ping [animation-duration:2s]", ringClass)} />
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
{label}
|
{label}
|
||||||
|
|
@ -407,6 +421,11 @@ type MachineDetailsProps = {
|
||||||
|
|
||||||
export function MachineDetails({ machine }: MachineDetailsProps) {
|
export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
const { convexUserId } = useAuth()
|
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 metadata = machine?.inventory ?? null
|
||||||
const metrics = machine?.metrics ?? null
|
const metrics = machine?.metrics ?? null
|
||||||
const hardware = metadata?.hardware ?? 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<string, unknown>
|
||||||
|
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 [renaming, setRenaming] = useState(false)
|
||||||
const [newName, setNewName] = useState<string>(machine?.hostname ?? "")
|
const [newName, setNewName] = useState<string>(machine?.hostname ?? "")
|
||||||
|
|
||||||
|
|
@ -489,48 +524,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
return jsonText.replace(new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"), (m) => `__HIGHLIGHT__${m}__END__`)
|
return jsonText.replace(new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"), (m) => `__HIGHLIGHT__${m}__END__`)
|
||||||
}, [jsonText, dialogQuery])
|
}, [jsonText, dialogQuery])
|
||||||
|
|
||||||
const exportInventoryJson = () => {
|
// removed copy/export inventory JSON buttons as requested
|
||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border-slate-200">
|
<Card className="border-slate-200">
|
||||||
|
|
@ -567,12 +561,32 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
{/* ping integrado na badge de status */}
|
{/* ping integrado na badge de status */}
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||||
<span className="mr-2">{osIcon(machine.osName)}</span>
|
<span className="mr-2 inline-flex items-center"><OsIcon osName={machine.osName} /></span>
|
||||||
{machine.osName ?? "SO desconhecido"} {machine.osVersion ?? ""}
|
{machine.osName ?? "SO desconhecido"} {machine.osVersion ?? ""}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||||
{machine.architecture?.toUpperCase() ?? "Arquitetura indefinida"}
|
{machine.architecture?.toUpperCase() ?? "Arquitetura indefinida"}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{windowsExt?.osInfo ? (
|
||||||
|
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||||
|
Build: {String((windowsExt.osInfo as any)?.CurrentBuildNumber ?? (windowsExt.osInfo as any)?.CurrentBuild ?? "—")}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
{windowsExt?.osInfo ? (
|
||||||
|
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||||
|
Ativado: {((windowsExt.osInfo as any)?.IsActivated === true) ? "Sim" : "Não"}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
{companyName ? (
|
||||||
|
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||||
|
Empresa: {companyName}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
{collaborator?.email ? (
|
||||||
|
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||||
|
Colaborador: {collaborator?.name ? `${collaborator.name} · ` : ""}{collaborator.email}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{machine.authEmail ? (
|
{machine.authEmail ? (
|
||||||
|
|
@ -581,11 +595,10 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
Copiar e-mail
|
Copiar e-mail
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
<Button size="sm" onClick={exportInventoryJson} className="gap-2 border border-black bg-black text-white hover:bg-black/90">
|
|
||||||
Exportar inventário (JSON)
|
|
||||||
</Button>
|
|
||||||
{machine.registeredBy ? (
|
{machine.registeredBy ? (
|
||||||
<Badge variant="outline">Registrada via {machine.registeredBy}</Badge>
|
<Badge variant="outline" className="h-7 border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700">
|
||||||
|
Registrada via {machine.registeredBy}
|
||||||
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -1141,8 +1154,6 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2 pt-2">
|
<div className="flex flex-wrap gap-2 pt-2">
|
||||||
<Button size="sm" variant="outline" onClick={copyInventoryJson}>Copiar JSON</Button>
|
|
||||||
<Button size="sm" onClick={exportInventoryJson}>Exportar JSON</Button>
|
|
||||||
{Array.isArray(software) && software.length > 0 ? (
|
{Array.isArray(software) && software.length > 0 ? (
|
||||||
<Button size="sm" variant="outline" onClick={() => exportCsv(software, "softwares.csv")}>Softwares CSV</Button>
|
<Button size="sm" variant="outline" onClick={() => exportCsv(software, "softwares.csv")}>Softwares CSV</Button>
|
||||||
) : null}
|
) : 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 (value === null || value === undefined) return null
|
||||||
if (typeof value === "string" && (value.trim() === "" || value === "undefined" || value === "null")) {
|
if (typeof value === "string" && (value.trim() === "" || value === "undefined" || value === "null")) {
|
||||||
return null
|
return null
|
||||||
|
|
@ -1337,7 +1348,7 @@ function DetailLine({ label, value }: { label: string; value?: string | number |
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
<span className="text-right font-medium text-foreground">{value}</span>
|
<span className={cn("text-right font-medium text-foreground", classNameValue)}>{value}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
src/components/admin/machines/machine-breadcrumbs.client.tsx
Normal file
30
src/components/admin/machines/machine-breadcrumbs.client.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<nav className="mb-4 text-sm text-neutral-600">
|
||||||
|
<ol className="flex items-center gap-2">
|
||||||
|
<li>
|
||||||
|
<Link href="/admin/machines" className="underline-offset-4 hover:underline">Máquinas</Link>
|
||||||
|
</li>
|
||||||
|
<li className="text-neutral-400">/</li>
|
||||||
|
<li className="text-neutral-800">{hostname}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue