From e1dd43be06231e5b3a226a1488228bdb1abfce06 Mon Sep 17 00:00:00 2001 From: kingfs Date: Mon, 14 Sep 2026 15:29:00 +0800 Subject: [PATCH 1/2] fix: make upstream and model routing switches authoritative --- cmd/server/main_test.go | 48 ++++- cmd/server/management.go | 2 +- cmd/server/serve.go | 6 +- docs/MONITOR_GUIDE.md | 18 +- internal/channel/service.go | 126 +++++++---- internal/channel/service_test.go | 42 +++- internal/config/config.go | 1 + internal/monitor/configuration.go | 112 ++++++++++ internal/monitor/configuration_test.go | 200 ++++++++++++++++++ internal/monitor/server.go | 20 +- internal/monitor/server_test.go | 4 +- .../{index-BJxNZE_g.js => index-Csr12ZyE.js} | 64 +++--- internal/monitor/ui/dist/index.html | 2 +- internal/proxy/handler_e2e_test.go | 5 +- internal/router/model_authorization_test.go | 72 +++++++ internal/router/router.go | 69 +++++- internal/router/router_test.go | 2 + internal/store/config_transaction.go | 58 +++++ internal/store/store.go | 17 +- web/monitor-ui/src/lib/i18n.jsx | 10 +- .../src/routes/ChannelDetailPage.jsx | 17 +- web/monitor-ui/src/routes/ChannelsPage.jsx | 21 +- .../tests-real/monitor-real-server.spec.js | 40 ++-- web/monitor-ui/tests/monitor-smoke.spec.js | 66 ++++-- 24 files changed, 870 insertions(+), 152 deletions(-) create mode 100644 internal/monitor/configuration.go create mode 100644 internal/monitor/configuration_test.go rename internal/monitor/ui/dist/assets/{index-BJxNZE_g.js => index-Csr12ZyE.js} (63%) create mode 100644 internal/router/model_authorization_test.go create mode 100644 internal/store/config_transaction.go diff --git a/cmd/server/main_test.go b/cmd/server/main_test.go index 39c8a38..1b720e0 100644 --- a/cmd/server/main_test.go +++ b/cmd/server/main_test.go @@ -179,8 +179,8 @@ func TestResponsesServerConfigFromServeConfigDefaultsDisabled(t *testing.T) { if got.ForceStore { t.Fatalf("ForceStore = true, want false") } - if got.MaxRequestBodyBytes != 16<<20 { - t.Fatalf("MaxRequestBodyBytes = %d, want %d", got.MaxRequestBodyBytes, 16<<20) + if got.MaxRequestBodyBytes != (config.Config{}).ResponsesMaxRequestBodyBytes() { + t.Fatalf("MaxRequestBodyBytes = %d, want %d", got.MaxRequestBodyBytes, (config.Config{}).ResponsesMaxRequestBodyBytes()) } if got.Path != "/v1/responses" { t.Fatalf("Path = %q, want /v1/responses", got.Path) @@ -5552,3 +5552,47 @@ func (t authTransport) RoundTrip(req *http.Request) (*http.Response, error) { clone.Header.Set("Authorization", "Bearer "+t.Token) return http.DefaultTransport.RoundTrip(clone) } + +func TestRouterConfigurationKeepsDisabledAndDeletedChannelsAcrossBootstrap(t *testing.T) { + for _, deleted := range []bool{false, true} { + t.Run(map[bool]string{false: "disabled", true: "deleted"}[deleted], func(t *testing.T) { + st, err := store.New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer st.Close() + cfg := &config.Config{Upstream: config.UpstreamConfig{BaseURL: "https://yaml.example.invalid/v1", ProviderPreset: "openai"}} + svc := channel.NewService(st) + if _, err := svc.BootstrapFromConfig(cfg); err != nil { + t.Fatal(err) + } + channels, err := st.ListChannelConfigs() + if err != nil { + t.Fatal(err) + } + if len(channels) != 1 { + t.Fatalf("channels=%v", channels) + } + if deleted { + err = st.DeleteChannelConfig(channels[0].ID) + } else { + record := channels[0] + record.Enabled = false + _, err = st.UpsertChannelConfig(record) + } + if err != nil { + t.Fatal(err) + } + if imported, err := svc.BootstrapFromConfig(cfg); err != nil || imported != 0 { + t.Fatalf("bootstrap=%d %v", imported, err) + } + runtimeCfg, source, err := routerConfigFromChannels(cfg, svc) + if err != nil { + t.Fatal(err) + } + if source != "database" || len(runtimeCfg.EffectiveUpstreams()) != 0 { + t.Fatalf("source=%s targets=%v", source, runtimeCfg.EffectiveUpstreams()) + } + }) + } +} diff --git a/cmd/server/management.go b/cmd/server/management.go index d56121c..c9044fa 100644 --- a/cmd/server/management.go +++ b/cmd/server/management.go @@ -67,7 +67,7 @@ func newManagementMuxWithFunctionExecutorManager( } monitor.RegisterRoutes(mux, traceStore, monitor.RouteOptions{ Router: rtr, - ChannelService: channel.NewService(traceStore), + ChannelService: channel.NewService(traceStore).WithReadOnly(configHasExplicitCredentials(cfg)), AuthVerifier: verifier, MonitorAuthVerifier: monitorVerifier, MonitorJWT: monitorJWT, diff --git a/cmd/server/serve.go b/cmd/server/serve.go index 31243a6..677eb66 100644 --- a/cmd/server/serve.go +++ b/cmd/server/serve.go @@ -366,7 +366,11 @@ func routerConfigFromChannels(cfg *config.Config, channelService *channel.Servic if err != nil { return nil, "", err } - if len(targets) == 0 { + initialized, err := channelService.HasConfiguration() + if err != nil { + return nil, "", err + } + if !initialized { return cfg, "yaml", nil } routerCfg := *cfg diff --git a/docs/MONITOR_GUIDE.md b/docs/MONITOR_GUIDE.md index e98288f..6662e53 100644 --- a/docs/MONITOR_GUIDE.md +++ b/docs/MONITOR_GUIDE.md @@ -93,15 +93,27 @@ Monitor 使用两类数据: - 设置 provider preset、base URL、API key、headers、routing 字段。 - 设置 provider API surface:`api_type`、`mode`,以及 Responses、Chat Completions、tool calling、models 等 capability 开关;这些字段会写入 channel store,并在运行时还原为 upstream routing target。 - 创建前可用 Detect provider 做临时探测,不落库返回 API surface 建议;需要采用建议时,使用 Apply suggestions 显式写入表单。 -- 创建前需用 Validate setup 调用 provider setup validate:它会组合 base URL、API key、provider preset、model discovery 和 capability 字段做一次探测并把归一化配置写回表单,但不会落库;dialog 会展示 normalized config、probe 和 redacted secret state,字段变更会清空旧验证结果;Create provider 才通过 setup apply 写入 channel store;若 probe 未检测成功,需显式提供 `api_type` 与 `protocol_family`。 +- 创建前可用 Validate setup 调用 provider setup validate:它会组合 base URL、API key、provider preset、model discovery 和 capability 字段做一次探测并把归一化配置写回表单,但不会落库;dialog 会展示 normalized config、probe 和 redacted secret state,字段变更会清空旧验证结果;Create provider 通过 setup apply 写入 channel store;显式提供协议信息时也允许直接创建,若 probe 未检测成功,需显式提供 `api_type` 与 `protocol_family`。 - setup validate/apply 响应不会回显 API key;只返回 `api_key_hint`、secret storage mode 和 redacted header 状态。 - 探测模型,并查看 provider detection 建议;需要写回建议时,使用 Apply suggestions 显式更新 channel 配置。 -- 在 Channels/Providers 列表页使用 Batch probe and apply 先运行只读 `POST /api/provider-probe/report` 预览,再对 detected 且有可补字段的 provider 执行批量 Apply detected suggestions;Monitor 不展示或传递 API key。批量应用只填缺失的 `api_type`、`protocol_family` 和未设置 capability,不覆盖显式配置,也不覆盖显式 `false` capability。 +- 批量探测 API 支持先通过只读 `POST /api/provider-probe/report` 预览,再通过 `POST /api/provider-probe/report/apply` 对 detected 且有可补字段的 provider 批量应用建议;单个 provider 的探测入口在卡片上。批量应用只填缺失的 `api_type`、`protocol_family` 和未设置 capability,不覆盖显式配置,也不覆盖显式 `false` capability。 - 启停渠道。 - 启停单个模型。 - 查看渠道用量、token、失败和 probe 结果。 -长期渠道配置保存在 application store;Postgres 部署使用版本化迁移,SQLite 仍作为本地 fallback。YAML 只作为启动和首次 bootstrap 输入。 +长期渠道配置保存在 application store;Postgres 部署使用版本化迁移,SQLite 仍作为本地 fallback。通常 YAML 只作为启动和首次 bootstrap 输入;首次初始化会留下持久化标记,即使后来停用或删除全部渠道,重启也不会重新导入 YAML。使用显式 `credentials` 列表的 YAML 配置仍由 YAML 管理,此模式下 Monitor 的渠道、模型和别名写操作返回 409,避免数据库操作替换 YAML 中的凭据路由。 + +### 上游和模型的启停语义 + +- 创建上游只保存连接配置。创建成功后进入详情页,发现或手动添加模型,再选择启用。没有启用模型且未允许未知模型时,不接受命名模型请求。 +- 上游启用表示允许参与新请求路由;停用会移除该上游的路由资格,但保留各模型的选择。已在途请求不会因此被取消。 +- 模型启用只作用于当前上游,不会启动或停止远端模型进程,也不会改变同名模型在其他上游的配置。 +- 模型停用是明确拒绝:自动发现、未知模型放行和 fallback 策略均不能重新放行该上游上的已停用模型。删除模型则是移除配置,不等同于停用;允许未知模型时,删除后的模型可能再次作为未知模型被调用。 +- 模型发现默认不启用新模型,保留已有模型的启停选择和能力配置。API 调用方可显式传入 `enable_discovered: true` 启用本次新发现的模型。数据库管理的运行时只从已启用配置建立模型目录,不通过周期刷新扩大准入范围。 +- 配置开关与健康状态相互独立。上游停用时,模型行仍保留“已启用”的选择,同时提示路由被上游开关阻止;健康检查和熔断仍可能使已启用模型暂时不可用。 +- 仅来自历史请求的模型行不提供启停开关;需要管理时先手动添加模型。发现后停用的模型不再被标为“新模型”。 +- 渠道、模型、模型别名及批量修改通过同一数据库事务完成。运行时配置准备成功后才提交并切换路由;校验、批量更新或提交失败时保留原配置和原路由。返回失败的模型探测仍保留诊断记录。 + ### Routing diff --git a/internal/channel/service.go b/internal/channel/service.go index 4757227..fa958c1 100644 --- a/internal/channel/service.go +++ b/internal/channel/service.go @@ -20,6 +20,8 @@ import ( ) type Store interface { + LoadAppSettingJSON(context.Context, string, any) (bool, error) + SaveAppSettingJSON(context.Context, string, any) error ListChannelConfigs() ([]store.ChannelConfigRecord, error) GetChannelConfig(channelID string) (store.ChannelConfigRecord, error) UpsertChannelConfig(store.ChannelConfigRecord) (store.ChannelConfigRecord, error) @@ -36,6 +38,7 @@ type Store interface { type Service struct { store Store httpClient *http.Client + readOnly bool } func NewService(st Store) *Service { @@ -47,6 +50,39 @@ func (s *Service) WithHTTPClient(client *http.Client) *Service { return s } +// WithStore preserves probe options when applying a configuration transaction. +func (s *Service) WithStore(st Store) *Service { + next := *s + next.store = st + return &next +} + +func (s *Service) WithReadOnly(readOnly bool) *Service { + s.readOnly = readOnly + return s +} + +func (s *Service) ReadOnly() bool { return s != nil && s.readOnly } + +const configurationInitializedKey = "channels.initialized" + +// HasConfiguration distinguishes an intentionally empty database from first startup. +func (s *Service) HasConfiguration() (bool, error) { + var initialized bool + if _, err := s.store.LoadAppSettingJSON(context.Background(), configurationInitializedKey, &initialized); err != nil { + return false, err + } + if initialized { + return true, nil + } + channels, err := s.store.ListChannelConfigs() + return len(channels) > 0, err +} + +func (s *Service) MarkConfigurationInitialized() error { + return s.store.SaveAppSettingJSON(context.Background(), configurationInitializedKey, true) +} + type ProbeResult struct { ChannelID string Status string @@ -89,12 +125,12 @@ func (s *Service) BootstrapFromConfig(cfg *config.Config) (int, error) { if s == nil || s.store == nil { return 0, fmt.Errorf("channel service store is required") } - existing, err := s.store.ListChannelConfigs() + initialized, err := s.HasConfiguration() if err != nil { return 0, err } - if len(existing) > 0 { - return 0, nil + if initialized { + return 0, s.MarkConfigurationInitialized() } targets := configuredUpstreams(cfg) @@ -114,6 +150,10 @@ func (s *Service) BootstrapFromConfig(cfg *config.Config) (int, error) { if target.Enabled != nil { enabled = *target.Enabled } + allowUnknown := false + if target.AllowUnknownModels != nil { + allowUnknown = *target.AllowUnknownModels + } headersJSON := "{}" if len(target.Upstream.Headers) > 0 { data, err := json.Marshal(target.Upstream.Headers) @@ -127,29 +167,30 @@ func (s *Service) BootstrapFromConfig(cfg *config.Config) (int, error) { return imported, err } _, err = s.store.UpsertChannelConfig(store.ChannelConfigRecord{ - ID: channelID, - Name: defaultChannelName(channelID, target.Upstream.ProviderPreset), - Source: "bootstrap", - BaseURL: target.Upstream.BaseURL, - ProviderPreset: target.Upstream.ProviderPreset, - APIType: target.Upstream.APIType, - Mode: target.Upstream.Mode, - CapabilitiesJSON: capabilitiesJSON, - ProtocolFamily: target.Upstream.ProtocolFamily, - RoutingProfile: target.Upstream.RoutingProfile, - APIVersion: target.Upstream.APIVersion, - Deployment: target.Upstream.Deployment, - Project: target.Upstream.Project, - Location: target.Upstream.Location, - ModelResource: target.Upstream.ModelResource, - APIKeyCiphertext: []byte(target.Upstream.ApiKey), - APIKeyHint: secretHint(target.Upstream.ApiKey), - HeadersJSON: headersJSON, - Enabled: enabled, - Priority: target.Priority, - Weight: target.Weight, - CapacityHint: target.CapacityHint, - ModelDiscovery: target.ModelDiscovery, + ID: channelID, + Name: defaultChannelName(channelID, target.Upstream.ProviderPreset), + Source: "bootstrap", + BaseURL: target.Upstream.BaseURL, + ProviderPreset: target.Upstream.ProviderPreset, + APIType: target.Upstream.APIType, + Mode: target.Upstream.Mode, + CapabilitiesJSON: capabilitiesJSON, + ProtocolFamily: target.Upstream.ProtocolFamily, + RoutingProfile: target.Upstream.RoutingProfile, + APIVersion: target.Upstream.APIVersion, + Deployment: target.Upstream.Deployment, + Project: target.Upstream.Project, + Location: target.Upstream.Location, + ModelResource: target.Upstream.ModelResource, + APIKeyCiphertext: []byte(target.Upstream.ApiKey), + APIKeyHint: secretHint(target.Upstream.ApiKey), + HeadersJSON: headersJSON, + Enabled: enabled, + Priority: target.Priority, + Weight: target.Weight, + CapacityHint: target.CapacityHint, + ModelDiscovery: target.ModelDiscovery, + AllowUnknownModels: allowUnknown, }) if err != nil { return imported, err @@ -171,7 +212,7 @@ func (s *Service) BootstrapFromConfig(cfg *config.Config) (int, error) { } imported++ } - return imported, nil + return imported, s.MarkConfigurationInitialized() } func configuredUpstreams(cfg *config.Config) []config.UpstreamTargetConfig { @@ -213,10 +254,20 @@ func (s *Service) RuntimeTargets() ([]config.UpstreamTargetConfig, error) { if !channel.Enabled { continue } - models, err := s.store.ListChannelModels(channel.ID, true) + models, err := s.store.ListChannelModels(channel.ID, false) if err != nil { return nil, err } + var enabledModels []store.ChannelModelRecord + var disabledModels []string + for _, model := range models { + if model.Enabled { + enabledModels = append(enabledModels, model) + } else { + disabledModels = append(disabledModels, model.Model) + } + } + models = enabledModels headers := map[string]string{} if strings.TrimSpace(channel.HeadersJSON) != "" { if err := json.Unmarshal([]byte(channel.HeadersJSON), &headers); err != nil { @@ -238,6 +289,7 @@ func (s *Service) RuntimeTargets() ([]config.UpstreamTargetConfig, error) { StaticModels: channelModelNamesWithAliases(models, aliases, channel.ID), ModelAliases: channelModelAliases(models, aliases, channel.ID), ConfiguredModelsOnly: true, + DisabledModels: disabledModels, AllowUnknownModels: &channel.AllowUnknownModels, Upstream: config.UpstreamConfig{ BaseURL: channel.BaseURL, @@ -533,16 +585,12 @@ func (s *Service) mergeDiscoveredChannelModels(channelID string, discovered []st } record.Enabled = enableNew if previous, ok := existingByModel[model]; ok { - record.Enabled = previous.Enabled - if strings.TrimSpace(previous.Source) != "" && previous.Source != "discovered" { - record.Source = previous.Source - } - if strings.TrimSpace(previous.DisplayName) != "" { - record.DisplayName = previous.DisplayName - } - if !previous.FirstSeenAt.IsZero() { - record.FirstSeenAt = previous.FirstSeenAt - } + // Re-discovery updates observation timestamps, not operator choices + // or an adopted model profile. + lastSeen, lastProbe := record.LastSeenAt, record.LastProbeAt + record = previous + record.LastSeenAt = lastSeen + record.LastProbeAt = lastProbe } saved, err := s.store.UpsertChannelModel(channelID, record) if err != nil { @@ -555,7 +603,7 @@ func (s *Service) mergeDiscoveredChannelModels(channelID string, discovered []st func enableDiscoveredByDefault(options ProbeOptions) bool { if options.EnableDiscovered == nil { - return true + return false } return *options.EnableDiscovered } diff --git a/internal/channel/service_test.go b/internal/channel/service_test.go index 502cb9c..1dfc693 100644 --- a/internal/channel/service_test.go +++ b/internal/channel/service_test.go @@ -186,7 +186,8 @@ func TestProbeDiscoversModelsAndUpdatesCatalogs(t *testing.T) { t.Fatalf("UpsertChannelConfig() error = %v", err) } - result, err := NewService(st).Probe("probe-channel") + enable := true + result, err := NewService(st).ProbeWithOptions("probe-channel", ProbeOptions{EnableDiscovered: &enable}) if err != nil { t.Fatalf("Probe() error = %v", err) } @@ -743,3 +744,42 @@ func TestRuntimeTargetsProjectsChannelScopedModelAliases(t *testing.T) { t.Fatalf("secondary StaticModels = %#v", modelsByTarget["secondary"]) } } + +func TestProbeDefaultsToDisabledAndPreservesManualChoices(t *testing.T) { + upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"data":[{"id":"new"},{"id":"enabled"},{"id":"disabled"}]}`)) + })) + defer upstreamServer.Close() + st, err := store.New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer st.Close() + if _, err := st.UpsertChannelConfig(store.ChannelConfigRecord{ID: "p", Name: "P", Enabled: true, BaseURL: upstreamServer.URL + "/v1", ProviderPreset: "openai"}); err != nil { + t.Fatal(err) + } + contextWindow := 128000 + for _, model := range []store.ChannelModelRecord{{Model: "enabled", Enabled: true, Source: "manual", ContextWindow: &contextWindow, ProfileAdoptionStatus: "adopted"}, {Model: "disabled", Enabled: false, Source: "discovered"}} { + if _, err := st.UpsertChannelModel("p", model); err != nil { + t.Fatal(err) + } + } + if _, err := NewService(st).Probe("p"); err != nil { + t.Fatal(err) + } + models, err := st.ListChannelModels("p", false) + if err != nil { + t.Fatal(err) + } + if len(models) != 3 { + t.Fatalf("models=%v", models) + } + for _, model := range models { + if model.Model == "enabled" && (model.ContextWindow == nil || *model.ContextWindow != contextWindow || model.ProfileAdoptionStatus != "adopted") { + t.Fatalf("discovery erased model profile: %+v", model) + } + if model.Enabled != (model.Model == "enabled") { + t.Fatalf("discovery changed authorization: %+v", model) + } + } +} diff --git a/internal/config/config.go b/internal/config/config.go index ba06e72..83eb125 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -105,6 +105,7 @@ type UpstreamTargetConfig struct { StaticModels []string `yaml:"static_models"` ModelAliases map[string]string `yaml:"-"` ConfiguredModelsOnly bool `yaml:"-"` + DisabledModels []string `yaml:"-"` AllowUnknownModels *bool `yaml:"allow_unknown_models"` Upstream UpstreamConfig `yaml:"upstream"` Credentials []CredentialConfig `yaml:"credentials"` diff --git a/internal/monitor/configuration.go b/internal/monitor/configuration.go new file mode 100644 index 0000000..82e9f32 --- /dev/null +++ b/internal/monitor/configuration.go @@ -0,0 +1,112 @@ +package monitor + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "strings" + + "github.com/kingfs/llm-tracelab/internal/channel" + "github.com/kingfs/llm-tracelab/internal/config" + "github.com/kingfs/llm-tracelab/internal/router" + "github.com/kingfs/llm-tracelab/internal/store" +) + +type configurationHandlerFactory func(*store.Store, *router.Router, *channel.Service) http.HandlerFunc + +// Buffer the small JSON response until both persistence and runtime publication +// succeed. Existing handlers operate on the transaction, with reload deferred. +func configurationAPIHandler(st *store.Store, rtr *router.Router, svc *channel.Service, factory configurationHandlerFactory) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if st == nil || r.Method == http.MethodGet || r.Method == http.MethodHead || strings.HasSuffix(r.URL.Path, "/validate") { + factory(st, rtr, svc)(w, r) + return + } + svc := effectiveChannelService(st, svc) + if svc.ReadOnly() { + writeJSON(w, http.StatusConflict, map[string]string{"error": "upstreams are managed by YAML credentials; edit YAML and restart instead"}) + return + } + response := &configurationResponse{header: make(http.Header)} + rejected := errors.New("configuration request rejected") + err := st.ConfigurationTransaction(r.Context(), func(tx *store.Store, commit func() error) error { + txService := svc.WithStore(tx) + factory(tx, nil, txService)(response, r) + if response.status >= http.StatusBadRequest { + // Failed discovery keeps its diagnostic run, but never changes routing. + if response.status == http.StatusBadGateway && strings.HasSuffix(r.URL.Path, "/probe") { + var result channelProbeResponse + if json.Unmarshal(response.body.Bytes(), &result) == nil && result.Status == "failed" { + return commit() + } + } + return rejected + } + if err := txService.MarkConfigurationInitialized(); err != nil { + return err + } + targets, err := txService.RuntimeTargets() + if err != nil { + return err + } + if rtr == nil { + prepared, err := router.New(&config.Config{}, nil) + if err != nil { + return err + } + return prepared.ReloadWithCommit(targets, tx, commit) + } + return rtr.ReloadWithCommit(targets, tx, commit) + }) + if err != nil && !errors.Is(err, rejected) { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "configuration not applied: " + err.Error()}) + return + } + for key, values := range response.header { + w.Header()[key] = values + } + if response.status != 0 { + w.WriteHeader(response.status) + } + _, _ = w.Write(response.body.Bytes()) + } +} + +type configurationResponse struct { + header http.Header + body bytes.Buffer + status int +} + +func (w *configurationResponse) Header() http.Header { return w.header } +func (w *configurationResponse) WriteHeader(status int) { + if w.status == 0 { + w.status = status + } +} +func (w *configurationResponse) Write(p []byte) (int, error) { + if w.status == 0 { + w.status = http.StatusOK + } + return w.body.Write(p) +} +func channelListCreateAPIHandler(st *store.Store, rtr *router.Router, svc *channel.Service) http.HandlerFunc { + return configurationAPIHandler(st, rtr, svc, channelListCreateAPIHandlerUncommitted) +} + +func channelDetailAPIHandler(st *store.Store, rtr *router.Router, svc *channel.Service) http.HandlerFunc { + return configurationAPIHandler(st, rtr, svc, channelDetailAPIHandlerUncommitted) +} + +func providerSetupAPIHandler(st *store.Store, rtr *router.Router, svc *channel.Service) http.HandlerFunc { + return configurationAPIHandler(st, rtr, svc, providerSetupAPIHandlerUncommitted) +} + +func providerProbeReportApplyAPIHandler(st *store.Store, rtr *router.Router, svc *channel.Service) http.HandlerFunc { + return configurationAPIHandler(st, rtr, svc, providerProbeReportApplyAPIHandlerUncommitted) +} + +func routerReloadAPIHandler(st *store.Store, rtr *router.Router, svc *channel.Service) http.HandlerFunc { + return configurationAPIHandler(st, rtr, svc, routerReloadAPIHandlerUncommitted) +} diff --git a/internal/monitor/configuration_test.go b/internal/monitor/configuration_test.go new file mode 100644 index 0000000..4ba2d0d --- /dev/null +++ b/internal/monitor/configuration_test.go @@ -0,0 +1,200 @@ +package monitor + +import ( + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + + "github.com/kingfs/llm-tracelab/internal/channel" + "github.com/kingfs/llm-tracelab/internal/config" + "github.com/kingfs/llm-tracelab/internal/router" + "github.com/kingfs/llm-tracelab/internal/store" +) + +func configurationFixture(t *testing.T) (*store.Store, *router.Router, *channel.Service) { + t.Helper() + st, err := store.New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = st.Close() }) + _, err = st.UpsertChannelConfig(store.ChannelConfigRecord{ID: "primary", Name: "Primary", Enabled: true, BaseURL: "https://example.invalid/v1", ProviderPreset: "openai", ModelDiscovery: "list_models", AllowUnknownModels: true}) + if err != nil { + t.Fatal(err) + } + for _, model := range []string{"one", "two"} { + if _, err := st.UpsertChannelModel("primary", store.ChannelModelRecord{Model: model, Enabled: true, Source: "manual"}); err != nil { + t.Fatal(err) + } + } + svc := channel.NewService(st) + rtr, err := router.New(&config.Config{}, st) + if err != nil { + t.Fatal(err) + } + targets, err := svc.RuntimeTargets() + if err != nil { + t.Fatal(err) + } + if err := rtr.Reload(targets); err != nil { + t.Fatal(err) + } + return st, rtr, svc +} + +func configurationRequest(h http.Handler, method, path, body string) *httptest.ResponseRecorder { + response := httptest.NewRecorder() + h.ServeHTTP(response, httptest.NewRequest(method, path, strings.NewReader(body))) + return response +} + +func TestConfigurationMutationRollsBackOnInvalidRouter(t *testing.T) { + st, rtr, svc := configurationFixture(t) + response := configurationRequest(channelDetailAPIHandler(st, rtr, svc), http.MethodPatch, "/api/channels/primary", `{"base_url":"://invalid"}`) + if response.Code != http.StatusInternalServerError { + t.Fatalf("status=%d body=%s", response.Code, response.Body) + } + saved, err := st.GetChannelConfig("primary") + if err != nil { + t.Fatal(err) + } + if saved.BaseURL != "https://example.invalid/v1" { + t.Fatalf("failed update persisted: %s", saved.BaseURL) + } + if got := rtr.AggregatedModels(); len(got) != 2 { + t.Fatalf("runtime changed: %v", got) + } +} + +func TestConfigurationBatchFailureIsAtomic(t *testing.T) { + st, rtr, svc := configurationFixture(t) + response := configurationRequest(channelDetailAPIHandler(st, rtr, svc), http.MethodPatch, "/api/channels/primary/models/batch", `{"models":["one","missing"],"enabled":false}`) + if response.Code != http.StatusBadRequest { + t.Fatalf("status=%d body=%s", response.Code, response.Body) + } + models, err := st.ListChannelModels("primary", true) + if err != nil { + t.Fatal(err) + } + if len(models) != 2 { + t.Fatalf("partial batch persisted: %v", models) + } + if got := rtr.AggregatedModels(); len(got) != 2 { + t.Fatalf("runtime changed: %v", got) + } +} + +func TestConfigurationDisableAndReenablePreservesModelChoices(t *testing.T) { + st, rtr, svc := configurationFixture(t) + h := channelDetailAPIHandler(st, rtr, svc) + for _, op := range []struct{ path, body string }{{"/api/channels/primary/models/one", `{"enabled":false}`}, {"/api/channels/primary", `{"enabled":false}`}} { + rr := configurationRequest(h, http.MethodPatch, op.path, op.body) + if rr.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rr.Code, rr.Body) + } + } + if len(rtr.Targets()) != 0 { + t.Fatal("disabled provider remains in runtime") + } + rr := configurationRequest(h, http.MethodPatch, "/api/channels/primary", `{"enabled":true}`) + if rr.Code != http.StatusOK { + t.Fatal(rr.Body.String()) + } + if got := rtr.AggregatedModels(); len(got) != 1 || got[0] != "two" { + t.Fatalf("model choices changed: %v", got) + } + if _, err := rtr.Select(httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{"model":"one"}`))); err == nil { + t.Fatal("disabled model admitted through unknown-model fallback") + } +} + +func TestConfigurationConcurrentMutationsKeepLatestSnapshot(t *testing.T) { + st, rtr, svc := configurationFixture(t) + h := channelDetailAPIHandler(st, rtr, svc) + var wg sync.WaitGroup + for _, model := range []string{"one", "two"} { + wg.Add(1) + go func(model string) { + defer wg.Done() + rr := configurationRequest(h, http.MethodPatch, "/api/channels/primary/models/"+model, `{"enabled":false}`) + if rr.Code != http.StatusOK { + t.Errorf("status=%d body=%s", rr.Code, rr.Body) + } + }(model) + } + wg.Wait() + if got := rtr.AggregatedModels(); len(got) != 0 { + t.Fatalf("stale snapshot: %v", got) + } +} + +func TestYAMLCredentialsRejectDatabaseMutation(t *testing.T) { + st, rtr, svc := configurationFixture(t) + rr := configurationRequest(channelDetailAPIHandler(st, rtr, svc.WithReadOnly(true)), http.MethodPatch, "/api/channels/primary", `{"enabled":false}`) + if rr.Code != http.StatusConflict { + t.Fatalf("status=%d body=%s", rr.Code, rr.Body) + } + record, err := st.GetChannelConfig("primary") + if err != nil { + t.Fatal(err) + } + if !record.Enabled || len(rtr.Targets()) != 1 { + t.Fatal("YAML runtime changed through database management") + } +} + +func TestModelAliasMutationPublishesRuntimeAndRollsBack(t *testing.T) { + st, rtr, svc := configurationFixture(t) + mux := http.NewServeMux() + RegisterRoutes(mux, st, RouteOptions{Router: rtr, ChannelService: svc}) + rr := configurationRequest(mux, http.MethodPost, "/api/model-aliases", `{"id":"alias-one","alias":"friendly","target_model":"one","channel_id":"primary","enabled":true}`) + if rr.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rr.Code, rr.Body) + } + if rtr.Targets()[0].ResolveModelAlias("friendly") != "one" { + t.Fatal("alias was saved without publishing runtime") + } + // A bad active configuration forces preparation failure after the raw SQL write. + record, err := st.GetChannelConfig("primary") + if err != nil { + t.Fatal(err) + } + record.BaseURL = "://invalid" + if _, err := st.UpsertChannelConfig(record); err != nil { + t.Fatal(err) + } + rr = configurationRequest(mux, http.MethodPatch, "/api/model-aliases/alias-one", `{"enabled":false}`) + if rr.Code != http.StatusInternalServerError { + t.Fatalf("status=%d body=%s", rr.Code, rr.Body) + } + alias, err := st.GetModelAlias("alias-one") + if err != nil { + t.Fatal(err) + } + if !alias.Enabled || rtr.Targets()[0].ResolveModelAlias("friendly") != "one" { + t.Fatal("failed alias update changed database or runtime") + } +} + +func TestDeleteChannelRollbackRestoresItsModels(t *testing.T) { + st, rtr, svc := configurationFixture(t) + if _, err := st.UpsertChannelConfig(store.ChannelConfigRecord{ID: "broken", Name: "Broken", Enabled: true, BaseURL: "://invalid"}); err != nil { + t.Fatal(err) + } + rr := configurationRequest(channelDetailAPIHandler(st, rtr, svc), http.MethodDelete, "/api/channels/primary", "") + if rr.Code != http.StatusInternalServerError { + t.Fatalf("status=%d body=%s", rr.Code, rr.Body) + } + if _, err := st.GetChannelConfig("primary"); err != nil { + t.Fatal("nested delete transaction escaped rollback:", err) + } + models, err := st.ListChannelModels("primary", true) + if err != nil { + t.Fatal(err) + } + if len(models) != 2 { + t.Fatalf("models not restored: %v", models) + } +} diff --git a/internal/monitor/server.go b/internal/monitor/server.go index 7a738a0..ca2077e 100644 --- a/internal/monitor/server.go +++ b/internal/monitor/server.go @@ -1509,8 +1509,12 @@ func RegisterRoutes(mux *http.ServeMux, st *store.Store, opts ...RouteOptions) { mux.HandleFunc("/api/routing/summary", monitorAuthRequired(routingSummaryAPIHandler(st), monitorVerifier)) mux.HandleFunc("/api/settings/routing", monitorAuthRequired(routingSettingsAPIHandler(st), monitorVerifier)) mux.HandleFunc("/api/model-aliases/validate", monitorAuthRequired(modelAliasValidateAPIHandler(st), monitorVerifier)) - mux.HandleFunc("/api/model-aliases", monitorAuthRequired(modelAliasListCreateAPIHandler(st), monitorVerifier)) - mux.HandleFunc("/api/model-aliases/", monitorAuthRequired(modelAliasDetailAPIHandler(st), monitorVerifier)) + mux.HandleFunc("/api/model-aliases", monitorAuthRequired(configurationAPIHandler(st, opt.Router, opt.ChannelService, func(st *store.Store, _ *router.Router, _ *channel.Service) http.HandlerFunc { + return modelAliasListCreateAPIHandler(st) + }), monitorVerifier)) + mux.HandleFunc("/api/model-aliases/", monitorAuthRequired(configurationAPIHandler(st, opt.Router, opt.ChannelService, func(st *store.Store, _ *router.Router, _ *channel.Service) http.HandlerFunc { + return modelAliasDetailAPIHandler(st) + }), monitorVerifier)) mux.HandleFunc("/api/traces", monitorAuthRequired(listAPIHandler(st), monitorVerifier)) mux.HandleFunc("/api/traces/", monitorAuthRequired(traceAPIHandler(st, opt.Router), monitorVerifier)) mux.HandleFunc("/api/sessions", monitorAuthRequired(sessionListAPIHandler(st), monitorVerifier)) @@ -2530,7 +2534,7 @@ func providerProbeAPIHandler() http.HandlerFunc { } } -func providerSetupAPIHandler(st *store.Store, rtr *router.Router, channelService *channel.Service) http.HandlerFunc { +func providerSetupAPIHandlerUncommitted(st *store.Store, rtr *router.Router, channelService *channel.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.NotFound(w, r) @@ -2758,7 +2762,7 @@ func providerProbeReportAPIHandler(st *store.Store, channelService *channel.Serv } } -func providerProbeReportApplyAPIHandler(st *store.Store, rtr *router.Router, channelService *channel.Service) http.HandlerFunc { +func providerProbeReportApplyAPIHandlerUncommitted(st *store.Store, rtr *router.Router, channelService *channel.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.NotFound(w, r) @@ -2895,7 +2899,7 @@ func appHandler() http.Handler { }) } -func channelListCreateAPIHandler(st *store.Store, rtr *router.Router, channelService *channel.Service) http.HandlerFunc { +func channelListCreateAPIHandlerUncommitted(st *store.Store, rtr *router.Router, channelService *channel.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if st == nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "store not configured"}) @@ -2945,7 +2949,7 @@ func channelListCreateAPIHandler(st *store.Store, rtr *router.Router, channelSer } } -func channelDetailAPIHandler(st *store.Store, rtr *router.Router, channelService *channel.Service) http.HandlerFunc { +func channelDetailAPIHandlerUncommitted(st *store.Store, rtr *router.Router, channelService *channel.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if st == nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "store not configured"}) @@ -3863,7 +3867,7 @@ func parseBoolQuery(value string, fallback bool) bool { return parsed } -func routerReloadAPIHandler(st *store.Store, rtr *router.Router, channelService *channel.Service) http.HandlerFunc { +func routerReloadAPIHandlerUncommitted(st *store.Store, rtr *router.Router, channelService *channel.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.NotFound(w, r) @@ -3950,7 +3954,7 @@ func channelRecordFromRequest(req channelUpsertRequest, existing store.ChannelCo } if req.Enabled != nil { record.Enabled = *req.Enabled - } else if record.ID == "" { + } else if existing.ID == "" { record.Enabled = true } if req.Priority != nil { diff --git a/internal/monitor/server_test.go b/internal/monitor/server_test.go index bf7dd7e..0fe3c00 100644 --- a/internal/monitor/server_test.go +++ b/internal/monitor/server_test.go @@ -2017,7 +2017,7 @@ func TestChannelManagementAPI(t *testing.T) { t.Fatalf("created.Capabilities.Responses = %#v", created.Capabilities.Responses) } - req = httptest.NewRequest(http.MethodPost, "/api/channels/openai-primary/probe", nil) + req = httptest.NewRequest(http.MethodPost, "/api/channels/openai-primary/probe", strings.NewReader(`{"enable_discovered":true}`)) rr = httptest.NewRecorder() channelDetailAPIHandler(st, nil, nil).ServeHTTP(rr, req) if rr.Code != http.StatusOK { @@ -2602,7 +2602,7 @@ func TestProviderSetupSuggestionsDoNotOverrideExplicitFields(t *testing.T) { "id":"explicit-provider", "name":"Explicit Provider", "base_url":"` + upstreamServer.URL + `/v1", - "provider_preset":"custom", + "provider_preset":"anthropic", "api_type":"messages", "protocol_family":"anthropic_messages", "capabilities":{"chat_completions":false}, diff --git a/internal/monitor/ui/dist/assets/index-BJxNZE_g.js b/internal/monitor/ui/dist/assets/index-Csr12ZyE.js similarity index 63% rename from internal/monitor/ui/dist/assets/index-BJxNZE_g.js rename to internal/monitor/ui/dist/assets/index-Csr12ZyE.js index 546ef68..8a2c144 100644 --- a/internal/monitor/ui/dist/assets/index-BJxNZE_g.js +++ b/internal/monitor/ui/dist/assets/index-Csr12ZyE.js @@ -1,4 +1,4 @@ -var sL=Object.defineProperty;var oL=(e,t,n)=>t in e?sL(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var hv=(e,t,n)=>oL(e,typeof t!="symbol"?t+"":t,n);function cL(e,t){for(var n=0;nr[l]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const o of l)if(o.type==="childList")for(const u of o.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&r(u)}).observe(document,{childList:!0,subtree:!0});function n(l){const o={};return l.integrity&&(o.integrity=l.integrity),l.referrerPolicy&&(o.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?o.credentials="include":l.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(l){if(l.ep)return;l.ep=!0;const o=n(l);fetch(l.href,o)}})();var u1=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Ra(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var pv={exports:{}},Wo={};/** +var sL=Object.defineProperty;var oL=(e,t,n)=>t in e?sL(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var hv=(e,t,n)=>oL(e,typeof t!="symbol"?t+"":t,n);function cL(e,t){for(var n=0;nr[l]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const o of l)if(o.type==="childList")for(const u of o.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&r(u)}).observe(document,{childList:!0,subtree:!0});function n(l){const o={};return l.integrity&&(o.integrity=l.integrity),l.referrerPolicy&&(o.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?o.credentials="include":l.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(l){if(l.ep)return;l.ep=!0;const o=n(l);fetch(l.href,o)}})();var d1=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Ma(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var pv={exports:{}},Wo={};/** * @license React * react-jsx-runtime.production.js * @@ -6,7 +6,7 @@ var sL=Object.defineProperty;var oL=(e,t,n)=>t in e?sL(e,t,{enumerable:!0,config * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var d1;function uL(){if(d1)return Wo;d1=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function n(r,l,o){var u=null;if(o!==void 0&&(u=""+o),l.key!==void 0&&(u=""+l.key),"key"in l){o={};for(var d in l)d!=="key"&&(o[d]=l[d])}else o=l;return l=o.ref,{$$typeof:e,type:r,key:u,ref:l!==void 0?l:null,props:o}}return Wo.Fragment=t,Wo.jsx=n,Wo.jsxs=n,Wo}var f1;function dL(){return f1||(f1=1,pv.exports=uL()),pv.exports}var s=dL(),mv={exports:{}},Te={};/** + */var f1;function uL(){if(f1)return Wo;f1=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function n(r,l,o){var u=null;if(o!==void 0&&(u=""+o),l.key!==void 0&&(u=""+l.key),"key"in l){o={};for(var d in l)d!=="key"&&(o[d]=l[d])}else o=l;return l=o.ref,{$$typeof:e,type:r,key:u,ref:l!==void 0?l:null,props:o}}return Wo.Fragment=t,Wo.jsx=n,Wo.jsxs=n,Wo}var h1;function dL(){return h1||(h1=1,pv.exports=uL()),pv.exports}var s=dL(),mv={exports:{}},Ce={};/** * @license React * react.production.js * @@ -14,7 +14,7 @@ var sL=Object.defineProperty;var oL=(e,t,n)=>t in e?sL(e,t,{enumerable:!0,config * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var h1;function fL(){if(h1)return Te;h1=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),l=Symbol.for("react.profiler"),o=Symbol.for("react.consumer"),u=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),h=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),v=Symbol.for("react.lazy"),m=Symbol.for("react.activity"),y=Symbol.iterator;function x(z){return z===null||typeof z!="object"?null:(z=y&&z[y]||z["@@iterator"],typeof z=="function"?z:null)}var _={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},w=Object.assign,S={};function N(z,Q,ce){this.props=z,this.context=Q,this.refs=S,this.updater=ce||_}N.prototype.isReactComponent={},N.prototype.setState=function(z,Q){if(typeof z!="object"&&typeof z!="function"&&z!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,z,Q,"setState")},N.prototype.forceUpdate=function(z){this.updater.enqueueForceUpdate(this,z,"forceUpdate")};function E(){}E.prototype=N.prototype;function T(z,Q,ce){this.props=z,this.context=Q,this.refs=S,this.updater=ce||_}var R=T.prototype=new E;R.constructor=T,w(R,N.prototype),R.isPureReactComponent=!0;var k=Array.isArray;function A(){}var P={H:null,A:null,T:null,S:null},L=Object.prototype.hasOwnProperty;function M(z,Q,ce){var oe=ce.ref;return{$$typeof:e,type:z,key:Q,ref:oe!==void 0?oe:null,props:ce}}function W(z,Q){return M(z.type,Q,z.props)}function K(z){return typeof z=="object"&&z!==null&&z.$$typeof===e}function I(z){var Q={"=":"=0",":":"=2"};return"$"+z.replace(/[=:]/g,function(ce){return Q[ce]})}var ue=/\/+/g;function pe(z,Q){return typeof z=="object"&&z!==null&&z.key!=null?I(""+z.key):Q.toString(36)}function ge(z){switch(z.status){case"fulfilled":return z.value;case"rejected":throw z.reason;default:switch(typeof z.status=="string"?z.then(A,A):(z.status="pending",z.then(function(Q){z.status==="pending"&&(z.status="fulfilled",z.value=Q)},function(Q){z.status==="pending"&&(z.status="rejected",z.reason=Q)})),z.status){case"fulfilled":return z.value;case"rejected":throw z.reason}}throw z}function $(z,Q,ce,oe,de){var Ee=typeof z;(Ee==="undefined"||Ee==="boolean")&&(z=null);var re=!1;if(z===null)re=!0;else switch(Ee){case"bigint":case"string":case"number":re=!0;break;case"object":switch(z.$$typeof){case e:case t:re=!0;break;case v:return re=z._init,$(re(z._payload),Q,ce,oe,de)}}if(re)return de=de(z),re=oe===""?"."+pe(z,0):oe,k(de)?(ce="",re!=null&&(ce=re.replace(ue,"$&/")+"/"),$(de,Q,ce,"",function(fe){return fe})):de!=null&&(K(de)&&(de=W(de,ce+(de.key==null||z&&z.key===de.key?"":(""+de.key).replace(ue,"$&/")+"/")+re)),Q.push(de)),1;re=0;var _e=oe===""?".":oe+":";if(k(z))for(var se=0;set in e?sL(e,t,{enumerable:!0,config * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var m1;function pL(){return m1||(m1=1,(function(e){function t($,X){var G=$.length;$.push(X);e:for(;0>>1,xe=$[le];if(0>>1;lel(ce,G))oel(de,ce)?($[le]=de,$[oe]=G,le=oe):($[le]=ce,$[Q]=G,le=Q);else if(oel(de,G))$[le]=de,$[oe]=G,le=oe;else break e}}return X}function l($,X){var G=$.sortIndex-X.sortIndex;return G!==0?G:$.id-X.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var u=Date,d=u.now();e.unstable_now=function(){return u.now()-d}}var h=[],p=[],v=1,m=null,y=3,x=!1,_=!1,w=!1,S=!1,N=typeof setTimeout=="function"?setTimeout:null,E=typeof clearTimeout=="function"?clearTimeout:null,T=typeof setImmediate<"u"?setImmediate:null;function R($){for(var X=n(p);X!==null;){if(X.callback===null)r(p);else if(X.startTime<=$)r(p),X.sortIndex=X.expirationTime,t(h,X);else break;X=n(p)}}function k($){if(w=!1,R($),!_)if(n(h)!==null)_=!0,A||(A=!0,I());else{var X=n(p);X!==null&&ge(k,X.startTime-$)}}var A=!1,P=-1,L=5,M=-1;function W(){return S?!0:!(e.unstable_now()-M$&&W());){var le=m.callback;if(typeof le=="function"){m.callback=null,y=m.priorityLevel;var xe=le(m.expirationTime<=$);if($=e.unstable_now(),typeof xe=="function"){m.callback=xe,R($),X=!0;break t}m===n(h)&&r(h),R($)}else r(h);m=n(h)}if(m!==null)X=!0;else{var z=n(p);z!==null&&ge(k,z.startTime-$),X=!1}}break e}finally{m=null,y=G,x=!1}X=void 0}}finally{X?I():A=!1}}}var I;if(typeof T=="function")I=function(){T(K)};else if(typeof MessageChannel<"u"){var ue=new MessageChannel,pe=ue.port2;ue.port1.onmessage=K,I=function(){pe.postMessage(null)}}else I=function(){N(K,0)};function ge($,X){P=N(function(){$(e.unstable_now())},X)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function($){$.callback=null},e.unstable_forceFrameRate=function($){0>$||125<$?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):L=0<$?Math.floor(1e3/$):5},e.unstable_getCurrentPriorityLevel=function(){return y},e.unstable_next=function($){switch(y){case 1:case 2:case 3:var X=3;break;default:X=y}var G=y;y=X;try{return $()}finally{y=G}},e.unstable_requestPaint=function(){S=!0},e.unstable_runWithPriority=function($,X){switch($){case 1:case 2:case 3:case 4:case 5:break;default:$=3}var G=y;y=$;try{return X()}finally{y=G}},e.unstable_scheduleCallback=function($,X,G){var le=e.unstable_now();switch(typeof G=="object"&&G!==null?(G=G.delay,G=typeof G=="number"&&0le?($.sortIndex=G,t(p,$),n(h)===null&&$===n(p)&&(w?(E(P),P=-1):w=!0,ge(k,G-le))):($.sortIndex=xe,t(h,$),_||x||(_=!0,A||(A=!0,I()))),$},e.unstable_shouldYield=W,e.unstable_wrapCallback=function($){var X=y;return function(){var G=y;y=X;try{return $.apply(this,arguments)}finally{y=G}}}})(yv)),yv}var v1;function mL(){return v1||(v1=1,gv.exports=pL()),gv.exports}var bv={exports:{}},un={};/** + */var v1;function pL(){return v1||(v1=1,(function(e){function t($,X){var Y=$.length;$.push(X);e:for(;0>>1,xe=$[le];if(0>>1;lel(ce,Y))oel(de,ce)?($[le]=de,$[oe]=Y,le=oe):($[le]=ce,$[Q]=Y,le=Q);else if(oel(de,Y))$[le]=de,$[oe]=Y,le=oe;else break e}}return X}function l($,X){var Y=$.sortIndex-X.sortIndex;return Y!==0?Y:$.id-X.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var u=Date,d=u.now();e.unstable_now=function(){return u.now()-d}}var h=[],p=[],v=1,m=null,y=3,x=!1,_=!1,w=!1,S=!1,N=typeof setTimeout=="function"?setTimeout:null,E=typeof clearTimeout=="function"?clearTimeout:null,A=typeof setImmediate<"u"?setImmediate:null;function P($){for(var X=n(p);X!==null;){if(X.callback===null)r(p);else if(X.startTime<=$)r(p),X.sortIndex=X.expirationTime,t(h,X);else break;X=n(p)}}function k($){if(w=!1,P($),!_)if(n(h)!==null)_=!0,C||(C=!0,I());else{var X=n(p);X!==null&&ge(k,X.startTime-$)}}var C=!1,M=-1,L=5,R=-1;function W(){return S?!0:!(e.unstable_now()-R$&&W());){var le=m.callback;if(typeof le=="function"){m.callback=null,y=m.priorityLevel;var xe=le(m.expirationTime<=$);if($=e.unstable_now(),typeof xe=="function"){m.callback=xe,P($),X=!0;break t}m===n(h)&&r(h),P($)}else r(h);m=n(h)}if(m!==null)X=!0;else{var z=n(p);z!==null&&ge(k,z.startTime-$),X=!1}}break e}finally{m=null,y=Y,x=!1}X=void 0}}finally{X?I():C=!1}}}var I;if(typeof A=="function")I=function(){A(K)};else if(typeof MessageChannel<"u"){var ue=new MessageChannel,pe=ue.port2;ue.port1.onmessage=K,I=function(){pe.postMessage(null)}}else I=function(){N(K,0)};function ge($,X){M=N(function(){$(e.unstable_now())},X)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function($){$.callback=null},e.unstable_forceFrameRate=function($){0>$||125<$?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):L=0<$?Math.floor(1e3/$):5},e.unstable_getCurrentPriorityLevel=function(){return y},e.unstable_next=function($){switch(y){case 1:case 2:case 3:var X=3;break;default:X=y}var Y=y;y=X;try{return $()}finally{y=Y}},e.unstable_requestPaint=function(){S=!0},e.unstable_runWithPriority=function($,X){switch($){case 1:case 2:case 3:case 4:case 5:break;default:$=3}var Y=y;y=$;try{return X()}finally{y=Y}},e.unstable_scheduleCallback=function($,X,Y){var le=e.unstable_now();switch(typeof Y=="object"&&Y!==null?(Y=Y.delay,Y=typeof Y=="number"&&0le?($.sortIndex=Y,t(p,$),n(h)===null&&$===n(p)&&(w?(E(M),M=-1):w=!0,ge(k,Y-le))):($.sortIndex=xe,t(h,$),_||x||(_=!0,C||(C=!0,I()))),$},e.unstable_shouldYield=W,e.unstable_wrapCallback=function($){var X=y;return function(){var Y=y;y=X;try{return $.apply(this,arguments)}finally{y=Y}}}})(yv)),yv}var g1;function mL(){return g1||(g1=1,gv.exports=pL()),gv.exports}var bv={exports:{}},un={};/** * @license React * react-dom.production.js * @@ -30,7 +30,7 @@ var sL=Object.defineProperty;var oL=(e,t,n)=>t in e?sL(e,t,{enumerable:!0,config * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var g1;function vL(){if(g1)return un;g1=1;var e=Bs();function t(h){var p="https://react.dev/errors/"+h;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),bv.exports=vL(),bv.exports}/** + */var y1;function vL(){if(y1)return un;y1=1;var e=Bs();function t(h){var p="https://react.dev/errors/"+h;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),bv.exports=vL(),bv.exports}/** * @license React * react-dom-client.production.js * @@ -38,15 +38,15 @@ var sL=Object.defineProperty;var oL=(e,t,n)=>t in e?sL(e,t,{enumerable:!0,config * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var b1;function gL(){if(b1)return Zo;b1=1;var e=mL(),t=Bs(),n=$k();function r(a){var i="https://react.dev/errors/"+a;if(1xe||(a.current=le[xe],le[xe]=null,xe--)}function ce(a,i){xe++,le[xe]=a.current,a.current=i}var oe=z(null),de=z(null),Ee=z(null),re=z(null);function _e(a,i){switch(ce(Ee,i),ce(de,a),ce(oe,null),i.nodeType){case 9:case 11:a=(a=i.documentElement)&&(a=a.namespaceURI)?Rw(a):0;break;default:if(a=i.tagName,i=i.namespaceURI)i=Rw(i),a=Mw(i,a);else switch(a){case"svg":a=1;break;case"math":a=2;break;default:a=0}}Q(oe),ce(oe,a)}function se(){Q(oe),Q(de),Q(Ee)}function fe(a){a.memoizedState!==null&&ce(re,a);var i=oe.current,c=Mw(i,a.type);i!==c&&(ce(de,a),ce(oe,c))}function Re(a){de.current===a&&(Q(oe),Q(de)),re.current===a&&(Q(re),Yo._currentValue=G)}var ie,Pt;function De(a){if(ie===void 0)try{throw Error()}catch(c){var i=c.stack.trim().match(/\n( *(at )?)/);ie=i&&i[1]||"",Pt=-1xe||(a.current=le[xe],le[xe]=null,xe--)}function ce(a,i){xe++,le[xe]=a.current,a.current=i}var oe=z(null),de=z(null),Ee=z(null),re=z(null);function _e(a,i){switch(ce(Ee,i),ce(de,a),ce(oe,null),i.nodeType){case 9:case 11:a=(a=i.documentElement)&&(a=a.namespaceURI)?Rw(a):0;break;default:if(a=i.tagName,i=i.namespaceURI)i=Rw(i),a=Dw(i,a);else switch(a){case"svg":a=1;break;case"math":a=2;break;default:a=0}}Q(oe),ce(oe,a)}function se(){Q(oe),Q(de),Q(Ee)}function fe(a){a.memoizedState!==null&&ce(re,a);var i=oe.current,c=Dw(i,a.type);i!==c&&(ce(de,a),ce(oe,c))}function Me(a){de.current===a&&(Q(oe),Q(de)),re.current===a&&(Q(re),Yo._currentValue=Y)}var ie,Pt;function De(a){if(ie===void 0)try{throw Error()}catch(c){var i=c.stack.trim().match(/\n( *(at )?)/);ie=i&&i[1]||"",Pt=-1)":-1g||D[f]!==H[g]){var J=` `+D[f].replace(" at new "," at ");return a.displayName&&J.includes("")&&(J=J.replace("",a.displayName)),J}while(1<=f&&0<=g);break}}}finally{St=!1,Error.prepareStackTrace=c}return(c=a?a.displayName||a.name:"")?De(c):""}function Qn(a,i){switch(a.tag){case 26:case 27:case 5:return De(a.type);case 16:return De("Lazy");case 13:return a.child!==i&&i!==null?De("Suspense Fallback"):De("Suspense");case 19:return De("SuspenseList");case 0:case 15:return Nt(a.type,!1);case 11:return Nt(a.type.render,!1);case 1:return Nt(a.type,!0);case 31:return De("Activity");default:return""}}function Ba(a){try{var i="",c=null;do i+=Qn(a,c),c=a,a=a.return;while(a);return i}catch(f){return` Error generating stack: `+f.message+` -`+f.stack}}var Nl=Object.prototype.hasOwnProperty,Ol=e.unstable_scheduleCallback,El=e.unstable_cancelCallback,cu=e.unstable_shouldYield,uu=e.unstable_requestPaint,sn=e.unstable_now,du=e.unstable_getCurrentPriorityLevel,no=e.unstable_ImmediatePriority,ro=e.unstable_UserBlockingPriority,ki=e.unstable_NormalPriority,fu=e.unstable_LowPriority,ao=e.unstable_IdlePriority,hu=e.log,pu=e.unstable_setDisableYieldValue,Ha=null,on=null;function xr(a){if(typeof hu=="function"&&pu(a),on&&typeof on.setStrictMode=="function")try{on.setStrictMode(Ha,a)}catch{}}var cn=Math.clz32?Math.clz32:gu,mu=Math.log,vu=Math.LN2;function gu(a){return a>>>=0,a===0?32:31-(mu(a)/vu|0)|0}var Ai=256,Ti=262144,Ci=4194304;function Ae(a){var i=a&42;if(i!==0)return i;switch(a&-a){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return a&261888;case 262144:case 524288:case 1048576:case 2097152:return a&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return a&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return a}}function pt(a,i,c){var f=a.pendingLanes;if(f===0)return 0;var g=0,b=a.suspendedLanes,O=a.pingedLanes;a=a.warmLanes;var C=f&134217727;return C!==0?(f=C&~b,f!==0?g=Ae(f):(O&=C,O!==0?g=Ae(O):c||(c=C&~a,c!==0&&(g=Ae(c))))):(C=f&~b,C!==0?g=Ae(C):O!==0?g=Ae(O):c||(c=f&~a,c!==0&&(g=Ae(c)))),g===0?0:i!==0&&i!==g&&(i&b)===0&&(b=g&-g,c=i&-i,b>=c||b===32&&(c&4194048)!==0)?i:g}function Vt(a,i){return(a.pendingLanes&~(a.suspendedLanes&~a.pingedLanes)&i)===0}function yu(a,i){switch(a){case 1:case 2:case 4:case 8:case 64:return i+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return i+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function io(){var a=Ci;return Ci<<=1,(Ci&62914560)===0&&(Ci=4194304),a}function kl(a){for(var i=[],c=0;31>c;c++)i.push(a);return i}function jr(a,i){a.pendingLanes|=i,i!==268435456&&(a.suspendedLanes=0,a.pingedLanes=0,a.warmLanes=0)}function tp(a,i,c,f,g,b){var O=a.pendingLanes;a.pendingLanes=c,a.suspendedLanes=0,a.pingedLanes=0,a.warmLanes=0,a.expiredLanes&=c,a.entangledLanes&=c,a.errorRecoveryDisabledLanes&=c,a.shellSuspendCounter=0;var C=a.entanglements,D=a.expirationTimes,H=a.hiddenUpdates;for(c=O&~c;0"u")return null;try{return a.activeElement||a.body}catch{return a.body}}var tM=/[\n"\\]/g;function er(a){return a.replace(tM,function(i){return"\\"+i.charCodeAt(0).toString(16)+" "})}function sp(a,i,c,f,g,b,O,C){a.name="",O!=null&&typeof O!="function"&&typeof O!="symbol"&&typeof O!="boolean"?a.type=O:a.removeAttribute("type"),i!=null?O==="number"?(i===0&&a.value===""||a.value!=i)&&(a.value=""+Jn(i)):a.value!==""+Jn(i)&&(a.value=""+Jn(i)):O!=="submit"&&O!=="reset"||a.removeAttribute("value"),i!=null?op(a,O,Jn(i)):c!=null?op(a,O,Jn(c)):f!=null&&a.removeAttribute("value"),g==null&&b!=null&&(a.defaultChecked=!!b),g!=null&&(a.checked=g&&typeof g!="function"&&typeof g!="symbol"),C!=null&&typeof C!="function"&&typeof C!="symbol"&&typeof C!="boolean"?a.name=""+Jn(C):a.removeAttribute("name")}function E0(a,i,c,f,g,b,O,C){if(b!=null&&typeof b!="function"&&typeof b!="symbol"&&typeof b!="boolean"&&(a.type=b),i!=null||c!=null){if(!(b!=="submit"&&b!=="reset"||i!=null)){lp(a);return}c=c!=null?""+Jn(c):"",i=i!=null?""+Jn(i):c,C||i===a.value||(a.value=i),a.defaultValue=i}f=f??g,f=typeof f!="function"&&typeof f!="symbol"&&!!f,a.checked=C?a.checked:!!f,a.defaultChecked=!!f,O!=null&&typeof O!="function"&&typeof O!="symbol"&&typeof O!="boolean"&&(a.name=O),lp(a)}function op(a,i,c){i==="number"&&ju(a.ownerDocument)===a||a.defaultValue===""+c||(a.defaultValue=""+c)}function Ml(a,i,c,f){if(a=a.options,i){i={};for(var g=0;g"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),hp=!1;if(ra)try{var co={};Object.defineProperty(co,"passive",{get:function(){hp=!0}}),window.addEventListener("test",co,co),window.removeEventListener("test",co,co)}catch{hp=!1}var Ka=null,pp=null,wu=null;function M0(){if(wu)return wu;var a,i=pp,c=i.length,f,g="value"in Ka?Ka.value:Ka.textContent,b=g.length;for(a=0;a=ho),$0=" ",I0=!1;function B0(a,i){switch(a){case"keyup":return AM.indexOf(i.keyCode)!==-1;case"keydown":return i.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function H0(a){return a=a.detail,typeof a=="object"&&"data"in a?a.data:null}var ql=!1;function CM(a,i){switch(a){case"compositionend":return H0(i);case"keypress":return i.which!==32?null:(I0=!0,$0);case"textInput":return a=i.data,a===$0&&I0?null:a;default:return null}}function PM(a,i){if(ql)return a==="compositionend"||!bp&&B0(a,i)?(a=M0(),wu=pp=Ka=null,ql=!1,a):null;switch(a){case"paste":return null;case"keypress":if(!(i.ctrlKey||i.altKey||i.metaKey)||i.ctrlKey&&i.altKey){if(i.char&&1=i)return{node:c,offset:i-a};a=f}e:{for(;c;){if(c.nextSibling){c=c.nextSibling;break e}c=c.parentNode}c=void 0}c=Z0(c)}}function J0(a,i){return a&&i?a===i?!0:a&&a.nodeType===3?!1:i&&i.nodeType===3?J0(a,i.parentNode):"contains"in a?a.contains(i):a.compareDocumentPosition?!!(a.compareDocumentPosition(i)&16):!1:!1}function ej(a){a=a!=null&&a.ownerDocument!=null&&a.ownerDocument.defaultView!=null?a.ownerDocument.defaultView:window;for(var i=ju(a.document);i instanceof a.HTMLIFrameElement;){try{var c=typeof i.contentWindow.location.href=="string"}catch{c=!1}if(c)a=i.contentWindow;else break;i=ju(a.document)}return i}function _p(a){var i=a&&a.nodeName&&a.nodeName.toLowerCase();return i&&(i==="input"&&(a.type==="text"||a.type==="search"||a.type==="tel"||a.type==="url"||a.type==="password")||i==="textarea"||a.contentEditable==="true")}var $M=ra&&"documentMode"in document&&11>=document.documentMode,Ul=null,wp=null,go=null,Sp=!1;function tj(a,i,c){var f=c.window===c?c.document:c.nodeType===9?c:c.ownerDocument;Sp||Ul==null||Ul!==ju(f)||(f=Ul,"selectionStart"in f&&_p(f)?f={start:f.selectionStart,end:f.selectionEnd}:(f=(f.ownerDocument&&f.ownerDocument.defaultView||window).getSelection(),f={anchorNode:f.anchorNode,anchorOffset:f.anchorOffset,focusNode:f.focusNode,focusOffset:f.focusOffset}),go&&vo(go,f)||(go=f,f=vd(wp,"onSelect"),0>=O,g-=O,Pr=1<<32-cn(i)+g|c<Me?(Ue=je,je=null):Ue=je.sibling;var Ke=F(U,je,B[Me],te);if(Ke===null){je===null&&(je=Ue);break}a&&je&&Ke.alternate===null&&i(U,je),q=b(Ke,q,Me),Fe===null?Se=Ke:Fe.sibling=Ke,Fe=Ke,je=Ue}if(Me===B.length)return c(U,je),Ie&&ia(U,Me),Se;if(je===null){for(;MeMe?(Ue=je,je=null):Ue=je.sibling;var hi=F(U,je,Ke.value,te);if(hi===null){je===null&&(je=Ue);break}a&&je&&hi.alternate===null&&i(U,je),q=b(hi,q,Me),Fe===null?Se=hi:Fe.sibling=hi,Fe=hi,je=Ue}if(Ke.done)return c(U,je),Ie&&ia(U,Me),Se;if(je===null){for(;!Ke.done;Me++,Ke=B.next())Ke=ne(U,Ke.value,te),Ke!==null&&(q=b(Ke,q,Me),Fe===null?Se=Ke:Fe.sibling=Ke,Fe=Ke);return Ie&&ia(U,Me),Se}for(je=f(je);!Ke.done;Me++,Ke=B.next())Ke=Z(je,U,Me,Ke.value,te),Ke!==null&&(a&&Ke.alternate!==null&&je.delete(Ke.key===null?Me:Ke.key),q=b(Ke,q,Me),Fe===null?Se=Ke:Fe.sibling=Ke,Fe=Ke);return a&&je.forEach(function(lL){return i(U,lL)}),Ie&&ia(U,Me),Se}function et(U,q,B,te){if(typeof B=="object"&&B!==null&&B.type===w&&B.key===null&&(B=B.props.children),typeof B=="object"&&B!==null){switch(B.$$typeof){case x:e:{for(var Se=B.key;q!==null;){if(q.key===Se){if(Se=B.type,Se===w){if(q.tag===7){c(U,q.sibling),te=g(q,B.props.children),te.return=U,U=te;break e}}else if(q.elementType===Se||typeof Se=="object"&&Se!==null&&Se.$$typeof===L&&Bi(Se)===q.type){c(U,q.sibling),te=g(q,B.props),wo(te,B),te.return=U,U=te;break e}c(U,q);break}else i(U,q);q=q.sibling}B.type===w?(te=zi(B.props.children,U.mode,te,B.key),te.return=U,U=te):(te=Ru(B.type,B.key,B.props,null,U.mode,te),wo(te,B),te.return=U,U=te)}return O(U);case _:e:{for(Se=B.key;q!==null;){if(q.key===Se)if(q.tag===4&&q.stateNode.containerInfo===B.containerInfo&&q.stateNode.implementation===B.implementation){c(U,q.sibling),te=g(q,B.children||[]),te.return=U,U=te;break e}else{c(U,q);break}else i(U,q);q=q.sibling}te=Cp(B,U.mode,te),te.return=U,U=te}return O(U);case L:return B=Bi(B),et(U,q,B,te)}if(ge(B))return be(U,q,B,te);if(I(B)){if(Se=I(B),typeof Se!="function")throw Error(r(150));return B=Se.call(B),Oe(U,q,B,te)}if(typeof B.then=="function")return et(U,q,$u(B),te);if(B.$$typeof===T)return et(U,q,Lu(U,B),te);Iu(U,B)}return typeof B=="string"&&B!==""||typeof B=="number"||typeof B=="bigint"?(B=""+B,q!==null&&q.tag===6?(c(U,q.sibling),te=g(q,B),te.return=U,U=te):(c(U,q),te=Tp(B,U.mode,te),te.return=U,U=te),O(U)):c(U,q)}return function(U,q,B,te){try{_o=0;var Se=et(U,q,B,te);return Wl=null,Se}catch(je){if(je===Xl||je===qu)throw je;var Fe=zn(29,je,null,U.mode);return Fe.lanes=te,Fe.return=U,Fe}finally{}}}var Fi=Sj(!0),Nj=Sj(!1),Wa=!1;function Hp(a){a.updateQueue={baseState:a.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Fp(a,i){a=a.updateQueue,i.updateQueue===a&&(i.updateQueue={baseState:a.baseState,firstBaseUpdate:a.firstBaseUpdate,lastBaseUpdate:a.lastBaseUpdate,shared:a.shared,callbacks:null})}function Za(a){return{lane:a,tag:0,payload:null,callback:null,next:null}}function Qa(a,i,c){var f=a.updateQueue;if(f===null)return null;if(f=f.shared,(Ve&2)!==0){var g=f.pending;return g===null?i.next=i:(i.next=g.next,g.next=i),f.pending=i,i=Pu(a),oj(a,null,c),i}return Cu(a,f,i,c),Pu(a)}function So(a,i,c){if(i=i.updateQueue,i!==null&&(i=i.shared,(c&4194048)!==0)){var f=i.lanes;f&=a.pendingLanes,c|=f,i.lanes=c,v0(a,c)}}function Kp(a,i){var c=a.updateQueue,f=a.alternate;if(f!==null&&(f=f.updateQueue,c===f)){var g=null,b=null;if(c=c.firstBaseUpdate,c!==null){do{var O={lane:c.lane,tag:c.tag,payload:c.payload,callback:null,next:null};b===null?g=b=O:b=b.next=O,c=c.next}while(c!==null);b===null?g=b=i:b=b.next=i}else g=b=i;c={baseState:f.baseState,firstBaseUpdate:g,lastBaseUpdate:b,shared:f.shared,callbacks:f.callbacks},a.updateQueue=c;return}a=c.lastBaseUpdate,a===null?c.firstBaseUpdate=i:a.next=i,c.lastBaseUpdate=i}var Yp=!1;function No(){if(Yp){var a=Vl;if(a!==null)throw a}}function Oo(a,i,c,f){Yp=!1;var g=a.updateQueue;Wa=!1;var b=g.firstBaseUpdate,O=g.lastBaseUpdate,C=g.shared.pending;if(C!==null){g.shared.pending=null;var D=C,H=D.next;D.next=null,O===null?b=H:O.next=H,O=D;var J=a.alternate;J!==null&&(J=J.updateQueue,C=J.lastBaseUpdate,C!==O&&(C===null?J.firstBaseUpdate=H:C.next=H,J.lastBaseUpdate=D))}if(b!==null){var ne=g.baseState;O=0,J=H=D=null,C=b;do{var F=C.lane&-536870913,Z=F!==C.lane;if(Z?(qe&F)===F:(f&F)===F){F!==0&&F===Gl&&(Yp=!0),J!==null&&(J=J.next={lane:0,tag:C.tag,payload:C.payload,callback:null,next:null});e:{var be=a,Oe=C;F=i;var et=c;switch(Oe.tag){case 1:if(be=Oe.payload,typeof be=="function"){ne=be.call(et,ne,F);break e}ne=be;break e;case 3:be.flags=be.flags&-65537|128;case 0:if(be=Oe.payload,F=typeof be=="function"?be.call(et,ne,F):be,F==null)break e;ne=m({},ne,F);break e;case 2:Wa=!0}}F=C.callback,F!==null&&(a.flags|=64,Z&&(a.flags|=8192),Z=g.callbacks,Z===null?g.callbacks=[F]:Z.push(F))}else Z={lane:F,tag:C.tag,payload:C.payload,callback:C.callback,next:null},J===null?(H=J=Z,D=ne):J=J.next=Z,O|=F;if(C=C.next,C===null){if(C=g.shared.pending,C===null)break;Z=C,C=Z.next,Z.next=null,g.lastBaseUpdate=Z,g.shared.pending=null}}while(!0);J===null&&(D=ne),g.baseState=D,g.firstBaseUpdate=H,g.lastBaseUpdate=J,b===null&&(g.shared.lanes=0),ri|=O,a.lanes=O,a.memoizedState=ne}}function Oj(a,i){if(typeof a!="function")throw Error(r(191,a));a.call(i)}function Ej(a,i){var c=a.callbacks;if(c!==null)for(a.callbacks=null,a=0;ab?b:8;var O=$.T,C={};$.T=C,dm(a,!1,i,c);try{var D=g(),H=$.S;if(H!==null&&H(C,D),D!==null&&typeof D=="object"&&typeof D.then=="function"){var J=XM(D,f);Ao(a,i,J,Bn(a))}else Ao(a,i,f,Bn(a))}catch(ne){Ao(a,i,{then:function(){},status:"rejected",reason:ne},Bn())}finally{X.p=b,O!==null&&C.types!==null&&(O.types=C.types),$.T=O}}function tD(){}function cm(a,i,c,f){if(a.tag!==5)throw Error(r(476));var g=i_(a).queue;a_(a,g,i,G,c===null?tD:function(){return l_(a),c(f)})}function i_(a){var i=a.memoizedState;if(i!==null)return i;i={memoizedState:G,baseState:G,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ca,lastRenderedState:G},next:null};var c={};return i.next={memoizedState:c,baseState:c,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ca,lastRenderedState:c},next:null},a.memoizedState=i,a=a.alternate,a!==null&&(a.memoizedState=i),i}function l_(a){var i=i_(a);i.next===null&&(i=a.alternate.memoizedState),Ao(a,i.next.queue,{},Bn())}function um(){return Zt(Yo)}function s_(){return jt().memoizedState}function o_(){return jt().memoizedState}function nD(a){for(var i=a.return;i!==null;){switch(i.tag){case 24:case 3:var c=Bn();a=Za(c);var f=Qa(i,a,c);f!==null&&(Tn(f,i,c),So(f,i,c)),i={cache:Up()},a.payload=i;return}i=i.return}}function rD(a,i,c){var f=Bn();c={lane:f,revertLane:0,gesture:null,action:c,hasEagerState:!1,eagerState:null,next:null},Zu(a)?u_(i,c):(c=kp(a,i,c,f),c!==null&&(Tn(c,a,f),d_(c,i,f)))}function c_(a,i,c){var f=Bn();Ao(a,i,c,f)}function Ao(a,i,c,f){var g={lane:f,revertLane:0,gesture:null,action:c,hasEagerState:!1,eagerState:null,next:null};if(Zu(a))u_(i,g);else{var b=a.alternate;if(a.lanes===0&&(b===null||b.lanes===0)&&(b=i.lastRenderedReducer,b!==null))try{var O=i.lastRenderedState,C=b(O,c);if(g.hasEagerState=!0,g.eagerState=C,Ln(C,O))return Cu(a,i,g,0),nt===null&&Tu(),!1}catch{}finally{}if(c=kp(a,i,g,f),c!==null)return Tn(c,a,f),d_(c,i,f),!0}return!1}function dm(a,i,c,f){if(f={lane:2,revertLane:Hm(),gesture:null,action:f,hasEagerState:!1,eagerState:null,next:null},Zu(a)){if(i)throw Error(r(479))}else i=kp(a,c,f,2),i!==null&&Tn(i,a,2)}function Zu(a){var i=a.alternate;return a===Ce||i!==null&&i===Ce}function u_(a,i){Ql=Fu=!0;var c=a.pending;c===null?i.next=i:(i.next=c.next,c.next=i),a.pending=i}function d_(a,i,c){if((c&4194048)!==0){var f=i.lanes;f&=a.pendingLanes,c|=f,i.lanes=c,v0(a,c)}}var To={readContext:Zt,use:Gu,useCallback:mt,useContext:mt,useEffect:mt,useImperativeHandle:mt,useLayoutEffect:mt,useInsertionEffect:mt,useMemo:mt,useReducer:mt,useRef:mt,useState:mt,useDebugValue:mt,useDeferredValue:mt,useTransition:mt,useSyncExternalStore:mt,useId:mt,useHostTransitionStatus:mt,useFormState:mt,useActionState:mt,useOptimistic:mt,useMemoCache:mt,useCacheRefresh:mt};To.useEffectEvent=mt;var f_={readContext:Zt,use:Gu,useCallback:function(a,i){return vn().memoizedState=[a,i===void 0?null:i],a},useContext:Zt,useEffect:Xj,useImperativeHandle:function(a,i,c){c=c!=null?c.concat([a]):null,Xu(4194308,4,Jj.bind(null,i,a),c)},useLayoutEffect:function(a,i){return Xu(4194308,4,a,i)},useInsertionEffect:function(a,i){Xu(4,2,a,i)},useMemo:function(a,i){var c=vn();i=i===void 0?null:i;var f=a();if(Ki){xr(!0);try{a()}finally{xr(!1)}}return c.memoizedState=[f,i],f},useReducer:function(a,i,c){var f=vn();if(c!==void 0){var g=c(i);if(Ki){xr(!0);try{c(i)}finally{xr(!1)}}}else g=i;return f.memoizedState=f.baseState=g,a={pending:null,lanes:0,dispatch:null,lastRenderedReducer:a,lastRenderedState:g},f.queue=a,a=a.dispatch=rD.bind(null,Ce,a),[f.memoizedState,a]},useRef:function(a){var i=vn();return a={current:a},i.memoizedState=a},useState:function(a){a=am(a);var i=a.queue,c=c_.bind(null,Ce,i);return i.dispatch=c,[a.memoizedState,c]},useDebugValue:sm,useDeferredValue:function(a,i){var c=vn();return om(c,a,i)},useTransition:function(){var a=am(!1);return a=a_.bind(null,Ce,a.queue,!0,!1),vn().memoizedState=a,[!1,a]},useSyncExternalStore:function(a,i,c){var f=Ce,g=vn();if(Ie){if(c===void 0)throw Error(r(407));c=c()}else{if(c=i(),nt===null)throw Error(r(349));(qe&127)!==0||Rj(f,i,c)}g.memoizedState=c;var b={value:c,getSnapshot:i};return g.queue=b,Xj(Dj.bind(null,f,b,a),[a]),f.flags|=2048,es(9,{destroy:void 0},Mj.bind(null,f,b,c,i),null),c},useId:function(){var a=vn(),i=nt.identifierPrefix;if(Ie){var c=Rr,f=Pr;c=(f&~(1<<32-cn(f)-1)).toString(32)+c,i="_"+i+"R_"+c,c=Ku++,0<\/script>",b=b.removeChild(b.firstChild);break;case"select":b=typeof f.is=="string"?O.createElement("select",{is:f.is}):O.createElement("select"),f.multiple?b.multiple=!0:f.size&&(b.size=f.size);break;default:b=typeof f.is=="string"?O.createElement(g,{is:f.is}):O.createElement(g)}}b[Xt]=i,b[Sn]=f;e:for(O=i.child;O!==null;){if(O.tag===5||O.tag===6)b.appendChild(O.stateNode);else if(O.tag!==4&&O.tag!==27&&O.child!==null){O.child.return=O,O=O.child;continue}if(O===i)break e;for(;O.sibling===null;){if(O.return===null||O.return===i)break e;O=O.return}O.sibling.return=O.return,O=O.sibling}i.stateNode=b;e:switch(Jt(b,g,f),g){case"button":case"input":case"select":case"textarea":f=!!f.autoFocus;break e;case"img":f=!0;break e;default:f=!1}f&&da(i)}}return lt(i),Nm(i,i.type,a===null?null:a.memoizedProps,i.pendingProps,c),null;case 6:if(a&&i.stateNode!=null)a.memoizedProps!==f&&da(i);else{if(typeof f!="string"&&i.stateNode===null)throw Error(r(166));if(a=Ee.current,Kl(i)){if(a=i.stateNode,c=i.memoizedProps,f=null,g=Wt,g!==null)switch(g.tag){case 27:case 5:f=g.memoizedProps}a[Xt]=i,a=!!(a.nodeValue===c||f!==null&&f.suppressHydrationWarning===!0||Cw(a.nodeValue,c)),a||Va(i,!0)}else a=gd(a).createTextNode(f),a[Xt]=i,i.stateNode=a}return lt(i),null;case 31:if(c=i.memoizedState,a===null||a.memoizedState!==null){if(f=Kl(i),c!==null){if(a===null){if(!f)throw Error(r(318));if(a=i.memoizedState,a=a!==null?a.dehydrated:null,!a)throw Error(r(557));a[Xt]=i}else qi(),(i.flags&128)===0&&(i.memoizedState=null),i.flags|=4;lt(i),a=!1}else c=Dp(),a!==null&&a.memoizedState!==null&&(a.memoizedState.hydrationErrors=c),a=!0;if(!a)return i.flags&256?(Un(i),i):(Un(i),null);if((i.flags&128)!==0)throw Error(r(558))}return lt(i),null;case 13:if(f=i.memoizedState,a===null||a.memoizedState!==null&&a.memoizedState.dehydrated!==null){if(g=Kl(i),f!==null&&f.dehydrated!==null){if(a===null){if(!g)throw Error(r(318));if(g=i.memoizedState,g=g!==null?g.dehydrated:null,!g)throw Error(r(317));g[Xt]=i}else qi(),(i.flags&128)===0&&(i.memoizedState=null),i.flags|=4;lt(i),g=!1}else g=Dp(),a!==null&&a.memoizedState!==null&&(a.memoizedState.hydrationErrors=g),g=!0;if(!g)return i.flags&256?(Un(i),i):(Un(i),null)}return Un(i),(i.flags&128)!==0?(i.lanes=c,i):(c=f!==null,a=a!==null&&a.memoizedState!==null,c&&(f=i.child,g=null,f.alternate!==null&&f.alternate.memoizedState!==null&&f.alternate.memoizedState.cachePool!==null&&(g=f.alternate.memoizedState.cachePool.pool),b=null,f.memoizedState!==null&&f.memoizedState.cachePool!==null&&(b=f.memoizedState.cachePool.pool),b!==g&&(f.flags|=2048)),c!==a&&c&&(i.child.flags|=8192),nd(i,i.updateQueue),lt(i),null);case 4:return se(),a===null&&Gm(i.stateNode.containerInfo),lt(i),null;case 10:return sa(i.type),lt(i),null;case 19:if(Q(xt),f=i.memoizedState,f===null)return lt(i),null;if(g=(i.flags&128)!==0,b=f.rendering,b===null)if(g)Po(f,!1);else{if(vt!==0||a!==null&&(a.flags&128)!==0)for(a=i.child;a!==null;){if(b=Hu(a),b!==null){for(i.flags|=128,Po(f,!1),a=b.updateQueue,i.updateQueue=a,nd(i,a),i.subtreeFlags=0,a=c,c=i.child;c!==null;)cj(c,a),c=c.sibling;return ce(xt,xt.current&1|2),Ie&&ia(i,f.treeForkCount),i.child}a=a.sibling}f.tail!==null&&sn()>sd&&(i.flags|=128,g=!0,Po(f,!1),i.lanes=4194304)}else{if(!g)if(a=Hu(b),a!==null){if(i.flags|=128,g=!0,a=a.updateQueue,i.updateQueue=a,nd(i,a),Po(f,!0),f.tail===null&&f.tailMode==="hidden"&&!b.alternate&&!Ie)return lt(i),null}else 2*sn()-f.renderingStartTime>sd&&c!==536870912&&(i.flags|=128,g=!0,Po(f,!1),i.lanes=4194304);f.isBackwards?(b.sibling=i.child,i.child=b):(a=f.last,a!==null?a.sibling=b:i.child=b,f.last=b)}return f.tail!==null?(a=f.tail,f.rendering=a,f.tail=a.sibling,f.renderingStartTime=sn(),a.sibling=null,c=xt.current,ce(xt,g?c&1|2:c&1),Ie&&ia(i,f.treeForkCount),a):(lt(i),null);case 22:case 23:return Un(i),Vp(),f=i.memoizedState!==null,a!==null?a.memoizedState!==null!==f&&(i.flags|=8192):f&&(i.flags|=8192),f?(c&536870912)!==0&&(i.flags&128)===0&&(lt(i),i.subtreeFlags&6&&(i.flags|=8192)):lt(i),c=i.updateQueue,c!==null&&nd(i,c.retryQueue),c=null,a!==null&&a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(c=a.memoizedState.cachePool.pool),f=null,i.memoizedState!==null&&i.memoizedState.cachePool!==null&&(f=i.memoizedState.cachePool.pool),f!==c&&(i.flags|=2048),a!==null&&Q(Ii),null;case 24:return c=null,a!==null&&(c=a.memoizedState.cache),i.memoizedState.cache!==c&&(i.flags|=2048),sa(Ot),lt(i),null;case 25:return null;case 30:return null}throw Error(r(156,i.tag))}function oD(a,i){switch(Rp(i),i.tag){case 1:return a=i.flags,a&65536?(i.flags=a&-65537|128,i):null;case 3:return sa(Ot),se(),a=i.flags,(a&65536)!==0&&(a&128)===0?(i.flags=a&-65537|128,i):null;case 26:case 27:case 5:return Re(i),null;case 31:if(i.memoizedState!==null){if(Un(i),i.alternate===null)throw Error(r(340));qi()}return a=i.flags,a&65536?(i.flags=a&-65537|128,i):null;case 13:if(Un(i),a=i.memoizedState,a!==null&&a.dehydrated!==null){if(i.alternate===null)throw Error(r(340));qi()}return a=i.flags,a&65536?(i.flags=a&-65537|128,i):null;case 19:return Q(xt),null;case 4:return se(),null;case 10:return sa(i.type),null;case 22:case 23:return Un(i),Vp(),a!==null&&Q(Ii),a=i.flags,a&65536?(i.flags=a&-65537|128,i):null;case 24:return sa(Ot),null;case 25:return null;default:return null}}function L_(a,i){switch(Rp(i),i.tag){case 3:sa(Ot),se();break;case 26:case 27:case 5:Re(i);break;case 4:se();break;case 31:i.memoizedState!==null&&Un(i);break;case 13:Un(i);break;case 19:Q(xt);break;case 10:sa(i.type);break;case 22:case 23:Un(i),Vp(),a!==null&&Q(Ii);break;case 24:sa(Ot)}}function Ro(a,i){try{var c=i.updateQueue,f=c!==null?c.lastEffect:null;if(f!==null){var g=f.next;c=g;do{if((c.tag&a)===a){f=void 0;var b=c.create,O=c.inst;f=b(),O.destroy=f}c=c.next}while(c!==g)}}catch(C){We(i,i.return,C)}}function ti(a,i,c){try{var f=i.updateQueue,g=f!==null?f.lastEffect:null;if(g!==null){var b=g.next;f=b;do{if((f.tag&a)===a){var O=f.inst,C=O.destroy;if(C!==void 0){O.destroy=void 0,g=i;var D=c,H=C;try{H()}catch(J){We(g,D,J)}}}f=f.next}while(f!==b)}}catch(J){We(i,i.return,J)}}function z_(a){var i=a.updateQueue;if(i!==null){var c=a.stateNode;try{Ej(i,c)}catch(f){We(a,a.return,f)}}}function q_(a,i,c){c.props=Yi(a.type,a.memoizedProps),c.state=a.memoizedState;try{c.componentWillUnmount()}catch(f){We(a,i,f)}}function Mo(a,i){try{var c=a.ref;if(c!==null){switch(a.tag){case 26:case 27:case 5:var f=a.stateNode;break;case 30:f=a.stateNode;break;default:f=a.stateNode}typeof c=="function"?a.refCleanup=c(f):c.current=f}}catch(g){We(a,i,g)}}function Mr(a,i){var c=a.ref,f=a.refCleanup;if(c!==null)if(typeof f=="function")try{f()}catch(g){We(a,i,g)}finally{a.refCleanup=null,a=a.alternate,a!=null&&(a.refCleanup=null)}else if(typeof c=="function")try{c(null)}catch(g){We(a,i,g)}else c.current=null}function U_(a){var i=a.type,c=a.memoizedProps,f=a.stateNode;try{e:switch(i){case"button":case"input":case"select":case"textarea":c.autoFocus&&f.focus();break e;case"img":c.src?f.src=c.src:c.srcSet&&(f.srcset=c.srcSet)}}catch(g){We(a,a.return,g)}}function Om(a,i,c){try{var f=a.stateNode;TD(f,a.type,c,i),f[Sn]=i}catch(g){We(a,a.return,g)}}function $_(a){return a.tag===5||a.tag===3||a.tag===26||a.tag===27&&oi(a.type)||a.tag===4}function Em(a){e:for(;;){for(;a.sibling===null;){if(a.return===null||$_(a.return))return null;a=a.return}for(a.sibling.return=a.return,a=a.sibling;a.tag!==5&&a.tag!==6&&a.tag!==18;){if(a.tag===27&&oi(a.type)||a.flags&2||a.child===null||a.tag===4)continue e;a.child.return=a,a=a.child}if(!(a.flags&2))return a.stateNode}}function km(a,i,c){var f=a.tag;if(f===5||f===6)a=a.stateNode,i?(c.nodeType===9?c.body:c.nodeName==="HTML"?c.ownerDocument.body:c).insertBefore(a,i):(i=c.nodeType===9?c.body:c.nodeName==="HTML"?c.ownerDocument.body:c,i.appendChild(a),c=c._reactRootContainer,c!=null||i.onclick!==null||(i.onclick=na));else if(f!==4&&(f===27&&oi(a.type)&&(c=a.stateNode,i=null),a=a.child,a!==null))for(km(a,i,c),a=a.sibling;a!==null;)km(a,i,c),a=a.sibling}function rd(a,i,c){var f=a.tag;if(f===5||f===6)a=a.stateNode,i?c.insertBefore(a,i):c.appendChild(a);else if(f!==4&&(f===27&&oi(a.type)&&(c=a.stateNode),a=a.child,a!==null))for(rd(a,i,c),a=a.sibling;a!==null;)rd(a,i,c),a=a.sibling}function I_(a){var i=a.stateNode,c=a.memoizedProps;try{for(var f=a.type,g=i.attributes;g.length;)i.removeAttributeNode(g[0]);Jt(i,f,c),i[Xt]=a,i[Sn]=c}catch(b){We(a,a.return,b)}}var fa=!1,At=!1,Am=!1,B_=typeof WeakSet=="function"?WeakSet:Set,$t=null;function cD(a,i){if(a=a.containerInfo,Wm=Sd,a=ej(a),_p(a)){if("selectionStart"in a)var c={start:a.selectionStart,end:a.selectionEnd};else e:{c=(c=a.ownerDocument)&&c.defaultView||window;var f=c.getSelection&&c.getSelection();if(f&&f.rangeCount!==0){c=f.anchorNode;var g=f.anchorOffset,b=f.focusNode;f=f.focusOffset;try{c.nodeType,b.nodeType}catch{c=null;break e}var O=0,C=-1,D=-1,H=0,J=0,ne=a,F=null;t:for(;;){for(var Z;ne!==c||g!==0&&ne.nodeType!==3||(C=O+g),ne!==b||f!==0&&ne.nodeType!==3||(D=O+f),ne.nodeType===3&&(O+=ne.nodeValue.length),(Z=ne.firstChild)!==null;)F=ne,ne=Z;for(;;){if(ne===a)break t;if(F===c&&++H===g&&(C=O),F===b&&++J===f&&(D=O),(Z=ne.nextSibling)!==null)break;ne=F,F=ne.parentNode}ne=Z}c=C===-1||D===-1?null:{start:C,end:D}}else c=null}c=c||{start:0,end:0}}else c=null;for(Zm={focusedElem:a,selectionRange:c},Sd=!1,$t=i;$t!==null;)if(i=$t,a=i.child,(i.subtreeFlags&1028)!==0&&a!==null)a.return=i,$t=a;else for(;$t!==null;){switch(i=$t,b=i.alternate,a=i.flags,i.tag){case 0:if((a&4)!==0&&(a=i.updateQueue,a=a!==null?a.events:null,a!==null))for(c=0;c title"))),Jt(b,f,c),b[Xt]=a,Ut(b),f=b;break e;case"link":var O=Vw("link","href",g).get(f+(c.href||""));if(O){for(var C=0;Cet&&(O=et,et=Oe,Oe=O);var U=Q0(C,Oe),q=Q0(C,et);if(U&&q&&(Z.rangeCount!==1||Z.anchorNode!==U.node||Z.anchorOffset!==U.offset||Z.focusNode!==q.node||Z.focusOffset!==q.offset)){var B=ne.createRange();B.setStart(U.node,U.offset),Z.removeAllRanges(),Oe>et?(Z.addRange(B),Z.extend(q.node,q.offset)):(B.setEnd(q.node,q.offset),Z.addRange(B))}}}}for(ne=[],Z=C;Z=Z.parentNode;)Z.nodeType===1&&ne.push({element:Z,left:Z.scrollLeft,top:Z.scrollTop});for(typeof C.focus=="function"&&C.focus(),C=0;Cc?32:c,$.T=null,c=Lm,Lm=null;var b=ii,O=ga;if(Rt=0,is=ii=null,ga=0,(Ve&6)!==0)throw Error(r(331));var C=Ve;if(Ve|=4,J_(b.current),W_(b,b.current,O,c),Ve=C,$o(0,!1),on&&typeof on.onPostCommitFiberRoot=="function")try{on.onPostCommitFiberRoot(Ha,b)}catch{}return!0}finally{X.p=g,$.T=f,gw(a,i)}}function bw(a,i,c){i=nr(c,i),i=mm(a.stateNode,i,2),a=Qa(a,i,2),a!==null&&(jr(a,2),Dr(a))}function We(a,i,c){if(a.tag===3)bw(a,a,c);else for(;i!==null;){if(i.tag===3){bw(i,a,c);break}else if(i.tag===1){var f=i.stateNode;if(typeof i.type.getDerivedStateFromError=="function"||typeof f.componentDidCatch=="function"&&(ai===null||!ai.has(f))){a=nr(c,a),c=x_(2),f=Qa(i,c,2),f!==null&&(j_(c,f,i,a),jr(f,2),Dr(f));break}}i=i.return}}function $m(a,i,c){var f=a.pingCache;if(f===null){f=a.pingCache=new fD;var g=new Set;f.set(i,g)}else g=f.get(i),g===void 0&&(g=new Set,f.set(i,g));g.has(c)||(Pm=!0,g.add(c),a=gD.bind(null,a,i,c),i.then(a,a))}function gD(a,i,c){var f=a.pingCache;f!==null&&f.delete(i),a.pingedLanes|=a.suspendedLanes&c,a.warmLanes&=~c,nt===a&&(qe&c)===c&&(vt===4||vt===3&&(qe&62914560)===qe&&300>sn()-ld?(Ve&2)===0&&ls(a,0):Rm|=c,as===qe&&(as=0)),Dr(a)}function xw(a,i){i===0&&(i=io()),a=Li(a,i),a!==null&&(jr(a,i),Dr(a))}function yD(a){var i=a.memoizedState,c=0;i!==null&&(c=i.retryLane),xw(a,c)}function bD(a,i){var c=0;switch(a.tag){case 31:case 13:var f=a.stateNode,g=a.memoizedState;g!==null&&(c=g.retryLane);break;case 19:f=a.stateNode;break;case 22:f=a.stateNode._retryCache;break;default:throw Error(r(314))}f!==null&&f.delete(i),xw(a,c)}function xD(a,i){return Ol(a,i)}var hd=null,os=null,Im=!1,pd=!1,Bm=!1,si=0;function Dr(a){a!==os&&a.next===null&&(os===null?hd=os=a:os=os.next=a),pd=!0,Im||(Im=!0,_D())}function $o(a,i){if(!Bm&&pd){Bm=!0;do for(var c=!1,f=hd;f!==null;){if(a!==0){var g=f.pendingLanes;if(g===0)var b=0;else{var O=f.suspendedLanes,C=f.pingedLanes;b=(1<<31-cn(42|a)+1)-1,b&=g&~(O&~C),b=b&201326741?b&201326741|1:b?b|2:0}b!==0&&(c=!0,Sw(f,b))}else b=qe,b=pt(f,f===nt?b:0,f.cancelPendingCommit!==null||f.timeoutHandle!==-1),(b&3)===0||Vt(f,b)||(c=!0,Sw(f,b));f=f.next}while(c);Bm=!1}}function jD(){jw()}function jw(){pd=Im=!1;var a=0;si!==0&&PD()&&(a=si);for(var i=sn(),c=null,f=hd;f!==null;){var g=f.next,b=_w(f,i);b===0?(f.next=null,c===null?hd=g:c.next=g,g===null&&(os=c)):(c=f,(a!==0||(b&3)!==0)&&(pd=!0)),f=g}Rt!==0&&Rt!==5||$o(a),si!==0&&(si=0)}function _w(a,i){for(var c=a.suspendedLanes,f=a.pingedLanes,g=a.expirationTimes,b=a.pendingLanes&-62914561;0C)break;var J=D.transferSize,ne=D.initiatorType;J&&Pw(ne)&&(D=D.responseEnd,O+=J*(D"u"?null:document;function Fw(a,i,c){var f=cs;if(f&&typeof i=="string"&&i){var g=er(i);g='link[rel="'+a+'"][href="'+g+'"]',typeof c=="string"&&(g+='[crossorigin="'+c+'"]'),Hw.has(g)||(Hw.add(g),a={rel:a,crossOrigin:c,href:i},f.querySelector(g)===null&&(i=f.createElement("link"),Jt(i,"link",a),Ut(i),f.head.appendChild(i)))}}function ID(a){ya.D(a),Fw("dns-prefetch",a,null)}function BD(a,i){ya.C(a,i),Fw("preconnect",a,i)}function HD(a,i,c){ya.L(a,i,c);var f=cs;if(f&&a&&i){var g='link[rel="preload"][as="'+er(i)+'"]';i==="image"&&c&&c.imageSrcSet?(g+='[imagesrcset="'+er(c.imageSrcSet)+'"]',typeof c.imageSizes=="string"&&(g+='[imagesizes="'+er(c.imageSizes)+'"]')):g+='[href="'+er(a)+'"]';var b=g;switch(i){case"style":b=us(a);break;case"script":b=ds(a)}or.has(b)||(a=m({rel:"preload",href:i==="image"&&c&&c.imageSrcSet?void 0:a,as:i},c),or.set(b,a),f.querySelector(g)!==null||i==="style"&&f.querySelector(Fo(b))||i==="script"&&f.querySelector(Ko(b))||(i=f.createElement("link"),Jt(i,"link",a),Ut(i),f.head.appendChild(i)))}}function FD(a,i){ya.m(a,i);var c=cs;if(c&&a){var f=i&&typeof i.as=="string"?i.as:"script",g='link[rel="modulepreload"][as="'+er(f)+'"][href="'+er(a)+'"]',b=g;switch(f){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":b=ds(a)}if(!or.has(b)&&(a=m({rel:"modulepreload",href:a},i),or.set(b,a),c.querySelector(g)===null)){switch(f){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(c.querySelector(Ko(b)))return}f=c.createElement("link"),Jt(f,"link",a),Ut(f),c.head.appendChild(f)}}}function KD(a,i,c){ya.S(a,i,c);var f=cs;if(f&&a){var g=Pl(f).hoistableStyles,b=us(a);i=i||"default";var O=g.get(b);if(!O){var C={loading:0,preload:null};if(O=f.querySelector(Fo(b)))C.loading=5;else{a=m({rel:"stylesheet",href:a,"data-precedence":i},c),(c=or.get(b))&&av(a,c);var D=O=f.createElement("link");Ut(D),Jt(D,"link",a),D._p=new Promise(function(H,J){D.onload=H,D.onerror=J}),D.addEventListener("load",function(){C.loading|=1}),D.addEventListener("error",function(){C.loading|=2}),C.loading|=4,bd(O,i,f)}O={type:"stylesheet",instance:O,count:1,state:C},g.set(b,O)}}}function YD(a,i){ya.X(a,i);var c=cs;if(c&&a){var f=Pl(c).hoistableScripts,g=ds(a),b=f.get(g);b||(b=c.querySelector(Ko(g)),b||(a=m({src:a,async:!0},i),(i=or.get(g))&&iv(a,i),b=c.createElement("script"),Ut(b),Jt(b,"link",a),c.head.appendChild(b)),b={type:"script",instance:b,count:1,state:null},f.set(g,b))}}function GD(a,i){ya.M(a,i);var c=cs;if(c&&a){var f=Pl(c).hoistableScripts,g=ds(a),b=f.get(g);b||(b=c.querySelector(Ko(g)),b||(a=m({src:a,async:!0,type:"module"},i),(i=or.get(g))&&iv(a,i),b=c.createElement("script"),Ut(b),Jt(b,"link",a),c.head.appendChild(b)),b={type:"script",instance:b,count:1,state:null},f.set(g,b))}}function Kw(a,i,c,f){var g=(g=Ee.current)?yd(g):null;if(!g)throw Error(r(446));switch(a){case"meta":case"title":return null;case"style":return typeof c.precedence=="string"&&typeof c.href=="string"?(i=us(c.href),c=Pl(g).hoistableStyles,f=c.get(i),f||(f={type:"style",instance:null,count:0,state:null},c.set(i,f)),f):{type:"void",instance:null,count:0,state:null};case"link":if(c.rel==="stylesheet"&&typeof c.href=="string"&&typeof c.precedence=="string"){a=us(c.href);var b=Pl(g).hoistableStyles,O=b.get(a);if(O||(g=g.ownerDocument||g,O={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},b.set(a,O),(b=g.querySelector(Fo(a)))&&!b._p&&(O.instance=b,O.state.loading=5),or.has(a)||(c={rel:"preload",as:"style",href:c.href,crossOrigin:c.crossOrigin,integrity:c.integrity,media:c.media,hrefLang:c.hrefLang,referrerPolicy:c.referrerPolicy},or.set(a,c),b||VD(g,a,c,O.state))),i&&f===null)throw Error(r(528,""));return O}if(i&&f!==null)throw Error(r(529,""));return null;case"script":return i=c.async,c=c.src,typeof c=="string"&&i&&typeof i!="function"&&typeof i!="symbol"?(i=ds(c),c=Pl(g).hoistableScripts,f=c.get(i),f||(f={type:"script",instance:null,count:0,state:null},c.set(i,f)),f):{type:"void",instance:null,count:0,state:null};default:throw Error(r(444,a))}}function us(a){return'href="'+er(a)+'"'}function Fo(a){return'link[rel="stylesheet"]['+a+"]"}function Yw(a){return m({},a,{"data-precedence":a.precedence,precedence:null})}function VD(a,i,c,f){a.querySelector('link[rel="preload"][as="style"]['+i+"]")?f.loading=1:(i=a.createElement("link"),f.preload=i,i.addEventListener("load",function(){return f.loading|=1}),i.addEventListener("error",function(){return f.loading|=2}),Jt(i,"link",c),Ut(i),a.head.appendChild(i))}function ds(a){return'[src="'+er(a)+'"]'}function Ko(a){return"script[async]"+a}function Gw(a,i,c){if(i.count++,i.instance===null)switch(i.type){case"style":var f=a.querySelector('style[data-href~="'+er(c.href)+'"]');if(f)return i.instance=f,Ut(f),f;var g=m({},c,{"data-href":c.href,"data-precedence":c.precedence,href:null,precedence:null});return f=(a.ownerDocument||a).createElement("style"),Ut(f),Jt(f,"style",g),bd(f,c.precedence,a),i.instance=f;case"stylesheet":g=us(c.href);var b=a.querySelector(Fo(g));if(b)return i.state.loading|=4,i.instance=b,Ut(b),b;f=Yw(c),(g=or.get(g))&&av(f,g),b=(a.ownerDocument||a).createElement("link"),Ut(b);var O=b;return O._p=new Promise(function(C,D){O.onload=C,O.onerror=D}),Jt(b,"link",f),i.state.loading|=4,bd(b,c.precedence,a),i.instance=b;case"script":return b=ds(c.src),(g=a.querySelector(Ko(b)))?(i.instance=g,Ut(g),g):(f=c,(g=or.get(b))&&(f=m({},c),iv(f,g)),a=a.ownerDocument||a,g=a.createElement("script"),Ut(g),Jt(g,"link",f),a.head.appendChild(g),i.instance=g);case"void":return null;default:throw Error(r(443,i.type))}else i.type==="stylesheet"&&(i.state.loading&4)===0&&(f=i.instance,i.state.loading|=4,bd(f,c.precedence,a));return i.instance}function bd(a,i,c){for(var f=c.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),g=f.length?f[f.length-1]:null,b=g,O=0;O title"):null)}function XD(a,i,c){if(c===1||i.itemProp!=null)return!1;switch(a){case"meta":case"title":return!0;case"style":if(typeof i.precedence!="string"||typeof i.href!="string"||i.href==="")break;return!0;case"link":if(typeof i.rel!="string"||typeof i.href!="string"||i.href===""||i.onLoad||i.onError)break;switch(i.rel){case"stylesheet":return a=i.disabled,typeof i.precedence=="string"&&a==null;default:return!0}case"script":if(i.async&&typeof i.async!="function"&&typeof i.async!="symbol"&&!i.onLoad&&!i.onError&&i.src&&typeof i.src=="string")return!0}return!1}function Ww(a){return!(a.type==="stylesheet"&&(a.state.loading&3)===0)}function WD(a,i,c,f){if(c.type==="stylesheet"&&(typeof f.media!="string"||matchMedia(f.media).matches!==!1)&&(c.state.loading&4)===0){if(c.instance===null){var g=us(f.href),b=i.querySelector(Fo(g));if(b){i=b._p,i!==null&&typeof i=="object"&&typeof i.then=="function"&&(a.count++,a=jd.bind(a),i.then(a,a)),c.state.loading|=4,c.instance=b,Ut(b);return}b=i.ownerDocument||i,f=Yw(f),(g=or.get(g))&&av(f,g),b=b.createElement("link"),Ut(b);var O=b;O._p=new Promise(function(C,D){O.onload=C,O.onerror=D}),Jt(b,"link",f),c.instance=b}a.stylesheets===null&&(a.stylesheets=new Map),a.stylesheets.set(c,i),(i=c.state.preload)&&(c.state.loading&3)===0&&(a.count++,c=jd.bind(a),i.addEventListener("load",c),i.addEventListener("error",c))}}var lv=0;function ZD(a,i){return a.stylesheets&&a.count===0&&wd(a,a.stylesheets),0lv?50:800)+i);return a.unsuspend=c,function(){a.unsuspend=null,clearTimeout(f),clearTimeout(g)}}:null}function jd(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)wd(this,this.stylesheets);else if(this.unsuspend){var a=this.unsuspend;this.unsuspend=null,a()}}}var _d=null;function wd(a,i){a.stylesheets=null,a.unsuspend!==null&&(a.count++,_d=new Map,i.forEach(QD,a),_d=null,jd.call(a))}function QD(a,i){if(!(i.state.loading&4)){var c=_d.get(a);if(c)var f=c.get(null);else{c=new Map,_d.set(a,c);for(var g=a.querySelectorAll("link[data-precedence],style[data-precedence]"),b=0;b"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),vv.exports=gL(),vv.exports}var bL=yL();const xL=Ra(bL);/** +`+f.stack}}var Nl=Object.prototype.hasOwnProperty,Ol=e.unstable_scheduleCallback,El=e.unstable_cancelCallback,uu=e.unstable_shouldYield,du=e.unstable_requestPaint,sn=e.unstable_now,fu=e.unstable_getCurrentPriorityLevel,no=e.unstable_ImmediatePriority,ro=e.unstable_UserBlockingPriority,ki=e.unstable_NormalPriority,hu=e.unstable_LowPriority,ao=e.unstable_IdlePriority,pu=e.log,mu=e.unstable_setDisableYieldValue,Ha=null,on=null;function xr(a){if(typeof pu=="function"&&mu(a),on&&typeof on.setStrictMode=="function")try{on.setStrictMode(Ha,a)}catch{}}var cn=Math.clz32?Math.clz32:yu,vu=Math.log,gu=Math.LN2;function yu(a){return a>>>=0,a===0?32:31-(vu(a)/gu|0)|0}var Ai=256,Ci=262144,Ti=4194304;function Ae(a){var i=a&42;if(i!==0)return i;switch(a&-a){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return a&261888;case 262144:case 524288:case 1048576:case 2097152:return a&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return a&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return a}}function pt(a,i,c){var f=a.pendingLanes;if(f===0)return 0;var g=0,b=a.suspendedLanes,O=a.pingedLanes;a=a.warmLanes;var T=f&134217727;return T!==0?(f=T&~b,f!==0?g=Ae(f):(O&=T,O!==0?g=Ae(O):c||(c=T&~a,c!==0&&(g=Ae(c))))):(T=f&~b,T!==0?g=Ae(T):O!==0?g=Ae(O):c||(c=f&~a,c!==0&&(g=Ae(c)))),g===0?0:i!==0&&i!==g&&(i&b)===0&&(b=g&-g,c=i&-i,b>=c||b===32&&(c&4194048)!==0)?i:g}function Vt(a,i){return(a.pendingLanes&~(a.suspendedLanes&~a.pingedLanes)&i)===0}function bu(a,i){switch(a){case 1:case 2:case 4:case 8:case 64:return i+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return i+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function io(){var a=Ti;return Ti<<=1,(Ti&62914560)===0&&(Ti=4194304),a}function kl(a){for(var i=[],c=0;31>c;c++)i.push(a);return i}function jr(a,i){a.pendingLanes|=i,i!==268435456&&(a.suspendedLanes=0,a.pingedLanes=0,a.warmLanes=0)}function tp(a,i,c,f,g,b){var O=a.pendingLanes;a.pendingLanes=c,a.suspendedLanes=0,a.pingedLanes=0,a.warmLanes=0,a.expiredLanes&=c,a.entangledLanes&=c,a.errorRecoveryDisabledLanes&=c,a.shellSuspendCounter=0;var T=a.entanglements,D=a.expirationTimes,H=a.hiddenUpdates;for(c=O&~c;0"u")return null;try{return a.activeElement||a.body}catch{return a.body}}var tR=/[\n"\\]/g;function er(a){return a.replace(tR,function(i){return"\\"+i.charCodeAt(0).toString(16)+" "})}function sp(a,i,c,f,g,b,O,T){a.name="",O!=null&&typeof O!="function"&&typeof O!="symbol"&&typeof O!="boolean"?a.type=O:a.removeAttribute("type"),i!=null?O==="number"?(i===0&&a.value===""||a.value!=i)&&(a.value=""+Jn(i)):a.value!==""+Jn(i)&&(a.value=""+Jn(i)):O!=="submit"&&O!=="reset"||a.removeAttribute("value"),i!=null?op(a,O,Jn(i)):c!=null?op(a,O,Jn(c)):f!=null&&a.removeAttribute("value"),g==null&&b!=null&&(a.defaultChecked=!!b),g!=null&&(a.checked=g&&typeof g!="function"&&typeof g!="symbol"),T!=null&&typeof T!="function"&&typeof T!="symbol"&&typeof T!="boolean"?a.name=""+Jn(T):a.removeAttribute("name")}function k0(a,i,c,f,g,b,O,T){if(b!=null&&typeof b!="function"&&typeof b!="symbol"&&typeof b!="boolean"&&(a.type=b),i!=null||c!=null){if(!(b!=="submit"&&b!=="reset"||i!=null)){lp(a);return}c=c!=null?""+Jn(c):"",i=i!=null?""+Jn(i):c,T||i===a.value||(a.value=i),a.defaultValue=i}f=f??g,f=typeof f!="function"&&typeof f!="symbol"&&!!f,a.checked=T?a.checked:!!f,a.defaultChecked=!!f,O!=null&&typeof O!="function"&&typeof O!="symbol"&&typeof O!="boolean"&&(a.name=O),lp(a)}function op(a,i,c){i==="number"&&_u(a.ownerDocument)===a||a.defaultValue===""+c||(a.defaultValue=""+c)}function Rl(a,i,c,f){if(a=a.options,i){i={};for(var g=0;g"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),hp=!1;if(ra)try{var co={};Object.defineProperty(co,"passive",{get:function(){hp=!0}}),window.addEventListener("test",co,co),window.removeEventListener("test",co,co)}catch{hp=!1}var Ka=null,pp=null,Su=null;function D0(){if(Su)return Su;var a,i=pp,c=i.length,f,g="value"in Ka?Ka.value:Ka.textContent,b=g.length;for(a=0;a=ho),I0=" ",B0=!1;function H0(a,i){switch(a){case"keyup":return AR.indexOf(i.keyCode)!==-1;case"keydown":return i.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function F0(a){return a=a.detail,typeof a=="object"&&"data"in a?a.data:null}var ql=!1;function TR(a,i){switch(a){case"compositionend":return F0(i);case"keypress":return i.which!==32?null:(B0=!0,I0);case"textInput":return a=i.data,a===I0&&B0?null:a;default:return null}}function PR(a,i){if(ql)return a==="compositionend"||!bp&&H0(a,i)?(a=D0(),Su=pp=Ka=null,ql=!1,a):null;switch(a){case"paste":return null;case"keypress":if(!(i.ctrlKey||i.altKey||i.metaKey)||i.ctrlKey&&i.altKey){if(i.char&&1=i)return{node:c,offset:i-a};a=f}e:{for(;c;){if(c.nextSibling){c=c.nextSibling;break e}c=c.parentNode}c=void 0}c=Q0(c)}}function ej(a,i){return a&&i?a===i?!0:a&&a.nodeType===3?!1:i&&i.nodeType===3?ej(a,i.parentNode):"contains"in a?a.contains(i):a.compareDocumentPosition?!!(a.compareDocumentPosition(i)&16):!1:!1}function tj(a){a=a!=null&&a.ownerDocument!=null&&a.ownerDocument.defaultView!=null?a.ownerDocument.defaultView:window;for(var i=_u(a.document);i instanceof a.HTMLIFrameElement;){try{var c=typeof i.contentWindow.location.href=="string"}catch{c=!1}if(c)a=i.contentWindow;else break;i=_u(a.document)}return i}function _p(a){var i=a&&a.nodeName&&a.nodeName.toLowerCase();return i&&(i==="input"&&(a.type==="text"||a.type==="search"||a.type==="tel"||a.type==="url"||a.type==="password")||i==="textarea"||a.contentEditable==="true")}var $R=ra&&"documentMode"in document&&11>=document.documentMode,Ul=null,wp=null,go=null,Sp=!1;function nj(a,i,c){var f=c.window===c?c.document:c.nodeType===9?c:c.ownerDocument;Sp||Ul==null||Ul!==_u(f)||(f=Ul,"selectionStart"in f&&_p(f)?f={start:f.selectionStart,end:f.selectionEnd}:(f=(f.ownerDocument&&f.ownerDocument.defaultView||window).getSelection(),f={anchorNode:f.anchorNode,anchorOffset:f.anchorOffset,focusNode:f.focusNode,focusOffset:f.focusOffset}),go&&vo(go,f)||(go=f,f=gd(wp,"onSelect"),0>=O,g-=O,Pr=1<<32-cn(i)+g|c<Re?(Ue=je,je=null):Ue=je.sibling;var Ke=F(U,je,B[Re],te);if(Ke===null){je===null&&(je=Ue);break}a&&je&&Ke.alternate===null&&i(U,je),q=b(Ke,q,Re),Fe===null?Se=Ke:Fe.sibling=Ke,Fe=Ke,je=Ue}if(Re===B.length)return c(U,je),Ie&&ia(U,Re),Se;if(je===null){for(;ReRe?(Ue=je,je=null):Ue=je.sibling;var hi=F(U,je,Ke.value,te);if(hi===null){je===null&&(je=Ue);break}a&&je&&hi.alternate===null&&i(U,je),q=b(hi,q,Re),Fe===null?Se=hi:Fe.sibling=hi,Fe=hi,je=Ue}if(Ke.done)return c(U,je),Ie&&ia(U,Re),Se;if(je===null){for(;!Ke.done;Re++,Ke=B.next())Ke=ne(U,Ke.value,te),Ke!==null&&(q=b(Ke,q,Re),Fe===null?Se=Ke:Fe.sibling=Ke,Fe=Ke);return Ie&&ia(U,Re),Se}for(je=f(je);!Ke.done;Re++,Ke=B.next())Ke=Z(je,U,Re,Ke.value,te),Ke!==null&&(a&&Ke.alternate!==null&&je.delete(Ke.key===null?Re:Ke.key),q=b(Ke,q,Re),Fe===null?Se=Ke:Fe.sibling=Ke,Fe=Ke);return a&&je.forEach(function(lL){return i(U,lL)}),Ie&&ia(U,Re),Se}function et(U,q,B,te){if(typeof B=="object"&&B!==null&&B.type===w&&B.key===null&&(B=B.props.children),typeof B=="object"&&B!==null){switch(B.$$typeof){case x:e:{for(var Se=B.key;q!==null;){if(q.key===Se){if(Se=B.type,Se===w){if(q.tag===7){c(U,q.sibling),te=g(q,B.props.children),te.return=U,U=te;break e}}else if(q.elementType===Se||typeof Se=="object"&&Se!==null&&Se.$$typeof===L&&Bi(Se)===q.type){c(U,q.sibling),te=g(q,B.props),wo(te,B),te.return=U,U=te;break e}c(U,q);break}else i(U,q);q=q.sibling}B.type===w?(te=zi(B.props.children,U.mode,te,B.key),te.return=U,U=te):(te=Ru(B.type,B.key,B.props,null,U.mode,te),wo(te,B),te.return=U,U=te)}return O(U);case _:e:{for(Se=B.key;q!==null;){if(q.key===Se)if(q.tag===4&&q.stateNode.containerInfo===B.containerInfo&&q.stateNode.implementation===B.implementation){c(U,q.sibling),te=g(q,B.children||[]),te.return=U,U=te;break e}else{c(U,q);break}else i(U,q);q=q.sibling}te=Tp(B,U.mode,te),te.return=U,U=te}return O(U);case L:return B=Bi(B),et(U,q,B,te)}if(ge(B))return be(U,q,B,te);if(I(B)){if(Se=I(B),typeof Se!="function")throw Error(r(150));return B=Se.call(B),Oe(U,q,B,te)}if(typeof B.then=="function")return et(U,q,Iu(B),te);if(B.$$typeof===A)return et(U,q,zu(U,B),te);Bu(U,B)}return typeof B=="string"&&B!==""||typeof B=="number"||typeof B=="bigint"?(B=""+B,q!==null&&q.tag===6?(c(U,q.sibling),te=g(q,B),te.return=U,U=te):(c(U,q),te=Cp(B,U.mode,te),te.return=U,U=te),O(U)):c(U,q)}return function(U,q,B,te){try{_o=0;var Se=et(U,q,B,te);return Wl=null,Se}catch(je){if(je===Xl||je===Uu)throw je;var Fe=zn(29,je,null,U.mode);return Fe.lanes=te,Fe.return=U,Fe}finally{}}}var Fi=Nj(!0),Oj=Nj(!1),Wa=!1;function Hp(a){a.updateQueue={baseState:a.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Fp(a,i){a=a.updateQueue,i.updateQueue===a&&(i.updateQueue={baseState:a.baseState,firstBaseUpdate:a.firstBaseUpdate,lastBaseUpdate:a.lastBaseUpdate,shared:a.shared,callbacks:null})}function Za(a){return{lane:a,tag:0,payload:null,callback:null,next:null}}function Qa(a,i,c){var f=a.updateQueue;if(f===null)return null;if(f=f.shared,(Ve&2)!==0){var g=f.pending;return g===null?i.next=i:(i.next=g.next,g.next=i),f.pending=i,i=Mu(a),cj(a,null,c),i}return Pu(a,f,i,c),Mu(a)}function So(a,i,c){if(i=i.updateQueue,i!==null&&(i=i.shared,(c&4194048)!==0)){var f=i.lanes;f&=a.pendingLanes,c|=f,i.lanes=c,g0(a,c)}}function Kp(a,i){var c=a.updateQueue,f=a.alternate;if(f!==null&&(f=f.updateQueue,c===f)){var g=null,b=null;if(c=c.firstBaseUpdate,c!==null){do{var O={lane:c.lane,tag:c.tag,payload:c.payload,callback:null,next:null};b===null?g=b=O:b=b.next=O,c=c.next}while(c!==null);b===null?g=b=i:b=b.next=i}else g=b=i;c={baseState:f.baseState,firstBaseUpdate:g,lastBaseUpdate:b,shared:f.shared,callbacks:f.callbacks},a.updateQueue=c;return}a=c.lastBaseUpdate,a===null?c.firstBaseUpdate=i:a.next=i,c.lastBaseUpdate=i}var Yp=!1;function No(){if(Yp){var a=Vl;if(a!==null)throw a}}function Oo(a,i,c,f){Yp=!1;var g=a.updateQueue;Wa=!1;var b=g.firstBaseUpdate,O=g.lastBaseUpdate,T=g.shared.pending;if(T!==null){g.shared.pending=null;var D=T,H=D.next;D.next=null,O===null?b=H:O.next=H,O=D;var J=a.alternate;J!==null&&(J=J.updateQueue,T=J.lastBaseUpdate,T!==O&&(T===null?J.firstBaseUpdate=H:T.next=H,J.lastBaseUpdate=D))}if(b!==null){var ne=g.baseState;O=0,J=H=D=null,T=b;do{var F=T.lane&-536870913,Z=F!==T.lane;if(Z?(qe&F)===F:(f&F)===F){F!==0&&F===Gl&&(Yp=!0),J!==null&&(J=J.next={lane:0,tag:T.tag,payload:T.payload,callback:null,next:null});e:{var be=a,Oe=T;F=i;var et=c;switch(Oe.tag){case 1:if(be=Oe.payload,typeof be=="function"){ne=be.call(et,ne,F);break e}ne=be;break e;case 3:be.flags=be.flags&-65537|128;case 0:if(be=Oe.payload,F=typeof be=="function"?be.call(et,ne,F):be,F==null)break e;ne=m({},ne,F);break e;case 2:Wa=!0}}F=T.callback,F!==null&&(a.flags|=64,Z&&(a.flags|=8192),Z=g.callbacks,Z===null?g.callbacks=[F]:Z.push(F))}else Z={lane:F,tag:T.tag,payload:T.payload,callback:T.callback,next:null},J===null?(H=J=Z,D=ne):J=J.next=Z,O|=F;if(T=T.next,T===null){if(T=g.shared.pending,T===null)break;Z=T,T=Z.next,Z.next=null,g.lastBaseUpdate=Z,g.shared.pending=null}}while(!0);J===null&&(D=ne),g.baseState=D,g.firstBaseUpdate=H,g.lastBaseUpdate=J,b===null&&(g.shared.lanes=0),ri|=O,a.lanes=O,a.memoizedState=ne}}function Ej(a,i){if(typeof a!="function")throw Error(r(191,a));a.call(i)}function kj(a,i){var c=a.callbacks;if(c!==null)for(a.callbacks=null,a=0;ab?b:8;var O=$.T,T={};$.T=T,dm(a,!1,i,c);try{var D=g(),H=$.S;if(H!==null&&H(T,D),D!==null&&typeof D=="object"&&typeof D.then=="function"){var J=XR(D,f);Ao(a,i,J,Bn(a))}else Ao(a,i,f,Bn(a))}catch(ne){Ao(a,i,{then:function(){},status:"rejected",reason:ne},Bn())}finally{X.p=b,O!==null&&T.types!==null&&(O.types=T.types),$.T=O}}function tD(){}function cm(a,i,c,f){if(a.tag!==5)throw Error(r(476));var g=l_(a).queue;i_(a,g,i,Y,c===null?tD:function(){return s_(a),c(f)})}function l_(a){var i=a.memoizedState;if(i!==null)return i;i={memoizedState:Y,baseState:Y,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ca,lastRenderedState:Y},next:null};var c={};return i.next={memoizedState:c,baseState:c,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ca,lastRenderedState:c},next:null},a.memoizedState=i,a=a.alternate,a!==null&&(a.memoizedState=i),i}function s_(a){var i=l_(a);i.next===null&&(i=a.alternate.memoizedState),Ao(a,i.next.queue,{},Bn())}function um(){return Zt(Yo)}function o_(){return jt().memoizedState}function c_(){return jt().memoizedState}function nD(a){for(var i=a.return;i!==null;){switch(i.tag){case 24:case 3:var c=Bn();a=Za(c);var f=Qa(i,a,c);f!==null&&(Cn(f,i,c),So(f,i,c)),i={cache:Up()},a.payload=i;return}i=i.return}}function rD(a,i,c){var f=Bn();c={lane:f,revertLane:0,gesture:null,action:c,hasEagerState:!1,eagerState:null,next:null},Qu(a)?d_(i,c):(c=kp(a,i,c,f),c!==null&&(Cn(c,a,f),f_(c,i,f)))}function u_(a,i,c){var f=Bn();Ao(a,i,c,f)}function Ao(a,i,c,f){var g={lane:f,revertLane:0,gesture:null,action:c,hasEagerState:!1,eagerState:null,next:null};if(Qu(a))d_(i,g);else{var b=a.alternate;if(a.lanes===0&&(b===null||b.lanes===0)&&(b=i.lastRenderedReducer,b!==null))try{var O=i.lastRenderedState,T=b(O,c);if(g.hasEagerState=!0,g.eagerState=T,Ln(T,O))return Pu(a,i,g,0),nt===null&&Tu(),!1}catch{}finally{}if(c=kp(a,i,g,f),c!==null)return Cn(c,a,f),f_(c,i,f),!0}return!1}function dm(a,i,c,f){if(f={lane:2,revertLane:Hm(),gesture:null,action:f,hasEagerState:!1,eagerState:null,next:null},Qu(a)){if(i)throw Error(r(479))}else i=kp(a,c,f,2),i!==null&&Cn(i,a,2)}function Qu(a){var i=a.alternate;return a===Te||i!==null&&i===Te}function d_(a,i){Ql=Ku=!0;var c=a.pending;c===null?i.next=i:(i.next=c.next,c.next=i),a.pending=i}function f_(a,i,c){if((c&4194048)!==0){var f=i.lanes;f&=a.pendingLanes,c|=f,i.lanes=c,g0(a,c)}}var Co={readContext:Zt,use:Vu,useCallback:mt,useContext:mt,useEffect:mt,useImperativeHandle:mt,useLayoutEffect:mt,useInsertionEffect:mt,useMemo:mt,useReducer:mt,useRef:mt,useState:mt,useDebugValue:mt,useDeferredValue:mt,useTransition:mt,useSyncExternalStore:mt,useId:mt,useHostTransitionStatus:mt,useFormState:mt,useActionState:mt,useOptimistic:mt,useMemoCache:mt,useCacheRefresh:mt};Co.useEffectEvent=mt;var h_={readContext:Zt,use:Vu,useCallback:function(a,i){return vn().memoizedState=[a,i===void 0?null:i],a},useContext:Zt,useEffect:Wj,useImperativeHandle:function(a,i,c){c=c!=null?c.concat([a]):null,Wu(4194308,4,e_.bind(null,i,a),c)},useLayoutEffect:function(a,i){return Wu(4194308,4,a,i)},useInsertionEffect:function(a,i){Wu(4,2,a,i)},useMemo:function(a,i){var c=vn();i=i===void 0?null:i;var f=a();if(Ki){xr(!0);try{a()}finally{xr(!1)}}return c.memoizedState=[f,i],f},useReducer:function(a,i,c){var f=vn();if(c!==void 0){var g=c(i);if(Ki){xr(!0);try{c(i)}finally{xr(!1)}}}else g=i;return f.memoizedState=f.baseState=g,a={pending:null,lanes:0,dispatch:null,lastRenderedReducer:a,lastRenderedState:g},f.queue=a,a=a.dispatch=rD.bind(null,Te,a),[f.memoizedState,a]},useRef:function(a){var i=vn();return a={current:a},i.memoizedState=a},useState:function(a){a=am(a);var i=a.queue,c=u_.bind(null,Te,i);return i.dispatch=c,[a.memoizedState,c]},useDebugValue:sm,useDeferredValue:function(a,i){var c=vn();return om(c,a,i)},useTransition:function(){var a=am(!1);return a=i_.bind(null,Te,a.queue,!0,!1),vn().memoizedState=a,[!1,a]},useSyncExternalStore:function(a,i,c){var f=Te,g=vn();if(Ie){if(c===void 0)throw Error(r(407));c=c()}else{if(c=i(),nt===null)throw Error(r(349));(qe&127)!==0||Rj(f,i,c)}g.memoizedState=c;var b={value:c,getSnapshot:i};return g.queue=b,Wj(Lj.bind(null,f,b,a),[a]),f.flags|=2048,es(9,{destroy:void 0},Dj.bind(null,f,b,c,i),null),c},useId:function(){var a=vn(),i=nt.identifierPrefix;if(Ie){var c=Mr,f=Pr;c=(f&~(1<<32-cn(f)-1)).toString(32)+c,i="_"+i+"R_"+c,c=Yu++,0<\/script>",b=b.removeChild(b.firstChild);break;case"select":b=typeof f.is=="string"?O.createElement("select",{is:f.is}):O.createElement("select"),f.multiple?b.multiple=!0:f.size&&(b.size=f.size);break;default:b=typeof f.is=="string"?O.createElement(g,{is:f.is}):O.createElement(g)}}b[Xt]=i,b[Sn]=f;e:for(O=i.child;O!==null;){if(O.tag===5||O.tag===6)b.appendChild(O.stateNode);else if(O.tag!==4&&O.tag!==27&&O.child!==null){O.child.return=O,O=O.child;continue}if(O===i)break e;for(;O.sibling===null;){if(O.return===null||O.return===i)break e;O=O.return}O.sibling.return=O.return,O=O.sibling}i.stateNode=b;e:switch(Jt(b,g,f),g){case"button":case"input":case"select":case"textarea":f=!!f.autoFocus;break e;case"img":f=!0;break e;default:f=!1}f&&da(i)}}return lt(i),Nm(i,i.type,a===null?null:a.memoizedProps,i.pendingProps,c),null;case 6:if(a&&i.stateNode!=null)a.memoizedProps!==f&&da(i);else{if(typeof f!="string"&&i.stateNode===null)throw Error(r(166));if(a=Ee.current,Kl(i)){if(a=i.stateNode,c=i.memoizedProps,f=null,g=Wt,g!==null)switch(g.tag){case 27:case 5:f=g.memoizedProps}a[Xt]=i,a=!!(a.nodeValue===c||f!==null&&f.suppressHydrationWarning===!0||Pw(a.nodeValue,c)),a||Va(i,!0)}else a=yd(a).createTextNode(f),a[Xt]=i,i.stateNode=a}return lt(i),null;case 31:if(c=i.memoizedState,a===null||a.memoizedState!==null){if(f=Kl(i),c!==null){if(a===null){if(!f)throw Error(r(318));if(a=i.memoizedState,a=a!==null?a.dehydrated:null,!a)throw Error(r(557));a[Xt]=i}else qi(),(i.flags&128)===0&&(i.memoizedState=null),i.flags|=4;lt(i),a=!1}else c=Dp(),a!==null&&a.memoizedState!==null&&(a.memoizedState.hydrationErrors=c),a=!0;if(!a)return i.flags&256?(Un(i),i):(Un(i),null);if((i.flags&128)!==0)throw Error(r(558))}return lt(i),null;case 13:if(f=i.memoizedState,a===null||a.memoizedState!==null&&a.memoizedState.dehydrated!==null){if(g=Kl(i),f!==null&&f.dehydrated!==null){if(a===null){if(!g)throw Error(r(318));if(g=i.memoizedState,g=g!==null?g.dehydrated:null,!g)throw Error(r(317));g[Xt]=i}else qi(),(i.flags&128)===0&&(i.memoizedState=null),i.flags|=4;lt(i),g=!1}else g=Dp(),a!==null&&a.memoizedState!==null&&(a.memoizedState.hydrationErrors=g),g=!0;if(!g)return i.flags&256?(Un(i),i):(Un(i),null)}return Un(i),(i.flags&128)!==0?(i.lanes=c,i):(c=f!==null,a=a!==null&&a.memoizedState!==null,c&&(f=i.child,g=null,f.alternate!==null&&f.alternate.memoizedState!==null&&f.alternate.memoizedState.cachePool!==null&&(g=f.alternate.memoizedState.cachePool.pool),b=null,f.memoizedState!==null&&f.memoizedState.cachePool!==null&&(b=f.memoizedState.cachePool.pool),b!==g&&(f.flags|=2048)),c!==a&&c&&(i.child.flags|=8192),rd(i,i.updateQueue),lt(i),null);case 4:return se(),a===null&&Gm(i.stateNode.containerInfo),lt(i),null;case 10:return sa(i.type),lt(i),null;case 19:if(Q(xt),f=i.memoizedState,f===null)return lt(i),null;if(g=(i.flags&128)!==0,b=f.rendering,b===null)if(g)Po(f,!1);else{if(vt!==0||a!==null&&(a.flags&128)!==0)for(a=i.child;a!==null;){if(b=Fu(a),b!==null){for(i.flags|=128,Po(f,!1),a=b.updateQueue,i.updateQueue=a,rd(i,a),i.subtreeFlags=0,a=c,c=i.child;c!==null;)uj(c,a),c=c.sibling;return ce(xt,xt.current&1|2),Ie&&ia(i,f.treeForkCount),i.child}a=a.sibling}f.tail!==null&&sn()>od&&(i.flags|=128,g=!0,Po(f,!1),i.lanes=4194304)}else{if(!g)if(a=Fu(b),a!==null){if(i.flags|=128,g=!0,a=a.updateQueue,i.updateQueue=a,rd(i,a),Po(f,!0),f.tail===null&&f.tailMode==="hidden"&&!b.alternate&&!Ie)return lt(i),null}else 2*sn()-f.renderingStartTime>od&&c!==536870912&&(i.flags|=128,g=!0,Po(f,!1),i.lanes=4194304);f.isBackwards?(b.sibling=i.child,i.child=b):(a=f.last,a!==null?a.sibling=b:i.child=b,f.last=b)}return f.tail!==null?(a=f.tail,f.rendering=a,f.tail=a.sibling,f.renderingStartTime=sn(),a.sibling=null,c=xt.current,ce(xt,g?c&1|2:c&1),Ie&&ia(i,f.treeForkCount),a):(lt(i),null);case 22:case 23:return Un(i),Vp(),f=i.memoizedState!==null,a!==null?a.memoizedState!==null!==f&&(i.flags|=8192):f&&(i.flags|=8192),f?(c&536870912)!==0&&(i.flags&128)===0&&(lt(i),i.subtreeFlags&6&&(i.flags|=8192)):lt(i),c=i.updateQueue,c!==null&&rd(i,c.retryQueue),c=null,a!==null&&a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(c=a.memoizedState.cachePool.pool),f=null,i.memoizedState!==null&&i.memoizedState.cachePool!==null&&(f=i.memoizedState.cachePool.pool),f!==c&&(i.flags|=2048),a!==null&&Q(Ii),null;case 24:return c=null,a!==null&&(c=a.memoizedState.cache),i.memoizedState.cache!==c&&(i.flags|=2048),sa(Ot),lt(i),null;case 25:return null;case 30:return null}throw Error(r(156,i.tag))}function oD(a,i){switch(Mp(i),i.tag){case 1:return a=i.flags,a&65536?(i.flags=a&-65537|128,i):null;case 3:return sa(Ot),se(),a=i.flags,(a&65536)!==0&&(a&128)===0?(i.flags=a&-65537|128,i):null;case 26:case 27:case 5:return Me(i),null;case 31:if(i.memoizedState!==null){if(Un(i),i.alternate===null)throw Error(r(340));qi()}return a=i.flags,a&65536?(i.flags=a&-65537|128,i):null;case 13:if(Un(i),a=i.memoizedState,a!==null&&a.dehydrated!==null){if(i.alternate===null)throw Error(r(340));qi()}return a=i.flags,a&65536?(i.flags=a&-65537|128,i):null;case 19:return Q(xt),null;case 4:return se(),null;case 10:return sa(i.type),null;case 22:case 23:return Un(i),Vp(),a!==null&&Q(Ii),a=i.flags,a&65536?(i.flags=a&-65537|128,i):null;case 24:return sa(Ot),null;case 25:return null;default:return null}}function z_(a,i){switch(Mp(i),i.tag){case 3:sa(Ot),se();break;case 26:case 27:case 5:Me(i);break;case 4:se();break;case 31:i.memoizedState!==null&&Un(i);break;case 13:Un(i);break;case 19:Q(xt);break;case 10:sa(i.type);break;case 22:case 23:Un(i),Vp(),a!==null&&Q(Ii);break;case 24:sa(Ot)}}function Mo(a,i){try{var c=i.updateQueue,f=c!==null?c.lastEffect:null;if(f!==null){var g=f.next;c=g;do{if((c.tag&a)===a){f=void 0;var b=c.create,O=c.inst;f=b(),O.destroy=f}c=c.next}while(c!==g)}}catch(T){We(i,i.return,T)}}function ti(a,i,c){try{var f=i.updateQueue,g=f!==null?f.lastEffect:null;if(g!==null){var b=g.next;f=b;do{if((f.tag&a)===a){var O=f.inst,T=O.destroy;if(T!==void 0){O.destroy=void 0,g=i;var D=c,H=T;try{H()}catch(J){We(g,D,J)}}}f=f.next}while(f!==b)}}catch(J){We(i,i.return,J)}}function q_(a){var i=a.updateQueue;if(i!==null){var c=a.stateNode;try{kj(i,c)}catch(f){We(a,a.return,f)}}}function U_(a,i,c){c.props=Yi(a.type,a.memoizedProps),c.state=a.memoizedState;try{c.componentWillUnmount()}catch(f){We(a,i,f)}}function Ro(a,i){try{var c=a.ref;if(c!==null){switch(a.tag){case 26:case 27:case 5:var f=a.stateNode;break;case 30:f=a.stateNode;break;default:f=a.stateNode}typeof c=="function"?a.refCleanup=c(f):c.current=f}}catch(g){We(a,i,g)}}function Rr(a,i){var c=a.ref,f=a.refCleanup;if(c!==null)if(typeof f=="function")try{f()}catch(g){We(a,i,g)}finally{a.refCleanup=null,a=a.alternate,a!=null&&(a.refCleanup=null)}else if(typeof c=="function")try{c(null)}catch(g){We(a,i,g)}else c.current=null}function $_(a){var i=a.type,c=a.memoizedProps,f=a.stateNode;try{e:switch(i){case"button":case"input":case"select":case"textarea":c.autoFocus&&f.focus();break e;case"img":c.src?f.src=c.src:c.srcSet&&(f.srcset=c.srcSet)}}catch(g){We(a,a.return,g)}}function Om(a,i,c){try{var f=a.stateNode;CD(f,a.type,c,i),f[Sn]=i}catch(g){We(a,a.return,g)}}function I_(a){return a.tag===5||a.tag===3||a.tag===26||a.tag===27&&oi(a.type)||a.tag===4}function Em(a){e:for(;;){for(;a.sibling===null;){if(a.return===null||I_(a.return))return null;a=a.return}for(a.sibling.return=a.return,a=a.sibling;a.tag!==5&&a.tag!==6&&a.tag!==18;){if(a.tag===27&&oi(a.type)||a.flags&2||a.child===null||a.tag===4)continue e;a.child.return=a,a=a.child}if(!(a.flags&2))return a.stateNode}}function km(a,i,c){var f=a.tag;if(f===5||f===6)a=a.stateNode,i?(c.nodeType===9?c.body:c.nodeName==="HTML"?c.ownerDocument.body:c).insertBefore(a,i):(i=c.nodeType===9?c.body:c.nodeName==="HTML"?c.ownerDocument.body:c,i.appendChild(a),c=c._reactRootContainer,c!=null||i.onclick!==null||(i.onclick=na));else if(f!==4&&(f===27&&oi(a.type)&&(c=a.stateNode,i=null),a=a.child,a!==null))for(km(a,i,c),a=a.sibling;a!==null;)km(a,i,c),a=a.sibling}function ad(a,i,c){var f=a.tag;if(f===5||f===6)a=a.stateNode,i?c.insertBefore(a,i):c.appendChild(a);else if(f!==4&&(f===27&&oi(a.type)&&(c=a.stateNode),a=a.child,a!==null))for(ad(a,i,c),a=a.sibling;a!==null;)ad(a,i,c),a=a.sibling}function B_(a){var i=a.stateNode,c=a.memoizedProps;try{for(var f=a.type,g=i.attributes;g.length;)i.removeAttributeNode(g[0]);Jt(i,f,c),i[Xt]=a,i[Sn]=c}catch(b){We(a,a.return,b)}}var fa=!1,At=!1,Am=!1,H_=typeof WeakSet=="function"?WeakSet:Set,$t=null;function cD(a,i){if(a=a.containerInfo,Wm=Nd,a=tj(a),_p(a)){if("selectionStart"in a)var c={start:a.selectionStart,end:a.selectionEnd};else e:{c=(c=a.ownerDocument)&&c.defaultView||window;var f=c.getSelection&&c.getSelection();if(f&&f.rangeCount!==0){c=f.anchorNode;var g=f.anchorOffset,b=f.focusNode;f=f.focusOffset;try{c.nodeType,b.nodeType}catch{c=null;break e}var O=0,T=-1,D=-1,H=0,J=0,ne=a,F=null;t:for(;;){for(var Z;ne!==c||g!==0&&ne.nodeType!==3||(T=O+g),ne!==b||f!==0&&ne.nodeType!==3||(D=O+f),ne.nodeType===3&&(O+=ne.nodeValue.length),(Z=ne.firstChild)!==null;)F=ne,ne=Z;for(;;){if(ne===a)break t;if(F===c&&++H===g&&(T=O),F===b&&++J===f&&(D=O),(Z=ne.nextSibling)!==null)break;ne=F,F=ne.parentNode}ne=Z}c=T===-1||D===-1?null:{start:T,end:D}}else c=null}c=c||{start:0,end:0}}else c=null;for(Zm={focusedElem:a,selectionRange:c},Nd=!1,$t=i;$t!==null;)if(i=$t,a=i.child,(i.subtreeFlags&1028)!==0&&a!==null)a.return=i,$t=a;else for(;$t!==null;){switch(i=$t,b=i.alternate,a=i.flags,i.tag){case 0:if((a&4)!==0&&(a=i.updateQueue,a=a!==null?a.events:null,a!==null))for(c=0;c title"))),Jt(b,f,c),b[Xt]=a,Ut(b),f=b;break e;case"link":var O=Xw("link","href",g).get(f+(c.href||""));if(O){for(var T=0;Tet&&(O=et,et=Oe,Oe=O);var U=J0(T,Oe),q=J0(T,et);if(U&&q&&(Z.rangeCount!==1||Z.anchorNode!==U.node||Z.anchorOffset!==U.offset||Z.focusNode!==q.node||Z.focusOffset!==q.offset)){var B=ne.createRange();B.setStart(U.node,U.offset),Z.removeAllRanges(),Oe>et?(Z.addRange(B),Z.extend(q.node,q.offset)):(B.setEnd(q.node,q.offset),Z.addRange(B))}}}}for(ne=[],Z=T;Z=Z.parentNode;)Z.nodeType===1&&ne.push({element:Z,left:Z.scrollLeft,top:Z.scrollTop});for(typeof T.focus=="function"&&T.focus(),T=0;Tc?32:c,$.T=null,c=Lm,Lm=null;var b=ii,O=ga;if(Mt=0,is=ii=null,ga=0,(Ve&6)!==0)throw Error(r(331));var T=Ve;if(Ve|=4,ew(b.current),Z_(b,b.current,O,c),Ve=T,$o(0,!1),on&&typeof on.onPostCommitFiberRoot=="function")try{on.onPostCommitFiberRoot(Ha,b)}catch{}return!0}finally{X.p=g,$.T=f,yw(a,i)}}function xw(a,i,c){i=nr(c,i),i=mm(a.stateNode,i,2),a=Qa(a,i,2),a!==null&&(jr(a,2),Dr(a))}function We(a,i,c){if(a.tag===3)xw(a,a,c);else for(;i!==null;){if(i.tag===3){xw(i,a,c);break}else if(i.tag===1){var f=i.stateNode;if(typeof i.type.getDerivedStateFromError=="function"||typeof f.componentDidCatch=="function"&&(ai===null||!ai.has(f))){a=nr(c,a),c=j_(2),f=Qa(i,c,2),f!==null&&(__(c,f,i,a),jr(f,2),Dr(f));break}}i=i.return}}function $m(a,i,c){var f=a.pingCache;if(f===null){f=a.pingCache=new fD;var g=new Set;f.set(i,g)}else g=f.get(i),g===void 0&&(g=new Set,f.set(i,g));g.has(c)||(Pm=!0,g.add(c),a=gD.bind(null,a,i,c),i.then(a,a))}function gD(a,i,c){var f=a.pingCache;f!==null&&f.delete(i),a.pingedLanes|=a.suspendedLanes&c,a.warmLanes&=~c,nt===a&&(qe&c)===c&&(vt===4||vt===3&&(qe&62914560)===qe&&300>sn()-sd?(Ve&2)===0&&ls(a,0):Mm|=c,as===qe&&(as=0)),Dr(a)}function jw(a,i){i===0&&(i=io()),a=Li(a,i),a!==null&&(jr(a,i),Dr(a))}function yD(a){var i=a.memoizedState,c=0;i!==null&&(c=i.retryLane),jw(a,c)}function bD(a,i){var c=0;switch(a.tag){case 31:case 13:var f=a.stateNode,g=a.memoizedState;g!==null&&(c=g.retryLane);break;case 19:f=a.stateNode;break;case 22:f=a.stateNode._retryCache;break;default:throw Error(r(314))}f!==null&&f.delete(i),jw(a,c)}function xD(a,i){return Ol(a,i)}var pd=null,os=null,Im=!1,md=!1,Bm=!1,si=0;function Dr(a){a!==os&&a.next===null&&(os===null?pd=os=a:os=os.next=a),md=!0,Im||(Im=!0,_D())}function $o(a,i){if(!Bm&&md){Bm=!0;do for(var c=!1,f=pd;f!==null;){if(a!==0){var g=f.pendingLanes;if(g===0)var b=0;else{var O=f.suspendedLanes,T=f.pingedLanes;b=(1<<31-cn(42|a)+1)-1,b&=g&~(O&~T),b=b&201326741?b&201326741|1:b?b|2:0}b!==0&&(c=!0,Nw(f,b))}else b=qe,b=pt(f,f===nt?b:0,f.cancelPendingCommit!==null||f.timeoutHandle!==-1),(b&3)===0||Vt(f,b)||(c=!0,Nw(f,b));f=f.next}while(c);Bm=!1}}function jD(){_w()}function _w(){md=Im=!1;var a=0;si!==0&&PD()&&(a=si);for(var i=sn(),c=null,f=pd;f!==null;){var g=f.next,b=ww(f,i);b===0?(f.next=null,c===null?pd=g:c.next=g,g===null&&(os=c)):(c=f,(a!==0||(b&3)!==0)&&(md=!0)),f=g}Mt!==0&&Mt!==5||$o(a),si!==0&&(si=0)}function ww(a,i){for(var c=a.suspendedLanes,f=a.pingedLanes,g=a.expirationTimes,b=a.pendingLanes&-62914561;0T)break;var J=D.transferSize,ne=D.initiatorType;J&&Mw(ne)&&(D=D.responseEnd,O+=J*(D"u"?null:document;function Kw(a,i,c){var f=cs;if(f&&typeof i=="string"&&i){var g=er(i);g='link[rel="'+a+'"][href="'+g+'"]',typeof c=="string"&&(g+='[crossorigin="'+c+'"]'),Fw.has(g)||(Fw.add(g),a={rel:a,crossOrigin:c,href:i},f.querySelector(g)===null&&(i=f.createElement("link"),Jt(i,"link",a),Ut(i),f.head.appendChild(i)))}}function ID(a){ya.D(a),Kw("dns-prefetch",a,null)}function BD(a,i){ya.C(a,i),Kw("preconnect",a,i)}function HD(a,i,c){ya.L(a,i,c);var f=cs;if(f&&a&&i){var g='link[rel="preload"][as="'+er(i)+'"]';i==="image"&&c&&c.imageSrcSet?(g+='[imagesrcset="'+er(c.imageSrcSet)+'"]',typeof c.imageSizes=="string"&&(g+='[imagesizes="'+er(c.imageSizes)+'"]')):g+='[href="'+er(a)+'"]';var b=g;switch(i){case"style":b=us(a);break;case"script":b=ds(a)}or.has(b)||(a=m({rel:"preload",href:i==="image"&&c&&c.imageSrcSet?void 0:a,as:i},c),or.set(b,a),f.querySelector(g)!==null||i==="style"&&f.querySelector(Fo(b))||i==="script"&&f.querySelector(Ko(b))||(i=f.createElement("link"),Jt(i,"link",a),Ut(i),f.head.appendChild(i)))}}function FD(a,i){ya.m(a,i);var c=cs;if(c&&a){var f=i&&typeof i.as=="string"?i.as:"script",g='link[rel="modulepreload"][as="'+er(f)+'"][href="'+er(a)+'"]',b=g;switch(f){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":b=ds(a)}if(!or.has(b)&&(a=m({rel:"modulepreload",href:a},i),or.set(b,a),c.querySelector(g)===null)){switch(f){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(c.querySelector(Ko(b)))return}f=c.createElement("link"),Jt(f,"link",a),Ut(f),c.head.appendChild(f)}}}function KD(a,i,c){ya.S(a,i,c);var f=cs;if(f&&a){var g=Pl(f).hoistableStyles,b=us(a);i=i||"default";var O=g.get(b);if(!O){var T={loading:0,preload:null};if(O=f.querySelector(Fo(b)))T.loading=5;else{a=m({rel:"stylesheet",href:a,"data-precedence":i},c),(c=or.get(b))&&av(a,c);var D=O=f.createElement("link");Ut(D),Jt(D,"link",a),D._p=new Promise(function(H,J){D.onload=H,D.onerror=J}),D.addEventListener("load",function(){T.loading|=1}),D.addEventListener("error",function(){T.loading|=2}),T.loading|=4,xd(O,i,f)}O={type:"stylesheet",instance:O,count:1,state:T},g.set(b,O)}}}function YD(a,i){ya.X(a,i);var c=cs;if(c&&a){var f=Pl(c).hoistableScripts,g=ds(a),b=f.get(g);b||(b=c.querySelector(Ko(g)),b||(a=m({src:a,async:!0},i),(i=or.get(g))&&iv(a,i),b=c.createElement("script"),Ut(b),Jt(b,"link",a),c.head.appendChild(b)),b={type:"script",instance:b,count:1,state:null},f.set(g,b))}}function GD(a,i){ya.M(a,i);var c=cs;if(c&&a){var f=Pl(c).hoistableScripts,g=ds(a),b=f.get(g);b||(b=c.querySelector(Ko(g)),b||(a=m({src:a,async:!0,type:"module"},i),(i=or.get(g))&&iv(a,i),b=c.createElement("script"),Ut(b),Jt(b,"link",a),c.head.appendChild(b)),b={type:"script",instance:b,count:1,state:null},f.set(g,b))}}function Yw(a,i,c,f){var g=(g=Ee.current)?bd(g):null;if(!g)throw Error(r(446));switch(a){case"meta":case"title":return null;case"style":return typeof c.precedence=="string"&&typeof c.href=="string"?(i=us(c.href),c=Pl(g).hoistableStyles,f=c.get(i),f||(f={type:"style",instance:null,count:0,state:null},c.set(i,f)),f):{type:"void",instance:null,count:0,state:null};case"link":if(c.rel==="stylesheet"&&typeof c.href=="string"&&typeof c.precedence=="string"){a=us(c.href);var b=Pl(g).hoistableStyles,O=b.get(a);if(O||(g=g.ownerDocument||g,O={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},b.set(a,O),(b=g.querySelector(Fo(a)))&&!b._p&&(O.instance=b,O.state.loading=5),or.has(a)||(c={rel:"preload",as:"style",href:c.href,crossOrigin:c.crossOrigin,integrity:c.integrity,media:c.media,hrefLang:c.hrefLang,referrerPolicy:c.referrerPolicy},or.set(a,c),b||VD(g,a,c,O.state))),i&&f===null)throw Error(r(528,""));return O}if(i&&f!==null)throw Error(r(529,""));return null;case"script":return i=c.async,c=c.src,typeof c=="string"&&i&&typeof i!="function"&&typeof i!="symbol"?(i=ds(c),c=Pl(g).hoistableScripts,f=c.get(i),f||(f={type:"script",instance:null,count:0,state:null},c.set(i,f)),f):{type:"void",instance:null,count:0,state:null};default:throw Error(r(444,a))}}function us(a){return'href="'+er(a)+'"'}function Fo(a){return'link[rel="stylesheet"]['+a+"]"}function Gw(a){return m({},a,{"data-precedence":a.precedence,precedence:null})}function VD(a,i,c,f){a.querySelector('link[rel="preload"][as="style"]['+i+"]")?f.loading=1:(i=a.createElement("link"),f.preload=i,i.addEventListener("load",function(){return f.loading|=1}),i.addEventListener("error",function(){return f.loading|=2}),Jt(i,"link",c),Ut(i),a.head.appendChild(i))}function ds(a){return'[src="'+er(a)+'"]'}function Ko(a){return"script[async]"+a}function Vw(a,i,c){if(i.count++,i.instance===null)switch(i.type){case"style":var f=a.querySelector('style[data-href~="'+er(c.href)+'"]');if(f)return i.instance=f,Ut(f),f;var g=m({},c,{"data-href":c.href,"data-precedence":c.precedence,href:null,precedence:null});return f=(a.ownerDocument||a).createElement("style"),Ut(f),Jt(f,"style",g),xd(f,c.precedence,a),i.instance=f;case"stylesheet":g=us(c.href);var b=a.querySelector(Fo(g));if(b)return i.state.loading|=4,i.instance=b,Ut(b),b;f=Gw(c),(g=or.get(g))&&av(f,g),b=(a.ownerDocument||a).createElement("link"),Ut(b);var O=b;return O._p=new Promise(function(T,D){O.onload=T,O.onerror=D}),Jt(b,"link",f),i.state.loading|=4,xd(b,c.precedence,a),i.instance=b;case"script":return b=ds(c.src),(g=a.querySelector(Ko(b)))?(i.instance=g,Ut(g),g):(f=c,(g=or.get(b))&&(f=m({},c),iv(f,g)),a=a.ownerDocument||a,g=a.createElement("script"),Ut(g),Jt(g,"link",f),a.head.appendChild(g),i.instance=g);case"void":return null;default:throw Error(r(443,i.type))}else i.type==="stylesheet"&&(i.state.loading&4)===0&&(f=i.instance,i.state.loading|=4,xd(f,c.precedence,a));return i.instance}function xd(a,i,c){for(var f=c.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),g=f.length?f[f.length-1]:null,b=g,O=0;O title"):null)}function XD(a,i,c){if(c===1||i.itemProp!=null)return!1;switch(a){case"meta":case"title":return!0;case"style":if(typeof i.precedence!="string"||typeof i.href!="string"||i.href==="")break;return!0;case"link":if(typeof i.rel!="string"||typeof i.href!="string"||i.href===""||i.onLoad||i.onError)break;switch(i.rel){case"stylesheet":return a=i.disabled,typeof i.precedence=="string"&&a==null;default:return!0}case"script":if(i.async&&typeof i.async!="function"&&typeof i.async!="symbol"&&!i.onLoad&&!i.onError&&i.src&&typeof i.src=="string")return!0}return!1}function Zw(a){return!(a.type==="stylesheet"&&(a.state.loading&3)===0)}function WD(a,i,c,f){if(c.type==="stylesheet"&&(typeof f.media!="string"||matchMedia(f.media).matches!==!1)&&(c.state.loading&4)===0){if(c.instance===null){var g=us(f.href),b=i.querySelector(Fo(g));if(b){i=b._p,i!==null&&typeof i=="object"&&typeof i.then=="function"&&(a.count++,a=_d.bind(a),i.then(a,a)),c.state.loading|=4,c.instance=b,Ut(b);return}b=i.ownerDocument||i,f=Gw(f),(g=or.get(g))&&av(f,g),b=b.createElement("link"),Ut(b);var O=b;O._p=new Promise(function(T,D){O.onload=T,O.onerror=D}),Jt(b,"link",f),c.instance=b}a.stylesheets===null&&(a.stylesheets=new Map),a.stylesheets.set(c,i),(i=c.state.preload)&&(c.state.loading&3)===0&&(a.count++,c=_d.bind(a),i.addEventListener("load",c),i.addEventListener("error",c))}}var lv=0;function ZD(a,i){return a.stylesheets&&a.count===0&&Sd(a,a.stylesheets),0lv?50:800)+i);return a.unsuspend=c,function(){a.unsuspend=null,clearTimeout(f),clearTimeout(g)}}:null}function _d(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Sd(this,this.stylesheets);else if(this.unsuspend){var a=this.unsuspend;this.unsuspend=null,a()}}}var wd=null;function Sd(a,i){a.stylesheets=null,a.unsuspend!==null&&(a.count++,wd=new Map,i.forEach(QD,a),wd=null,_d.call(a))}function QD(a,i){if(!(i.state.loading&4)){var c=wd.get(a);if(c)var f=c.get(null);else{c=new Map,wd.set(a,c);for(var g=a.querySelectorAll("link[data-precedence],style[data-precedence]"),b=0;b"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),vv.exports=gL(),vv.exports}var bL=yL();const xL=Ma(bL);/** * react-router v7.13.2 * * Copyright (c) Remix Software Inc. @@ -55,14 +55,14 @@ Error generating stack: `+f.message+` * LICENSE.md file in the root directory of this source tree. * * @license MIT - */var j1="popstate";function _1(e){return typeof e=="object"&&e!=null&&"pathname"in e&&"search"in e&&"hash"in e&&"state"in e&&"key"in e}function jL(e={}){function t(r,l){var p;let o=(p=l.state)==null?void 0:p.masked,{pathname:u,search:d,hash:h}=o||r.location;return hy("",{pathname:u,search:d,hash:h},l.state&&l.state.usr||null,l.state&&l.state.key||"default",o?{pathname:r.location.pathname,search:r.location.search,hash:r.location.hash}:void 0)}function n(r,l){return typeof l=="string"?l:jc(l)}return wL(t,n,null,e)}function ft(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function vr(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function _L(){return Math.random().toString(36).substring(2,10)}function w1(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.unstable_mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function hy(e,t,n=null,r,l){return{pathname:typeof e=="string"?e:e.pathname,search:"",hash:"",...typeof t=="string"?Hs(t):t,state:n,key:t&&t.key||r||_L(),unstable_mask:l}}function jc({pathname:e="/",search:t="",hash:n=""}){return t&&t!=="?"&&(e+=t.charAt(0)==="?"?t:"?"+t),n&&n!=="#"&&(e+=n.charAt(0)==="#"?n:"#"+n),e}function Hs(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf("?");r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function wL(e,t,n,r={}){let{window:l=document.defaultView,v5Compat:o=!1}=r,u=l.history,d="POP",h=null,p=v();p==null&&(p=0,u.replaceState({...u.state,idx:p},""));function v(){return(u.state||{idx:null}).idx}function m(){d="POP";let S=v(),N=S==null?null:S-p;p=S,h&&h({action:d,location:w.location,delta:N})}function y(S,N){d="PUSH";let E=_1(S)?S:hy(w.location,S,N);p=v()+1;let T=w1(E,p),R=w.createHref(E.unstable_mask||E);try{u.pushState(T,"",R)}catch(k){if(k instanceof DOMException&&k.name==="DataCloneError")throw k;l.location.assign(R)}o&&h&&h({action:d,location:w.location,delta:1})}function x(S,N){d="REPLACE";let E=_1(S)?S:hy(w.location,S,N);p=v();let T=w1(E,p),R=w.createHref(E.unstable_mask||E);u.replaceState(T,"",R),o&&h&&h({action:d,location:w.location,delta:0})}function _(S){return SL(S)}let w={get action(){return d},get location(){return e(l,u)},listen(S){if(h)throw new Error("A history only accepts one active listener");return l.addEventListener(j1,m),h=S,()=>{l.removeEventListener(j1,m),h=null}},createHref(S){return t(l,S)},createURL:_,encodeLocation(S){let N=_(S);return{pathname:N.pathname,search:N.search,hash:N.hash}},push:y,replace:x,go(S){return u.go(S)}};return w}function SL(e,t=!1){let n="http://localhost";typeof window<"u"&&(n=window.location.origin!=="null"?window.location.origin:window.location.href),ft(n,"No window.location.(origin|href) available to create URL");let r=typeof e=="string"?e:jc(e);return r=r.replace(/ $/,"%20"),!t&&r.startsWith("//")&&(r=n+r),new URL(r,n)}function Ik(e,t,n="/"){return NL(e,t,n,!1)}function NL(e,t,n,r){let l=typeof t=="string"?Hs(t):t,o=Oa(l.pathname||"/",n);if(o==null)return null;let u=Bk(e);OL(u);let d=null;for(let h=0;d==null&&h{let v={relativePath:p===void 0?u.path||"":p,caseSensitive:u.caseSensitive===!0,childrenIndex:d,route:u};if(v.relativePath.startsWith("/")){if(!v.relativePath.startsWith(r)&&h)return;ft(v.relativePath.startsWith(r),`Absolute route path "${v.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),v.relativePath=v.relativePath.slice(r.length)}let m=Kr([r,v.relativePath]),y=n.concat(v);u.children&&u.children.length>0&&(ft(u.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${m}".`),Bk(u.children,t,y,m,h)),!(u.path==null&&!u.index)&&t.push({path:m,score:RL(m,u.index),routesMeta:y})};return e.forEach((u,d)=>{var h;if(u.path===""||!((h=u.path)!=null&&h.includes("?")))o(u,d);else for(let p of Hk(u.path))o(u,d,!0,p)}),t}function Hk(e){let t=e.split("/");if(t.length===0)return[];let[n,...r]=t,l=n.endsWith("?"),o=n.replace(/\?$/,"");if(r.length===0)return l?[o,""]:[o];let u=Hk(r.join("/")),d=[];return d.push(...u.map(h=>h===""?o:[o,h].join("/"))),l&&d.push(...u),d.map(h=>e.startsWith("/")&&h===""?"/":h)}function OL(e){e.sort((t,n)=>t.score!==n.score?n.score-t.score:ML(t.routesMeta.map(r=>r.childrenIndex),n.routesMeta.map(r=>r.childrenIndex)))}var EL=/^:[\w-]+$/,kL=3,AL=2,TL=1,CL=10,PL=-2,S1=e=>e==="*";function RL(e,t){let n=e.split("/"),r=n.length;return n.some(S1)&&(r+=PL),t&&(r+=AL),n.filter(l=>!S1(l)).reduce((l,o)=>l+(EL.test(o)?kL:o===""?TL:CL),r)}function ML(e,t){return e.length===t.length&&e.slice(0,-1).every((r,l)=>r===t[l])?e[e.length-1]-t[t.length-1]:0}function DL(e,t,n=!1){let{routesMeta:r}=e,l={},o="/",u=[];for(let d=0;d{if(v==="*"){let _=d[y]||"";u=o.slice(0,o.length-_.length).replace(/(.)\/+$/,"$1")}const x=d[y];return m&&!x?p[v]=void 0:p[v]=(x||"").replace(/%2F/g,"/"),p},{}),pathname:o,pathnameBase:u,pattern:e}}function LL(e,t=!1,n=!0){vr(e==="*"||!e.endsWith("*")||e.endsWith("/*"),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,"/*")}".`);let r=[],l="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(u,d,h,p,v)=>{if(r.push({paramName:d,isOptional:h!=null}),h){let m=v.charAt(p+u.length);return m&&m!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return e.endsWith("*")?(r.push({paramName:"*"}),l+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):n?l+="\\/*$":e!==""&&e!=="/"&&(l+="(?:(?=\\/|$))"),[new RegExp(l,t?void 0:"i"),r]}function zL(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return vr(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function Oa(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith("/")?t.length-1:t.length,r=e.charAt(n);return r&&r!=="/"?null:e.slice(n)||"/"}var qL=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function UL(e,t="/"){let{pathname:n,search:r="",hash:l=""}=typeof e=="string"?Hs(e):e,o;return n?(n=n.replace(/\/\/+/g,"/"),n.startsWith("/")?o=N1(n.substring(1),"/"):o=N1(n,t)):o=t,{pathname:o,search:BL(r),hash:HL(l)}}function N1(e,t){let n=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(l=>{l===".."?n.length>1&&n.pop():l!=="."&&n.push(l)}),n.length>1?n.join("/"):"/"}function xv(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function $L(e){return e.filter((t,n)=>n===0||t.route.path&&t.route.path.length>0)}function vb(e){let t=$L(e);return t.map((n,r)=>r===t.length-1?n.pathname:n.pathnameBase)}function dh(e,t,n,r=!1){let l;typeof e=="string"?l=Hs(e):(l={...e},ft(!l.pathname||!l.pathname.includes("?"),xv("?","pathname","search",l)),ft(!l.pathname||!l.pathname.includes("#"),xv("#","pathname","hash",l)),ft(!l.search||!l.search.includes("#"),xv("#","search","hash",l)));let o=e===""||l.pathname==="",u=o?"/":l.pathname,d;if(u==null)d=n;else{let m=t.length-1;if(!r&&u.startsWith("..")){let y=u.split("/");for(;y[0]==="..";)y.shift(),m-=1;l.pathname=y.join("/")}d=m>=0?t[m]:"/"}let h=UL(l,d),p=u&&u!=="/"&&u.endsWith("/"),v=(o||u===".")&&n.endsWith("/");return!h.pathname.endsWith("/")&&(p||v)&&(h.pathname+="/"),h}var Kr=e=>e.join("/").replace(/\/\/+/g,"/"),IL=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),BL=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,HL=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e,FL=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||"",this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function KL(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}function YL(e){return e.map(t=>t.route.path).filter(Boolean).join("/").replace(/\/\/*/g,"/")||"/"}var Fk=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Kk(e,t){let n=e;if(typeof n!="string"||!qL.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,l=!1;if(Fk)try{let o=new URL(window.location.href),u=n.startsWith("//")?new URL(o.protocol+n):new URL(n),d=Oa(u.pathname,t);u.origin===o.origin&&d!=null?n=d+u.search+u.hash:l=!0}catch{vr(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:l,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var Yk=["POST","PUT","PATCH","DELETE"];new Set(Yk);var GL=["GET",...Yk];new Set(GL);var Fs=j.createContext(null);Fs.displayName="DataRouter";var fh=j.createContext(null);fh.displayName="DataRouterState";var VL=j.createContext(!1),Gk=j.createContext({isTransitioning:!1});Gk.displayName="ViewTransition";var XL=j.createContext(new Map);XL.displayName="Fetchers";var WL=j.createContext(null);WL.displayName="Await";var Zn=j.createContext(null);Zn.displayName="Navigation";var $c=j.createContext(null);$c.displayName="Location";var Ar=j.createContext({outlet:null,matches:[],isDataRoute:!1});Ar.displayName="Route";var gb=j.createContext(null);gb.displayName="RouteError";var Vk="REACT_ROUTER_ERROR",ZL="REDIRECT",QL="ROUTE_ERROR_RESPONSE";function JL(e){if(e.startsWith(`${Vk}:${ZL}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string"&&typeof t.location=="string"&&typeof t.reloadDocument=="boolean"&&typeof t.replace=="boolean")return t}catch{}}function ez(e){if(e.startsWith(`${Vk}:${QL}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string")return new FL(t.status,t.statusText,t.data)}catch{}}function tz(e,{relative:t}={}){ft(Ks(),"useHref() may be used only in the context of a component.");let{basename:n,navigator:r}=j.useContext(Zn),{hash:l,pathname:o,search:u}=Bc(e,{relative:t}),d=o;return n!=="/"&&(d=o==="/"?n:Kr([n,o])),r.createHref({pathname:d,search:u,hash:l})}function Ks(){return j.useContext($c)!=null}function Tr(){return ft(Ks(),"useLocation() may be used only in the context of a component."),j.useContext($c).location}var Xk="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function Wk(e){j.useContext(Zn).static||j.useLayoutEffect(e)}function hh(){let{isDataRoute:e}=j.useContext(Ar);return e?pz():nz()}function nz(){ft(Ks(),"useNavigate() may be used only in the context of a component.");let e=j.useContext(Fs),{basename:t,navigator:n}=j.useContext(Zn),{matches:r}=j.useContext(Ar),{pathname:l}=Tr(),o=JSON.stringify(vb(r)),u=j.useRef(!1);return Wk(()=>{u.current=!0}),j.useCallback((h,p={})=>{if(vr(u.current,Xk),!u.current)return;if(typeof h=="number"){n.go(h);return}let v=dh(h,JSON.parse(o),l,p.relative==="path");e==null&&t!=="/"&&(v.pathname=v.pathname==="/"?t:Kr([t,v.pathname])),(p.replace?n.replace:n.push)(v,p.state,p)},[t,n,o,l,e])}j.createContext(null);function Ic(){let{matches:e}=j.useContext(Ar),t=e[e.length-1];return t?t.params:{}}function Bc(e,{relative:t}={}){let{matches:n}=j.useContext(Ar),{pathname:r}=Tr(),l=JSON.stringify(vb(n));return j.useMemo(()=>dh(e,JSON.parse(l),r,t==="path"),[e,l,r,t])}function rz(e,t){return Zk(e,t)}function Zk(e,t,n){var S;ft(Ks(),"useRoutes() may be used only in the context of a component.");let{navigator:r}=j.useContext(Zn),{matches:l}=j.useContext(Ar),o=l[l.length-1],u=o?o.params:{},d=o?o.pathname:"/",h=o?o.pathnameBase:"/",p=o&&o.route;{let N=p&&p.path||"";Jk(d,!p||N.endsWith("*")||N.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${d}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + */var _1="popstate";function w1(e){return typeof e=="object"&&e!=null&&"pathname"in e&&"search"in e&&"hash"in e&&"state"in e&&"key"in e}function jL(e={}){function t(r,l){var p;let o=(p=l.state)==null?void 0:p.masked,{pathname:u,search:d,hash:h}=o||r.location;return hy("",{pathname:u,search:d,hash:h},l.state&&l.state.usr||null,l.state&&l.state.key||"default",o?{pathname:r.location.pathname,search:r.location.search,hash:r.location.hash}:void 0)}function n(r,l){return typeof l=="string"?l:jc(l)}return wL(t,n,null,e)}function ft(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function vr(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function _L(){return Math.random().toString(36).substring(2,10)}function S1(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.unstable_mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function hy(e,t,n=null,r,l){return{pathname:typeof e=="string"?e:e.pathname,search:"",hash:"",...typeof t=="string"?Hs(t):t,state:n,key:t&&t.key||r||_L(),unstable_mask:l}}function jc({pathname:e="/",search:t="",hash:n=""}){return t&&t!=="?"&&(e+=t.charAt(0)==="?"?t:"?"+t),n&&n!=="#"&&(e+=n.charAt(0)==="#"?n:"#"+n),e}function Hs(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf("?");r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function wL(e,t,n,r={}){let{window:l=document.defaultView,v5Compat:o=!1}=r,u=l.history,d="POP",h=null,p=v();p==null&&(p=0,u.replaceState({...u.state,idx:p},""));function v(){return(u.state||{idx:null}).idx}function m(){d="POP";let S=v(),N=S==null?null:S-p;p=S,h&&h({action:d,location:w.location,delta:N})}function y(S,N){d="PUSH";let E=w1(S)?S:hy(w.location,S,N);p=v()+1;let A=S1(E,p),P=w.createHref(E.unstable_mask||E);try{u.pushState(A,"",P)}catch(k){if(k instanceof DOMException&&k.name==="DataCloneError")throw k;l.location.assign(P)}o&&h&&h({action:d,location:w.location,delta:1})}function x(S,N){d="REPLACE";let E=w1(S)?S:hy(w.location,S,N);p=v();let A=S1(E,p),P=w.createHref(E.unstable_mask||E);u.replaceState(A,"",P),o&&h&&h({action:d,location:w.location,delta:0})}function _(S){return SL(S)}let w={get action(){return d},get location(){return e(l,u)},listen(S){if(h)throw new Error("A history only accepts one active listener");return l.addEventListener(_1,m),h=S,()=>{l.removeEventListener(_1,m),h=null}},createHref(S){return t(l,S)},createURL:_,encodeLocation(S){let N=_(S);return{pathname:N.pathname,search:N.search,hash:N.hash}},push:y,replace:x,go(S){return u.go(S)}};return w}function SL(e,t=!1){let n="http://localhost";typeof window<"u"&&(n=window.location.origin!=="null"?window.location.origin:window.location.href),ft(n,"No window.location.(origin|href) available to create URL");let r=typeof e=="string"?e:jc(e);return r=r.replace(/ $/,"%20"),!t&&r.startsWith("//")&&(r=n+r),new URL(r,n)}function Bk(e,t,n="/"){return NL(e,t,n,!1)}function NL(e,t,n,r){let l=typeof t=="string"?Hs(t):t,o=Oa(l.pathname||"/",n);if(o==null)return null;let u=Hk(e);OL(u);let d=null;for(let h=0;d==null&&h{let v={relativePath:p===void 0?u.path||"":p,caseSensitive:u.caseSensitive===!0,childrenIndex:d,route:u};if(v.relativePath.startsWith("/")){if(!v.relativePath.startsWith(r)&&h)return;ft(v.relativePath.startsWith(r),`Absolute route path "${v.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),v.relativePath=v.relativePath.slice(r.length)}let m=Kr([r,v.relativePath]),y=n.concat(v);u.children&&u.children.length>0&&(ft(u.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${m}".`),Hk(u.children,t,y,m,h)),!(u.path==null&&!u.index)&&t.push({path:m,score:ML(m,u.index),routesMeta:y})};return e.forEach((u,d)=>{var h;if(u.path===""||!((h=u.path)!=null&&h.includes("?")))o(u,d);else for(let p of Fk(u.path))o(u,d,!0,p)}),t}function Fk(e){let t=e.split("/");if(t.length===0)return[];let[n,...r]=t,l=n.endsWith("?"),o=n.replace(/\?$/,"");if(r.length===0)return l?[o,""]:[o];let u=Fk(r.join("/")),d=[];return d.push(...u.map(h=>h===""?o:[o,h].join("/"))),l&&d.push(...u),d.map(h=>e.startsWith("/")&&h===""?"/":h)}function OL(e){e.sort((t,n)=>t.score!==n.score?n.score-t.score:RL(t.routesMeta.map(r=>r.childrenIndex),n.routesMeta.map(r=>r.childrenIndex)))}var EL=/^:[\w-]+$/,kL=3,AL=2,CL=1,TL=10,PL=-2,N1=e=>e==="*";function ML(e,t){let n=e.split("/"),r=n.length;return n.some(N1)&&(r+=PL),t&&(r+=AL),n.filter(l=>!N1(l)).reduce((l,o)=>l+(EL.test(o)?kL:o===""?CL:TL),r)}function RL(e,t){return e.length===t.length&&e.slice(0,-1).every((r,l)=>r===t[l])?e[e.length-1]-t[t.length-1]:0}function DL(e,t,n=!1){let{routesMeta:r}=e,l={},o="/",u=[];for(let d=0;d{if(v==="*"){let _=d[y]||"";u=o.slice(0,o.length-_.length).replace(/(.)\/+$/,"$1")}const x=d[y];return m&&!x?p[v]=void 0:p[v]=(x||"").replace(/%2F/g,"/"),p},{}),pathname:o,pathnameBase:u,pattern:e}}function LL(e,t=!1,n=!0){vr(e==="*"||!e.endsWith("*")||e.endsWith("/*"),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,"/*")}".`);let r=[],l="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(u,d,h,p,v)=>{if(r.push({paramName:d,isOptional:h!=null}),h){let m=v.charAt(p+u.length);return m&&m!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return e.endsWith("*")?(r.push({paramName:"*"}),l+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):n?l+="\\/*$":e!==""&&e!=="/"&&(l+="(?:(?=\\/|$))"),[new RegExp(l,t?void 0:"i"),r]}function zL(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return vr(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function Oa(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith("/")?t.length-1:t.length,r=e.charAt(n);return r&&r!=="/"?null:e.slice(n)||"/"}var qL=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function UL(e,t="/"){let{pathname:n,search:r="",hash:l=""}=typeof e=="string"?Hs(e):e,o;return n?(n=n.replace(/\/\/+/g,"/"),n.startsWith("/")?o=O1(n.substring(1),"/"):o=O1(n,t)):o=t,{pathname:o,search:BL(r),hash:HL(l)}}function O1(e,t){let n=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(l=>{l===".."?n.length>1&&n.pop():l!=="."&&n.push(l)}),n.length>1?n.join("/"):"/"}function xv(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function $L(e){return e.filter((t,n)=>n===0||t.route.path&&t.route.path.length>0)}function vb(e){let t=$L(e);return t.map((n,r)=>r===t.length-1?n.pathname:n.pathnameBase)}function fh(e,t,n,r=!1){let l;typeof e=="string"?l=Hs(e):(l={...e},ft(!l.pathname||!l.pathname.includes("?"),xv("?","pathname","search",l)),ft(!l.pathname||!l.pathname.includes("#"),xv("#","pathname","hash",l)),ft(!l.search||!l.search.includes("#"),xv("#","search","hash",l)));let o=e===""||l.pathname==="",u=o?"/":l.pathname,d;if(u==null)d=n;else{let m=t.length-1;if(!r&&u.startsWith("..")){let y=u.split("/");for(;y[0]==="..";)y.shift(),m-=1;l.pathname=y.join("/")}d=m>=0?t[m]:"/"}let h=UL(l,d),p=u&&u!=="/"&&u.endsWith("/"),v=(o||u===".")&&n.endsWith("/");return!h.pathname.endsWith("/")&&(p||v)&&(h.pathname+="/"),h}var Kr=e=>e.join("/").replace(/\/\/+/g,"/"),IL=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),BL=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,HL=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e,FL=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||"",this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function KL(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}function YL(e){return e.map(t=>t.route.path).filter(Boolean).join("/").replace(/\/\/*/g,"/")||"/"}var Kk=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Yk(e,t){let n=e;if(typeof n!="string"||!qL.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,l=!1;if(Kk)try{let o=new URL(window.location.href),u=n.startsWith("//")?new URL(o.protocol+n):new URL(n),d=Oa(u.pathname,t);u.origin===o.origin&&d!=null?n=d+u.search+u.hash:l=!0}catch{vr(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:l,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var Gk=["POST","PUT","PATCH","DELETE"];new Set(Gk);var GL=["GET",...Gk];new Set(GL);var Fs=j.createContext(null);Fs.displayName="DataRouter";var hh=j.createContext(null);hh.displayName="DataRouterState";var VL=j.createContext(!1),Vk=j.createContext({isTransitioning:!1});Vk.displayName="ViewTransition";var XL=j.createContext(new Map);XL.displayName="Fetchers";var WL=j.createContext(null);WL.displayName="Await";var Zn=j.createContext(null);Zn.displayName="Navigation";var $c=j.createContext(null);$c.displayName="Location";var Ar=j.createContext({outlet:null,matches:[],isDataRoute:!1});Ar.displayName="Route";var gb=j.createContext(null);gb.displayName="RouteError";var Xk="REACT_ROUTER_ERROR",ZL="REDIRECT",QL="ROUTE_ERROR_RESPONSE";function JL(e){if(e.startsWith(`${Xk}:${ZL}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string"&&typeof t.location=="string"&&typeof t.reloadDocument=="boolean"&&typeof t.replace=="boolean")return t}catch{}}function ez(e){if(e.startsWith(`${Xk}:${QL}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string")return new FL(t.status,t.statusText,t.data)}catch{}}function tz(e,{relative:t}={}){ft(Ks(),"useHref() may be used only in the context of a component.");let{basename:n,navigator:r}=j.useContext(Zn),{hash:l,pathname:o,search:u}=Hc(e,{relative:t}),d=o;return n!=="/"&&(d=o==="/"?n:Kr([n,o])),r.createHref({pathname:d,search:u,hash:l})}function Ks(){return j.useContext($c)!=null}function Cr(){return ft(Ks(),"useLocation() may be used only in the context of a component."),j.useContext($c).location}var Wk="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function Zk(e){j.useContext(Zn).static||j.useLayoutEffect(e)}function Ic(){let{isDataRoute:e}=j.useContext(Ar);return e?pz():nz()}function nz(){ft(Ks(),"useNavigate() may be used only in the context of a component.");let e=j.useContext(Fs),{basename:t,navigator:n}=j.useContext(Zn),{matches:r}=j.useContext(Ar),{pathname:l}=Cr(),o=JSON.stringify(vb(r)),u=j.useRef(!1);return Zk(()=>{u.current=!0}),j.useCallback((h,p={})=>{if(vr(u.current,Wk),!u.current)return;if(typeof h=="number"){n.go(h);return}let v=fh(h,JSON.parse(o),l,p.relative==="path");e==null&&t!=="/"&&(v.pathname=v.pathname==="/"?t:Kr([t,v.pathname])),(p.replace?n.replace:n.push)(v,p.state,p)},[t,n,o,l,e])}j.createContext(null);function Bc(){let{matches:e}=j.useContext(Ar),t=e[e.length-1];return t?t.params:{}}function Hc(e,{relative:t}={}){let{matches:n}=j.useContext(Ar),{pathname:r}=Cr(),l=JSON.stringify(vb(n));return j.useMemo(()=>fh(e,JSON.parse(l),r,t==="path"),[e,l,r,t])}function rz(e,t){return Qk(e,t)}function Qk(e,t,n){var S;ft(Ks(),"useRoutes() may be used only in the context of a component.");let{navigator:r}=j.useContext(Zn),{matches:l}=j.useContext(Ar),o=l[l.length-1],u=o?o.params:{},d=o?o.pathname:"/",h=o?o.pathnameBase:"/",p=o&&o.route;{let N=p&&p.path||"";eA(d,!p||N.endsWith("*")||N.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${d}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. -Please change the parent to .`)}let v=Tr(),m;if(t){let N=typeof t=="string"?Hs(t):t;ft(h==="/"||((S=N.pathname)==null?void 0:S.startsWith(h)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${h}" but pathname "${N.pathname}" was given in the \`location\` prop.`),m=N}else m=v;let y=m.pathname||"/",x=y;if(h!=="/"){let N=h.replace(/^\//,"").split("/");x="/"+y.replace(/^\//,"").split("/").slice(N.length).join("/")}let _=Ik(e,{pathname:x});vr(p||_!=null,`No routes matched location "${m.pathname}${m.search}${m.hash}" `),vr(_==null||_[_.length-1].route.element!==void 0||_[_.length-1].route.Component!==void 0||_[_.length-1].route.lazy!==void 0,`Matched leaf route at location "${m.pathname}${m.search}${m.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let w=oz(_&&_.map(N=>Object.assign({},N,{params:Object.assign({},u,N.params),pathname:Kr([h,r.encodeLocation?r.encodeLocation(N.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:N.pathname]),pathnameBase:N.pathnameBase==="/"?h:Kr([h,r.encodeLocation?r.encodeLocation(N.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:N.pathnameBase])})),l,n);return t&&w?j.createElement($c.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",unstable_mask:void 0,...m},navigationType:"POP"}},w):w}function az(){let e=hz(),t=KL(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r="rgba(200,200,200, 0.5)",l={padding:"0.5rem",backgroundColor:r},o={padding:"2px 4px",backgroundColor:r},u=null;return console.error("Error handled by React Router default ErrorBoundary:",e),u=j.createElement(j.Fragment,null,j.createElement("p",null,"💿 Hey developer 👋"),j.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",j.createElement("code",{style:o},"ErrorBoundary")," or"," ",j.createElement("code",{style:o},"errorElement")," prop on your route.")),j.createElement(j.Fragment,null,j.createElement("h2",null,"Unexpected Application Error!"),j.createElement("h3",{style:{fontStyle:"italic"}},t),n?j.createElement("pre",{style:l},n):null,u)}var iz=j.createElement(az,null),Qk=class extends j.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!=="idle"&&e.revalidation==="idle"?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error!==void 0?e.error:t.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error("React Router caught the following error during render",e)}render(){let e=this.state.error;if(this.context&&typeof e=="object"&&e&&"digest"in e&&typeof e.digest=="string"){const n=ez(e.digest);n&&(e=n)}let t=e!==void 0?j.createElement(Ar.Provider,{value:this.props.routeContext},j.createElement(gb.Provider,{value:e,children:this.props.component})):this.props.children;return this.context?j.createElement(lz,{error:e},t):t}};Qk.contextType=VL;var jv=new WeakMap;function lz({children:e,error:t}){let{basename:n}=j.useContext(Zn);if(typeof t=="object"&&t&&"digest"in t&&typeof t.digest=="string"){let r=JL(t.digest);if(r){let l=jv.get(t);if(l)throw l;let o=Kk(r.location,n);if(Fk&&!jv.get(t))if(o.isExternal||r.reloadDocument)window.location.href=o.absoluteURL||o.to;else{const u=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(o.to,{replace:r.replace}));throw jv.set(t,u),u}return j.createElement("meta",{httpEquiv:"refresh",content:`0;url=${o.absoluteURL||o.to}`})}}return e}function sz({routeContext:e,match:t,children:n}){let r=j.useContext(Fs);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),j.createElement(Ar.Provider,{value:e},n)}function oz(e,t=[],n){let r=n==null?void 0:n.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let l=e,o=r==null?void 0:r.errors;if(o!=null){let v=l.findIndex(m=>m.route.id&&(o==null?void 0:o[m.route.id])!==void 0);ft(v>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(o).join(",")}`),l=l.slice(0,Math.min(l.length,v+1))}let u=!1,d=-1;if(n&&r){u=r.renderFallback;for(let v=0;v=0?l=l.slice(0,d+1):l=[l[0]];break}}}}let h=n==null?void 0:n.onError,p=r&&h?(v,m)=>{var y,x;h(v,{location:r.location,params:((x=(y=r.matches)==null?void 0:y[0])==null?void 0:x.params)??{},unstable_pattern:YL(r.matches),errorInfo:m})}:void 0;return l.reduceRight((v,m,y)=>{let x,_=!1,w=null,S=null;r&&(x=o&&m.route.id?o[m.route.id]:void 0,w=m.route.errorElement||iz,u&&(d<0&&y===0?(Jk("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),_=!0,S=null):d===y&&(_=!0,S=m.route.hydrateFallbackElement||null)));let N=t.concat(l.slice(0,y+1)),E=()=>{let T;return x?T=w:_?T=S:m.route.Component?T=j.createElement(m.route.Component,null):m.route.element?T=m.route.element:T=v,j.createElement(sz,{match:m,routeContext:{outlet:v,matches:N,isDataRoute:r!=null},children:T})};return r&&(m.route.ErrorBoundary||m.route.errorElement||y===0)?j.createElement(Qk,{location:r.location,revalidation:r.revalidation,component:w,error:x,children:E(),routeContext:{outlet:null,matches:N,isDataRoute:!0},onError:p}):E()},null)}function yb(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function cz(e){let t=j.useContext(Fs);return ft(t,yb(e)),t}function uz(e){let t=j.useContext(fh);return ft(t,yb(e)),t}function dz(e){let t=j.useContext(Ar);return ft(t,yb(e)),t}function bb(e){let t=dz(e),n=t.matches[t.matches.length-1];return ft(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function fz(){return bb("useRouteId")}function hz(){var r;let e=j.useContext(gb),t=uz("useRouteError"),n=bb("useRouteError");return e!==void 0?e:(r=t.errors)==null?void 0:r[n]}function pz(){let{router:e}=cz("useNavigate"),t=bb("useNavigate"),n=j.useRef(!1);return Wk(()=>{n.current=!0}),j.useCallback(async(l,o={})=>{vr(n.current,Xk),n.current&&(typeof l=="number"?await e.navigate(l):await e.navigate(l,{fromRouteId:t,...o}))},[e,t])}var O1={};function Jk(e,t,n){!t&&!O1[e]&&(O1[e]=!0,vr(!1,n))}j.memo(mz);function mz({routes:e,future:t,state:n,isStatic:r,onError:l}){return Zk(e,void 0,{state:n,isStatic:r,onError:l})}function E1({to:e,replace:t,state:n,relative:r}){ft(Ks()," may be used only in the context of a component.");let{static:l}=j.useContext(Zn);vr(!l," must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.");let{matches:o}=j.useContext(Ar),{pathname:u}=Tr(),d=hh(),h=dh(e,vb(o),u,r==="path"),p=JSON.stringify(h);return j.useEffect(()=>{d(JSON.parse(p),{replace:t,state:n,relative:r})},[d,p,r,t,n]),null}function _t(e){ft(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function vz({basename:e="/",children:t=null,location:n,navigationType:r="POP",navigator:l,static:o=!1,unstable_useTransitions:u}){ft(!Ks(),"You cannot render a inside another . You should never have more than one in your app.");let d=e.replace(/^\/*/,"/"),h=j.useMemo(()=>({basename:d,navigator:l,static:o,unstable_useTransitions:u,future:{}}),[d,l,o,u]);typeof n=="string"&&(n=Hs(n));let{pathname:p="/",search:v="",hash:m="",state:y=null,key:x="default",unstable_mask:_}=n,w=j.useMemo(()=>{let S=Oa(p,d);return S==null?null:{location:{pathname:S,search:v,hash:m,state:y,key:x,unstable_mask:_},navigationType:r}},[d,p,v,m,y,x,r,_]);return vr(w!=null,` is not able to match the URL "${p}${v}${m}" because it does not start with the basename, so the won't render anything.`),w==null?null:j.createElement(Zn.Provider,{value:h},j.createElement($c.Provider,{children:t,value:w}))}function gz({children:e,location:t}){return rz(py(e),t)}function py(e,t=[]){let n=[];return j.Children.forEach(e,(r,l)=>{if(!j.isValidElement(r))return;let o=[...t,l];if(r.type===j.Fragment){n.push.apply(n,py(r.props.children,o));return}ft(r.type===_t,`[${typeof r.type=="string"?r.type:r.type.name}] is not a component. All component children of must be a or `),ft(!r.props.index||!r.props.children,"An index route cannot have child routes.");let u={id:r.props.id||o.join("-"),caseSensitive:r.props.caseSensitive,element:r.props.element,Component:r.props.Component,index:r.props.index,path:r.props.path,middleware:r.props.middleware,loader:r.props.loader,action:r.props.action,hydrateFallbackElement:r.props.hydrateFallbackElement,HydrateFallback:r.props.HydrateFallback,errorElement:r.props.errorElement,ErrorBoundary:r.props.ErrorBoundary,hasErrorBoundary:r.props.hasErrorBoundary===!0||r.props.ErrorBoundary!=null||r.props.errorElement!=null,shouldRevalidate:r.props.shouldRevalidate,handle:r.props.handle,lazy:r.props.lazy};r.props.children&&(u.children=py(r.props.children,o)),n.push(u)}),n}var ef="get",tf="application/x-www-form-urlencoded";function ph(e){return typeof HTMLElement<"u"&&e instanceof HTMLElement}function yz(e){return ph(e)&&e.tagName.toLowerCase()==="button"}function bz(e){return ph(e)&&e.tagName.toLowerCase()==="form"}function xz(e){return ph(e)&&e.tagName.toLowerCase()==="input"}function jz(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function _z(e,t){return e.button===0&&(!t||t==="_self")&&!jz(e)}function my(e=""){return new URLSearchParams(typeof e=="string"||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce((t,n)=>{let r=e[n];return t.concat(Array.isArray(r)?r.map(l=>[n,l]):[[n,r]])},[]))}function wz(e,t){let n=my(e);return t&&t.forEach((r,l)=>{n.has(l)||t.getAll(l).forEach(o=>{n.append(l,o)})}),n}var Cd=null;function Sz(){if(Cd===null)try{new FormData(document.createElement("form"),0),Cd=!1}catch{Cd=!0}return Cd}var Nz=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function _v(e){return e!=null&&!Nz.has(e)?(vr(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${tf}"`),null):e}function Oz(e,t){let n,r,l,o,u;if(bz(e)){let d=e.getAttribute("action");r=d?Oa(d,t):null,n=e.getAttribute("method")||ef,l=_v(e.getAttribute("enctype"))||tf,o=new FormData(e)}else if(yz(e)||xz(e)&&(e.type==="submit"||e.type==="image")){let d=e.form;if(d==null)throw new Error('Cannot submit a - +
{modelsUsage.length ? modelsUsage.map((model) => ( setModelEnabled(model.model, !model.enabled)} @@ -458,7 +461,7 @@ function ProviderProbeSuggestionPanel({ report, busy, onApply }) { ); } -function ProviderModelRow({ item, busy, deleting, onToggle, onDelete }) { +function ProviderModelRow({ item, providerEnabled, busy, deleting, onToggle, onDelete }) { const summary = item.summary || {}; const isDiscoveredDisabled = item.source === "discovered" && !item.enabled; const canDelete = item.source !== "trace"; @@ -467,10 +470,10 @@ function ProviderModelRow({ item, busy, deleting, onToggle, onDelete }) {
{item.model} - {isDiscoveredDisabled ? "discovered, awaiting enable" : modelSourceLabel(item.source)} + {isDiscoveredDisabled ? "discovered, disabled" : modelSourceLabel(item.source)}
- + {item.source !== "trace" ? : History only — add model to configure routing} {canDelete ? (
- {item.enabled ? "enabled" : "disabled"} - {isDiscoveredDisabled ? new : null} + {item.source === "trace" ? "history only" : item.enabled ? "enabled" : "disabled"} + {item.enabled && !providerEnabled ? blocked: provider disabled : null}
diff --git a/web/monitor-ui/src/routes/ChannelsPage.jsx b/web/monitor-ui/src/routes/ChannelsPage.jsx index b3199fe..5e4a605 100644 --- a/web/monitor-ui/src/routes/ChannelsPage.jsx +++ b/web/monitor-ui/src/routes/ChannelsPage.jsx @@ -1,6 +1,6 @@ import React, { useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; -import { Link, useSearchParams } from "react-router-dom"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; import { StatCard } from "../components/common/Display"; import { DeleteIcon, InlineTag, PlusIcon } from "../components/common/Badges"; import { EmptyState } from "../components/common/EmptyState"; @@ -35,6 +35,7 @@ const DEFAULT_FORM = { }; export function ProvidersPage() { + const navigate = useNavigate(); const { t } = useI18n(); const [searchParams, setSearchParams] = useSearchParams(); const windowValue = normalizeAnalyticsWindow(searchParams.get("window")); @@ -126,9 +127,10 @@ export function ProvidersPage() { setFormOpen(false)} - onCreated={() => { + onCreated={(item) => { setFormOpen(false); setRefreshTick((tick) => tick + 1); + if (item?.id) navigate(buildProviderLink(item.id, windowValue)); }} /> ) : null} @@ -265,8 +267,8 @@ function CreateProviderDialog({ presetData, onClose, onCreated }) { setSaving(true); setError(""); try { - await postJSON(apiPaths.providerSetupApply, normalizeProviderPayload(form)); - onCreated(); + const result = await postJSON(apiPaths.providerSetupApply, normalizeProviderPayload(form)); + onCreated(result.channel); } catch (err) { setError(err.message || t("providers.validateRequiredReason")); } finally { @@ -306,7 +308,7 @@ function CreateProviderDialog({ presetData, onClose, onCreated }) { {error ?

{error}

: null}
- +
, @@ -319,14 +321,18 @@ function ProviderCard({ item, windowValue, onRefresh }) { const summary = item.summary || {}; const modeTag = providerModeTag(item.mode); const [saving, setSaving] = useState(false); + const [error, setError] = useState(""); const [probeOpen, setProbeOpen] = useState(false); const setEnabled = async (enabled, event) => { event?.preventDefault(); event?.stopPropagation(); setSaving(true); + setError(""); try { await patchJSON(apiPaths.provider(item.id), { enabled }); onRefresh?.(); + } catch (err) { + setError(err.message); } finally { setSaving(false); } @@ -338,9 +344,12 @@ function ProviderCard({ item, windowValue, onRefresh }) { return; } setSaving(true); + setError(""); try { await deleteJSON(apiPaths.provider(item.id)); onRefresh?.(); + } catch (err) { + setError(err.message); } finally { setSaving(false); } @@ -379,6 +388,8 @@ function ProviderCard({ item, windowValue, onRefresh }) {
+ {error ?

{error}

: null} +

{!item.enabled ? t("providers.routingDisabled") : !item.enabled_model_count && !item.allow_unknown_models ? t("providers.chooseModels") : t("providers.routingEnabled")}

{item.base_url} {formatDateTime(item.last_probe_at || item.updated_at)} diff --git a/web/monitor-ui/tests-real/monitor-real-server.spec.js b/web/monitor-ui/tests-real/monitor-real-server.spec.js index 6639af9..b916195 100644 --- a/web/monitor-ui/tests-real/monitor-real-server.spec.js +++ b/web/monitor-ui/tests-real/monitor-real-server.spec.js @@ -1,5 +1,9 @@ import { expect, test } from "@playwright/test"; +test.beforeEach(async ({ page }) => { + await page.addInitScript(() => window.localStorage.setItem("llm-tracelab.monitor.language", "en")); +}); + test("real monitor server renders seeded model and channel data", async ({ page }) => { await page.goto("/models"); await expect(page.getByRole("heading", { name: "Models", exact: true })).toBeVisible(); @@ -16,24 +20,22 @@ test("real monitor server renders seeded model and channel data", async ({ page await expect(page.getByText("encrypted-local").first()).toBeVisible(); await expect(page.getByText("rate limited")).toBeVisible(); - await page.getByRole("button", { name: "Edit channel" }).click(); - await expect(page.getByRole("heading", { name: "Edit channel" })).toBeVisible(); + await page.getByRole("button", { name: "Edit provider" }).click(); + await expect(page.getByRole("heading", { name: "Edit provider" })).toBeVisible(); await expect(page.getByLabel("Provider preset")).toHaveValue("openai"); await page.getByRole("button", { name: "Advanced options" }).click(); await expect(page.locator("textarea")).toContainText("Authorization: ***"); await expect(page.locator("textarea")).toContainText("X-Test: visible"); }); -test("real monitor server supports local secret key operations", async ({ page }) => { - await page.goto("/channels"); - await expect(page.getByRole("heading", { name: "Channel secret storage" })).toBeVisible(); - await expect(page.getByText("encrypted-local").first()).toBeVisible(); - await expect(page.getByRole("button", { name: "Rotate key" })).toBeDisabled(); - - await page.getByLabel("Confirm rotate").check(); - await page.getByRole("button", { name: "Rotate key" }).click(); - await expect(page.getByRole("button", { name: "Rotating" })).toBeHidden(); - await expect(page.getByRole("button", { name: "Rotate key" })).toBeDisabled(); +test("real monitor server supports local secret key operations", async ({ request }) => { + const before = await request.get("/api/secrets/local-key"); + expect(before.ok()).toBeTruthy(); + const rotation = await request.post("/api/secrets/local-key?rotate=1"); + expect(rotation.ok()).toBeTruthy(); + const result = await rotation.json(); + expect(result.new_fingerprint).toBeTruthy(); + expect(result.new_fingerprint).not.toBe(result.old_fingerprint); }); test("real monitor server supports probe and manual model mutation", async ({ page }) => { @@ -43,10 +45,10 @@ test("real monitor server supports probe and manual model mutation", async ({ pa await page.getByRole("button", { name: "Add model" }).click(); await expect(page.getByText("gpt-manual-real")).toBeVisible(); - await page.getByRole("button", { name: "Probe channel" }).click(); + await page.getByRole("button", { name: "Probe provider" }).click(); await expect(page.getByText("success").first()).toBeVisible(); - await expect(page.getByText("discovered, awaiting enable").first()).toBeVisible(); - await page.getByRole("button", { name: "Enable new (1)" }).click(); + await expect(page.getByText("discovered, disabled").first()).toBeVisible(); + await page.getByRole("button", { name: "Enable discovered (1)" }).click(); await expect(page.getByText("gpt-new-real")).toBeVisible(); }); @@ -54,7 +56,7 @@ test("real monitor server shows probe failure guidance", async ({ page }) => { await page.goto("/channels/broken-auth"); await expect(page.getByRole("heading", { name: "Broken Auth" })).toBeVisible(); - await page.getByRole("button", { name: "Probe channel" }).click(); + await page.getByRole("button", { name: "Probe provider" }).click(); await expect(page.getByText("not_found").first()).toBeVisible(); await expect(page.getByText(/Check the base URL/i).first()).toBeVisible(); }); @@ -63,14 +65,16 @@ test("real monitor server renders routing decision records", async ({ page }) => await page.goto("/routing"); await expect(page.getByRole("heading", { name: "Routing" })).toBeVisible(); await expect(page.getByText("Recent selected routes")).toBeVisible(); - await expect(page.getByText("openai-primary").first()).toBeVisible(); + const exchanges = await page.request.get("/api/routing/exchanges").then((response) => response.json()); + expect(exchanges.items).toHaveLength(2); + expect(exchanges.items.every((item) => item.selected_upstream_id === "openai-primary")).toBeTruthy(); await expect(page.getByText("gpt-5").first()).toBeVisible(); }); test("real monitor server serves trace routing links", async ({ page }) => { const fixture = await page.request.get("/__fixture/state").then((response) => response.json()); await page.goto(`/traces/${fixture.routed_trace_id}`); - await expect(page.getByText("Routing decision")).toBeVisible(); + await expect(page.getByRole("heading", { name: "Selected route target" })).toBeVisible(); await expect(page.getByRole("link", { name: "Open Channel" })).toHaveAttribute("href", "/channels/openai-primary"); await expect(page.getByRole("link", { name: "Open Upstream" })).toHaveAttribute("href", "/upstreams/openai-primary"); }); diff --git a/web/monitor-ui/tests/monitor-smoke.spec.js b/web/monitor-ui/tests/monitor-smoke.spec.js index 78ffac3..21dc488 100644 --- a/web/monitor-ui/tests/monitor-smoke.spec.js +++ b/web/monitor-ui/tests/monitor-smoke.spec.js @@ -1,6 +1,7 @@ import { expect, test } from "@playwright/test"; test.beforeEach(async ({ page }) => { + await page.addInitScript(() => window.localStorage.setItem("llm-tracelab.monitor.language", "en")); await page.route("**/api/**", async (route) => { const url = new URL(route.request().url()); const path = url.pathname; @@ -17,6 +18,9 @@ test.beforeEach(async ({ page }) => { expect(url.searchParams.get("status")).toBe("unread"); return route.fulfill({ json: eventListPayload() }); } + if (path === "/api/routing/summary") { + return route.fulfill({ json: {} }); + } if (path === "/api/models") { return route.fulfill({ json: modelListPayload() }); } @@ -60,7 +64,7 @@ test.beforeEach(async ({ page }) => { expect(body.api_key).toBe("sk-test-secret"); expect(body.api_type).toBe("chat_completions"); expect(body.protocol_family).toBe("openai_compatible"); - return route.fulfill({ json: channelDetailPayload() }); + return route.fulfill({ json: { applied: true, channel: channelDetailPayload() } }); } if (path === "/api/provider-presets") { return route.fulfill({ json: providerPresetPayload() }); @@ -97,7 +101,7 @@ test.beforeEach(async ({ page }) => { expect(body).toEqual({ models: ["gpt-new"], enabled: true }); return route.fulfill({ json: { updated: 1, models: ["gpt-new"], enabled: true } }); } - if (path === "/api/traces") { + if (path === "/api/traces" || path === "/api/routing/exchanges") { if (url.searchParams.get("status") === "error") { expect(url.searchParams.get("model")).toBe("gpt-5"); expect(url.searchParams.get("upstream")).toBe("openai-primary"); @@ -153,7 +157,7 @@ test("models marketplace and detail render", async ({ page }) => { await page.getByRole("link", { name: /gpt-5/i }).first().click(); await expect(page.getByRole("heading", { name: "gpt-5" })).toBeVisible(); await expect(page.getByText("Provider coverage")).toBeVisible(); - await expect(page.getByText("openai-primary")).toBeVisible(); + await expect(page.getByRole("link", { name: /openai-primary manual enabled/ })).toBeVisible(); }); test("provider management renders and supports core actions", async ({ page }) => { @@ -161,19 +165,11 @@ test("provider management renders and supports core actions", async ({ page }) = await expect(page.getByRole("heading", { name: "Providers", exact: true })).toBeVisible(); await expect(page.getByText("web-managed").first()).toBeVisible(); await expect(page.getByText("encrypted-local").first()).toBeVisible(); - await expect(page.getByRole("heading", { name: "Provider secret storage" })).toBeVisible(); - await expect(page.getByText("abc123")).toBeVisible(); - await expect(page.getByRole("button", { name: "Rotate key" })).toBeDisabled(); - await expect(page.getByRole("button", { name: "Apply detected suggestions" })).toBeDisabled(); - await page.getByRole("button", { name: "Preview batch probe" }).click(); - await expect(page.getByText("1 fillable")).toBeVisible(); - await expect(page.getByText("explicit configuration and false capabilities are preserved")).toBeVisible(); - await page.getByRole("button", { name: "Apply detected suggestions" }).click(); - await expect(page.getByText("1 applied, 0 skipped")).toBeVisible(); await page.getByRole("button", { name: "New provider" }).click(); await expect(page.getByRole("heading", { name: "Create provider" })).toBeVisible(); await expect(page.getByLabel("Provider preset")).toHaveValue("openai"); await expect(page.getByRole("button", { name: "Create provider" })).toBeDisabled(); + await page.getByLabel("Name", { exact: true }).fill("OpenAI Primary"); await page.getByLabel("Base URL").fill("https://api.openai.example/v1"); await page.getByLabel("API key").fill("sk-test-secret"); await page.getByRole("button", { name: "Detect provider" }).click(); @@ -181,23 +177,23 @@ test("provider management renders and supports core actions", async ({ page }) = await page.getByRole("button", { name: "Apply suggestions" }).click(); await expect(page.getByLabel("API type")).toHaveValue("chat_completions"); await expect(page.getByLabel("API mode")).toHaveValue("proxy"); - await expect(page.getByLabel("Protocol family")).toHaveValue("openai_compatible"); + await expect(page.getByRole("combobox", { name: "protocol", exact: true })).toHaveValue("openai_compatible"); await expect(page.getByLabel("Routing profile")).toHaveValue("openai_default"); - await expect(page.getByRole("button", { name: "Create provider" })).toBeDisabled(); + await expect(page.getByRole("button", { name: "Create provider" })).toBeEnabled(); await page.getByRole("button", { name: "Validate setup" }).click(); await expect(page.getByRole("heading", { name: "Ready to create" })).toBeVisible(); await expect(page.getByText("stored as sk-...cret")).toBeVisible(); await expect(page.getByRole("button", { name: "Create provider" })).toBeEnabled(); await page.getByLabel("Base URL").fill("https://api.openai.example/v1/"); - await expect(page.getByText("Validate setup before creating the provider.")).toBeVisible(); - await expect(page.getByRole("button", { name: "Create provider" })).toBeDisabled(); + await expect(page.getByText("Validate and save the connection, then discover or add models and enable your selection.")).toBeVisible(); + await expect(page.getByRole("button", { name: "Create provider" })).toBeEnabled(); await page.getByLabel("Base URL").fill("https://api.openai.example/v1"); await page.getByRole("button", { name: "Validate setup" }).click(); await expect(page.getByRole("button", { name: "Create provider" })).toBeEnabled(); await page.getByRole("button", { name: "Create provider" }).click(); await expect(page.getByRole("heading", { name: "Create provider" })).toBeHidden(); - await page.getByRole("link", { name: /OpenAI Primary/i }).first().click(); + await expect(page).toHaveURL(/providers\/openai-primary/); await expect(page.getByRole("heading", { name: "OpenAI Primary" })).toBeVisible(); await expect(page.getByText("config source").first()).toBeVisible(); await expect(page.getByText("web-managed").first()).toBeVisible(); @@ -205,7 +201,7 @@ test("provider management renders and supports core actions", async ({ page }) = await expect(page.getByText("responses_server").first()).toBeVisible(); await expect(page.getByText("1 missing usage").first()).toBeVisible(); await expect(page.getByText("encrypted-local").first()).toBeVisible(); - await expect(page.getByText("discovered, awaiting enable")).toBeVisible(); + await expect(page.getByText("discovered, disabled")).toBeVisible(); await page.getByRole("button", { name: "Edit provider" }).click(); await expect(page.getByRole("heading", { name: "Edit provider" })).toBeVisible(); @@ -215,7 +211,7 @@ test("provider management renders and supports core actions", async ({ page }) = await page.getByRole("button", { name: "Advanced options" }).click(); await expect(page.getByLabel("API type")).toHaveValue("chat_completions"); await expect(page.getByLabel("API mode")).toHaveValue("responses_server"); - await expect(page.getByLabel("Protocol family")).toHaveValue("openai_compatible"); + await expect(page.getByRole("combobox", { name: "protocol", exact: true })).toHaveValue("openai_compatible"); await expect(page.getByLabel("Routing profile")).toHaveValue("openai_default"); await expect(page.locator("textarea")).toContainText("Authorization: ***"); await page.getByRole("button", { name: "Cancel" }).click(); @@ -230,7 +226,7 @@ test("provider management renders and supports core actions", async ({ page }) = await expect(page.getByRole("heading", { name: "Probe suggestions" })).toBeVisible(); await expect(page.getByText("chat_completions").first()).toBeVisible(); - await page.getByRole("button", { name: "Enable new (1)" }).click(); + await page.getByRole("button", { name: "Enable discovered (1)" }).click(); await expect(page.getByRole("button", { name: "Enabling" })).toBeHidden(); }); @@ -305,7 +301,7 @@ test("analysis page renders runs and reanalysis jobs", async ({ page }) => { await expect(page.getByText("Job queue")).toBeVisible(); await expect(page.getByText("trace_reanalyze")).toBeVisible(); await expect(page.getByText("session_summary", { exact: true })).toBeVisible(); - await page.getByRole("button", { name: "Repair missing usage" }).click(); + await page.getByRole("button", { name: "Repair token stats" }).click(); await expect(page.getByText(/Batch reanalysis job #202 queued/)).toBeVisible(); }); @@ -818,3 +814,31 @@ function usageSummary(overrides = {}) { ...overrides, }; } + + +test("disabled provider preserves model choices without claiming availability", async ({ page }) => { + await page.route("**/api/channels/openai-primary?*", (route) => { + const payload = channelDetailPayload(); + payload.enabled = false; + payload.models_usage.push({ model: "historical-model", source: "trace", enabled: false, summary: {} }); + return route.fulfill({ json: payload }); + }); + await page.goto("/providers/openai-primary"); + await expect(page.getByText("Provider disabled: all model routes are blocked. Model selections are preserved.")).toBeVisible(); + await expect(page.getByLabel("gpt-5 enabled", { exact: true })).toBeChecked(); + await expect(page.getByText("blocked: provider disabled").first()).toBeVisible(); + await expect(page.getByLabel("historical-model enabled", { exact: true })).toHaveCount(0); + await expect(page.getByText("History only — add model to configure routing")).toBeVisible(); + await expect(page.locator(".provider-model-card").getByText("new", { exact: true })).toHaveCount(0); +}); + +test("provider toggle failure is visible and preserves the displayed choice", async ({ page }) => { + await page.route("**/api/channels/openai-primary", (route) => { + if (route.request().method() === "PATCH") return route.fulfill({ status: 409, json: { error: "Upstreams are managed by YAML credentials" } }); + return route.fallback(); + }); + await page.goto("/providers"); + await page.getByLabel("OpenAI Primary enabled", { exact: true }).click(); + await expect(page.getByRole("alert")).toHaveText("Upstreams are managed by YAML credentials"); + await expect(page.getByLabel("OpenAI Primary enabled", { exact: true })).toBeChecked(); +}); From c137c26c97a3d9ee43a9597a3405c32841a54fc2 Mon Sep 17 00:00:00 2001 From: kingfs Date: Mon, 14 Sep 2026 17:21:57 +0800 Subject: [PATCH 2/2] fix: close the remaining configuration-consistency gaps Follow-up hardening on top of the routing-switch work: - store: every Store view of one database now shares one state block, so a configuration transaction no longer runs with a fresh event sequence and an empty live subscriber set. - store: rebindingDB.Begin/BeginTx fail with ErrNestedTransaction instead of silently borrowing a pooled connection, and a nested ConfigurationTransaction is rejected before it can deadlock on the configuration lock. - store: add the upstream write lock. ConfigurationTransaction holds it for the whole transaction, while background/proxy refresh takes it on a best-effort basis and filters results against the live target set, so a refresh that raced a configuration change can no longer write rows for a deleted target. - router: commit the database transaction before publishing the new snapshot and outside router.mu, so request routing is not blocked for the duration of the commit. - channel: WithHTTPClient and WithReadOnly now return a derived service like WithStore, and an already initialized startup no longer rewrites the bootstrap marker. - monitor: add GET/DELETE /api/settings/channels to report and clear the channels.initialized marker through the configuration transaction. - docs: document the channels.initialized key and reset path plus the transaction/lock ordering invariants. Tests: new store, router, channel and monitor coverage for each fix. --- AGENTS.md | 2 + docs/MAINTAINER_BASELINE.md | 6 +- docs/MONITOR_GUIDE.md | 2 +- internal/channel/service.go | 31 +++- internal/channel/service_test.go | 145 ++++++++++++++++ internal/monitor/configuration_test.go | 37 +++++ internal/monitor/server.go | 50 ++++++ internal/router/router.go | 169 ++++++++++++++----- internal/router/router_config_lock_test.go | 132 +++++++++++++++ internal/store/app_settings.go | 20 +++ internal/store/config_transaction.go | 62 ++++++- internal/store/config_transaction_test.go | 185 +++++++++++++++++++++ internal/store/store.go | 75 ++++++--- 13 files changed, 844 insertions(+), 72 deletions(-) create mode 100644 internal/router/router_config_lock_test.go create mode 100644 internal/store/config_transaction_test.go diff --git a/AGENTS.md b/AGENTS.md index 2c3b8ba..e628bfc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,6 +35,8 @@ Structured state (trace index, sessions, channel/provider config, upstream targe - Production and the tracked default config use Postgres; the checked-in SQL migrations live in `ent/postgres-migrations/`. - SQLite is a local/dev/test fallback only, with default file `{{output_dir}}/llm_tracelab.sqlite3`; SQLite schema is applied at startup rather than by versioned migrations. - Raw `.http` cassettes remain the source of truth for replay and detail views; the database is a derived index for lists, filters, and aggregates. +- YAML channel configuration is a first-bootstrap input only. The first database write stores the application-database `app_settings` key `channels.initialized`; afterwards the database owns routing configuration even when every channel was disabled or deleted. `GET /api/settings/channels` reports the marker and `DELETE /api/settings/channels` clears it, which only re-opens the YAML bootstrap while the database still has no channels. A YAML config with an explicit `credentials` list stays YAML-managed and rejects Monitor channel/model/alias writes with 409. +- All management writes (channels, models, aliases, provider setup and probe apply) run in one `store.ConfigurationTransaction`, which holds the process-wide configuration lock, the upstream write lock, and one SQL transaction; runtime routing is published only after the commit succeeds. Background upstream refresh persists through the same upstream write lock on a best-effort basis. Current protocol families are documented in `docs/protocol-reference/implemented-protocols.md`. The proxy is protocol-aware pass-through plus recording/parsing; it does not currently translate requests between OpenAI, Anthropic, Gemini, and Vertex protocol families in the forwarding hot path. diff --git a/docs/MAINTAINER_BASELINE.md b/docs/MAINTAINER_BASELINE.md index d7e573c..fb3893c 100644 --- a/docs/MAINTAINER_BASELINE.md +++ b/docs/MAINTAINER_BASELINE.md @@ -98,9 +98,11 @@ YAML `upstream` / `upstreams` 是兼容 bootstrap 输入。 修改渠道管理时必须保持: - DB 优先。 -- legacy YAML 可首次导入。 +- legacy YAML 可首次导入。首次数据库写入会记录 `app_settings` 键 `channels.initialized`;此后 DB 是路由配置来源,全部渠道停用或删除也不会回退到 YAML。`GET /api/settings/channels` 报告该标记,`DELETE /api/settings/channels` 仅清除标记:只有在 DB 中确实没有渠道时,下次启动才会重新导入 YAML。 - API key 和敏感 header 本地加密。 - channel/model 启停能 reload router。 +- 所有管理写入(渠道、模型、别名、provider setup/probe apply)共用 `store.ConfigurationTransaction`:它按 `configMu` -> `upstreamMu` -> `reloadMu` -> `router.mu` 的顺序持锁并使用单个 SQL 事务,只有 commit 成功后才发布新路由快照。不要在事务内再开事务(会返回 `store.ErrNestedTransaction`),也不要新增 `reloadMu` -> `upstreamMu` 的反向获取。 +- 后台刷新和代理侧 `RefreshNow` 只以 best-effort 方式获取 `upstreamMu`:配置变更持锁时跳过落库但仍更新内存,并在获得锁后按当前 live target 集合过滤,避免为已删除的 target 复活 `upstream_targets`/`upstream_models` 行。 ## 协议边界 @@ -125,6 +127,8 @@ YAML `upstream` / `upstreams` 是兼容 bootstrap 输入。 - `/api/models` - `/api/channels` - `/api/routing/summary` +- `/api/settings/routing` +- `/api/settings/channels` - `/api/events` - `/api/findings` - `/api/analysis` diff --git a/docs/MONITOR_GUIDE.md b/docs/MONITOR_GUIDE.md index feb7146..b78f898 100644 --- a/docs/MONITOR_GUIDE.md +++ b/docs/MONITOR_GUIDE.md @@ -104,7 +104,7 @@ Monitor 使用两类数据: - 启停单个模型。 - 查看渠道用量、token、失败和 probe 结果。 -长期渠道配置保存在 application store;Postgres 部署使用版本化迁移,SQLite 仍作为本地 fallback。通常 YAML 只作为启动和首次 bootstrap 输入;首次初始化会留下持久化标记,即使后来停用或删除全部渠道,重启也不会重新导入 YAML。使用显式 `credentials` 列表的 YAML 配置仍由 YAML 管理,此模式下 Monitor 的渠道、模型和别名写操作返回 409,避免数据库操作替换 YAML 中的凭据路由。 +长期渠道配置保存在 application store;Postgres 部署使用版本化迁移,SQLite 仍作为本地 fallback。通常 YAML 只作为启动和首次 bootstrap 输入;首次初始化会在应用库 `app_settings` 写入 `channels.initialized` 标记(`GET /api/settings/channels` 报告,`DELETE /api/settings/channels` 清除),即使后来停用或删除全部渠道,重启也不会重新导入 YAML。清除该标记只是重新放行 YAML bootstrap:只要数据库里仍有渠道配置,数据库依旧是路由配置来源;只有数据库确实为空时,下次启动才重新导入 YAML。使用显式 `credentials` 列表的 YAML 配置仍由 YAML 管理,此模式下 Monitor 的渠道、模型和别名写操作返回 409,避免数据库操作替换 YAML 中的凭据路由。 ### 上游和模型的启停语义 diff --git a/internal/channel/service.go b/internal/channel/service.go index 2b2a2c6..7c7b14b 100644 --- a/internal/channel/service.go +++ b/internal/channel/service.go @@ -22,6 +22,7 @@ import ( type Store interface { LoadAppSettingJSON(context.Context, string, any) (bool, error) SaveAppSettingJSON(context.Context, string, any) error + DeleteAppSetting(context.Context, string) (bool, error) ListChannelConfigs() ([]store.ChannelConfigRecord, error) GetChannelConfig(channelID string) (store.ChannelConfigRecord, error) UpsertChannelConfig(store.ChannelConfigRecord) (store.ChannelConfigRecord, error) @@ -45,9 +46,15 @@ func NewService(st Store) *Service { return &Service{store: st} } +// WithHTTPClient returns a copy of the service that probes with client. +// +// Every With* helper returns a derived service and leaves the receiver +// untouched, so a service shared by several callers (for example the one built +// for the Monitor) can be specialised without mutating it in place. func (s *Service) WithHTTPClient(client *http.Client) *Service { - s.httpClient = client - return s + next := *s + next.httpClient = client + return &next } // WithStore preserves probe options when applying a configuration transaction. @@ -58,8 +65,9 @@ func (s *Service) WithStore(st Store) *Service { } func (s *Service) WithReadOnly(readOnly bool) *Service { - s.readOnly = readOnly - return s + next := *s + next.readOnly = readOnly + return &next } func (s *Service) ReadOnly() bool { return s != nil && s.readOnly } @@ -83,6 +91,15 @@ func (s *Service) MarkConfigurationInitialized() error { return s.store.SaveAppSettingJSON(context.Background(), configurationInitializedKey, true) } +// ResetConfigurationInitialized clears the explicit bootstrap marker so the next +// start may import YAML again. It deliberately does not delete channels: a +// database that still holds channel configuration keeps counting as +// initialized, because HasConfiguration also falls back to the stored channels. +func (s *Service) ResetConfigurationInitialized() error { + _, err := s.store.DeleteAppSetting(context.Background(), configurationInitializedKey) + return err +} + type ProbeResult struct { ChannelID string Status string @@ -121,6 +138,10 @@ type ProviderProbeApplyItem struct { SkippedReason string `json:"skipped_reason,omitempty"` } +// BootstrapFromConfig imports YAML upstreams into the channel store on first +// startup only. It never writes once the database owns the configuration: the +// marker is stored by the first import or by the first management write, so a +// startup that is already initialized stays read-only. func (s *Service) BootstrapFromConfig(cfg *config.Config) (int, error) { if s == nil || s.store == nil { return 0, fmt.Errorf("channel service store is required") @@ -130,7 +151,7 @@ func (s *Service) BootstrapFromConfig(cfg *config.Config) (int, error) { return 0, err } if initialized { - return 0, s.MarkConfigurationInitialized() + return 0, nil } targets := configuredUpstreams(cfg) diff --git a/internal/channel/service_test.go b/internal/channel/service_test.go index 7bef9e4..105e53c 100644 --- a/internal/channel/service_test.go +++ b/internal/channel/service_test.go @@ -892,3 +892,148 @@ func TestProbeDefaultsToDisabledAndPreservesManualChoices(t *testing.T) { } } } + +func TestServiceBuildersLeaveTheirReceiverUntouched(t *testing.T) { + st, err := store.New(t.TempDir()) + if err != nil { + t.Fatalf("store.New() error = %v", err) + } + defer st.Close() + + base := NewService(st) + scoped := base.WithStore(st) + readOnly := base.WithReadOnly(true) + withClient := base.WithHTTPClient(&http.Client{}) + + if base.ReadOnly() { + t.Fatal("WithReadOnly mutated the receiver") + } + if !readOnly.ReadOnly() { + t.Fatal("WithReadOnly did not apply to the derived service") + } + if base.httpClient != nil { + t.Fatal("WithHTTPClient mutated the receiver") + } + if withClient.httpClient == nil { + t.Fatal("WithHTTPClient did not apply to the derived service") + } + if scoped == base || readOnly == base || withClient == base { + t.Fatal("With* helpers returned the receiver instead of a derived service") + } + // A derived service must keep the specialization of the service it came from. + if !readOnly.WithStore(st).ReadOnly() { + t.Fatal("WithStore dropped the read-only flag") + } +} + +func TestResetConfigurationInitializedReopensYAMLImport(t *testing.T) { + st, err := store.New(t.TempDir()) + if err != nil { + t.Fatalf("store.New() error = %v", err) + } + defer st.Close() + svc := NewService(st) + + if initialized, err := svc.HasConfiguration(); err != nil || initialized { + t.Fatalf("HasConfiguration() on a fresh store = %v, %v; want false", initialized, err) + } + if err := svc.MarkConfigurationInitialized(); err != nil { + t.Fatalf("MarkConfigurationInitialized() error = %v", err) + } + if initialized, err := svc.HasConfiguration(); err != nil || !initialized { + t.Fatalf("HasConfiguration() after marking = %v, %v; want true", initialized, err) + } + if err := svc.ResetConfigurationInitialized(); err != nil { + t.Fatalf("ResetConfigurationInitialized() error = %v", err) + } + if initialized, err := svc.HasConfiguration(); err != nil || initialized { + t.Fatalf("HasConfiguration() after reset = %v, %v; want false", initialized, err) + } + // Resetting never deletes channels: stored configuration still wins over YAML. + if _, err := st.UpsertChannelConfig(store.ChannelConfigRecord{ID: "primary", Name: "Primary", Enabled: true, BaseURL: "https://example.invalid/v1"}); err != nil { + t.Fatal(err) + } + if initialized, err := svc.HasConfiguration(); err != nil || !initialized { + t.Fatalf("HasConfiguration() with a stored channel = %v, %v; want true", initialized, err) + } + if err := svc.ResetConfigurationInitialized(); err != nil { + t.Fatalf("ResetConfigurationInitialized() error = %v", err) + } + if initialized, err := svc.HasConfiguration(); err != nil || !initialized { + t.Fatalf("reset dropped stored channels: initialized=%v err=%v", initialized, err) + } +} + +func TestBootstrapFromConfigStopsWritingOnceInitialized(t *testing.T) { + st, err := store.New(t.TempDir()) + if err != nil { + t.Fatalf("store.New() error = %v", err) + } + defer st.Close() + svc := NewService(st) + + if _, err := st.UpsertChannelConfig(store.ChannelConfigRecord{ID: "primary", Name: "Primary", Enabled: true, BaseURL: "https://example.invalid/v1"}); err != nil { + t.Fatal(err) + } + if err := svc.ResetConfigurationInitialized(); err != nil { + t.Fatal(err) + } + imported, err := svc.BootstrapFromConfig(&config.Config{}) + if err != nil || imported != 0 { + t.Fatalf("BootstrapFromConfig() = %d, %v; want 0, nil", imported, err) + } + var marker bool + if found, err := st.LoadAppSettingJSON(context.Background(), "channels.initialized", &marker); err != nil { + t.Fatal(err) + } else if found { + t.Fatal("an already-initialized startup rewrote the bootstrap marker") + } + if initialized, err := svc.HasConfiguration(); err != nil || !initialized { + t.Fatalf("stored channels no longer count as initialized: initialized=%v err=%v", initialized, err) + } +} + +func TestBootstrapFromConfigReimportsYAMLAfterReset(t *testing.T) { + st, err := store.New(t.TempDir()) + if err != nil { + t.Fatalf("store.New() error = %v", err) + } + defer st.Close() + svc := NewService(st) + + enabled := true + cfg := &config.Config{ + Upstreams: []config.UpstreamTargetConfig{{ + ID: "openai-primary", + Enabled: &enabled, + Upstream: config.UpstreamConfig{ + BaseURL: "https://example.invalid/v1", + ProviderPreset: "openai", + }, + }}, + } + if imported, err := svc.BootstrapFromConfig(cfg); err != nil || imported != 1 { + t.Fatalf("first BootstrapFromConfig() = %d, %v; want 1, nil", imported, err) + } + channels, err := st.ListChannelConfigs() + if err != nil { + t.Fatal(err) + } + for _, record := range channels { + if err := st.DeleteChannelConfig(record.ID); err != nil { + t.Fatal(err) + } + } + if initialized, err := svc.HasConfiguration(); err != nil || !initialized { + t.Fatalf("marker was not persisted by the first import: initialized=%v err=%v", initialized, err) + } + if imported, err := svc.BootstrapFromConfig(cfg); err != nil || imported != 0 { + t.Fatalf("BootstrapFromConfig() before reset = %d, %v; want 0, nil", imported, err) + } + if err := svc.ResetConfigurationInitialized(); err != nil { + t.Fatal(err) + } + if imported, err := svc.BootstrapFromConfig(cfg); err != nil || imported != 1 { + t.Fatalf("BootstrapFromConfig() after reset = %d, %v; want 1, nil", imported, err) + } +} diff --git a/internal/monitor/configuration_test.go b/internal/monitor/configuration_test.go index 4ba2d0d..b4ff5b9 100644 --- a/internal/monitor/configuration_test.go +++ b/internal/monitor/configuration_test.go @@ -198,3 +198,40 @@ func TestDeleteChannelRollbackRestoresItsModels(t *testing.T) { t.Fatalf("models not restored: %v", models) } } + +func TestChannelBootstrapSettingsReportAndResetTheMarker(t *testing.T) { + st, err := store.New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = st.Close() }) + svc := channel.NewService(st) + mux := http.NewServeMux() + RegisterRoutes(mux, st, RouteOptions{ChannelService: svc}) + + rr := configurationRequest(mux, http.MethodGet, "/api/settings/channels", "") + if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), `"initialized":false`) { + t.Fatalf("GET on a fresh database status=%d body=%s", rr.Code, rr.Body) + } + + if err := svc.MarkConfigurationInitialized(); err != nil { + t.Fatal(err) + } + rr = configurationRequest(mux, http.MethodGet, "/api/settings/channels", "") + if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), `"initialized":true`) { + t.Fatalf("GET after marking status=%d body=%s", rr.Code, rr.Body) + } + + rr = configurationRequest(mux, http.MethodDelete, "/api/settings/channels", "") + if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), `"initialized":false`) { + t.Fatalf("DELETE status=%d body=%s", rr.Code, rr.Body) + } + if initialized, err := svc.HasConfiguration(); err != nil || initialized { + t.Fatalf("marker survived the reset: initialized=%v err=%v", initialized, err) + } + + rr = configurationRequest(mux, http.MethodPost, "/api/settings/channels", "") + if rr.Code != http.StatusNotFound { + t.Fatalf("POST status=%d body=%s, want 404", rr.Code, rr.Body) + } +} diff --git a/internal/monitor/server.go b/internal/monitor/server.go index 1b2e314..7dfb6ed 100644 --- a/internal/monitor/server.go +++ b/internal/monitor/server.go @@ -1509,6 +1509,7 @@ func RegisterRoutes(mux *http.ServeMux, st *store.Store, opts ...RouteOptions) { mux.HandleFunc("/api/routing/inspect", monitorAuthRequired(routingInspectAPIHandler(st), monitorVerifier)) mux.HandleFunc("/api/routing/summary", monitorAuthRequired(routingSummaryAPIHandler(st), monitorVerifier)) mux.HandleFunc("/api/settings/routing", monitorAuthRequired(routingSettingsAPIHandler(st), monitorVerifier)) + mux.HandleFunc("/api/settings/channels", monitorAuthRequired(channelBootstrapSettingsAPIHandler(st, opt.ChannelService), monitorVerifier)) mux.HandleFunc("/api/model-aliases/validate", monitorAuthRequired(modelAliasValidateAPIHandler(st), monitorVerifier)) mux.HandleFunc("/api/model-aliases", monitorAuthRequired(configurationAPIHandler(st, opt.Router, opt.ChannelService, func(st *store.Store, _ *router.Router, _ *channel.Service) http.HandlerFunc { return modelAliasListCreateAPIHandler(st) @@ -3094,6 +3095,55 @@ func routingSettingsAPIHandler(st *store.Store) http.HandlerFunc { } } +// channelBootstrapSettingsAPIHandler reports and clears the YAML bootstrap +// marker for channel configuration. GET answers whether the database owns the +// routing configuration; DELETE clears only the explicit marker, so a database +// that still stores channels keeps winning over YAML. +func channelBootstrapSettingsAPIHandler(st *store.Store, channelService *channel.Service) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if st == nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "store not configured"}) + return + } + svc := effectiveChannelService(st, channelService) + if svc == nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "store not configured"}) + return + } + switch r.Method { + case http.MethodGet: + initialized, err := svc.HasConfiguration() + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, map[string]bool{"initialized": initialized}) + case http.MethodDelete: + // Clearing the marker is a settings write, but it still goes through + // the configuration transaction so it cannot interleave with a + // management write that re-marks it. + err := st.ConfigurationTransaction(r.Context(), func(tx *store.Store, commit func() error) error { + if err := svc.WithStore(tx).ResetConfigurationInitialized(); err != nil { + return err + } + return commit() + }) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + initialized, err := svc.HasConfiguration() + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, map[string]bool{"initialized": initialized}) + default: + http.NotFound(w, r) + } + } +} + func modelAliasListCreateAPIHandler(st *store.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if st == nil { diff --git a/internal/router/router.go b/internal/router/router.go index 7870298..15ae682 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -528,10 +528,21 @@ func (r *Router) Reload(targetCfgs []config.UpstreamTargetConfig) error { // ReloadWithCommit prepares the next snapshot before committing configuration. // A failed preparation or commit leaves the running snapshot unchanged. +// +// The upstream write lock is taken before reloadMu so the lock order stays +// configMu -> upstreamMu -> reloadMu -> r.mu: a configuration request that +// already owns the upstream lock must never wait for a reload that waits for it. func (r *Router) ReloadWithCommit(targetCfgs []config.UpstreamTargetConfig, st *store.Store, commit func() error) error { if r == nil { return fmt.Errorf("router is nil") } + if st != nil && !st.TransactionScoped() { + release, err := st.LockUpstreamWrites() + if err != nil { + return err + } + defer release() + } r.reloadMu.Lock() defer r.reloadMu.Unlock() nextTargets, err := buildTargets(targetCfgs) @@ -551,19 +562,22 @@ func (r *Router) ReloadWithCommit(targetCfgs []config.UpstreamTargetConfig, st * } } - if len(nextTargets) > 0 { - if _, err := r.refreshTargetsWithStore(nextTargets, st); err != nil { - return err - } + // The caller holds the upstream write lock, so persisting here cannot + // interleave with a background refresh. + if _, err := r.refreshTargetsWithStore(nextTargets, st); err != nil { + return err } - r.mu.Lock() + // Commit before publishing: the running snapshot must never describe + // configuration the database rejected, while a failed commit has to leave + // the previous snapshot in place. Publishing after the commit keeps the + // router lock out of the database transaction. if commit != nil { if err := commit(); err != nil { - r.mu.Unlock() return err } } + r.mu.Lock() r.targets = nextTargets r.rebuildCatalog() r.mu.Unlock() @@ -1232,54 +1246,127 @@ func (r *Router) refreshAll() (int, error) { return r.refreshTargets(targets) } +// refreshTargets is the background and proxy entry point. Persistence is best +// effort there: when a configuration change owns the upstream write lock the +// refreshed state still lands in memory, and the next refresh stores it. func (r *Router) refreshTargets(targets []*Target) (int, error) { - return r.refreshTargetsWithStore(targets, r.store) + usable, writes := r.refreshTargetsInMemory(targets) + st := r.store + if st == nil || len(writes) == 0 { + return usable, nil + } + release, locked, err := st.TryLockUpstreamWrites() + if err != nil { + return usable, err + } + if !locked { + return usable, nil + } + defer release() + // The configuration change may have removed targets after this snapshot was + // taken; never resurrect rows for targets that are no longer live. + writes = r.liveRefreshWrites(writes) + if len(writes) == 0 { + return usable, nil + } + if err := r.persistRefreshWrites(st, writes); err != nil { + return usable, err + } + return usable, nil +} + +// liveRefreshWrites drops refresh results whose target left the running +// snapshot. Callers must hold the upstream write lock so a concurrent reload +// cannot change the live set between this filter and the write. +func (r *Router) liveRefreshWrites(writes []refreshWrite) []refreshWrite { + r.mu.RLock() + live := make(map[string]struct{}, len(r.targets)) + for _, target := range r.targets { + live[target.ID] = struct{}{} + } + r.mu.RUnlock() + kept := writes[:0] + for _, write := range writes { + if _, ok := live[write.record.ID]; ok { + kept = append(kept, write) + } + } + return kept } +// refreshTargetsWithStore refreshes targets and stores what it learned through +// st. The caller must hold the upstream write lock for st unless st is nil; +// ReloadWithCommit and refreshTargets both do. func (r *Router) refreshTargetsWithStore(targets []*Target, st *store.Store) (int, error) { + usable, writes := r.refreshTargetsInMemory(targets) + if st == nil || len(writes) == 0 { + return usable, nil + } + if err := r.persistRefreshWrites(st, writes); err != nil { + return usable, err + } + return usable, nil +} + +type refreshWrite struct { + record store.UpstreamTargetRecord + models []store.UpstreamModelRecord +} + +// refreshTargetsInMemory refreshes targets over the network and applies the +// result to the running targets. It never touches the database. +func (r *Router) refreshTargetsInMemory(targets []*Target) (int, []refreshWrite) { var usable int + writes := make([]refreshWrite, 0, len(targets)) for _, target := range targets { models, status, refreshErr := r.refreshTarget(target) if refreshErr == nil || len(models) > 0 || target.allowUnknownModels { usable++ } target.setRefreshResult(models, status, refreshErr, r.failureThreshold, r.openWindow, r.costs) - if st != nil { - record := store.UpstreamTargetRecord{ - ID: target.ID, - BaseURL: target.Upstream.BaseURL, - ProviderPreset: target.Upstream.ProviderPreset, - ProtocolFamily: target.Upstream.ProtocolFamily, - RoutingProfile: target.Upstream.RoutingProfile, - Enabled: target.Enabled, - Priority: target.Priority, - Weight: target.Weight, - CapacityHint: target.CapacityHint, - LastRefreshAt: target.snapshot().LastRefreshAt, - LastRefreshStatus: status, - } - if refreshErr != nil { - record.LastRefreshError = refreshErr.Error() - } - if err := st.UpsertUpstreamTarget(record); err != nil { - return 0, err - } - modelRecords := make([]store.UpstreamModelRecord, 0, len(models)) - seenAt := time.Now().UTC() - for _, model := range models { - modelRecords = append(modelRecords, store.UpstreamModelRecord{ - UpstreamID: target.ID, - Model: model, - Source: "catalog", - SeenAt: seenAt, - }) - } - if err := st.ReplaceUpstreamModels(target.ID, modelRecords); err != nil { - return 0, err - } + record := store.UpstreamTargetRecord{ + ID: target.ID, + BaseURL: target.Upstream.BaseURL, + ProviderPreset: target.Upstream.ProviderPreset, + ProtocolFamily: target.Upstream.ProtocolFamily, + RoutingProfile: target.Upstream.RoutingProfile, + Enabled: target.Enabled, + Priority: target.Priority, + Weight: target.Weight, + CapacityHint: target.CapacityHint, + LastRefreshAt: target.snapshot().LastRefreshAt, + LastRefreshStatus: status, + } + if refreshErr != nil { + record.LastRefreshError = refreshErr.Error() + } + modelRecords := make([]store.UpstreamModelRecord, 0, len(models)) + seenAt := time.Now().UTC() + for _, model := range models { + modelRecords = append(modelRecords, store.UpstreamModelRecord{ + UpstreamID: target.ID, + Model: model, + Source: "catalog", + SeenAt: seenAt, + }) + } + writes = append(writes, refreshWrite{record: record, models: modelRecords}) + } + return usable, writes +} + +// persistRefreshWrites stores refresh results. The caller must hold the +// upstream write lock. +func (r *Router) persistRefreshWrites(st *store.Store, writes []refreshWrite) error { + for _, write := range writes { + if err := st.UpsertUpstreamTarget(write.record); err != nil { + return err + } + if err := st.ReplaceUpstreamModels(write.record.ID, write.models); err != nil { + return err } } - return usable, nil + return nil } func (r *Router) rebuildCatalog() { diff --git a/internal/router/router_config_lock_test.go b/internal/router/router_config_lock_test.go new file mode 100644 index 0000000..d772f7a --- /dev/null +++ b/internal/router/router_config_lock_test.go @@ -0,0 +1,132 @@ +package router + +import ( + "testing" + "time" + + "github.com/kingfs/llm-tracelab/internal/config" + "github.com/kingfs/llm-tracelab/internal/store" +) + +func staticRefreshTarget(id string, models ...string) config.UpstreamTargetConfig { + return config.UpstreamTargetConfig{ + ID: id, + Enabled: boolPtr(true), + Priority: 100, + ModelDiscovery: ModelDiscoveryStaticOnly, + StaticModels: models, + Upstream: config.UpstreamConfig{ + BaseURL: "https://api.openai.com/v1", + ProviderPreset: "openai", + }, + } +} + +// Committing configuration can take a database round trip. Routing must keep +// working while it settles: the router lock has to be released before commit. +func TestReloadWithCommitReleasesRouterLockWhileCommitting(t *testing.T) { + rtr, err := New(&config.Config{}, nil) + if err != nil { + t.Fatal(err) + } + if err := rtr.Reload([]config.UpstreamTargetConfig{staticRefreshTarget("primary", "gpt-5")}); err != nil { + t.Fatal(err) + } + + committing := make(chan struct{}) + allowCommit := make(chan struct{}) + reloaded := make(chan error, 1) + go func() { + reloaded <- rtr.ReloadWithCommit([]config.UpstreamTargetConfig{staticRefreshTarget("next", "gpt-5")}, nil, func() error { + close(committing) + <-allowCommit + return nil + }) + }() + <-committing + + targets := make(chan int, 1) + go func() { targets <- len(rtr.Targets()) }() + select { + case count := <-targets: + if count != 1 { + t.Fatalf("targets visible during commit = %d, want the previous snapshot with 1", count) + } + case <-time.After(2 * time.Second): + t.Fatal("router lock is held while the configuration transaction commits") + } + + close(allowCommit) + if err := <-reloaded; err != nil { + t.Fatalf("ReloadWithCommit() error = %v", err) + } + got := rtr.Targets() + if len(got) != 1 || got[0].ID != "next" { + t.Fatalf("published targets = %v, want next", got) + } +} + +// A refresh snapshot taken before a configuration change must not resurrect rows +// for targets that change removed. +func TestLiveRefreshWritesDropsTargetsThatLeftTheSnapshot(t *testing.T) { + rtr, err := New(&config.Config{}, nil) + if err != nil { + t.Fatal(err) + } + if err := rtr.Reload([]config.UpstreamTargetConfig{staticRefreshTarget("primary", "gpt-5")}); err != nil { + t.Fatal(err) + } + writes := []refreshWrite{ + {record: store.UpstreamTargetRecord{ID: "primary"}}, + {record: store.UpstreamTargetRecord{ID: "deleted"}}, + } + kept := rtr.liveRefreshWrites(writes) + if len(kept) != 1 || kept[0].record.ID != "primary" { + t.Fatalf("liveRefreshWrites() = %+v, want only primary", kept) + } +} + +func TestRefreshTargetsSkipsPersistenceWhileTheUpstreamLockIsHeld(t *testing.T) { + st, err := store.New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = st.Close() }) + + rtr, err := New(&config.Config{}, st) + if err != nil { + t.Fatal(err) + } + if err := rtr.ReloadWithCommit([]config.UpstreamTargetConfig{staticRefreshTarget("primary", "gpt-5")}, nil, nil); err != nil { + t.Fatal(err) + } + + release, err := st.LockUpstreamWrites() + if err != nil { + t.Fatal(err) + } + usable, err := rtr.refreshTargets(rtr.Targets()) + if err != nil { + t.Fatalf("refreshTargets() error = %v", err) + } + if usable != 1 { + t.Fatalf("usable targets = %d, want 1", usable) + } + if rows, err := st.ListUpstreamTargets(); err != nil { + t.Fatal(err) + } else if len(rows) != 0 { + t.Fatalf("refresh persisted %d rows while a configuration change held the lock", len(rows)) + } + + release() + if _, err := rtr.refreshTargets(rtr.Targets()); err != nil { + t.Fatalf("refreshTargets() after release error = %v", err) + } + rows, err := st.ListUpstreamTargets() + if err != nil { + t.Fatal(err) + } + if len(rows) != 1 || rows[0].ID != "primary" { + t.Fatalf("refresh rows after release = %+v, want primary", rows) + } +} diff --git a/internal/store/app_settings.go b/internal/store/app_settings.go index c2b07ec..87d9140 100644 --- a/internal/store/app_settings.go +++ b/internal/store/app_settings.go @@ -53,3 +53,23 @@ func (s *Store) LoadAppSettingJSON(ctx context.Context, key string, out any) (bo } return true, nil } + +// DeleteAppSetting removes one app setting and reports whether a row existed. +func (s *Store) DeleteAppSetting(ctx context.Context, key string) (bool, error) { + if s == nil || s.db == nil { + return false, errorsNewStoreClosed() + } + key = strings.TrimSpace(key) + if key == "" { + return false, fmt.Errorf("setting key is required") + } + result, err := s.db.ExecContext(ctx, `DELETE FROM app_settings WHERE setting_key = ?`, key) + if err != nil { + return false, fmt.Errorf("delete app setting %q: %w", key, err) + } + affected, err := result.RowsAffected() + if err != nil { + return false, fmt.Errorf("delete app setting %q: %w", key, err) + } + return affected > 0, nil +} diff --git a/internal/store/config_transaction.go b/internal/store/config_transaction.go index d69b247..217f42e 100644 --- a/internal/store/config_transaction.go +++ b/internal/store/config_transaction.go @@ -13,9 +13,24 @@ import ( // ConfigurationTransaction serializes management writes in this process. Both // raw SQL and ent operations share one transaction. The callback must commit // explicitly, after preparing its runtime snapshot; all other exits roll back. +// +// The transaction is also the only writer of upstream_targets/upstream_models, +// so it holds the upstream write lock for its whole duration. See +// LockUpstreamWrites and TryLockUpstreamWrites for the refresh path. func (s *Store) ConfigurationTransaction(ctx context.Context, apply func(*Store, func() error) error) error { - s.configMu.Lock() - defer s.configMu.Unlock() + if s == nil || s.db == nil || s.shared == nil { + return fmt.Errorf("configuration transaction: %w", errorsNewStoreClosed()) + } + if apply == nil { + return fmt.Errorf("configuration transaction: apply callback is required") + } + if s.TransactionScoped() { + return fmt.Errorf("configuration transaction: %w", ErrNestedTransaction) + } + s.shared.configMu.Lock() + defer s.shared.configMu.Unlock() + s.shared.upstreamMu.Lock() + defer s.shared.upstreamMu.Unlock() tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) if err != nil { return err @@ -31,6 +46,7 @@ func (s *Store) ConfigurationTransaction(ctx context.Context, apply func(*Store, client: dao.NewClient(dao.Driver(driver)), outputDir: s.outputDir, dbPath: s.dbPath, driver: s.driver, secrets: s.secrets, useSessionSummaryRead: s.useSessionSummaryRead, + shared: s.shared, } committed := false commit := func() error { @@ -49,6 +65,48 @@ func (s *Store) ConfigurationTransaction(ctx context.Context, apply func(*Store, return nil } +// TransactionScoped reports whether this store view writes through one open +// transaction instead of the connection pool. A transaction-scoped store +// already holds the configuration and upstream write locks. +func (s *Store) TransactionScoped() bool { + return s != nil && s.db != nil && s.db.tx != nil +} + +// LockUpstreamWrites blocks until this process may write upstream_targets and +// upstream_models on its own, and returns the release function. +// +// ConfigurationTransaction holds the same lock for its whole duration, so a +// store that already runs inside one must not call this again; that case +// reports ErrNestedTransaction instead of deadlocking. +func (s *Store) LockUpstreamWrites() (func(), error) { + if s == nil || s.shared == nil { + return nil, fmt.Errorf("lock upstream writes: %w", errorsNewStoreClosed()) + } + if s.TransactionScoped() { + return nil, fmt.Errorf("lock upstream writes: %w", ErrNestedTransaction) + } + s.shared.upstreamMu.Lock() + return s.shared.upstreamMu.Unlock, nil +} + +// TryLockUpstreamWrites is the non-blocking form of LockUpstreamWrites. It +// reports locked=false when a configuration change currently owns the lock, +// which lets best-effort refresh persistence skip the write and stay +// up-to-date in memory instead of stalling its caller (for example a proxy +// retry). +func (s *Store) TryLockUpstreamWrites() (release func(), locked bool, err error) { + if s == nil || s.shared == nil { + return nil, false, fmt.Errorf("lock upstream writes: %w", errorsNewStoreClosed()) + } + if s.TransactionScoped() { + return nil, false, fmt.Errorf("lock upstream writes: %w", ErrNestedTransaction) + } + if !s.shared.upstreamMu.TryLock() { + return nil, false, nil + } + return s.shared.upstreamMu.Unlock, true, nil +} + // Store helpers may open nested ent transactions. Their commits are scoped to // the outer configuration operation, so a later failure rolls back everything. type configurationTxDriver struct{ *entsql.Driver } diff --git a/internal/store/config_transaction_test.go b/internal/store/config_transaction_test.go new file mode 100644 index 0000000..c5301ff --- /dev/null +++ b/internal/store/config_transaction_test.go @@ -0,0 +1,185 @@ +package store + +import ( + "context" + "errors" + "testing" + "time" +) + +func configTransactionTestStore(t *testing.T) *Store { + t.Helper() + st, err := New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = st.Close() }) + return st +} + +func TestConfigurationTransactionRejectsNestedTransactions(t *testing.T) { + st := configTransactionTestStore(t) + var nestedConfiguration, nestedBegin, nestedBeginTx error + err := st.ConfigurationTransaction(context.Background(), func(tx *Store, commit func() error) error { + if !tx.TransactionScoped() { + t.Error("configuration view is not transaction scoped") + } + if st.TransactionScoped() { + t.Error("base store became transaction scoped") + } + nestedConfiguration = tx.ConfigurationTransaction(context.Background(), func(*Store, func() error) error { + return nil + }) + if _, _, err := tx.TryLockUpstreamWrites(); !errors.Is(err, ErrNestedTransaction) { + t.Errorf("TryLockUpstreamWrites() error = %v, want ErrNestedTransaction", err) + } + if _, err := tx.LockUpstreamWrites(); !errors.Is(err, ErrNestedTransaction) { + t.Errorf("LockUpstreamWrites() error = %v, want ErrNestedTransaction", err) + } + _, nestedBeginTx = tx.db.BeginTx(context.Background(), nil) + _, nestedBegin = tx.db.Begin() + return commit() + }) + if err != nil { + t.Fatalf("ConfigurationTransaction() error = %v", err) + } + for name, err := range map[string]error{"ConfigurationTransaction": nestedConfiguration, "BeginTx": nestedBeginTx, "Begin": nestedBegin} { + if !errors.Is(err, ErrNestedTransaction) { + t.Errorf("nested %s error = %v, want ErrNestedTransaction", name, err) + } + } +} + +func TestConfigurationTransactionLeavesBaseStoreTransactionsUsable(t *testing.T) { + st := configTransactionTestStore(t) + tx, err := st.db.Begin() + if err != nil { + t.Fatalf("Begin() error = %v", err) + } + if err := tx.Rollback(); err != nil { + t.Fatalf("Rollback() error = %v", err) + } + if st.TransactionScoped() { + t.Fatal("base store reported itself as transaction scoped") + } +} + +func TestConfigurationTransactionRejectsUnusableInputs(t *testing.T) { + if err := (*Store)(nil).ConfigurationTransaction(context.Background(), func(*Store, func() error) error { return nil }); err == nil { + t.Fatal("nil store accepted a configuration transaction") + } + st := configTransactionTestStore(t) + if err := st.ConfigurationTransaction(context.Background(), nil); err == nil { + t.Fatal("nil apply callback was accepted") + } + release, err := st.LockUpstreamWrites() + if err != nil { + t.Fatalf("LockUpstreamWrites() on an idle store error = %v", err) + } + release() +} + +func TestConfigurationTransactionViewSharesEventSubscribers(t *testing.T) { + st := configTransactionTestStore(t) + notifications, cancel := st.SubscribeSystemEvents(1) + defer cancel() + + err := st.ConfigurationTransaction(context.Background(), func(tx *Store, commit func() error) error { + if _, err := tx.UpsertSystemEvent(SystemEvent{Fingerprint: "fp-config", Source: "monitor", Category: "configuration", Severity: "info", Title: "config"}); err != nil { + return err + } + return commit() + }) + if err != nil { + t.Fatalf("ConfigurationTransaction() error = %v", err) + } + + select { + case notification := <-notifications: + if notification.Sequence != 1 { + t.Fatalf("notification sequence = %d, want 1", notification.Sequence) + } + case <-time.After(2 * time.Second): + t.Fatal("configuration view did not publish system events to the base store subscribers") + } +} + +func TestLockUpstreamWritesIsExclusiveAndNonBlockingVariantReportsBusy(t *testing.T) { + st := configTransactionTestStore(t) + release, err := st.LockUpstreamWrites() + if err != nil { + t.Fatalf("LockUpstreamWrites() error = %v", err) + } + released := false + defer func() { + if !released { + release() + } + }() + + if _, locked, err := st.TryLockUpstreamWrites(); err != nil { + t.Fatalf("TryLockUpstreamWrites() error = %v", err) + } else if locked { + t.Fatal("TryLockUpstreamWrites() reported a free lock while it was held") + } + + acquired := make(chan struct{}) + go func() { + inner, err := st.LockUpstreamWrites() + if err != nil { + t.Errorf("LockUpstreamWrites() error = %v", err) + close(acquired) + return + } + inner() + close(acquired) + }() + select { + case <-acquired: + t.Fatal("LockUpstreamWrites() did not wait for the current holder") + case <-time.After(50 * time.Millisecond): + } + release() + released = true + select { + case <-acquired: + case <-time.After(2 * time.Second): + t.Fatal("LockUpstreamWrites() did not unblock after release") + } +} + +func TestDeleteAppSettingRemovesOnlyTheRequestedKey(t *testing.T) { + st := configTransactionTestStore(t) + ctx := context.Background() + if err := st.SaveAppSettingJSON(ctx, "keep", map[string]string{"value": "keep"}); err != nil { + t.Fatal(err) + } + if err := st.SaveAppSettingJSON(ctx, "drop", map[string]string{"value": "drop"}); err != nil { + t.Fatal(err) + } + deleted, err := st.DeleteAppSetting(ctx, "drop") + if err != nil { + t.Fatalf("DeleteAppSetting() error = %v", err) + } + if !deleted { + t.Fatal("DeleteAppSetting() reported no deletion for an existing key") + } + var out map[string]string + found, err := st.LoadAppSettingJSON(ctx, "drop", &out) + if err != nil { + t.Fatal(err) + } + if found { + t.Fatalf("deleted setting is still readable: %v", out) + } + if found, err := st.LoadAppSettingJSON(ctx, "keep", &out); err != nil || !found { + t.Fatalf("unrelated setting changed: found=%v err=%v", found, err) + } + deleted, err = st.DeleteAppSetting(ctx, "drop") + if err != nil { + t.Fatalf("DeleteAppSetting() error = %v", err) + } + if deleted { + t.Fatal("DeleteAppSetting() reported a deletion for a missing key") + } +} diff --git a/internal/store/store.go b/internal/store/store.go index f221e15..7959abb 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -94,11 +94,21 @@ type Store struct { driver string secrets *secretBox useSessionSummaryRead bool - configMu sync.Mutex - syncMu sync.Mutex - eventMu sync.Mutex - eventSeq uint64 - eventSubs map[chan SystemEventNotification]struct{} + shared *storeShared +} + +// storeShared keeps the state that every Store view of the same database must +// share. ConfigurationTransaction builds a view whose db/client are rebound to +// one transaction; if the writer locks or the live system-event subscribers were +// copied instead of shared, such a view would silently diverge from the store it +// was derived from. +type storeShared struct { + configMu sync.Mutex + upstreamMu sync.Mutex + syncMu sync.Mutex + eventMu sync.Mutex + eventSeq uint64 + eventSubs map[chan SystemEventNotification]struct{} } type DatabaseOptions struct { @@ -112,6 +122,26 @@ type rebindingDB struct { driver string } +// ErrNestedTransaction reports an attempt to open a second, independent +// transaction from a store that already runs inside one. database/sql has no +// nested transactions, and the promoted *sql.DB.Begin/BeginTx would silently +// borrow a pooled connection and escape the outer transaction. +var ErrNestedTransaction = errors.New("store: nested transaction on a transaction-scoped store") + +func (db *rebindingDB) Begin() (*sql.Tx, error) { + return db.BeginTx(context.Background(), nil) +} + +func (db *rebindingDB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) { + if db == nil || db.DB == nil { + return nil, errors.New("store: database is not configured") + } + if db.tx != nil { + return nil, ErrNestedTransaction + } + return db.DB.BeginTx(ctx, opts) +} + func (db *rebindingDB) Exec(query string, args ...any) (sql.Result, error) { return db.ExecContext(context.Background(), query, args...) } @@ -2871,6 +2901,7 @@ func NewWithDatabaseOptions(outputDir string, driver string, dsn string, maxOpen outputDir: outputDir, dbPath: dbPath, driver: driver, + shared: &storeShared{}, secrets: secrets, useSessionSummaryRead: opts.UseSessionSummaryRead, } @@ -5766,8 +5797,8 @@ func (s *Store) insertSessionSummaryFromLogsSQL(whereSQL string) string { const timeLayout = "2006-01-02T15:04:05.999999999Z07:00" func (s *Store) Sync() error { - s.syncMu.Lock() - defer s.syncMu.Unlock() + s.shared.syncMu.Lock() + defer s.shared.syncMu.Unlock() freshness, err := s.loadFreshness() if err != nil { @@ -6766,19 +6797,19 @@ func (s *Store) SubscribeSystemEvents(buffer int) (<-chan SystemEventNotificatio buffer = 8 } ch := make(chan SystemEventNotification, buffer) - s.eventMu.Lock() - if s.eventSubs == nil { - s.eventSubs = map[chan SystemEventNotification]struct{}{} + s.shared.eventMu.Lock() + if s.shared.eventSubs == nil { + s.shared.eventSubs = map[chan SystemEventNotification]struct{}{} } - s.eventSubs[ch] = struct{}{} - s.eventMu.Unlock() + s.shared.eventSubs[ch] = struct{}{} + s.shared.eventMu.Unlock() return ch, func() { - s.eventMu.Lock() - if _, ok := s.eventSubs[ch]; ok { - delete(s.eventSubs, ch) + s.shared.eventMu.Lock() + if _, ok := s.shared.eventSubs[ch]; ok { + delete(s.shared.eventSubs, ch) close(ch) } - s.eventMu.Unlock() + s.shared.eventMu.Unlock() } } @@ -9292,10 +9323,10 @@ func (s *Store) notifySystemEventIDChanged(id string) { } func (s *Store) notifySystemEventChanged(event SystemEvent) { - s.eventMu.Lock() - s.eventSeq++ + s.shared.eventMu.Lock() + s.shared.eventSeq++ notification := SystemEventNotification{ - Sequence: s.eventSeq, + Sequence: s.shared.eventSeq, EventID: event.ID, Status: event.Status, Severity: event.Severity, @@ -9303,11 +9334,11 @@ func (s *Store) notifySystemEventChanged(event SystemEvent) { Category: event.Category, At: time.Now().UTC(), } - subs := make([]chan SystemEventNotification, 0, len(s.eventSubs)) - for ch := range s.eventSubs { + subs := make([]chan SystemEventNotification, 0, len(s.shared.eventSubs)) + for ch := range s.shared.eventSubs { subs = append(subs, ch) } - s.eventMu.Unlock() + s.shared.eventMu.Unlock() for _, ch := range subs { select {