diff --git a/BACKLOG.md b/BACKLOG.md index 02f5388..0554f71 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -28,7 +28,8 @@ What is left, most needed first. The detail is in the item each names. 3. **K**: fixes from the fabric test the operator runs with docs/large-models.md. **Soon: the customer's experience, and HA.** -4. **C24**: English errors on Turkish pages. (S) +4. ~~**C24**~~: done 2026-10-21. The store's, catalog's and mirror's errors, percent signs, footer times and stale + forms are in the page's language. Left: the /docs documents, and a language kept per person (cookie only today). 5. **J4**: the model mirror on more than one machine, the last single point of an HA install. (M) 6. **F5**: large imports limited by the embedding engine's slots. (M) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd97245..f3cda0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,13 @@ APIs change between minors without a deprecation period. ### Fixed +- **Turkish pages are Turkish to the end.** + - **Errors:** those from the store, catalog and mirror that a page shows ("not found", "not empty", "already in the + catalog", the settings changed meanwhile, and others) are now in the page's language. + - **Percent:** a percentage is written the language's way: `%93` in Turkish, `93%` in English. + - **Footer:** the time at the foot of a page is in the language's date form. + - **Stale forms:** they are refused with a sentence in that language rather than "bad request". + - **`llmgw keys limits` no longer clears the limits it was not given.** Setting one limit at a scope wrote the whole row, so `--tools 'jira.*'` alone took away that scope's rate, budget and model list. Only what is given changes now. `--replace` writes every limit, as the console's limits form does. diff --git a/internal/web/account.go b/internal/web/account.go index 3f09caf..d9c2c28 100644 --- a/internal/web/account.go +++ b/internal/web/account.go @@ -344,7 +344,7 @@ func (p *AccountPages) logout(w http.ResponseWriter, r *http.Request) { // page can sign a visitor out of this one. sess, _, ok := p.session(r) if ok && !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } if c, err := r.Cookie(SessionCookie); err == nil { @@ -437,7 +437,7 @@ func (p *AccountPages) createKey(w http.ResponseWriter, r *http.Request) { return } if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } @@ -487,7 +487,7 @@ func (p *AccountPages) revokeKey(w http.ResponseWriter, r *http.Request) { return } if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } @@ -721,7 +721,7 @@ func (p *AccountPages) changePassword(w http.ResponseWriter, r *http.Request) { return } if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } diff --git a/internal/web/admin_mirror.go b/internal/web/admin_mirror.go index 95ce1be..c1c41aa 100644 --- a/internal/web/admin_mirror.go +++ b/internal/web/admin_mirror.go @@ -401,7 +401,7 @@ func (p *AccountPages) mirrorAction(m MirrorAdmin, return p.adminOnly(func(w http.ResponseWriter, r *http.Request, admin store.User) { sess, _, _ := p.session(r) if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } msg, err := do(r.Context(), admin, r) diff --git a/internal/web/admin_nodeengine.go b/internal/web/admin_nodeengine.go index 9d96926..8b6195a 100644 --- a/internal/web/admin_nodeengine.go +++ b/internal/web/admin_nodeengine.go @@ -21,15 +21,16 @@ func (p *AccountPages) RegisterNodeEngines(mux *http.ServeMux, a NodeEngineAdmin mux.HandleFunc("POST /admin/nodes/{id}/engines", p.adminOnly(func(w http.ResponseWriter, r *http.Request, admin store.User) { sess, _, _ := p.session(r) if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } node, engine, build := r.PathValue("id"), r.FormValue("engine"), r.FormValue("build") q := url.Values{} if err := a.SetNodeEngine(r.Context(), node, engine, build, admin.Email); err != nil { p.log.Warn("an engine build was not chosen", "admin", admin.Email, "node", node, "err", err) - // The control plane's reason, as it gave it: in English. - q.Set("problem", node+": "+err.Error()) + // The control plane's reason, in the page's language where it + // is one the pages know, else as it gave it. + q.Set("problem", node+": "+langOf(r).Err(err)) } else { p.log.Info("an engine build was chosen", "admin", admin.Email, "node", node, "engine", engine, "build", build) // Said in the language of the person who chose, since the diff --git a/internal/web/admin_presets.go b/internal/web/admin_presets.go index f2c75bd..edb43d3 100644 --- a/internal/web/admin_presets.go +++ b/internal/web/admin_presets.go @@ -108,7 +108,7 @@ func (p *AccountPages) RegisterPresets(mux *http.ServeMux, a PresetAdmin) { return p.adminOnly(func(w http.ResponseWriter, r *http.Request, admin store.User) { sess, _, _ := p.session(r) if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } h(w, r, admin) @@ -120,7 +120,7 @@ func (p *AccountPages) RegisterPresets(mux *http.ServeMux, a PresetAdmin) { q := url.Values{} if err := a.SetAutoPresets(r.Context(), on, admin.Email); err != nil { p.log.Warn("presets.auto was not changed", "admin", admin.Email, "err", err) - q.Set("problem", err.Error()) + q.Set("problem", langOf(r).Err(err)) } else { p.log.Info("presets.auto changed", "admin", admin.Email, "on", on) key := "dash.presets.notice.auto_off" @@ -136,8 +136,9 @@ func (p *AccountPages) RegisterPresets(mux *http.ServeMux, a PresetAdmin) { q := url.Values{} if err := a.AddPreset(r.Context(), name, admin.Email); err != nil { p.log.Warn("a preset was not added", "admin", admin.Email, "preset", name, "err", err) - // The control plane's reason, as it gave it: in English. - q.Set("problem", name+": "+err.Error()) + // The control plane's reason, in the page's language where it + // is one the pages know, else as it gave it. + q.Set("problem", name+": "+langOf(r).Err(err)) } else { p.log.Info("a preset was added", "admin", admin.Email, "preset", name) q.Set("notice", langOf(r).T("dash.presets.notice.added", name)) diff --git a/internal/web/admin_quotas.go b/internal/web/admin_quotas.go index 4e0b27c..79dcb2c 100644 --- a/internal/web/admin_quotas.go +++ b/internal/web/admin_quotas.go @@ -210,7 +210,7 @@ func (p *AccountPages) RegisterAdminQuotas(mux *http.ServeMux, s QuotaAdmin, pri func(w http.ResponseWriter, r *http.Request, admin store.User) { sess, _, _ := p.session(r) if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } msg, err := p.applyQuota(r, s, admin) @@ -228,7 +228,7 @@ func (p *AccountPages) RegisterAdminQuotas(mux *http.ServeMux, s QuotaAdmin, pri func(w http.ResponseWriter, r *http.Request, admin store.User) { sess, _, _ := p.session(r) if p.capacity == nil || !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } msg, err := p.applyCapacity(r, admin) diff --git a/internal/web/admin_settings.go b/internal/web/admin_settings.go index fd5e376..ce17ef3 100644 --- a/internal/web/admin_settings.go +++ b/internal/web/admin_settings.go @@ -89,7 +89,7 @@ func (p *AccountPages) RegisterAdminSettings(mux *http.ServeMux, s SettingsAdmin if r.Method == http.MethodPost { sess, _, _ := p.session(r) if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } } diff --git a/internal/web/admin_users.go b/internal/web/admin_users.go index d27eeef..d19b977 100644 --- a/internal/web/admin_users.go +++ b/internal/web/admin_users.go @@ -476,7 +476,7 @@ func (p *AccountPages) tenancyAction(s AccountAdmin, return p.adminOnly(func(w http.ResponseWriter, r *http.Request, admin store.User) { sess, _, _ := p.session(r) if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } l := langOf(r) @@ -524,7 +524,7 @@ func (p *AccountPages) adminAction(s AccountAdmin, return p.adminOnly(func(w http.ResponseWriter, r *http.Request, admin store.User) { sess, _, _ := p.session(r) if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } id := r.PathValue("id") diff --git a/internal/web/chat.go b/internal/web/chat.go index 9262e70..4e13f03 100644 --- a/internal/web/chat.go +++ b/internal/web/chat.go @@ -125,7 +125,7 @@ func (p *AccountPages) Guard( token = r.FormValue("csrf") } if !p.accounts.CheckCSRF(sess, token) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } } diff --git a/internal/web/connect.go b/internal/web/connect.go index 83851ed..f633394 100644 --- a/internal/web/connect.go +++ b/internal/web/connect.go @@ -164,7 +164,7 @@ func (p *AccountPages) connect(w http.ResponseWriter, r *http.Request) { return } if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } l := langOf(r) diff --git a/internal/web/errors_i18n.go b/internal/web/errors_i18n.go new file mode 100644 index 0000000..6a12ae2 --- /dev/null +++ b/internal/web/errors_i18n.go @@ -0,0 +1,44 @@ +package web + +import ( + "net/http" + + "gitea.mynodes.xyz/ilker/llmgw/internal/account" + "gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog" + "gitea.mynodes.xyz/ilker/llmgw/internal/mirror" + "gitea.mynodes.xyz/ilker/llmgw/internal/store" +) + +// The errors of other packages a page can show, in the page's language: an +// administrator's page in Turkish said "not found" and "not empty" in +// English, because they came from the store as written there. +func init() { + for err, key := range map[error]string{ + store.ErrNotFound: "err.not_found", + store.ErrNotEmpty: "err.not_empty", + store.ErrDuplicateName: "err.duplicate_name", + store.ErrEmailTaken: "err.email_taken", + store.ErrKeyTTL: "err.key_ttl", + store.ErrNotMember: "err.not_member", + store.ErrOwnerInactive: "err.owner_inactive", + store.ErrSettingsChanged: "err.settings_changed", + store.ErrNoSession: "err.no_session", + store.ErrKnowledgeOff: "err.knowledge_off", + store.ErrMemoryNotEnabled: "err.memory_off", + store.ErrMemoryFull: "err.memory_full", + store.ErrThreadsNotEnabled: "err.threads_off", + account.ErrNoReset: "err.no_reset", + catalog.ErrExists: "err.in_catalog", + mirror.ErrAlreadyFetching: "err.mirror_fetching", + mirror.ErrExists: "err.mirror_exists", + mirror.ErrIncomplete: "err.mirror_incomplete", + } { + registerErrorKey(err, key) + } +} + +// badForm answers a form whose token did not match: one left open too long, +// or sent from another site. In the page's language, plainly. +func badForm(w http.ResponseWriter, r *http.Request) { + http.Error(w, langOf(r).T("err.bad_form"), http.StatusBadRequest) +} diff --git a/internal/web/errors_i18n_test.go b/internal/web/errors_i18n_test.go new file mode 100644 index 0000000..b3cf5ad --- /dev/null +++ b/internal/web/errors_i18n_test.go @@ -0,0 +1,41 @@ +package web + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "gitea.mynodes.xyz/ilker/llmgw/internal/store" +) + +// A Turkish page says the store's errors, percentages and moments the +// Turkish way, and a stale form plainly. +func TestATurkishPageSpeaksTurkishToTheEnd(t *testing.T) { + tr, en := langByCode("tr"), langByCode("en") + wrapped := fmt.Errorf("org Acme: %w", store.ErrNotEmpty) + if got := tr.Err(wrapped); !strings.Contains(got, "boş değil") { + t.Errorf("a wrapped store error in Turkish: %q", got) + } + if got := en.Err(store.ErrNotFound); !strings.Contains(got, "not there") { + t.Errorf("in English: %q", got) + } + if got := tr.Pct(93, 0); got != "%93" { + t.Errorf("Turkish percent %q", got) + } + if got := en.Pct(93.5, 1); got != "93.5%" { + t.Errorf("English percent %q", got) + } + stamp := tr.funcs()["stamp"].(func(string) string) + if got := stamp("2026-10-21T08:05:00Z"); got != "21.10.2026 08:05 UTC" { + t.Errorf("a Turkish moment %q", got) + } + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/x", nil) + req.Header.Set("Accept-Language", "tr") + badForm(rec, req) + if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "form süresi") { + t.Errorf("a stale form in Turkish: %d %q", rec.Code, rec.Body.String()) + } +} diff --git a/internal/web/i18n.go b/internal/web/i18n.go index 4595214..2ce0e79 100644 --- a/internal/web/i18n.go +++ b/internal/web/i18n.go @@ -55,6 +55,9 @@ type Lang struct { decimal, group string // date and dateTime are Go layouts. date, dateTime string + // percent writes a percentage around its number: "%s%%" in English, + // "%%%s" in Turkish, which puts the sign first. + percent string tmpl *template.Template } @@ -149,6 +152,7 @@ func loadLang(fsys fs.FS, code string) (*Lang, error) { l.group = l.msgs["_meta.group"] l.date = cmpOr(l.msgs["_meta.date"], "2006-01-02") l.dateTime = cmpOr(l.msgs["_meta.datetime"], "2006-01-02 15:04") + l.percent = cmpOr(l.msgs["_meta.percent"], "%s%%") return l, nil } @@ -375,6 +379,9 @@ func registerKeys(keys ...string) { // Numbers. +// Pct writes a percentage the way this language does: 93% or %93. +func (l *Lang) Pct(v float64, places int) string { return fmt.Sprintf(l.percent, l.Num(v, places)) } + // Num writes v with places decimals, in this language's separators. func (l *Lang) Num(v float64, places int) string { if math.IsNaN(v) || math.IsInf(v, 0) { @@ -624,6 +631,17 @@ func (l *Lang) funcs() template.FuncMap { // a style attribute or an SVG coordinate, which are always written // the C way. "num": func(v any, places int) string { return l.Num(toFloat(v), places) }, + // stamp is a moment kept as RFC 3339, the way this language writes + // dates and times. + "stamp": func(s string) string { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + return s + } + return t.UTC().Format(l.dateTime) + " UTC" + }, + // pc is a percentage, its sign where the language puts it. + "pc": func(v any, places int) string { return l.Pct(toFloat(v), places) }, "count": func(v any) string { if n, ok := toInt(v); ok { return l.Int(n) diff --git a/internal/web/i18n_cluster_test.go b/internal/web/i18n_cluster_test.go index 4ec6f1e..5411cb9 100644 --- a/internal/web/i18n_cluster_test.go +++ b/internal/web/i18n_cluster_test.go @@ -37,7 +37,7 @@ func TestTheDashboardSpeaksTurkishWhenAsked(t *testing.T) { // A state is still its class, and a Turkish word. `hazır`, // The GPU map: its modes, and what a card says under the pointer. - "meşguliyet", "gpu07 GPU 2 · code (code-1) · 93% meşgul · 39,1 / 80 GB", + "meşguliyet", "gpu07 GPU 2 · code (code-1) · %93 meşgul · 39,1 / 80 GB", // How long a replica has sat idle, in Turkish units. "30dk0sn önce", // The traffic chart's peak, with Turkish thousands. @@ -67,7 +67,7 @@ func TestTheNodePageWritesTurkishNumbers(t *testing.T) { nodeMux(gpuSource{}, h).ServeHTTP(w, r) body := w.Body.String() for _, want := range []string{ - "64 çekirdek", "bellek 64%", "71,2 / 80 GB", "1.980 MHz", + "64 çekirdek", "bellek %64", "71,2 / 80 GB", "1.980 MHz", "3 düzeltilemeyen ECC hatası", "

GPU'lar

", "Meşgul GPU, karta göre", "GPU sıcaklığı, °C", "Son bir saat", // The chart's axis and peak in Turkish figures. diff --git a/internal/web/licences.go b/internal/web/licences.go index 3ae0b08..ed93e5d 100644 --- a/internal/web/licences.go +++ b/internal/web/licences.go @@ -101,11 +101,11 @@ func (p *AccountPages) setLicences(w http.ResponseWriter, r *http.Request) { return } if !p.accounts.CheckCSRF(sess, r.FormValue("csrf")) { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } if err := r.ParseForm(); err != nil { - http.Error(w, "bad request", http.StatusBadRequest) + badForm(w, r) return } var ex []string diff --git a/internal/web/locales/en/common.json b/internal/web/locales/en/common.json index 9b971e5..32c2a9f 100644 --- a/internal/web/locales/en/common.json +++ b/internal/web/locales/en/common.json @@ -4,6 +4,7 @@ "_meta.group": "", "_meta.date": "2006-01-02", "_meta.datetime": "2006-01-02 15:04", + "_meta.percent": "%s%%", "lang.switch": "Language", diff --git a/internal/web/locales/en/errors.json b/internal/web/locales/en/errors.json new file mode 100644 index 0000000..a586941 --- /dev/null +++ b/internal/web/locales/en/errors.json @@ -0,0 +1,21 @@ +{ + "err.not_found": "it is not there: it may have been removed meanwhile", + "err.not_empty": "it is not empty: remove what is in it first", + "err.duplicate_name": "something of that name is already here", + "err.email_taken": "that address already has an account", + "err.key_ttl": "a key may not live that long here", + "err.not_member": "not a member of that project", + "err.owner_inactive": "the key's owner is not an active account", + "err.settings_changed": "the cluster's settings were changed meanwhile: open them again and make the change there", + "err.no_session": "you are not signed in", + "err.knowledge_off": "knowledge bases are off on this gateway", + "err.memory_off": "this project does not keep memory notes", + "err.memory_full": "this agent has reached its limit of notes", + "err.threads_off": "this project does not keep conversations", + "err.no_reset": "there is no reset link for that address", + "err.in_catalog": "a model of that name is already in the catalog", + "err.mirror_fetching": "a download of that version is already running", + "err.mirror_exists": "that version is already in the mirror", + "err.mirror_incomplete": "that version is not complete in the mirror", + "err.bad_form": "the form had expired, or did not come from this site: go back, reload the page and try again" +} diff --git a/internal/web/locales/tr/common.json b/internal/web/locales/tr/common.json index 60c14ed..c270416 100644 --- a/internal/web/locales/tr/common.json +++ b/internal/web/locales/tr/common.json @@ -4,6 +4,7 @@ "_meta.group": ".", "_meta.date": "02.01.2006", "_meta.datetime": "02.01.2006 15:04", + "_meta.percent": "%%%s", "lang.switch": "Dil", diff --git a/internal/web/locales/tr/dashboard.json b/internal/web/locales/tr/dashboard.json index bf7c72b..346dc89 100644 --- a/internal/web/locales/tr/dashboard.json +++ b/internal/web/locales/tr/dashboard.json @@ -30,7 +30,7 @@ "dash.gpumap.legend.memory": "Açık renk boş, koyu renk dolu. Motorlar başlarken bir kartın belleğinin çoğunu ayırır.", "dash.gpumap.legend.util": "Açık renk boşta, koyu yeşil tamamen meşgul. Kesikli kenarlık, hiçbir replikanın tutmadığı bir karttır.", "dash.gpumap.free": "boş", - "dash.gpumap.busy": "%s%% meşgul", + "dash.gpumap.busy": "%%%s meşgul", "dash.gpumap.memory": "%s / %s GB", "dash.gpumap.held_back": "kısıtlayan: %s", "dash.gpumap.note": "Kırmızı çerçeve, yıpranmakta olan bir karttır: XID hataları, yeniden eşlenmiş ya da sıfırlama bekleyen bellek satırları, NVLink hataları. Karttaki rakam, renklendirmede kullanılan değerdir; kartın geri kalanı imlecin altındadır.", diff --git a/internal/web/locales/tr/errors.json b/internal/web/locales/tr/errors.json new file mode 100644 index 0000000..98e16e9 --- /dev/null +++ b/internal/web/locales/tr/errors.json @@ -0,0 +1,21 @@ +{ + "err.not_found": "bulunamadı: bu arada kaldırılmış olabilir", + "err.not_empty": "boş değil: önce içindekileri kaldırın", + "err.duplicate_name": "bu adla bir şey zaten var", + "err.email_taken": "bu adresin zaten bir hesabı var", + "err.key_ttl": "burada bir anahtar bu kadar uzun yaşayamaz", + "err.not_member": "o projenin üyesi değil", + "err.owner_inactive": "anahtarın sahibi etkin bir hesap değil", + "err.settings_changed": "cluster ayarları bu arada değişti: ayarları yeniden açıp değişikliği orada yapın", + "err.no_session": "giriş yapmadınız", + "err.knowledge_off": "bu gateway'de bilgi tabanları kapalı", + "err.memory_off": "bu proje not tutmuyor", + "err.memory_full": "bu ajan not sınırına ulaştı", + "err.threads_off": "bu proje konuşmaları tutmuyor", + "err.no_reset": "bu adres için bir sıfırlama bağlantısı yok", + "err.in_catalog": "bu adla bir model katalogda zaten var", + "err.mirror_fetching": "bu sürümün indirmesi zaten sürüyor", + "err.mirror_exists": "bu sürüm mirror'da zaten var", + "err.mirror_incomplete": "bu sürüm mirror'da eksik", + "err.bad_form": "form süresi dolmuş ya da bu siteden gelmemiş: geri dönün, sayfayı yenileyip yeniden deneyin" +} diff --git a/internal/web/locales/tr/node.json b/internal/web/locales/tr/node.json index 7df2668..404bce3 100644 --- a/internal/web/locales/tr/node.json +++ b/internal/web/locales/tr/node.json @@ -18,7 +18,7 @@ "node.gpu.power": "Güç", "node.gpu.clock": "Saat hızı", "node.gpu.held_back": "Kısıtlayan ve yıpranma", - "node.gpu.memory_util": "bellek %s%%", + "node.gpu.memory_util": "bellek %%%s", "node.of_gb": "/ %s GB", "node.of": "/ %s", "node.gpu.nothing": "hiçbir şey", diff --git a/internal/web/templates/account.html b/internal/web/templates/account.html index 7b9441d..a10ee8d 100644 --- a/internal/web/templates/account.html +++ b/internal/web/templates/account.html @@ -239,7 +239,7 @@ document.addEventListener("click", (e) => {

{{t "profile.password_note"}}

-
llmgw {{.Version}}{{.Now}}{{template "langs"}}
+
llmgw {{.Version}}{{stamp .Now}}{{template "langs"}}
{{end}} {{define "forgot"}} diff --git a/internal/web/templates/admin_mirror.html b/internal/web/templates/admin_mirror.html index a35f97d..37b3980 100644 --- a/internal/web/templates/admin_mirror.html +++ b/internal/web/templates/admin_mirror.html @@ -437,7 +437,7 @@ document.querySelector("table.explore").addEventListener("submit", async functio

{{t "mirror.fetch.note"}}

{{end}} -
llmgw {{.Version}}{{.Now}}{{template "langs"}}
+
llmgw {{.Version}}{{stamp .Now}}{{template "langs"}}
{{end}} {{/* One repository: its card and its files. The whole page when opened by diff --git a/internal/web/templates/admin_people.html b/internal/web/templates/admin_people.html index 50bce01..c1fbafd 100644 --- a/internal/web/templates/admin_people.html +++ b/internal/web/templates/admin_people.html @@ -34,7 +34,7 @@ {{end}} -
llmgw {{.Version}}{{.Now}}{{template "langs"}}
+
llmgw {{.Version}}{{stamp .Now}}{{template "langs"}}
{{end}} -
llmgw {{.Version}}{{.Now}}{{template "langs"}}
+
llmgw {{.Version}}{{stamp .Now}}{{template "langs"}}
{{end}} diff --git a/internal/web/templates/images.html b/internal/web/templates/images.html index 69148d9..4309c77 100644 --- a/internal/web/templates/images.html +++ b/internal/web/templates/images.html @@ -61,7 +61,7 @@

{{th "images.empty_why"}}

{{end}} -
llmgw {{.Version}}{{.Now}}{{template "langs"}}
+
llmgw {{.Version}}{{stamp .Now}}{{template "langs"}}
{{else if not .Error}}

{{t "logs.empty"}}

{{end}} -
llmgw {{.Version}} · {{.Now}}{{template "langs"}}
+
llmgw {{.Version}} · {{stamp .Now}}{{template "langs"}}
{{end}} diff --git a/internal/web/templates/nvlink.html b/internal/web/templates/nvlink.html index d2fb182..d4cbfbf 100644 --- a/internal/web/templates/nvlink.html +++ b/internal/web/templates/nvlink.html @@ -49,5 +49,5 @@

{{t "nvlink.note"}}

-
llmgw {{.Version}} · {{t "common.refreshes_every" 30}}{{.Now}}{{template "langs"}}
+
llmgw {{.Version}} · {{t "common.refreshes_every" 30}}{{stamp .Now}}{{template "langs"}}
{{end}} diff --git a/internal/web/templates/org.html b/internal/web/templates/org.html index 349d2b1..a50de22 100644 --- a/internal/web/templates/org.html +++ b/internal/web/templates/org.html @@ -87,7 +87,7 @@ {{else}}

{{t "org.members.empty"}}

{{end}} -
llmgw {{.Version}}{{.Now}}{{template "langs"}}
+
llmgw {{.Version}}{{stamp .Now}}{{template "langs"}}
{{end}} @@ -103,5 +103,5 @@

{{t "org.none.about"}}

{{th "org.none.how"}}

-
llmgw {{.Version}}{{.Now}}{{template "langs"}}
+ {{end}}