hvac: add device delete (retire stale mhi_modbus rows after MQTT switch)

There was no way to remove a zone_devices mapping — a gap exposed by the
Modbus->MQTT switchover, which leaves the old mhi_modbus rows stale beside
the freshly auto-registered mhi_mqtt ones. Adds DELETE /api/devices/:id
(manage_devices cap, unlinks photo files then deletes; device_photos
cascade) and a 'Delete device' button in the expanded device row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-13 13:51:41 +00:00
parent 6325532cfe
commit 7ce95a31da
3 changed files with 53 additions and 7 deletions

View file

@ -71,6 +71,9 @@ export function discoverDevices(deviceType: string): Promise<{ ok: boolean; devi
export function updateDevice(id: number, body: { zone_id?: number | null; location?: string }): Promise<Device> {
return request(`/devices/${id}`, { method: 'PATCH', body: JSON.stringify(body) })
}
export function deleteDevice(id: number): Promise<{ ok: boolean }> {
return request(`/devices/${id}`, { method: 'DELETE' })
}
export function createMaintenanceAsset(id: number): Promise<{ ok: boolean; notImplemented?: boolean; error?: string }> {
return request(`/devices/${id}/create-maintenance-asset`, { method: 'POST', body: JSON.stringify({}) })
}

View file

@ -1,7 +1,7 @@
import { Fragment, useEffect, useState } from 'react'
import { RadioTower, ChevronDown, ChevronRight, Wrench } from 'lucide-react'
import { RadioTower, ChevronDown, ChevronRight, Wrench, Trash2 } from 'lucide-react'
import {
fetchDevices, discoverDevices, updateDevice, fetchZones, fetchDevicePhotos, createMaintenanceAsset,
fetchDevices, discoverDevices, updateDevice, deleteDevice, fetchZones, fetchDevicePhotos, createMaintenanceAsset,
} from '../api'
import type { Device, Zone, DevicePhoto, DeviceType } from '../types'
import { DEVICE_TYPE_LABELS, IMPLEMENTED_DEVICE_TYPES } from '../types'
@ -76,6 +76,17 @@ export default function Devices() {
setMsg(res.error || (res.ok ? 'Asset created' : 'Not available yet'))
}
async function remove(id: number, label: string) {
if (!confirm(`Delete device "${label}"? This removes its mapping and any photos, and cannot be undone.`)) return
try {
await deleteDevice(id)
setMsg('Device deleted')
load()
} catch (e) {
setMsg(e instanceof Error ? e.message : 'Delete failed')
}
}
if (loading) return <div className="page"><p className="muted">Loading</p></div>
return (
@ -182,11 +193,18 @@ export default function Devices() {
/>
</div>
</div>
{canManage && d.zone_id && (
<button className="btn btn-sm" onClick={() => createAsset(d.id)}>
<Wrench size={13} strokeWidth={1.75} /> Create asset in Maintenance
</button>
)}
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
{canManage && d.zone_id && (
<button className="btn btn-sm" onClick={() => createAsset(d.id)}>
<Wrench size={13} strokeWidth={1.75} /> Create asset in Maintenance
</button>
)}
{canManage && (
<button className="btn btn-sm btn-danger" onClick={() => remove(d.id, d.discovered_name || d.external_ref)}>
<Trash2 size={13} strokeWidth={1.75} /> Delete device
</button>
)}
</div>
</td>
</tr>
)}