diff --git a/frontend/src/routes/_authenticated/tenants/$tenantId/devices/$deviceId.tsx b/frontend/src/routes/_authenticated/tenants/$tenantId/devices/$deviceId.tsx index 0b961d2..db52c53 100644 --- a/frontend/src/routes/_authenticated/tenants/$tenantId/devices/$deviceId.tsx +++ b/frontend/src/routes/_authenticated/tenants/$tenantId/devices/$deviceId.tsx @@ -59,6 +59,7 @@ import { WinBoxButton } from '@/components/fleet/WinBoxButton' import { RemoteWinBoxButton } from '@/components/fleet/RemoteWinBoxButton' import { SSHTerminal } from '@/components/fleet/SSHTerminal' import { RollbackAlert } from '@/components/config/RollbackAlert' +import { SNMPMetricsSection } from '@/components/fleet/SNMPMetricsSection' export const Route = createFileRoute( '/_authenticated/tenants/$tenantId/devices/$deviceId', @@ -308,27 +309,31 @@ function DeviceDetailPage() { document.getElementById('main-content')?.scrollTo(0, 0) } const [editOpen, setEditOpen] = useState(false) - const { mode, toggleMode } = useSimpleConfigMode(deviceId) - const { data: device, isLoading } = useQuery({ queryKey: ['device', tenantId, deviceId], queryFn: () => devicesApi.get(tenantId, deviceId), }) + const isRouterOS = (device?.device_type ?? 'routeros') === 'routeros' + const isSNMP = device?.device_type === 'snmp' + + const { mode, toggleMode } = useSimpleConfigMode(isRouterOS ? deviceId : '__snmp__') + const { data: backups } = useQuery({ queryKey: ['config-backups', tenantId, deviceId], queryFn: () => configApi.listBackups(tenantId, deviceId), + enabled: isRouterOS, }) // True if a pre-restore backup was created within the last 30 minutes, // indicating a config push just happened before the device went offline. - const hasRecentPushAlert = backups?.some((b) => { + const hasRecentPushAlert = isRouterOS && (backups?.some((b) => { if (b.trigger_type !== 'pre-restore') return false // created_at within last 30 minutes — compare timestamps without Date.now() const thirtyMinAgo = new Date() thirtyMinAgo.setMinutes(thirtyMinAgo.getMinutes() - 30) return new Date(b.created_at) > thirtyMinAgo - }) ?? false + }) ?? false) const { data: groups } = useQuery({ queryKey: ['device-groups', tenantId], @@ -457,7 +462,7 @@ function DeviceDetailPage() { )}> {device.status} - + {isRouterOS && } {/* Metadata + actions row */}
@@ -465,22 +470,28 @@ function DeviceDetailPage() { {device.model ?? device.board_name ?? '\u2014'} {' \u00b7 '} {device.ip_address} - {device.routeros_version && ( + {isRouterOS && device.routeros_version && ( <> {' \u00b7 '} v{device.routeros_version} )} + {isSNMP && device.snmp_version && ( + <> + {' \u00b7 '} + SNMP {device.snmp_version.toUpperCase()} + + )}
- + {isRouterOS && } {user?.role !== 'viewer' && device.routeros_version !== null && ( <> )} - {user?.role !== 'viewer' && ( + {isRouterOS && user?.role !== 'viewer' && ( )} {canWrite(user) && ( @@ -497,218 +508,364 @@ function DeviceDetailPage() {
- {/* Emergency rollback banner */} - + {/* Emergency rollback banner (RouterOS only) */} + {isRouterOS && ( + + )} - {/* Config View (Simple or Standard) */} - - {/* Device info */} -
- - - - - - - - - - {(user?.role === 'admin' || user?.role === 'super_admin') && ( - - )} -
- } - /> - - - - {canWrite(user) ? ( - - ) : ( - {device.site_name ?? 'Unassigned'} - )} - - } - /> - + {/* Main content: RouterOS gets SimpleConfigView, SNMP gets dedicated layout */} + {isRouterOS ? ( + + {/* Device info */} +
+ + + + + + + + + + {(user?.role === 'admin' || user?.role === 'super_admin') && ( + + )} +
+ } + /> + + + + {canWrite(user) ? ( + + ) : ( + {device.site_name ?? 'Unassigned'} + )} + + } + /> + - {/* Credentials (masked) */} -
-
-

Credentials

- -
-
-
- Username - - {showCreds ? '(stored \u2014 not returned by API)' : '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022'} - -
-
- Password - - {showCreds ? '(encrypted at rest \u2014 not returned by API)' : '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022'} - -
-
-
- - {/* Groups */} -
-
- -

Groups

-
-
- {device.groups.map((group) => ( -
+
+

Credentials

+ + {showCreds ? ( + <> + Hide + + ) : ( + <> + Reveal + )} -
- ))} - {device.groups.length === 0 && ( - No groups assigned - )} -
- {canWrite(user) && availableGroups.length > 0 && ( -
- + +
+
+
+ Username + + {showCreds ? '(stored \u2014 not returned by API)' : '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022'} + +
+
+ Password + + {showCreds ? '(encrypted at rest \u2014 not returned by API)' : '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022'} + +
- )} -
- - {/* Tags */} -
-
- -

Tags

-
- {device.tags.map((tag) => ( -
- - {tag.name} + + {/* Groups */} +
+
+ +

Groups

+
+
+ {device.groups.map((group) => ( +
+ {group.name} {canWrite(user) && ( )} - +
+ ))} + {device.groups.length === 0 && ( + No groups assigned + )} +
+ {canWrite(user) && availableGroups.length > 0 && ( +
+
- ))} - {device.tags.length === 0 && ( - No tags assigned )}
- {canWrite(user) && availableTags.length > 0 && ( - addTagMutation.mutate(id)}> + + + + + {availableTags.map((t) => ( + + {t.name} + + ))} + + + )} +
+ + {/* Interface Utilization */} +
+

Interface Utilization

+ +
+ + {/* Configuration History */} + + + } + alertsContent={ + + } + /> + ) : ( +
+ {/* SNMP device system info */} +
+ + + + + + + + + + + + {canWrite(user) ? ( + + ) : ( + {device.site_name ?? 'Unassigned'} + )} +
+ } + /> +
+ + {/* SNMP Profile info */} + + + {/* Interface Utilization (works for SNMP via standard MIB mapping) */} +
+

Interface Utilization

+ +
+ + {/* Groups */} +
+
+ +

Groups

+
+
+ {device.groups.map((group) => ( +
+ {group.name} + {canWrite(user) && ( + + )} +
+ ))} + {device.groups.length === 0 && ( + No groups assigned + )} +
+ {canWrite(user) && availableGroups.length > 0 && ( +
+ +
+ )} +
+ + {/* Tags */} +
+
+ +

Tags

+
+
+ {device.tags.map((tag) => ( +
+ + {tag.name} + {canWrite(user) && ( + + )} + +
+ ))} + {device.tags.length === 0 && ( + No tags assigned )}
+ {canWrite(user) && availableTags.length > 0 && ( + + )} +
- {/* Interface Utilization */} -
-

Interface Utilization

- -
- - {/* Configuration History */} - - - } - alertsContent={ - - } - /> + {/* Alerts */} + +
+ )} {canWrite(user) && (