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 @@