TestRunReturnsStartError expected a start with no TLS flags to fail;
since the first-install wizard that start succeeds (install mode with
a self-signed certificate) and Run blocked until the 10 min timeout.
Use --tls-cert without --tls-key, which is always refused.
TestICMPStateMachine could advance the fake clock twice in one step on
a slow runner, firing an extra probe ([1 1 1 0 0 1 1]). Advance once,
only after the ICMP loop has re-armed its timer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TuKgyKve2XLFtrrXWMi2w
The owner asked whether remote pollers could work per site over an
outbound-only MQTT link, keep polling while the master is down, and
receive device credentials sealed to their own key. The assessment
found it feasible but deferred it to v2, so the requirements, the gaps
in the current code and the main risks are written down for later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operators coming from LibreNMS expect the same figures under a port
graph. Add them:
- Totals: bytes transferred in and out over the selected range
(GraphResponse.totals), integrated from the average rate across the
stitched raw and hourly tiers without double counting; partial
coverage is flagged.
- Summaries read Now / Avg / Max / 95th / Total per direction, plus an
aggregate (in+out) 95th (p95.aggregate_bps, hot store only).
- Utilisation as a percentage of port speed, and a "% of speed" axis.
- "Compare previous period" overlay on the graph view and expanded row.
- Errors and discards graphs (GET /api/v1/graph/counters), hot-store
range only since they have no rollups. Packet and broadcast/multicast
graphs stay disabled: those counters are not polled (DEVIATIONS 56).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TuKgyKve2XLFtrrXWMi2w
Make nmon easier to install and run without editing YAML by hand:
- Settings: point-and-click forms over config.yaml (GET/PUT
/api/v1/settings/form); the YAML editor moves to /settings/yaml.
- First install: with no sign-in configured nmon starts in install
mode, serves only the setup wizard (sign-in, basics, devices,
alerting) and closes it for good once sign-in is saved. A
self-signed certificate is generated when no TLS source is given.
poller.interval becomes a config setting.
- Reverse proxies: opt-in --http-listen plain-HTTP listener beside
HTTPS; sign-in stays required.
- Webhooks: Generic, Teams Adaptive Card, Slack and Discord presets,
template preview and test send.
- Help: the operations, coverage and profile docs served at /help.
- Polling: per-device "Poll now", a dry-run test poll returning an
SNMP trace, and time-limited in-memory SNMP debug recording, in the
UI and as `nmon device poll`.
- Ansible: deploys into install mode when no sign-in is set, and no
longer overwrites the config.yaml the wizard wrote.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TuKgyKve2XLFtrrXWMi2w
The remote is a Gitea server whose only runner label is debian-13, so the
ubuntu-latest jobs never got picked up. The arm64 test leg is dropped
because no arm runner exists there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TuKgyKve2XLFtrrXWMi2w
The owner wants LibreNMS-like 5-minute polling by default and the option
to poll a given device faster or slower. Devices gain poll_interval_s
(new migration; 0 means the default, otherwise 30 to 3600 s), settable in
the device edit dialog and through PUT /api/v1/devices/{slug}; the
poller picks it up on the next inventory refresh.
Everything that assumed one-minute samples now follows the device's
interval: graph and p95 rate windows are at least two polls, the max
series uses irate, the up-series lookback, the no_data warning, the
"unheard of for three polls" fallback and the rollup's thin-window check.
interface_errors widens to 10 m. profiletest keeps 60 s because the
expect budgets are keyed by the 1m group. Recorded as DEVIATIONS 49.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TuKgyKve2XLFtrrXWMi2w
The Ports grid only drew a fixed 24 h graph per port, and an expanded row
was fixed at 24 h too, so there was no way to look at 1 h, 7 d or 30 d
the way LibreNMS allows, and the grid never showed p95. The grid now has
one range picker (1 h to 1 y) and a p95 toggle shared by every card, and
each card shows current, average and p95 per direction under the graph.
An expanded row gets its own range picker and p95 toggle, and "Open graph
view" keeps the chosen range.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TuKgyKve2XLFtrrXWMi2w
Brings in the L2 table job, Find host, the L2 and Routing tabs, the BGP page,
bgp_peer_down and the optional IEEE vendor registry on top of v1.2 custom
alert rules. Conflicts were the shared backup table list, DEVIATIONS rows
(v1.3's 50-55 renumbered to 43-48) and the generated openapi.json and web
schema, which were regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4PdJUPrakM8fHvN8jYv2x
The UI for the MAC/IP lookup and BGP monitoring.
Find host (/find, and the header search when the text is a MAC, IP, CIDR or
vendor: query; a customer name or circuit id still goes to the port search)
answers "device X port Y, VLAN Z, last seen 3 min ago" with the uplinks and
history collapsed underneath, IPv6 addresses first. A vendor column and a
"private" badge for locally administered addresses appear wherever a MAC does.
The device page gets an L2 tab only when the job holds rows (MAC table with a
VLAN filter and history toggle, ARP/ND, VLAN list), and the Ports tab shows
each port's MAC count linking to that port's MACs.
The BGP page lists every session with the down ones first and state, AS,
family and text filters; a device's Routing tab shows its sessions with
click-to-graph on accepted prefixes and update rates. The Poller page shows the
L2 job status and non-alert notes such as oui_unavailable. The policy template
"Routers: BGP peer down" binds the new rule. The mock API and a new Playwright
spec cover all of it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4PdJUPrakM8fHvN8jYv2x
Owner request: finding where a host is plugged in from a MAC or IP is a must,
and BGP peer monitoring is wanted.
L2 job (internal/l2): a job of its own, off the 60 s poll path (its sessions
are path "l2"), walks BRIDGE-MIB, Q-BRIDGE-MIB, CISCO-VTP-MIB (a per-VLAN
context walk, enabled by the cisco-ios profile flag l2.per_vlan_context, not by
detection) and the IP-MIB neighbour tables (IPv4 ARP and IPv6 ND, IPv4-only
fallback) every l2.interval, with its own concurrency cap and a per-device
deadline. It yields to the poller like the rollup job, and a device that times
out is skipped for the cycle, counted and named on /poller/health. Migration 14
adds fdb_entries, arp_entries, vlans and l2_walks with first/last-seen history,
batched upserts, daily pruning by last_seen (l2.keep) and backup coverage.
API (additive): /l2/search ranks edge ports first (a port is an uplink above
l2.edge_max_macs MACs, with an LLDP neighbour, or as a LAG), plus the device
fdb/arp/l2/l2-ports tables and /interfaces/{id}/macs; vendor and mac_kind are
computed at query time from the optional registry.
BGP: profiles label peer rows by decoding the table index (index_label.layout),
so each peer, IPv4 or IPv6, is one component whatever MIB feeds it; AS, local
AS, router id and description are component attributes, never labels.
CISCO-BGP4 (cisco-ios, cisco-nxos), Juniper BGP4-V2 and Arista BGP4V2 replace
the optional generic BGP4-MIB metrics, which edgeos, fortigate, pfsense and vyos
switch on. /bgp/peers lists sessions down first. The built-in rule
bgp_peer_down (critical, not established for 2 minutes) fires only where a
policy binds it; its notification names the peer's AS and description.
Fixtures are generated (Q-BRIDGE with IPv4/IPv6 neighbours, BRIDGE-MIB only,
Cisco per-VLAN contexts via a new simagent contexts feature) and checked
against real LibreNMS recordings. DEVIATIONS 50-55 record the departures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4PdJUPrakM8fHvN8jYv2x
The owner asked to see the vendor of each MAC where the IEEE registry can be
fetched, and never to depend on it. internal/oui downloads MA-L, MA-M and MA-S
weekly through the outbound allow-list (timeouts, a size cap and a row-count
sanity check, so a truncated or wrong download keeps the previous file), stores
one checksummed, atomically written file in <data-dir>/oui that nmon backup
includes, and looks MACs up by the longest prefix. A missing registry only
leaves the vendor empty. `nmon oui import DIR|FILE` loads the CSVs offline, and
the Ansible role can ship them next to the MIB sidecar.
internal/hostaddr parses the MAC, prefix, IP and CIDR forms the lookup accepts
and classifies a MAC as global, local or multicast. config gains the l2 and oui
sections (validated here; the L2 job that reads l2 follows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4PdJUPrakM8fHvN8jYv2x
The predefined rules are too constraining: an ISP cannot say "CPU over
80% on servers for 15 minutes" or "uplink without a description", and
every new need meant editing config.yaml. Admins now build their own
rules; the shipped ones stay as read-only built-in macros with their
names unchanged, so v1.1 policies and alert history keep working.
Custom rules live in a new alert_rules table (migration 13) and are
builder-only. A metric rule names a catalogue metric (every pollable
profile metric plus push-ingested names) with a transform (value, rate,
percent_of), window, function, aggregate, comparison and threshold; the
pure internal/alertrule package compiles it to MetricsQL from fixed
templates, so no user text reaches a query. An inventory rule ANDs one to
five conditions over whitelisted device or port fields and is evaluated
in Go over store rows. The evaluator takes its rule set from a function
each cycle, so edits apply live, and both kinds use the existing state
machine, policies, suppression and notifications. Comparisons now cover
> >= < <= == != everywhere. utilisation is not offered: port speed is not
a stored series. config.yaml rules still work but are deprecated.
API (additive): rule list gains id/kind/editable/spec/expr, plus get,
create, update, delete, catalogue, test (viewer, read-only) and copy of
a built-in (cpu_high, ups_on_battery, printer_supply_empty) to a custom
rule. Deleting or renaming a rule that a policy names is a 409. The UI
gets an Alert rules tab with a builder dialog, a test button and the
generated query, and the policy dialog lists custom rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4PdJUPrakM8fHvN8jYv2x
992f09b removed DeviceHeader and OverviewSummary, but six mock specs and
the live gate screenshots still waited for their test ids (device-header,
summary-tile, generic-hint, device-warnings), so they failed and stopped
gate-m4 at its UI step. Assert on what the page shows now: the System
panel (role, profile, status), the Processors/Memory/Storage panels, and
the per-port unreliable flag on the Ports tab. The device-level warning
badge and the generic-profile hint no longer exist, so their assertions
are dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4PdJUPrakM8fHvN8jYv2x
Preprod fires everything: every default rule applied to every device and
port, so a WiFi port that always sees errors was judged like a fiber
uplink where one error matters. WiFi errors are acceptable, fiber errors
are not, and one global threshold cannot say both.
An alert policy (new alert_policies table, migration 12) binds one rule
to a scope: groups plus device and port attribute matchers (ifType,
name/alias regexes, speed, site, role, profile, kind, the alert_down
bell). The highest priority decides, ignore beats alert on a tie, and a
policy's severity/threshold/for override the rule's (its threshold also
beats group overrides). With no policy nothing alerts: not metric rules,
not device_down/device_warning, not system alarms. Nothing is seeded, so
on upgrade open alerts without a binding close silently as retired.
The pure internal/alertpolicy package makes the decision; alert, reach
and the system alarms take a gate (Options.Policy, Options.Wants, a
system policy) that the app backs with a cache refreshed every 30 s and
invalidated on every API write, so policy edits apply live. New API:
/alert-policies CRUD, a viewer-level preview of what a scope covers, and
/alert-rules. The UI gets an Alert policies tab with templates and a
live preview; the port bell now says it only marks the port. Gate
scripts create the policies they need. DEVIATIONS row 41 and the
operations guide describe the new default.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4PdJUPrakM8fHvN8jYv2x
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Profiles gain category/label/of/display/scale_by/sentinels/enum_state and component_attrs;
every pollable non-interface metric is annotated and an orphan test enforces it. New routes
GET /devices/{slug}/metrics, /metrics/graph and /printers; printer supply type/colour read at
discovery into device_components.attrs; printer_supply_empty alert; supply_replaced audit event;
docs/coverage.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
Discovery re-matches unpinned devices every run and at startup; an admin can pin a
profile (PUT /devices/{slug} profile, GET /profiles, DeviceView profile_pinned and
profile_match). Rows on the device list, interface search and poller health navigate on
click and Enter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
The UI had no way to add a device. Adds an admin-only Add device dialog
(IPv6/IPv4 addresses, SNMP v1/v2c/v3, synchronous discovery, reattach 409
flow) and a device-page Actions menu (Edit with parents and SNMP
replacement, Rediscover now, Retire). Purge now requires typing the name.
API: POST /api/v1/devices/{slug}/retire (admin, audited, 409 if already
retired). OpenAPI change is additive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
The playbook (deploy/ansible) builds the release binary, installs nmon as a hardened systemd service with
self-signed/files/ACME/insecure TLS modes, break-glass and optional OIDC, MIB sidecar, ping sysctl and nightly backups.
Tested end to end in a Debian 13 systemd container, idempotent on a second run.
Bug found by that test: Run installed its SIGHUP handler only after Start, so a 'systemctl reload' during the
seconds-long startup hit Go's default action and exited nmon. The handler is now installed before Start; an early
SIGHUP is queued and applied once startup finishes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP