feat: Copy VLANs wizard + CPU display fix for hardware switches (v0.11.0 Beta)
Add 'Copy VLANs from Another Switch' — a 3-step modal available on the VLANs tab of any switch device. Step 1 selects a source switch and which VLAN IDs to copy (VLAN 1 deselected by default with a management risk warning). Step 2 lets the user assign interfaces on the target device per VLAN: clicking a port chip cycles it through None → Tagged → Untagged → None, with a live summary and bridge picker per VLAN. Conflict detection identifies VLANs already present on the target and offers per-VLAN Skip/Overwrite with the port grid disabling itself when Skip is active. Step 3 shows the full plan before a single apply call executes all operations over one device connection and reports per-VLAN results. Also fix CPU load display on the Devices page for hardware switches that report 0% CPU via ASIC offloading — the previous filter (v > 0) silently dropped all valid zero readings; now filters on field presence instead.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mikrotik-manager-backend",
|
||||
"version": "0.10.9-beta",
|
||||
"version": "0.11.0-beta",
|
||||
"description": "Mikrotik Network Management Platform - Backend API",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1364,6 +1364,55 @@ router.delete('/:id/vlans/:vlanDbId', requireWrite, async (req: Request, res: Re
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/devices/:id/vlans/copy (bulk copy VLANs from another switch)
|
||||
router.post('/:id/vlans/copy', requireWrite, async (req: Request, res: Response) => {
|
||||
const deviceRow = await queryOne<any>(
|
||||
`SELECT id, ip_address, api_port, api_username, api_password_encrypted FROM devices WHERE id = $1`,
|
||||
[req.params.id]
|
||||
);
|
||||
if (!deviceRow) return res.status(404).json({ error: 'Device not found' });
|
||||
|
||||
const { operations } = req.body as {
|
||||
operations: Array<{
|
||||
action: 'add' | 'update';
|
||||
vlan_id: number;
|
||||
bridge: string;
|
||||
tagged_ports: string[];
|
||||
untagged_ports: string[];
|
||||
}>;
|
||||
};
|
||||
|
||||
if (!Array.isArray(operations) || operations.length === 0) {
|
||||
return res.status(400).json({ error: 'operations array is required and must not be empty' });
|
||||
}
|
||||
|
||||
const collector = new DeviceCollector(deviceRow);
|
||||
const results: Array<{ vlan_id: number; action: string; success: boolean; error?: string }> = [];
|
||||
|
||||
try {
|
||||
await collector.connect();
|
||||
|
||||
for (const op of operations) {
|
||||
try {
|
||||
if (op.action === 'add') {
|
||||
await collector.addBridgeVlan(op.bridge, op.vlan_id, op.tagged_ports, op.untagged_ports);
|
||||
} else {
|
||||
await collector.updateBridgeVlan(op.bridge, op.vlan_id, op.tagged_ports, op.untagged_ports);
|
||||
}
|
||||
results.push({ vlan_id: op.vlan_id, action: op.action, success: true });
|
||||
} catch (err) {
|
||||
results.push({ vlan_id: op.vlan_id, action: op.action, success: false, error: err instanceof Error ? err.message : String(err) });
|
||||
}
|
||||
}
|
||||
|
||||
await collector.collectVlans();
|
||||
const vlans = await query(`SELECT * FROM vlans WHERE device_id = $1 ORDER BY vlan_id ASC`, [req.params.id]);
|
||||
return res.json({ results, vlans });
|
||||
} finally {
|
||||
collector.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Bond (LAG / LACP) routes ────────────────────────────────────────────────
|
||||
|
||||
// POST /api/devices/:id/bonds
|
||||
|
||||
Reference in New Issue
Block a user