feat: manual Proxmox re-sync button + make connection config env-only
Add a 'Re-sync now' button to the Proxmox auto-sync settings section that triggers an immediate inventory import (POST /proxmox/sync-now) using the server env config — the manual counterpart to scheduled auto-sync. Also fix a dual-source-of-truth bug: Proxmox connection config (host, port, token, verify_tls) is now env-only and never persisted to scan_config.json. Previously save_overrides() dumped host/port/verify alongside scan settings, so saving an unrelated setting wrote an empty proxmox_host that load_overrides then clobbered PROXMOX_HOST with on every boot. Only the auto-sync activation (sync_enabled + sync_interval) stays user-editable and persisted. ha-relevant: no
This commit is contained in:
@@ -243,7 +243,7 @@ describe('api/client', () => {
|
||||
it('proxmoxApi.getConfig/saveConfig hit /proxmox/config', () => {
|
||||
mod.proxmoxApi.getConfig()
|
||||
expect(api.get).toHaveBeenCalledWith('/proxmox/config')
|
||||
const conf = { host: 'pve', port: 8006, verify_tls: true, sync_enabled: false, sync_interval: 3600 }
|
||||
const conf = { sync_enabled: false, sync_interval: 3600 }
|
||||
mod.proxmoxApi.saveConfig(conf)
|
||||
expect(api.post).toHaveBeenCalledWith('/proxmox/config', conf)
|
||||
})
|
||||
|
||||
@@ -161,8 +161,22 @@ export const proxmoxApi = {
|
||||
}>('/proxmox/import-pending', data),
|
||||
|
||||
getConfig: () => api.get<ProxmoxConfigData>('/proxmox/config'),
|
||||
saveConfig: (data: Omit<ProxmoxConfigData, 'token_configured'>) =>
|
||||
// Only the auto-sync activation is persisted. Connection config
|
||||
// (host/port/token/verify_tls) is env-only and never sent.
|
||||
saveConfig: (data: { sync_enabled: boolean; sync_interval: number }) =>
|
||||
api.post<ProxmoxConfigData>('/proxmox/config', data),
|
||||
|
||||
syncNow: () =>
|
||||
api.post<{
|
||||
id: string
|
||||
status: string
|
||||
kind: string
|
||||
ranges: string[]
|
||||
devices_found: number
|
||||
started_at: string
|
||||
finished_at: string | null
|
||||
error: string | null
|
||||
}>('/proxmox/sync-now'),
|
||||
}
|
||||
|
||||
export const designsApi = {
|
||||
|
||||
@@ -26,6 +26,7 @@ export function SettingsModal({ open, onClose }: SettingsModalProps) {
|
||||
const [pmConfig, setPmConfig] = useState<ProxmoxConfigData | null>(null)
|
||||
const [pmSyncEnabled, setPmSyncEnabled] = useState(false)
|
||||
const [pmInterval, setPmInterval] = useState(3600)
|
||||
const [pmSyncing, setPmSyncing] = useState(false)
|
||||
const [alignment, setAlignment] = useState<AlignmentSettings>(readAlignmentSettings)
|
||||
const hideIp = useCanvasStore((s) => s.hideIp)
|
||||
const setHideIp = useCanvasStore((s) => s.setHideIp)
|
||||
@@ -56,6 +57,18 @@ export function SettingsModal({ open, onClose }: SettingsModalProps) {
|
||||
writeAlignmentSettings(next)
|
||||
}
|
||||
|
||||
const handleSyncNow = async () => {
|
||||
setPmSyncing(true)
|
||||
try {
|
||||
await proxmoxApi.syncNow()
|
||||
toast.success('Proxmox sync started')
|
||||
} catch {
|
||||
toast.error('Failed to start Proxmox sync')
|
||||
} finally {
|
||||
setPmSyncing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
// Canvas prefs (alignment, hide-IP) persist on change; only the backend
|
||||
// status-check interval needs an API round-trip.
|
||||
@@ -71,10 +84,9 @@ export function SettingsModal({ open, onClose }: SettingsModalProps) {
|
||||
service_check_interval: serviceInterval,
|
||||
})
|
||||
if (pmConfig) {
|
||||
// Connection config (host/port/token/verify) is env-only; only the
|
||||
// auto-sync activation is persisted.
|
||||
await proxmoxApi.saveConfig({
|
||||
host: pmConfig.host,
|
||||
port: pmConfig.port,
|
||||
verify_tls: pmConfig.verify_tls,
|
||||
sync_enabled: pmSyncEnabled,
|
||||
sync_interval: pmInterval,
|
||||
})
|
||||
@@ -186,6 +198,25 @@ export function SettingsModal({ open, onClose }: SettingsModalProps) {
|
||||
Re-imports hosts/VMs/LXC into the pending inventory. Min 300s (5 min).
|
||||
</p>
|
||||
</div>
|
||||
{pmConfig.host ? (
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleSyncNow}
|
||||
disabled={pmSyncing}
|
||||
className="h-7 text-xs border-[#e57000] text-[#e57000] hover:bg-[#e57000]/10"
|
||||
>
|
||||
{pmSyncing ? 'Syncing…' : 'Re-sync now'}
|
||||
</Button>
|
||||
<span className="text-[10px] text-muted-foreground leading-tight">
|
||||
Runs one import immediately using the server .env config.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[10px] text-[#e3b341] leading-tight pt-1">
|
||||
Set <span className="font-mono">PROXMOX_HOST</span> in the server .env to enable manual re-sync.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ vi.mock('@/api/client', () => ({
|
||||
proxmoxApi: {
|
||||
getConfig: vi.fn(),
|
||||
saveConfig: vi.fn(),
|
||||
syncNow: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -98,6 +99,51 @@ describe('SettingsModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('persists only sync fields (not connection config) on Save', async () => {
|
||||
vi.mocked(proxmoxApi.getConfig).mockResolvedValue({
|
||||
data: { host: 'pve', port: 8006, verify_tls: true, sync_enabled: true, sync_interval: 3600, token_configured: true },
|
||||
} as never)
|
||||
vi.mocked(proxmoxApi.saveConfig).mockResolvedValue({ data: {} } as never)
|
||||
render(<SettingsModal open onClose={vi.fn()} />)
|
||||
await screen.findByDisplayValue('60')
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
|
||||
await waitFor(() => {
|
||||
expect(proxmoxApi.saveConfig).toHaveBeenCalledWith({ sync_enabled: true, sync_interval: 3600 })
|
||||
})
|
||||
})
|
||||
|
||||
it('triggers an immediate Proxmox sync from the Re-sync now button', async () => {
|
||||
vi.mocked(proxmoxApi.getConfig).mockResolvedValue({
|
||||
data: { host: 'pve', port: 8006, verify_tls: true, sync_enabled: false, sync_interval: 3600, token_configured: true },
|
||||
} as never)
|
||||
vi.mocked(proxmoxApi.syncNow).mockResolvedValue({ data: { status: 'running' } } as never)
|
||||
render(<SettingsModal open onClose={vi.fn()} />)
|
||||
const btn = await screen.findByRole('button', { name: 'Re-sync now' })
|
||||
fireEvent.click(btn)
|
||||
await waitFor(() => {
|
||||
expect(proxmoxApi.syncNow).toHaveBeenCalledOnce()
|
||||
expect(toast.success).toHaveBeenCalledWith('Proxmox sync started')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows a PROXMOX_HOST hint instead of the button when host is unset', async () => {
|
||||
vi.mocked(proxmoxApi.getConfig).mockResolvedValue({
|
||||
data: { host: '', port: 8006, verify_tls: true, sync_enabled: false, sync_interval: 3600, token_configured: true },
|
||||
} as never)
|
||||
render(<SettingsModal open onClose={vi.fn()} />)
|
||||
await screen.findByText('PROXMOX_HOST')
|
||||
expect(screen.queryByRole('button', { name: 'Re-sync now' })).toBeNull()
|
||||
})
|
||||
|
||||
it('hides Re-sync now when no Proxmox token is configured', async () => {
|
||||
vi.mocked(proxmoxApi.getConfig).mockResolvedValue({
|
||||
data: { host: 'pve', port: 8006, verify_tls: true, sync_enabled: false, sync_interval: 3600, token_configured: false },
|
||||
} as never)
|
||||
render(<SettingsModal open onClose={vi.fn()} />)
|
||||
await screen.findByDisplayValue('60')
|
||||
expect(screen.queryByRole('button', { name: 'Re-sync now' })).toBeNull()
|
||||
})
|
||||
|
||||
it('calls onClose on Cancel', async () => {
|
||||
const onClose = vi.fn()
|
||||
render(<SettingsModal open onClose={onClose} />)
|
||||
|
||||
Reference in New Issue
Block a user