Every template and handler message in internal/web goes through a JSON
catalogue under internal/web/locales/<code>/, with English as the fallback.
The language is the person's choice (a cookie set by /_lang/{code}), else
Accept-Language, else landing.language. Tests refuse a key some language
lacks, a translation with other arguments than English, and template text
left in English.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
73 lines
2.1 KiB
Go
73 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: langOf(r).T("logs.cannot_read")}
|
|
} else {
|
|
// An error the control plane gives back is shown as it came, in
|
|
// English.
|
|
d.LogsView = ls.NodeLogs(r.Context(), node, r.URL.Query().Get("source"), d.Count)
|
|
}
|
|
noStore(w)
|
|
render(w, r, "node_logs", d)
|
|
}
|
|
}
|