Each page wrote its own nav: the same links in a different order, some missing, the current page sometimes listed and sometimes not. A shared "nav" template now draws it everywhere — Cluster, People, Quotas, Usage, Mirror for an administrator, then Chat, Your keys and Sign out — with the current page marked by aria-current. Pages rendered outside the account pages (the dashboard, node logs) get it through the request context their admin wrapper sets. The admin listener's own page keeps its metrics/pprof/api links. The chat and key pages said "project proj-3kq7…"; they now show the project's path, and the key page no longer repeats the email. A test renders several admin pages and requires the same menu on each. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
71 lines
2.1 KiB
Go
71 lines
2.1 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
|
|
)
|
|
|
|
// LogSource reads a node's logs. Asked for by type assertion on the cluster
|
|
// source, so a source that cannot — a test's, an older one — leaves the page
|
|
// saying so rather than every Source having to grow the method.
|
|
type LogSource interface {
|
|
NodeLogs(ctx context.Context, node, source string, lines int) adminapi.LogsView
|
|
}
|
|
|
|
type nodeLogsData struct {
|
|
Nav Nav
|
|
adminapi.LogsView
|
|
OnAdminListener bool
|
|
// Base is where this page lives, which differs between the admin
|
|
// listener and the client-facing port.
|
|
Base string
|
|
Back string
|
|
Count int
|
|
Counts []int
|
|
Follow bool
|
|
Version string
|
|
Now string
|
|
}
|
|
|
|
// NodeLogsPage shows the recent lines of one of a node's logs: the agent's
|
|
// own, or an engine's output.
|
|
//
|
|
// What a node did used to be learned by ssh through a jump host and a journal
|
|
// only root may read — which, when an engine install fails at two in the
|
|
// morning, is several steps between the page that says it failed and the line
|
|
// that says why.
|
|
func NodeLogsPage(src Source, onAdminListener bool, version string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
node := r.PathValue("id")
|
|
d := nodeLogsData{
|
|
Nav: navFrom(r, "cluster"),
|
|
OnAdminListener: onAdminListener,
|
|
Base: "/admin/nodes/" + node + "/logs",
|
|
Back: "/admin",
|
|
Counts: []int{100, 300, 1000, 2000},
|
|
Follow: r.URL.Query().Get("follow") != "",
|
|
Version: version,
|
|
Now: time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
if onAdminListener {
|
|
d.Base, d.Back = "/nodes/"+node+"/logs", "/"
|
|
}
|
|
d.Count, _ = strconv.Atoi(r.URL.Query().Get("lines"))
|
|
if d.Count <= 0 {
|
|
d.Count = 300
|
|
}
|
|
ls, ok := src.(LogSource)
|
|
if !ok {
|
|
d.LogsView = adminapi.LogsView{Node: node, Error: "this control plane cannot read node logs"}
|
|
} else {
|
|
d.LogsView = ls.NodeLogs(r.Context(), node, r.URL.Query().Get("source"), d.Count)
|
|
}
|
|
noStore(w)
|
|
render(w, "node_logs", d)
|
|
}
|
|
}
|