webui: show device hostname in topbar

Surface the configured hostname right of the logo, pinned to the
sidebar's right edge so it reads as the start of the content area. When
location and/or contact are set they appear in a hover/focus popover, so
the bar stays uncluttered; the name links to Configure > System.

Loaded asynchronously (hx-trigger="load") to keep it out of every page's
data path — the topbar persists across htmx swaps, so it fetches once per
full page load and renders nothing on error or when no hostname is set.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-06-15 19:45:29 +02:00
parent 89392c56be
commit a975f55284
5 changed files with 135 additions and 8 deletions
+28 -4
View File
@@ -37,10 +37,11 @@ func raucInstallationStatus(ctx context.Context) (swInstallerState, error) {
// SystemHandler provides reboot, config download, and software install actions.
type SystemHandler struct {
RC *restconf.Client
Template *template.Template // software page template
SysCtrlTmpl *template.Template // system control page template
BackupTmpl *template.Template // backup & restore page template
RC *restconf.Client
Template *template.Template // software page template
SysCtrlTmpl *template.Template // system control page template
BackupTmpl *template.Template // backup & restore page template
IdentityTmpl *template.Template // topbar device-identity fragment
// swSlots caches the last successfully-fetched Software card payload
// so /software?installing=1 can keep rendering slot details — the
@@ -316,6 +317,29 @@ func migrateConfig(ctx context.Context, raw []byte) ([]byte, error) {
return os.ReadFile(tmp)
}
// Identity renders the topbar device-identity widget — hostname, plus an
// optional location/contact hover popover. It is loaded asynchronously via
// hx-trigger="load" so it stays out of the per-page data path; the topbar
// persists across htmx content swaps, so this fetches once per full page load.
// On any fetch error it renders nothing, leaving the topbar slot empty.
func (h *SystemHandler) Identity(w http.ResponseWriter, r *http.Request) {
var resp struct {
System struct {
Hostname string `json:"hostname"`
Location string `json:"location"`
Contact string `json:"contact"`
} `json:"ietf-system:system"`
}
if err := h.RC.Get(r.Context(), "/data/ietf-system:system", &resp); err != nil {
log.Printf("identity: %v", err)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := h.IdentityTmpl.ExecuteTemplate(w, "topbar-identity", resp.System); err != nil {
log.Printf("identity: render: %v", err)
}
}
func (h *SystemHandler) RestoreConfig(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(10 << 20); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
+10 -4
View File
@@ -65,6 +65,10 @@ func New(
if err != nil {
return nil, err
}
identityTmpl, err := template.ParseFS(templateFS, "fragments/topbar-identity.html")
if err != nil {
return nil, err
}
logsTmpl, err := template.ParseFS(templateFS, "layouts/*.html", "pages/logs.html")
if err != nil {
return nil, err
@@ -238,10 +242,11 @@ func New(
}
sys := &handlers.SystemHandler{
RC: rc,
Template: swTmpl,
SysCtrlTmpl: sysCtrlTmpl,
BackupTmpl: backupTmpl,
RC: rc,
Template: swTmpl,
SysCtrlTmpl: sysCtrlTmpl,
BackupTmpl: backupTmpl,
IdentityTmpl: identityTmpl,
}
logs := &handlers.LogsHandler{Template: logsTmpl}
diag := &handlers.DiagnosticsHandler{RC: rc, Template: diagTmpl}
@@ -338,6 +343,7 @@ func New(
mux.HandleFunc("GET /nacm", nacm.Overview)
mux.HandleFunc("GET /services", services.Overview)
mux.HandleFunc("GET /containers", containers.Overview)
mux.HandleFunc("GET /identity", sys.Identity)
// Configure routes.
mux.HandleFunc("POST /configure/enter", cfg.Enter)
+77
View File
@@ -237,6 +237,83 @@ a:hover {
display: block;
}
/* Device identity: hostname shown right of the logo, reading like a titlebar.
Location/contact (when set) appear in a hover/focus popover so the bar stays
uncluttered. */
.topbar-identity {
position: absolute;
left: var(--sidebar-width);
top: 0;
bottom: 0;
display: flex;
align-items: center;
min-width: 0;
padding-left: 0.9rem;
}
/* Short divider pinned to the sidebar's right edge, so the hostname reads as
the start of the content area rather than part of the logo lockup. */
.topbar-identity::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 1px;
height: 1.4rem;
background: var(--sidebar-border);
}
.topbar-identity-name {
display: inline-flex;
align-items: center;
gap: 0.25rem;
max-width: 22rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.875rem;
font-weight: 500;
color: var(--fg-muted);
text-decoration: none;
padding: 0.2rem 0.45rem;
border-radius: var(--radius-sm);
transition: color 0.15s, background 0.15s;
}
.topbar-identity-name:hover { background: var(--slate-100); color: var(--fg); }
.dark .topbar-identity-name:hover { background: var(--slate-800); color: var(--fg); }
.topbar-identity-caret { font-size: 0.6rem; opacity: 0.6; }
.topbar-identity-pop {
display: none;
position: absolute;
top: 100%;
left: 0;
margin-top: 0.3rem;
padding: 0.5rem 0.7rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
white-space: nowrap;
z-index: 250;
font-size: 0.8rem;
}
.topbar-identity:hover .topbar-identity-pop,
.topbar-identity:focus-within .topbar-identity-pop { display: block; }
.topbar-identity-pop .ti-row { display: flex; gap: 0.6rem; }
.topbar-identity-pop .ti-row + .ti-row { margin-top: 0.25rem; }
.topbar-identity-pop .ti-label {
color: var(--fg-muted);
font-weight: 600;
min-width: 4.5rem;
}
/* Narrow screens: the hamburger takes over — drop identity so it never
competes with the menu button. */
@media (max-width: 1024px) {
.topbar-identity,
.topbar-identity-slot { display: none; }
}
.sidebar-nav {
flex: 1;
padding: 0.5rem 0;
@@ -0,0 +1,17 @@
{{define "topbar-identity"}}
{{if .Hostname}}
<div class="topbar-identity">
<a class="topbar-identity-name" href="/configure/system"
hx-get="/configure/system" hx-target="#content" hx-push-url="true"
title="System identity">
{{.Hostname}}{{if or .Location .Contact}}<span class="topbar-identity-caret" aria-hidden="true"></span>{{end}}
</a>
{{if or .Location .Contact}}
<div class="topbar-identity-pop" role="tooltip">
{{if .Location}}<div class="ti-row"><span class="ti-label">Location</span><span>{{.Location}}</span></div>{{end}}
{{if .Contact}}<div class="ti-row"><span class="ti-label">Contact</span><span>{{.Contact}}</span></div>{{end}}
</div>
{{end}}
</div>
{{end}}
{{end}}
+3
View File
@@ -23,6 +23,9 @@
{{template "icon-menu"}}
</button>
<img src="/assets/img/logo.png" alt="Infix" class="topbar-logo">
{{/* Device identity (hostname + location/contact popover), loaded async so
it stays out of every page's data path. Replaces itself on load. */}}
<div class="topbar-identity-slot" hx-get="/identity" hx-trigger="load" hx-swap="outerHTML"></div>
<div class="topbar-right">
{{if .Capabilities.Has "docs"}}
{{/* On-device User's Guide (static mkdocs site at /guide/). */}}