diff --git a/internal/server/server_test.go b/internal/server/server_test.go index f353876..c9344d2 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -15,6 +15,7 @@ import ( "git.misaka.ren/M1saka/zhanlu_proxy/internal/auth" "git.misaka.ren/M1saka/zhanlu_proxy/internal/config" + "git.misaka.ren/M1saka/zhanlu_proxy/internal/stats" "git.misaka.ren/M1saka/zhanlu_proxy/internal/store" ) @@ -365,6 +366,47 @@ func TestModelTestNoCredentials(t *testing.T) { } } +// TestAdminRendersModelRangeFilter verifies the admin page renders without +// panic for both empty and populated summaries, and that the by-model panel +// carries the 1d/7d/all range filter controls and a client-renderable tbody. +func TestAdminRendersModelRangeFilter(t *testing.T) { + populated := &stats.Summary{ + PerModel: []stats.ModelStat{ + {Model: "GLM-4.7", Requests: 3, PromptTokens: 10, CompletionTokens: 20, TotalTokens: 30, CachedTokens: 4}, + }, + } + cases := []struct { + name string + summary *stats.Summary + }{ + {"empty", &stats.Summary{}}, + {"populated", populated}, + } + for _, tc := range cases { + var buf bytes.Buffer + if err := adminTemplate.Execute(&buf, map[string]any{ + "Enabled": true, + "Stats": tc.summary, + "DBPath": "zhanlu.db", + "PasswordEnabled": false, + }); err != nil { + t.Fatalf("%s: render admin: %v", tc.name, err) + } + body := buf.String() + for _, want := range []string{ + `id="model-range"`, + `data-range="1d"`, + `data-range="7d"`, + `data-range="all"`, + `id="model-tbody"`, + } { + if !strings.Contains(body, want) { + t.Fatalf("%s: admin output missing %q", tc.name, want) + } + } + } +} + // totNum extracts an int from a JSON-decoded numeric value (float64). func totNum(v any) int { f, _ := v.(float64) diff --git a/internal/server/templates/admin.html b/internal/server/templates/admin.html index 2f25421..551688f 100644 --- a/internal/server/templates/admin.html +++ b/internal/server/templates/admin.html @@ -41,6 +41,13 @@ .tabpanel{display:none;min-width:0} .tabpanel.active{display:block;min-width:0} .sub-actions{display:flex;justify-content:flex-end;gap:10px;margin-bottom:16px} + .panel-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:16px;flex-wrap:wrap} + .panel-head h2{margin:0} + .seg{display:inline-flex;border:1px solid var(--border-strong);border-radius:999px;padding:2px;background:var(--bg)} + .seg-btn{border:0;background:none;font:inherit;font-weight:600;font-size:12.5px;color:var(--body);padding:6px 14px;border-radius:999px;cursor:pointer;transition:background .15s ease,color .15s ease} + .seg-btn:hover{color:var(--ink)} + .seg-btn[aria-current="true"]{background:var(--accent);color:#fff} + .seg-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px} .panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 1px 2px rgba(16,24,40,.04);padding:24px;min-width:0} .panel.disabled{background:#eef0f3} h2{margin:0 0 16px;font-size:16px;font-weight:600;letter-spacing:-.01em} @@ -129,15 +136,21 @@
| 模型 | 请求数 | Prompt | Comp | Total | 缓存 | 命中率 |
|---|---|---|---|---|---|---|
| {{.Model}} | {{.Requests}} | {{human .PromptTokens}} | {{human .CompletionTokens}} | {{human .TotalTokens}} | {{human .CachedTokens}} | {{rate .CachedTokens .PromptTokens}} |
暂无数据
{{end}} +