diff --git a/src/webui/internal/handlers/interfaces.go b/src/webui/internal/handlers/interfaces.go
index 0bc28180..224bd132 100644
--- a/src/webui/internal/handlers/interfaces.go
+++ b/src/webui/internal/handlers/interfaces.go
@@ -10,6 +10,7 @@ import (
"net/http"
"sort"
"strings"
+ "sync"
"github.com/kernelkit/webui/internal/restconf"
)
@@ -33,10 +34,16 @@ type ifaceJSON struct {
Statistics *ifaceStats `json:"statistics"`
Ethernet *ethernetJSON `json:"ieee802-ethernet-interface:ethernet"`
BridgePort *bridgePortJSON `json:"infix-interfaces:bridge-port"`
+ Vlan *vlanJSON `json:"infix-interfaces:vlan"`
WiFi *wifiJSON `json:"infix-interfaces:wifi"`
WireGuard *wireGuardJSON `json:"infix-interfaces:wireguard"`
}
+type vlanJSON struct {
+ ID int `json:"id"`
+ LowerLayerIf string `json:"lower-layer-if"`
+}
+
type bridgePortJSON struct {
Bridge string `json:"bridge"`
STP *struct {
@@ -190,16 +197,20 @@ type interfacesData struct {
}
type ifaceEntry struct {
- Indent string // tree prefix for bridge/LAG members
- Name string
- Type string
- Status string
- StatusUp bool
- PhysAddr string
- Addresses []addrEntry
- Detail string // extra info: wifi AP, wireguard peers, etc.
- RxBytes string
- TxBytes string
+ HasMembers bool // is a bridge/LAG master with child ports
+ IsMember bool // is a bridge port or LAG member
+ IsLastMember bool // is the last child in its group
+ Forwarding bool // IP forwarding enabled (⇅ flag)
+ GroupID string // bridge/LAG name — set on parent and all its members
+ Name string
+ Type string
+ Status string
+ StatusUp bool
+ PhysAddr string
+ Addresses []addrEntry
+ Detail string // extra info: wifi AP, wireguard peers, etc.
+ RxBytes string
+ TxBytes string
}
type addrEntry struct {
@@ -221,12 +232,41 @@ func (h *InterfacesHandler) Overview(w http.ResponseWriter, r *http.Request) {
PageData: newPageData(r, "interfaces", "Interfaces"),
}
- var ifaces interfacesWrapper
- if err := h.RC.Get(r.Context(), "/data/ietf-interfaces:interfaces", &ifaces); err != nil {
- log.Printf("restconf interfaces: %v", err)
+ var (
+ ifaces interfacesWrapper
+ ri struct {
+ Routing struct {
+ Interfaces struct {
+ Interface []string `json:"interface"`
+ } `json:"interfaces"`
+ } `json:"ietf-routing:routing"`
+ }
+ ifaceErr error
+ wg sync.WaitGroup
+ )
+ wg.Add(2)
+ go func() {
+ defer wg.Done()
+ ifaceErr = h.RC.Get(r.Context(), "/data/ietf-interfaces:interfaces", &ifaces)
+ }()
+ go func() {
+ defer wg.Done()
+ // Best-effort: ignore errors (routing may not be configured).
+ // Fetch the full routing object — the /interfaces sub-path returns empty
+ // on Infix even when the data is present in the parent resource.
+ h.RC.Get(r.Context(), "/data/ietf-routing:routing", &ri) //nolint:errcheck
+ }()
+ wg.Wait()
+
+ if ifaceErr != nil {
+ log.Printf("restconf interfaces: %v", ifaceErr)
data.Error = "Could not fetch interface information"
} else {
- data.Interfaces = buildIfaceList(ifaces.Interfaces.Interface)
+ fwdSet := make(map[string]bool, len(ri.Routing.Interfaces.Interface))
+ for _, name := range ri.Routing.Interfaces.Interface {
+ fwdSet[name] = true
+ }
+ data.Interfaces = buildIfaceList(ifaces.Interfaces.Interface, fwdSet)
}
tmplName := "interfaces.html"
@@ -274,9 +314,10 @@ func prettyIfType(full string) string {
}
// buildIfaceList converts raw RESTCONF interface data into a flat,
-// hierarchically ordered display list matching the cli_pretty style.
-// Bridge members are grouped under their parent with tree indicators.
-func buildIfaceList(raw []ifaceJSON) []ifaceEntry {
+// hierarchically ordered display list. Bridge/LAG members are grouped
+// under their parent. fwdSet contains the names of interfaces with IP
+// forwarding enabled (the ⇅ flag).
+func buildIfaceList(raw []ifaceJSON, fwdSet map[string]bool) []ifaceEntry {
byName := map[string]*ifaceJSON{}
children := map[string][]string{}
childSet := map[string]bool{}
@@ -291,46 +332,68 @@ func buildIfaceList(raw []ifaceJSON) []ifaceEntry {
}
}
+ // Collect top-level interfaces (not bridge/LAG members) and sort them:
+ // loopback first, then alphabetically.
+ var topLevel []ifaceJSON
+ for _, iface := range raw {
+ if !childSet[iface.Name] {
+ topLevel = append(topLevel, iface)
+ }
+ }
+ sort.Slice(topLevel, func(i, j int) bool {
+ li := prettyIfType(topLevel[i].Type) == "loopback"
+ lj := prettyIfType(topLevel[j].Type) == "loopback"
+ if li != lj {
+ return li
+ }
+ return topLevel[i].Name < topLevel[j].Name
+ })
+
+ // Sort children of each bridge/LAG alphabetically.
+ for parent := range children {
+ sort.Strings(children[parent])
+ }
+
var result []ifaceEntry
- for _, iface := range raw {
- if childSet[iface.Name] {
- continue
- }
-
- result = append(result, makeIfaceEntry(iface, ""))
-
+ for _, iface := range topLevel {
+ e := makeIfaceEntry(iface, fwdSet)
members := children[iface.Name]
+ e.HasMembers = len(members) > 0
+ if e.HasMembers {
+ e.GroupID = iface.Name
+ }
+ result = append(result, e)
+
for i, childName := range members {
child, ok := byName[childName]
if !ok {
continue
}
- prefix := "\u251c\u00a0" // ├
- if i == len(members)-1 {
- prefix = "\u2514\u00a0" // └
- }
- e := makeIfaceEntry(*child, prefix)
+ me := makeIfaceEntry(*child, fwdSet)
+ me.IsMember = true
+ me.IsLastMember = i == len(members)-1
+ me.GroupID = iface.Name
if child.BridgePort != nil && child.BridgePort.STP != nil &&
child.BridgePort.STP.CIST != nil && child.BridgePort.STP.CIST.State != "" {
- e.Status = child.BridgePort.STP.CIST.State
- e.StatusUp = e.Status == "forwarding"
+ me.Status = child.BridgePort.STP.CIST.State
+ me.StatusUp = me.Status == "forwarding"
}
- result = append(result, e)
+ result = append(result, me)
}
}
return result
}
-func makeIfaceEntry(iface ifaceJSON, indent string) ifaceEntry {
+func makeIfaceEntry(iface ifaceJSON, fwdSet map[string]bool) ifaceEntry {
e := ifaceEntry{
- Indent: indent,
- Name: iface.Name,
- Type: prettyIfType(iface.Type),
- Status: iface.OperStatus,
- StatusUp: iface.OperStatus == "up",
- PhysAddr: iface.PhysAddress,
+ Forwarding: fwdSet[iface.Name],
+ Name: iface.Name,
+ Type: prettyIfType(iface.Type),
+ Status: iface.OperStatus,
+ StatusUp: iface.OperStatus == "up",
+ PhysAddr: iface.PhysAddress,
}
if iface.Statistics != nil {
@@ -355,6 +418,14 @@ func makeIfaceEntry(iface ifaceJSON, indent string) ifaceEntry {
}
}
+ if v := iface.Vlan; v != nil {
+ if v.LowerLayerIf != "" {
+ e.Detail = fmt.Sprintf("vid %d (%s)", v.ID, v.LowerLayerIf)
+ } else {
+ e.Detail = fmt.Sprintf("vid %d", v.ID)
+ }
+ }
+
if iface.WiFi != nil {
if ap := iface.WiFi.AccessPoint; ap != nil {
n := len(ap.Stations.Station)
diff --git a/src/webui/internal/handlers/routing.go b/src/webui/internal/handlers/routing.go
index c8464cc7..fb3dbe51 100644
--- a/src/webui/internal/handlers/routing.go
+++ b/src/webui/internal/handlers/routing.go
@@ -11,6 +11,7 @@ import (
"net/http"
"strings"
"sync"
+ "time"
"github.com/kernelkit/webui/internal/restconf"
)
@@ -20,24 +21,31 @@ type RouteEntry struct {
NextHopIface string
NextHopAddr string
Protocol string
- Preference int
+ Preference string // formatted as "distance/metric"
+ Uptime string
Active bool
}
type OSPFNeighbor struct {
RouterID string
- State string
- Role string
- Interface string
+ Priority int
+ State string // includes role suffix, e.g. "Full/DR"
Uptime string
+ Address string
+ Interface string
+ Area string
}
type OSPFIface struct {
- Name string
- State string
- Cost int
- DR string
- BDR string
+ Name string
+ Area string
+ Type string
+ State string
+ Cost int
+ Priority int
+ DR string
+ BDR string
+ NbrCount int
}
type routingData struct {
@@ -82,6 +90,8 @@ type ribRouteJSON struct {
SourceProtocol string `json:"source-protocol"`
Active *json.RawMessage `json:"active"`
RoutePreference int `json:"route-preference"`
+ OspfMetric int `json:"ietf-ospf:metric"`
+ LastUpdated string `json:"last-updated"`
}
func (r ribRouteJSON) destinationPrefix() string {
@@ -157,18 +167,22 @@ type ospfAreaJSON struct {
}
type ospfIfaceJSON struct {
- Name string `json:"name"`
- State string `json:"state"`
- Cost int `json:"cost"`
- DRRouterID string `json:"dr-router-id"`
- BDRRouterID string `json:"bdr-router-id"`
- Neighbors struct {
+ Name string `json:"name"`
+ State string `json:"state"`
+ Cost int `json:"cost"`
+ Priority int `json:"priority"`
+ InterfaceType string `json:"interface-type"`
+ DRRouterID string `json:"dr-router-id"`
+ BDRRouterID string `json:"bdr-router-id"`
+ Neighbors struct {
Neighbor []ospfNeighborJSON `json:"neighbor"`
} `json:"neighbors"`
}
type ospfNeighborJSON struct {
NeighborRouterID string `json:"neighbor-router-id"`
+ Address string `json:"address"`
+ Priority int `json:"priority"`
State string `json:"state"`
Role string `json:"infix-routing:role"`
InterfaceName string `json:"infix-routing:interface-name"`
@@ -211,7 +225,8 @@ func (h *RoutingHandler) Overview(w http.ResponseWriter, r *http.Request) {
NextHopIface: iface,
NextHopAddr: addr,
Protocol: shortProto(route.SourceProtocol),
- Preference: route.RoutePreference,
+ Preference: fmt.Sprintf("%d/%d", route.RoutePreference, route.OspfMetric),
+ Uptime: uptimeFromTimestamp(route.LastUpdated),
Active: route.Active != nil,
})
}
@@ -235,19 +250,33 @@ func (h *RoutingHandler) Overview(w http.ResponseWriter, r *http.Request) {
for _, area := range proto.OSPF.Areas.Area {
for _, iface := range area.Interfaces.Interface {
data.OSPFIfaces = append(data.OSPFIfaces, OSPFIface{
- Name: iface.Name,
- State: iface.State,
- Cost: iface.Cost,
- DR: iface.DRRouterID,
- BDR: iface.BDRRouterID,
+ Name: iface.Name,
+ Area: area.AreaID,
+ Type: ospfIfaceType(iface.InterfaceType),
+ State: ospfIfaceState(iface.State),
+ Cost: iface.Cost,
+ Priority: iface.Priority,
+ DR: iface.DRRouterID,
+ BDR: iface.BDRRouterID,
+ NbrCount: len(iface.Neighbors.Neighbor),
})
for _, nbr := range iface.Neighbors.Neighbor {
+ stateStr := capitalize(nbr.State)
+ if nbr.Role != "" && nbr.State == "full" {
+ stateStr = "Full/" + nbr.Role
+ }
+ ifaceName := nbr.InterfaceName
+ if ifaceName == "" {
+ ifaceName = iface.Name
+ }
data.OSPFNeighbors = append(data.OSPFNeighbors, OSPFNeighbor{
RouterID: nbr.NeighborRouterID,
- State: nbr.State,
- Role: nbr.Role,
- Interface: iface.Name,
+ Priority: nbr.Priority,
+ State: stateStr,
Uptime: formatUptime(nbr.Uptime),
+ Address: nbr.Address,
+ Interface: ifaceName,
+ Area: area.AreaID,
})
}
}
@@ -267,6 +296,57 @@ func (h *RoutingHandler) Overview(w http.ResponseWriter, r *http.Request) {
}
}
+func uptimeFromTimestamp(s string) string {
+ if s == "" {
+ return ""
+ }
+ t, err := time.Parse(time.RFC3339, s)
+ if err != nil {
+ return ""
+ }
+ return formatUptime(uint32(time.Since(t).Seconds()))
+}
+
+func ospfIfaceType(t string) string {
+ switch t {
+ case "broadcast":
+ return "Broadcast"
+ case "point-to-point":
+ return "P2P"
+ case "point-to-multipoint":
+ return "P2MP"
+ case "non-broadcast":
+ return "NBMA"
+ case "virtual-link":
+ return "VLink"
+ default:
+ if t != "" {
+ return capitalize(t)
+ }
+ return ""
+ }
+}
+
+func ospfIfaceState(s string) string {
+ switch s {
+ case "dr":
+ return "DR"
+ case "bdr":
+ return "BDR"
+ case "dr-other":
+ return "DROther"
+ default:
+ return capitalize(s)
+ }
+}
+
+func capitalize(s string) string {
+ if len(s) == 0 {
+ return s
+ }
+ return strings.ToUpper(s[:1]) + s[1:]
+}
+
func shortProto(proto string) string {
if i := strings.LastIndex(proto, ":"); i >= 0 {
return proto[i+1:]
diff --git a/src/webui/static/css/style.css b/src/webui/static/css/style.css
index fc1d6885..b9b5637d 100644
--- a/src/webui/static/css/style.css
+++ b/src/webui/static/css/style.css
@@ -606,6 +606,7 @@ details[open].nav-group-top > summary.nav-group-summary-top::after {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
text-align: left;
+ vertical-align: top;
}
.data-table th {
@@ -1079,7 +1080,98 @@ details[open].nav-group-top > summary.nav-group-summary-top::after {
.iface-name a { color: var(--primary); text-decoration: none; }
.iface-name a:hover { text-decoration: underline; }
-.tree-pipe { color: var(--slate-300); font-family: var(--font-mono); margin-right: 0.5rem; }
+/* Forwarding flag column */
+.iface-fwd-col { width: 1.5rem; min-width: 1.5rem; padding: 0 0.25rem !important; text-align: center; }
+.iface-fwd-flag { font-size: 0.8rem; color: var(--primary); user-select: none; }
+
+/* Tree connector column — pseudo-elements draw the continuous vertical/horizontal lines.
+ Wide enough to be a comfortable click target for the collapse button on parent rows. */
+.iface-tree-col {
+ width: 1.75rem;
+ min-width: 1.75rem;
+ padding: 0 !important;
+ position: relative;
+ overflow: visible;
+ text-align: center;
+}
+/* These two narrow icon columns must stay middle-aligned even though the rest of the
+ table uses vertical-align: top. Use td.class for higher specificity (0-1-1 beats 0-1-0). */
+.data-table td.iface-tree-col,
+.data-table td.iface-fwd-col,
+.data-table th.iface-fwd-col { vertical-align: middle; }
+
+/* Parent row: draw a vertical line from row center down to the bottom edge */
+.iface-tree-col.tree-parent::after {
+ content: '';
+ position: absolute;
+ left: 50%; top: 50%; bottom: 0;
+ width: 2px; margin-left: -1px;
+ background: var(--border);
+}
+
+/* Middle member: full-height vertical line + horizontal arm right */
+.iface-tree-col.tree-mid::before {
+ content: '';
+ position: absolute;
+ left: 50%; top: 0; bottom: 0;
+ width: 2px; margin-left: -1px;
+ background: var(--border);
+}
+.iface-tree-col.tree-mid::after {
+ content: '';
+ position: absolute;
+ left: 50%; top: 50%; right: 0;
+ height: 2px; margin-top: -1px;
+ background: var(--border);
+}
+
+/* Last member: vertical line top-to-center + horizontal arm right */
+.iface-tree-col.tree-last::before {
+ content: '';
+ position: absolute;
+ left: 50%; top: 0; bottom: 50%;
+ width: 2px; margin-left: -1px;
+ background: var(--border);
+}
+.iface-tree-col.tree-last::after {
+ content: '';
+ position: absolute;
+ left: 50%; top: 50%; right: 0;
+ height: 2px; margin-top: -1px;
+ background: var(--border);
+}
+
+/* Member rows: subtle background tint; name indented to clear the tree arm */
+.iface-member { background: var(--surface-alt); }
+/* Tighter left padding on the name cell so connector arms read as attached to the text */
+.iface-name { padding-left: 0.4rem !important; }
+.iface-member-name { padding-left: 1rem !important; }
+
+/* Bridge/LAG collapse toggle — sits inside .iface-tree-col, centred over the connector line.
+ z-index: 1 ensures it is clickable above the ::after pseudo-element. */
+.bridge-toggle {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ color: var(--fg-muted);
+ font-size: 0.65rem;
+ line-height: 1;
+ display: block;
+ width: 100%;
+ position: relative;
+ z-index: 1;
+}
+.bridge-toggle-arrow {
+ display: inline-block;
+ transition: transform 0.15s ease;
+}
+/* When the parent row is collapsed: rotate arrow and hide the connector line below */
+.bridge-collapsed .bridge-toggle-arrow { transform: rotate(-90deg); }
+.bridge-collapsed .iface-tree-col.tree-parent::after { display: none; }
+
+.data-detail { color: var(--fg-muted); font-size: 0.85em; }
+
.addr-origin { color: var(--fg-muted); font-size: 0.75rem; margin-left: 0.5rem; }
.wg-peer {
diff --git a/src/webui/static/js/app.js b/src/webui/static/js/app.js
index 87e3a3d6..e244897f 100644
--- a/src/webui/static/js/app.js
+++ b/src/webui/static/js/app.js
@@ -445,6 +445,21 @@
});
})();
+// Bridge/LAG member row collapse toggle
+(function() {
+ document.addEventListener('click', function(e) {
+ var btn = e.target.closest('.bridge-toggle');
+ if (!btn) return;
+ var group = btn.getAttribute('data-group');
+ var parentRow = btn.closest('tr');
+ var collapsed = parentRow.classList.toggle('bridge-collapsed');
+ btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
+ document.querySelectorAll('[data-bridge-member="' + group + '"]').forEach(function(row) {
+ row.style.display = collapsed ? 'none' : '';
+ });
+ });
+})();
+
// Sidebar toggle (mobile)
(function() {
var MOBILE_BP = 1024;
diff --git a/src/webui/templates/pages/interfaces.html b/src/webui/templates/pages/interfaces.html
index 6d611397..be717cd9 100644
--- a/src/webui/templates/pages/interfaces.html
+++ b/src/webui/templates/pages/interfaces.html
@@ -14,23 +14,25 @@
- | Name |
+ ⚑ |
+ |
+ Name |
Type |
Status |
MAC |
- Address |
- Detail |
+ Data |
{{range .Interfaces}}
-
- | {{if .Indent}}{{.Indent}}{{end}}{{.Name}} |
+
+ | {{if .Forwarding}}⇅{{end}} |
+ {{if .HasMembers}}{{end}} |
+ {{.Name}} |
{{.Type}} |
{{.Status}} |
{{.PhysAddr}} |
- {{range $i, $a := .Addresses}}{{if $i}} {{end}}{{$a.Address}}{{if $a.Origin}} ({{$a.Origin}}){{end}}{{end}} |
- {{.Detail}} |
+ {{range $i, $a := .Addresses}}{{if $i}} {{end}}{{$a.Address}}{{if $a.Origin}} ({{$a.Origin}}){{end}}{{end}}{{if and .Addresses .Detail}} {{end}}{{if .Detail}}{{.Detail}}{{end}} |
{{end}}
diff --git a/src/webui/templates/pages/routing.html b/src/webui/templates/pages/routing.html
index 9d9df7a5..5c570d80 100644
--- a/src/webui/templates/pages/routing.html
+++ b/src/webui/templates/pages/routing.html
@@ -3,111 +3,123 @@
{{end}}
{{define "content"}}
-
-
Routing
- {{if .Error}}
{{.Error}}
{{end}}
+{{if .Error}}
{{.Error}}
{{end}}
-
-
Routing Table
- {{if .Routes}}
-
-
-
- | Destination |
- Next Hop |
- Protocol |
- Preference |
- Active |
-
-
- {{range .Routes}}
-
- | {{.DestPrefix}} |
- {{if .NextHopAddr}}{{.NextHopAddr}}{{else}}{{.NextHopIface}}{{end}} |
-
- {{if eq .Protocol "ospfv2"}}ospfv2
- {{else if eq .Protocol "static"}}static
- {{else if eq .Protocol "kernel"}}kernel
- {{else if eq .Protocol "direct"}}direct
- {{else}}{{.Protocol}}{{end}}
- |
- {{.Preference}} |
- {{if .Active}}{{else}}{{end}} |
-
- {{end}}
-
-
-
- {{else}}
-
No routes found.
- {{end}}
-
-
- {{if .HasOSPF}}
-
-
OSPF Neighbors
- {{if .OSPFNeighbors}}
-
-
-
- | Router ID |
- State |
- Role |
- Interface |
- Uptime |
-
-
- {{range .OSPFNeighbors}}
-
- | {{.RouterID}} |
- {{.State}} |
- {{.Role}} |
- {{.Interface}} |
- {{.Uptime}} |
-
- {{end}}
-
-
-
- {{else}}
-
No OSPF neighbors.
- {{end}}
-
-
-
-
OSPF Interfaces
- {{if .OSPFIfaces}}
-
-
-
- | Name |
- State |
- Cost |
- DR |
- BDR |
-
-
- {{range .OSPFIfaces}}
-
- | {{.Name}} |
- {{.State}} |
- {{.Cost}} |
- {{.DR}} |
- {{.BDR}} |
-
- {{end}}
-
-
-
- {{else}}
-
No OSPF interfaces.
- {{end}}
+
+
+ {{if .Routes}}
+
+
+
+ | Destination |
+ Next Hop |
+ Protocol |
+ Preference |
+ Uptime |
+ Active |
+
+
+ {{range .Routes}}
+
+ | {{.DestPrefix}} |
+ {{if .NextHopAddr}}{{.NextHopAddr}}{{else}}{{.NextHopIface}}{{end}} |
+
+ {{if eq .Protocol "ospfv2"}}ospfv2
+ {{else if eq .Protocol "ospfv3"}}ospfv3
+ {{else if eq .Protocol "static"}}static
+ {{else if eq .Protocol "kernel"}}kernel
+ {{else if eq .Protocol "direct"}}direct
+ {{else}}{{.Protocol}}{{end}}
+ |
+ {{.Preference}} |
+ {{.Uptime}} |
+ {{if .Active}}{{else}}{{end}} |
+
+ {{end}}
+
+
{{else}}
-
-
OSPF
-
OSPF is not configured on this device.
-
+
No routes found.
{{end}}
+
+{{if .HasOSPF}}
+
+
+ {{if .OSPFNeighbors}}
+
+
+
+ | Router ID |
+ Pri |
+ State |
+ Uptime |
+ Address |
+ Interface |
+ Area |
+
+
+ {{range .OSPFNeighbors}}
+
+ | {{.RouterID}} |
+ {{.Priority}} |
+ {{.State}} |
+ {{.Uptime}} |
+ {{.Address}} |
+ {{.Interface}} |
+ {{.Area}} |
+
+ {{end}}
+
+
+
+ {{else}}
+
No OSPF neighbors.
+ {{end}}
+
+
+
+
+ {{if .OSPFIfaces}}
+
+
+
+ | Interface |
+ Area |
+ Type |
+ State |
+ Cost |
+ Pri |
+ DR |
+ BDR |
+ Nbrs |
+
+
+ {{range .OSPFIfaces}}
+
+ | {{.Name}} |
+ {{.Area}} |
+ {{if .Type}}{{.Type}}{{else}}—{{end}} |
+ {{.State}} |
+ {{.Cost}} |
+ {{.Priority}} |
+ {{if .DR}}{{.DR}}{{else}}—{{end}} |
+ {{if .BDR}}{{.BDR}}{{else}}—{{end}} |
+ {{.NbrCount}} |
+
+ {{end}}
+
+
+
+ {{else}}
+
No OSPF interfaces.
+ {{end}}
+
+{{else}}
+
+
+
OSPF is not configured on this device.
+
+{{end}}
{{end}}