From a975f55284c7c10a3df759df110d7112ac4beea9 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 14 Jun 2026 19:49:46 +0200 Subject: [PATCH] webui: show device hostname in topbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/webui/internal/handlers/system.go | 32 +++++++- src/webui/internal/server/server.go | 14 +++- src/webui/static/css/style.css | 77 +++++++++++++++++++ .../templates/fragments/topbar-identity.html | 17 ++++ src/webui/templates/layouts/base.html | 3 + 5 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 src/webui/templates/fragments/topbar-identity.html diff --git a/src/webui/internal/handlers/system.go b/src/webui/internal/handlers/system.go index 3a63d8ff..f7a20dec 100644 --- a/src/webui/internal/handlers/system.go +++ b/src/webui/internal/handlers/system.go @@ -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) diff --git a/src/webui/internal/server/server.go b/src/webui/internal/server/server.go index 0c9e60a8..c565c13c 100644 --- a/src/webui/internal/server/server.go +++ b/src/webui/internal/server/server.go @@ -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) diff --git a/src/webui/static/css/style.css b/src/webui/static/css/style.css index db935cc5..6fdbe6f5 100644 --- a/src/webui/static/css/style.css +++ b/src/webui/static/css/style.css @@ -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; diff --git a/src/webui/templates/fragments/topbar-identity.html b/src/webui/templates/fragments/topbar-identity.html new file mode 100644 index 00000000..0a6bcf94 --- /dev/null +++ b/src/webui/templates/fragments/topbar-identity.html @@ -0,0 +1,17 @@ +{{define "topbar-identity"}} +{{if .Hostname}} +
+ + {{.Hostname}}{{if or .Location .Contact}}{{end}} + + {{if or .Location .Contact}} + + {{end}} +
+{{end}} +{{end}} diff --git a/src/webui/templates/layouts/base.html b/src/webui/templates/layouts/base.html index 4eb345c9..31ca736f 100644 --- a/src/webui/templates/layouts/base.html +++ b/src/webui/templates/layouts/base.html @@ -23,6 +23,9 @@ {{template "icon-menu"}} + {{/* Device identity (hostname + location/contact popover), loaded async so + it stays out of every page's data path. Replaces itself on load. */}} +
{{if .Capabilities.Has "docs"}} {{/* On-device User's Guide (static mkdocs site at /guide/). */}}