feat(channel-monitor-v2): add user ranking toggle and keep missing TTFT neutral

Give operators a V2 setting (channel_monitor_hide_user_ranking) to hide
the user ranking tab and the /users payload from non-admin channel
monitor v2 viewers. Default false (ranking stays visible). Admin
endpoints and views always keep the ranking.

Also stop painting first-token '-' as a red/empty failure when traffic
is sync-only and has no first-token samples: a missing TTFT is
"not applicable" (health-unknown), not a breached latency budget.

Changes:
- new setting key + fail-open parse (only literal "true" hides), wired
  through public/admin settings DTOs, handlers, and the V2 runtime
- ChannelMonitorV2Service.Users returns an empty list to non-admin
  viewers when the toggle is on
- frontend: feature flag + SettingsView toggle + user V2 view hides the
  ranking tab and skips the /users call
- monitorFormat: isTtftUnavailable/ttftDisplayState keep missing TTFT
  neutral

Tests:
- setting parse defaults to visible and only literal "true" hides
- V2 Users honors the toggle for non-admin viewers
- frontend MetricCell/monitorFormat keep missing TTFT neutral
This commit is contained in:
TT committed 2026-09-07 18:31:03 +08:00
1 parent 909b8c7ef4
commit 0d3dbae710
24 files changed
+262 -42

No files matched your search

@@ -376,6 +376,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds,
ChannelMonitorHideThroughput: settings.ChannelMonitorHideThroughput,
ChannelMonitorShowQuota: settings.ChannelMonitorShowQuota,
ChannelMonitorHideUserRanking: settings.ChannelMonitorHideUserRanking,
GrokDefaultTextModel: settings.GrokDefaultTextModel,
GrokCrossClientModelMapEnabled: settings.GrokCrossClientModelMapEnabled,
@@ -334,6 +334,7 @@ type UpdateSettingsRequest struct {
ChannelMonitorDefaultIntervalSeconds *int `json:"channel_monitor_default_interval_seconds"`
ChannelMonitorHideThroughput *bool `json:"channel_monitor_hide_throughput"`
ChannelMonitorShowQuota *bool `json:"channel_monitor_show_quota"`
ChannelMonitorHideUserRanking *bool `json:"channel_monitor_hide_user_ranking"`
// Grok model mapping policy
GrokDefaultTextModel *string `json:"grok_default_text_model"`
@@ -1906,6 +1907,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
return previousSettings.ChannelMonitorShowQuota
}(),
ChannelMonitorHideUserRanking: func() bool {
if req.ChannelMonitorHideUserRanking != nil {
return *req.ChannelMonitorHideUserRanking
}
return previousSettings.ChannelMonitorHideUserRanking
}(),
GrokDefaultTextModel: func() string {
if req.GrokDefaultTextModel != nil {
return *req.GrokDefaultTextModel
@@ -2366,6 +2373,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
ChannelMonitorDefaultIntervalSeconds: updatedSettings.ChannelMonitorDefaultIntervalSeconds,
ChannelMonitorHideThroughput: updatedSettings.ChannelMonitorHideThroughput,
ChannelMonitorShowQuota: updatedSettings.ChannelMonitorShowQuota,
ChannelMonitorHideUserRanking: updatedSettings.ChannelMonitorHideUserRanking,
GrokDefaultTextModel: updatedSettings.GrokDefaultTextModel,
GrokCrossClientModelMapEnabled: updatedSettings.GrokCrossClientModelMapEnabled,
+2
View File
@@ -307,6 +307,7 @@ type SystemSettings struct {
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
ChannelMonitorHideThroughput bool `json:"channel_monitor_hide_throughput"`
ChannelMonitorShowQuota bool `json:"channel_monitor_show_quota"`
ChannelMonitorHideUserRanking bool `json:"channel_monitor_hide_user_ranking"`
// Grok model mapping policy (admin settings; empty account mapping falls back to these).
GrokDefaultTextModel string `json:"grok_default_text_model"`
@@ -417,6 +418,7 @@ type PublicSettings struct {
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
ChannelMonitorHideThroughput bool `json:"channel_monitor_hide_throughput"`
ChannelMonitorShowQuota bool `json:"channel_monitor_show_quota"`
ChannelMonitorHideUserRanking bool `json:"channel_monitor_hide_user_ranking"`
AvailableChannelsEnabled bool `json:"available_channels_enabled"`
@@ -108,6 +108,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds,
ChannelMonitorHideThroughput: settings.ChannelMonitorHideThroughput,
ChannelMonitorShowQuota: settings.ChannelMonitorShowQuota,
ChannelMonitorHideUserRanking: settings.ChannelMonitorHideUserRanking,
AvailableChannelsEnabled: settings.AvailableChannelsEnabled,
@@ -992,6 +992,7 @@ func TestAPIContracts(t *testing.T) {
"channel_monitor_mode": "v1",
"channel_monitor_hide_throughput": true,
"channel_monitor_show_quota": false,
"channel_monitor_hide_user_ranking": false,
"channel_monitor_default_interval_seconds": 60,
"available_channels_enabled": false,
"model_plaza_enabled": false,
@@ -1305,6 +1306,7 @@ func TestAPIContracts(t *testing.T) {
"channel_monitor_mode": "v1",
"channel_monitor_hide_throughput": true,
"channel_monitor_show_quota": false,
"channel_monitor_hide_user_ranking": false,
"channel_monitor_default_interval_seconds": 60,
"available_channels_enabled": false,
"model_plaza_enabled": false,
@@ -405,6 +405,18 @@ func (s *ChannelMonitorV2Service) hideThroughputForViewer(ctx context.Context, a
return s.settings.GetChannelMonitorRuntime(ctx).HideThroughput
}
func (s *ChannelMonitorV2Service) hideUserRankingForViewer(ctx context.Context, admin bool) bool {
if admin {
return false
}
// Missing reader keeps the current ranking tab visible. Unlike throughput,
// ranking is already public and must stay on until an operator turns it off.
if s == nil || s.settings == nil {
return false
}
return s.settings.GetChannelMonitorRuntime(ctx).HideUserRanking
}
func (s *ChannelMonitorV2Service) GetConfig(ctx context.Context) (*ChannelMonitorV2Config, error) {
return s.repo.GetConfig(ctx)
}
@@ -652,6 +664,9 @@ func (s *ChannelMonitorV2Service) Users(ctx context.Context, filter ChannelMonit
if err != nil {
return nil, err
}
if s.hideUserRankingForViewer(ctx, admin) {
return &ChannelMonitorV2List[ChannelMonitorV2UserRow]{Items: []ChannelMonitorV2UserRow{}}, nil
}
result, err := s.repo.GetUsers(ctx, filter, *cfg, admin)
if err != nil {
return nil, err
@@ -252,6 +252,20 @@ func TestChannelMonitorV2HealthBlendsErrorTTFTAndCache(t *testing.T) {
require.Nil(t, health.Score)
}
func TestChannelMonitorV2HealthLeavesMissingTTFTUnknown(t *testing.T) {
health := ChannelMonitorV2HealthFor(ChannelMonitorV2Metric{
RequestCount: 200,
ErrorRate: 0,
CacheRate: 0,
CacheRateDenominator: 200,
TTFT: ChannelMonitorV2Latency{SampleCount: 0},
})
require.Equal(t, "unknown", health.TTFT)
require.Nil(t, health.TTFTScore)
require.NotEqual(t, "critical", health.Overall)
require.Equal(t, "healthy", health.Overall)
}
func TestChannelMonitorV2DefaultHealthThresholdsAreTolerant(t *testing.T) {
p50 := int64(2500)
health := ChannelMonitorV2HealthFor(ChannelMonitorV2Metric{
@@ -287,6 +301,32 @@ func TestErrorRateTTFTAndCacheScoreHelpers(t *testing.T) {
require.Equal(t, "healthy", cacheRateBand(0.50, 0.20, 0.05))
}
type channelMonitorV2RuntimeStub struct {
rt ChannelMonitorRuntime
}
func (s channelMonitorV2RuntimeStub) GetChannelMonitorRuntime(context.Context) ChannelMonitorRuntime {
return s.rt
}
func TestChannelMonitorV2UsersHiddenWhenSettingEnabled(t *testing.T) {
selfID, otherID := int64(7), int64(9)
repo := &channelMonitorV2RepoStub{config: ChannelMonitorV2Config{Enabled: true}, users: &ChannelMonitorV2List[ChannelMonitorV2UserRow]{Items: []ChannelMonitorV2UserRow{
{UserID: &otherID, Email: "other@example.com", Username: "other"},
{UserID: &selfID, Email: "self@example.com", Username: "self"},
}}}
svc := NewChannelMonitorV2Service(repo)
svc.SetRuntimeReader(channelMonitorV2RuntimeStub{rt: ChannelMonitorRuntime{HideUserRanking: true}})
hidden, err := svc.Users(context.Background(), ChannelMonitorV2Filter{}, selfID, false)
require.NoError(t, err)
require.Empty(t, hidden.Items)
admin, err := svc.Users(context.Background(), ChannelMonitorV2Filter{}, selfID, true)
require.NoError(t, err)
require.Len(t, admin.Items, 2)
}
func TestChannelMonitorV2UsersRemovesOtherUserIdentity(t *testing.T) {
selfID, otherID := int64(7), int64(9)
repo := &channelMonitorV2RepoStub{config: ChannelMonitorV2Config{Enabled: true}, users: &ChannelMonitorV2List[ChannelMonitorV2UserRow]{Items: []ChannelMonitorV2UserRow{
@@ -484,6 +484,10 @@ const (
// fail-closed (only the literal "true" enables it). Admin endpoints always
// keep the full snapshots regardless of this flag.
SettingKeyChannelMonitorShowQuota = "channel_monitor_show_quota"
// SettingKeyChannelMonitorHideUserRanking hides the user ranking tab and
// /users payload from non-admin channel-monitor v2 viewers.
// Default false (keep the current ranking tab). Admin endpoints always keep it.
SettingKeyChannelMonitorHideUserRanking = "channel_monitor_hide_user_ranking"
// SettingKeyGrokDefaultTextModel is the fallback Grok text model for empty
// request models and built-in Grok aliases (e.g. "grok" → this id). Default grok-4.5.
+11
View File
@@ -191,6 +191,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyChannelMonitorDefaultIntervalSeconds: "60",
SettingKeyChannelMonitorHideThroughput: "true",
SettingKeyChannelMonitorShowQuota: "false",
SettingKeyChannelMonitorHideUserRanking: "false",
// Grok compatibility defaults: cross-client mapping stays enabled unless
// operators explicitly disable it.
@@ -804,6 +805,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
// 配额展示默认关闭且 fail-closed:仅字面 "true" 视为开启
// (与 setting_public.go 公开读取路径保持一致)。
result.ChannelMonitorShowQuota = settings[SettingKeyChannelMonitorShowQuota] == "true"
result.ChannelMonitorHideUserRanking = isTrueSettingValue(settings[SettingKeyChannelMonitorHideUserRanking])
// Grok default mapping policy
result.GrokDefaultTextModel = strings.TrimSpace(settings[SettingKeyGrokDefaultTextModel])
@@ -1010,6 +1012,15 @@ func isFalseSettingValue(value string) bool {
}
}
func isTrueSettingValue(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "true", "1", "on", "enabled":
return true
default:
return false
}
}
func normalizeVisibleMethodSettingSource(method, source string, enabled bool) (string, error) {
_ = enabled
source = strings.TrimSpace(source)
+19 -8
View File
@@ -232,6 +232,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
SettingKeyChannelMonitorDefaultIntervalSeconds,
SettingKeyChannelMonitorHideThroughput,
SettingKeyChannelMonitorShowQuota,
SettingKeyChannelMonitorHideUserRanking,
SettingKeyAvailableChannelsEnabled,
SettingKeyModelPlazaEnabled,
SettingKeyModelPlazaRequireAuth,
@@ -358,6 +359,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
ChannelMonitorDefaultIntervalSeconds: parseChannelMonitorInterval(settings[SettingKeyChannelMonitorDefaultIntervalSeconds]),
ChannelMonitorHideThroughput: !isFalseSettingValue(settings[SettingKeyChannelMonitorHideThroughput]),
ChannelMonitorShowQuota: settings[SettingKeyChannelMonitorShowQuota] == "true",
ChannelMonitorHideUserRanking: isTrueSettingValue(settings[SettingKeyChannelMonitorHideUserRanking]),
AvailableChannelsEnabled: settings[SettingKeyAvailableChannelsEnabled] == "true",
@@ -430,6 +432,9 @@ type ChannelMonitorRuntime struct {
// snapshots; otherwise the user handler strips them server-side.
// Parsed fail-closed (only literal "true" enables). Admin always sees them.
ShowQuota bool
// HideUserRanking: when true, user-facing V2 views hide the user ranking tab
// and the /users payload. Parsed fail-open (only literal "true" hides it).
HideUserRanking bool
}
// ActiveProbesAllowed reports whether V1 active provider probes may run.
@@ -459,6 +464,7 @@ func (s *SettingService) GetChannelMonitorRuntime(ctx context.Context) ChannelMo
SettingKeyChannelMonitorDefaultIntervalSeconds,
SettingKeyChannelMonitorHideThroughput,
SettingKeyChannelMonitorShowQuota,
SettingKeyChannelMonitorHideUserRanking,
})
if err != nil {
return ChannelMonitorRuntime{
@@ -474,6 +480,7 @@ func (s *SettingService) GetChannelMonitorRuntime(ctx context.Context) ChannelMo
DefaultIntervalSeconds: parseChannelMonitorInterval(vals[SettingKeyChannelMonitorDefaultIntervalSeconds]),
HideThroughput: !isFalseSettingValue(vals[SettingKeyChannelMonitorHideThroughput]),
ShowQuota: vals[SettingKeyChannelMonitorShowQuota] == "true",
HideUserRanking: isTrueSettingValue(vals[SettingKeyChannelMonitorHideUserRanking]),
}
}
@@ -618,14 +625,17 @@ type PublicSettingsInjectionPayload struct {
ChannelMonitorHideThroughput bool `json:"channel_monitor_hide_throughput"`
// ChannelMonitorShowQuota gates the user-facing quota/balance display on
// monitors; fail-closed (absent/false = hidden). Admin UI always shows it.
ChannelMonitorShowQuota bool `json:"channel_monitor_show_quota"`
AvailableChannelsEnabled bool `json:"available_channels_enabled"`
ModelPlazaEnabled bool `json:"model_plaza_enabled"`
ModelPlazaRequireAuth bool `json:"model_plaza_require_auth"`
PluginManagementEnabled bool `json:"plugin_management_enabled"`
AffiliateEnabled bool `json:"affiliate_enabled"`
RiskControlEnabled bool `json:"risk_control_enabled"`
AllowUserViewErrorRequests bool `json:"allow_user_view_error_requests"`
// ChannelMonitorHideUserRanking hides the user ranking tab and /users payload
// from non-admin channel-monitor v2 viewers; default false (visible).
ChannelMonitorHideUserRanking bool `json:"channel_monitor_hide_user_ranking"`
ChannelMonitorShowQuota bool `json:"channel_monitor_show_quota"`
AvailableChannelsEnabled bool `json:"available_channels_enabled"`
ModelPlazaEnabled bool `json:"model_plaza_enabled"`
ModelPlazaRequireAuth bool `json:"model_plaza_require_auth"`
PluginManagementEnabled bool `json:"plugin_management_enabled"`
AffiliateEnabled bool `json:"affiliate_enabled"`
RiskControlEnabled bool `json:"risk_control_enabled"`
AllowUserViewErrorRequests bool `json:"allow_user_view_error_requests"`
}
// GetPublicSettingsForInjection returns public settings in a format suitable for HTML injection.
@@ -700,6 +710,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds,
ChannelMonitorHideThroughput: settings.ChannelMonitorHideThroughput,
ChannelMonitorShowQuota: settings.ChannelMonitorShowQuota,
ChannelMonitorHideUserRanking: settings.ChannelMonitorHideUserRanking,
AvailableChannelsEnabled: settings.AvailableChannelsEnabled,
ModelPlazaEnabled: settings.ModelPlazaEnabled,
ModelPlazaRequireAuth: settings.ModelPlazaRequireAuth,
@@ -139,6 +139,21 @@ func TestSettingService_ChannelMonitorShowQuotaFailsClosed(t *testing.T) {
}
}
func TestSettingService_ChannelMonitorHideUserRankingDefaultsToVisible(t *testing.T) {
missing := NewSettingService(&settingPublicRepoStub{values: map[string]string{}}, &config.Config{}).GetChannelMonitorRuntime(context.Background())
require.False(t, missing.HideUserRanking)
public, err := NewSettingService(&settingPublicRepoStub{values: map[string]string{}}, &config.Config{}).GetPublicSettings(context.Background())
require.NoError(t, err)
require.False(t, public.ChannelMonitorHideUserRanking)
for _, value := range []string{"true", "1", "on", "enabled"} {
runtime := NewSettingService(&settingPublicRepoStub{values: map[string]string{
SettingKeyChannelMonitorHideUserRanking: value,
}}, &config.Config{}).GetChannelMonitorRuntime(context.Background())
require.True(t, runtime.HideUserRanking, "value=%q", value)
}
}
func TestSettingService_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t *testing.T) {
repo := &settingPublicRepoStub{
values: map[string]string{
@@ -418,6 +418,7 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
}
updates[SettingKeyChannelMonitorHideThroughput] = strconv.FormatBool(settings.ChannelMonitorHideThroughput)
updates[SettingKeyChannelMonitorShowQuota] = strconv.FormatBool(settings.ChannelMonitorShowQuota)
updates[SettingKeyChannelMonitorHideUserRanking] = strconv.FormatBool(settings.ChannelMonitorHideUserRanking)
// Grok model mapping policy
if v := strings.TrimSpace(settings.GrokDefaultTextModel); v != "" {
@@ -202,6 +202,7 @@ type SystemSettings struct {
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
ChannelMonitorHideThroughput bool `json:"channel_monitor_hide_throughput"`
ChannelMonitorShowQuota bool `json:"channel_monitor_show_quota"`
ChannelMonitorHideUserRanking bool `json:"channel_monitor_hide_user_ranking"`
// Grok model mapping policy (admin settings; empty mapping falls back to these).
GrokDefaultTextModel string `json:"grok_default_text_model"`
@@ -382,6 +383,7 @@ type PublicSettings struct {
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
ChannelMonitorHideThroughput bool `json:"channel_monitor_hide_throughput"`
ChannelMonitorShowQuota bool `json:"channel_monitor_show_quota"`
ChannelMonitorHideUserRanking bool `json:"channel_monitor_hide_user_ranking"`
// Grok model mapping policy (admin settings).
GrokDefaultTextModel string `json:"grok_default_text_model"`