From 8b3ba1f5c5e7f779f5dc558afcb7f563b61fb838 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 13 Jun 2026 17:59:07 +0200 Subject: [PATCH] webui: add web console shortcut to topbar and nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface the ttyd web console (port 7681) from the WebUI: a terminal icon in the topbar and a Console entry under Maintenance, both opening the console in a new tab. The link target is built client-side from the current hostname since the console runs on its own HTTPS port. The console is a config-gated capability (infix-services web/console enabled), so both entries are detected at login and rendered only when it is enabled — a disabled console shows no entry rather than a dead link. Detection fails closed: a config read error hides the entry. Signed-off-by: Joachim Wiberg --- src/webui/internal/auth/login.go | 3 +++ src/webui/internal/handlers/capabilities.go | 22 +++++++++++++++++++++ src/webui/static/css/style.css | 16 +++++++++++++++ src/webui/static/img/icons/README.md | 1 + src/webui/static/img/icons/console.svg | 1 + src/webui/static/js/app.js | 14 +++++++++++++ src/webui/templates/layouts/base.html | 8 ++++++++ src/webui/templates/layouts/icons.html | 1 + src/webui/templates/layouts/sidebar.html | 10 ++++++++++ 9 files changed, 76 insertions(+) create mode 100644 src/webui/static/img/icons/console.svg diff --git a/src/webui/internal/auth/login.go b/src/webui/internal/auth/login.go index 1eda5576..7257bedb 100644 --- a/src/webui/internal/auth/login.go +++ b/src/webui/internal/auth/login.go @@ -74,6 +74,9 @@ 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) // Trigger any post-login hooks (e.g. schema sync) with full credentials. if h.OnLogin != nil { diff --git a/src/webui/internal/handlers/capabilities.go b/src/webui/internal/handlers/capabilities.go index be64edf9..87485aac 100644 --- a/src/webui/internal/handlers/capabilities.go +++ b/src/webui/internal/handlers/capabilities.go @@ -70,6 +70,28 @@ 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 { + Web struct { + Console struct { + Enabled bool `json:"enabled"` + } `json:"console"` + } `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 + if err := rc.Get(ctx, "/data/infix-services:web", &cfg); err != nil { + log.Printf("web/console config: %v (console entry hidden)", err) + return false + } + return cfg.Web.Console.Enabled +} + func DetectCapabilities(ctx context.Context, rc restconf.Fetcher) *Capabilities { var lib yangLibrary if err := rc.Get(ctx, "/data/ietf-yang-library:yang-library", &lib); err != nil { diff --git a/src/webui/static/css/style.css b/src/webui/static/css/style.css index a5a2b661..42a63109 100644 --- a/src/webui/static/css/style.css +++ b/src/webui/static/css/style.css @@ -2133,6 +2133,22 @@ details[open] > .cfg-multi-summary { border-color: var(--fg-muted); } +/* Web console (terminal) shortcut in the topbar */ +.topbar-console { + display: flex; + align-items: center; + justify-content: center; + padding: 0.35rem; + border-radius: var(--radius); + color: var(--fg-muted); + text-decoration: none; + transition: color 0.15s, background 0.15s; +} +.topbar-console:hover { + color: var(--fg); + background: var(--border); +} + /* User menu button */ .user-menu { position: relative; diff --git a/src/webui/static/img/icons/README.md b/src/webui/static/img/icons/README.md index 4b54b184..7f137fd7 100644 --- a/src/webui/static/img/icons/README.md +++ b/src/webui/static/img/icons/README.md @@ -14,6 +14,7 @@ untouched. |-------------------|-----------------------| | advanced.svg | folder-git-2 | | backup.svg | archive-restore | +| console.svg | square-terminal | | containers.svg | container | | dashboard.svg | circle-gauge | | dhcp.svg | network | diff --git a/src/webui/static/img/icons/console.svg b/src/webui/static/img/icons/console.svg new file mode 100644 index 00000000..8454b87c --- /dev/null +++ b/src/webui/static/img/icons/console.svg @@ -0,0 +1 @@ + diff --git a/src/webui/static/js/app.js b/src/webui/static/js/app.js index 21c1652d..288a1f75 100644 --- a/src/webui/static/js/app.js +++ b/src/webui/static/js/app.js @@ -2633,3 +2633,17 @@ function renderCfgLog() { } }); })(); + +// ─── Web console (ttyd) link ──────────────────────────────────────────────── +// The console runs on its own HTTPS port (7681), so the link target can't be +// a relative path — build it from the current hostname at load time. CSP +// (script-src 'self') forbids an inline onclick, hence this lives here. The +// topbar/sidebar only render on full page loads, so DOMContentLoaded covers it. +(function () { + document.addEventListener('DOMContentLoaded', function () { + var links = document.querySelectorAll('[data-console-link]'); + if (!links.length) return; + var url = 'https://' + window.location.hostname + ':7681/'; + links.forEach(function (a) { a.href = url; }); + }); +})(); diff --git a/src/webui/templates/layouts/base.html b/src/webui/templates/layouts/base.html index 5cb7a47e..8e53cb38 100644 --- a/src/webui/templates/layouts/base.html +++ b/src/webui/templates/layouts/base.html @@ -24,6 +24,14 @@
+ {{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. */}} + + {{template "icon-terminal"}} + + {{end}}