feat: cascade-delete hosts when removing a network

When a VLAN/LAN is deleted, all non-gateway, non-livebox devices
with an interface in that network are deleted automatically.
Gateway and livebox devices are preserved; their interface is
unlinked (vlan_id set to NULL).

The confirmation dialog now shows the exact count of devices
that will be deleted (all three locales: fr/en/es).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 10:48:36 +02:00
parent cf07461436
commit 14de657deb
4 changed files with 30 additions and 3 deletions
+15
View File
@@ -97,6 +97,21 @@ def delete_vlan(vlan_pk: int, db: Session = Depends(get_db)):
db_vlan = db.query(models.Vlan).filter(models.Vlan.id == vlan_pk).first()
if not db_vlan:
raise HTTPException(status_code=404, detail="VLAN introuvable")
# Collect devices with an interface in this VLAN
ifaces = (
db.query(models.DeviceInterface)
.filter(models.DeviceInterface.vlan_id == vlan_pk)
.all()
)
device_ids = {i.device_id for i in ifaces}
for device_id in device_ids:
device = db.query(models.Device).filter(models.Device.id == device_id).first()
if device and not device.is_gateway and not device.is_livebox:
db.delete(device) # cascade deletes all its interfaces
# Gateway/livebox interfaces in this VLAN will be SET NULL by SQLAlchemy
db.delete(db_vlan)
db.commit()
return {"ok": True}
+1 -1
View File
@@ -107,7 +107,7 @@
:devices="devices"
:vlans="vlans"
/>
<VlanManager v-if="view === 'vlans'" :vlans="vlans" @refresh="loadAll" />
<VlanManager v-if="view === 'vlans'" :vlans="vlans" :devices="devices" @refresh="loadAll" />
<DeviceManager v-if="view === 'devices'" :devices="devices" :vlans="vlans" @refresh="loadAll" />
</main>
</div>
+11 -2
View File
@@ -90,7 +90,7 @@ import { ref, reactive } from 'vue'
import { vlansApi } from '../api.js'
import { t, tFmt } from '../i18n.js'
const props = defineProps({ vlans: Array })
const props = defineProps({ vlans: Array, devices: { type: Array, default: () => [] } })
const emit = defineEmits(['refresh'])
const showForm = ref(false)
@@ -145,9 +145,18 @@ async function save() {
}
}
function _affectedCount(vlan) {
return props.devices.filter(
d => !d.is_gateway && !d.is_livebox && d.interfaces.some(i => i.vlan_id === vlan.id)
).length
}
async function remove(vlan) {
const label = vlan.vlan_id != null ? `VLAN ${vlan.vlan_id}${vlan.name}` : `LAN ${vlan.name}`
if (!confirm(`Supprimer ${tFmt('confirmDeleteNetwork', label)}`)) return
const count = _affectedCount(vlan)
let msg = `Supprimer ${tFmt('confirmDeleteNetwork', label)}`
if (count > 0) msg += '\n' + tFmt('confirmDeleteNetworkHosts', count)
if (!confirm(msg)) return
try {
await vlansApi.remove(vlan.id)
emit('refresh')
+3
View File
@@ -70,6 +70,7 @@ const LANGS = {
badgeLivebox: 'Livebox',
confirmDeleteDevice: '{0} et tous ses liens ?',
confirmDeleteNetwork: '{0} ?',
confirmDeleteNetworkHosts: '{0} équipement(s) hors passerelle seront également supprimés.',
saveError: 'Erreur lors de la sauvegarde',
deleteError: 'Erreur lors de la suppression',
descPlaceholder: 'Rôle, OS, notes…',
@@ -229,6 +230,7 @@ const LANGS = {
badgeLivebox: 'ISP Box',
confirmDeleteDevice: '{0} and all its links?',
confirmDeleteNetwork: '{0}?',
confirmDeleteNetworkHosts: '{0} non-gateway device(s) will also be deleted.',
saveError: 'Error while saving',
deleteError: 'Error while deleting',
descPlaceholder: 'Role, OS, notes…',
@@ -384,6 +386,7 @@ const LANGS = {
badgeLivebox: 'Router ISP',
confirmDeleteDevice: '{0} y todos sus enlaces?',
confirmDeleteNetwork: '{0}?',
confirmDeleteNetworkHosts: '{0} equipo(s) no gateway también serán eliminados.',
saveError: 'Error al guardar',
deleteError: 'Error al eliminar',
descPlaceholder: 'Rol, SO, notas…',