webui: bundle on-device User's Guide

Build the mkdocs User's Guide into WebUI images and surface it in the UI.
post-build.sh runs `mkdocs build` from the top-level mkdocs.yml into
/var/www/guide when the webui package is selected and mkdocs is on the
build host; nginx serves it read-only at /guide/.  The build is
best-effort — a missing mkdocs warns and ships without the guide rather
than failing — and minimal images (no webui) skip it.

The WebUI gates a book icon in the topbar and a User Guide item in the
user menu on the docs being present on disk (os.Stat, fail-closed), both
opening /guide/ in a new tab.

CI: a local setup-mkdocs composite action installs mkdocs and the
plugins from mkdocs.yml; the build and release workflows run it before
the build so images produced in CI include the guide.  developers-guide
documents the new build dependency and restores the missing
mkdocs-glightbox plugin.

Fixes #633

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-06-15 19:45:25 +02:00
parent 3ccbdf2f0b
commit e412c28c37
10 changed files with 104 additions and 5 deletions
+3
View File
@@ -80,6 +80,9 @@ func (h *LoginHandler) DoLogin(w http.ResponseWriter, r *http.Request) {
console, netbrowse := handlers.DetectWebShortcuts(ctx, h.RC)
caps.Features()["console"] = console
caps.Features()["netbrowse"] = netbrowse
// The User's Guide is bundled at build time (a filesystem check, not
// config); gate the Help entry on its presence.
caps.Features()["docs"] = handlers.DetectDocs()
// Trigger any post-login hooks (e.g. schema sync) with full credentials.
if h.OnLogin != nil {
@@ -5,10 +5,22 @@ package handlers
import (
"context"
"log"
"os"
"infix/webui/internal/restconf"
)
// docsIndexPath is the entry point of the on-device User's Guide bundled
// by post-build.sh (mkdocs → /var/www/guide), served by nginx at /guide/.
const docsIndexPath = "/var/www/guide/index.html"
// DetectDocs reports whether the User's Guide was bundled into this image
// (the build host had mkdocs), so the UI can show or hide the entry.
func DetectDocs() bool {
_, err := os.Stat(docsIndexPath)
return err == nil
}
type feature struct {
Name string // key used in Has() and session cookie
Module string // YANG module that carries the feature
+14
View File
@@ -24,6 +24,13 @@
</button>
<img src="/assets/img/logo.png" alt="Infix" class="topbar-logo">
<div class="topbar-right">
{{if .Capabilities.Has "docs"}}
{{/* On-device User's Guide (static mkdocs site at /guide/). */}}
<a class="topbar-link" href="/guide/" target="_blank" rel="noopener"
title="Open the User Guide in a new tab" aria-label="User Guide">
{{template "icon-book"}}
</a>
{{end}}
{{if .Capabilities.Has "netbrowse"}}
{{/* mDNS network browser — fixed network.local vhost, so a static
href (no JS, no CSP concern). */}}
@@ -85,6 +92,13 @@
Off
{{template "icon-check"}}
</button>
{{if .Capabilities.Has "docs"}}
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/guide/" target="_blank" rel="noopener">
{{template "icon-book"}}
User Guide
</a>
{{end}}
<div class="dropdown-divider"></div>
<form method="POST" action="/logout">
<input type="hidden" name="csrf" value="{{.CsrfToken}}">
+1
View File
@@ -12,6 +12,7 @@
{{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-radar"}}<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"><path d="M19.07 4.93A10 10 0 0 0 6.99 3.34" /><path d="M4 6h.01" /><path d="M2.29 9.62A10 10 0 1 0 21.31 8.35" /><path d="M16.24 7.76A6 6 0 1 0 8.23 16.67" /><path d="M12 18h.01" /><path d="M17.99 11.66A6 6 0 0 1 15.77 16.67" /><circle cx="12" cy="12" r="2" /><path d="m13.41 10.59 5.66-5.66" /></svg>{{end}}
{{define "icon-book"}}<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"><path d="M12 7v14" /><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" /></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}}