mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-09 08:58:23 +00:00
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:
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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in new issue
Block a user