Files
Ilker ManapandClaude Opus 5.5 11231d8c2b The portal in English and Turkish: a catalogue per language, a switcher at the foot of every page, landing.language, Turkish numbers and dates
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>
2026-10-05 00:12:53 +02:00

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