Hover Enrichment & Device Intelligence
Hover any IP, MAC, hostname, or interface in the terminal to resolve it via DNS, OUI, NetBox, LibreNMS, Netdisco, and the NetStacks Crawler.
Overview
Hover Enrichment turns inert terminal text into live, clickable context. When you hover an IP address, MAC address, hostname, or interface name in any terminal session, NetStacks recognizes the token, looks it up across every configured data source in parallel, and renders the merged result in a small popover — without leaving your shell.
Out of the box it can answer questions like:
- What is this IP? — reverse-DNS (PTR), plus NetBox IPAM assignment and Crawler device lookup when those integrations are wired in.
- Whose MAC is this? — IEEE OUI vendor, MAC classification (broadcast, multicast, VRRP virtual, randomized / locally administered), and the last-seen switchport from Netdisco / the NetStacks Crawler.
- What is on the other end of this interface? — CDP/LLDP neighbor and per-port details from the Crawler, plus NetBox interface metadata.
Two concepts drive the whole system. Matchers decide which tokens are recognized (via regex patterns, optionally gated by the session's CLI flavor). Sources decide what data is fetched for a recognized token. Each matcher is assigned a list of sources, and they run concurrently when the matcher fires.
Enrichment is handled entirely by the bundled Local Agent. Matchers, sources, the OUI cache, and lookup results all live agent-side; the terminal webview is a thin renderer. This is a Terminal feature — no Controller is required. Lookups that reach external systems (NetBox, LibreNMS, Netdisco/Crawler, macvendors.com) go out only when you have configured those sources.
How It Works
The pipeline for a single hover, end to end:
- On terminal startup the webview calls
GET /enrich/active-matchers. Only matchers that have at least one available source are returned, and the webview registers one xterm link provider per pattern. A source is "available" if it is a built-in, or an API-resource source that has an API resource bound to it. - As output scrolls, each link provider scans visible lines for its regex. Matched tokens become hoverable (no underline, no pointer cursor — they look like normal text until you hover).
- On hover, the webview calls
POST /enrich/matchwith the token, the session id, and the session's CLI flavor. The agent picks the highest-priority matcher whose pattern matches and whose CLI-flavor gate accepts the session, then returns the normalized token and the list of source names to run. - The webview fires
POST /enrich/sourcefor each enabled source in parallel, streaming each result into the popover as it lands. A skeleton spinner shows per source until it resolves. - The agent caches the merged result per
(session host, token type, token)with a 300-second TTL, so re-hovering the same token is instant and makes no requests.
Sources run concurrently and fail independently. A 404 or empty result simply leaves that source out of the popover (no "no data" noise). A real failure — network error, HTTP 5xx, or a rate limit — is surfaced in a dedicated errors section so you can tell "nothing to show" apart from "couldn't fetch".
When several matchers' patterns overlap on the same token, the matcher with the greatest priority wins and only its source list runs. All built-in matchers ship at priority 10 — raise a custom matcher above that to override a built-in for a specific token shape.
The Hover Popover
The popover appears next to the cursor and fills in as sources return. Its header shows the token, a badge with the matcher name that fired, and — when AI Digest is enabled — a ✦ button. Below the header, one section per source renders as labelled key/value rows, each tagged with the source's display name (DNS, OUI, Crawler, NetBox IPAM, and so on).
Popover behavior worth knowing:
- Force-touch / move-into. When you move the cursor off the token, the popover waits ~150 ms before closing so you can travel into it — the moment the cursor enters the popover it stays open. This makes the toggles and buttons inside reachable without it vanishing under you.
- Drag to pin. Grab the header and drag to reposition. A dragged (user-positioned) popover is pinned: it no longer auto-repositions on content changes and no longer auto-closes on mouse-leave. Use the × close button, press elsewhere, or click outside it to dismiss.
- Raw response toggle. Any API-backed section that returned more than its picked fields shows a
{}button. Click it to expand the full raw response inline — handy for discovering fields you haven't picked yet. - AI Digest. With AI Digest on, the ✦ button replaces the sections with a 2–3 sentence AI summary of everything the sources returned for that token. A ← button takes you back to the raw sections.
All matching, source dispatch, JSON unwrapping, and field selection happen in the agent. The popover just paints what the agent sends, driven by a small _meta.fields block attached to each result. That is why a custom source you add shows up correctly with no front-end changes.
Matchers
A matcher is a named set of regex patterns, an optional list of CLI flavors it applies to, a priority, and a list of assigned sources. Manage them under Settings → Enrichment → Matchers.
Patterns are standard regex (one per line in the editor). The full match (group 0) is used as the lookup token unless a capture group is configured. The built-in editor has a Test against sample text box that highlights exactly what each pattern matches before you save.
Built-in matchers can have their assigned sources changed, but their name, patterns, flavors, and priority are immutable and they cannot be deleted. To customize matching behavior, create a new matcher (optionally at a higher priority) rather than trying to edit a built-in's regex.
Built-in Matchers
NetStacks ships five built-in matchers, all at priority 10. The IP and MAC matchers fire for every CLI flavor; the interface matchers are flavor-gated (see below).
| Matcher | Matches | CLI flavors | Default sources |
|---|---|---|---|
ipv4 | IPv4 addresses anywhere in output | all | dns_ptr, crawler_device, netbox_ip |
mac_colon | MACs with : or - separators | all | mac_address_type, oui_vendor, crawler_mac |
mac_cisco | Cisco-format MACs (aabb.ccdd.eeff) | all | mac_address_type, oui_vendor, crawler_mac |
cisco_interface | Cisco / Arista interfaces (Gi0/1, GigabitEthernet0/1, Po1, Vl10…) | cisco-ios, cisco-iosxr, cisco-nxos, arista | crawler_port, crawler_neighbor, netbox_interface |
junos_interface | Juniper interfaces (ge-0/0/0, xe-0/1/0, ae0…) | juniper | crawler_port, crawler_neighbor, netbox_interface |
The exact regex behind the IPv4 and Cisco-MAC matchers, for reference:
# ipv4
\b(?:\d{1,3}\.){3}\d{1,3}\b
# mac_cisco (XXXX.XXXX.XXXX)
\b(?:[0-9a-fA-F]{4}\.){2}[0-9a-fA-F]{4}\bCLI Flavors
A matcher with a non-empty CLI-flavor list only fires when the session's flavor is in that list. This is what stops Gi0/1 from being treated as a Cisco interface on a Linux box, or ge-0/0/0 from matching on a Cisco switch. Recognized flavors:
auto · linux · cisco-ios · cisco-ios-xr · cisco-nxos · juniper · arista · paloalto · fortinetSet the flavor per session in Session Settings → CLI Flavor. The default is auto.
Interface matchers require a specific flavor — they do not fire while the session flavor is auto. If you hover an interface name and the popover says "No matcher fired", set the session's CLI Flavor (juniper, cisco-ios, etc.) so the right matcher activates. IP and MAC matchers are flavor-agnostic and work regardless.
Sources
A source is one place to look up a token. There are two kinds: builtin sources resolve in-process (DNS, OUI, MAC classification), and api_resource sources call an external HTTP API through a configured API Resource. Manage sources under Settings → Enrichment → Sources, where each row shows its kind, its bound API resource and path (or field count for built-ins), and an enable/disable toggle.
Built-in Sources
Three sources require no integration and work the moment you install NetStacks:
dns_ptr— DNS- Reverse-DNS (PTR) lookup for an IP. Returns the first PTR name (and keeps the rest under
all). No record found is treated as "no data", not an error. oui_vendor— OUI- IEEE OUI vendor lookup for a MAC. Layered to avoid hammering external APIs: locally administered MACs are skipped entirely; otherwise a local SQLite
oui_vendorstable is checked first (seeded from IEEE in the background); only on a miss does it fall back tomacvendors.com, caching the result back into the local table so the next lookup for that prefix is offline. mac_address_type— classification- Pure-logic MAC classification from the IEEE bits + well-known prefixes. Explains what kind of address you're looking at when there's no real vendor: Broadcast, IPv4/IPv6 Multicast, IEEE 802.1 (STP/LLDP), VRRP Virtual (gateway VIP), QEMU/KVM, Docker, and randomized / locally administered MACs. A normal vendor-assigned MAC returns nothing here (the OUI section already covers it).
API Resource Sources
The remaining built-in sources are api_resource sources. Each one needs an API Resource (base URL + auth) bound to it before it becomes available. NetStacks tries to auto-bind these at first run by matching API-resource names (containing netbox, or netdisco / crawler); until a resource is bound, the source shows as "unconfigured (no API resource bound)" and is silently skipped.
| Source | For | Path template | Unwrap |
|---|---|---|---|
crawler_mac | Last-seen switchport for a MAC (Netdisco / Crawler) | /api/v1/search/node?q={token_url} | — |
crawler_device | Device by IP / hostname (Crawler) | /api/v1/search/device?q={token_url} | — |
crawler_port | Per-port info from device neighbors | /api/v1/device/{session_host}/neighbors | — |
crawler_neighbor | CDP/LLDP neighbor for an interface | /api/v1/device/{session_host}/neighbors | — |
netbox_ip | NetBox IPAM assignment (device / VRF / status) | /api/ipam/ip-addresses/?address={token_url} | results.0 |
netbox_interface | NetBox interface details | /api/dcim/interfaces/?name={token_url} | results.0 |
LibreNMS participates as an integration source as well — the agent knows how to query LibreNMS devices and link stats — and is also available to the AI assistant alongside NetBox and the NetStacks Crawler. To surface LibreNMS in the hover popover, add an api_resource source pointing at your LibreNMS API resource (see Worked Examples).
The Crawler, NetBox, and LibreNMS sources are only as good as the API Resource behind them. Set up those integrations and their credentials first, then bind them to the enrichment sources. For NetBox specifically, see NetBox Integration.
Template Variables
The path template, the response unwrap, and any picked-field key can interpolate variables that the agent substitutes per hover. This is how a bare token like Gi0/1 — which can't identify its own device — gets answered: the session host is injected from context.
{token}- The literal hovered (normalized) token.
{token_url}- The token, URL-encoded — use this inside query strings.
{session_host}- The session's hostname / connect target. (
{sessions_host}is accepted as a typo-friendly alias.) {session_host_ip}- The session host resolved to its first IPv4 (returned as-is if it is already an IP, or empty if resolution fails). Only triggers a DNS resolve when the marker is actually present.
{session_name}- The session's display name.
Unknown markers are left in place on purpose, so missing context is visible in the agent logs rather than silently dropped.
Picked Fields & Formats
Response unwrap drills into the API response to the object you care about (empty = whole body). It accepts dotted/bracket notation (results.0, ips[0].name) or a JSONPath expression (anything starting with $, e.g. $.ips[?(@.router_name=='{session_host}')]). When the unwrapped value is an array, dotted-path picks project to the first element automatically.
Picked fields then choose which values appear in the popover. Each field has a key (dotted/bracket or JSONPath), a display label, and a format. Available formats:
string · datetime · uptime · bytes · status_pillstatus_pill renders the value as a coloured badge (up/down/warn); datetime, uptime, and bytes humanize their values. If none of your picked keys resolve against the actual response, the agent falls back to showing the whole object so you can see the real shape and fix your keys — the same data is always reachable via the {} raw toggle.
When editing an api_resource source, use Run Test with a sample token (and optional sample session host). NetStacks shows the raw response, applies your unwrap, and lets you click fields to pick them, with a live preview of the formatted result — so you never have to guess at JSON paths.
Settings & Toggles
Everything lives under Settings → Enrichment. The toggles you'll use most:
- Enable hover enrichment — the master switch for the popover.
- Show AI Digest button (✦) — adds the AI-summary button to the popover header. Off by default.
- Per-source enable/disable — each source row has a checkbox. Disabled sources are skipped on hover but stay configured (the list is stored as
terminal.enrichment.disabledSources). Use this to mute a noisy or slow source without deleting it.
There is a master Show hover info toggle in General settings. If that is off, enrichment popovers will not appear even with hover enrichment enabled — the Enrichment panel shows a reminder when this is the case.
After any matcher or source edit, NetStacks calls POST /enrichment/reload to rebuild the agent's in-memory matcher/source registry and clear the result cache, so changes take effect immediately on your next hover.
Import & Export (TOML)
The Backup section of the Enrichment panel exports all matchers, sources, and their assignments to a single TOML file (named netstacks-enrichment-<date>.toml), and imports the same format back. This is the portable way to version-control your enrichment config or share it across a team.
Matchers reference sources by name (not internal id), so the file stays portable across installs. API-resource sources also carry the resource name alongside its id for human readability; on import, names are resolved back to ids on the target machine.
[[matcher]]
name = "ipv4"
description = "IPv4 addresses anywhere in terminal output"
patterns = ["\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b"]
priority = 10
sources = ["dns_ptr", "crawler_device", "netbox_ip"]
[[matcher]]
name = "junos_interface"
patterns = ["\\b(?:ge|xe|et|ae|lo|irb|em|fxp|vme)-\\d+/\\d+/\\d+(?:\\.\\d+)?\\b"]
cli_flavors = ["juniper"]
priority = 10
sources = ["crawler_port", "crawler_neighbor", "netbox_interface"]
[[source]]
name = "netbox_ip"
description = "NetBox IP address assignment (device / VRF / status)"
kind = "api_resource"
api_resource_name = "NetBox"
method = "GET"
path_template = "/api/ipam/ip-addresses/?address={token_url}"
response_unwrap = "results.0"
[[source.picked_fields]]
key = "assigned_object.device.name"
label = "Device"
format = "string"
[[source.picked_fields]]
key = "status.label"
label = "Status"
format = "status_pill"Import matches rows by name. By default it only adds rows that don't already exist (a safe append). Tick overwrite existing to replace matching rows in place. The import summary reports counts of matchers/sources added vs updated and assignments updated. Built-in rows can be updated by an import but not renamed.
Worked Examples
Add a LibreNMS device source
Create an api_resource source bound to your LibreNMS API resource so hovering a hostname or IP shows the LibreNMS device. LibreNMS device search returns an array under devices; unwrap to the first element and pick the fields you want.
Name: librenms_device
Kind: api_resource
API resource: LibreNMS
Method: GET
Path template: /api/v0/devices/{token_url}
Response unwrap: devices.0
Picked fields:
hostname → "Hostname" (string)
sysName → "sysName" (string)
hardware → "Hardware" (string)
version → "OS Version" (string)
uptime → "Uptime" (uptime)
status → "Status" (status_pill)Then assign it: open the ipv4 matcher and tick librenms_device under Assigned sources. Hover any IP and the LibreNMS section now appears alongside DNS, Crawler, and NetBox.
A custom CMDB source keyed on the device you're on
Use {session_host} so a port/interface token resolves against the device of the current session, and a JSONPath unwrap to select the matching record:
Path template: /api/cmdb/ports?device={session_host}&name={token_url}
Response unwrap: $.ports[?(@.name=='{token}')]
Picked fields:
description → "Description" (string)
remote_device → "Neighbor" (string)
remote_port → "Remote Port" (string)
admin_state → "State" (status_pill)A high-priority matcher for asset tags
Recognize your internal asset-tag format (e.g. AST-12345) and route it to a custom source. Priority 20 beats the built-ins so it wins on overlap.
Name: asset_tag
Patterns: \bAST-\d{4,6}\b
Priority: 20
Sources: my_cmdb_assetQ&A
- Do I need the Controller or a license for hover enrichment?
- No. Enrichment runs entirely in the Terminal's bundled Local Agent. Built-in DNS, OUI, and MAC classification work out of the box; the NetBox / LibreNMS / Crawler sources just need you to configure those integrations.
- Why does nothing happen when I hover an interface name?
- Interface matchers are CLI-flavor gated and do not fire while the session flavor is
auto. Set the session's CLI Flavor (juniper, cisco-ios, cisco-nxos, arista…) in Session Settings and try again. IP and MAC tokens work regardless of flavor. - A source shows as "unconfigured". What does that mean?
- It's an
api_resourcesource with no API Resource bound. Bind one in the source editor (or set up the matching integration so it auto-binds). Unconfigured sources are skipped silently, so they don't produce errors in the popover. - How fresh is the data — is it cached?
- Results are cached per
(session host, token type, token)for 300 seconds, so re-hovering the same token is instant and makes no requests. Editing matchers/sources triggers a reload that clears the cache. - What's the difference between an empty popover and an error?
- A 404 or empty result is "no data" and simply leaves that source out. A network failure, HTTP 5xx, or rate limit (e.g. macvendors.com returning 429) appears in a dedicated errors section so you can tell the two apart.
- Why does hovering a randomized MAC show a "Type" but no vendor?
- Locally administered MACs (randomized client MACs, VXLAN VTEPs, VM/container NICs) have no IEEE OUI to look up, so the OUI source is skipped. The
mac_address_typesource instead tells you what kind of address it is — QEMU/KVM, Docker, VRRP virtual, randomized, and so on. - Can I share my enrichment setup with my team?
- Yes — use Export TOML to produce a portable file (sources are referenced by name) and Import TOML… on each machine. Import is additive by default; tick overwrite existing to replace matching rows.
- How does a port lookup know which device I'm on?
- The session host is injected via the
{session_host}/{session_host_ip}template variables. That's how a bare token likeGi0/1can be resolved against the correct device in the Crawler or your CMDB.
Troubleshooting
No popover appears on any token
Confirm both switches are on: Settings → Enrichment → Enable hover enrichment and the General Show hover info toggle (the latter also gates enrichment). Also remember the active-matcher list only includes matchers that have at least one available source — if every assigned source is disabled or unconfigured, the matcher won't register at all.
Interface tokens never light up
Set the session's CLI Flavor away from auto. Interface matchers require a specific flavor to fire.
OUI shows "rate limited (HTTP 429)"
That's the free macvendors.com tier throttling. It heals itself: the agent downloads the IEEE OUI database in the background, after which lookups are served locally from the oui_vendors table with no external call.
A NetBox / Crawler section is empty but you expect data
Open the source in the editor and use Run Test with a known token. Check the raw response shape against your response unwrap (a common cause is results.0 vs data.0). If your picked keys don't resolve, the agent falls back to showing the whole object — expand the {} raw toggle to see the real field names.
Changes to a matcher/source didn't take effect
Saving in the UI reloads the agent registry automatically. If you edited config out of band, the reload runs on the next save; the popover always reads the live agent state, so simply re-hover after saving.
Related
- Terminal Overview — the interface the popover lives in.
- Session Context — how CLI flavor and session metadata feed enrichment.
- NetBox Integration — set up the NetBox API resource the IPAM/interface sources use.
- Network Discovery — the Crawler / Netdisco data behind switchport and neighbor lookups.
- AI Chat — the assistant behind the AI Digest button, with the same NetBox / LibreNMS / Crawler integration sources.
- The Local Agent — the component that performs all enrichment lookups.