webui: add network browser shortcut to topbar and nav

Surface the mDNS network browser (netbrowse at network.local) from the
WebUI: a radar icon in the topbar and a Network Browser entry under
Status > Network, both opening it in a new tab.

Like the console, this is a config-gated capability; fold the console and
netbrowse enable flags into a single infix-services web read at login
(DetectWebShortcuts) and render each entry only when enabled, fail-closed
on a read error.  The link is a static https://network.local/ since
netbrowse is a fixed name-based vhost.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-06-15 19:45:24 +02:00
parent 8b3ba1f5c5
commit 3ccbdf2f0b
8 changed files with 47 additions and 18 deletions
+6 -3
View File
@@ -74,9 +74,12 @@ func (h *LoginHandler) DoLogin(w http.ResponseWriter, r *http.Request) {
// Probe optional features once at login and bake into the session.
caps := handlers.DetectCapabilities(ctx, h.RC)
// The web console (ttyd) is config-gated; fold it into the same feature
// map so templates gate on .Capabilities.Has "console".
caps.Features()["console"] = handlers.DetectConsole(ctx, h.RC)
// The external web-app shortcuts (console/ttyd, netbrowse) are
// config-gated; fold them into the same feature map so templates gate
// on .Capabilities.Has "console" / "netbrowse".
console, netbrowse := handlers.DetectWebShortcuts(ctx, h.RC)
caps.Features()["console"] = console
caps.Features()["netbrowse"] = netbrowse
// Trigger any post-login hooks (e.g. schema sync) with full credentials.
if h.OnLogin != nil {
+16 -11
View File
@@ -70,26 +70,31 @@ type yangLibrary struct {
} `json:"ietf-yang-library:yang-library"`
}
// webConsoleConfig mirrors the slice of infix-services we need for the
// console gate.
type webConsoleConfig struct {
// webConfig mirrors the slice of infix-services we need to gate the
// external web-app shortcuts (console, netbrowse).
type webConfig struct {
Web struct {
Console struct {
Enabled bool `json:"enabled"`
} `json:"console"`
Netbrowse struct {
Enabled bool `json:"enabled"`
} `json:"netbrowse"`
} `json:"infix-services:web"`
}
// DetectConsole reports whether the web console (ttyd on :7681) is enabled
// in config, so the UI can show or hide the terminal entry. Fails closed:
// a read error hides the entry rather than offering a dead link.
func DetectConsole(ctx context.Context, rc restconf.Fetcher) bool {
var cfg webConsoleConfig
// DetectWebShortcuts reports whether the web console (ttyd on :7681) and
// the mDNS network browser (netbrowse at network.local) are enabled in
// config, so the UI can show or hide their shortcuts. One read covers
// both. Fails closed: a read error hides both rather than offering a
// dead link.
func DetectWebShortcuts(ctx context.Context, rc restconf.Fetcher) (console, netbrowse bool) {
var cfg webConfig
if err := rc.Get(ctx, "/data/infix-services:web", &cfg); err != nil {
log.Printf("web/console config: %v (console entry hidden)", err)
return false
log.Printf("web config: %v (console/netbrowse shortcuts hidden)", err)
return false, false
}
return cfg.Web.Console.Enabled
return cfg.Web.Console.Enabled, cfg.Web.Netbrowse.Enabled
}
func DetectCapabilities(ctx context.Context, rc restconf.Fetcher) *Capabilities {
+3 -3
View File
@@ -2133,8 +2133,8 @@ details[open] > .cfg-multi-summary {
border-color: var(--fg-muted);
}
/* Web console (terminal) shortcut in the topbar */
.topbar-console {
/* External web-app shortcuts (console, network browser) in the topbar */
.topbar-link {
display: flex;
align-items: center;
justify-content: center;
@@ -2144,7 +2144,7 @@ details[open] > .cfg-multi-summary {
text-decoration: none;
transition: color 0.15s, background 0.15s;
}
.topbar-console:hover {
.topbar-link:hover {
color: var(--fg);
background: var(--border);
}
+1
View File
@@ -29,6 +29,7 @@ untouched.
| logs.svg | scroll-text |
| mdns.svg | radio |
| nacm.svg | users |
| netbrowse.svg | radar |
| ntp.svg | clock |
| routes.svg | git-compare-arrows |
| routing.svg | git-compare-arrows |
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19.07 4.93A10 10 0 0 0 6.99 3.34"/><path d="M4 6h.01"/><path d="M2.29 9.62A10 10 0 1 0 21.31 8.35"/><path d="M16.24 7.76A6 6 0 1 0 8.23 16.67"/><path d="M12 18h.01"/><path d="M17.99 11.66A6 6 0 0 1 15.77 16.67"/><circle cx="12" cy="12" r="2"/><path d="m13.41 10.59 5.66-5.66"/></svg>

After

Width:  |  Height:  |  Size: 475 B

+9 -1
View File
@@ -24,10 +24,18 @@
</button>
<img src="/assets/img/logo.png" alt="Infix" class="topbar-logo">
<div class="topbar-right">
{{if .Capabilities.Has "netbrowse"}}
{{/* mDNS network browser — fixed network.local vhost, so a static
href (no JS, no CSP concern). */}}
<a class="topbar-link" href="https://network.local/" target="_blank" rel="noopener"
title="Browse mDNS neighbors (network.local) in a new tab" aria-label="Network browser">
{{template "icon-radar"}}
</a>
{{end}}
{{if .Capabilities.Has "console"}}
{{/* Web console (ttyd) on :7681. href is set by JS from the current
hostname since the port differs and CSP forbids inline script. */}}
<a class="topbar-console" data-console-link href="#" target="_blank" rel="noopener"
<a class="topbar-link" data-console-link href="#" target="_blank" rel="noopener"
title="Open web console (terminal) in a new tab" aria-label="Web console">
{{template "icon-terminal"}}
</a>
+1
View File
@@ -11,6 +11,7 @@
{{define "icon-reset"}}<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 14 4 9l5-5" /><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" /></svg>{{end}}
{{define "icon-menu"}}<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M4 5h16" /><path d="M4 12h16" /><path d="M4 19h16" /></svg>{{end}}
{{define "icon-terminal"}}<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true"><polyline points="4 17 10 11 4 5" /><line x1="12" x2="20" y1="19" y2="19" /></svg>{{end}}
{{define "icon-radar"}}<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true"><path d="M19.07 4.93A10 10 0 0 0 6.99 3.34" /><path d="M4 6h.01" /><path d="M2.29 9.62A10 10 0 1 0 21.31 8.35" /><path d="M16.24 7.76A6 6 0 1 0 8.23 16.67" /><path d="M12 18h.01" /><path d="M17.99 11.66A6 6 0 0 1 15.77 16.67" /><circle cx="12" cy="12" r="2" /><path d="m13.41 10.59 5.66-5.66" /></svg>{{end}}
{{define "icon-user"}}<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>{{end}}
{{define "icon-user-lg"}}<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>{{end}}
{{define "icon-chevron-down"}}<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" class="chevron" aria-hidden="true" stroke-width="2.5"><path d="m6 9 6 6 6-6" /></svg>{{end}}
+10
View File
@@ -103,6 +103,16 @@
mDNS
</a>
</li>
{{if .Capabilities.Has "netbrowse"}}
{{/* External link to the mDNS network browser at network.local
(new tab); not an htmx nav. */}}
<li>
<a href="https://network.local/" target="_blank" rel="noopener" class="nav-link">
<span class="nav-icon" style="--icon: url('/assets/img/icons/netbrowse.svg')"></span>
Network Browser
</a>
</li>
{{end}}
<li>
<a href="/ntp"
hx-get="/ntp"