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) } }