feat: proxmox import diagnostics, node style fix, and cluster edges

Scan History:
- Proxmox is now a first-class scan kind (badge, filter chip, Server icon,
  completion toast) instead of being mislabeled as an IP scan.
- A done run carrying a non-fatal advisory renders amber (info) with a
  warning toast, distinct from red failures.

Import diagnostics:
- test-connection probes /access/permissions and warns when the API token
  has no ACL (VMs/LXC would be invisible) — points at the PVEAuditor grant.
- import surfaces an advisory when hosts import but no guests are visible
  (privilege-separated token whose rights are the intersection with the user),
  rather than a silent "done".

Node style:
- Proxmox container mode is now opt-in (container_mode === true), matching the
  rest of the codebase (App.tsx nesting logic). Imported proxmox nodes leave
  the flag unset and render like a manually-created node instead of an empty
  group container.

Cluster edges:
- Hosts from one import are chained with 'cluster' edges via left/right handles,
  distinct from the vertical host->guest 'virtual' edges. Wired for both the
  direct "Add to Canvas" path and the pending -> approve path (host<->host
  proxmox_cluster links, resolved to cluster edges on approve; cluster hosts
  get left/right handles).

Tests added on both sides.

ha-relevant: maybe
This commit is contained in:
Pouzor
2026-07-06 10:26:52 +02:00
parent abefc42fdf
commit 9670d0a86a
12 changed files with 475 additions and 41 deletions
@@ -3,7 +3,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { ScanHistoryModal } from '../ScanHistoryModal'
import { TooltipProvider } from '@/components/ui/tooltip'
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn(), warning: vi.fn() } }))
vi.mock('@/stores/canvasStore', () => ({
useCanvasStore: { getState: () => ({ notifyScanDeviceFound: vi.fn() }) },
}))
@@ -72,6 +72,17 @@ const ZWAVE_RUN = {
error: null,
}
const PROXMOX_RUN = {
id: 'run-6',
status: 'done',
kind: 'proxmox',
ranges: ['pve:8006'],
devices_found: 9,
started_at: new Date().toISOString(),
finished_at: new Date().toISOString(),
error: null,
}
function renderModal() {
return render(
<TooltipProvider>
@@ -84,6 +95,7 @@ describe('ScanHistoryModal', () => {
beforeEach(() => {
vi.mocked(toast.success).mockReset()
vi.mocked(toast.error).mockReset()
vi.mocked(toast.warning).mockReset()
vi.mocked(scanApi.stop).mockReset()
vi.mocked(scanApi.runs).mockResolvedValue({ data: [] } as never)
})
@@ -176,4 +188,31 @@ describe('ScanHistoryModal', () => {
expect(screen.getByText('5 found')).toBeDefined()
expect(screen.queryByText('3 found')).toBeNull()
})
it('renders a done proxmox run with an advisory as info, not a failure', async () => {
const ADVISORY_RUN = {
...PROXMOX_RUN,
id: 'run-7',
devices_found: 3,
error: 'Imported 3 host(s) but no VMs or LXC were visible to the API token. Grant PVEAuditor…',
}
vi.mocked(scanApi.runs).mockResolvedValue({ data: [ADVISORY_RUN] } as never)
renderModal()
// Status stays "done" (success), yet the advisory text is surfaced.
await waitFor(() => expect(screen.getByText('done')).toBeDefined())
expect(screen.getByText(/no VMs or LXC were visible/)).toBeDefined()
})
it('shows a proxmox run under its own kind, not IP', async () => {
vi.mocked(scanApi.runs).mockResolvedValue({ data: [DONE_RUN, PROXMOX_RUN] } as never)
renderModal()
await waitFor(() => expect(screen.getAllByText('done').length).toBe(2))
// A dedicated Proxmox badge is rendered on the run (would be mislabeled "IP"
// before the fix). Both the filter chip and the run badge carry the label.
expect(screen.getAllByText('Proxmox').length).toBeGreaterThanOrEqual(2)
// Filtering to Proxmox keeps only the proxmox run (9 found), drops the IP run.
fireEvent.click(screen.getByRole('button', { name: 'Proxmox' }))
expect(screen.getByText('9 found')).toBeDefined()
expect(screen.queryByText('3 found')).toBeNull()
})
})