Files
nmon/internal/api/interfaces.go
jleung ad220eae58 Interface-down alerts are opt-in: per port (bell toggle, bulk) and via groups
interfaces.alert_down / groups.alert_down (migration 9); interface_down rule OptIn; non-opted ports' open alerts close silently as retired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
2026-10-23 05:04:41 +00:00

90 lines
3.1 KiB
Go

package api
import (
"context"
"net/http"
"github.com/danielgtaylor/huma/v2"
"github.com/v10networks/nmon/internal/store"
)
// InterfaceHit is one interface search result with its device and live rates.
type InterfaceHit struct {
ID string `json:"id" doc:"Interface ULID."`
Device DeviceRef `json:"device"`
IfName string `json:"ifname"`
IfAlias string `json:"ifalias"`
Description string `json:"description" doc:"Operator-owned port description (CSV import); discovery never overwrites it."`
IfDescr string `json:"ifdescr"`
OperStatus int64 `json:"oper_status"`
AdminStatus int64 `json:"admin_status"`
SpeedBPS int64 `json:"speed_bps" doc:"Link speed in bits per second (ifHighSpeed when set)."`
InBPS *float64 `json:"in_bps" nullable:"true"`
OutBPS *float64 `json:"out_bps" nullable:"true"`
AlertDownState
}
// InterfaceList is a page of InterfaceHit.
type InterfaceList struct {
Items []InterfaceHit `json:"items"`
Page int64 `json:"page"`
PerPage int64 `json:"per_page"`
Total int64 `json:"total"`
}
func (s *server) registerInterfaces(api huma.API) {
huma.Register(api, huma.Operation{
OperationID: "list-interfaces", Method: http.MethodGet, Path: "/api/v1/interfaces", Tags: []string{"interfaces"},
Summary: "Search interfaces by name or alias",
Description: "q matches ifname, ifalias and description, case-insensitively, across all devices. " +
"Rates are null when unknown or unreliable.",
}, s.listInterfaces)
}
func (s *server) listInterfaces(ctx context.Context, in *struct {
Q string `query:"q" doc:"Substring of ifname, ifalias or description."`
PageParams
}) (*struct{ Body InterfaceList }, error) {
rows, total, err := s.Store.SearchInterfaceHits(ctx, in.Q, store.Page{Offset: in.offset(), Limit: in.limit()})
if err != nil {
return nil, s.storeErr(err)
}
out := InterfaceList{Items: make([]InterfaceHit, 0, len(rows)), Page: in.pageNo(), PerPage: in.limit(), Total: total}
var rates map[string]*ifRates
if len(rows) > 0 {
rates, _ = s.liveRates(ctx, `iface=~`+quote(regexAlt(hitIDs(rows))), false)
}
via, err := s.Store.InterfaceAlertDownVia(ctx, hitIDs(rows))
if err != nil {
return nil, s.storeErr(err)
}
for i := range rows {
r := &rows[i]
inB, outB := rates[r.ID].bps(&r.Interface)
out.Items = append(out.Items, InterfaceHit{
ID: r.ID, Device: DeviceRef{Slug: r.DeviceSlug, Name: r.DeviceName}, IfName: r.IfName, IfAlias: r.IfAlias, Description: r.Description,
IfDescr: r.IfDescr, OperStatus: r.OperStatus, AdminStatus: r.AdminStatus, SpeedBPS: speedBPS(&r.Interface),
InBPS: inB, OutBPS: outB, AlertDownState: alertDownState(r.AlertDown, via[r.ID]),
})
}
return &struct{ Body InterfaceList }{out}, nil
}
// speedBPS is the link speed in bits per second: ifHighSpeed (Mbit/s) when the
// device reports it, since ifSpeed saturates at 4.29 Gbit/s.
func speedBPS(i *store.Interface) int64 {
if i.IfHighSpeed > 0 {
return i.IfHighSpeed * 1_000_000
}
return i.IfSpeed
}
func hitIDs(rows []store.InterfaceHit) []string {
ids := make([]string, len(rows))
for i := range rows {
ids[i] = rows[i].ID
}
return ids
}