modbus: add explicit cascade delete (device + readings + toggles + HA cleanup)
A disabled device with historical readings still cannot be deleted by default
(409 guard stays, to prevent accidental data loss). A new explicit opt-in lets
the user remove it for real:
- DELETE /api/modbus/devices/{uuid}?cascade=true removes the device's readings
and exposed_entity_toggle rows, then the device, in one transaction (FK order:
readings before device). Best-effort clears the HA discovery configs first
(MQTT not connected -> no-op, never blocks the delete). Default (no cascade)
unchanged. Returns 200 with deletion counts.
- Frontend: a 'Force Delete (all data)' button appears only after the 409, with
a red irreversible warning; cascade is sent only on that explicit action.
- Also corrected a stale comment: FK RESTRICT IS enforced at runtime now.
This commit is contained in:
@@ -288,6 +288,7 @@ describe('EnergyPage — delete device', () => {
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId(`device-delete-${DEVICE.uuid}`)).toBeInTheDocument())
|
||||
fireEvent.click(screen.getByTestId(`device-delete-${DEVICE.uuid}`))
|
||||
// On first open (no 409 yet), the regular confirm button is visible.
|
||||
await waitFor(() => expect(screen.getByTestId('device-delete-confirm')).toBeInTheDocument())
|
||||
|
||||
fireEvent.click(screen.getByTestId('device-delete-confirm'))
|
||||
@@ -295,6 +296,38 @@ describe('EnergyPage — delete device', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('device-delete-409-hint')).toBeInTheDocument()
|
||||
})
|
||||
// After 409, the force-delete button appears instead of the normal confirm.
|
||||
expect(screen.getByTestId('device-delete-force')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('device-delete-confirm')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('force-delete button triggers cascade delete after 409', async () => {
|
||||
const { ApiError } = await import('../api/client')
|
||||
// First call → 409, second call (cascade) → success
|
||||
mockDelete
|
||||
.mockRejectedValueOnce(new ApiError(409, { detail: 'device has readings' }))
|
||||
.mockResolvedValueOnce({ data: { deleted: true, readings_deleted: 3, toggles_deleted: 1 } })
|
||||
|
||||
renderEnergy()
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId(`device-delete-${DEVICE.uuid}`)).toBeInTheDocument())
|
||||
fireEvent.click(screen.getByTestId(`device-delete-${DEVICE.uuid}`))
|
||||
await waitFor(() => expect(screen.getByTestId('device-delete-confirm')).toBeInTheDocument())
|
||||
|
||||
// First attempt → 409
|
||||
fireEvent.click(screen.getByTestId('device-delete-confirm'))
|
||||
|
||||
// Force-delete button appears
|
||||
await waitFor(() => expect(screen.getByTestId('device-delete-force')).toBeInTheDocument())
|
||||
|
||||
// Click force-delete → should call DELETE with cascade=true
|
||||
fireEvent.click(screen.getByTestId('device-delete-force'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDelete).toHaveBeenCalledWith('/api/modbus/devices/{uuid}', {
|
||||
params: { path: { uuid: DEVICE.uuid }, query: { cascade: true } },
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user