From 8b5dea152e436d1c3ec5314b74f5ff50ddc6747e Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Sat, 23 May 2026 20:33:28 -0600 Subject: [PATCH] Add guided SNMP discovery UI --- docs/agent-handoff.md | 2 +- docs/progress.md | 28 ++- frontend/src/api/client.ts | 7 + frontend/src/app/App.tsx | 3 +- frontend/src/pages/DiscoveryPage.tsx | 267 +++++++++++++++++++++++++++ frontend/src/types/api.ts | 32 ++++ 6 files changed, 328 insertions(+), 11 deletions(-) create mode 100644 frontend/src/pages/DiscoveryPage.tsx diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index b60c113..cc4495d 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -76,7 +76,7 @@ Issue source docs: - `docs/progress.md` - `docs/roadmap.md` -Current completed items include TLS expiry monitor support, HTTP/website checks, ping and TCP port checks, basic alert evaluation, alert rule editing UI, incident actions, webhook notification channels, SNMPv2c credential profiles, and the SNMP device discovery API. The next recommended implementation issue is guided SNMP discovery UI, followed by SNMP monitor selection. +Current completed items include TLS expiry monitor support, HTTP/website checks, ping and TCP port checks, basic alert evaluation, alert rule editing UI, incident actions, webhook notification channels, SNMPv2c credential profiles, the SNMP device discovery API, and guided SNMP discovery UI. The next recommended implementation issue is creating monitors from SNMP discovery selections. ## Guardrails diff --git a/docs/progress.md b/docs/progress.md index 447d497..5026512 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -73,6 +73,13 @@ Implemented SNMP device discovery API slice: - Initial monitorable items include device uptime, interface status, interface traffic, and interface errors/discards. - Backend tests cover successful discovery, missing profiles, unusable secrets, probe failures, secret masking, and raw OID avoidance. +Implemented guided SNMP discovery UI slice: + +- Discovery page can run SNMP discovery against a host using a saved SNMP profile. +- UI shows friendly device summary details and discovered interfaces. +- UI displays friendly monitorable item groups and supports selecting items for the next monitor-creation step. +- Normal discovery UI avoids raw SNMP OIDs and saved secret values. + ## Known Gaps - General credential vault workflows beyond SNMP profiles are not complete. @@ -80,8 +87,8 @@ Implemented SNMP device discovery API slice: - User management UI is not implemented. - Role management is basic and needs full admin flows. - Richer alert condition editing is not implemented yet. -- Guided SNMP device discovery and friendly SNMP monitor selection are not implemented yet. -- SNMP monitor creation and collection for interface status, traffic counters, errors, uptime, CPU, and memory checks are not implemented yet. +- SNMP monitor creation from selected discovery items is not implemented yet. +- SNMP collection for interface status, traffic counters, errors, uptime, CPU, and memory checks is not implemented yet. - Notification routing/policies are not implemented; all enabled webhook channels receive incident notifications. - Email/SMTP notifications are not implemented yet. - Graphing exists only as placeholders; metric visualization is not implemented. @@ -91,13 +98,16 @@ Implemented SNMP device discovery API slice: ## Recommended Next Work -1. Add guided SNMP discovery UI. -2. Create monitors from SNMP discovery selections. -3. Add SNMP interface status, traffic, errors, uptime, CPU, and memory collection. -4. Add notification policy/routing controls. -5. Add email/SMTP notification channel. -6. Add audit event writes for auth, monitor, credential, notification, and incident actions. -7. Build general credential vault workflows with masked secret handling. +1. Create monitors from SNMP discovery selections. +2. Add SNMP interface status, traffic, errors, uptime, CPU, and memory collection. +3. Add notification policy/routing controls. +4. Add email/SMTP notification channel. +5. Add audit event writes for auth, monitor, credential, notification, and incident actions. +6. Build general credential vault workflows with masked secret handling. +7. Add user administration UI. +8. Add graphs for website response time and monitor status history. +9. Add richer alert condition editing. +10. Add frontend coverage for monitor, alert, and notification workflows. 8. Add user administration UI. 9. Add graphs for website response time and monitor status history. 10. Add richer alert condition editing. diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 3dc6969..8f3b90d 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -13,6 +13,8 @@ import type { SnmpCredentialProfile, SnmpCredentialProfileCreate, SnmpCredentialProfileUpdate, + SnmpDiscoveryRequest, + SnmpDiscoveryResult, TcpMonitorCreate, User, WebsiteMonitorCreate, @@ -139,4 +141,9 @@ export const api = { request(`/credentials/snmp/${profileId}`, token, { method: "DELETE", }), + discoverSnmpDevice: (token: string, payload: SnmpDiscoveryRequest) => + request("/discovery/snmp", token, { + method: "POST", + body: JSON.stringify(payload), + }), }; diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index dcd5fb0..1120ac4 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -6,6 +6,7 @@ import { useAuth } from "../hooks/useAuth"; import { AlertsPage } from "../pages/AlertsPage"; import { CredentialsPage } from "../pages/CredentialsPage"; import { DashboardPage } from "../pages/DashboardPage"; +import { DiscoveryPage } from "../pages/DiscoveryPage"; import { ListPage } from "../pages/ListPage"; import { LoginPage } from "../pages/LoginPage"; import { NetworkChecksPage } from "../pages/NetworkChecksPage"; @@ -87,7 +88,7 @@ export function App() { {page === "alerts" ? ( ) : null} - {page === "discovery" ? : null} + {page === "discovery" ? : null} {page === "graphs" ? : null} {page === "credentials" ? : null} {page === "notifications" ? : null} diff --git a/frontend/src/pages/DiscoveryPage.tsx b/frontend/src/pages/DiscoveryPage.tsx new file mode 100644 index 0000000..3fbcc82 --- /dev/null +++ b/frontend/src/pages/DiscoveryPage.tsx @@ -0,0 +1,267 @@ +import { FormEvent, useEffect, useMemo, useState } from "react"; +import { CheckSquare, Network, RefreshCw, Router, Search, Square } from "lucide-react"; + +import { api } from "../api/client"; +import { Button } from "../components/Button"; +import type { SnmpCredentialProfile, SnmpDiscoveryItem, SnmpDiscoveryResult } from "../types/api"; + +interface DiscoveryPageProps { + token: string; +} + +export function DiscoveryPage({ token }: DiscoveryPageProps) { + const [profiles, setProfiles] = useState([]); + const [host, setHost] = useState(""); + const [profileId, setProfileId] = useState(""); + const [result, setResult] = useState(null); + const [selectedItemIds, setSelectedItemIds] = useState>(new Set()); + const [loadingProfiles, setLoadingProfiles] = useState(false); + const [discovering, setDiscovering] = useState(false); + const [message, setMessage] = useState(null); + + const selectedItems = useMemo( + () => (result?.monitorable_items ?? []).filter((item) => selectedItemIds.has(item.item_id)), + [result, selectedItemIds] + ); + const groupedItems = useMemo(() => groupItems(result?.monitorable_items ?? []), [result]); + + async function refreshProfiles() { + setLoadingProfiles(true); + try { + const nextProfiles = await api.snmpCredentialProfiles(token); + setProfiles(nextProfiles); + setProfileId((current) => current || nextProfiles[0]?.id || ""); + } finally { + setLoadingProfiles(false); + } + } + + useEffect(() => { + refreshProfiles().catch(() => setProfiles([])); + }, [token]); + + async function submit(event: FormEvent) { + event.preventDefault(); + if (!profileId) return; + + setDiscovering(true); + setMessage(null); + setResult(null); + setSelectedItemIds(new Set()); + try { + const discovered = await api.discoverSnmpDevice(token, { + host, + credential_profile_id: profileId, + }); + setResult(discovered); + } catch (err) { + setMessage(err instanceof Error ? err.message : "SNMP discovery failed"); + } finally { + setDiscovering(false); + } + } + + function toggleItem(itemId: string) { + setSelectedItemIds((current) => { + const next = new Set(current); + if (next.has(itemId)) { + next.delete(itemId); + } else { + next.add(itemId); + } + return next; + }); + } + + function selectGroup(items: SnmpDiscoveryItem[]) { + setSelectedItemIds((current) => { + const next = new Set(current); + const allSelected = items.every((item) => next.has(item.item_id)); + for (const item of items) { + if (allSelected) { + next.delete(item.item_id); + } else { + next.add(item.item_id); + } + } + return next; + }); + } + + return ( +
+
+
+

Discovery

+

Find SNMP devices and choose friendly items for monitoring.

+
+ +
+ +
+
+
+ +

SNMP Device Discovery

+
+ + + + + + {message ?
{message}
: null} + + {!profiles.length ?
Add an SNMP credential profile before running discovery.
: null} + + +
+ +
+
+
+

Device Summary

+
+ {result ? ( +
+ + + +
+
Description
+
{result.description || "No description reported"}
+
+
+ ) : ( +
Run discovery to see device details.
+ )} +
+ +
+
+ +

Interfaces

+
+
+ {result?.interfaces.length ? ( + result.interfaces.map((item) => ( +
+
+
{item.name}
+
{item.description || "No description"}
+
+ + +
{formatSpeed(item.speed_bps)}
+
+ )) + ) : ( +
{result ? "No interfaces reported." : "No discovery results yet."}
+ )} +
+
+ +
+
+
+ +

Monitorable Items

+
+
{selectedItems.length} selected
+
+
+ {result ? ( + groupedItems.map(({ group, items }) => ( +
+
+

{group}

+ +
+
+ {items.map((item) => ( + + ))} +
+
+ )) + ) : ( +
Run discovery to choose monitorable items.
+ )} +
+
+
+
+
+ ); +} + +function groupItems(items: SnmpDiscoveryItem[]) { + const groups = new Map(); + for (const item of items) { + groups.set(item.group, [...(groups.get(item.group) ?? []), item]); + } + return Array.from(groups, ([group, groupItems]) => ({ group, items: groupItems })); +} + +function SummaryItem({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +function Status({ value }: { value: string }) { + const classes = value === "up" ? "border-teal-500/40 bg-teal-950/40 text-teal-200" : value === "down" ? "border-red-500/40 bg-red-950/40 text-red-200" : "border-slate-600 bg-slate-900 text-slate-300"; + return {value}; +} + +function formatDuration(seconds?: number | null) { + if (!seconds) return "Unknown"; + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + if (days) return `${days}d ${hours}h`; + return `${hours}h ${Math.floor((seconds % 3600) / 60)}m`; +} + +function formatSpeed(value?: number | null) { + if (!value) return "Unknown speed"; + if (value >= 1_000_000_000) return `${value / 1_000_000_000} Gbps`; + if (value >= 1_000_000) return `${value / 1_000_000} Mbps`; + if (value >= 1_000) return `${value / 1_000} Kbps`; + return `${value} bps`; +} + +function friendlyItemType(value: string) { + return value.replaceAll("_", " "); +} diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index e9919f2..cef76b8 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -140,6 +140,38 @@ export interface SnmpCredentialProfileUpdate { retries?: number; } +export interface SnmpDiscoveryRequest { + host: string; + credential_profile_id: number; +} + +export interface SnmpDiscoveredInterface { + index: number; + name: string; + description?: string | null; + admin_status?: string | null; + oper_status?: string | null; + speed_bps?: number | null; +} + +export interface SnmpDiscoveryItem { + item_id: string; + item_type: string; + group: string; + label: string; + unit?: string | null; +} + +export interface SnmpDiscoveryResult { + host: string; + credential_profile_id: number; + device_name?: string | null; + description?: string | null; + uptime_seconds?: number | null; + interfaces: SnmpDiscoveredInterface[]; + monitorable_items: SnmpDiscoveryItem[]; +} + export interface WebsiteMonitorCreate { name: string; url: string;