diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index e8b2c49a68..ebc2c11dfe 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -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, diff --git a/backend/internal/handler/admin/setting_handler_update.go b/backend/internal/handler/admin/setting_handler_update.go index b4840f0ba6..4d9447bd0e 100644 --- a/backend/internal/handler/admin/setting_handler_update.go +++ b/backend/internal/handler/admin/setting_handler_update.go @@ -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, diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index ff9797d86f..eba009a7e6 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -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"` diff --git a/backend/internal/handler/setting_handler.go b/backend/internal/handler/setting_handler.go index d314a4f94b..ec597a7641 100644 --- a/backend/internal/handler/setting_handler.go +++ b/backend/internal/handler/setting_handler.go @@ -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, diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index 5edbfbb106..7ef6745960 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -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, diff --git a/backend/internal/service/channel_monitor_v2.go b/backend/internal/service/channel_monitor_v2.go index 1afc20293c..cb2251de7a 100644 --- a/backend/internal/service/channel_monitor_v2.go +++ b/backend/internal/service/channel_monitor_v2.go @@ -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 diff --git a/backend/internal/service/channel_monitor_v2_test.go b/backend/internal/service/channel_monitor_v2_test.go index 9172b333df..d26f43eef2 100644 --- a/backend/internal/service/channel_monitor_v2_test.go +++ b/backend/internal/service/channel_monitor_v2_test.go @@ -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{ diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go index f4a13ee1a9..3e0cb28c5b 100644 --- a/backend/internal/service/domain_constants.go +++ b/backend/internal/service/domain_constants.go @@ -490,6 +490,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. diff --git a/backend/internal/service/setting_parse.go b/backend/internal/service/setting_parse.go index ade4de0d41..7fc929e9c6 100644 --- a/backend/internal/service/setting_parse.go +++ b/backend/internal/service/setting_parse.go @@ -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) diff --git a/backend/internal/service/setting_public.go b/backend/internal/service/setting_public.go index 7072dd7c03..d985ce6dd6 100644 --- a/backend/internal/service/setting_public.go +++ b/backend/internal/service/setting_public.go @@ -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, diff --git a/backend/internal/service/setting_service_public_test.go b/backend/internal/service/setting_service_public_test.go index 3fa0d40426..316df8ffda 100644 --- a/backend/internal/service/setting_service_public_test.go +++ b/backend/internal/service/setting_service_public_test.go @@ -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{ diff --git a/backend/internal/service/setting_update.go b/backend/internal/service/setting_update.go index 8928a63534..1a4793aa55 100644 --- a/backend/internal/service/setting_update.go +++ b/backend/internal/service/setting_update.go @@ -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 != "" { diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index 9b6932934d..8216535309 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -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"` diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index efe6f0402d..7c976756df 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -722,6 +722,7 @@ export interface SystemSettings { channel_monitor_default_interval_seconds: number; channel_monitor_hide_throughput?: boolean; channel_monitor_show_quota?: boolean; + channel_monitor_hide_user_ranking?: boolean; // Available Channels feature switch available_channels_enabled: boolean; @@ -1023,6 +1024,7 @@ export interface UpdateSettingsRequest { channel_monitor_default_interval_seconds?: number; channel_monitor_hide_throughput?: boolean; channel_monitor_show_quota?: boolean; + channel_monitor_hide_user_ranking?: boolean; // Available Channels feature switch available_channels_enabled?: boolean; diff --git a/frontend/src/features/channel-monitor-v2/MetricCell.vue b/frontend/src/features/channel-monitor-v2/MetricCell.vue index aa3eee65eb..e2fb5a3b52 100644 --- a/frontend/src/features/channel-monitor-v2/MetricCell.vue +++ b/frontend/src/features/channel-monitor-v2/MetricCell.vue @@ -4,7 +4,7 @@ :title="title || undefined" > -
-
-

- {{ t('admin.settings.features.channelMonitor.hideThroughput') }} -

-

- {{ t('admin.settings.features.channelMonitor.hideThroughputHint') }} -

+
+
+
+

+ {{ t('admin.settings.features.channelMonitor.hideThroughput') }} +

+

+ {{ t('admin.settings.features.channelMonitor.hideThroughputHint') }} +

+
+ +
+
+
+

+ {{ t('admin.settings.features.channelMonitor.hideUserRanking') }} +

+

+ {{ t('admin.settings.features.channelMonitor.hideUserRankingHint') }} +

+
+
-
@@ -9483,6 +9496,7 @@ type SettingsForm = Omit< /** Form always binds a concrete boolean (SystemSettings marks this optional). */ channel_monitor_hide_throughput: boolean; channel_monitor_show_quota: boolean; + channel_monitor_hide_user_ranking: boolean; smtp_password: string; turnstile_secret_key: string; tencent_captcha_app_secret_key: string; @@ -9797,6 +9811,7 @@ const form = reactive({ channel_monitor_default_interval_seconds: 60, channel_monitor_hide_throughput: false, channel_monitor_show_quota: false, + channel_monitor_hide_user_ranking: false, // Available Channels feature switch available_channels_enabled: false, // Model Plaza feature switches + description @@ -10803,6 +10818,9 @@ async function loadSettings() { form.channel_monitor_show_quota = Boolean( settings.channel_monitor_show_quota ); + form.channel_monitor_hide_user_ranking = Boolean( + settings.channel_monitor_hide_user_ranking + ); form.login_agreement_updated_at = settings.login_agreement_updated_at || "2026-03-31"; form.login_agreement_documents = @@ -11457,6 +11475,7 @@ async function saveSettings() { Number(form.channel_monitor_default_interval_seconds) || 60, channel_monitor_hide_throughput: Boolean(form.channel_monitor_hide_throughput), channel_monitor_show_quota: Boolean(form.channel_monitor_show_quota), + channel_monitor_hide_user_ranking: Boolean(form.channel_monitor_hide_user_ranking), // Available Channels feature switch available_channels_enabled: form.available_channels_enabled, // Model Plaza feature switches + description diff --git a/frontend/src/views/user/ChannelStatusV2View.vue b/frontend/src/views/user/ChannelStatusV2View.vue index d97b14cb99..4fdcaf278a 100644 --- a/frontend/src/views/user/ChannelStatusV2View.vue +++ b/frontend/src/views/user/ChannelStatusV2View.vue @@ -211,7 +211,7 @@ :value="formatMs(snapshot.metrics.ttft.p50_ms)" :detail="latencyKpiSecondary(snapshot.metrics.ttft)" :title="latencyDetail(snapshot.metrics.ttft)" - :state="snapshot.health.ttft" + :state="ttftCellState(snapshot.health.ttft, snapshot.metrics.ttft)" /> authStore.isAdmin) /** Admins always see RPM/TPM; users honor the hide-throughput system setting. */ const showThroughput = computed(() => isAdmin.value || !isChannelMonitorThroughputHidden()) +/** Admins always see ranking; users honor the hide-user-ranking system setting. */ +const showUserRanking = computed(() => isAdmin.value || !isChannelMonitorUserRankingHidden()) const ranges = computed(() => [ { value: '90m' as MonitorRange, label: t('channelMonitorV2.ranges.90m') }, @@ -519,11 +522,16 @@ const ranges = computed(() => [ { value: '7d' as MonitorRange, label: t('channelMonitorV2.ranges.7d') }, { value: '30d' as MonitorRange, label: t('channelMonitorV2.ranges.30d') }, ]) -const tabs = computed(() => [ - { value: 'models' as Tab, label: t('channelMonitorV2.tabs.models') }, - { value: 'errors' as Tab, label: t('channelMonitorV2.tabs.errors') }, - { value: 'users' as Tab, label: t('channelMonitorV2.tabs.users') }, -]) +const tabs = computed(() => { + const items: Array<{ value: Tab; label: string }> = [ + { value: 'models', label: t('channelMonitorV2.tabs.models') }, + { value: 'errors', label: t('channelMonitorV2.tabs.errors') }, + ] + if (showUserRanking.value) { + items.push({ value: 'users', label: t('channelMonitorV2.tabs.users') }) + } + return items +}) const matrixGroupOptions = computed(() => [ { value: 'platform' as MonitorMatrixGroupBy, label: t('channelMonitorV2.groupBy.platform') }, { value: 'platform_group' as MonitorMatrixGroupBy, label: t('channelMonitorV2.groupBy.platformGroup') }, @@ -543,9 +551,7 @@ const filter = ref({ groupIds: csv(route.query.group).map(Number).filter(Boolean), models: csv(route.query.model), }) -const activeTab = ref( - (['models', 'errors', 'users'].includes(String(route.query.tab)) ? route.query.tab : 'models') as Tab -) +const activeTab = ref(parseTab(route.query.tab, showUserRanking.value)) const matrixGroupBy = ref(parseMatrixGroupBy(route.query.group_by)) const healthMode = ref(parseHealthMode(route.query.health_mode)) const trendView = ref(parseTrendView(route.query.trend_view)) @@ -672,6 +678,10 @@ function parseMatrixGroupBy(value: unknown): MonitorMatrixGroupBy { ? (value as MonitorMatrixGroupBy) : 'platform_group' } +function parseTab(value: unknown, allowUsers: boolean): Tab { + const allowed: Tab[] = allowUsers ? ['models', 'errors', 'users'] : ['models', 'errors'] + return allowed.includes(value as Tab) ? (value as Tab) : 'models' +} function parseHealthMode(value: unknown): HealthMode { const allowed: HealthMode[] = ['overall', 'success', 'ttft', 'cache'] return allowed.includes(value as HealthMode) ? (value as HealthMode) : 'overall' @@ -773,8 +783,10 @@ async function loadTab(signal?: AbortSignal, id = sequence) { modelRows.value = (await api.getModels(filter.value, isAdmin.value, signal)).items || [] } else if (activeTab.value === 'errors') { errorRows.value = (await api.getErrors(filter.value, isAdmin.value, signal)).items || [] - } else { + } else if (showUserRanking.value) { userRows.value = (await api.getUsers(filter.value, isAdmin.value, signal)).items || [] + } else { + userRows.value = [] } } catch (error) { const e = error as { name?: string; code?: string } @@ -835,6 +847,9 @@ function formatPercent(value: number) { function formatMs(value: number | null) { return formatMonitorMs(value) } +function ttftCellState(state: HealthState | undefined, metric: { p50_ms: number | null; sample_count?: number }) { + return ttftDisplayState(state, metric) +} function latencyDetail(metric: { p50_ms: number | null p90_ms?: number | null @@ -904,6 +919,11 @@ watch(activeTab, () => { syncQuery() void loadTab() }) +watch(showUserRanking, (allowed) => { + if (!allowed && activeTab.value === 'users') { + activeTab.value = 'models' + } +}) onMounted(() => void reload(false)) onBeforeUnmount(() => { controller?.abort()