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>
128 lines
3.9 KiB
Go
128 lines
3.9 KiB
Go
package web
|
|
|
|
import (
|
|
"encoding/json"
|
|
"html/template"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
|
|
)
|
|
|
|
// TopologySource is a Source that can map its fabrics.
|
|
type TopologySource interface {
|
|
Topologies() []adminapi.TopologyView
|
|
Topology(id string) (adminapi.TopologyView, bool)
|
|
}
|
|
|
|
type fabricPageData struct {
|
|
Nav Nav
|
|
OnAdminListener bool
|
|
Base string
|
|
List []adminapi.TopologyView
|
|
Current *adminapi.TopologyView
|
|
// Data is the current fabric as JSON, for the drawing to start from
|
|
// without a round trip. json.Marshal escapes <, > and &, so a switch
|
|
// description cannot close the script it sits in.
|
|
Data template.JS
|
|
// Strings are what the drawing says, in the page's language, as JSON
|
|
// beside the data: the script is a static file and has no catalogue.
|
|
Strings template.JS
|
|
Version string
|
|
Now string
|
|
}
|
|
|
|
// fabricJSStrings are the drawing's sentences and how many arguments each
|
|
// takes. An argument is handed to the catalogue as "{0}", "{1}", which the
|
|
// script fills in, so a translation moves them with %[2]s as anywhere else.
|
|
var fabricJSStrings = []struct {
|
|
key string
|
|
args int
|
|
}{
|
|
{"map_failed", 1}, {"not_mapped", 0},
|
|
{"tier.leaf", 0}, {"tier.spine", 0}, {"tier.core", 0}, {"tier.supercore", 0}, {"tier.n", 1},
|
|
{"adapters_row", 0}, {"ports_used", 2}, {"hosts", 1}, {"cables_n", 1}, {"below_rate", 1},
|
|
{"adapters_n", 1}, {"degraded_n", 1}, {"rail", 1}, {"not_ours", 0},
|
|
{"dt.tier", 0}, {"dt.ports", 0}, {"dt.adapters", 0}, {"dt.adapter", 0}, {"dt.rail", 0},
|
|
{"dt.leaf", 0}, {"dt.state", 0}, {"dt.rate", 0}, {"dt.sending", 0}, {"dt.receiving", 0},
|
|
{"cabled", 2}, {"degraded_cables", 0}, {"all_good", 0}, {"port_to", 3}, {"close", 0},
|
|
{"open_node", 1}, {"adapter", 0},
|
|
{"stat.switches", 0}, {"stat.adapters", 0}, {"stat.cables", 0}, {"stat.degraded", 0},
|
|
{"stat.rails", 0}, {"stat.aligned", 0}, {"stat.crossed", 0}, {"stat.sent", 0},
|
|
}
|
|
|
|
func init() {
|
|
for _, s := range fabricJSStrings {
|
|
registerKeys("fabric.js." + s.key)
|
|
}
|
|
}
|
|
|
|
// fabricStrings is the drawing's sentences in l, with the decimal sign its
|
|
// figures are written with.
|
|
func fabricStrings(l *Lang) template.JS {
|
|
m := map[string]string{"decimal": l.decimal}
|
|
for _, s := range fabricJSStrings {
|
|
args := make([]any, s.args)
|
|
for i := range args {
|
|
args[i] = "{" + strconv.Itoa(i) + "}"
|
|
}
|
|
m[s.key] = l.T("fabric.js."+s.key, args...)
|
|
}
|
|
b, _ := json.Marshal(m)
|
|
return template.JS(b) //nolint:gosec // json.Marshal escapes <, > and &
|
|
}
|
|
|
|
// FabricsPage shows every physical fabric and, for one, its map.
|
|
func FabricsPage(src Source, onAdminListener bool, version string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ts, ok := src.(TopologySource)
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
d := fabricPageData{Nav: navFrom(r, "cluster"), OnAdminListener: onAdminListener, Base: "/admin/fabrics",
|
|
List: ts.Topologies(), Version: version, Now: time.Now().UTC().Format(time.RFC3339),
|
|
Strings: fabricStrings(langOf(r))}
|
|
if onAdminListener {
|
|
d.Base = "/fabrics"
|
|
}
|
|
id := r.PathValue("id")
|
|
if id == "" && len(d.List) > 0 {
|
|
id = d.List[0].ID
|
|
}
|
|
if id != "" {
|
|
t, ok := ts.Topology(id)
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
d.Current = &t
|
|
b, _ := json.Marshal(t)
|
|
d.Data = template.JS(b) //nolint:gosec // json.Marshal escapes <, > and &
|
|
}
|
|
noStore(w)
|
|
render(w, r, "fabrics", d)
|
|
}
|
|
}
|
|
|
|
// FabricTopologyJSON is one fabric's map and live state, for the drawing to
|
|
// refresh from.
|
|
func FabricTopologyJSON(src Source) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ts, ok := src.(TopologySource)
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
t, ok := ts.Topology(r.PathValue("id"))
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
noStore(w)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(t)
|
|
}
|
|
}
|