Files
MIKROTIK-RMM/backend/src/routes/switches.ts
T
2GT-Rich 3eb85087f2 fix: SNMP status table no longer vanishes after back/return navigation (v0.11.9 Beta)
Root cause: backend filtered switches by status='online', so a mid-poll-cycle
device (briefly 'unknown') caused the endpoint to return [] — and the frontend
rendered nothing when the array was empty.

- Backend: query ALL switch devices (not just online ones); unreachable devices
  already fall through to the Promise.allSettled error path and render with an
  error badge, so no data is lost
- Frontend: replace silent `length > 0 &&` guard with a three-way ternary that
  shows a "No managed switches found" message when the array is empty, so the
  user always sees feedback instead of a blank section
- Frontend: add staleTime: 60_000 to the SNMP query to prevent the hair-trigger
  background refetch that raced with the poller on every navigation
2026-05-17 20:04:33 -07:00

173 lines
5.9 KiB
TypeScript

import { Router, Request, Response } from 'express';
import { query } from '../config/database';
import { requireAuth, requireWrite } from '../middleware/auth';
import { DeviceCollector, DeviceRow } from '../services/mikrotik/DeviceCollector';
const router = Router();
router.use(requireAuth);
// GET /api/switches — all switch devices with port statistics
router.get('/', async (_req: Request, res: Response) => {
const switches = await query(`
SELECT d.id, d.name, d.ip_address, d.model, d.device_type, d.status, d.last_seen,
d.ros_version, d.firmware_version, d.serial_number, d.rack_name, d.rack_slot,
COUNT(i.id) FILTER (WHERE i.running = true AND i.disabled = false) AS ports_up,
COUNT(i.id) FILTER (WHERE i.running = false AND i.disabled = false) AS ports_down,
COUNT(i.id) FILTER (WHERE i.disabled = true) AS ports_disabled,
COUNT(i.id) AS ports_total
FROM devices d
LEFT JOIN interfaces i ON i.device_id = d.id
AND (i.type ILIKE 'ether%' OR i.type ILIKE 'sfp%'
OR i.name ILIKE 'ether%' OR i.name ILIKE 'sfp%')
WHERE d.device_type = 'switch'
GROUP BY d.id
ORDER BY d.name ASC
`);
res.json(switches);
});
// GET /api/switches/lldp — LLDP enabled/disabled status per online switch
router.get('/lldp', async (_req: Request, res: Response) => {
const switches = await query<DeviceRow>(
`SELECT * FROM devices WHERE device_type = 'switch' AND status = 'online'`
);
const results = await Promise.allSettled(
switches.map(async (sw: DeviceRow) => {
const collector = new DeviceCollector(sw);
try {
await collector.connect();
const lldp = await collector.getLldpEnabled();
return { id: sw.id, name: sw.name, ip_address: sw.ip_address, ...lldp };
} finally {
collector.disconnect();
}
})
);
const statuses = results.map((r, i) => {
if (r.status === 'fulfilled') return r.value;
return {
id: switches[i].id,
name: switches[i].name,
ip_address: switches[i].ip_address,
enabled: null as boolean | null,
protocol: null as string | null,
error: (r.reason as Error).message,
};
});
res.json(statuses);
});
// PUT /api/switches/lldp — enable or disable LLDP on all online switches
router.put('/lldp', requireWrite, async (req: Request, res: Response) => {
const { enabled } = req.body;
if (typeof enabled !== 'boolean') {
return res.status(400).json({ error: '"enabled" (boolean) is required' });
}
const switches = await query<DeviceRow>(
`SELECT * FROM devices WHERE device_type = 'switch' AND status = 'online'`
);
const results = await Promise.allSettled(
switches.map(async (sw: DeviceRow) => {
const collector = new DeviceCollector(sw);
try {
await collector.connect();
await collector.setLldpEnabled(enabled);
return { id: sw.id, name: sw.name, success: true };
} finally {
collector.disconnect();
}
})
);
const statuses = results.map((r, i) => {
if (r.status === 'fulfilled') return r.value;
return {
id: switches[i].id,
name: switches[i].name,
success: false,
error: (r.reason as Error).message,
};
});
return res.json({
applied: statuses.filter(s => s.success).length,
total: switches.length,
results: statuses,
});
});
// GET /api/switches/snmp — SNMP config/status for all managed switches
// Includes offline/unknown devices so the list never disappears during a poll cycle.
// Unreachable devices fall through to the Promise.allSettled error path and render with an error note.
router.get('/snmp', async (_req: Request, res: Response) => {
const switches = await query<DeviceRow>(
`SELECT * FROM devices WHERE device_type = 'switch' ORDER BY name`
);
const results = await Promise.allSettled(
switches.map(async (sw: DeviceRow) => {
const collector = new DeviceCollector(sw);
try {
await collector.connect();
const snmp = await collector.getSnmpConfig();
return { id: sw.id, name: sw.name, ip_address: sw.ip_address, ...snmp };
} finally {
collector.disconnect();
}
})
);
const statuses = results.map((r, i) => {
if (r.status === 'fulfilled') return r.value;
return {
id: switches[i].id, name: switches[i].name, ip_address: switches[i].ip_address,
enabled: null as boolean | null, error: (r.reason as Error).message,
};
});
res.json(statuses);
});
// PUT /api/switches/snmp — apply SNMP config to all online switches
router.put('/snmp', requireWrite, async (req: Request, res: Response) => {
const config = req.body as {
enabled: boolean; community_name: string; version: 'v1' | 'v2c' | 'v3';
contact?: string; location?: string; trap_target?: string;
auth_protocol?: string; auth_password?: string;
priv_protocol?: string; priv_password?: string;
};
if (!config.community_name || !config.version) {
return res.status(400).json({ error: 'community_name and version are required' });
}
const switches = await query<DeviceRow>(
`SELECT * FROM devices WHERE device_type = 'switch' AND status = 'online'`
);
const results = await Promise.allSettled(
switches.map(async (sw: DeviceRow) => {
const collector = new DeviceCollector(sw);
try {
await collector.connect();
await collector.setSnmpConfig(config);
return { id: sw.id, name: sw.name, success: true };
} finally {
collector.disconnect();
}
})
);
const statuses = results.map((r, i) => {
if (r.status === 'fulfilled') return r.value;
return { id: switches[i].id, name: switches[i].name, success: false, error: (r.reason as Error).message };
});
return res.json({ applied: statuses.filter(s => s.success).length, total: switches.length, results: statuses });
});
export default router;