mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}}
|
||||
@@ -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/). */}}
|
||||
|
||||
Reference in New Issue
Block a user