The errors other packages give a page (store, catalog, mirror) are shown in its language; a percentage is written as the language writes it (%93 in Turkish); the footer's time in its date form; and a stale form is refused with a sentence rather than "bad request". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
3a4488b41e
commit
f8d18efaef
36 files changed
+203
-46
No files matched your search
+2
-1
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestTheDashboardSpeaksTurkishWhenAsked(t *testing.T) {
|
||||
// A state is still its class, and a Turkish word.
|
||||
`<td class="state state-ready">hazır</td>`,
|
||||
// The GPU map: its modes, and what a card says under the pointer.
|
||||
"<strong>meşguliyet</strong>", "gpu07 GPU 2 · code (code-1) · 93% meşgul · 39,1 / 80 GB",
|
||||
"<strong>meşguliyet</strong>", "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 <span class=\"note\">/ 80 GB", "1.980 MHz",
|
||||
"64 çekirdek", "bellek %64", "71,2 <span class=\"note\">/ 80 GB", "1.980 MHz",
|
||||
"3 düzeltilemeyen ECC hatası", "<h2>GPU'lar</h2>",
|
||||
"Meşgul GPU, karta göre", "GPU sıcaklığı, °C", "Son bir saat",
|
||||
// The chart's axis and peak in Turkish figures.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"_meta.group": "",
|
||||
"_meta.date": "2006-01-02",
|
||||
"_meta.datetime": "2006-01-02 15:04",
|
||||
"_meta.percent": "%s%%",
|
||||
|
||||
"lang.switch": "Language",
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"_meta.group": ".",
|
||||
"_meta.date": "02.01.2006",
|
||||
"_meta.datetime": "02.01.2006 15:04",
|
||||
"_meta.percent": "%%%s",
|
||||
|
||||
"lang.switch": "Dil",
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -239,7 +239,7 @@ document.addEventListener("click", (e) => {
|
||||
</form>
|
||||
<p class="note">{{t "profile.password_note"}}</p>
|
||||
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span>{{template "langs"}}</footer>
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
||||
</div></body></html>{{end}}
|
||||
|
||||
{{define "forgot"}}<!doctype html>
|
||||
|
||||
@@ -437,7 +437,7 @@ document.querySelector("table.explore").addEventListener("submit", async functio
|
||||
<p class="note">{{t "mirror.fetch.note"}}</p>
|
||||
{{end}}
|
||||
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span>{{template "langs"}}</footer>
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
||||
</div></body></html>{{end}}
|
||||
|
||||
{{/* One repository: its card and its files. The whole page when opened by
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span>{{template "langs"}}</footer>
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
||||
</div>
|
||||
<script>
|
||||
// Search the account list. Outside #people so what was typed survives a swap,
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<p class="note">{{th "chat.empty_why"}}</p>
|
||||
{{end}}
|
||||
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span>{{template "langs"}}</footer>
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -40,5 +40,5 @@ client = OpenAI(base_url="{{.BaseURL}}/v1", api_key=os.environ["LLMGW_API_KEY"])
|
||||
<p class="note">{{t "claim.expires" .ExpiresAt}}</p>
|
||||
{{end}}
|
||||
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span>{{template "langs"}}</footer>
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
||||
</div></body></html>{{end}}
|
||||
@@ -66,7 +66,7 @@
|
||||
room is paying. */}}
|
||||
<table class="rows"><tr><th>{{t "dash.col.model"}}</th><th class="num">GPUs</th><th class="num">{{t "dash.eff.tps"}}</th><th class="num">{{t "dash.eff.per_gpu"}}</th><th class="num">{{t "dash.eff.busy"}}</th><th class="num">{{t "dash.eff.draw"}}</th><th class="num">{{t "dash.eff.per_wh"}}</th></tr>
|
||||
{{range .}}<tr><td>{{.Model}}</td><td class="num">{{num .GPUs 0}}</td><td class="num">{{num .TokensPerSec 1}}</td>
|
||||
<td class="num">{{num .PerGPU 1}}</td><td class="num">{{num .Util 0}}%</td>
|
||||
<td class="num">{{num .PerGPU 1}}</td><td class="num">{{pc .Util 0}}</td>
|
||||
<td class="num">{{if .Watts}}{{num .Watts 0}} W{{else}}—{{end}}</td><td class="num">{{if .PerWh}}{{num .PerWh 0}}{{else}}—{{end}}</td></tr>
|
||||
{{end}}</table>
|
||||
{{end}}
|
||||
@@ -77,7 +77,7 @@
|
||||
be is the operator's call. */}}
|
||||
<table class="rows"><tr><th>{{t "dash.col.model"}}</th><th>{{t "dash.col.replica"}}</th><th>{{t "dash.col.node"}}</th><th class="num">GPUs</th><th class="num">{{t "dash.idle.busiest"}}</th><th>{{t "dash.idle.last_request"}}</th></tr>
|
||||
{{range .}}<tr><td>{{.Model}}</td><td class="clip"><code>{{.Replica}}</code></td><td><code>{{.Node}}</code></td>
|
||||
<td class="num">{{len .GPUs}}</td><td class="num">{{num .MaxUtil 0}}%</td><td>{{if .Idle}}{{t "dash.idle.ago" .Idle}}{{else}}<span class="note">{{t "dash.idle.unknown"}}</span>{{end}}</td></tr>
|
||||
<td class="num">{{len .GPUs}}</td><td class="num">{{pc .MaxUtil 0}}</td><td>{{if .Idle}}{{t "dash.idle.ago" .Idle}}{{else}}<span class="note">{{t "dash.idle.unknown"}}</span>{{end}}</td></tr>
|
||||
{{end}}</table>
|
||||
{{end}}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<td class="state state-{{.State}}">{{t (print "dash.state." .State)}}</td>
|
||||
{{/* A dash, not an empty bar, when the agent has not measured it lately:
|
||||
unknown is not idle. */}}
|
||||
<td class="load">{{with .Load}}{{template "minibar" pctbar .CPUPercent}}<span class="load-v">{{num .CPUPercent 0}}%{{if .CPUs}} <span class="note">{{t "dash.nodes.cpus_of" .CPUs}}</span>{{end}}</span>{{else}}—{{end}}</td>
|
||||
<td class="load">{{with .Load}}{{template "minibar" pctbar .CPUPercent}}<span class="load-v">{{pc .CPUPercent 0}}{{if .CPUs}} <span class="note">{{t "dash.nodes.cpus_of" .CPUs}}</span>{{end}}</span>{{else}}—{{end}}</td>
|
||||
<td class="load">{{with .Load}}{{template "minibar" pctbar .MemPercent}}<span class="load-v">{{bytes .MemUsed}} <span class="note">{{t "dash.nodes.mem_of" (bytes .MemTotal)}}</span></span>{{else}}{{if .MemoryGB}}{{.MemoryGB}}G{{else}}—{{end}}{{end}}</td>
|
||||
{{if $.AnyGPUs}}<td class="num">{{if .GPUTotal}}{{.GPUFree}}/{{.GPUTotal}}{{else}}—{{end}}</td>{{end}}
|
||||
{{/* Free out of total, because the first is meaningless without the
|
||||
@@ -360,7 +360,7 @@ document.querySelectorAll("form.build-choice select").forEach(function (s) {
|
||||
|
||||
<footer>
|
||||
<span>llmgw {{.Version}} · {{t "common.refreshes_every" .RefreshSec}}</span>
|
||||
<span>{{.Now}}</span>
|
||||
<span>{{stamp .Now}}</span>
|
||||
{{template "langs"}}</footer>
|
||||
</div></body></html>{{end}}
|
||||
|
||||
|
||||
@@ -70,5 +70,5 @@
|
||||
<script src="{{asset "fabric.js"}}" defer></script>
|
||||
{{end}}
|
||||
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span>{{template "langs"}}</footer>
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
||||
</div></body></html>{{end}}
|
||||
@@ -61,7 +61,7 @@
|
||||
<p class="note">{{th "images.empty_why"}}</p>
|
||||
{{end}}
|
||||
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span>{{template "langs"}}</footer>
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -171,7 +171,7 @@ print(reply.choices[0].message.content)</code></pre>
|
||||
<li><code>GET {{.BaseURL}}/me</code><span>{{t "landing.me_endpoint"}}</span></li>
|
||||
</ul>
|
||||
|
||||
<footer><span><a href="/about">llmgw {{.Version}}</a></span><span>{{.Now}}</span>{{template "langs"}}</footer>
|
||||
<footer><span><a href="/about">llmgw {{.Version}}</a></span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<p class="note">{{th "meshes.empty_why"}}</p>
|
||||
{{end}}
|
||||
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span>{{template "langs"}}</footer>
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
{{with .Node}}
|
||||
<div class="tiles">
|
||||
{{with .Load}}
|
||||
<div class="tile"><div class="v">{{num .CPUPercent 0}}%</div><div class="k">CPU{{if .CPUs}} · {{t "node.cores" .CPUs}}{{end}}</div>{{template "minibar" pctbar .CPUPercent}}</div>
|
||||
<div class="tile"><div class="v">{{pc .CPUPercent 0}}</div><div class="k">CPU{{if .CPUs}} · {{t "node.cores" .CPUs}}{{end}}</div>{{template "minibar" pctbar .CPUPercent}}</div>
|
||||
<div class="tile"><div class="v">{{bytes .MemUsed}}</div><div class="k">{{t "node.ram_in_use_of" (bytes .MemTotal)}}</div>{{template "minibar" pctbar .MemPercent}}</div>
|
||||
<div class="tile"><div class="v">{{num .Load1 2}}</div><div class="k">{{t "node.load" (num .Load5 2) (num .Load15 2)}}</div></div>
|
||||
{{/* Only what this machine has sensors for: a VM has none, and a tile
|
||||
@@ -45,7 +45,7 @@
|
||||
<td class="num">{{.Index}}</td>
|
||||
{{/* From the last sample; a dash where the driver or an older agent did
|
||||
not say, rather than a zero nobody measured. */}}
|
||||
<td class="load">{{if and .MemoryTotalMB (ge .UtilPercent 0.0)}}{{template "minibar" pctbar .UtilPercent}}<span class="load-v">{{num .UtilPercent 0}}%{{if ge .MemUtilPercent 0.0}} <span class="note">{{t "node.gpu.memory_util" (num .MemUtilPercent 0)}}</span>{{end}}</span>{{else}}—{{end}}</td>
|
||||
<td class="load">{{if and .MemoryTotalMB (ge .UtilPercent 0.0)}}{{template "minibar" pctbar .UtilPercent}}<span class="load-v">{{pc .UtilPercent 0}}{{if ge .MemUtilPercent 0.0}} <span class="note">{{t "node.gpu.memory_util" (num .MemUtilPercent 0)}}</span>{{end}}</span>{{else}}—{{end}}</td>
|
||||
<td class="load">{{if .MemoryTotalMB}}{{template "minibar" pctbar (percent .MemoryUsedMB .MemoryTotalMB)}}<span class="load-v">{{num (gib .MemoryUsedMB) 1}} <span class="note">{{t "node.of_gb" (num (gib .MemoryTotalMB) 0)}}</span></span>{{else}}—{{end}}</td>
|
||||
<td class="num">{{if .TempC}}{{num .TempC 0}} °C{{else}}—{{end}}</td>
|
||||
<td class="num">{{if .MemoryTempC}}{{num .MemoryTempC 0}} °C{{else}}—{{end}}</td>
|
||||
@@ -151,5 +151,5 @@
|
||||
<p class="note">{{th "node.no_store"}}</p>
|
||||
{{end}}
|
||||
|
||||
<footer><span>llmgw {{.Version}} · {{t "common.refreshes_every" .RefreshSec}}</span><span>{{.Now}}</span>{{template "langs"}}</footer>
|
||||
<footer><span>llmgw {{.Version}} · {{t "common.refreshes_every" .RefreshSec}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
||||
</div></body></html>{{end}}
|
||||
@@ -28,5 +28,5 @@
|
||||
<script>window.scrollTo(0, document.body.scrollHeight);</script>
|
||||
{{else if not .Error}}<p class="empty">{{t "logs.empty"}}</p>{{end}}
|
||||
|
||||
<footer class="note">llmgw {{.Version}} · {{.Now}}{{template "langs"}}</footer>
|
||||
<footer class="note">llmgw {{.Version}} · {{stamp .Now}}{{template "langs"}}</footer>
|
||||
</div></body></html>{{end}}
|
||||
@@ -49,5 +49,5 @@
|
||||
|
||||
<p class="note">{{t "nvlink.note"}}</p>
|
||||
|
||||
<footer><span>llmgw {{.Version}} · {{t "common.refreshes_every" 30}}</span><span>{{.Now}}</span>{{template "langs"}}</footer>
|
||||
<footer><span>llmgw {{.Version}} · {{t "common.refreshes_every" 30}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
||||
</div></body></html>{{end}}
|
||||
@@ -87,7 +87,7 @@
|
||||
</table>
|
||||
{{else}}<p class="empty">{{t "org.members.empty"}}</p>{{end}}
|
||||
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span>{{template "langs"}}</footer>
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
||||
</div></body></html>{{end}}
|
||||
|
||||
|
||||
@@ -103,5 +103,5 @@
|
||||
</header>
|
||||
<p>{{t "org.none.about"}}</p>
|
||||
<p>{{th "org.none.how"}}</p>
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{.Now}}</span>{{template "langs"}}</footer>
|
||||
<footer><span>llmgw {{.Version}}</span><span>{{stamp .Now}}</span>{{template "langs"}}</footer>
|
||||
</div></body></html>{{end}}
|
||||
Reference in new issue
Block a user