feat: configurable bottom handles, scanner rewrite, UI polish
## New features - Configurable bottom connection points per node (1–4 handles) - Fit view on load - LiveView improvements - Node modal: inline Type/Icon picker, default icon in trigger - Remove redundant Save button from ScanConfigModal ## Scanner fixes - Phase 1: replace nmap ARP sweep with concurrent asyncio ping sweep (50 parallel pings, 1s timeout). Zero false positives, works in any Docker network mode. Supplements with /proc/net/arp for ICMP-blocked devices. - Phase 2: explicit -sS (root) / -sT (non-root) scan type; bump host-timeout to 60s; gather(return_exceptions=True) so one failing host doesn't abort the batch - Fix 404 on missing device in hide/ignore - Validate CIDR ranges to prevent nmap injection - Thread-safe cancel set, pre-fetch canvas/hidden IPs (no N+1 queries) - Logging: attach StreamHandler to root logger so app.* logs are visible ## Tests - 21 backend scanner tests (ping sweep, ARP cache, Phase 2 tolerance) - Full NodeModal coverage (53 tests) - LiveView, store, edge label tests
This commit is contained in:
@@ -11,6 +11,7 @@ vi.mock('@xyflow/react', () => ({
|
||||
Controls: () => null,
|
||||
BackgroundVariant: { Dots: 'dots' },
|
||||
ConnectionMode: { Loose: 'loose' },
|
||||
useReactFlow: () => ({ fitView: vi.fn() }),
|
||||
}))
|
||||
vi.mock('@xyflow/react/dist/style.css', () => ({}))
|
||||
|
||||
@@ -50,6 +51,8 @@ describe('LiveView (non-standalone)', () => {
|
||||
useCanvasStore.setState({ nodes: [], edges: [] })
|
||||
})
|
||||
|
||||
afterEach(() => { setSearch('') })
|
||||
|
||||
// ── No key ────────────────────────────────────────────────────────────────
|
||||
|
||||
it('shows no-key error when ?key= is missing', async () => {
|
||||
@@ -133,11 +136,25 @@ describe('LiveView (non-standalone)', () => {
|
||||
|
||||
// ── Standalone mode ────────────────────────────────────────────────────────
|
||||
|
||||
const XYFLOW_MOCK = {
|
||||
ReactFlowProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
ReactFlow: () => <div data-testid="react-flow" />,
|
||||
Background: () => null,
|
||||
Controls: () => null,
|
||||
BackgroundVariant: { Dots: 'dots' },
|
||||
ConnectionMode: { Loose: 'loose' },
|
||||
useReactFlow: () => ({ fitView: vi.fn() }),
|
||||
}
|
||||
|
||||
describe('LiveView (standalone — localStorage)', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
useCanvasStore.setState({ nodes: [], edges: [] })
|
||||
vi.mocked(liveviewApi.load).mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
setSearch('')
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
it('loads canvas from localStorage without calling the API', async () => {
|
||||
@@ -151,25 +168,12 @@ describe('LiveView (standalone — localStorage)', () => {
|
||||
}
|
||||
localStorage.setItem('homelable_canvas', JSON.stringify(stored))
|
||||
|
||||
// Stub VITE_STANDALONE before re-importing
|
||||
vi.stubEnv('VITE_STANDALONE', 'true')
|
||||
vi.resetModules()
|
||||
const { default: LiveViewStandalone } = await import('../LiveView')
|
||||
|
||||
setSearch('') // no key needed in standalone
|
||||
render(<LiveViewStandalone />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('react-flow')).toBeDefined()
|
||||
})
|
||||
expect(liveviewApi.load).not.toHaveBeenCalled()
|
||||
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
it('shows canvas (empty) when localStorage has no saved data', async () => {
|
||||
vi.stubEnv('VITE_STANDALONE', 'true')
|
||||
vi.resetModules()
|
||||
const mockLoad = vi.fn()
|
||||
vi.doMock('@xyflow/react', () => XYFLOW_MOCK)
|
||||
vi.doMock('@xyflow/react/dist/style.css', () => ({}))
|
||||
vi.doMock('@/api/client', () => ({ liveviewApi: { load: mockLoad } }))
|
||||
const { default: LiveViewStandalone } = await import('../LiveView')
|
||||
|
||||
setSearch('')
|
||||
@@ -178,8 +182,24 @@ describe('LiveView (standalone — localStorage)', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('react-flow')).toBeDefined()
|
||||
})
|
||||
expect(liveviewApi.load).not.toHaveBeenCalled()
|
||||
expect(mockLoad).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
vi.unstubAllEnvs()
|
||||
it('shows canvas (empty) when localStorage has no saved data', async () => {
|
||||
vi.stubEnv('VITE_STANDALONE', 'true')
|
||||
vi.resetModules()
|
||||
const mockLoad = vi.fn()
|
||||
vi.doMock('@xyflow/react', () => XYFLOW_MOCK)
|
||||
vi.doMock('@xyflow/react/dist/style.css', () => ({}))
|
||||
vi.doMock('@/api/client', () => ({ liveviewApi: { load: mockLoad } }))
|
||||
const { default: LiveViewStandalone } = await import('../LiveView')
|
||||
|
||||
setSearch('')
|
||||
render(<LiveViewStandalone />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('react-flow')).toBeDefined()
|
||||
})
|
||||
expect(mockLoad).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user