Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bd18ab543 | |||
| 4d8bb246f1 | |||
| 07da498d18 |
+1
-1
@@ -35,7 +35,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
|
||||
app = FastAPI(
|
||||
title="Homelable API",
|
||||
version="1.8.1",
|
||||
version="1.8.2",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
@@ -57,8 +58,12 @@ async def check_node(check_method: str, target: str | None, ip: str | None) -> d
|
||||
|
||||
|
||||
async def _ping(host: str) -> bool:
|
||||
if sys.platform == "win32":
|
||||
args = ["ping", "-n", "1", "-w", "1000", host]
|
||||
else:
|
||||
args = ["ping", "-c", "1", "-W", "1", host]
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"ping", "-c", "1", "-W", "1", host,
|
||||
*args,
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.status_checker import _tcp_connect, check_node
|
||||
from app.services.status_checker import _ping, _tcp_connect, check_node
|
||||
|
||||
# --- check_node dispatcher ---
|
||||
|
||||
@@ -149,6 +149,48 @@ async def test_check_node_exception_returns_offline():
|
||||
assert result["response_time_ms"] is None
|
||||
|
||||
|
||||
# --- _ping platform args ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ping_uses_unix_args_on_non_windows():
|
||||
captured = {}
|
||||
|
||||
async def fake_exec(*args, **kwargs):
|
||||
captured["args"] = args
|
||||
proc = MagicMock()
|
||||
proc.returncode = 0
|
||||
proc.wait = AsyncMock()
|
||||
return proc
|
||||
|
||||
with patch("app.services.status_checker.sys.platform", "linux"), \
|
||||
patch("asyncio.create_subprocess_exec", side_effect=fake_exec):
|
||||
await _ping("192.168.1.1")
|
||||
|
||||
assert "-c" in captured["args"]
|
||||
assert "-W" in captured["args"]
|
||||
assert "-n" not in captured["args"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ping_uses_windows_args_on_win32():
|
||||
captured = {}
|
||||
|
||||
async def fake_exec(*args, **kwargs):
|
||||
captured["args"] = args
|
||||
proc = MagicMock()
|
||||
proc.returncode = 0
|
||||
proc.wait = AsyncMock()
|
||||
return proc
|
||||
|
||||
with patch("app.services.status_checker.sys.platform", "win32"), \
|
||||
patch("asyncio.create_subprocess_exec", side_effect=fake_exec):
|
||||
await _ping("192.168.1.1")
|
||||
|
||||
assert "-n" in captured["args"]
|
||||
assert "-w" in captured["args"]
|
||||
assert "-c" not in captured["args"]
|
||||
|
||||
|
||||
# --- _tcp_connect ---
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "1.8.1",
|
||||
"version": "1.8.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function App() {
|
||||
|
||||
const [themeModalOpen, setThemeModalOpen] = useState(false)
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
const [sidebarForceView, setSidebarForceView] = useState<'pending' | undefined>(undefined)
|
||||
const [sidebarForceView, setSidebarForceView] = useState<'pending' | 'history' | undefined>(undefined)
|
||||
const [highlightPendingId, setHighlightPendingId] = useState<string | undefined>(undefined)
|
||||
const [shortcutsOpen, setShortcutsOpen] = useState(false)
|
||||
const [addNodeOpen, setAddNodeOpen] = useState(false)
|
||||
@@ -454,7 +454,11 @@ export default function App() {
|
||||
<ScanConfigModal
|
||||
open={scanConfigOpen}
|
||||
onClose={() => setScanConfigOpen(false)}
|
||||
onScanNow={() => toast.success('Scan triggered')}
|
||||
onScanNow={() => {
|
||||
toast.success('Network scan started — check Scan History for results')
|
||||
setSidebarForceView(undefined)
|
||||
setTimeout(() => setSidebarForceView('history'), 0)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -24,29 +24,22 @@ export function ScanConfigModal({ open, onClose, onScanNow }: ScanConfigModalPro
|
||||
.catch(() => {/* use defaults */})
|
||||
}, [open])
|
||||
|
||||
const handleSave = async () => {
|
||||
const handleScanNow = async () => {
|
||||
const cleaned = ranges.map((r) => r.trim()).filter(Boolean)
|
||||
if (cleaned.length === 0) { toast.error('Add at least one IP range'); return }
|
||||
setSaving(true)
|
||||
try {
|
||||
await scanApi.saveConfig({ ranges: cleaned })
|
||||
toast.success('Scan config saved')
|
||||
await scanApi.trigger()
|
||||
onScanNow()
|
||||
onClose()
|
||||
} catch {
|
||||
toast.error('Failed to save config')
|
||||
toast.error('Failed to start scan')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleScanNow = async () => {
|
||||
const cleaned = ranges.map((r) => r.trim()).filter(Boolean)
|
||||
if (cleaned.length === 0) { toast.error('Add at least one IP range'); return }
|
||||
await handleSave()
|
||||
onScanNow()
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="bg-[#161b22] border-border max-w-md">
|
||||
|
||||
@@ -6,6 +6,7 @@ vi.mock('@/api/client', () => ({
|
||||
scanApi: {
|
||||
getConfig: vi.fn(),
|
||||
saveConfig: vi.fn(),
|
||||
trigger: vi.fn(),
|
||||
},
|
||||
}))
|
||||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn(), info: vi.fn() } }))
|
||||
@@ -20,6 +21,8 @@ describe('ScanConfigModal', () => {
|
||||
vi.mocked(scanApi.getConfig).mockResolvedValue(defaultConfig as never)
|
||||
vi.mocked(scanApi.saveConfig).mockReset()
|
||||
vi.mocked(scanApi.saveConfig).mockResolvedValue({} as never)
|
||||
vi.mocked(scanApi.trigger).mockReset()
|
||||
vi.mocked(scanApi.trigger).mockResolvedValue({} as never)
|
||||
vi.mocked(toast.success).mockReset()
|
||||
vi.mocked(toast.error).mockReset()
|
||||
})
|
||||
@@ -72,7 +75,7 @@ describe('ScanConfigModal', () => {
|
||||
expect(scanApi.saveConfig).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('calls onScanNow after saving on "Scan Now" click', async () => {
|
||||
it('saves config, triggers scan, calls onScanNow and closes on "Scan Now" click', async () => {
|
||||
const onScanNow = vi.fn()
|
||||
const onClose = vi.fn()
|
||||
render(<ScanConfigModal open onClose={onClose} onScanNow={onScanNow} />)
|
||||
@@ -80,7 +83,9 @@ describe('ScanConfigModal', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Scan Now' }))
|
||||
await waitFor(() => {
|
||||
expect(scanApi.saveConfig).toHaveBeenCalledWith({ ranges: ['192.168.1.0/24'] })
|
||||
expect(scanApi.trigger).toHaveBeenCalledOnce()
|
||||
expect(onScanNow).toHaveBeenCalledOnce()
|
||||
expect(onClose).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -53,15 +53,8 @@ export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeAppro
|
||||
const onlineCount = networkNodes.filter((n) => n.data.status === 'online').length
|
||||
const offlineCount = networkNodes.filter((n) => n.data.status === 'offline').length
|
||||
|
||||
const handleScan = useCallback(async () => {
|
||||
try {
|
||||
await scanApi.trigger()
|
||||
toast.success('Network scan started — check Scan History for results')
|
||||
setActiveView('history')
|
||||
onScan()
|
||||
} catch {
|
||||
toast.error('Failed to trigger scan')
|
||||
}
|
||||
const handleScan = useCallback(() => {
|
||||
onScan()
|
||||
}, [onScan])
|
||||
|
||||
return (
|
||||
|
||||
@@ -222,20 +222,12 @@ describe('Sidebar', () => {
|
||||
|
||||
// ── Scan action ────────────────────────────────────────────────────────────
|
||||
|
||||
it('calls scanApi.trigger and onScan prop when Scan Network is clicked', async () => {
|
||||
const { scanApi } = await import('@/api/client')
|
||||
it('calls onScan prop when Scan Network is clicked (scan trigger moved to ScanConfigModal)', () => {
|
||||
render(<Sidebar {...defaultProps} />)
|
||||
fireEvent.click(screen.getByText('Scan Network'))
|
||||
await waitFor(() => expect(scanApi.trigger).toHaveBeenCalledOnce())
|
||||
expect(defaultProps.onScan).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('switches to Scan History view after scan is triggered', async () => {
|
||||
render(<Sidebar {...defaultProps} />)
|
||||
fireEvent.click(screen.getByText('Scan Network'))
|
||||
await waitFor(() => expect(screen.getByText('History')).toBeInTheDocument())
|
||||
})
|
||||
|
||||
// ── Navigation ─────────────────────────────────────────────────────────────
|
||||
|
||||
it('shows Pending panel when Pending Devices nav item is clicked', async () => {
|
||||
|
||||
Reference in New Issue
Block a user