mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
webui: add web console shortcut to topbar and nav
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 <troglobit@gmail.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 11 2-2-2-2"/><path d="M11 13h4"/><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/></svg>
|
||||
|
After Width: | Height: | Size: 289 B |
@@ -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; });
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -24,6 +24,14 @@
|
||||
</button>
|
||||
<img src="/assets/img/logo.png" alt="Infix" class="topbar-logo">
|
||||
<div class="topbar-right">
|
||||
{{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. */}}
|
||||
<a class="topbar-console" data-console-link href="#" target="_blank" rel="noopener"
|
||||
title="Open web console (terminal) in a new tab" aria-label="Web console">
|
||||
{{template "icon-terminal"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<div class="user-menu" id="user-menu">
|
||||
<button class="user-menu-btn" id="user-menu-btn" aria-haspopup="true" aria-expanded="false">
|
||||
{{template "icon-user"}}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
{{define "icon-trash"}}<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6M14 11v6"></path></svg>{{end}}
|
||||
{{define "icon-reset"}}<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 14 4 9l5-5" /><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" /></svg>{{end}}
|
||||
{{define "icon-menu"}}<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M4 5h16" /><path d="M4 12h16" /><path d="M4 19h16" /></svg>{{end}}
|
||||
{{define "icon-terminal"}}<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true"><polyline points="4 17 10 11 4 5" /><line x1="12" x2="20" y1="19" y2="19" /></svg>{{end}}
|
||||
{{define "icon-user"}}<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>{{end}}
|
||||
{{define "icon-user-lg"}}<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>{{end}}
|
||||
{{define "icon-chevron-down"}}<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" class="chevron" aria-hidden="true" stroke-width="2.5"><path d="m6 9 6 6 6-6" /></svg>{{end}}
|
||||
|
||||
@@ -324,6 +324,16 @@
|
||||
Diagnostics
|
||||
</a>
|
||||
</li>
|
||||
{{if .Capabilities.Has "console"}}
|
||||
{{/* External link to ttyd on :7681 (new tab); not an htmx nav.
|
||||
href is filled in by JS from the current hostname. */}}
|
||||
<li>
|
||||
<a data-console-link href="#" target="_blank" rel="noopener" class="nav-link">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/console.svg')"></span>
|
||||
Console
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li>
|
||||
<a href="/maintenance/backup"
|
||||
hx-get="/maintenance/backup"
|
||||
|
||||
Reference in New Issue
Block a user