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
+
- ))}
- {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) && (
removeTagMutation.mutate(tag.id)}
- className="ml-1 opacity-60 hover:opacity-100"
- title="Remove tag"
+ onClick={() => removeFromGroupMutation.mutate(group.id)}
+ className="text-text-muted hover:text-text-secondary ml-1"
+ title="Remove from group"
>
×
)}
-
+
+ ))}
+ {device.groups.length === 0 && (
+
No groups assigned
+ )}
+
+ {canWrite(user) && availableGroups.length > 0 && (
+
+
- ))}
- {device.tags.length === 0 && (
-
No tags assigned
)}
- {canWrite(user) && availableTags.length > 0 && (
-
+ )}
{canWrite(user) && (