From d7cbb0c45269be5192af6a47a77e59bdeb1e1e8e Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Thu, 13 Aug 2026 14:19:02 +0000 Subject: [PATCH] hvac: add gateway delete (retire the old Modbus gateway config) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There was no way to remove an mhi_gateways row either, alongside the already-fixed device-delete gap — exposed by the same Modbus->MQTT switchover. Adds DELETE /api/mhi-gateways/:id (manage_devices cap) and a 'Delete gateway' button. Devices referencing the gateway are NOT deleted (mhi_gateway_id just goes NULL per the existing ON DELETE SET NULL FK) — the confirm dialog says so and points at the separate device-delete flow for retiring the stale mhi_modbus rows too. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/routes/mhi-gateways.js | 13 +++++++++++++ frontend/src/api.ts | 3 +++ frontend/src/components/GatewaysPanel.tsx | 23 +++++++++++++++++++++-- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/backend/src/routes/mhi-gateways.js b/backend/src/routes/mhi-gateways.js index 90e037f..acaf870 100644 --- a/backend/src/routes/mhi-gateways.js +++ b/backend/src/routes/mhi-gateways.js @@ -54,6 +54,19 @@ export async function mhiGatewayRoutes(app) { return rows[0] }) + // DELETE /api/mhi-gateways/:id — remove a gateway config entirely. Main use: + // retiring a gateway after it's switched to native MQTT (see lib/mqtt.js) and + // its Modbus register-map config is no longer needed. Devices that reference + // this gateway (device_type='mhi_modbus') are NOT deleted — mhi_gateway_id + // just goes NULL (ON DELETE SET NULL) — delete those rows separately via + // DELETE /api/devices/:id if they're being retired too. + app.delete('/api/mhi-gateways/:id', { preHandler: requireCap('manage_devices') }, async (req, reply) => { + const { rows } = await pool.query('SELECT id FROM mhi_gateways WHERE id = $1', [req.params.id]) + if (!rows.length) return reply.status(404).send({ error: 'Gateway not found' }) + await pool.query('DELETE FROM mhi_gateways WHERE id = $1', [req.params.id]) + return { ok: true } + }) + // POST /api/mhi-gateways/:id/test-connection — live Modbus read of the // gateway comm status register (same check as scripts/test-modbus-connection.py) app.post('/api/mhi-gateways/:id/test-connection', { preHandler: requireCap('manage_devices') }, async (req, reply) => { diff --git a/frontend/src/api.ts b/frontend/src/api.ts index c7c6058..40d144c 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -113,6 +113,9 @@ export function updateMhiGateway(id: number, body: Partial<{ name: string; host: export function testMhiGatewayConnection(id: number): Promise<{ ok: boolean; note?: string; error?: string }> { return request(`/mhi-gateways/${id}/test-connection`, { method: 'POST', body: JSON.stringify({}) }) } +export function deleteMhiGateway(id: number): Promise<{ ok: boolean }> { + return request(`/mhi-gateways/${id}`, { method: 'DELETE' }) +} export async function importMhiRegisterMap(id: number, file: File): Promise { const form = new FormData() form.append('file', file) diff --git a/frontend/src/components/GatewaysPanel.tsx b/frontend/src/components/GatewaysPanel.tsx index 56e3b72..0bbe92c 100644 --- a/frontend/src/components/GatewaysPanel.tsx +++ b/frontend/src/components/GatewaysPanel.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react' -import { Plus, Wifi, WifiOff, Upload, Loader2 } from 'lucide-react' +import { Plus, Wifi, WifiOff, Upload, Loader2, Trash2 } from 'lucide-react' import type { Zone, MhiGateway } from '../types' import { - fetchMhiGateways, createMhiGateway, testMhiGatewayConnection, importMhiRegisterMap, assignMhiUnit, + fetchMhiGateways, createMhiGateway, testMhiGatewayConnection, importMhiRegisterMap, assignMhiUnit, deleteMhiGateway, } from '../api' // Gateway configuration + MAPS register-map import for MHI aircon units (manual @@ -65,6 +65,22 @@ export default function GatewaysPanel({ zones, onAssigned }: { zones: Zone[]; on } } + async function handleDelete(gw: MhiGateway) { + if (!confirm( + `Delete gateway "${gw.name}"? This removes its connection config and register-map import.` + + (gw.assigned_count > 0 ? ` ${gw.assigned_count} assigned device(s) will be unlinked from it (not deleted) — ` + + 'if this gateway was switched to native MQTT, remove those Modbus device rows separately in Devices.' : '') + )) return + setError(''); setMsg('') + try { + await deleteMhiGateway(gw.id) + setMsg('Gateway deleted.') + load() + } catch (e) { + setError(e instanceof Error ? e.message : 'Delete failed') + } + } + async function handleImport(id: number, file: File | undefined) { if (!file) return setImportingId(id); setError(''); setMsg('') @@ -170,6 +186,9 @@ export default function GatewaysPanel({ zones, onAssigned }: { zones: Zone[]; on onChange={e => { handleImport(gw.id, e.target.files?.[0]); e.target.value = '' }} /> +