diff --git a/.github/codecov.yml b/.github/codecov.yml index df12f874..7912dde0 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,6 +1,9 @@ ignore: - - "watchdog/watchdog.go" - "storage/store/sql/specific_postgres.go" # Can't test for postgres + - "watchdog/endpoint.go" + - "watchdog/external_endpoint.go" + - "watchdog/suite.go" + - "watchdog/watchdog.go" comment: false coverage: status: diff --git a/README.md b/README.md index ae5c1e42..a87bdfc3 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga - [Configuration](#configuration) - [Endpoints](#endpoints) - [External Endpoints](#external-endpoints) + - [Suites (ALPHA)](#suites-alpha) - [Conditions](#conditions) - [Placeholders](#placeholders) - [Functions](#functions) @@ -122,7 +123,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga - [Monitoring an endpoint using STARTTLS](#monitoring-an-endpoint-using-starttls) - [Monitoring an endpoint using TLS](#monitoring-an-endpoint-using-tls) - [Monitoring domain expiration](#monitoring-domain-expiration) - - [disable-monitoring-lock](#disable-monitoring-lock) + - [Concurrency](#concurrency) - [Reloading configuration on the fly](#reloading-configuration-on-the-fly) - [Endpoint groups](#endpoint-groups) - [How do I sort by group by default?](#how-do-i-sort-by-group-by-default) @@ -247,7 +248,8 @@ If you want to test it locally, see [Docker](#docker). | `endpoints` | [Endpoints configuration](#endpoints). | Required `[]` | | `external-endpoints` | [External Endpoints configuration](#external-endpoints). | `[]` | | `security` | [Security configuration](#security). | `{}` | -| `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock). | `false` | +| `concurrency` | Maximum number of endpoints/suites to monitor concurrently. Set to `0` for unlimited. See [Concurrency](#concurrency). | `3` | +| `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock). **Deprecated**: Use `concurrency: 0` instead. | `false` | | `skip-invalid-config-update` | Whether to ignore invalid configuration update.
See [Reloading configuration on the fly](#reloading-configuration-on-the-fly). | `false` | | `web` | Web configuration. | `{}` | | `web.address` | Address to listen on. | `0.0.0.0` | @@ -309,6 +311,8 @@ You can then configure alerts to be triggered when an endpoint is unhealthy once | `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` | | `endpoints[].ui.badge.response-time` | List of response time thresholds. Each time a threshold is reached, the badge has a different color. | `[50, 200, 300, 500, 750]` | | `endpoints[].extra-labels` | Extra labels to add to the metrics. Useful for grouping endpoints together. | `{}` | +| `endpoints[].always-run` | (SUITES ONLY) Whether to execute this endpoint even if previous endpoints in the suite failed. | `false` | +| `endpoints[].store` | (SUITES ONLY) Map of values to extract from the response and store in the suite context (stored even on failure). | `{}` | You may use the following placeholders in the body (`endpoints[].body`): - `[ENDPOINT_NAME]` (resolved from `endpoints[].name`) @@ -366,6 +370,99 @@ Where: You must also pass the token as a `Bearer` token in the `Authorization` header. +### Suites (ALPHA) +Suites are collections of endpoints that are executed sequentially with a shared context. +This allows you to create complex monitoring scenarios where the result from one endpoint can be used in subsequent endpoints, enabling workflow-style monitoring. + +Here are a few cases in which suites could be useful: +- Testing multi-step authentication flows (login -> access protected resource -> logout) +- API workflows where you need to chain requests (create resource -> update -> verify -> delete) +- Monitoring business processes that span multiple services +- Validating data consistency across multiple endpoints + +| Parameter | Description | Default | +|:----------------------------------|:----------------------------------------------------------------------------------------------------|:--------------| +| `suites` | List of suites to monitor. | `[]` | +| `suites[].enabled` | Whether to monitor the suite. | `true` | +| `suites[].name` | Name of the suite. Must be unique. | Required `""` | +| `suites[].group` | Group name. Used to group multiple suites together on the dashboard. | `""` | +| `suites[].interval` | Duration to wait between suite executions. | `10m` | +| `suites[].timeout` | Maximum duration for the entire suite execution. | `5m` | +| `suites[].context` | Initial context values that can be referenced by endpoints. | `{}` | +| `suites[].endpoints` | List of endpoints to execute sequentially. | Required `[]` | +| `suites[].endpoints[].store` | Map of values to extract from the response and store in the suite context (stored even on failure). | `{}` | +| `suites[].endpoints[].always-run` | Whether to execute this endpoint even if previous endpoints in the suite failed. | `false` | + +**Note**: Suite-level alerts are not supported yet. Configure alerts on individual endpoints within the suite instead. + +#### Using Context in Endpoints +Once values are stored in the context, they can be referenced in subsequent endpoints: +- In the URL: `https://api.example.com/users/[CONTEXT].userId` +- In headers: `Authorization: Bearer [CONTEXT].authToken` +- In the body: `{"user_id": "[CONTEXT].userId"}` +- In conditions: `[BODY].server_ip == [CONTEXT].serverIp` + +#### Example Suite Configuration +```yaml +suites: + - name: item-crud-workflow + group: api-tests + interval: 5m + context: + price: "19.99" # Initial static value in context + endpoints: + # Step 1: Create an item and store the item ID + - name: create-item + url: https://api.example.com/items + method: POST + body: '{"name": "Test Item", "price": "[CONTEXT].price"}' + conditions: + - "[STATUS] == 201" + - "len([BODY].id) > 0" + - "[BODY].price == [CONTEXT].price" + store: + itemId: "[BODY].id" + alerts: + - type: slack + description: "Failed to create item" + + # Step 2: Update the item using the stored item ID + - name: update-item + url: https://api.example.com/items/[CONTEXT].itemId + method: PUT + body: '{"price": "24.99"}' + conditions: + - "[STATUS] == 200" + alerts: + - type: slack + description: "Failed to update item" + + # Step 3: Fetch the item and validate the price + - name: get-item + url: https://api.example.com/items/[CONTEXT].itemId + method: GET + conditions: + - "[STATUS] == 200" + - "[BODY].price == 24.99" + alerts: + - type: slack + description: "Item price did not update correctly" + + # Step 4: Delete the item (always-run: true to ensure cleanup even if step 2 or 3 fails) + - name: delete-item + url: https://api.example.com/items/[CONTEXT].itemId + method: DELETE + always-run: true + conditions: + - "[STATUS] == 204" + alerts: + - type: slack + description: "Failed to delete item" +``` + +The suite will be considered successful only if all required endpoints pass their conditions. + + ### Conditions Here are some examples of conditions you can use: @@ -2921,17 +3018,34 @@ endpoints: > using the `[DOMAIN_EXPIRATION]` placeholder on an endpoint with an interval of less than `5m`. -### disable-monitoring-lock -Setting `disable-monitoring-lock` to `true` means that multiple endpoints could be monitored at the same time (i.e. parallel execution). +### Concurrency +By default, Gatus allows up to 5 endpoints/suites to be monitored concurrently. This provides a balance between performance and resource usage while maintaining accurate response time measurements. -While this behavior wouldn't generally be harmful, conditions using the `[RESPONSE_TIME]` placeholder could be impacted -by the evaluation of multiple endpoints at the same time, therefore, the default value for this parameter is `false`. +You can configure the concurrency level using the `concurrency` parameter: -There are three main reasons why you might want to disable the monitoring lock: -- You're using Gatus for load testing (each endpoint are periodically evaluated on a different goroutine, so -technically, if you create 100 endpoints with a 1 seconds interval, Gatus will send 100 requests per second) -- You have a _lot_ of endpoints to monitor -- You want to test multiple endpoints at very short intervals (< 5s) +```yaml +# Allow 10 endpoints/suites to be monitored concurrently +concurrency: 10 + +# Allow unlimited concurrent monitoring +concurrency: 0 + +# Use default concurrency (3) +# concurrency: 3 +``` + +**Important considerations:** +- Higher concurrency can improve monitoring performance when you have many endpoints +- Conditions using the `[RESPONSE_TIME]` placeholder may be less accurate with very high concurrency due to system resource contention +- Set to `0` for unlimited concurrency (equivalent to the deprecated `disable-monitoring-lock: true`) + +**Use cases for higher concurrency:** +- You have a large number of endpoints to monitor +- You want to monitor endpoints at very short intervals (< 5s) +- You're using Gatus for load testing scenarios + +**Legacy configuration:** +The `disable-monitoring-lock` parameter is deprecated but still supported for backward compatibility. It's equivalent to setting `concurrency: 0`. ### Reloading configuration on the fly diff --git a/api/api.go b/api/api.go index cb4bfeb5..93f10f4a 100644 --- a/api/api.go +++ b/api/api.go @@ -87,7 +87,8 @@ func (a *API) createRouter(cfg *config.Config) *fiber.App { unprotectedAPIRouter.Post("/v1/endpoints/:key/external", CreateExternalEndpointResult(cfg)) // SPA app.Get("/", SinglePageApplication(cfg.UI)) - app.Get("/endpoints/:name", SinglePageApplication(cfg.UI)) + app.Get("/endpoints/:key", SinglePageApplication(cfg.UI)) + app.Get("/suites/:key", SinglePageApplication(cfg.UI)) // Health endpoint healthHandler := health.Handler().WithJSON(true) app.Get("/health", func(c *fiber.Ctx) error { @@ -127,5 +128,7 @@ func (a *API) createRouter(cfg *config.Config) *fiber.App { } protectedAPIRouter.Get("/v1/endpoints/statuses", EndpointStatuses(cfg)) protectedAPIRouter.Get("/v1/endpoints/:key/statuses", EndpointStatus(cfg)) + protectedAPIRouter.Get("/v1/suites/statuses", SuiteStatuses(cfg)) + protectedAPIRouter.Get("/v1/suites/:key/statuses", SuiteStatus(cfg)) return app } diff --git a/api/badge_test.go b/api/badge_test.go index a5be60d2..63982ed5 100644 --- a/api/badge_test.go +++ b/api/badge_test.go @@ -34,8 +34,8 @@ func TestBadge(t *testing.T) { cfg.Endpoints[0].UIConfig = ui.GetDefaultConfig() cfg.Endpoints[1].UIConfig = ui.GetDefaultConfig() - watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()}) - watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()}) + watchdog.UpdateEndpointStatus(cfg.Endpoints[0], &endpoint.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()}) + watchdog.UpdateEndpointStatus(cfg.Endpoints[1], &endpoint.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()}) api := New(cfg) router := api.Router() type Scenario struct { @@ -284,8 +284,8 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) { }, } - store.Get().Insert(&firstTestEndpoint, &testSuccessfulResult) - store.Get().Insert(&secondTestEndpoint, &testSuccessfulResult) + store.Get().InsertEndpointResult(&firstTestEndpoint, &testSuccessfulResult) + store.Get().InsertEndpointResult(&secondTestEndpoint, &testSuccessfulResult) scenarios := []struct { Key string diff --git a/api/chart_test.go b/api/chart_test.go index 2a699a5b..86712112 100644 --- a/api/chart_test.go +++ b/api/chart_test.go @@ -28,8 +28,8 @@ func TestResponseTimeChart(t *testing.T) { }, }, } - watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()}) - watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()}) + watchdog.UpdateEndpointStatus(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()}) + watchdog.UpdateEndpointStatus(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()}) api := New(cfg) router := api.Router() type Scenario struct { diff --git a/api/endpoint_status_test.go b/api/endpoint_status_test.go index f835db06..dd37e570 100644 --- a/api/endpoint_status_test.go +++ b/api/endpoint_status_test.go @@ -101,8 +101,8 @@ func TestEndpointStatus(t *testing.T) { MaximumNumberOfEvents: storage.DefaultMaximumNumberOfEvents, }, } - watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()}) - watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()}) + watchdog.UpdateEndpointStatus(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()}) + watchdog.UpdateEndpointStatus(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()}) api := New(cfg) router := api.Router() type Scenario struct { @@ -156,8 +156,8 @@ func TestEndpointStatuses(t *testing.T) { defer cache.Clear() firstResult := &testSuccessfulResult secondResult := &testUnsuccessfulResult - store.Get().Insert(&testEndpoint, firstResult) - store.Get().Insert(&testEndpoint, secondResult) + store.Get().InsertEndpointResult(&testEndpoint, firstResult) + store.Get().InsertEndpointResult(&testEndpoint, secondResult) // Can't be bothered dealing with timezone issues on the worker that runs the automated tests firstResult.Timestamp = time.Time{} secondResult.Timestamp = time.Time{} diff --git a/api/external_endpoint.go b/api/external_endpoint.go index 88c7e444..2694c85c 100644 --- a/api/external_endpoint.go +++ b/api/external_endpoint.go @@ -60,7 +60,7 @@ func CreateExternalEndpointResult(cfg *config.Config) fiber.Handler { result.Errors = append(result.Errors, c.Query("error")) } convertedEndpoint := externalEndpoint.ToEndpoint() - if err := store.Get().Insert(convertedEndpoint, result); err != nil { + if err := store.Get().InsertEndpointResult(convertedEndpoint, result); err != nil { if errors.Is(err, common.ErrEndpointNotFound) { return c.Status(404).SendString(err.Error()) } diff --git a/api/raw_test.go b/api/raw_test.go index ac62c450..a61c4b39 100644 --- a/api/raw_test.go +++ b/api/raw_test.go @@ -33,8 +33,8 @@ func TestRawDataEndpoint(t *testing.T) { cfg.Endpoints[0].UIConfig = ui.GetDefaultConfig() cfg.Endpoints[1].UIConfig = ui.GetDefaultConfig() - watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()}) - watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()}) + watchdog.UpdateEndpointStatus(cfg.Endpoints[0], &endpoint.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()}) + watchdog.UpdateEndpointStatus(cfg.Endpoints[1], &endpoint.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()}) api := New(cfg) router := api.Router() type Scenario struct { diff --git a/api/spa_test.go b/api/spa_test.go index 58e02680..291892dc 100644 --- a/api/spa_test.go +++ b/api/spa_test.go @@ -34,8 +34,8 @@ func TestSinglePageApplication(t *testing.T) { Title: "example-title", }, } - watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()}) - watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()}) + watchdog.UpdateEndpointStatus(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()}) + watchdog.UpdateEndpointStatus(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()}) api := New(cfg) router := api.Router() type Scenario struct { diff --git a/api/suite_status.go b/api/suite_status.go new file mode 100644 index 00000000..5ead9c52 --- /dev/null +++ b/api/suite_status.go @@ -0,0 +1,59 @@ +package api + +import ( + "fmt" + + "github.com/TwiN/gatus/v5/config" + "github.com/TwiN/gatus/v5/config/suite" + "github.com/TwiN/gatus/v5/storage/store" + "github.com/TwiN/gatus/v5/storage/store/common/paging" + "github.com/gofiber/fiber/v2" +) + +// SuiteStatuses handles requests to retrieve all suite statuses +func SuiteStatuses(cfg *config.Config) fiber.Handler { + return func(c *fiber.Ctx) error { + page, pageSize := extractPageAndPageSizeFromRequest(c, 100) + params := paging.NewSuiteStatusParams().WithPagination(page, pageSize) + suiteStatuses, err := store.Get().GetAllSuiteStatuses(params) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": fmt.Sprintf("Failed to retrieve suite statuses: %v", err), + }) + } + // If no statuses exist yet, create empty ones from config + if len(suiteStatuses) == 0 { + for _, s := range cfg.Suites { + if s.IsEnabled() { + suiteStatuses = append(suiteStatuses, suite.NewStatus(s)) + } + } + } + return c.Status(fiber.StatusOK).JSON(suiteStatuses) + } +} + +// SuiteStatus handles requests to retrieve a single suite's status +func SuiteStatus(cfg *config.Config) fiber.Handler { + return func(c *fiber.Ctx) error { + page, pageSize := extractPageAndPageSizeFromRequest(c, 100) + key := c.Params("key") + params := paging.NewSuiteStatusParams().WithPagination(page, pageSize) + status, err := store.Get().GetSuiteStatusByKey(key, params) + if err != nil || status == nil { + // Try to find the suite in config + for _, s := range cfg.Suites { + if s.Key() == key { + status = suite.NewStatus(s) + break + } + } + if status == nil { + return c.Status(404).JSON(fiber.Map{ + "error": fmt.Sprintf("Suite with key '%s' not found", key), + }) + } + } + return c.Status(fiber.StatusOK).JSON(status) + } +} diff --git a/api/suite_status_test.go b/api/suite_status_test.go new file mode 100644 index 00000000..162b6b6e --- /dev/null +++ b/api/suite_status_test.go @@ -0,0 +1,519 @@ +package api + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/TwiN/gatus/v5/config" + "github.com/TwiN/gatus/v5/config/endpoint" + "github.com/TwiN/gatus/v5/config/suite" + "github.com/TwiN/gatus/v5/storage" + "github.com/TwiN/gatus/v5/storage/store" + "github.com/TwiN/gatus/v5/watchdog" +) + +var ( + suiteTimestamp = time.Now() + + testSuiteEndpoint1 = endpoint.Endpoint{ + Name: "endpoint1", + Group: "suite-group", + URL: "https://example.org/endpoint1", + Method: "GET", + Interval: 30 * time.Second, + Conditions: []endpoint.Condition{endpoint.Condition("[STATUS] == 200"), endpoint.Condition("[RESPONSE_TIME] < 500")}, + NumberOfFailuresInARow: 0, + NumberOfSuccessesInARow: 0, + } + testSuiteEndpoint2 = endpoint.Endpoint{ + Name: "endpoint2", + Group: "suite-group", + URL: "https://example.org/endpoint2", + Method: "GET", + Interval: 30 * time.Second, + Conditions: []endpoint.Condition{endpoint.Condition("[STATUS] == 200"), endpoint.Condition("[RESPONSE_TIME] < 300")}, + NumberOfFailuresInARow: 0, + NumberOfSuccessesInARow: 0, + } + testSuite = suite.Suite{ + Name: "test-suite", + Group: "suite-group", + Interval: 60 * time.Second, + Endpoints: []*endpoint.Endpoint{ + &testSuiteEndpoint1, + &testSuiteEndpoint2, + }, + } + testSuccessfulSuiteResult = suite.Result{ + Name: "test-suite", + Group: "suite-group", + Success: true, + Timestamp: suiteTimestamp, + Duration: 250 * time.Millisecond, + EndpointResults: []*endpoint.Result{ + { + Hostname: "example.org", + IP: "127.0.0.1", + HTTPStatus: 200, + Success: true, + Timestamp: suiteTimestamp, + Duration: 100 * time.Millisecond, + ConditionResults: []*endpoint.ConditionResult{ + { + Condition: "[STATUS] == 200", + Success: true, + }, + { + Condition: "[RESPONSE_TIME] < 500", + Success: true, + }, + }, + }, + { + Hostname: "example.org", + IP: "127.0.0.1", + HTTPStatus: 200, + Success: true, + Timestamp: suiteTimestamp, + Duration: 150 * time.Millisecond, + ConditionResults: []*endpoint.ConditionResult{ + { + Condition: "[STATUS] == 200", + Success: true, + }, + { + Condition: "[RESPONSE_TIME] < 300", + Success: true, + }, + }, + }, + }, + } + testUnsuccessfulSuiteResult = suite.Result{ + Name: "test-suite", + Group: "suite-group", + Success: false, + Timestamp: suiteTimestamp, + Duration: 850 * time.Millisecond, + Errors: []string{"suite-error-1", "suite-error-2"}, + EndpointResults: []*endpoint.Result{ + { + Hostname: "example.org", + IP: "127.0.0.1", + HTTPStatus: 200, + Success: true, + Timestamp: suiteTimestamp, + Duration: 100 * time.Millisecond, + ConditionResults: []*endpoint.ConditionResult{ + { + Condition: "[STATUS] == 200", + Success: true, + }, + { + Condition: "[RESPONSE_TIME] < 500", + Success: true, + }, + }, + }, + { + Hostname: "example.org", + IP: "127.0.0.1", + HTTPStatus: 500, + Errors: []string{"endpoint-error-1"}, + Success: false, + Timestamp: suiteTimestamp, + Duration: 750 * time.Millisecond, + ConditionResults: []*endpoint.ConditionResult{ + { + Condition: "[STATUS] == 200", + Success: false, + }, + { + Condition: "[RESPONSE_TIME] < 300", + Success: false, + }, + }, + }, + }, + } +) + +func TestSuiteStatus(t *testing.T) { + defer store.Get().Clear() + defer cache.Clear() + cfg := &config.Config{ + Metrics: true, + Suites: []*suite.Suite{ + { + Name: "frontend-suite", + Group: "core", + }, + { + Name: "backend-suite", + Group: "core", + }, + }, + Storage: &storage.Config{ + MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults, + MaximumNumberOfEvents: storage.DefaultMaximumNumberOfEvents, + }, + } + watchdog.UpdateSuiteStatus(cfg.Suites[0], &suite.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now(), Name: cfg.Suites[0].Name, Group: cfg.Suites[0].Group}) + watchdog.UpdateSuiteStatus(cfg.Suites[1], &suite.Result{Success: false, Duration: time.Second, Timestamp: time.Now(), Name: cfg.Suites[1].Name, Group: cfg.Suites[1].Group}) + api := New(cfg) + router := api.Router() + type Scenario struct { + Name string + Path string + ExpectedCode int + Gzip bool + } + scenarios := []Scenario{ + { + Name: "suite-status", + Path: "/api/v1/suites/core_frontend-suite/statuses", + ExpectedCode: http.StatusOK, + }, + { + Name: "suite-status-gzip", + Path: "/api/v1/suites/core_frontend-suite/statuses", + ExpectedCode: http.StatusOK, + Gzip: true, + }, + { + Name: "suite-status-pagination", + Path: "/api/v1/suites/core_frontend-suite/statuses?page=1&pageSize=20", + ExpectedCode: http.StatusOK, + }, + { + Name: "suite-status-for-invalid-key", + Path: "/api/v1/suites/invalid_key/statuses", + ExpectedCode: http.StatusNotFound, + }, + } + for _, scenario := range scenarios { + t.Run(scenario.Name, func(t *testing.T) { + request := httptest.NewRequest("GET", scenario.Path, http.NoBody) + if scenario.Gzip { + request.Header.Set("Accept-Encoding", "gzip") + } + response, err := router.Test(request) + if err != nil { + return + } + if response.StatusCode != scenario.ExpectedCode { + t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, response.StatusCode) + } + }) + } +} + +func TestSuiteStatus_SuiteNotInStoreButInConfig(t *testing.T) { + defer store.Get().Clear() + defer cache.Clear() + tests := []struct { + name string + suiteKey string + cfg *config.Config + expectedCode int + expectJSON bool + expectError string + }{ + { + name: "suite-not-in-store-but-exists-in-config-enabled", + suiteKey: "test-group_test-suite", + cfg: &config.Config{ + Metrics: true, + Suites: []*suite.Suite{ + { + Name: "test-suite", + Group: "test-group", + Enabled: boolPtr(true), + Endpoints: []*endpoint.Endpoint{ + { + Name: "endpoint-1", + Group: "test-group", + URL: "https://example.com", + }, + }, + }, + }, + Storage: &storage.Config{ + MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults, + MaximumNumberOfEvents: storage.DefaultMaximumNumberOfEvents, + }, + }, + expectedCode: http.StatusOK, + expectJSON: true, + }, + { + name: "suite-not-in-store-but-exists-in-config-disabled", + suiteKey: "test-group_disabled-suite", + cfg: &config.Config{ + Metrics: true, + Suites: []*suite.Suite{ + { + Name: "disabled-suite", + Group: "test-group", + Enabled: boolPtr(false), + }, + }, + Storage: &storage.Config{ + MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults, + MaximumNumberOfEvents: storage.DefaultMaximumNumberOfEvents, + }, + }, + expectedCode: http.StatusOK, + expectJSON: true, + }, + { + name: "suite-not-in-store-and-not-in-config", + suiteKey: "nonexistent_suite", + cfg: &config.Config{ + Metrics: true, + Suites: []*suite.Suite{ + { + Name: "different-suite", + Group: "different-group", + }, + }, + Storage: &storage.Config{ + MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults, + MaximumNumberOfEvents: storage.DefaultMaximumNumberOfEvents, + }, + }, + expectedCode: http.StatusNotFound, + expectError: "Suite with key 'nonexistent_suite' not found", + }, + { + name: "suite-with-empty-group-in-config", + suiteKey: "_empty-group-suite", + cfg: &config.Config{ + Metrics: true, + Suites: []*suite.Suite{ + { + Name: "empty-group-suite", + Group: "", + }, + }, + Storage: &storage.Config{ + MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults, + MaximumNumberOfEvents: storage.DefaultMaximumNumberOfEvents, + }, + }, + expectedCode: http.StatusOK, + expectJSON: true, + }, + { + name: "suite-nil-enabled-defaults-to-true", + suiteKey: "default_enabled-suite", + cfg: &config.Config{ + Metrics: true, + Suites: []*suite.Suite{ + { + Name: "enabled-suite", + Group: "default", + Enabled: nil, + }, + }, + Storage: &storage.Config{ + MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults, + MaximumNumberOfEvents: storage.DefaultMaximumNumberOfEvents, + }, + }, + expectedCode: http.StatusOK, + expectJSON: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + api := New(tt.cfg) + router := api.Router() + request := httptest.NewRequest("GET", "/api/v1/suites/"+tt.suiteKey+"/statuses", http.NoBody) + response, err := router.Test(request) + if err != nil { + t.Fatalf("Router test failed: %v", err) + } + defer response.Body.Close() + if response.StatusCode != tt.expectedCode { + t.Errorf("Expected status code %d, got %d", tt.expectedCode, response.StatusCode) + } + body, err := io.ReadAll(response.Body) + if err != nil { + t.Fatalf("Failed to read response body: %v", err) + } + bodyStr := string(body) + if tt.expectJSON { + if response.Header.Get("Content-Type") != "application/json" { + t.Errorf("Expected JSON content type, got %s", response.Header.Get("Content-Type")) + } + if len(bodyStr) == 0 || bodyStr[0] != '{' { + t.Errorf("Expected JSON response, got: %s", bodyStr) + } + } + if tt.expectError != "" { + if !contains(bodyStr, tt.expectError) { + t.Errorf("Expected error message '%s' in response, got: %s", tt.expectError, bodyStr) + } + } + }) + } +} + +func TestSuiteStatuses(t *testing.T) { + defer store.Get().Clear() + defer cache.Clear() + firstResult := &testSuccessfulSuiteResult + secondResult := &testUnsuccessfulSuiteResult + store.Get().InsertSuiteResult(&testSuite, firstResult) + store.Get().InsertSuiteResult(&testSuite, secondResult) + // Can't be bothered dealing with timezone issues on the worker that runs the automated tests + firstResult.Timestamp = time.Time{} + secondResult.Timestamp = time.Time{} + for i := range firstResult.EndpointResults { + firstResult.EndpointResults[i].Timestamp = time.Time{} + } + for i := range secondResult.EndpointResults { + secondResult.EndpointResults[i].Timestamp = time.Time{} + } + api := New(&config.Config{ + Metrics: true, + Storage: &storage.Config{ + MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults, + MaximumNumberOfEvents: storage.DefaultMaximumNumberOfEvents, + }, + }) + router := api.Router() + type Scenario struct { + Name string + Path string + ExpectedCode int + ExpectedBody string + } + scenarios := []Scenario{ + { + Name: "no-pagination", + Path: "/api/v1/suites/statuses", + ExpectedCode: http.StatusOK, + ExpectedBody: `[{"name":"test-suite","group":"suite-group","key":"suite-group_test-suite","results":[{"name":"test-suite","group":"suite-group","success":true,"timestamp":"0001-01-01T00:00:00Z","duration":250000000,"endpointResults":[{"status":200,"hostname":"example.org","duration":100000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 300","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"}]},{"name":"test-suite","group":"suite-group","success":false,"timestamp":"0001-01-01T00:00:00Z","duration":850000000,"endpointResults":[{"status":200,"hostname":"example.org","duration":100000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":500,"hostname":"example.org","duration":750000000,"errors":["endpoint-error-1"],"conditionResults":[{"condition":"[STATUS] == 200","success":false},{"condition":"[RESPONSE_TIME] \u003c 300","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"errors":["suite-error-1","suite-error-2"]}]}]`, + }, + { + Name: "pagination-first-result", + Path: "/api/v1/suites/statuses?page=1&pageSize=1", + ExpectedCode: http.StatusOK, + ExpectedBody: `[{"name":"test-suite","group":"suite-group","key":"suite-group_test-suite","results":[{"name":"test-suite","group":"suite-group","success":false,"timestamp":"0001-01-01T00:00:00Z","duration":850000000,"endpointResults":[{"status":200,"hostname":"example.org","duration":100000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":500,"hostname":"example.org","duration":750000000,"errors":["endpoint-error-1"],"conditionResults":[{"condition":"[STATUS] == 200","success":false},{"condition":"[RESPONSE_TIME] \u003c 300","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"errors":["suite-error-1","suite-error-2"]}]}]`, + }, + { + Name: "pagination-second-result", + Path: "/api/v1/suites/statuses?page=2&pageSize=1", + ExpectedCode: http.StatusOK, + ExpectedBody: `[{"name":"test-suite","group":"suite-group","key":"suite-group_test-suite","results":[{"name":"test-suite","group":"suite-group","success":true,"timestamp":"0001-01-01T00:00:00Z","duration":250000000,"endpointResults":[{"status":200,"hostname":"example.org","duration":100000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 300","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"}]}]}]`, + }, + { + Name: "pagination-no-results", + Path: "/api/v1/suites/statuses?page=5&pageSize=20", + ExpectedCode: http.StatusOK, + ExpectedBody: `[{"name":"test-suite","group":"suite-group","key":"suite-group_test-suite","results":[]}]`, + }, + { + Name: "invalid-pagination-should-fall-back-to-default", + Path: "/api/v1/suites/statuses?page=INVALID&pageSize=INVALID", + ExpectedCode: http.StatusOK, + ExpectedBody: `[{"name":"test-suite","group":"suite-group","key":"suite-group_test-suite","results":[{"name":"test-suite","group":"suite-group","success":true,"timestamp":"0001-01-01T00:00:00Z","duration":250000000,"endpointResults":[{"status":200,"hostname":"example.org","duration":100000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 300","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"}]},{"name":"test-suite","group":"suite-group","success":false,"timestamp":"0001-01-01T00:00:00Z","duration":850000000,"endpointResults":[{"status":200,"hostname":"example.org","duration":100000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":500,"hostname":"example.org","duration":750000000,"errors":["endpoint-error-1"],"conditionResults":[{"condition":"[STATUS] == 200","success":false},{"condition":"[RESPONSE_TIME] \u003c 300","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"errors":["suite-error-1","suite-error-2"]}]}]`, + }, + } + for _, scenario := range scenarios { + t.Run(scenario.Name, func(t *testing.T) { + request := httptest.NewRequest("GET", scenario.Path, http.NoBody) + response, err := router.Test(request) + if err != nil { + return + } + defer response.Body.Close() + if response.StatusCode != scenario.ExpectedCode { + t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, response.StatusCode) + } + body, err := io.ReadAll(response.Body) + if err != nil { + t.Error("expected err to be nil, but was", err) + } + if string(body) != scenario.ExpectedBody { + t.Errorf("expected:\n %s\n\ngot:\n %s", scenario.ExpectedBody, string(body)) + } + }) + } +} + +func TestSuiteStatuses_NoSuitesInStoreButExistInConfig(t *testing.T) { + defer store.Get().Clear() + defer cache.Clear() + cfg := &config.Config{ + Metrics: true, + Suites: []*suite.Suite{ + { + Name: "config-only-suite-1", + Group: "test-group", + Enabled: boolPtr(true), + }, + { + Name: "config-only-suite-2", + Group: "test-group", + Enabled: boolPtr(true), + }, + { + Name: "disabled-suite", + Group: "test-group", + Enabled: boolPtr(false), + }, + }, + Storage: &storage.Config{ + MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults, + MaximumNumberOfEvents: storage.DefaultMaximumNumberOfEvents, + }, + } + api := New(cfg) + router := api.Router() + request := httptest.NewRequest("GET", "/api/v1/suites/statuses", http.NoBody) + response, err := router.Test(request) + if err != nil { + t.Fatalf("Router test failed: %v", err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode) + } + body, err := io.ReadAll(response.Body) + if err != nil { + t.Fatalf("Failed to read response body: %v", err) + } + bodyStr := string(body) + if !contains(bodyStr, "config-only-suite-1") { + t.Error("Expected config-only-suite-1 in response") + } + if !contains(bodyStr, "config-only-suite-2") { + t.Error("Expected config-only-suite-2 in response") + } + if contains(bodyStr, "disabled-suite") { + t.Error("Should not include disabled-suite in response") + } +} + +func boolPtr(b bool) *bool { + return &b +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(substr) == 0 || + (len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || + func() bool { + for i := 1; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false + }()))) +} diff --git a/config/config.go b/config/config.go index 6ba5e6e3..7256b2e0 100644 --- a/config/config.go +++ b/config/config.go @@ -17,8 +17,10 @@ import ( "github.com/TwiN/gatus/v5/config/announcement" "github.com/TwiN/gatus/v5/config/connectivity" "github.com/TwiN/gatus/v5/config/endpoint" + "github.com/TwiN/gatus/v5/config/key" "github.com/TwiN/gatus/v5/config/maintenance" "github.com/TwiN/gatus/v5/config/remote" + "github.com/TwiN/gatus/v5/config/suite" "github.com/TwiN/gatus/v5/config/ui" "github.com/TwiN/gatus/v5/config/web" "github.com/TwiN/gatus/v5/security" @@ -35,6 +37,9 @@ const ( // DefaultFallbackConfigurationFilePath is the default fallback path that will be used to search for the // configuration file if DefaultConfigurationFilePath didn't work DefaultFallbackConfigurationFilePath = "config/config.yml" + + // DefaultConcurrency is the default number of endpoints/suites that can be monitored concurrently + DefaultConcurrency = 3 ) var ( @@ -67,8 +72,14 @@ type Config struct { // DisableMonitoringLock Whether to disable the monitoring lock // The monitoring lock is what prevents multiple endpoints from being processed at the same time. // Disabling this may lead to inaccurate response times + // + // Deprecated: Use Concurrency instead TODO: REMOVE THIS IN v6.0.0 DisableMonitoringLock bool `yaml:"disable-monitoring-lock,omitempty"` + // Concurrency is the maximum number of endpoints/suites that can be monitored concurrently + // Defaults to DefaultConcurrency. Set to 0 for unlimited concurrency. + Concurrency int `yaml:"concurrency,omitempty"` + // Security is the configuration for securing access to Gatus Security *security.Config `yaml:"security,omitempty"` @@ -81,6 +92,9 @@ type Config struct { // ExternalEndpoints is the list of all external endpoints ExternalEndpoints []*endpoint.ExternalEndpoint `yaml:"external-endpoints,omitempty"` + // Suites is the list of suites to monitor + Suites []*suite.Suite `yaml:"suites,omitempty"` + // Storage is the configuration for how the data is stored Storage *storage.Config `yaml:"storage,omitempty"` @@ -309,6 +323,13 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) { if err := validateAnnouncementsConfig(config); err != nil { return nil, err } + if err := validateSuitesConfig(config); err != nil { + return nil, err + } + if err := validateUniqueKeys(config); err != nil { + return nil, err + } + validateAndSetConcurrencyDefaults(config) // Cross-config changes config.UI.MaximumNumberOfResults = config.Storage.MaximumNumberOfResults } @@ -405,7 +426,7 @@ func validateEndpointsConfig(config *Config) error { logr.Infof("[config.validateEndpointsConfig] Validated %d endpoints", len(config.Endpoints)) // Validate external endpoints for _, ee := range config.ExternalEndpoints { - logr.Debugf("[config.validateEndpointsConfig] Validating external endpoint '%s'", ee.Name) + logr.Debugf("[config.validateEndpointsConfig] Validating external endpoint '%s'", ee.Key()) if endpointKey := ee.Key(); duplicateValidationMap[endpointKey] { return fmt.Errorf("invalid external endpoint %s: name and group combination must be unique", ee.Key()) } else { @@ -419,6 +440,78 @@ func validateEndpointsConfig(config *Config) error { return nil } +func validateSuitesConfig(config *Config) error { + if config.Suites == nil || len(config.Suites) == 0 { + logr.Info("[config.validateSuitesConfig] No suites configured") + return nil + } + suiteNames := make(map[string]bool) + for _, suite := range config.Suites { + // Check for duplicate suite names + if suiteNames[suite.Name] { + return fmt.Errorf("duplicate suite name: %s", suite.Key()) + } + suiteNames[suite.Name] = true + // Validate the suite configuration + if err := suite.ValidateAndSetDefaults(); err != nil { + return fmt.Errorf("invalid suite '%s': %w", suite.Key(), err) + } + // Check that endpoints referenced in Store mappings use valid placeholders + for _, suiteEndpoint := range suite.Endpoints { + if suiteEndpoint.Store != nil { + for contextKey, placeholder := range suiteEndpoint.Store { + // Basic validation that the context key is a valid identifier + if len(contextKey) == 0 { + return fmt.Errorf("suite '%s' endpoint '%s' has empty context key in store mapping", suite.Key(), suiteEndpoint.Key()) + } + if len(placeholder) == 0 { + return fmt.Errorf("suite '%s' endpoint '%s' has empty placeholder in store mapping for key '%s'", suite.Key(), suiteEndpoint.Key(), contextKey) + } + } + } + } + } + logr.Infof("[config.validateSuitesConfig] Validated %d suite(s)", len(config.Suites)) + return nil +} + +func validateUniqueKeys(config *Config) error { + keyMap := make(map[string]string) // key -> description for error messages + // Check all endpoints + for _, ep := range config.Endpoints { + epKey := ep.Key() + if existing, exists := keyMap[epKey]; exists { + return fmt.Errorf("duplicate key '%s': endpoint '%s' conflicts with %s", epKey, ep.Key(), existing) + } + keyMap[epKey] = fmt.Sprintf("endpoint '%s'", ep.Key()) + } + // Check all external endpoints + for _, ee := range config.ExternalEndpoints { + eeKey := ee.Key() + if existing, exists := keyMap[eeKey]; exists { + return fmt.Errorf("duplicate key '%s': external endpoint '%s' conflicts with %s", eeKey, ee.Key(), existing) + } + keyMap[eeKey] = fmt.Sprintf("external endpoint '%s'", ee.Key()) + } + // Check all suites + for _, suite := range config.Suites { + suiteKey := suite.Key() + if existing, exists := keyMap[suiteKey]; exists { + return fmt.Errorf("duplicate key '%s': suite '%s' conflicts with %s", suiteKey, suite.Key(), existing) + } + keyMap[suiteKey] = fmt.Sprintf("suite '%s'", suite.Key()) + // Check endpoints within suites (they generate keys using suite group + endpoint name) + for _, ep := range suite.Endpoints { + epKey := key.ConvertGroupAndNameToKey(suite.Group, ep.Name) + if existing, exists := keyMap[epKey]; exists { + return fmt.Errorf("duplicate key '%s': endpoint '%s' in suite '%s' conflicts with %s", epKey, epKey, suite.Key(), existing) + } + keyMap[epKey] = fmt.Sprintf("endpoint '%s' in suite '%s'", epKey, suite.Key()) + } + } + return nil +} + func validateSecurityConfig(config *Config) error { if config.Security != nil { if config.Security.IsValid() { @@ -531,3 +624,17 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoi } logr.Infof("[config.validateAlertingConfig] configuredProviders=%s; ignoredProviders=%s", validProviders, invalidProviders) } + +func validateAndSetConcurrencyDefaults(config *Config) { + if config.DisableMonitoringLock { + config.Concurrency = 0 + logr.Warn("WARNING: The 'disable-monitoring-lock' configuration has been deprecated and will be removed in v6.0.0") + logr.Warn("WARNING: Please set 'concurrency: 0' instead") + logr.Debug("[config.validateAndSetConcurrencyDefaults] DisableMonitoringLock is true, setting unlimited (0) concurrency") + } else if config.Concurrency <= 0 && !config.DisableMonitoringLock { + config.Concurrency = DefaultConcurrency + logr.Debugf("[config.validateAndSetConcurrencyDefaults] Setting default concurrency to %d", config.Concurrency) + } else { + logr.Debugf("[config.validateAndSetConcurrencyDefaults] Using configured concurrency of %d", config.Concurrency) + } +} diff --git a/config/config_test.go b/config/config_test.go index dc210684..ca81161b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2105,3 +2105,382 @@ func TestConfig_GetUniqueExtraMetricLabels(t *testing.T) { }) } } + +func TestParseAndValidateConfigBytesWithDuplicateKeysAcrossEntityTypes(t *testing.T) { + scenarios := []struct { + name string + shouldError bool + expectedErr string + config string + }{ + { + name: "endpoint-suite-same-key", + shouldError: true, + expectedErr: "duplicate key 'backend_test-api': suite 'backend_test-api' conflicts with endpoint 'backend_test-api'", + config: ` +endpoints: + - name: test-api + group: backend + url: https://example.com/api + conditions: + - "[STATUS] == 200" + +suites: + - name: test-api + group: backend + interval: 30s + endpoints: + - name: step1 + url: https://example.com/test + conditions: + - "[STATUS] == 200"`, + }, + { + name: "endpoint-suite-different-keys", + shouldError: false, + config: ` +endpoints: + - name: api-service + group: backend + url: https://example.com/api + conditions: + - "[STATUS] == 200" + +suites: + - name: integration-tests + group: testing + interval: 30s + endpoints: + - name: step1 + url: https://example.com/test + conditions: + - "[STATUS] == 200"`, + }, + { + name: "endpoint-external-endpoint-suite-unique-keys", + shouldError: false, + config: ` +endpoints: + - name: api-service + group: backend + url: https://example.com/api + conditions: + - "[STATUS] == 200" + +external-endpoints: + - name: monitoring-agent + group: infrastructure + token: "secret-token" + heartbeat: + interval: 5m + +suites: + - name: integration-tests + group: testing + interval: 30s + endpoints: + - name: step1 + url: https://example.com/test + conditions: + - "[STATUS] == 200"`, + }, + { + name: "suite-with-same-key-as-external-endpoint", + shouldError: true, + expectedErr: "duplicate key 'monitoring_health-check': suite 'monitoring_health-check' conflicts with external endpoint 'monitoring_health-check'", + config: ` +endpoints: + - name: dummy + url: https://example.com/dummy + conditions: + - "[STATUS] == 200" + +external-endpoints: + - name: health-check + group: monitoring + token: "secret-token" + heartbeat: + interval: 5m + +suites: + - name: health-check + group: monitoring + interval: 30s + endpoints: + - name: step1 + url: https://example.com/test + conditions: + - "[STATUS] == 200"`, + }, + { + name: "endpoint-with-same-name-as-suite-endpoint-different-groups", + shouldError: false, + config: ` +endpoints: + - name: api-health + group: backend + url: https://example.com/health + conditions: + - "[STATUS] == 200" + +suites: + - name: integration-suite + group: testing + interval: 30s + endpoints: + - name: api-health + url: https://example.com/api/health + conditions: + - "[STATUS] == 200"`, + }, + { + name: "endpoint-conflicting-with-suite-endpoint", + shouldError: true, + expectedErr: "duplicate key 'backend_api-health': endpoint 'backend_api-health' in suite 'backend_integration-suite' conflicts with endpoint 'backend_api-health'", + config: ` +endpoints: + - name: api-health + group: backend + url: https://example.com/health + conditions: + - "[STATUS] == 200" + +suites: + - name: integration-suite + group: backend + interval: 30s + endpoints: + - name: api-health + url: https://example.com/api/health + conditions: + - "[STATUS] == 200"`, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + _, err := parseAndValidateConfigBytes([]byte(scenario.config)) + if scenario.shouldError { + if err == nil { + t.Error("should've returned an error") + } else if scenario.expectedErr != "" && err.Error() != scenario.expectedErr { + t.Errorf("expected error message '%s', got '%s'", scenario.expectedErr, err.Error()) + } + } else if err != nil { + t.Errorf("shouldn't have returned an error, got: %v", err) + } + }) + } +} + +func TestParseAndValidateConfigBytesWithSuites(t *testing.T) { + scenarios := []struct { + name string + shouldError bool + expectedErr string + config string + }{ + { + name: "suite-with-no-name", + shouldError: true, + expectedErr: "invalid suite 'testing_': suite must have a name", + config: ` +endpoints: + - name: dummy + url: https://example.com/dummy + conditions: + - "[STATUS] == 200" + +suites: + - group: testing + interval: 30s + endpoints: + - name: step1 + url: https://example.com/test + conditions: + - "[STATUS] == 200"`, + }, + { + name: "suite-with-no-endpoints", + shouldError: true, + expectedErr: "invalid suite 'testing_empty-suite': suite must have at least one endpoint", + config: ` +endpoints: + - name: dummy + url: https://example.com/dummy + conditions: + - "[STATUS] == 200" + +suites: + - name: empty-suite + group: testing + interval: 30s + endpoints: []`, + }, + { + name: "suite-with-duplicate-endpoint-names", + shouldError: true, + expectedErr: "invalid suite 'testing_duplicate-test': suite cannot have duplicate endpoint names: duplicate endpoint name 'step1'", + config: ` +endpoints: + - name: dummy + url: https://example.com/dummy + conditions: + - "[STATUS] == 200" + +suites: + - name: duplicate-test + group: testing + interval: 30s + endpoints: + - name: step1 + url: https://example.com/test1 + conditions: + - "[STATUS] == 200" + - name: step1 + url: https://example.com/test2 + conditions: + - "[STATUS] == 200"`, + }, + { + name: "suite-with-invalid-negative-timeout", + shouldError: true, + expectedErr: "invalid suite 'testing_negative-timeout-suite': suite timeout must be positive", + config: ` +endpoints: + - name: dummy + url: https://example.com/dummy + conditions: + - "[STATUS] == 200" + +suites: + - name: negative-timeout-suite + group: testing + interval: 30s + timeout: -5m + endpoints: + - name: step1 + url: https://example.com/test + conditions: + - "[STATUS] == 200"`, + }, + { + name: "valid-suite-with-defaults", + shouldError: false, + config: ` +endpoints: + - name: api-service + group: backend + url: https://example.com/api + conditions: + - "[STATUS] == 200" + +suites: + - name: integration-test + group: testing + endpoints: + - name: step1 + url: https://example.com/test + conditions: + - "[STATUS] == 200" + - name: step2 + url: https://example.com/validate + conditions: + - "[STATUS] == 200"`, + }, + { + name: "valid-suite-with-all-fields", + shouldError: false, + config: ` +endpoints: + - name: api-service + group: backend + url: https://example.com/api + conditions: + - "[STATUS] == 200" + +suites: + - name: full-integration-test + group: testing + enabled: true + interval: 15m + timeout: 10m + context: + base_url: "https://example.com" + user_id: 12345 + endpoints: + - name: authentication + url: https://example.com/auth + conditions: + - "[STATUS] == 200" + - name: user-profile + url: https://example.com/profile + conditions: + - "[STATUS] == 200" + - "[BODY].user_id == 12345"`, + }, + { + name: "valid-suite-with-endpoint-inheritance", + shouldError: false, + config: ` +endpoints: + - name: api-service + group: backend + url: https://example.com/api + conditions: + - "[STATUS] == 200" + +suites: + - name: inheritance-test + group: parent-group + endpoints: + - name: child-endpoint + url: https://example.com/test + conditions: + - "[STATUS] == 200"`, + }, + { + name: "valid-suite-with-store-functionality", + shouldError: false, + config: ` +endpoints: + - name: api-service + group: backend + url: https://example.com/api + conditions: + - "[STATUS] == 200" + +suites: + - name: store-test + group: testing + endpoints: + - name: get-token + url: https://example.com/auth + conditions: + - "[STATUS] == 200" + store: + auth_token: "[BODY].token" + - name: use-token + url: https://example.com/data + headers: + Authorization: "Bearer {auth_token}" + conditions: + - "[STATUS] == 200"`, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + _, err := parseAndValidateConfigBytes([]byte(scenario.config)) + if scenario.shouldError { + if err == nil { + t.Error("should've returned an error") + } else if scenario.expectedErr != "" && err.Error() != scenario.expectedErr { + t.Errorf("expected error message '%s', got '%s'", scenario.expectedErr, err.Error()) + } + } else if err != nil { + t.Errorf("shouldn't have returned an error, got: %v", err) + } + }) + } +} diff --git a/config/endpoint/condition.go b/config/endpoint/condition.go index 678ad5e2..184b5085 100644 --- a/config/endpoint/condition.go +++ b/config/endpoint/condition.go @@ -7,82 +7,11 @@ import ( "strings" "time" - "github.com/TwiN/gatus/v5/jsonpath" + "github.com/TwiN/gatus/v5/config/gontext" "github.com/TwiN/gatus/v5/pattern" ) -// Placeholders const ( - // StatusPlaceholder is a placeholder for a HTTP status. - // - // Values that could replace the placeholder: 200, 404, 500, ... - StatusPlaceholder = "[STATUS]" - - // IPPlaceholder is a placeholder for an IP. - // - // Values that could replace the placeholder: 127.0.0.1, 10.0.0.1, ... - IPPlaceholder = "[IP]" - - // DNSRCodePlaceholder is a placeholder for DNS_RCODE - // - // Values that could replace the placeholder: NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, REFUSED - DNSRCodePlaceholder = "[DNS_RCODE]" - - // ResponseTimePlaceholder is a placeholder for the request response time, in milliseconds. - // - // Values that could replace the placeholder: 1, 500, 1000, ... - ResponseTimePlaceholder = "[RESPONSE_TIME]" - - // BodyPlaceholder is a placeholder for the Body of the response - // - // Values that could replace the placeholder: {}, {"data":{"name":"john"}}, ... - BodyPlaceholder = "[BODY]" - - // ConnectedPlaceholder is a placeholder for whether a connection was successfully established. - // - // Values that could replace the placeholder: true, false - ConnectedPlaceholder = "[CONNECTED]" - - // CertificateExpirationPlaceholder is a placeholder for the duration before certificate expiration, in milliseconds. - // - // Values that could replace the placeholder: 4461677039 (~52 days) - CertificateExpirationPlaceholder = "[CERTIFICATE_EXPIRATION]" - - // DomainExpirationPlaceholder is a placeholder for the duration before the domain expires, in milliseconds. - DomainExpirationPlaceholder = "[DOMAIN_EXPIRATION]" -) - -// Functions -const ( - // LengthFunctionPrefix is the prefix for the length function - // - // Usage: len([BODY].articles) == 10, len([BODY].name) > 5 - LengthFunctionPrefix = "len(" - - // HasFunctionPrefix is the prefix for the has function - // - // Usage: has([BODY].errors) == true - HasFunctionPrefix = "has(" - - // PatternFunctionPrefix is the prefix for the pattern function - // - // Usage: [IP] == pat(192.168.*.*) - PatternFunctionPrefix = "pat(" - - // AnyFunctionPrefix is the prefix for the any function - // - // Usage: [IP] == any(1.1.1.1, 1.0.0.1) - AnyFunctionPrefix = "any(" - - // FunctionSuffix is the suffix for all functions - FunctionSuffix = ")" -) - -// Other constants -const ( - // InvalidConditionElementSuffix is the suffix that will be appended to an invalid condition - InvalidConditionElementSuffix = "(INVALID)" - // maximumLengthBeforeTruncatingWhenComparedWithPattern is the maximum length an element being compared to a // pattern can have. // @@ -97,50 +26,50 @@ type Condition string // Validate checks if the Condition is valid func (c Condition) Validate() error { r := &Result{} - c.evaluate(r, false) + c.evaluate(r, false, nil) if len(r.Errors) != 0 { return errors.New(r.Errors[0]) } return nil } -// evaluate the Condition with the Result of the health check -func (c Condition) evaluate(result *Result, dontResolveFailedConditions bool) bool { +// evaluate the Condition with the Result and an optional context +func (c Condition) evaluate(result *Result, dontResolveFailedConditions bool, context *gontext.Gontext) bool { condition := string(c) success := false conditionToDisplay := condition if strings.Contains(condition, " == ") { - parameters, resolvedParameters := sanitizeAndResolve(strings.Split(condition, " == "), result) + parameters, resolvedParameters := sanitizeAndResolveWithContext(strings.Split(condition, " == "), result, context) success = isEqual(resolvedParameters[0], resolvedParameters[1]) if !success && !dontResolveFailedConditions { conditionToDisplay = prettify(parameters, resolvedParameters, "==") } } else if strings.Contains(condition, " != ") { - parameters, resolvedParameters := sanitizeAndResolve(strings.Split(condition, " != "), result) + parameters, resolvedParameters := sanitizeAndResolveWithContext(strings.Split(condition, " != "), result, context) success = !isEqual(resolvedParameters[0], resolvedParameters[1]) if !success && !dontResolveFailedConditions { conditionToDisplay = prettify(parameters, resolvedParameters, "!=") } } else if strings.Contains(condition, " <= ") { - parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, " <= "), result) + parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " <= "), result, context) success = resolvedParameters[0] <= resolvedParameters[1] if !success && !dontResolveFailedConditions { conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<=") } } else if strings.Contains(condition, " >= ") { - parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, " >= "), result) + parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " >= "), result, context) success = resolvedParameters[0] >= resolvedParameters[1] if !success && !dontResolveFailedConditions { conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">=") } } else if strings.Contains(condition, " > ") { - parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, " > "), result) + parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " > "), result, context) success = resolvedParameters[0] > resolvedParameters[1] if !success && !dontResolveFailedConditions { conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">") } } else if strings.Contains(condition, " < ") { - parameters, resolvedParameters := sanitizeAndResolveNumerical(strings.Split(condition, " < "), result) + parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " < "), result, context) success = resolvedParameters[0] < resolvedParameters[1] if !success && !dontResolveFailedConditions { conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<") @@ -235,79 +164,29 @@ func isEqual(first, second string) bool { return first == second } -// sanitizeAndResolve sanitizes and resolves a list of elements and returns the list of parameters as well as a list -// of resolved parameters -func sanitizeAndResolve(elements []string, result *Result) ([]string, []string) { +// sanitizeAndResolveWithContext sanitizes and resolves a list of elements with an optional context +func sanitizeAndResolveWithContext(elements []string, result *Result, context *gontext.Gontext) ([]string, []string) { parameters := make([]string, len(elements)) resolvedParameters := make([]string, len(elements)) - body := strings.TrimSpace(string(result.Body)) for i, element := range elements { element = strings.TrimSpace(element) parameters[i] = element - switch strings.ToUpper(element) { - case StatusPlaceholder: - element = strconv.Itoa(result.HTTPStatus) - case IPPlaceholder: - element = result.IP - case ResponseTimePlaceholder: - element = strconv.Itoa(int(result.Duration.Milliseconds())) - case BodyPlaceholder: - element = body - case DNSRCodePlaceholder: - element = result.DNSRCode - case ConnectedPlaceholder: - element = strconv.FormatBool(result.Connected) - case CertificateExpirationPlaceholder: - element = strconv.FormatInt(result.CertificateExpiration.Milliseconds(), 10) - case DomainExpirationPlaceholder: - element = strconv.FormatInt(result.DomainExpiration.Milliseconds(), 10) - default: - // if contains the BodyPlaceholder, then evaluate json path - if strings.Contains(element, BodyPlaceholder) { - checkingForLength := false - checkingForExistence := false - if strings.HasPrefix(element, LengthFunctionPrefix) && strings.HasSuffix(element, FunctionSuffix) { - checkingForLength = true - element = strings.TrimSuffix(strings.TrimPrefix(element, LengthFunctionPrefix), FunctionSuffix) - } - if strings.HasPrefix(element, HasFunctionPrefix) && strings.HasSuffix(element, FunctionSuffix) { - checkingForExistence = true - element = strings.TrimSuffix(strings.TrimPrefix(element, HasFunctionPrefix), FunctionSuffix) - } - resolvedElement, resolvedElementLength, err := jsonpath.Eval(strings.TrimPrefix(strings.TrimPrefix(element, BodyPlaceholder), "."), result.Body) - if checkingForExistence { - if err != nil { - element = "false" - } else { - element = "true" - } - } else { - if err != nil { - if err.Error() != "unexpected end of JSON input" { - result.AddError(err.Error()) - } - if checkingForLength { - element = LengthFunctionPrefix + element + FunctionSuffix + " " + InvalidConditionElementSuffix - } else { - element = element + " " + InvalidConditionElementSuffix - } - } else { - if checkingForLength { - element = strconv.Itoa(resolvedElementLength) - } else { - element = resolvedElement - } - } - } - } + + // Use the unified ResolvePlaceholder function + resolved, err := ResolvePlaceholder(element, result, context) + if err != nil { + // If there's an error, add it to the result + result.AddError(err.Error()) + resolvedParameters[i] = element + " " + InvalidConditionElementSuffix + } else { + resolvedParameters[i] = resolved } - resolvedParameters[i] = element } return parameters, resolvedParameters } -func sanitizeAndResolveNumerical(list []string, result *Result) (parameters []string, resolvedNumericalParameters []int64) { - parameters, resolvedParameters := sanitizeAndResolve(list, result) +func sanitizeAndResolveNumericalWithContext(list []string, result *Result, context *gontext.Gontext) (parameters []string, resolvedNumericalParameters []int64) { + parameters, resolvedParameters := sanitizeAndResolveWithContext(list, result, context) for _, element := range resolvedParameters { if duration, err := time.ParseDuration(element); duration != 0 && err == nil { // If the string is a duration, convert it to milliseconds diff --git a/config/endpoint/condition_bench_test.go b/config/endpoint/condition_bench_test.go index 82061f69..db2519ba 100644 --- a/config/endpoint/condition_bench_test.go +++ b/config/endpoint/condition_bench_test.go @@ -8,7 +8,7 @@ func BenchmarkCondition_evaluateWithBodyStringAny(b *testing.B) { condition := Condition("[BODY].name == any(john.doe, jane.doe)") for n := 0; n < b.N; n++ { result := &Result{Body: []byte("{\"name\": \"john.doe\"}")} - condition.evaluate(result, false) + condition.evaluate(result, false, nil) } b.ReportAllocs() } @@ -17,7 +17,7 @@ func BenchmarkCondition_evaluateWithBodyStringAnyFailure(b *testing.B) { condition := Condition("[BODY].name == any(john.doe, jane.doe)") for n := 0; n < b.N; n++ { result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")} - condition.evaluate(result, false) + condition.evaluate(result, false, nil) } b.ReportAllocs() } @@ -26,7 +26,7 @@ func BenchmarkCondition_evaluateWithBodyString(b *testing.B) { condition := Condition("[BODY].name == john.doe") for n := 0; n < b.N; n++ { result := &Result{Body: []byte("{\"name\": \"john.doe\"}")} - condition.evaluate(result, false) + condition.evaluate(result, false, nil) } b.ReportAllocs() } @@ -35,7 +35,7 @@ func BenchmarkCondition_evaluateWithBodyStringFailure(b *testing.B) { condition := Condition("[BODY].name == john.doe") for n := 0; n < b.N; n++ { result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")} - condition.evaluate(result, false) + condition.evaluate(result, false, nil) } b.ReportAllocs() } @@ -44,7 +44,7 @@ func BenchmarkCondition_evaluateWithBodyStringFailureInvalidPath(b *testing.B) { condition := Condition("[BODY].user.name == bob.doe") for n := 0; n < b.N; n++ { result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")} - condition.evaluate(result, false) + condition.evaluate(result, false, nil) } b.ReportAllocs() } @@ -53,7 +53,7 @@ func BenchmarkCondition_evaluateWithBodyStringLen(b *testing.B) { condition := Condition("len([BODY].name) == 8") for n := 0; n < b.N; n++ { result := &Result{Body: []byte("{\"name\": \"john.doe\"}")} - condition.evaluate(result, false) + condition.evaluate(result, false, nil) } b.ReportAllocs() } @@ -62,7 +62,7 @@ func BenchmarkCondition_evaluateWithBodyStringLenFailure(b *testing.B) { condition := Condition("len([BODY].name) == 8") for n := 0; n < b.N; n++ { result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")} - condition.evaluate(result, false) + condition.evaluate(result, false, nil) } b.ReportAllocs() } @@ -71,7 +71,7 @@ func BenchmarkCondition_evaluateWithStatus(b *testing.B) { condition := Condition("[STATUS] == 200") for n := 0; n < b.N; n++ { result := &Result{HTTPStatus: 200} - condition.evaluate(result, false) + condition.evaluate(result, false, nil) } b.ReportAllocs() } @@ -80,7 +80,7 @@ func BenchmarkCondition_evaluateWithStatusFailure(b *testing.B) { condition := Condition("[STATUS] == 200") for n := 0; n < b.N; n++ { result := &Result{HTTPStatus: 400} - condition.evaluate(result, false) + condition.evaluate(result, false, nil) } b.ReportAllocs() } diff --git a/config/endpoint/condition_test.go b/config/endpoint/condition_test.go index 3e98912f..52e68139 100644 --- a/config/endpoint/condition_test.go +++ b/config/endpoint/condition_test.go @@ -755,7 +755,7 @@ func TestCondition_evaluate(t *testing.T) { } for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { - scenario.Condition.evaluate(scenario.Result, scenario.DontResolveFailedConditions) + scenario.Condition.evaluate(scenario.Result, scenario.DontResolveFailedConditions, nil) if scenario.Result.ConditionResults[0].Success != scenario.ExpectedSuccess { t.Errorf("Condition '%s' should have been success=%v", scenario.Condition, scenario.ExpectedSuccess) } @@ -769,7 +769,7 @@ func TestCondition_evaluate(t *testing.T) { func TestCondition_evaluateWithInvalidOperator(t *testing.T) { condition := Condition("[STATUS] ? 201") result := &Result{HTTPStatus: 201} - condition.evaluate(result, false) + condition.evaluate(result, false, nil) if result.Success { t.Error("condition was invalid, result should've been a failure") } diff --git a/config/endpoint/endpoint.go b/config/endpoint/endpoint.go index d425bfa1..ed582a24 100644 --- a/config/endpoint/endpoint.go +++ b/config/endpoint/endpoint.go @@ -21,6 +21,8 @@ import ( "github.com/TwiN/gatus/v5/config/endpoint/dns" sshconfig "github.com/TwiN/gatus/v5/config/endpoint/ssh" "github.com/TwiN/gatus/v5/config/endpoint/ui" + "github.com/TwiN/gatus/v5/config/gontext" + "github.com/TwiN/gatus/v5/config/key" "github.com/TwiN/gatus/v5/config/maintenance" "golang.org/x/crypto/ssh" ) @@ -134,6 +136,18 @@ type Endpoint struct { // LastReminderSent is the time at which the last reminder was sent for this endpoint. LastReminderSent time.Time `yaml:"-"` + + /////////////////////// + // SUITE-ONLY FIELDS // + /////////////////////// + + // Store is a map of values to extract from the result and store in the suite context + // This field is only used when the endpoint is part of a suite + Store map[string]string `yaml:"store,omitempty"` + + // AlwaysRun defines whether to execute this endpoint even if previous endpoints in the suite failed + // This field is only used when the endpoint is part of a suite + AlwaysRun bool `yaml:"always-run,omitempty"` } // IsEnabled returns whether the endpoint is enabled or not @@ -255,7 +269,7 @@ func (e *Endpoint) DisplayName() string { // Key returns the unique key for the Endpoint func (e *Endpoint) Key() string { - return ConvertGroupAndEndpointNameToKey(e.Group, e.Name) + return key.ConvertGroupAndNameToKey(e.Group, e.Name) } // Close HTTP connections between watchdog and endpoints to avoid dangling socket file descriptors @@ -269,16 +283,26 @@ func (e *Endpoint) Close() { // EvaluateHealth sends a request to the endpoint's URL and evaluates the conditions of the endpoint. func (e *Endpoint) EvaluateHealth() *Result { + return e.EvaluateHealthWithContext(nil) +} + +// EvaluateHealthWithContext sends a request to the endpoint's URL with context support and evaluates the conditions +func (e *Endpoint) EvaluateHealthWithContext(context *gontext.Gontext) *Result { result := &Result{Success: true, Errors: []string{}} + // Preprocess the endpoint with context if provided + processedEndpoint := e + if context != nil { + processedEndpoint = e.preprocessWithContext(result, context) + } // Parse or extract hostname from URL - if e.DNSConfig != nil { - result.Hostname = strings.TrimSuffix(e.URL, ":53") - } else if e.Type() == TypeICMP { + if processedEndpoint.DNSConfig != nil { + result.Hostname = strings.TrimSuffix(processedEndpoint.URL, ":53") + } else if processedEndpoint.Type() == TypeICMP { // To handle IPv6 addresses, we need to handle the hostname differently here. This is to avoid, for instance, // "1111:2222:3333::4444" being displayed as "1111:2222:3333:" because :4444 would be interpreted as a port. - result.Hostname = strings.TrimPrefix(e.URL, "icmp://") + result.Hostname = strings.TrimPrefix(processedEndpoint.URL, "icmp://") } else { - urlObject, err := url.Parse(e.URL) + urlObject, err := url.Parse(processedEndpoint.URL) if err != nil { result.AddError(err.Error()) } else { @@ -287,11 +311,11 @@ func (e *Endpoint) EvaluateHealth() *Result { } } // Retrieve IP if necessary - if e.needsToRetrieveIP() { - e.getIP(result) + if processedEndpoint.needsToRetrieveIP() { + processedEndpoint.getIP(result) } // Retrieve domain expiration if necessary - if e.needsToRetrieveDomainExpiration() && len(result.Hostname) > 0 { + if processedEndpoint.needsToRetrieveDomainExpiration() && len(result.Hostname) > 0 { var err error if result.DomainExpiration, err = client.GetDomainExpiration(result.Hostname); err != nil { result.AddError(err.Error()) @@ -299,42 +323,91 @@ func (e *Endpoint) EvaluateHealth() *Result { } // Call the endpoint (if there's no errors) if len(result.Errors) == 0 { - e.call(result) + processedEndpoint.call(result) } else { result.Success = false } // Evaluate the conditions - for _, condition := range e.Conditions { - success := condition.evaluate(result, e.UIConfig.DontResolveFailedConditions) + for _, condition := range processedEndpoint.Conditions { + success := condition.evaluate(result, processedEndpoint.UIConfig.DontResolveFailedConditions, context) if !success { result.Success = false } } result.Timestamp = time.Now() // Clean up parameters that we don't need to keep in the results - if e.UIConfig.HideURL { + if processedEndpoint.UIConfig.HideURL { for errIdx, errorString := range result.Errors { - result.Errors[errIdx] = strings.ReplaceAll(errorString, e.URL, "") + result.Errors[errIdx] = strings.ReplaceAll(errorString, processedEndpoint.URL, "") } } - if e.UIConfig.HideHostname { + if processedEndpoint.UIConfig.HideHostname { for errIdx, errorString := range result.Errors { result.Errors[errIdx] = strings.ReplaceAll(errorString, result.Hostname, "") } result.Hostname = "" // remove it from the result so it doesn't get exposed } - if e.UIConfig.HidePort && len(result.port) > 0 { + if processedEndpoint.UIConfig.HidePort && len(result.port) > 0 { for errIdx, errorString := range result.Errors { result.Errors[errIdx] = strings.ReplaceAll(errorString, result.port, "") } result.port = "" } - if e.UIConfig.HideConditions { + if processedEndpoint.UIConfig.HideConditions { result.ConditionResults = nil } return result } +// preprocessWithContext creates a copy of the endpoint with context placeholders replaced +func (e *Endpoint) preprocessWithContext(result *Result, context *gontext.Gontext) *Endpoint { + // Create a deep copy of the endpoint + processed := &Endpoint{} + *processed = *e + var err error + // Replace context placeholders in URL + if processed.URL, err = replaceContextPlaceholders(e.URL, context); err != nil { + result.AddError(err.Error()) + } + // Replace context placeholders in Body + if processed.Body, err = replaceContextPlaceholders(e.Body, context); err != nil { + result.AddError(err.Error()) + } + // Replace context placeholders in Headers + if e.Headers != nil { + processed.Headers = make(map[string]string) + for k, v := range e.Headers { + if processed.Headers[k], err = replaceContextPlaceholders(v, context); err != nil { + result.AddError(err.Error()) + } + } + } + return processed +} + +// replaceContextPlaceholders replaces [CONTEXT].path placeholders with actual values +func replaceContextPlaceholders(input string, ctx *gontext.Gontext) (string, error) { + if ctx == nil { + return input, nil + } + var contextErrors []string + contextRegex := regexp.MustCompile(`\[CONTEXT\]\.[\w\.]+`) + result := contextRegex.ReplaceAllStringFunc(input, func(match string) string { + // Extract the path after [CONTEXT]. + path := strings.TrimPrefix(match, "[CONTEXT].") + value, err := ctx.Get(path) + if err != nil { + contextErrors = append(contextErrors, fmt.Sprintf("path '%s' not found", path)) + return match // Keep placeholder for error reporting + } + return fmt.Sprintf("%v", value) + }) + if len(contextErrors) > 0 { + return result, fmt.Errorf("context placeholder resolution failed: %s", strings.Join(contextErrors, ", ")) + } + return result, nil +} + func (e *Endpoint) getParsedBody() string { body := e.Body body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", e.Name) diff --git a/config/endpoint/endpoint_test.go b/config/endpoint/endpoint_test.go index 4d65ff90..8b5ca082 100644 --- a/config/endpoint/endpoint_test.go +++ b/config/endpoint/endpoint_test.go @@ -16,6 +16,7 @@ import ( "github.com/TwiN/gatus/v5/config/endpoint/dns" "github.com/TwiN/gatus/v5/config/endpoint/ssh" "github.com/TwiN/gatus/v5/config/endpoint/ui" + "github.com/TwiN/gatus/v5/config/gontext" "github.com/TwiN/gatus/v5/config/maintenance" "github.com/TwiN/gatus/v5/test" ) @@ -932,3 +933,352 @@ func TestEndpoint_needsToRetrieveIP(t *testing.T) { t.Error("expected true, got false") } } + +func TestEndpoint_preprocessWithContext(t *testing.T) { + // Import the gontext package for creating test contexts + // This test thoroughly exercises the replaceContextPlaceholders function + tests := []struct { + name string + endpoint *Endpoint + context map[string]interface{} + expectedURL string + expectedBody string + expectedHeaders map[string]string + expectedErrorCount int + expectedErrorContains []string + }{ + { + name: "successful_url_replacement", + endpoint: &Endpoint{ + URL: "https://api.example.com/users/[CONTEXT].userId", + Body: "", + }, + context: map[string]interface{}{ + "userId": "12345", + }, + expectedURL: "https://api.example.com/users/12345", + expectedBody: "", + expectedErrorCount: 0, + }, + { + name: "successful_body_replacement", + endpoint: &Endpoint{ + URL: "https://api.example.com", + Body: `{"userId": "[CONTEXT].userId", "action": "update"}`, + }, + context: map[string]interface{}{ + "userId": "67890", + }, + expectedURL: "https://api.example.com", + expectedBody: `{"userId": "67890", "action": "update"}`, + expectedErrorCount: 0, + }, + { + name: "successful_header_replacement", + endpoint: &Endpoint{ + URL: "https://api.example.com", + Body: "", + Headers: map[string]string{ + "Authorization": "Bearer [CONTEXT].token", + "X-User-ID": "[CONTEXT].userId", + }, + }, + context: map[string]interface{}{ + "token": "abc123token", + "userId": "user123", + }, + expectedURL: "https://api.example.com", + expectedBody: "", + expectedHeaders: map[string]string{ + "Authorization": "Bearer abc123token", + "X-User-ID": "user123", + }, + expectedErrorCount: 0, + }, + { + name: "multiple_placeholders_in_url", + endpoint: &Endpoint{ + URL: "https://[CONTEXT].host/api/v[CONTEXT].version/users/[CONTEXT].userId", + Body: "", + }, + context: map[string]interface{}{ + "host": "api.example.com", + "version": "2", + "userId": "12345", + }, + expectedURL: "https://api.example.com/api/v2/users/12345", + expectedBody: "", + expectedErrorCount: 0, + }, + { + name: "nested_context_path", + endpoint: &Endpoint{ + URL: "https://api.example.com/users/[CONTEXT].user.id", + Body: `{"name": "[CONTEXT].user.name"}`, + }, + context: map[string]interface{}{ + "user": map[string]interface{}{ + "id": "nested123", + "name": "John Doe", + }, + }, + expectedURL: "https://api.example.com/users/nested123", + expectedBody: `{"name": "John Doe"}`, + expectedErrorCount: 0, + }, + { + name: "url_context_not_found", + endpoint: &Endpoint{ + URL: "https://api.example.com/users/[CONTEXT].missingUserId", + Body: "", + }, + context: map[string]interface{}{ + "userId": "12345", // different key + }, + expectedURL: "https://api.example.com/users/[CONTEXT].missingUserId", + expectedBody: "", + expectedErrorCount: 1, + expectedErrorContains: []string{"path 'missingUserId' not found"}, + }, + { + name: "body_context_not_found", + endpoint: &Endpoint{ + URL: "https://api.example.com", + Body: `{"userId": "[CONTEXT].missingUserId"}`, + }, + context: map[string]interface{}{ + "userId": "12345", // different key + }, + expectedURL: "https://api.example.com", + expectedBody: `{"userId": "[CONTEXT].missingUserId"}`, + expectedErrorCount: 1, + expectedErrorContains: []string{"path 'missingUserId' not found"}, + }, + { + name: "header_context_not_found", + endpoint: &Endpoint{ + URL: "https://api.example.com", + Body: "", + Headers: map[string]string{ + "Authorization": "Bearer [CONTEXT].missingToken", + }, + }, + context: map[string]interface{}{ + "token": "validtoken", // different key + }, + expectedURL: "https://api.example.com", + expectedBody: "", + expectedHeaders: map[string]string{ + "Authorization": "Bearer [CONTEXT].missingToken", + }, + expectedErrorCount: 1, + expectedErrorContains: []string{"path 'missingToken' not found"}, + }, + { + name: "multiple_missing_context_paths", + endpoint: &Endpoint{ + URL: "https://[CONTEXT].missingHost/users/[CONTEXT].missingUserId", + Body: `{"token": "[CONTEXT].missingToken"}`, + }, + context: map[string]interface{}{ + "validKey": "validValue", + }, + expectedURL: "https://[CONTEXT].missingHost/users/[CONTEXT].missingUserId", + expectedBody: `{"token": "[CONTEXT].missingToken"}`, + expectedErrorCount: 2, // 1 for URL (both placeholders), 1 for Body + expectedErrorContains: []string{ + "path 'missingHost' not found", + "path 'missingUserId' not found", + "path 'missingToken' not found", + }, + }, + { + name: "mixed_valid_and_invalid_placeholders", + endpoint: &Endpoint{ + URL: "https://api.example.com/users/[CONTEXT].userId/posts/[CONTEXT].missingPostId", + Body: `{"userId": "[CONTEXT].userId", "action": "[CONTEXT].missingAction"}`, + }, + context: map[string]interface{}{ + "userId": "12345", + }, + expectedURL: "https://api.example.com/users/12345/posts/[CONTEXT].missingPostId", + expectedBody: `{"userId": "12345", "action": "[CONTEXT].missingAction"}`, + expectedErrorCount: 2, + expectedErrorContains: []string{ + "path 'missingPostId' not found", + "path 'missingAction' not found", + }, + }, + { + name: "nil_context", + endpoint: &Endpoint{ + URL: "https://api.example.com/users/[CONTEXT].userId", + Body: "", + }, + context: nil, + expectedURL: "https://api.example.com/users/[CONTEXT].userId", + expectedBody: "", + expectedErrorCount: 0, + }, + { + name: "empty_context", + endpoint: &Endpoint{ + URL: "https://api.example.com/users/[CONTEXT].userId", + Body: "", + }, + context: map[string]interface{}{}, + expectedURL: "https://api.example.com/users/[CONTEXT].userId", + expectedBody: "", + expectedErrorCount: 1, + expectedErrorContains: []string{"path 'userId' not found"}, + }, + { + name: "special_characters_in_context_values", + endpoint: &Endpoint{ + URL: "https://api.example.com/search?q=[CONTEXT].query", + Body: "", + }, + context: map[string]interface{}{ + "query": "hello world & special chars!", + }, + expectedURL: "https://api.example.com/search?q=hello world & special chars!", + expectedBody: "", + expectedErrorCount: 0, + }, + { + name: "numeric_context_values", + endpoint: &Endpoint{ + URL: "https://api.example.com/users/[CONTEXT].userId/limit/[CONTEXT].limit", + Body: "", + }, + context: map[string]interface{}{ + "userId": 12345, + "limit": 100, + }, + expectedURL: "https://api.example.com/users/12345/limit/100", + expectedBody: "", + expectedErrorCount: 0, + }, + { + name: "boolean_context_values", + endpoint: &Endpoint{ + URL: "https://api.example.com", + Body: `{"enabled": [CONTEXT].enabled, "active": [CONTEXT].active}`, + }, + context: map[string]interface{}{ + "enabled": true, + "active": false, + }, + expectedURL: "https://api.example.com", + expectedBody: `{"enabled": true, "active": false}`, + expectedErrorCount: 0, + }, + { + name: "no_context_placeholders", + endpoint: &Endpoint{ + URL: "https://api.example.com/health", + Body: `{"status": "check"}`, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + }, + context: map[string]interface{}{ + "userId": "12345", + }, + expectedURL: "https://api.example.com/health", + expectedBody: `{"status": "check"}`, + expectedHeaders: map[string]string{ + "Content-Type": "application/json", + }, + expectedErrorCount: 0, + }, + { + name: "deeply_nested_context_path", + endpoint: &Endpoint{ + URL: "https://api.example.com/users/[CONTEXT].response.data.user.id", + Body: "", + }, + context: map[string]interface{}{ + "response": map[string]interface{}{ + "data": map[string]interface{}{ + "user": map[string]interface{}{ + "id": "deep123", + }, + }, + }, + }, + expectedURL: "https://api.example.com/users/deep123", + expectedBody: "", + expectedErrorCount: 0, + }, + { + name: "invalid_nested_context_path", + endpoint: &Endpoint{ + URL: "https://api.example.com/users/[CONTEXT].response.missing.path", + Body: "", + }, + context: map[string]interface{}{ + "response": map[string]interface{}{ + "data": "value", + }, + }, + expectedURL: "https://api.example.com/users/[CONTEXT].response.missing.path", + expectedBody: "", + expectedErrorCount: 1, + expectedErrorContains: []string{"path 'response.missing.path' not found"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Import gontext package for creating context + var ctx *gontext.Gontext + if tt.context != nil { + ctx = gontext.New(tt.context) + } + // Create a new Result to capture errors + result := &Result{} + // Call preprocessWithContext + processed := tt.endpoint.preprocessWithContext(result, ctx) + // Verify URL + if processed.URL != tt.expectedURL { + t.Errorf("URL mismatch:\nexpected: %s\nactual: %s", tt.expectedURL, processed.URL) + } + // Verify Body + if processed.Body != tt.expectedBody { + t.Errorf("Body mismatch:\nexpected: %s\nactual: %s", tt.expectedBody, processed.Body) + } + // Verify Headers + if tt.expectedHeaders != nil { + if processed.Headers == nil { + t.Error("Expected headers but got nil") + } else { + for key, expectedValue := range tt.expectedHeaders { + if actualValue, exists := processed.Headers[key]; !exists { + t.Errorf("Expected header %s not found", key) + } else if actualValue != expectedValue { + t.Errorf("Header %s mismatch:\nexpected: %s\nactual: %s", key, expectedValue, actualValue) + } + } + } + } + // Verify error count + if len(result.Errors) != tt.expectedErrorCount { + t.Errorf("Error count mismatch:\nexpected: %d\nactual: %d\nerrors: %v", tt.expectedErrorCount, len(result.Errors), result.Errors) + } + // Verify error messages contain expected strings + if tt.expectedErrorContains != nil { + actualErrors := strings.Join(result.Errors, " ") + for _, expectedError := range tt.expectedErrorContains { + if !strings.Contains(actualErrors, expectedError) { + t.Errorf("Expected error containing '%s' not found in: %v", expectedError, result.Errors) + } + } + } + // Verify original endpoint is not modified + if tt.endpoint.URL != ((&Endpoint{URL: tt.endpoint.URL, Body: tt.endpoint.Body, Headers: tt.endpoint.Headers}).URL) { + t.Error("Original endpoint was modified") + } + }) + } +} diff --git a/config/endpoint/external_endpoint.go b/config/endpoint/external_endpoint.go index 3563c54e..6db95611 100644 --- a/config/endpoint/external_endpoint.go +++ b/config/endpoint/external_endpoint.go @@ -6,6 +6,7 @@ import ( "github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/config/endpoint/heartbeat" + "github.com/TwiN/gatus/v5/config/key" "github.com/TwiN/gatus/v5/config/maintenance" ) @@ -82,7 +83,7 @@ func (externalEndpoint *ExternalEndpoint) DisplayName() string { // Key returns the unique key for the Endpoint func (externalEndpoint *ExternalEndpoint) Key() string { - return ConvertGroupAndEndpointNameToKey(externalEndpoint.Group, externalEndpoint.Name) + return key.ConvertGroupAndNameToKey(externalEndpoint.Group, externalEndpoint.Name) } // ToEndpoint converts the ExternalEndpoint to an Endpoint diff --git a/config/endpoint/external_endpoint_test.go b/config/endpoint/external_endpoint_test.go index 4ead308b..0e619e57 100644 --- a/config/endpoint/external_endpoint_test.go +++ b/config/endpoint/external_endpoint_test.go @@ -2,24 +2,379 @@ package endpoint import ( "testing" + "time" + + "github.com/TwiN/gatus/v5/alerting/alert" + "github.com/TwiN/gatus/v5/config/endpoint/heartbeat" + "github.com/TwiN/gatus/v5/config/maintenance" ) -func TestExternalEndpoint_ToEndpoint(t *testing.T) { - externalEndpoint := &ExternalEndpoint{ - Name: "name", - Group: "group", +func TestExternalEndpoint_ValidateAndSetDefaults(t *testing.T) { + tests := []struct { + name string + endpoint *ExternalEndpoint + wantErr error + }{ + { + name: "valid-external-endpoint", + endpoint: &ExternalEndpoint{ + Name: "test-endpoint", + Group: "test-group", + Token: "valid-token", + }, + wantErr: nil, + }, + { + name: "valid-external-endpoint-with-heartbeat", + endpoint: &ExternalEndpoint{ + Name: "test-endpoint", + Token: "valid-token", + Heartbeat: heartbeat.Config{ + Interval: 30 * time.Second, + }, + }, + wantErr: nil, + }, + { + name: "missing-token", + endpoint: &ExternalEndpoint{ + Name: "test-endpoint", + Group: "test-group", + }, + wantErr: ErrExternalEndpointWithNoToken, + }, + { + name: "empty-token", + endpoint: &ExternalEndpoint{ + Name: "test-endpoint", + Token: "", + }, + wantErr: ErrExternalEndpointWithNoToken, + }, + { + name: "heartbeat-interval-too-low", + endpoint: &ExternalEndpoint{ + Name: "test-endpoint", + Token: "valid-token", + Heartbeat: heartbeat.Config{ + Interval: 5 * time.Second, // Less than 10 seconds + }, + }, + wantErr: ErrExternalEndpointHeartbeatIntervalTooLow, + }, + { + name: "heartbeat-interval-exactly-10-seconds", + endpoint: &ExternalEndpoint{ + Name: "test-endpoint", + Token: "valid-token", + Heartbeat: heartbeat.Config{ + Interval: 10 * time.Second, + }, + }, + wantErr: nil, + }, + { + name: "heartbeat-interval-zero-is-allowed", + endpoint: &ExternalEndpoint{ + Name: "test-endpoint", + Token: "valid-token", + Heartbeat: heartbeat.Config{ + Interval: 0, // Zero means no heartbeat monitoring + }, + }, + wantErr: nil, + }, + { + name: "missing-name", + endpoint: &ExternalEndpoint{ + Group: "test-group", + Token: "valid-token", + }, + wantErr: ErrEndpointWithNoName, + }, } - convertedEndpoint := externalEndpoint.ToEndpoint() - if externalEndpoint.Name != convertedEndpoint.Name { - t.Errorf("expected %s, got %s", externalEndpoint.Name, convertedEndpoint.Name) - } - if externalEndpoint.Group != convertedEndpoint.Group { - t.Errorf("expected %s, got %s", externalEndpoint.Group, convertedEndpoint.Group) - } - if externalEndpoint.Key() != convertedEndpoint.Key() { - t.Errorf("expected %s, got %s", externalEndpoint.Key(), convertedEndpoint.Key()) - } - if externalEndpoint.DisplayName() != convertedEndpoint.DisplayName() { - t.Errorf("expected %s, got %s", externalEndpoint.DisplayName(), convertedEndpoint.DisplayName()) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.endpoint.ValidateAndSetDefaults() + if tt.wantErr != nil { + if err == nil { + t.Errorf("Expected error %v, but got none", tt.wantErr) + return + } + if err.Error() != tt.wantErr.Error() { + t.Errorf("Expected error %v, got %v", tt.wantErr, err) + } + } else { + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } + } + }) } } + +func TestExternalEndpoint_IsEnabled(t *testing.T) { + tests := []struct { + name string + enabled *bool + expected bool + }{ + { + name: "nil-enabled-defaults-to-true", + enabled: nil, + expected: true, + }, + { + name: "explicitly-enabled", + enabled: boolPtr(true), + expected: true, + }, + { + name: "explicitly-disabled", + enabled: boolPtr(false), + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + endpoint := &ExternalEndpoint{ + Name: "test-endpoint", + Token: "test-token", + Enabled: tt.enabled, + } + result := endpoint.IsEnabled() + if result != tt.expected { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestExternalEndpoint_DisplayName(t *testing.T) { + tests := []struct { + name string + endpoint *ExternalEndpoint + expected string + }{ + { + name: "with-group", + endpoint: &ExternalEndpoint{ + Name: "test-endpoint", + Group: "test-group", + }, + expected: "test-group/test-endpoint", + }, + { + name: "without-group", + endpoint: &ExternalEndpoint{ + Name: "test-endpoint", + Group: "", + }, + expected: "test-endpoint", + }, + { + name: "empty-group-string", + endpoint: &ExternalEndpoint{ + Name: "api-health", + Group: "", + }, + expected: "api-health", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.endpoint.DisplayName() + if result != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result) + } + }) + } +} + +func TestExternalEndpoint_Key(t *testing.T) { + tests := []struct { + name string + endpoint *ExternalEndpoint + expected string + }{ + { + name: "with-group", + endpoint: &ExternalEndpoint{ + Name: "test-endpoint", + Group: "test-group", + }, + expected: "test-group_test-endpoint", + }, + { + name: "without-group", + endpoint: &ExternalEndpoint{ + Name: "test-endpoint", + Group: "", + }, + expected: "_test-endpoint", + }, + { + name: "special-characters-in-name", + endpoint: &ExternalEndpoint{ + Name: "test endpoint with spaces", + Group: "test-group", + }, + expected: "test-group_test-endpoint-with-spaces", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.endpoint.Key() + if result != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result) + } + }) + } +} + +func TestExternalEndpoint_ToEndpoint(t *testing.T) { + tests := []struct { + name string + externalEndpoint *ExternalEndpoint + }{ + { + name: "complete-external-endpoint", + externalEndpoint: &ExternalEndpoint{ + Enabled: boolPtr(true), + Name: "test-endpoint", + Group: "test-group", + Token: "test-token", + Alerts: []*alert.Alert{ + { + Type: alert.TypeSlack, + }, + }, + MaintenanceWindows: []*maintenance.Config{ + { + Start: "02:00", + Duration: time.Hour, + }, + }, + NumberOfFailuresInARow: 3, + NumberOfSuccessesInARow: 5, + }, + }, + { + name: "minimal-external-endpoint", + externalEndpoint: &ExternalEndpoint{ + Name: "minimal-endpoint", + Token: "minimal-token", + }, + }, + { + name: "disabled-external-endpoint", + externalEndpoint: &ExternalEndpoint{ + Enabled: boolPtr(false), + Name: "disabled-endpoint", + Token: "disabled-token", + }, + }, + { + name: "original-test-case", + externalEndpoint: &ExternalEndpoint{ + Name: "name", + Group: "group", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.externalEndpoint.ToEndpoint() + // Verify all fields are correctly copied + if result.Enabled != tt.externalEndpoint.Enabled { + t.Errorf("Expected Enabled=%v, got %v", tt.externalEndpoint.Enabled, result.Enabled) + } + if result.Name != tt.externalEndpoint.Name { + t.Errorf("Expected Name=%q, got %q", tt.externalEndpoint.Name, result.Name) + } + if result.Group != tt.externalEndpoint.Group { + t.Errorf("Expected Group=%q, got %q", tt.externalEndpoint.Group, result.Group) + } + if len(result.Alerts) != len(tt.externalEndpoint.Alerts) { + t.Errorf("Expected %d alerts, got %d", len(tt.externalEndpoint.Alerts), len(result.Alerts)) + } + if result.NumberOfFailuresInARow != tt.externalEndpoint.NumberOfFailuresInARow { + t.Errorf("Expected NumberOfFailuresInARow=%d, got %d", tt.externalEndpoint.NumberOfFailuresInARow, result.NumberOfFailuresInARow) + } + if result.NumberOfSuccessesInARow != tt.externalEndpoint.NumberOfSuccessesInARow { + t.Errorf("Expected NumberOfSuccessesInARow=%d, got %d", tt.externalEndpoint.NumberOfSuccessesInARow, result.NumberOfSuccessesInARow) + } + // Original test assertions + if tt.externalEndpoint.Key() != result.Key() { + t.Errorf("expected %s, got %s", tt.externalEndpoint.Key(), result.Key()) + } + if tt.externalEndpoint.DisplayName() != result.DisplayName() { + t.Errorf("expected %s, got %s", tt.externalEndpoint.DisplayName(), result.DisplayName()) + } + // Verify it's a proper Endpoint type + if result == nil { + t.Error("ToEndpoint() returned nil") + } + }) + } +} + +func TestExternalEndpoint_ValidationEdgeCases(t *testing.T) { + tests := []struct { + name string + endpoint *ExternalEndpoint + wantErr bool + }{ + { + name: "very-long-name", + endpoint: &ExternalEndpoint{ + Name: "this-is-a-very-long-endpoint-name-that-might-cause-issues-in-some-systems-but-should-be-handled-gracefully", + Token: "valid-token", + }, + wantErr: false, + }, + { + name: "special-characters-in-name", + endpoint: &ExternalEndpoint{ + Name: "test-endpoint@#$%^&*()", + Token: "valid-token", + }, + wantErr: false, + }, + { + name: "unicode-characters-in-name", + endpoint: &ExternalEndpoint{ + Name: "测试端点", + Token: "valid-token", + }, + wantErr: false, + }, + { + name: "very-long-token", + endpoint: &ExternalEndpoint{ + Name: "test-endpoint", + Token: "very-long-token-that-should-still-be-valid-even-though-it-is-extremely-long-and-might-not-be-practical-in-real-world-scenarios", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.endpoint.ValidateAndSetDefaults() + if tt.wantErr && err == nil { + t.Error("Expected error but got none") + } + if !tt.wantErr && err != nil { + t.Errorf("Expected no error but got: %v", err) + } + }) + } +} + +// Helper function to create bool pointers +func boolPtr(b bool) *bool { + return &b +} diff --git a/config/endpoint/key_bench_test.go b/config/endpoint/key_bench_test.go deleted file mode 100644 index cfec8c8b..00000000 --- a/config/endpoint/key_bench_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package endpoint - -import ( - "testing" -) - -func BenchmarkConvertGroupAndEndpointNameToKey(b *testing.B) { - for n := 0; n < b.N; n++ { - ConvertGroupAndEndpointNameToKey("group", "name") - } -} diff --git a/config/endpoint/placeholder.go b/config/endpoint/placeholder.go new file mode 100644 index 00000000..94ca221f --- /dev/null +++ b/config/endpoint/placeholder.go @@ -0,0 +1,273 @@ +package endpoint + +import ( + "fmt" + "strconv" + "strings" + + "github.com/TwiN/gatus/v5/config/gontext" + "github.com/TwiN/gatus/v5/jsonpath" +) + +// Placeholders +const ( + // StatusPlaceholder is a placeholder for a HTTP status. + // + // Values that could replace the placeholder: 200, 404, 500, ... + StatusPlaceholder = "[STATUS]" + + // IPPlaceholder is a placeholder for an IP. + // + // Values that could replace the placeholder: 127.0.0.1, 10.0.0.1, ... + IPPlaceholder = "[IP]" + + // DNSRCodePlaceholder is a placeholder for DNS_RCODE + // + // Values that could replace the placeholder: NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, REFUSED + DNSRCodePlaceholder = "[DNS_RCODE]" + + // ResponseTimePlaceholder is a placeholder for the request response time, in milliseconds. + // + // Values that could replace the placeholder: 1, 500, 1000, ... + ResponseTimePlaceholder = "[RESPONSE_TIME]" + + // BodyPlaceholder is a placeholder for the Body of the response + // + // Values that could replace the placeholder: {}, {"data":{"name":"john"}}, ... + BodyPlaceholder = "[BODY]" + + // ConnectedPlaceholder is a placeholder for whether a connection was successfully established. + // + // Values that could replace the placeholder: true, false + ConnectedPlaceholder = "[CONNECTED]" + + // CertificateExpirationPlaceholder is a placeholder for the duration before certificate expiration, in milliseconds. + // + // Values that could replace the placeholder: 4461677039 (~52 days) + CertificateExpirationPlaceholder = "[CERTIFICATE_EXPIRATION]" + + // DomainExpirationPlaceholder is a placeholder for the duration before the domain expires, in milliseconds. + DomainExpirationPlaceholder = "[DOMAIN_EXPIRATION]" + + // ContextPlaceholder is a placeholder for suite context values + // Usage: [CONTEXT].path.to.value + ContextPlaceholder = "[CONTEXT]" +) + +// Functions +const ( + // LengthFunctionPrefix is the prefix for the length function + // + // Usage: len([BODY].articles) == 10, len([BODY].name) > 5 + LengthFunctionPrefix = "len(" + + // HasFunctionPrefix is the prefix for the has function + // + // Usage: has([BODY].errors) == true + HasFunctionPrefix = "has(" + + // PatternFunctionPrefix is the prefix for the pattern function + // + // Usage: [IP] == pat(192.168.*.*) + PatternFunctionPrefix = "pat(" + + // AnyFunctionPrefix is the prefix for the any function + // + // Usage: [IP] == any(1.1.1.1, 1.0.0.1) + AnyFunctionPrefix = "any(" + + // FunctionSuffix is the suffix for all functions + FunctionSuffix = ")" +) + +// Other constants +const ( + // InvalidConditionElementSuffix is the suffix that will be appended to an invalid condition + InvalidConditionElementSuffix = "(INVALID)" +) + +// functionType represents the type of function wrapper +type functionType int + +const ( + // Note that not all functions are handled here. Only len() and has() directly impact the handler + // e.g. "len([BODY].name) > 0" vs pat() or any(), which would be used like "[BODY].name == pat(john*)" + + noFunction functionType = iota + functionLen + functionHas +) + +// ResolvePlaceholder resolves all types of placeholders to their string values. +// +// Supported placeholders: +// - [STATUS]: HTTP status code (e.g., "200", "404") +// - [IP]: IP address from the response (e.g., "127.0.0.1") +// - [RESPONSE_TIME]: Response time in milliseconds (e.g., "250") +// - [DNS_RCODE]: DNS response code (e.g., "NOERROR", "NXDOMAIN") +// - [CONNECTED]: Connection status (e.g., "true", "false") +// - [CERTIFICATE_EXPIRATION]: Certificate expiration time in milliseconds +// - [DOMAIN_EXPIRATION]: Domain expiration time in milliseconds +// - [BODY]: Full response body +// - [BODY].path: JSONPath expression on response body (e.g., [BODY].status, [BODY].data[0].name) +// - [CONTEXT].path: Suite context values (e.g., [CONTEXT].user_id, [CONTEXT].session_token) +// +// Function wrappers: +// - len(placeholder): Returns the length of the resolved value +// - has(placeholder): Returns "true" if the placeholder exists and is non-empty, "false" otherwise +// +// Examples: +// - ResolvePlaceholder("[STATUS]", result, nil) → "200" +// - ResolvePlaceholder("len([BODY].items)", result, nil) → "5" (for JSON array with 5 items) +// - ResolvePlaceholder("has([CONTEXT].user_id)", result, ctx) → "true" (if context has user_id) +// - ResolvePlaceholder("[BODY].user.name", result, nil) → "john" (for {"user":{"name":"john"}}) +// +// Case-insensitive: All placeholder names are handled case-insensitively, but paths preserve original case. +func ResolvePlaceholder(placeholder string, result *Result, ctx *gontext.Gontext) (string, error) { + placeholder = strings.TrimSpace(placeholder) + originalPlaceholder := placeholder + + // Extract function wrapper if present + fn, innerPlaceholder := extractFunctionWrapper(placeholder) + placeholder = innerPlaceholder + + // Handle CONTEXT placeholders + uppercasePlaceholder := strings.ToUpper(placeholder) + if strings.HasPrefix(uppercasePlaceholder, ContextPlaceholder) && ctx != nil { + return resolveContextPlaceholder(placeholder, fn, originalPlaceholder, ctx) + } + + // Handle basic placeholders (try uppercase first for backward compatibility) + switch uppercasePlaceholder { + case StatusPlaceholder: + return formatWithFunction(strconv.Itoa(result.HTTPStatus), fn), nil + case IPPlaceholder: + return formatWithFunction(result.IP, fn), nil + case ResponseTimePlaceholder: + return formatWithFunction(strconv.FormatInt(result.Duration.Milliseconds(), 10), fn), nil + case DNSRCodePlaceholder: + return formatWithFunction(result.DNSRCode, fn), nil + case ConnectedPlaceholder: + return formatWithFunction(strconv.FormatBool(result.Connected), fn), nil + case CertificateExpirationPlaceholder: + return formatWithFunction(strconv.FormatInt(result.CertificateExpiration.Milliseconds(), 10), fn), nil + case DomainExpirationPlaceholder: + return formatWithFunction(strconv.FormatInt(result.DomainExpiration.Milliseconds(), 10), fn), nil + case BodyPlaceholder: + body := strings.TrimSpace(string(result.Body)) + if fn == functionHas { + return strconv.FormatBool(len(body) > 0), nil + } + if fn == functionLen { + // For len([BODY]), we need to check if it's JSON and get the actual length + // Use jsonpath to evaluate the root element + _, resolvedLength, err := jsonpath.Eval("", result.Body) + if err == nil { + return strconv.Itoa(resolvedLength), nil + } + // Fall back to string length if not valid JSON + return strconv.Itoa(len(body)), nil + } + return body, nil + } + + // Handle JSONPath expressions on BODY (including array indexing) + if strings.HasPrefix(uppercasePlaceholder, BodyPlaceholder+".") || strings.HasPrefix(uppercasePlaceholder, BodyPlaceholder+"[") { + return resolveJSONPathPlaceholder(placeholder, fn, originalPlaceholder, result) + } + + // Not a recognized placeholder + if fn != noFunction { + if fn == functionHas { + return "false", nil + } + // For len() with unrecognized placeholder, return with INVALID suffix + return originalPlaceholder + " " + InvalidConditionElementSuffix, nil + } + + // Return the original placeholder if we can't resolve it + // This allows for literal string comparisons + return originalPlaceholder, nil +} + +// extractFunctionWrapper detects and extracts function wrappers (len, has) +func extractFunctionWrapper(placeholder string) (functionType, string) { + if strings.HasPrefix(placeholder, LengthFunctionPrefix) && strings.HasSuffix(placeholder, FunctionSuffix) { + inner := strings.TrimSuffix(strings.TrimPrefix(placeholder, LengthFunctionPrefix), FunctionSuffix) + return functionLen, inner + } + if strings.HasPrefix(placeholder, HasFunctionPrefix) && strings.HasSuffix(placeholder, FunctionSuffix) { + inner := strings.TrimSuffix(strings.TrimPrefix(placeholder, HasFunctionPrefix), FunctionSuffix) + return functionHas, inner + } + return noFunction, placeholder +} + +// resolveJSONPathPlaceholder handles [BODY].path and [BODY][index] placeholders +func resolveJSONPathPlaceholder(placeholder string, fn functionType, originalPlaceholder string, result *Result) (string, error) { + // Extract the path after [BODY] (case insensitive) + uppercasePlaceholder := strings.ToUpper(placeholder) + path := "" + if strings.HasPrefix(uppercasePlaceholder, BodyPlaceholder) { + path = placeholder[len(BodyPlaceholder):] + } else { + path = strings.TrimPrefix(placeholder, BodyPlaceholder) + } + // Remove leading dot if present + path = strings.TrimPrefix(path, ".") + resolvedValue, resolvedLength, err := jsonpath.Eval(path, result.Body) + if fn == functionHas { + return strconv.FormatBool(err == nil), nil + } + if err != nil { + return originalPlaceholder + " " + InvalidConditionElementSuffix, nil + } + if fn == functionLen { + return strconv.Itoa(resolvedLength), nil + } + return resolvedValue, nil +} + +// resolveContextPlaceholder handles [CONTEXT] placeholder resolution +func resolveContextPlaceholder(placeholder string, fn functionType, originalPlaceholder string, ctx *gontext.Gontext) (string, error) { + contextPath := strings.TrimPrefix(placeholder, ContextPlaceholder) + contextPath = strings.TrimPrefix(contextPath, ".") + if contextPath == "" { + if fn == functionHas { + return "false", nil + } + return originalPlaceholder + " " + InvalidConditionElementSuffix, nil + } + value, err := ctx.Get(contextPath) + if fn == functionHas { + return strconv.FormatBool(err == nil), nil + } + if err != nil { + return originalPlaceholder + " " + InvalidConditionElementSuffix, nil + } + if fn == functionLen { + switch v := value.(type) { + case string: + return strconv.Itoa(len(v)), nil + case []interface{}: + return strconv.Itoa(len(v)), nil + case map[string]interface{}: + return strconv.Itoa(len(v)), nil + default: + return strconv.Itoa(len(fmt.Sprintf("%v", v))), nil + } + } + return fmt.Sprintf("%v", value), nil +} + +// formatWithFunction applies len/has functions to any value +func formatWithFunction(value string, fn functionType) string { + switch fn { + case functionHas: + return strconv.FormatBool(value != "") + case functionLen: + return strconv.Itoa(len(value)) + default: + return value + } +} diff --git a/config/endpoint/placeholder_test.go b/config/endpoint/placeholder_test.go new file mode 100644 index 00000000..14ecfffb --- /dev/null +++ b/config/endpoint/placeholder_test.go @@ -0,0 +1,125 @@ +package endpoint + +import ( + "testing" + "time" + + "github.com/TwiN/gatus/v5/config/gontext" +) + +func TestResolvePlaceholder(t *testing.T) { + result := &Result{ + HTTPStatus: 200, + IP: "127.0.0.1", + Duration: 250 * time.Millisecond, + DNSRCode: "NOERROR", + Connected: true, + CertificateExpiration: 30 * 24 * time.Hour, + DomainExpiration: 365 * 24 * time.Hour, + Body: []byte(`{"status":"success","items":[1,2,3],"user":{"name":"john","id":123}}`), + } + + ctx := gontext.New(map[string]interface{}{ + "user_id": "abc123", + "session_token": "xyz789", + "array_data": []interface{}{"a", "b", "c"}, + "nested": map[string]interface{}{ + "value": "test", + }, + }) + + tests := []struct { + name string + placeholder string + expected string + }{ + // Basic placeholders + {"status", "[STATUS]", "200"}, + {"ip", "[IP]", "127.0.0.1"}, + {"response-time", "[RESPONSE_TIME]", "250"}, + {"dns-rcode", "[DNS_RCODE]", "NOERROR"}, + {"connected", "[CONNECTED]", "true"}, + {"certificate-expiration", "[CERTIFICATE_EXPIRATION]", "2592000000"}, + {"domain-expiration", "[DOMAIN_EXPIRATION]", "31536000000"}, + {"body", "[BODY]", `{"status":"success","items":[1,2,3],"user":{"name":"john","id":123}}`}, + + // Case insensitive placeholders + {"status-lowercase", "[status]", "200"}, + {"ip-mixed-case", "[Ip]", "127.0.0.1"}, + + // Function wrappers on basic placeholders + {"len-status", "len([STATUS])", "3"}, + {"len-ip", "len([IP])", "9"}, + {"has-status", "has([STATUS])", "true"}, + {"has-empty", "has()", "false"}, + + // JSONPath expressions + {"body-status", "[BODY].status", "success"}, + {"body-user-name", "[BODY].user.name", "john"}, + {"body-user-id", "[BODY].user.id", "123"}, + {"len-body-items", "len([BODY].items)", "3"}, + {"body-array-index", "[BODY].items[0]", "1"}, + {"has-body-status", "has([BODY].status)", "true"}, + {"has-body-missing", "has([BODY].missing)", "false"}, + + // Context placeholders + {"context-user-id", "[CONTEXT].user_id", "abc123"}, + {"context-session-token", "[CONTEXT].session_token", "xyz789"}, + {"context-nested", "[CONTEXT].nested.value", "test"}, + {"len-context-array", "len([CONTEXT].array_data)", "3"}, + {"has-context-user-id", "has([CONTEXT].user_id)", "true"}, + {"has-context-missing", "has([CONTEXT].missing)", "false"}, + + // Invalid placeholders + {"unknown-placeholder", "[UNKNOWN]", "[UNKNOWN]"}, + {"len-unknown", "len([UNKNOWN])", "len([UNKNOWN]) (INVALID)"}, + {"has-unknown", "has([UNKNOWN])", "false"}, + {"invalid-jsonpath", "[BODY].invalid.path", "[BODY].invalid.path (INVALID)"}, + + // Literal strings + {"literal-string", "literal", "literal"}, + {"number-string", "123", "123"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, err := ResolvePlaceholder(test.placeholder, result, ctx) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if actual != test.expected { + t.Errorf("expected '%s', got '%s'", test.expected, actual) + } + }) + } +} + +func TestResolvePlaceholderWithoutContext(t *testing.T) { + result := &Result{ + HTTPStatus: 404, + Body: []byte(`{"error":"not found"}`), + } + + tests := []struct { + name string + placeholder string + expected string + }{ + {"status-without-context", "[STATUS]", "404"}, + {"body-without-context", "[BODY].error", "not found"}, + {"context-without-context", "[CONTEXT].user_id", "[CONTEXT].user_id"}, + {"has-context-without-context", "has([CONTEXT].user_id)", "false"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, err := ResolvePlaceholder(test.placeholder, result, nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if actual != test.expected { + t.Errorf("expected '%s', got '%s'", test.expected, actual) + } + }) + } +} diff --git a/config/endpoint/result.go b/config/endpoint/result.go index 2a7aba94..8164cd79 100644 --- a/config/endpoint/result.go +++ b/config/endpoint/result.go @@ -4,7 +4,7 @@ import ( "time" ) -// Result of the evaluation of a Endpoint +// Result of the evaluation of an Endpoint type Result struct { // HTTPStatus is the HTTP response status code HTTPStatus int `json:"status,omitempty"` @@ -54,6 +54,13 @@ type Result struct { // Below is used only for the UI and is not persisted in the storage // /////////////////////////////////////////////////////////////////////// port string `yaml:"-"` // used for endpoints[].ui.hide-port + + /////////////////////////////////// + // BELOW IS ONLY USED FOR SUITES // + /////////////////////////////////// + // Name of the endpoint (ONLY USED FOR SUITES) + // Group is not needed because it's inherited from the suite + Name string `json:"name,omitempty"` } // AddError adds an error to the result's list of errors. diff --git a/config/endpoint/status.go b/config/endpoint/status.go index f1a725ba..93677276 100644 --- a/config/endpoint/status.go +++ b/config/endpoint/status.go @@ -1,6 +1,9 @@ package endpoint +import "github.com/TwiN/gatus/v5/config/key" + // Status contains the evaluation Results of an Endpoint +// This is essentially a DTO type Status struct { // Name of the endpoint Name string `json:"name,omitempty"` @@ -30,7 +33,7 @@ func NewStatus(group, name string) *Status { return &Status{ Name: name, Group: group, - Key: ConvertGroupAndEndpointNameToKey(group, name), + Key: key.ConvertGroupAndNameToKey(group, name), Results: make([]*Result, 0), Events: make([]*Event, 0), Uptime: NewUptime(), diff --git a/config/gontext/gontext.go b/config/gontext/gontext.go new file mode 100644 index 00000000..32170705 --- /dev/null +++ b/config/gontext/gontext.go @@ -0,0 +1,121 @@ +package gontext + +import ( + "errors" + "fmt" + "strings" + "sync" +) + +var ( + // ErrGontextPathNotFound is returned when a gontext path doesn't exist + ErrGontextPathNotFound = errors.New("gontext path not found") +) + +// Gontext holds values that can be shared between endpoints in a suite +type Gontext struct { + mu sync.RWMutex + values map[string]interface{} +} + +// New creates a new gontext with initial values +func New(initial map[string]interface{}) *Gontext { + if initial == nil { + initial = make(map[string]interface{}) + } + // Create a deep copy to avoid external modifications + values := make(map[string]interface{}) + for k, v := range initial { + values[k] = deepCopyValue(v) + } + return &Gontext{ + values: values, + } +} + +// Get retrieves a value from the gontext using dot notation +func (g *Gontext) Get(path string) (interface{}, error) { + g.mu.RLock() + defer g.mu.RUnlock() + parts := strings.Split(path, ".") + current := interface{}(g.values) + for _, part := range parts { + switch v := current.(type) { + case map[string]interface{}: + val, exists := v[part] + if !exists { + return nil, fmt.Errorf("%w: %s", ErrGontextPathNotFound, path) + } + current = val + default: + return nil, fmt.Errorf("%w: %s", ErrGontextPathNotFound, path) + } + } + return current, nil +} + +// Set stores a value in the gontext using dot notation +func (g *Gontext) Set(path string, value interface{}) error { + g.mu.Lock() + defer g.mu.Unlock() + parts := strings.Split(path, ".") + if len(parts) == 0 { + return errors.New("empty path") + } + // Navigate to the parent of the target + current := g.values + for i := 0; i < len(parts)-1; i++ { + part := parts[i] + if next, exists := current[part]; exists { + if nextMap, ok := next.(map[string]interface{}); ok { + current = nextMap + } else { + // Path exists but is not a map, create a new map + newMap := make(map[string]interface{}) + current[part] = newMap + current = newMap + } + } else { + // Create intermediate maps + newMap := make(map[string]interface{}) + current[part] = newMap + current = newMap + } + } + // Set the final value + current[parts[len(parts)-1]] = value + return nil +} + +// GetAll returns a copy of all gontext values +func (g *Gontext) GetAll() map[string]interface{} { + g.mu.RLock() + defer g.mu.RUnlock() + + result := make(map[string]interface{}) + for k, v := range g.values { + result[k] = deepCopyValue(v) + } + return result +} + +// deepCopyValue creates a deep copy of a value +func deepCopyValue(v interface{}) interface{} { + switch val := v.(type) { + case map[string]interface{}: + newMap := make(map[string]interface{}) + for k, v := range val { + newMap[k] = deepCopyValue(v) + } + return newMap + case []interface{}: + newSlice := make([]interface{}, len(val)) + for i, v := range val { + newSlice[i] = deepCopyValue(v) + } + return newSlice + default: + // For primitive types, return as-is (they're passed by value anyway) + return val + } +} diff --git a/config/gontext/gontext_test.go b/config/gontext/gontext_test.go new file mode 100644 index 00000000..a29bbe4d --- /dev/null +++ b/config/gontext/gontext_test.go @@ -0,0 +1,448 @@ +package gontext + +import ( + "errors" + "testing" +) + +func TestNew(t *testing.T) { + tests := []struct { + name string + initial map[string]interface{} + expected map[string]interface{} + }{ + { + name: "nil-input", + initial: nil, + expected: make(map[string]interface{}), + }, + { + name: "empty-input", + initial: make(map[string]interface{}), + expected: make(map[string]interface{}), + }, + { + name: "simple-values", + initial: map[string]interface{}{ + "key1": "value1", + "key2": 42, + }, + expected: map[string]interface{}{ + "key1": "value1", + "key2": 42, + }, + }, + { + name: "nested-values", + initial: map[string]interface{}{ + "user": map[string]interface{}{ + "id": 123, + "name": "John Doe", + }, + }, + expected: map[string]interface{}{ + "user": map[string]interface{}{ + "id": 123, + "name": "John Doe", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := New(tt.initial) + if ctx == nil { + t.Error("Expected non-nil gontext") + } + if ctx.values == nil { + t.Error("Expected non-nil values map") + } + + // Verify deep copy by modifying original + if tt.initial != nil { + tt.initial["modified"] = "should not appear" + if _, exists := ctx.values["modified"]; exists { + t.Error("Deep copy failed - original map modification affected gontext") + } + } + }) + } +} + +func TestGontext_Get(t *testing.T) { + ctx := New(map[string]interface{}{ + "simple": "value", + "number": 42, + "boolean": true, + "nested": map[string]interface{}{ + "level1": map[string]interface{}{ + "level2": "deep_value", + }, + }, + "user": map[string]interface{}{ + "id": 123, + "name": "John", + "profile": map[string]interface{}{ + "email": "john@example.com", + }, + }, + }) + + tests := []struct { + name string + path string + expected interface{} + shouldError bool + errorType error + }{ + { + name: "simple-value", + path: "simple", + expected: "value", + shouldError: false, + }, + { + name: "number-value", + path: "number", + expected: 42, + shouldError: false, + }, + { + name: "boolean-value", + path: "boolean", + expected: true, + shouldError: false, + }, + { + name: "nested-value", + path: "nested.level1.level2", + expected: "deep_value", + shouldError: false, + }, + { + name: "user-id", + path: "user.id", + expected: 123, + shouldError: false, + }, + { + name: "deep-nested-value", + path: "user.profile.email", + expected: "john@example.com", + shouldError: false, + }, + { + name: "non-existent-key", + path: "nonexistent", + expected: nil, + shouldError: true, + errorType: ErrGontextPathNotFound, + }, + { + name: "non-existent-nested-key", + path: "user.nonexistent", + expected: nil, + shouldError: true, + errorType: ErrGontextPathNotFound, + }, + { + name: "invalid-nested-path", + path: "simple.invalid", + expected: nil, + shouldError: true, + errorType: ErrGontextPathNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ctx.Get(tt.path) + + if tt.shouldError { + if err == nil { + t.Errorf("Expected error but got none") + } + if tt.errorType != nil && !errors.Is(err, tt.errorType) { + t.Errorf("Expected error type %v, got %v", tt.errorType, err) + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != tt.expected { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + } + }) + } +} + +func TestGontext_Set(t *testing.T) { + tests := []struct { + name string + path string + value interface{} + wantErr bool + }{ + { + name: "simple-set", + path: "key", + value: "value", + wantErr: false, + }, + { + name: "nested-set", + path: "user.name", + value: "John Doe", + wantErr: false, + }, + { + name: "deep-nested-set", + path: "user.profile.email", + value: "john@example.com", + wantErr: false, + }, + { + name: "override-primitive-with-nested", + path: "existing.new", + value: "nested_value", + wantErr: false, + }, + { + name: "empty-path", + path: "", + value: "value", + wantErr: false, // Actually, empty string creates a single part [""], which is valid + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := New(map[string]interface{}{ + "existing": "primitive", + }) + + err := ctx.Set(tt.path, tt.value) + + if tt.wantErr { + if err == nil { + t.Error("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + // Verify the value was set correctly + result, getErr := ctx.Get(tt.path) + if getErr != nil { + t.Errorf("Error retrieving set value: %v", getErr) + return + } + + if result != tt.value { + t.Errorf("Expected %v, got %v", tt.value, result) + } + }) + } +} + +func TestGontext_SetOverrideBehavior(t *testing.T) { + ctx := New(map[string]interface{}{ + "primitive": "value", + "nested": map[string]interface{}{ + "key": "existing", + }, + }) + + // Test overriding primitive with nested structure + err := ctx.Set("primitive.new", "nested_value") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Verify the primitive was replaced with a nested structure + result, err := ctx.Get("primitive.new") + if err != nil { + t.Errorf("Error getting nested value: %v", err) + } + if result != "nested_value" { + t.Errorf("Expected 'nested_value', got %v", result) + } + + // Test overriding existing nested value + err = ctx.Set("nested.key", "modified") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + result, err = ctx.Get("nested.key") + if err != nil { + t.Errorf("Error getting modified value: %v", err) + } + if result != "modified" { + t.Errorf("Expected 'modified', got %v", result) + } +} + +func TestGontext_GetAll(t *testing.T) { + initial := map[string]interface{}{ + "key1": "value1", + "key2": 42, + "nested": map[string]interface{}{ + "inner": "value", + }, + } + + ctx := New(initial) + + // Add another value after creation + ctx.Set("key3", "value3") + + result := ctx.GetAll() + + // Verify all values are present + if result["key1"] != "value1" { + t.Errorf("Expected key1=value1, got %v", result["key1"]) + } + if result["key2"] != 42 { + t.Errorf("Expected key2=42, got %v", result["key2"]) + } + if result["key3"] != "value3" { + t.Errorf("Expected key3=value3, got %v", result["key3"]) + } + + // Verify nested values + nested, ok := result["nested"].(map[string]interface{}) + if !ok { + t.Error("Expected nested to be map[string]interface{}") + } else if nested["inner"] != "value" { + t.Errorf("Expected nested.inner=value, got %v", nested["inner"]) + } + + // Verify deep copy - modifying returned map shouldn't affect gontext + result["key1"] = "modified" + original, _ := ctx.Get("key1") + if original != "value1" { + t.Error("GetAll did not return a deep copy - modification affected original") + } +} + +func TestGontext_ConcurrentAccess(t *testing.T) { + ctx := New(map[string]interface{}{ + "counter": 0, + }) + + done := make(chan bool, 10) + + // Start 5 goroutines that read values + for i := 0; i < 5; i++ { + go func(id int) { + for j := 0; j < 100; j++ { + _, err := ctx.Get("counter") + if err != nil { + t.Errorf("Reader %d error: %v", id, err) + } + } + done <- true + }(i) + } + + // Start 5 goroutines that write values + for i := 0; i < 5; i++ { + go func(id int) { + for j := 0; j < 100; j++ { + err := ctx.Set("counter", id*1000+j) + if err != nil { + t.Errorf("Writer %d error: %v", id, err) + } + } + done <- true + }(i) + } + + // Wait for all goroutines to complete + for i := 0; i < 10; i++ { + <-done + } +} + +func TestDeepCopyValue(t *testing.T) { + tests := []struct { + name string + input interface{} + }{ + { + name: "primitive-string", + input: "test", + }, + { + name: "primitive-int", + input: 42, + }, + { + name: "primitive-bool", + input: true, + }, + { + name: "simple-map", + input: map[string]interface{}{ + "key": "value", + }, + }, + { + name: "nested-map", + input: map[string]interface{}{ + "nested": map[string]interface{}{ + "deep": "value", + }, + }, + }, + { + name: "simple-slice", + input: []interface{}{"a", "b", "c"}, + }, + { + name: "mixed-slice", + input: []interface{}{ + "string", + 42, + map[string]interface{}{"nested": "value"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := deepCopyValue(tt.input) + + // For maps and slices, verify it's a different object + switch v := tt.input.(type) { + case map[string]interface{}: + resultMap, ok := result.(map[string]interface{}) + if !ok { + t.Error("Deep copy didn't preserve map type") + return + } + // Modify original to ensure independence + v["modified"] = "test" + if _, exists := resultMap["modified"]; exists { + t.Error("Deep copy failed - maps are not independent") + } + case []interface{}: + resultSlice, ok := result.([]interface{}) + if !ok { + t.Error("Deep copy didn't preserve slice type") + return + } + if len(resultSlice) != len(v) { + t.Error("Deep copy didn't preserve slice length") + } + } + }) + } +} diff --git a/config/endpoint/key.go b/config/key/key.go similarity index 58% rename from config/endpoint/key.go rename to config/key/key.go index 964f558f..d0b931dc 100644 --- a/config/endpoint/key.go +++ b/config/key/key.go @@ -1,10 +1,10 @@ -package endpoint +package key import "strings" -// ConvertGroupAndEndpointNameToKey converts a group and an endpoint to a key -func ConvertGroupAndEndpointNameToKey(groupName, endpointName string) string { - return sanitize(groupName) + "_" + sanitize(endpointName) +// ConvertGroupAndNameToKey converts a group and a name to a key +func ConvertGroupAndNameToKey(groupName, name string) string { + return sanitize(groupName) + "_" + sanitize(name) } func sanitize(s string) string { @@ -16,4 +16,4 @@ func sanitize(s string) string { s = strings.ReplaceAll(s, " ", "-") s = strings.ReplaceAll(s, "#", "-") return s -} +} \ No newline at end of file diff --git a/config/key/key_bench_test.go b/config/key/key_bench_test.go new file mode 100644 index 00000000..50f25f4c --- /dev/null +++ b/config/key/key_bench_test.go @@ -0,0 +1,11 @@ +package key + +import ( + "testing" +) + +func BenchmarkConvertGroupAndNameToKey(b *testing.B) { + for n := 0; n < b.N; n++ { + ConvertGroupAndNameToKey("group", "name") + } +} \ No newline at end of file diff --git a/config/endpoint/key_test.go b/config/key/key_test.go similarity index 63% rename from config/endpoint/key_test.go rename to config/key/key_test.go index c693ba5d..34179cfc 100644 --- a/config/endpoint/key_test.go +++ b/config/key/key_test.go @@ -1,33 +1,38 @@ -package endpoint +package key import "testing" -func TestConvertGroupAndEndpointNameToKey(t *testing.T) { +func TestConvertGroupAndNameToKey(t *testing.T) { type Scenario struct { GroupName string - EndpointName string + Name string ExpectedOutput string } scenarios := []Scenario{ { GroupName: "Core", - EndpointName: "Front End", + Name: "Front End", ExpectedOutput: "core_front-end", }, { GroupName: "Load balancers", - EndpointName: "us-west-2", + Name: "us-west-2", ExpectedOutput: "load-balancers_us-west-2", }, { GroupName: "a/b test", - EndpointName: "a", + Name: "a", ExpectedOutput: "a-b-test_a", }, + { + GroupName: "", + Name: "name", + ExpectedOutput: "_name", + }, } for _, scenario := range scenarios { t.Run(scenario.ExpectedOutput, func(t *testing.T) { - output := ConvertGroupAndEndpointNameToKey(scenario.GroupName, scenario.EndpointName) + output := ConvertGroupAndNameToKey(scenario.GroupName, scenario.Name) if output != scenario.ExpectedOutput { t.Errorf("expected '%s', got '%s'", scenario.ExpectedOutput, output) } diff --git a/config/suite/result.go b/config/suite/result.go new file mode 100644 index 00000000..23b2045f --- /dev/null +++ b/config/suite/result.go @@ -0,0 +1,55 @@ +package suite + +import ( + "time" + + "github.com/TwiN/gatus/v5/config/endpoint" +) + +// Result represents the result of a suite execution +type Result struct { + // Name of the suite + Name string `json:"name,omitempty"` + + // Group of the suite + Group string `json:"group,omitempty"` + + // Success indicates whether all required endpoints succeeded + Success bool `json:"success"` + + // Timestamp is when the suite execution started + Timestamp time.Time `json:"timestamp"` + + // Duration is how long the entire suite execution took + Duration time.Duration `json:"duration"` + + // EndpointResults contains the results of each endpoint execution + EndpointResults []*endpoint.Result `json:"endpointResults"` + + // Context is the final state of the context after all endpoints executed + Context map[string]interface{} `json:"-"` + + // Errors contains any suite-level errors + Errors []string `json:"errors,omitempty"` +} + +// AddError adds an error to the suite result +func (r *Result) AddError(err string) { + r.Errors = append(r.Errors, err) +} + +// CalculateSuccess determines if the suite execution was successful +func (r *Result) CalculateSuccess() { + r.Success = true + // Check if any endpoints failed (all endpoints are required) + for _, epResult := range r.EndpointResults { + if !epResult.Success { + r.Success = false + break + } + } + // Also check for suite-level errors + if len(r.Errors) > 0 { + r.Success = false + } +} diff --git a/config/suite/suite.go b/config/suite/suite.go new file mode 100644 index 00000000..fdbbeee4 --- /dev/null +++ b/config/suite/suite.go @@ -0,0 +1,214 @@ +package suite + +import ( + "errors" + "fmt" + "strconv" + "time" + + "github.com/TwiN/gatus/v5/config/endpoint" + "github.com/TwiN/gatus/v5/config/gontext" + "github.com/TwiN/gatus/v5/config/key" +) + +var ( + // ErrSuiteWithNoName is the error returned when a suite has no name + ErrSuiteWithNoName = errors.New("suite must have a name") + + // ErrSuiteWithNoEndpoints is the error returned when a suite has no endpoints + ErrSuiteWithNoEndpoints = errors.New("suite must have at least one endpoint") + + // ErrSuiteWithDuplicateEndpointNames is the error returned when a suite has duplicate endpoint names + ErrSuiteWithDuplicateEndpointNames = errors.New("suite cannot have duplicate endpoint names") + + // ErrSuiteWithInvalidTimeout is the error returned when a suite has an invalid timeout + ErrSuiteWithInvalidTimeout = errors.New("suite timeout must be positive") + + // DefaultInterval is the default interval for suite execution + DefaultInterval = 10 * time.Minute + + // DefaultTimeout is the default timeout for suite execution + DefaultTimeout = 5 * time.Minute +) + +// Suite is a collection of endpoints that are executed sequentially with shared context +type Suite struct { + // Name of the suite. Must be unique. + Name string `yaml:"name"` + + // Group the suite belongs to. Used for grouping multiple suites together. + Group string `yaml:"group,omitempty"` + + // Enabled defines whether the suite is enabled + Enabled *bool `yaml:"enabled,omitempty"` + + // Interval is the duration to wait between suite executions + Interval time.Duration `yaml:"interval,omitempty"` + + // Timeout is the maximum duration for the entire suite execution + Timeout time.Duration `yaml:"timeout,omitempty"` + + // InitialContext holds initial values that can be referenced by endpoints + InitialContext map[string]interface{} `yaml:"context,omitempty"` + + // Endpoints in the suite (executed sequentially) + Endpoints []*endpoint.Endpoint `yaml:"endpoints"` +} + +// IsEnabled returns whether the suite is enabled +func (s *Suite) IsEnabled() bool { + if s.Enabled == nil { + return true + } + return *s.Enabled +} + +// Key returns a unique key for the suite +func (s *Suite) Key() string { + return key.ConvertGroupAndNameToKey(s.Group, s.Name) +} + +// ValidateAndSetDefaults validates the suite configuration and sets default values +func (s *Suite) ValidateAndSetDefaults() error { + // Validate name + if len(s.Name) == 0 { + return ErrSuiteWithNoName + } + // Validate endpoints + if len(s.Endpoints) == 0 { + return ErrSuiteWithNoEndpoints + } + // Check for duplicate endpoint names + endpointNames := make(map[string]bool) + for _, ep := range s.Endpoints { + if endpointNames[ep.Name] { + return fmt.Errorf("%w: duplicate endpoint name '%s'", ErrSuiteWithDuplicateEndpointNames, ep.Name) + } + endpointNames[ep.Name] = true + // Suite endpoints inherit the group from the suite + ep.Group = s.Group + // Validate each endpoint + if err := ep.ValidateAndSetDefaults(); err != nil { + return fmt.Errorf("invalid endpoint '%s': %w", ep.Name, err) + } + } + // Set default interval + if s.Interval == 0 { + s.Interval = DefaultInterval + } + // Set default timeout + if s.Timeout == 0 { + s.Timeout = DefaultTimeout + } + // Validate timeout + if s.Timeout < 0 { + return ErrSuiteWithInvalidTimeout + } + // Initialize context if nil + if s.InitialContext == nil { + s.InitialContext = make(map[string]interface{}) + } + return nil +} + +// Execute executes all endpoints in the suite sequentially with context sharing +func (s *Suite) Execute() *Result { + start := time.Now() + // Initialize context from suite configuration + ctx := gontext.New(s.InitialContext) + // Create suite result + result := &Result{ + Name: s.Name, + Group: s.Group, + Success: true, + Timestamp: start, + EndpointResults: make([]*endpoint.Result, 0, len(s.Endpoints)), + } + // Set up timeout for the entire suite execution + timeoutChan := time.After(s.Timeout) + // Execute each endpoint sequentially + suiteHasFailed := false + for _, ep := range s.Endpoints { + // Skip non-always-run endpoints if suite has already failed + if suiteHasFailed && !ep.AlwaysRun { + continue + } + // Check timeout + select { + case <-timeoutChan: + result.AddError(fmt.Sprintf("suite execution timed out after %v", s.Timeout)) + result.Success = false + break + default: + } + // Execute endpoint with context + epStartTime := time.Now() + epResult := ep.EvaluateHealthWithContext(ctx) + epDuration := time.Since(epStartTime) + // Set endpoint name, timestamp, and duration on the result + epResult.Name = ep.Name + epResult.Timestamp = epStartTime + epResult.Duration = epDuration + // Store values from the endpoint result if configured (always store, even on failure) + if ep.Store != nil { + _, err := StoreResultValues(ctx, ep.Store, epResult) + if err != nil { + epResult.AddError(fmt.Sprintf("failed to store values: %v", err)) + } + } + result.EndpointResults = append(result.EndpointResults, epResult) + // Mark suite as failed on any endpoint failure + if !epResult.Success { + result.Success = false + suiteHasFailed = true + } + } + result.Context = ctx.GetAll() + result.Duration = time.Since(start) + result.CalculateSuccess() + return result +} + +// StoreResultValues extracts values from an endpoint result and stores them in the gontext +func StoreResultValues(ctx *gontext.Gontext, mappings map[string]string, result *endpoint.Result) (map[string]interface{}, error) { + if mappings == nil || len(mappings) == 0 { + return nil, nil + } + storedValues := make(map[string]interface{}) + for contextKey, placeholder := range mappings { + value, err := extractValueForStorage(placeholder, result) + if err != nil { + // Continue storing other values even if one fails + storedValues[contextKey] = fmt.Sprintf("ERROR: %v", err) + continue + } + if err := ctx.Set(contextKey, value); err != nil { + return storedValues, fmt.Errorf("failed to store %s: %w", contextKey, err) + } + storedValues[contextKey] = value + } + return storedValues, nil +} + +// extractValueForStorage extracts a value from an endpoint result for storage in context +func extractValueForStorage(placeholder string, result *endpoint.Result) (interface{}, error) { + // Use the unified ResolvePlaceholder function (no context needed for extraction) + resolved, err := endpoint.ResolvePlaceholder(placeholder, result, nil) + if err != nil { + return nil, err + } + // Try to parse as number or boolean to store as proper types + // Try int first for whole numbers + if num, err := strconv.ParseInt(resolved, 10, 64); err == nil { + return num, nil + } + // Then try float for decimals + if num, err := strconv.ParseFloat(resolved, 64); err == nil { + return num, nil + } + // Then try boolean + if boolVal, err := strconv.ParseBool(resolved); err == nil { + return boolVal, nil + } + return resolved, nil +} diff --git a/config/suite/suite_status.go b/config/suite/suite_status.go new file mode 100644 index 00000000..4767cb33 --- /dev/null +++ b/config/suite/suite_status.go @@ -0,0 +1,26 @@ +package suite + +// Status represents the status of a suite +type Status struct { + // Name of the suite + Name string `json:"name,omitempty"` + + // Group the suite is a part of. Used for grouping multiple suites together on the front end. + Group string `json:"group,omitempty"` + + // Key of the Suite + Key string `json:"key"` + + // Results is the list of suite execution results + Results []*Result `json:"results"` +} + +// NewStatus creates a new Status for a given Suite +func NewStatus(s *Suite) *Status { + return &Status{ + Name: s.Name, + Group: s.Group, + Key: s.Key(), + Results: []*Result{}, + } +} \ No newline at end of file diff --git a/config/suite/suite_test.go b/config/suite/suite_test.go new file mode 100644 index 00000000..3a7a48be --- /dev/null +++ b/config/suite/suite_test.go @@ -0,0 +1,449 @@ +package suite + +import ( + "testing" + "time" + + "github.com/TwiN/gatus/v5/config/endpoint" + "github.com/TwiN/gatus/v5/config/gontext" +) + +func TestSuite_ValidateAndSetDefaults(t *testing.T) { + tests := []struct { + name string + suite *Suite + wantErr bool + }{ + { + name: "valid-suite", + suite: &Suite{ + Name: "test-suite", + Endpoints: []*endpoint.Endpoint{ + { + Name: "endpoint1", + URL: "https://example.org", + Conditions: []endpoint.Condition{ + endpoint.Condition("[STATUS] == 200"), + }, + }, + }, + }, + wantErr: false, + }, + { + name: "suite-without-name", + suite: &Suite{ + Endpoints: []*endpoint.Endpoint{ + { + Name: "endpoint1", + URL: "https://example.org", + Conditions: []endpoint.Condition{ + endpoint.Condition("[STATUS] == 200"), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "suite-without-endpoints", + suite: &Suite{ + Name: "test-suite", + Endpoints: []*endpoint.Endpoint{}, + }, + wantErr: true, + }, + { + name: "suite-with-duplicate-endpoint-names", + suite: &Suite{ + Name: "test-suite", + Endpoints: []*endpoint.Endpoint{ + { + Name: "duplicate", + URL: "https://example.org", + Conditions: []endpoint.Condition{ + endpoint.Condition("[STATUS] == 200"), + }, + }, + { + Name: "duplicate", + URL: "https://example.com", + Conditions: []endpoint.Condition{ + endpoint.Condition("[STATUS] == 200"), + }, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.suite.ValidateAndSetDefaults() + if (err != nil) != tt.wantErr { + t.Errorf("Suite.ValidateAndSetDefaults() error = %v, wantErr %v", err, tt.wantErr) + } + // Check defaults were set + if err == nil { + if tt.suite.Interval == 0 { + t.Errorf("Expected Interval to be set to default, got 0") + } + if tt.suite.Timeout == 0 { + t.Errorf("Expected Timeout to be set to default, got 0") + } + } + }) + } +} + +func TestSuite_IsEnabled(t *testing.T) { + tests := []struct { + name string + enabled *bool + want bool + }{ + { + name: "nil-defaults-to-true", + enabled: nil, + want: true, + }, + { + name: "explicitly-enabled", + enabled: boolPtr(true), + want: true, + }, + { + name: "explicitly-disabled", + enabled: boolPtr(false), + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Suite{Enabled: tt.enabled} + if got := s.IsEnabled(); got != tt.want { + t.Errorf("Suite.IsEnabled() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSuite_Key(t *testing.T) { + tests := []struct { + name string + suite *Suite + want string + }{ + { + name: "with-group", + suite: &Suite{ + Name: "test-suite", + Group: "test-group", + }, + want: "test-group_test-suite", + }, + { + name: "without-group", + suite: &Suite{ + Name: "test-suite", + Group: "", + }, + want: "_test-suite", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.suite.Key(); got != tt.want { + t.Errorf("Suite.Key() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSuite_DefaultValues(t *testing.T) { + s := &Suite{ + Name: "test", + Endpoints: []*endpoint.Endpoint{ + { + Name: "endpoint1", + URL: "https://example.org", + Conditions: []endpoint.Condition{ + endpoint.Condition("[STATUS] == 200"), + }, + }, + }, + } + err := s.ValidateAndSetDefaults() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if s.Interval != DefaultInterval { + t.Errorf("Expected Interval to be %v, got %v", DefaultInterval, s.Interval) + } + if s.Timeout != DefaultTimeout { + t.Errorf("Expected Timeout to be %v, got %v", DefaultTimeout, s.Timeout) + } + if s.InitialContext == nil { + t.Error("Expected InitialContext to be initialized, got nil") + } +} + +// Helper function to create bool pointers +func boolPtr(b bool) *bool { + return &b +} + +func TestStoreResultValues(t *testing.T) { + ctx := gontext.New(nil) + // Create a mock result + result := &endpoint.Result{ + HTTPStatus: 200, + IP: "192.168.1.1", + Duration: 100 * time.Millisecond, + Body: []byte(`{"status": "OK", "value": 42}`), + Connected: true, + } + // Define store mappings + mappings := map[string]string{ + "response_code": "[STATUS]", + "server_ip": "[IP]", + "response_time": "[RESPONSE_TIME]", + "status": "[BODY].status", + "value": "[BODY].value", + "connected": "[CONNECTED]", + } + // Store values + stored, err := StoreResultValues(ctx, mappings, result) + if err != nil { + t.Fatalf("Unexpected error storing values: %v", err) + } + // Verify stored values + if stored["response_code"] != int64(200) { + t.Errorf("Expected response_code=200, got %v", stored["response_code"]) + } + if stored["server_ip"] != "192.168.1.1" { + t.Errorf("Expected server_ip=192.168.1.1, got %v", stored["server_ip"]) + } + if stored["status"] != "OK" { + t.Errorf("Expected status=OK, got %v", stored["status"]) + } + if stored["value"] != int64(42) { // Now parsed as int64 for whole numbers + t.Errorf("Expected value=42, got %v", stored["value"]) + } + if stored["connected"] != true { + t.Errorf("Expected connected=true, got %v", stored["connected"]) + } + // Verify values are in context + val, err := ctx.Get("status") + if err != nil || val != "OK" { + t.Errorf("Expected status=OK in context, got %v, err=%v", val, err) + } +} + +func TestSuite_ExecuteWithAlwaysRunEndpoints(t *testing.T) { + suite := &Suite{ + Name: "test-suite", + Endpoints: []*endpoint.Endpoint{ + { + Name: "create-resource", + URL: "https://example.org", + Conditions: []endpoint.Condition{ + endpoint.Condition("[STATUS] == 200"), + }, + Store: map[string]string{ + "created_id": "[BODY]", + }, + }, + { + Name: "failing-endpoint", + URL: "https://example.org", + Conditions: []endpoint.Condition{ + endpoint.Condition("[STATUS] != 200"), // This will fail + }, + }, + { + Name: "cleanup-resource", + URL: "https://example.org", + Conditions: []endpoint.Condition{ + endpoint.Condition("[STATUS] == 200"), + }, + AlwaysRun: true, + }, + }, + } + if err := suite.ValidateAndSetDefaults(); err != nil { + t.Fatalf("suite validation failed: %v", err) + } + result := suite.Execute() + if result.Success { + t.Error("expected suite to fail due to middle endpoint failure") + } + if len(result.EndpointResults) != 3 { + t.Errorf("expected 3 endpoint results, got %d", len(result.EndpointResults)) + } + if result.EndpointResults[0].Name != "create-resource" { + t.Errorf("expected first endpoint to be 'create-resource', got '%s'", result.EndpointResults[0].Name) + } + if result.EndpointResults[1].Name != "failing-endpoint" { + t.Errorf("expected second endpoint to be 'failing-endpoint', got '%s'", result.EndpointResults[1].Name) + } + if result.EndpointResults[1].Success { + t.Error("expected failing-endpoint to fail") + } + if result.EndpointResults[2].Name != "cleanup-resource" { + t.Errorf("expected third endpoint to be 'cleanup-resource', got '%s'", result.EndpointResults[2].Name) + } + if !result.EndpointResults[2].Success { + t.Error("expected cleanup endpoint to succeed") + } +} + +func TestSuite_ExecuteWithoutAlwaysRunEndpoints(t *testing.T) { + suite := &Suite{ + Name: "test-suite", + Endpoints: []*endpoint.Endpoint{ + { + Name: "create-resource", + URL: "https://example.org", + Conditions: []endpoint.Condition{ + endpoint.Condition("[STATUS] == 200"), + }, + }, + { + Name: "failing-endpoint", + URL: "https://example.org", + Conditions: []endpoint.Condition{ + endpoint.Condition("[STATUS] != 200"), // This will fail + }, + }, + { + Name: "skipped-endpoint", + URL: "https://example.org", + Conditions: []endpoint.Condition{ + endpoint.Condition("[STATUS] == 200"), + }, + }, + }, + } + if err := suite.ValidateAndSetDefaults(); err != nil { + t.Fatalf("suite validation failed: %v", err) + } + result := suite.Execute() + if result.Success { + t.Error("expected suite to fail due to middle endpoint failure") + } + if len(result.EndpointResults) != 2 { + t.Errorf("expected 2 endpoint results (execution should stop after failure), got %d", len(result.EndpointResults)) + } + if result.EndpointResults[0].Name != "create-resource" { + t.Errorf("expected first endpoint to be 'create-resource', got '%s'", result.EndpointResults[0].Name) + } + if result.EndpointResults[1].Name != "failing-endpoint" { + t.Errorf("expected second endpoint to be 'failing-endpoint', got '%s'", result.EndpointResults[1].Name) + } +} + +func TestResult_AddError(t *testing.T) { + result := &Result{ + Name: "test-suite", + Timestamp: time.Now(), + } + if len(result.Errors) != 0 { + t.Errorf("Expected 0 errors initially, got %d", len(result.Errors)) + } + result.AddError("first error") + if len(result.Errors) != 1 { + t.Errorf("Expected 1 error after AddError, got %d", len(result.Errors)) + } + if result.Errors[0] != "first error" { + t.Errorf("Expected 'first error', got '%s'", result.Errors[0]) + } + result.AddError("second error") + if len(result.Errors) != 2 { + t.Errorf("Expected 2 errors after second AddError, got %d", len(result.Errors)) + } + if result.Errors[1] != "second error" { + t.Errorf("Expected 'second error', got '%s'", result.Errors[1]) + } +} + +func TestResult_CalculateSuccess(t *testing.T) { + tests := []struct { + name string + endpointResults []*endpoint.Result + errors []string + expectedSuccess bool + }{ + { + name: "no-endpoints-no-errors", + endpointResults: []*endpoint.Result{}, + errors: []string{}, + expectedSuccess: true, + }, + { + name: "all-endpoints-successful-no-errors", + endpointResults: []*endpoint.Result{ + {Success: true}, + {Success: true}, + }, + errors: []string{}, + expectedSuccess: true, + }, + { + name: "second-endpoint-failed-no-errors", + endpointResults: []*endpoint.Result{ + {Success: true}, + {Success: false}, + }, + errors: []string{}, + expectedSuccess: false, + }, + { + name: "first-endpoint-failed-no-errors", + endpointResults: []*endpoint.Result{ + {Success: false}, + {Success: true}, + }, + errors: []string{}, + expectedSuccess: false, + }, + { + name: "all-endpoints-successful-with-errors", + endpointResults: []*endpoint.Result{ + {Success: true}, + {Success: true}, + }, + errors: []string{"suite level error"}, + expectedSuccess: false, + }, + { + name: "endpoint-failed-and-errors", + endpointResults: []*endpoint.Result{ + {Success: true}, + {Success: false}, + }, + errors: []string{"suite level error"}, + expectedSuccess: false, + }, + { + name: "no-endpoints-with-errors", + endpointResults: []*endpoint.Result{}, + errors: []string{"configuration error"}, + expectedSuccess: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := &Result{ + Name: "test-suite", + Timestamp: time.Now(), + EndpointResults: tt.endpointResults, + Errors: tt.errors, + } + result.CalculateSuccess() + if result.Success != tt.expectedSuccess { + t.Errorf("Expected success=%v, got %v", tt.expectedSuccess, result.Success) + } + }) + } +} diff --git a/go.mod b/go.mod index 96cb36ee..b7df03b0 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( golang.org/x/crypto v0.40.0 golang.org/x/net v0.42.0 golang.org/x/oauth2 v0.30.0 + golang.org/x/sync v0.16.0 google.golang.org/api v0.242.0 gopkg.in/mail.v2 v2.3.1 gopkg.in/yaml.v3 v3.0.1 @@ -74,7 +75,6 @@ require ( golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/image v0.18.0 // indirect golang.org/x/mod v0.25.0 // indirect - golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.27.0 // indirect golang.org/x/tools v0.34.0 // indirect diff --git a/main.go b/main.go index b8d48394..4d7ac255 100644 --- a/main.go +++ b/main.go @@ -103,6 +103,15 @@ func initializeStorage(cfg *config.Config) { if err != nil { panic(err) } + // Remove all SuiteStatuses that represent suites which no longer exist in the configuration + var suiteKeys []string + for _, suite := range cfg.Suites { + suiteKeys = append(suiteKeys, suite.Key()) + } + numberOfSuiteStatusesDeleted := store.Get().DeleteAllSuiteStatusesNotInKeys(suiteKeys) + if numberOfSuiteStatusesDeleted > 0 { + logr.Infof("[main.initializeStorage] Deleted %d suite statuses because their matching suites no longer existed", numberOfSuiteStatusesDeleted) + } // Remove all EndpointStatus that represent endpoints which no longer exist in the configuration var keys []string for _, ep := range cfg.Endpoints { @@ -111,6 +120,13 @@ func initializeStorage(cfg *config.Config) { for _, ee := range cfg.ExternalEndpoints { keys = append(keys, ee.Key()) } + // Also add endpoints that are part of suites + for _, suite := range cfg.Suites { + for _, ep := range suite.Endpoints { + keys = append(keys, ep.Key()) + } + } + logr.Infof("[main.initializeStorage] Total endpoint keys to preserve: %d", len(keys)) numberOfEndpointStatusesDeleted := store.Get().DeleteAllEndpointStatusesNotInKeys(keys) if numberOfEndpointStatusesDeleted > 0 { logr.Infof("[main.initializeStorage] Deleted %d endpoint statuses because their matching endpoints no longer existed", numberOfEndpointStatusesDeleted) diff --git a/metrics/metrics.go b/metrics/metrics.go index 59febeb0..0cf5c9c8 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -5,6 +5,7 @@ import ( "github.com/TwiN/gatus/v5/config" "github.com/TwiN/gatus/v5/config/endpoint" + "github.com/TwiN/gatus/v5/config/suite" "github.com/prometheus/client_golang/prometheus" ) @@ -18,6 +19,11 @@ var ( resultCertificateExpirationSeconds *prometheus.GaugeVec resultEndpointSuccess *prometheus.GaugeVec + // Suite metrics + suiteResultTotal *prometheus.CounterVec + suiteResultDurationSeconds *prometheus.GaugeVec + suiteResultSuccess *prometheus.GaugeVec + // Track if metrics have been initialized to prevent duplicate registration metricsInitialized bool currentRegisterer prometheus.Registerer @@ -49,6 +55,17 @@ func UnregisterPrometheusMetrics() { currentRegisterer.Unregister(resultEndpointSuccess) } + // Unregister suite metrics + if suiteResultTotal != nil { + currentRegisterer.Unregister(suiteResultTotal) + } + if suiteResultDurationSeconds != nil { + currentRegisterer.Unregister(suiteResultDurationSeconds) + } + if suiteResultSuccess != nil { + currentRegisterer.Unregister(suiteResultSuccess) + } + metricsInitialized = false currentRegisterer = nil } @@ -109,6 +126,28 @@ func InitializePrometheusMetrics(cfg *config.Config, reg prometheus.Registerer) }, append([]string{"key", "group", "name", "type"}, extraLabels...)) reg.MustRegister(resultEndpointSuccess) + // Suite metrics + suiteResultTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: namespace, + Name: "suite_results_total", + Help: "Total number of suite executions", + }, append([]string{"key", "group", "name", "success"}, extraLabels...)) + reg.MustRegister(suiteResultTotal) + + suiteResultDurationSeconds = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "suite_results_duration_seconds", + Help: "Duration of suite execution in seconds", + }, append([]string{"key", "group", "name"}, extraLabels...)) + reg.MustRegister(suiteResultDurationSeconds) + + suiteResultSuccess = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "suite_results_success", + Help: "Whether the suite execution was successful (1) or not (0)", + }, append([]string{"key", "group", "name"}, extraLabels...)) + reg.MustRegister(suiteResultSuccess) + // Mark as initialized metricsInitialized = true } @@ -116,7 +155,7 @@ func InitializePrometheusMetrics(cfg *config.Config, reg prometheus.Registerer) // PublishMetricsForEndpoint publishes metrics for the given endpoint and its result. // These metrics will be exposed at /metrics if the metrics are enabled func PublishMetricsForEndpoint(ep *endpoint.Endpoint, result *endpoint.Result, extraLabels []string) { - labelValues := []string{} + var labelValues []string for _, label := range extraLabels { if value, ok := ep.ExtraLabels[label]; ok { labelValues = append(labelValues, value) @@ -124,7 +163,6 @@ func PublishMetricsForEndpoint(ep *endpoint.Endpoint, result *endpoint.Result, e labelValues = append(labelValues, "") } } - endpointType := ep.Type() resultTotal.WithLabelValues(append([]string{ep.Key(), ep.Group, ep.Name, string(endpointType), strconv.FormatBool(result.Success)}, labelValues...)...).Inc() resultDurationSeconds.WithLabelValues(append([]string{ep.Key(), ep.Group, ep.Name, string(endpointType)}, labelValues...)...).Set(result.Duration.Seconds()) @@ -146,3 +184,35 @@ func PublishMetricsForEndpoint(ep *endpoint.Endpoint, result *endpoint.Result, e resultEndpointSuccess.WithLabelValues(append([]string{ep.Key(), ep.Group, ep.Name, string(endpointType)}, labelValues...)...).Set(0) } } + +// PublishMetricsForSuite publishes metrics for the given suite and its result. +// These metrics will be exposed at /metrics if the metrics are enabled +func PublishMetricsForSuite(s *suite.Suite, result *suite.Result, extraLabels []string) { + if !metricsInitialized { + return + } + var labelValues []string + // For now, suites don't have ExtraLabels, so we'll use empty values + // This maintains consistency with endpoint metrics structure + for range extraLabels { + labelValues = append(labelValues, "") + } + // Publish suite execution counter + suiteResultTotal.WithLabelValues( + append([]string{s.Key(), s.Group, s.Name, strconv.FormatBool(result.Success)}, labelValues...)..., + ).Inc() + // Publish suite duration + suiteResultDurationSeconds.WithLabelValues( + append([]string{s.Key(), s.Group, s.Name}, labelValues...)..., + ).Set(result.Duration.Seconds()) + // Publish suite success status + if result.Success { + suiteResultSuccess.WithLabelValues( + append([]string{s.Key(), s.Group, s.Name}, labelValues...)..., + ).Set(1) + } else { + suiteResultSuccess.WithLabelValues( + append([]string{s.Key(), s.Group, s.Name}, labelValues...)..., + ).Set(0) + } +} diff --git a/metrics/metrics_test.go b/metrics/metrics_test.go index f5eff0b3..a6d57a76 100644 --- a/metrics/metrics_test.go +++ b/metrics/metrics_test.go @@ -8,6 +8,7 @@ import ( "github.com/TwiN/gatus/v5/config" "github.com/TwiN/gatus/v5/config/endpoint" "github.com/TwiN/gatus/v5/config/endpoint/dns" + "github.com/TwiN/gatus/v5/config/suite" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" ) @@ -226,3 +227,93 @@ gatus_results_endpoint_success{group="http-ep-group",key="http-ep-group_http-ep- t.Errorf("Expected no errors but got: %v", err) } } + +func TestPublishMetricsForSuite(t *testing.T) { + reg := prometheus.NewRegistry() + InitializePrometheusMetrics(&config.Config{}, reg) + + testSuite := &suite.Suite{ + Name: "test-suite", + Group: "test-group", + } + // Test successful suite execution + successResult := &suite.Result{ + Success: true, + Duration: 5 * time.Second, + Name: "test-suite", + Group: "test-group", + } + PublishMetricsForSuite(testSuite, successResult, []string{}) + + err := testutil.GatherAndCompare(reg, bytes.NewBufferString(` +# HELP gatus_suite_results_duration_seconds Duration of suite execution in seconds +# TYPE gatus_suite_results_duration_seconds gauge +gatus_suite_results_duration_seconds{group="test-group",key="test-group_test-suite",name="test-suite"} 5 +# HELP gatus_suite_results_success Whether the suite execution was successful (1) or not (0) +# TYPE gatus_suite_results_success gauge +gatus_suite_results_success{group="test-group",key="test-group_test-suite",name="test-suite"} 1 +# HELP gatus_suite_results_total Total number of suite executions +# TYPE gatus_suite_results_total counter +gatus_suite_results_total{group="test-group",key="test-group_test-suite",name="test-suite",success="true"} 1 +`), "gatus_suite_results_duration_seconds", "gatus_suite_results_success", "gatus_suite_results_total") + if err != nil { + t.Errorf("Expected no errors but got: %v", err) + } + + // Test failed suite execution + failureResult := &suite.Result{ + Success: false, + Duration: 10 * time.Second, + Name: "test-suite", + Group: "test-group", + } + PublishMetricsForSuite(testSuite, failureResult, []string{}) + + err = testutil.GatherAndCompare(reg, bytes.NewBufferString(` +# HELP gatus_suite_results_duration_seconds Duration of suite execution in seconds +# TYPE gatus_suite_results_duration_seconds gauge +gatus_suite_results_duration_seconds{group="test-group",key="test-group_test-suite",name="test-suite"} 10 +# HELP gatus_suite_results_success Whether the suite execution was successful (1) or not (0) +# TYPE gatus_suite_results_success gauge +gatus_suite_results_success{group="test-group",key="test-group_test-suite",name="test-suite"} 0 +# HELP gatus_suite_results_total Total number of suite executions +# TYPE gatus_suite_results_total counter +gatus_suite_results_total{group="test-group",key="test-group_test-suite",name="test-suite",success="false"} 1 +gatus_suite_results_total{group="test-group",key="test-group_test-suite",name="test-suite",success="true"} 1 +`), "gatus_suite_results_duration_seconds", "gatus_suite_results_success", "gatus_suite_results_total") + if err != nil { + t.Errorf("Expected no errors but got: %v", err) + } +} + +func TestPublishMetricsForSuite_NoGroup(t *testing.T) { + reg := prometheus.NewRegistry() + InitializePrometheusMetrics(&config.Config{}, reg) + + testSuite := &suite.Suite{ + Name: "no-group-suite", + Group: "", + } + result := &suite.Result{ + Success: true, + Duration: 3 * time.Second, + Name: "no-group-suite", + Group: "", + } + PublishMetricsForSuite(testSuite, result, []string{}) + + err := testutil.GatherAndCompare(reg, bytes.NewBufferString(` +# HELP gatus_suite_results_duration_seconds Duration of suite execution in seconds +# TYPE gatus_suite_results_duration_seconds gauge +gatus_suite_results_duration_seconds{group="",key="_no-group-suite",name="no-group-suite"} 3 +# HELP gatus_suite_results_success Whether the suite execution was successful (1) or not (0) +# TYPE gatus_suite_results_success gauge +gatus_suite_results_success{group="",key="_no-group-suite",name="no-group-suite"} 1 +# HELP gatus_suite_results_total Total number of suite executions +# TYPE gatus_suite_results_total counter +gatus_suite_results_total{group="",key="_no-group-suite",name="no-group-suite",success="true"} 1 +`), "gatus_suite_results_duration_seconds", "gatus_suite_results_success", "gatus_suite_results_total") + if err != nil { + t.Errorf("Expected no errors but got: %v", err) + } +} diff --git a/storage/store/common/errors.go b/storage/store/common/errors.go index 77560f93..8cf453b3 100644 --- a/storage/store/common/errors.go +++ b/storage/store/common/errors.go @@ -4,5 +4,6 @@ import "errors" var ( ErrEndpointNotFound = errors.New("endpoint not found") // When an endpoint does not exist in the store + ErrSuiteNotFound = errors.New("suite not found") // When a suite does not exist in the store ErrInvalidTimeRange = errors.New("'from' cannot be older than 'to'") // When an invalid time range is provided ) diff --git a/storage/store/common/paging/paging.go b/storage/store/common/paging/endpoint_status_params.go similarity index 100% rename from storage/store/common/paging/paging.go rename to storage/store/common/paging/endpoint_status_params.go diff --git a/storage/store/common/paging/paging_test.go b/storage/store/common/paging/endpoint_status_params_test.go similarity index 100% rename from storage/store/common/paging/paging_test.go rename to storage/store/common/paging/endpoint_status_params_test.go diff --git a/storage/store/common/paging/suite_status_params.go b/storage/store/common/paging/suite_status_params.go new file mode 100644 index 00000000..cbabca35 --- /dev/null +++ b/storage/store/common/paging/suite_status_params.go @@ -0,0 +1,22 @@ +package paging + +// SuiteStatusParams represents the parameters for suite status queries +type SuiteStatusParams struct { + Page int // Page number + PageSize int // Number of results per page +} + +// NewSuiteStatusParams creates a new SuiteStatusParams +func NewSuiteStatusParams() *SuiteStatusParams { + return &SuiteStatusParams{ + Page: 1, + PageSize: 20, + } +} + +// WithPagination sets the page and page size +func (params *SuiteStatusParams) WithPagination(page, pageSize int) *SuiteStatusParams { + params.Page = page + params.PageSize = pageSize + return params +} \ No newline at end of file diff --git a/storage/store/common/paging/suite_status_params_test.go b/storage/store/common/paging/suite_status_params_test.go new file mode 100644 index 00000000..07bca069 --- /dev/null +++ b/storage/store/common/paging/suite_status_params_test.go @@ -0,0 +1,124 @@ +package paging + +import ( + "testing" +) + +func TestNewSuiteStatusParams(t *testing.T) { + params := NewSuiteStatusParams() + if params == nil { + t.Fatal("NewSuiteStatusParams should not return nil") + } + if params.Page != 1 { + t.Errorf("expected default Page to be 1, got %d", params.Page) + } + if params.PageSize != 20 { + t.Errorf("expected default PageSize to be 20, got %d", params.PageSize) + } +} + +func TestSuiteStatusParams_WithPagination(t *testing.T) { + tests := []struct { + name string + page int + pageSize int + expectedPage int + expectedSize int + }{ + { + name: "valid pagination", + page: 2, + pageSize: 50, + expectedPage: 2, + expectedSize: 50, + }, + { + name: "zero page", + page: 0, + pageSize: 10, + expectedPage: 0, + expectedSize: 10, + }, + { + name: "negative page", + page: -1, + pageSize: 20, + expectedPage: -1, + expectedSize: 20, + }, + { + name: "zero page size", + page: 1, + pageSize: 0, + expectedPage: 1, + expectedSize: 0, + }, + { + name: "negative page size", + page: 1, + pageSize: -10, + expectedPage: 1, + expectedSize: -10, + }, + { + name: "large values", + page: 1000, + pageSize: 10000, + expectedPage: 1000, + expectedSize: 10000, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := NewSuiteStatusParams().WithPagination(tt.page, tt.pageSize) + if params.Page != tt.expectedPage { + t.Errorf("expected Page to be %d, got %d", tt.expectedPage, params.Page) + } + if params.PageSize != tt.expectedSize { + t.Errorf("expected PageSize to be %d, got %d", tt.expectedSize, params.PageSize) + } + }) + } +} + +func TestSuiteStatusParams_ChainedMethods(t *testing.T) { + params := NewSuiteStatusParams(). + WithPagination(3, 100) + + if params.Page != 3 { + t.Errorf("expected Page to be 3, got %d", params.Page) + } + if params.PageSize != 100 { + t.Errorf("expected PageSize to be 100, got %d", params.PageSize) + } +} + +func TestSuiteStatusParams_OverwritePagination(t *testing.T) { + params := NewSuiteStatusParams() + + // Set initial pagination + params.WithPagination(2, 50) + if params.Page != 2 || params.PageSize != 50 { + t.Error("initial pagination not set correctly") + } + + // Overwrite pagination + params.WithPagination(5, 200) + if params.Page != 5 { + t.Errorf("expected Page to be overwritten to 5, got %d", params.Page) + } + if params.PageSize != 200 { + t.Errorf("expected PageSize to be overwritten to 200, got %d", params.PageSize) + } +} + +func TestSuiteStatusParams_ReturnsSelf(t *testing.T) { + params := NewSuiteStatusParams() + + // Verify WithPagination returns the same instance + result := params.WithPagination(1, 20) + if result != params { + t.Error("WithPagination should return the same instance for method chaining") + } +} \ No newline at end of file diff --git a/storage/store/memory/memory.go b/storage/store/memory/memory.go index e47b9ee8..e88c8c77 100644 --- a/storage/store/memory/memory.go +++ b/storage/store/memory/memory.go @@ -7,16 +7,20 @@ import ( "github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/config/endpoint" + "github.com/TwiN/gatus/v5/config/key" + "github.com/TwiN/gatus/v5/config/suite" "github.com/TwiN/gatus/v5/storage/store/common" "github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gocache/v2" + "github.com/TwiN/logr" ) // Store that leverages gocache type Store struct { sync.RWMutex - cache *gocache.Cache + endpointCache *gocache.Cache // Cache for endpoint statuses + suiteCache *gocache.Cache // Cache for suite statuses maximumNumberOfResults int // maximum number of results that an endpoint can have maximumNumberOfEvents int // maximum number of events that an endpoint can have @@ -28,7 +32,8 @@ type Store struct { // supports eventual persistence. func NewStore(maximumNumberOfResults, maximumNumberOfEvents int) (*Store, error) { store := &Store{ - cache: gocache.NewCache().WithMaxSize(gocache.NoMaxSize), + endpointCache: gocache.NewCache().WithMaxSize(gocache.NoMaxSize), + suiteCache: gocache.NewCache().WithMaxSize(gocache.NoMaxSize), maximumNumberOfResults: maximumNumberOfResults, maximumNumberOfEvents: maximumNumberOfEvents, } @@ -38,10 +43,12 @@ func NewStore(maximumNumberOfResults, maximumNumberOfEvents int) (*Store, error) // GetAllEndpointStatuses returns all monitored endpoint.Status // with a subset of endpoint.Result defined by the page and pageSize parameters func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*endpoint.Status, error) { - endpointStatuses := s.cache.GetAll() - pagedEndpointStatuses := make([]*endpoint.Status, 0, len(endpointStatuses)) - for _, v := range endpointStatuses { - pagedEndpointStatuses = append(pagedEndpointStatuses, ShallowCopyEndpointStatus(v.(*endpoint.Status), params)) + allStatuses := s.endpointCache.GetAll() + pagedEndpointStatuses := make([]*endpoint.Status, 0, len(allStatuses)) + for _, v := range allStatuses { + if status, ok := v.(*endpoint.Status); ok { + pagedEndpointStatuses = append(pagedEndpointStatuses, ShallowCopyEndpointStatus(status, params)) + } } sort.Slice(pagedEndpointStatuses, func(i, j int) bool { return pagedEndpointStatuses[i].Key < pagedEndpointStatuses[j].Key @@ -49,26 +56,53 @@ func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]* return pagedEndpointStatuses, nil } +// GetAllSuiteStatuses returns all monitored suite.Status +func (s *Store) GetAllSuiteStatuses(params *paging.SuiteStatusParams) ([]*suite.Status, error) { + s.RLock() + defer s.RUnlock() + suiteStatuses := make([]*suite.Status, 0) + for _, v := range s.suiteCache.GetAll() { + if status, ok := v.(*suite.Status); ok { + suiteStatuses = append(suiteStatuses, ShallowCopySuiteStatus(status, params)) + } + } + sort.Slice(suiteStatuses, func(i, j int) bool { + return suiteStatuses[i].Key < suiteStatuses[j].Key + }) + return suiteStatuses, nil +} + // GetEndpointStatus returns the endpoint status for a given endpoint name in the given group func (s *Store) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*endpoint.Status, error) { - return s.GetEndpointStatusByKey(endpoint.ConvertGroupAndEndpointNameToKey(groupName, endpointName), params) + return s.GetEndpointStatusByKey(key.ConvertGroupAndNameToKey(groupName, endpointName), params) } // GetEndpointStatusByKey returns the endpoint status for a given key func (s *Store) GetEndpointStatusByKey(key string, params *paging.EndpointStatusParams) (*endpoint.Status, error) { - endpointStatus := s.cache.GetValue(key) + endpointStatus := s.endpointCache.GetValue(key) if endpointStatus == nil { return nil, common.ErrEndpointNotFound } return ShallowCopyEndpointStatus(endpointStatus.(*endpoint.Status), params), nil } +// GetSuiteStatusByKey returns the suite status for a given key +func (s *Store) GetSuiteStatusByKey(key string, params *paging.SuiteStatusParams) (*suite.Status, error) { + s.RLock() + defer s.RUnlock() + suiteStatus := s.suiteCache.GetValue(key) + if suiteStatus == nil { + return nil, common.ErrSuiteNotFound + } + return ShallowCopySuiteStatus(suiteStatus.(*suite.Status), params), nil +} + // GetUptimeByKey returns the uptime percentage during a time range func (s *Store) GetUptimeByKey(key string, from, to time.Time) (float64, error) { if from.After(to) { return 0, common.ErrInvalidTimeRange } - endpointStatus := s.cache.GetValue(key) + endpointStatus := s.endpointCache.GetValue(key) if endpointStatus == nil || endpointStatus.(*endpoint.Status).Uptime == nil { return 0, common.ErrEndpointNotFound } @@ -97,7 +131,7 @@ func (s *Store) GetAverageResponseTimeByKey(key string, from, to time.Time) (int if from.After(to) { return 0, common.ErrInvalidTimeRange } - endpointStatus := s.cache.GetValue(key) + endpointStatus := s.endpointCache.GetValue(key) if endpointStatus == nil || endpointStatus.(*endpoint.Status).Uptime == nil { return 0, common.ErrEndpointNotFound } @@ -125,7 +159,7 @@ func (s *Store) GetHourlyAverageResponseTimeByKey(key string, from, to time.Time if from.After(to) { return nil, common.ErrInvalidTimeRange } - endpointStatus := s.cache.GetValue(key) + endpointStatus := s.endpointCache.GetValue(key) if endpointStatus == nil || endpointStatus.(*endpoint.Status).Uptime == nil { return nil, common.ErrEndpointNotFound } @@ -144,11 +178,11 @@ func (s *Store) GetHourlyAverageResponseTimeByKey(key string, from, to time.Time return hourlyAverageResponseTimes, nil } -// Insert adds the observed result for the specified endpoint into the store -func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error { - key := ep.Key() +// InsertEndpointResult adds the observed result for the specified endpoint into the store +func (s *Store) InsertEndpointResult(ep *endpoint.Endpoint, result *endpoint.Result) error { + endpointKey := ep.Key() s.Lock() - status, exists := s.cache.Get(key) + status, exists := s.endpointCache.Get(endpointKey) if !exists { status = endpoint.NewStatus(ep.Group, ep.Name) status.(*endpoint.Status).Events = append(status.(*endpoint.Status).Events, &endpoint.Event{ @@ -157,18 +191,45 @@ func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error { }) } AddResult(status.(*endpoint.Status), result, s.maximumNumberOfResults, s.maximumNumberOfEvents) - s.cache.Set(key, status) + s.endpointCache.Set(endpointKey, status) s.Unlock() return nil } +// InsertSuiteResult adds the observed result for the specified suite into the store +func (s *Store) InsertSuiteResult(su *suite.Suite, result *suite.Result) error { + s.Lock() + defer s.Unlock() + suiteKey := su.Key() + suiteStatus := s.suiteCache.GetValue(suiteKey) + if suiteStatus == nil { + suiteStatus = &suite.Status{ + Name: su.Name, + Group: su.Group, + Key: su.Key(), + Results: []*suite.Result{}, + } + logr.Debugf("[memory.InsertSuiteResult] Created new suite status for suiteKey=%s", suiteKey) + } + status := suiteStatus.(*suite.Status) + // Add the new result at the end (append like endpoint implementation) + status.Results = append(status.Results, result) + // Keep only the maximum number of results + if len(status.Results) > s.maximumNumberOfResults { + status.Results = status.Results[len(status.Results)-s.maximumNumberOfResults:] + } + s.suiteCache.Set(suiteKey, status) + logr.Debugf("[memory.InsertSuiteResult] Stored suite result for suiteKey=%s, total results=%d", suiteKey, len(status.Results)) + return nil +} + // DeleteAllEndpointStatusesNotInKeys removes all Status that are not within the keys provided func (s *Store) DeleteAllEndpointStatusesNotInKeys(keys []string) int { var keysToDelete []string - for _, existingKey := range s.cache.GetKeysByPattern("*", 0) { + for _, existingKey := range s.endpointCache.GetKeysByPattern("*", 0) { shouldDelete := true - for _, key := range keys { - if existingKey == key { + for _, k := range keys { + if existingKey == k { shouldDelete = false break } @@ -177,7 +238,24 @@ func (s *Store) DeleteAllEndpointStatusesNotInKeys(keys []string) int { keysToDelete = append(keysToDelete, existingKey) } } - return s.cache.DeleteAll(keysToDelete) + return s.endpointCache.DeleteAll(keysToDelete) +} + +// DeleteAllSuiteStatusesNotInKeys removes all suite statuses that are not within the keys provided +func (s *Store) DeleteAllSuiteStatusesNotInKeys(keys []string) int { + s.Lock() + defer s.Unlock() + keysToKeep := make(map[string]bool, len(keys)) + for _, k := range keys { + keysToKeep[k] = true + } + var keysToDelete []string + for existingKey := range s.suiteCache.GetAll() { + if !keysToKeep[existingKey] { + keysToDelete = append(keysToDelete, existingKey) + } + } + return s.suiteCache.DeleteAll(keysToDelete) } // GetTriggeredEndpointAlert returns whether the triggered alert for the specified endpoint as well as the necessary information to resolve it @@ -215,12 +293,16 @@ func (s *Store) DeleteAllTriggeredAlertsNotInChecksumsByEndpoint(ep *endpoint.En func (s *Store) HasEndpointStatusNewerThan(key string, timestamp time.Time) (bool, error) { s.RLock() defer s.RUnlock() - endpointStatus := s.cache.GetValue(key) + endpointStatus := s.endpointCache.GetValue(key) if endpointStatus == nil { // If no endpoint exists, there's no newer status, so return false instead of an error return false, nil } - for _, result := range endpointStatus.(*endpoint.Status).Results { + status, ok := endpointStatus.(*endpoint.Status) + if !ok { + return false, nil + } + for _, result := range status.Results { if result.Timestamp.After(timestamp) { return true, nil } @@ -230,7 +312,8 @@ func (s *Store) HasEndpointStatusNewerThan(key string, timestamp time.Time) (boo // Clear deletes everything from the store func (s *Store) Clear() { - s.cache.Clear() + s.endpointCache.Clear() + s.suiteCache.Clear() } // Save persists the cache to the store file diff --git a/storage/store/memory/memory_test.go b/storage/store/memory/memory_test.go index 2f21528d..78294895 100644 --- a/storage/store/memory/memory_test.go +++ b/storage/store/memory/memory_test.go @@ -1,10 +1,12 @@ package memory import ( + "sync" "testing" "time" "github.com/TwiN/gatus/v5/config/endpoint" + "github.com/TwiN/gatus/v5/config/suite" "github.com/TwiN/gatus/v5/storage" "github.com/TwiN/gatus/v5/storage/store/common/paging" ) @@ -86,12 +88,12 @@ func TestStore_SanityCheck(t *testing.T) { store, _ := NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents) defer store.Clear() defer store.Close() - store.Insert(&testEndpoint, &testSuccessfulResult) + store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult) endpointStatuses, _ := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams()) if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 1 { t.Fatalf("expected 1 EndpointStatus, got %d", numberOfEndpointStatuses) } - store.Insert(&testEndpoint, &testUnsuccessfulResult) + store.InsertEndpointResult(&testEndpoint, &testUnsuccessfulResult) // Both results inserted are for the same endpoint, therefore, the count shouldn't have increased endpointStatuses, _ = store.GetAllEndpointStatuses(paging.NewEndpointStatusParams()) if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 1 { @@ -140,8 +142,8 @@ func TestStore_HasEndpointStatusNewerThan(t *testing.T) { store, _ := NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents) defer store.Clear() defer store.Close() - // Insert a result - err := store.Insert(&testEndpoint, &testSuccessfulResult) + // InsertEndpointResult a result + err := store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult) if err != nil { t.Fatalf("expected no error while inserting result, got %v", err) } @@ -162,3 +164,931 @@ func TestStore_HasEndpointStatusNewerThan(t *testing.T) { t.Fatal("expected not to have a newer status, but did") } } + +// TestStore_MixedEndpointsAndSuites tests that having both endpoints and suites in the cache +// doesn't cause issues with core operations +func TestStore_MixedEndpointsAndSuites(t *testing.T) { + // Helper function to create and populate a store with test data + setupStore := func(t *testing.T) (*Store, *endpoint.Endpoint, *endpoint.Endpoint, *endpoint.Endpoint, *endpoint.Endpoint, *suite.Suite) { + store, err := NewStore(100, 50) + if err != nil { + t.Fatal("expected no error, got", err) + } + + // Create regular endpoints + endpoint1 := &endpoint.Endpoint{ + Name: "endpoint1", + Group: "group1", + URL: "https://example.com/1", + } + endpoint2 := &endpoint.Endpoint{ + Name: "endpoint2", + Group: "group2", + URL: "https://example.com/2", + } + + // Create suite endpoints (these would be part of a suite) + suiteEndpoint1 := &endpoint.Endpoint{ + Name: "suite-endpoint1", + Group: "suite-group", + URL: "https://example.com/suite1", + } + suiteEndpoint2 := &endpoint.Endpoint{ + Name: "suite-endpoint2", + Group: "suite-group", + URL: "https://example.com/suite2", + } + + // Create a suite + testSuite := &suite.Suite{ + Name: "test-suite", + Group: "suite-group", + Endpoints: []*endpoint.Endpoint{ + suiteEndpoint1, + suiteEndpoint2, + }, + } + + return store, endpoint1, endpoint2, suiteEndpoint1, suiteEndpoint2, testSuite + } + + // Test 1: InsertEndpointResult endpoint results + t.Run("InsertEndpointResults", func(t *testing.T) { + store, endpoint1, endpoint2, suiteEndpoint1, suiteEndpoint2, _ := setupStore(t) + // InsertEndpointResult regular endpoint results + result1 := &endpoint.Result{ + Success: true, + Timestamp: time.Now(), + Duration: 100 * time.Millisecond, + } + if err := store.InsertEndpointResult(endpoint1, result1); err != nil { + t.Fatalf("failed to insert endpoint1 result: %v", err) + } + + result2 := &endpoint.Result{ + Success: false, + Timestamp: time.Now(), + Duration: 200 * time.Millisecond, + Errors: []string{"error"}, + } + if err := store.InsertEndpointResult(endpoint2, result2); err != nil { + t.Fatalf("failed to insert endpoint2 result: %v", err) + } + + // InsertEndpointResult suite endpoint results + suiteResult1 := &endpoint.Result{ + Success: true, + Timestamp: time.Now(), + Duration: 50 * time.Millisecond, + } + if err := store.InsertEndpointResult(suiteEndpoint1, suiteResult1); err != nil { + t.Fatalf("failed to insert suite endpoint1 result: %v", err) + } + + suiteResult2 := &endpoint.Result{ + Success: true, + Timestamp: time.Now(), + Duration: 75 * time.Millisecond, + } + if err := store.InsertEndpointResult(suiteEndpoint2, suiteResult2); err != nil { + t.Fatalf("failed to insert suite endpoint2 result: %v", err) + } + }) + + // Test 2: InsertEndpointResult suite result + t.Run("InsertSuiteResult", func(t *testing.T) { + store, _, _, _, _, testSuite := setupStore(t) + timestamp := time.Now() + suiteResult := &suite.Result{ + Name: testSuite.Name, + Group: testSuite.Group, + Success: true, + Timestamp: timestamp, + Duration: 125 * time.Millisecond, + EndpointResults: []*endpoint.Result{ + {Success: true, Duration: 50 * time.Millisecond}, + {Success: true, Duration: 75 * time.Millisecond}, + }, + } + if err := store.InsertSuiteResult(testSuite, suiteResult); err != nil { + t.Fatalf("failed to insert suite result: %v", err) + } + + // Verify the suite result was stored correctly + status, err := store.GetSuiteStatusByKey(testSuite.Key(), nil) + if err != nil { + t.Fatalf("failed to get suite status: %v", err) + } + if len(status.Results) != 1 { + t.Errorf("expected 1 suite result, got %d", len(status.Results)) + } + + stored := status.Results[0] + if stored.Name != testSuite.Name { + t.Errorf("expected result name %s, got %s", testSuite.Name, stored.Name) + } + if stored.Group != testSuite.Group { + t.Errorf("expected result group %s, got %s", testSuite.Group, stored.Group) + } + if !stored.Success { + t.Error("expected result to be successful") + } + if stored.Duration != 125*time.Millisecond { + t.Errorf("expected duration 125ms, got %v", stored.Duration) + } + if len(stored.EndpointResults) != 2 { + t.Errorf("expected 2 endpoint results, got %d", len(stored.EndpointResults)) + } + }) + + // Test 3: GetAllEndpointStatuses should only return endpoints, not suites + t.Run("GetAllEndpointStatuses", func(t *testing.T) { + store, endpoint1, endpoint2, suiteEndpoint1, suiteEndpoint2, testSuite := setupStore(t) + + // InsertEndpointResult all test data + store.InsertEndpointResult(endpoint1, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 100 * time.Millisecond}) + store.InsertEndpointResult(endpoint2, &endpoint.Result{Success: false, Timestamp: time.Now(), Duration: 200 * time.Millisecond}) + store.InsertEndpointResult(suiteEndpoint1, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 50 * time.Millisecond}) + store.InsertEndpointResult(suiteEndpoint2, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 75 * time.Millisecond}) + store.InsertSuiteResult(testSuite, &suite.Result{ + Name: testSuite.Name, Group: testSuite.Group, Success: true, + Timestamp: time.Now(), Duration: 125 * time.Millisecond, + }) + statuses, err := store.GetAllEndpointStatuses(&paging.EndpointStatusParams{}) + if err != nil { + t.Fatalf("failed to get all endpoint statuses: %v", err) + } + + // Should have 4 endpoints (2 regular + 2 suite endpoints) + if len(statuses) != 4 { + t.Errorf("expected 4 endpoint statuses, got %d", len(statuses)) + } + + // Verify all are endpoint statuses with correct data, not suite statuses + expectedEndpoints := map[string]struct { + success bool + duration time.Duration + }{ + "endpoint1": {success: true, duration: 100 * time.Millisecond}, + "endpoint2": {success: false, duration: 200 * time.Millisecond}, + "suite-endpoint1": {success: true, duration: 50 * time.Millisecond}, + "suite-endpoint2": {success: true, duration: 75 * time.Millisecond}, + } + + for _, status := range statuses { + if status.Name == "" { + t.Error("endpoint status should have a name") + } + // Make sure none of them are the suite itself + if status.Name == "test-suite" { + t.Error("suite should not appear in endpoint statuses") + } + + // Verify detailed endpoint data + expected, exists := expectedEndpoints[status.Name] + if !exists { + t.Errorf("unexpected endpoint name: %s", status.Name) + continue + } + + // Check that endpoint has results and verify the data + if len(status.Results) != 1 { + t.Errorf("endpoint %s should have 1 result, got %d", status.Name, len(status.Results)) + continue + } + + result := status.Results[0] + if result.Success != expected.success { + t.Errorf("endpoint %s result success should be %v, got %v", status.Name, expected.success, result.Success) + } + if result.Duration != expected.duration { + t.Errorf("endpoint %s result duration should be %v, got %v", status.Name, expected.duration, result.Duration) + } + + delete(expectedEndpoints, status.Name) + } + if len(expectedEndpoints) > 0 { + t.Errorf("missing expected endpoints: %v", expectedEndpoints) + } + }) + + // Test 4: GetAllSuiteStatuses should only return suites, not endpoints + t.Run("GetAllSuiteStatuses", func(t *testing.T) { + store, endpoint1, _, _, _, testSuite := setupStore(t) + + // InsertEndpointResult test data + store.InsertEndpointResult(endpoint1, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 100 * time.Millisecond}) + timestamp := time.Now() + store.InsertSuiteResult(testSuite, &suite.Result{ + Name: testSuite.Name, Group: testSuite.Group, Success: true, + Timestamp: timestamp, Duration: 125 * time.Millisecond, + }) + statuses, err := store.GetAllSuiteStatuses(&paging.SuiteStatusParams{}) + if err != nil { + t.Fatalf("failed to get all suite statuses: %v", err) + } + + // Should have 1 suite + if len(statuses) != 1 { + t.Errorf("expected 1 suite status, got %d", len(statuses)) + } + + if len(statuses) > 0 { + suiteStatus := statuses[0] + if suiteStatus.Name != "test-suite" { + t.Errorf("expected suite name 'test-suite', got '%s'", suiteStatus.Name) + } + if suiteStatus.Group != "suite-group" { + t.Errorf("expected suite group 'suite-group', got '%s'", suiteStatus.Group) + } + if len(suiteStatus.Results) != 1 { + t.Errorf("expected 1 suite result, got %d", len(suiteStatus.Results)) + } + if len(suiteStatus.Results) > 0 { + result := suiteStatus.Results[0] + if !result.Success { + t.Error("expected suite result to be successful") + } + if result.Duration != 125*time.Millisecond { + t.Errorf("expected suite result duration 125ms, got %v", result.Duration) + } + } + } + }) + + // Test 5: GetEndpointStatusByKey should work for all endpoints + t.Run("GetEndpointStatusByKey", func(t *testing.T) { + store, endpoint1, _, suiteEndpoint1, _, _ := setupStore(t) + + // InsertEndpointResult test data with specific timestamps and durations + timestamp1 := time.Now() + timestamp2 := time.Now().Add(1 * time.Hour) + store.InsertEndpointResult(endpoint1, &endpoint.Result{Success: true, Timestamp: timestamp1, Duration: 100 * time.Millisecond}) + store.InsertEndpointResult(suiteEndpoint1, &endpoint.Result{Success: false, Timestamp: timestamp2, Duration: 50 * time.Millisecond, Errors: []string{"suite error"}}) + + // Test regular endpoints + status1, err := store.GetEndpointStatusByKey(endpoint1.Key(), &paging.EndpointStatusParams{}) + if err != nil { + t.Fatalf("failed to get endpoint1 status: %v", err) + } + if status1.Name != "endpoint1" { + t.Errorf("expected endpoint1, got %s", status1.Name) + } + if status1.Group != "group1" { + t.Errorf("expected group1, got %s", status1.Group) + } + if len(status1.Results) != 1 { + t.Errorf("expected 1 result for endpoint1, got %d", len(status1.Results)) + } + if len(status1.Results) > 0 { + result := status1.Results[0] + if !result.Success { + t.Error("expected endpoint1 result to be successful") + } + if result.Duration != 100*time.Millisecond { + t.Errorf("expected endpoint1 result duration 100ms, got %v", result.Duration) + } + } + + // Test suite endpoints + suiteStatus1, err := store.GetEndpointStatusByKey(suiteEndpoint1.Key(), &paging.EndpointStatusParams{}) + if err != nil { + t.Fatalf("failed to get suite endpoint1 status: %v", err) + } + if suiteStatus1.Name != "suite-endpoint1" { + t.Errorf("expected suite-endpoint1, got %s", suiteStatus1.Name) + } + if suiteStatus1.Group != "suite-group" { + t.Errorf("expected suite-group, got %s", suiteStatus1.Group) + } + if len(suiteStatus1.Results) != 1 { + t.Errorf("expected 1 result for suite-endpoint1, got %d", len(suiteStatus1.Results)) + } + if len(suiteStatus1.Results) > 0 { + result := suiteStatus1.Results[0] + if result.Success { + t.Error("expected suite-endpoint1 result to be unsuccessful") + } + if result.Duration != 50*time.Millisecond { + t.Errorf("expected suite-endpoint1 result duration 50ms, got %v", result.Duration) + } + if len(result.Errors) != 1 || result.Errors[0] != "suite error" { + t.Errorf("expected suite-endpoint1 to have error 'suite error', got %v", result.Errors) + } + } + }) + + // Test 6: GetSuiteStatusByKey should work for suites + t.Run("GetSuiteStatusByKey", func(t *testing.T) { + store, _, _, _, _, testSuite := setupStore(t) + + // InsertEndpointResult suite result with endpoint results + timestamp := time.Now() + store.InsertSuiteResult(testSuite, &suite.Result{ + Name: testSuite.Name, Group: testSuite.Group, Success: false, + Timestamp: timestamp, Duration: 125 * time.Millisecond, + EndpointResults: []*endpoint.Result{ + {Success: true, Duration: 50 * time.Millisecond}, + {Success: false, Duration: 75 * time.Millisecond, Errors: []string{"endpoint failed"}}, + }, + }) + suiteStatus, err := store.GetSuiteStatusByKey(testSuite.Key(), &paging.SuiteStatusParams{}) + if err != nil { + t.Fatalf("failed to get suite status: %v", err) + } + if suiteStatus.Name != "test-suite" { + t.Errorf("expected test-suite, got %s", suiteStatus.Name) + } + if suiteStatus.Group != "suite-group" { + t.Errorf("expected suite-group, got %s", suiteStatus.Group) + } + if len(suiteStatus.Results) != 1 { + t.Errorf("expected 1 suite result, got %d", len(suiteStatus.Results)) + } + + if len(suiteStatus.Results) > 0 { + result := suiteStatus.Results[0] + if result.Success { + t.Error("expected suite result to be unsuccessful") + } + if result.Duration != 125*time.Millisecond { + t.Errorf("expected suite result duration 125ms, got %v", result.Duration) + } + if len(result.EndpointResults) != 2 { + t.Errorf("expected 2 endpoint results, got %d", len(result.EndpointResults)) + } + if len(result.EndpointResults) >= 2 { + if !result.EndpointResults[0].Success { + t.Error("expected first endpoint result to be successful") + } + if result.EndpointResults[1].Success { + t.Error("expected second endpoint result to be unsuccessful") + } + if len(result.EndpointResults[1].Errors) != 1 || result.EndpointResults[1].Errors[0] != "endpoint failed" { + t.Errorf("expected second endpoint to have error 'endpoint failed', got %v", result.EndpointResults[1].Errors) + } + } + } + }) + + // Test 7: DeleteAllEndpointStatusesNotInKeys should not affect suites + t.Run("DeleteEndpointsNotInKeys", func(t *testing.T) { + store, endpoint1, endpoint2, suiteEndpoint1, suiteEndpoint2, testSuite := setupStore(t) + + // InsertEndpointResult all test data + store.InsertEndpointResult(endpoint1, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 100 * time.Millisecond}) + store.InsertEndpointResult(endpoint2, &endpoint.Result{Success: false, Timestamp: time.Now(), Duration: 200 * time.Millisecond}) + store.InsertEndpointResult(suiteEndpoint1, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 50 * time.Millisecond}) + store.InsertEndpointResult(suiteEndpoint2, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 75 * time.Millisecond}) + store.InsertSuiteResult(testSuite, &suite.Result{ + Name: testSuite.Name, Group: testSuite.Group, Success: true, + Timestamp: time.Now(), Duration: 125 * time.Millisecond, + }) + // Keep only endpoint1 and suite-endpoint1 + keysToKeep := []string{endpoint1.Key(), suiteEndpoint1.Key()} + deleted := store.DeleteAllEndpointStatusesNotInKeys(keysToKeep) + + // Should have deleted 2 endpoints (endpoint2 and suite-endpoint2) + if deleted != 2 { + t.Errorf("expected to delete 2 endpoints, deleted %d", deleted) + } + + // Verify remaining endpoints + statuses, _ := store.GetAllEndpointStatuses(&paging.EndpointStatusParams{}) + if len(statuses) != 2 { + t.Errorf("expected 2 remaining endpoint statuses, got %d", len(statuses)) + } + + // Suite should still exist + suiteStatuses, _ := store.GetAllSuiteStatuses(&paging.SuiteStatusParams{}) + if len(suiteStatuses) != 1 { + t.Errorf("suite should not be affected by DeleteAllEndpointStatusesNotInKeys") + } + }) + + // Test 8: DeleteAllSuiteStatusesNotInKeys should not affect endpoints + t.Run("DeleteSuitesNotInKeys", func(t *testing.T) { + store, endpoint1, _, _, _, testSuite := setupStore(t) + + // InsertEndpointResult test data + store.InsertEndpointResult(endpoint1, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 100 * time.Millisecond}) + store.InsertSuiteResult(testSuite, &suite.Result{ + Name: testSuite.Name, Group: testSuite.Group, Success: true, + Timestamp: time.Now(), Duration: 125 * time.Millisecond, + }) + // First, add another suite to test deletion + anotherSuite := &suite.Suite{ + Name: "another-suite", + Group: "another-group", + } + anotherSuiteResult := &suite.Result{ + Name: anotherSuite.Name, + Group: anotherSuite.Group, + Success: true, + Timestamp: time.Now(), + Duration: 100 * time.Millisecond, + } + store.InsertSuiteResult(anotherSuite, anotherSuiteResult) + + // Keep only the original test-suite + deleted := store.DeleteAllSuiteStatusesNotInKeys([]string{testSuite.Key()}) + + // Should have deleted 1 suite (another-suite) + if deleted != 1 { + t.Errorf("expected to delete 1 suite, deleted %d", deleted) + } + + // Endpoints should still exist + endpointStatuses, _ := store.GetAllEndpointStatuses(&paging.EndpointStatusParams{}) + if len(endpointStatuses) != 1 { + t.Errorf("endpoints should not be affected by DeleteAllSuiteStatusesNotInKeys") + } + + // Only one suite should remain + suiteStatuses, _ := store.GetAllSuiteStatuses(&paging.SuiteStatusParams{}) + if len(suiteStatuses) != 1 { + t.Errorf("expected 1 remaining suite, got %d", len(suiteStatuses)) + } + }) + + // Test 9: Clear should remove everything + t.Run("Clear", func(t *testing.T) { + store, endpoint1, _, _, _, testSuite := setupStore(t) + + // InsertEndpointResult test data + store.InsertEndpointResult(endpoint1, &endpoint.Result{Success: true, Timestamp: time.Now(), Duration: 100 * time.Millisecond}) + store.InsertSuiteResult(testSuite, &suite.Result{ + Name: testSuite.Name, Group: testSuite.Group, Success: true, + Timestamp: time.Now(), Duration: 125 * time.Millisecond, + }) + store.Clear() + + // No endpoints should remain + endpointStatuses, _ := store.GetAllEndpointStatuses(&paging.EndpointStatusParams{}) + if len(endpointStatuses) != 0 { + t.Errorf("expected 0 endpoints after clear, got %d", len(endpointStatuses)) + } + + // No suites should remain + suiteStatuses, _ := store.GetAllSuiteStatuses(&paging.SuiteStatusParams{}) + if len(suiteStatuses) != 0 { + t.Errorf("expected 0 suites after clear, got %d", len(suiteStatuses)) + } + }) +} + +// TestStore_EndpointStatusCastingSafety tests that type assertions are safe +func TestStore_EndpointStatusCastingSafety(t *testing.T) { + store, err := NewStore(100, 50) + if err != nil { + t.Fatal("expected no error, got", err) + } + + // InsertEndpointResult an endpoint + ep := &endpoint.Endpoint{ + Name: "test-endpoint", + Group: "test", + URL: "https://example.com", + } + result := &endpoint.Result{ + Success: true, + Timestamp: time.Now(), + Duration: 100 * time.Millisecond, + } + store.InsertEndpointResult(ep, result) + + // InsertEndpointResult a suite + testSuite := &suite.Suite{ + Name: "test-suite", + Group: "test", + } + suiteResult := &suite.Result{ + Name: testSuite.Name, + Group: testSuite.Group, + Success: true, + Timestamp: time.Now(), + Duration: 200 * time.Millisecond, + } + store.InsertSuiteResult(testSuite, suiteResult) + + // This should not panic even with mixed types in cache + statuses, err := store.GetAllEndpointStatuses(&paging.EndpointStatusParams{}) + if err != nil { + t.Fatalf("failed to get all endpoint statuses: %v", err) + } + + // Should only have the endpoint, not the suite + if len(statuses) != 1 { + t.Errorf("expected 1 endpoint status, got %d", len(statuses)) + } + if statuses[0].Name != "test-endpoint" { + t.Errorf("expected test-endpoint, got %s", statuses[0].Name) + } +} + +func TestStore_MaximumLimits(t *testing.T) { + // Use small limits to test trimming behavior + maxResults := 5 + maxEvents := 3 + store, err := NewStore(maxResults, maxEvents) + if err != nil { + t.Fatal("expected no error, got", err) + } + defer store.Clear() + + t.Run("endpoint-result-limits", func(t *testing.T) { + ep := &endpoint.Endpoint{Name: "test-endpoint", Group: "test", URL: "https://example.com"} + + // Insert more results than the maximum + baseTime := time.Now().Add(-10 * time.Hour) + for i := 0; i < maxResults*2; i++ { + result := &endpoint.Result{ + Success: i%2 == 0, + Timestamp: baseTime.Add(time.Duration(i) * time.Hour), + Duration: time.Duration(i*10) * time.Millisecond, + } + err := store.InsertEndpointResult(ep, result) + if err != nil { + t.Fatalf("failed to insert result %d: %v", i, err) + } + } + + // Verify only maxResults are kept + status, err := store.GetEndpointStatusByKey(ep.Key(), nil) + if err != nil { + t.Fatalf("failed to get endpoint status: %v", err) + } + if len(status.Results) != maxResults { + t.Errorf("expected %d results after trimming, got %d", maxResults, len(status.Results)) + } + + // Verify the newest results are kept (should be results 5-9, not 0-4) + if len(status.Results) > 0 { + firstResult := status.Results[0] + lastResult := status.Results[len(status.Results)-1] + // First result should be older than last result due to append order + if !lastResult.Timestamp.After(firstResult.Timestamp) { + t.Error("expected results to be in chronological order") + } + // The last result should be the most recent one we inserted + expectedLastDuration := time.Duration((maxResults*2-1)*10) * time.Millisecond + if lastResult.Duration != expectedLastDuration { + t.Errorf("expected last result duration %v, got %v", expectedLastDuration, lastResult.Duration) + } + } + }) + + t.Run("suite-result-limits", func(t *testing.T) { + testSuite := &suite.Suite{Name: "test-suite", Group: "test"} + + // Insert more results than the maximum + baseTime := time.Now().Add(-10 * time.Hour) + for i := 0; i < maxResults*2; i++ { + result := &suite.Result{ + Name: testSuite.Name, + Group: testSuite.Group, + Success: i%2 == 0, + Timestamp: baseTime.Add(time.Duration(i) * time.Hour), + Duration: time.Duration(i*10) * time.Millisecond, + } + err := store.InsertSuiteResult(testSuite, result) + if err != nil { + t.Fatalf("failed to insert suite result %d: %v", i, err) + } + } + + // Verify only maxResults are kept + status, err := store.GetSuiteStatusByKey(testSuite.Key(), &paging.SuiteStatusParams{}) + if err != nil { + t.Fatalf("failed to get suite status: %v", err) + } + if len(status.Results) != maxResults { + t.Errorf("expected %d results after trimming, got %d", maxResults, len(status.Results)) + } + + // Verify the newest results are kept (should be results 5-9, not 0-4) + if len(status.Results) > 0 { + firstResult := status.Results[0] + lastResult := status.Results[len(status.Results)-1] + // First result should be older than last result due to append order + if !lastResult.Timestamp.After(firstResult.Timestamp) { + t.Error("expected results to be in chronological order") + } + // The last result should be the most recent one we inserted + expectedLastDuration := time.Duration((maxResults*2-1)*10) * time.Millisecond + if lastResult.Duration != expectedLastDuration { + t.Errorf("expected last result duration %v, got %v", expectedLastDuration, lastResult.Duration) + } + } + }) +} + +func TestSuiteResultOrdering(t *testing.T) { + store, err := NewStore(10, 5) + if err != nil { + t.Fatal("expected no error, got", err) + } + defer store.Clear() + + testSuite := &suite.Suite{Name: "ordering-suite", Group: "test"} + + // Insert results with distinct timestamps + baseTime := time.Now().Add(-5 * time.Hour) + timestamps := make([]time.Time, 5) + + for i := 0; i < 5; i++ { + timestamp := baseTime.Add(time.Duration(i) * time.Hour) + timestamps[i] = timestamp + result := &suite.Result{ + Name: testSuite.Name, + Group: testSuite.Group, + Success: true, + Timestamp: timestamp, + Duration: time.Duration(i*100) * time.Millisecond, + } + err := store.InsertSuiteResult(testSuite, result) + if err != nil { + t.Fatalf("failed to insert result %d: %v", i, err) + } + } + + t.Run("chronological-append-order", func(t *testing.T) { + status, err := store.GetSuiteStatusByKey(testSuite.Key(), nil) + if err != nil { + t.Fatalf("failed to get suite status: %v", err) + } + + // Verify results are in chronological order (oldest first due to append) + for i := 0; i < len(status.Results)-1; i++ { + current := status.Results[i] + next := status.Results[i+1] + if !next.Timestamp.After(current.Timestamp) { + t.Errorf("result %d timestamp %v should be before result %d timestamp %v", + i, current.Timestamp, i+1, next.Timestamp) + } + } + + // Verify specific timestamp order + if !status.Results[0].Timestamp.Equal(timestamps[0]) { + t.Errorf("first result timestamp should be %v, got %v", timestamps[0], status.Results[0].Timestamp) + } + if !status.Results[len(status.Results)-1].Timestamp.Equal(timestamps[len(timestamps)-1]) { + t.Errorf("last result timestamp should be %v, got %v", timestamps[len(timestamps)-1], status.Results[len(status.Results)-1].Timestamp) + } + }) + + t.Run("pagination-newest-first", func(t *testing.T) { + // Test reverse pagination (newest first in paginated results) + page1 := ShallowCopySuiteStatus( + &suite.Status{ + Name: testSuite.Name, Group: testSuite.Group, Key: testSuite.Key(), + Results: []*suite.Result{ + {Timestamp: timestamps[0], Duration: 0 * time.Millisecond}, + {Timestamp: timestamps[1], Duration: 100 * time.Millisecond}, + {Timestamp: timestamps[2], Duration: 200 * time.Millisecond}, + {Timestamp: timestamps[3], Duration: 300 * time.Millisecond}, + {Timestamp: timestamps[4], Duration: 400 * time.Millisecond}, + }, + }, + paging.NewSuiteStatusParams().WithPagination(1, 3), + ) + + if len(page1.Results) != 3 { + t.Errorf("expected 3 results in page 1, got %d", len(page1.Results)) + } + + // With reverse pagination, page 1 should have the 3 newest results + // That means results[2], results[3], results[4] from original array + if page1.Results[0].Duration != 200*time.Millisecond { + t.Errorf("expected first result in page to have 200ms duration, got %v", page1.Results[0].Duration) + } + if page1.Results[2].Duration != 400*time.Millisecond { + t.Errorf("expected last result in page to have 400ms duration, got %v", page1.Results[2].Duration) + } + }) + + t.Run("trimming-preserves-newest", func(t *testing.T) { + limitedStore, err := NewStore(3, 2) // Very small limits + if err != nil { + t.Fatal("expected no error, got", err) + } + defer limitedStore.Clear() + + smallSuite := &suite.Suite{Name: "small-suite", Group: "test"} + + // Insert 6 results, should keep only the newest 3 + for i := 0; i < 6; i++ { + result := &suite.Result{ + Name: smallSuite.Name, + Group: smallSuite.Group, + Success: true, + Timestamp: baseTime.Add(time.Duration(i) * time.Hour), + Duration: time.Duration(i*50) * time.Millisecond, + } + err := limitedStore.InsertSuiteResult(smallSuite, result) + if err != nil { + t.Fatalf("failed to insert result %d: %v", i, err) + } + } + + status, err := limitedStore.GetSuiteStatusByKey(smallSuite.Key(), nil) + if err != nil { + t.Fatalf("failed to get suite status: %v", err) + } + + if len(status.Results) != 3 { + t.Errorf("expected 3 results after trimming, got %d", len(status.Results)) + } + + // Should have results 3, 4, 5 (the newest ones) + expectedDurations := []time.Duration{150 * time.Millisecond, 200 * time.Millisecond, 250 * time.Millisecond} + for i, expectedDuration := range expectedDurations { + if status.Results[i].Duration != expectedDuration { + t.Errorf("result %d should have duration %v, got %v", i, expectedDuration, status.Results[i].Duration) + } + } + }) +} + +func TestStore_ConcurrentAccess(t *testing.T) { + store, err := NewStore(100, 50) + if err != nil { + t.Fatal("expected no error, got", err) + } + defer store.Clear() + + t.Run("concurrent-endpoint-insertions", func(t *testing.T) { + var wg sync.WaitGroup + numGoroutines := 10 + resultsPerGoroutine := 5 + + // Create endpoints for concurrent testing + endpoints := make([]*endpoint.Endpoint, numGoroutines) + for i := 0; i < numGoroutines; i++ { + endpoints[i] = &endpoint.Endpoint{ + Name: "endpoint-" + string(rune('A'+i)), + Group: "concurrent", + URL: "https://example.com/" + string(rune('A'+i)), + } + } + + // Concurrently insert results for different endpoints + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(endpointIndex int) { + defer wg.Done() + ep := endpoints[endpointIndex] + for j := 0; j < resultsPerGoroutine; j++ { + result := &endpoint.Result{ + Success: j%2 == 0, + Timestamp: time.Now().Add(time.Duration(j) * time.Minute), + Duration: time.Duration(j*10) * time.Millisecond, + } + if err := store.InsertEndpointResult(ep, result); err != nil { + t.Errorf("failed to insert result for endpoint %d: %v", endpointIndex, err) + } + } + }(i) + } + + wg.Wait() + + // Verify all endpoints were created and have correct result counts + statuses, err := store.GetAllEndpointStatuses(&paging.EndpointStatusParams{}) + if err != nil { + t.Fatalf("failed to get all endpoint statuses: %v", err) + } + if len(statuses) != numGoroutines { + t.Errorf("expected %d endpoint statuses, got %d", numGoroutines, len(statuses)) + } + + // Verify each endpoint has the correct number of results + for _, status := range statuses { + if len(status.Results) != resultsPerGoroutine { + t.Errorf("endpoint %s should have %d results, got %d", status.Name, resultsPerGoroutine, len(status.Results)) + } + } + }) + + t.Run("concurrent-suite-insertions", func(t *testing.T) { + var wg sync.WaitGroup + numGoroutines := 5 + resultsPerGoroutine := 3 + + // Create suites for concurrent testing + suites := make([]*suite.Suite, numGoroutines) + for i := 0; i < numGoroutines; i++ { + suites[i] = &suite.Suite{ + Name: "suite-" + string(rune('A'+i)), + Group: "concurrent", + } + } + + // Concurrently insert results for different suites + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(suiteIndex int) { + defer wg.Done() + su := suites[suiteIndex] + for j := 0; j < resultsPerGoroutine; j++ { + result := &suite.Result{ + Name: su.Name, + Group: su.Group, + Success: j%2 == 0, + Timestamp: time.Now().Add(time.Duration(j) * time.Minute), + Duration: time.Duration(j*50) * time.Millisecond, + } + if err := store.InsertSuiteResult(su, result); err != nil { + t.Errorf("failed to insert result for suite %d: %v", suiteIndex, err) + } + } + }(i) + } + + wg.Wait() + + // Verify all suites were created and have correct result counts + statuses, err := store.GetAllSuiteStatuses(&paging.SuiteStatusParams{}) + if err != nil { + t.Fatalf("failed to get all suite statuses: %v", err) + } + if len(statuses) != numGoroutines { + t.Errorf("expected %d suite statuses, got %d", numGoroutines, len(statuses)) + } + + // Verify each suite has the correct number of results + for _, status := range statuses { + if len(status.Results) != resultsPerGoroutine { + t.Errorf("suite %s should have %d results, got %d", status.Name, resultsPerGoroutine, len(status.Results)) + } + } + }) + + t.Run("concurrent-mixed-operations", func(t *testing.T) { + var wg sync.WaitGroup + + // Setup test data + ep := &endpoint.Endpoint{Name: "mixed-endpoint", Group: "test", URL: "https://example.com"} + testSuite := &suite.Suite{Name: "mixed-suite", Group: "test"} + + // Concurrent endpoint insertions + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 5; i++ { + result := &endpoint.Result{ + Success: true, + Timestamp: time.Now(), + Duration: time.Duration(i*10) * time.Millisecond, + } + store.InsertEndpointResult(ep, result) + } + }() + + // Concurrent suite insertions + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 5; i++ { + result := &suite.Result{ + Name: testSuite.Name, + Group: testSuite.Group, + Success: true, + Timestamp: time.Now(), + Duration: time.Duration(i*20) * time.Millisecond, + } + store.InsertSuiteResult(testSuite, result) + } + }() + + // Concurrent reads + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 10; i++ { + store.GetAllEndpointStatuses(&paging.EndpointStatusParams{}) + store.GetAllSuiteStatuses(&paging.SuiteStatusParams{}) + time.Sleep(1 * time.Millisecond) + } + }() + + wg.Wait() + + // Verify final state is consistent + endpointStatuses, err := store.GetAllEndpointStatuses(&paging.EndpointStatusParams{}) + if err != nil { + t.Fatalf("failed to get endpoint statuses after concurrent operations: %v", err) + } + if len(endpointStatuses) == 0 { + t.Error("expected at least one endpoint status after concurrent operations") + } + + suiteStatuses, err := store.GetAllSuiteStatuses(&paging.SuiteStatusParams{}) + if err != nil { + t.Fatalf("failed to get suite statuses after concurrent operations: %v", err) + } + if len(suiteStatuses) == 0 { + t.Error("expected at least one suite status after concurrent operations") + } + }) +} diff --git a/storage/store/memory/util.go b/storage/store/memory/util.go index acb17eb1..514fda09 100644 --- a/storage/store/memory/util.go +++ b/storage/store/memory/util.go @@ -2,6 +2,7 @@ package memory import ( "github.com/TwiN/gatus/v5/config/endpoint" + "github.com/TwiN/gatus/v5/config/suite" "github.com/TwiN/gatus/v5/storage/store/common/paging" ) @@ -14,19 +15,46 @@ func ShallowCopyEndpointStatus(ss *endpoint.Status, params *paging.EndpointStatu Key: ss.Key, Uptime: endpoint.NewUptime(), } - numberOfResults := len(ss.Results) - resultsStart, resultsEnd := getStartAndEndIndex(numberOfResults, params.ResultsPage, params.ResultsPageSize) - if resultsStart < 0 || resultsEnd < 0 { - shallowCopy.Results = []*endpoint.Result{} + if params == nil || (params.ResultsPage == 0 && params.ResultsPageSize == 0 && params.EventsPage == 0 && params.EventsPageSize == 0) { + shallowCopy.Results = ss.Results + shallowCopy.Events = ss.Events } else { - shallowCopy.Results = ss.Results[resultsStart:resultsEnd] + numberOfResults := len(ss.Results) + resultsStart, resultsEnd := getStartAndEndIndex(numberOfResults, params.ResultsPage, params.ResultsPageSize) + if resultsStart < 0 || resultsEnd < 0 { + shallowCopy.Results = []*endpoint.Result{} + } else { + shallowCopy.Results = ss.Results[resultsStart:resultsEnd] + } + numberOfEvents := len(ss.Events) + eventsStart, eventsEnd := getStartAndEndIndex(numberOfEvents, params.EventsPage, params.EventsPageSize) + if eventsStart < 0 || eventsEnd < 0 { + shallowCopy.Events = []*endpoint.Event{} + } else { + shallowCopy.Events = ss.Events[eventsStart:eventsEnd] + } } - numberOfEvents := len(ss.Events) - eventsStart, eventsEnd := getStartAndEndIndex(numberOfEvents, params.EventsPage, params.EventsPageSize) - if eventsStart < 0 || eventsEnd < 0 { - shallowCopy.Events = []*endpoint.Event{} + return shallowCopy +} + +// ShallowCopySuiteStatus returns a shallow copy of a suite Status with only the results +// within the range defined by the page and pageSize parameters +func ShallowCopySuiteStatus(ss *suite.Status, params *paging.SuiteStatusParams) *suite.Status { + shallowCopy := &suite.Status{ + Name: ss.Name, + Group: ss.Group, + Key: ss.Key, + } + if params == nil || (params.Page == 0 && params.PageSize == 0) { + shallowCopy.Results = ss.Results } else { - shallowCopy.Events = ss.Events[eventsStart:eventsEnd] + numberOfResults := len(ss.Results) + resultsStart, resultsEnd := getStartAndEndIndex(numberOfResults, params.Page, params.PageSize) + if resultsStart < 0 || resultsEnd < 0 { + shallowCopy.Results = []*suite.Result{} + } else { + shallowCopy.Results = ss.Results[resultsStart:resultsEnd] + } } return shallowCopy } diff --git a/storage/store/memory/util_test.go b/storage/store/memory/util_test.go index 8287dafe..927ca937 100644 --- a/storage/store/memory/util_test.go +++ b/storage/store/memory/util_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/TwiN/gatus/v5/config/endpoint" + "github.com/TwiN/gatus/v5/config/suite" "github.com/TwiN/gatus/v5/storage" "github.com/TwiN/gatus/v5/storage/store/common/paging" ) @@ -64,3 +65,108 @@ func TestShallowCopyEndpointStatus(t *testing.T) { t.Error("expected to have 25 results, because there's only 25 results") } } + +func TestShallowCopySuiteStatus(t *testing.T) { + testSuite := &suite.Suite{Name: "test-suite", Group: "test-group"} + suiteStatus := &suite.Status{ + Name: testSuite.Name, + Group: testSuite.Group, + Key: testSuite.Key(), + Results: []*suite.Result{}, + } + + ts := time.Now().Add(-25 * time.Hour) + for i := 0; i < 25; i++ { + result := &suite.Result{ + Name: testSuite.Name, + Group: testSuite.Group, + Success: i%2 == 0, + Timestamp: ts, + Duration: time.Duration(i*10) * time.Millisecond, + } + suiteStatus.Results = append(suiteStatus.Results, result) + ts = ts.Add(time.Hour) + } + + t.Run("invalid-page-negative", func(t *testing.T) { + result := ShallowCopySuiteStatus(suiteStatus, paging.NewSuiteStatusParams().WithPagination(-1, 10)) + if len(result.Results) != 0 { + t.Errorf("expected 0 results for negative page, got %d", len(result.Results)) + } + }) + + t.Run("invalid-page-zero", func(t *testing.T) { + result := ShallowCopySuiteStatus(suiteStatus, paging.NewSuiteStatusParams().WithPagination(0, 10)) + if len(result.Results) != 0 { + t.Errorf("expected 0 results for zero page, got %d", len(result.Results)) + } + }) + + t.Run("invalid-pagesize-negative", func(t *testing.T) { + result := ShallowCopySuiteStatus(suiteStatus, paging.NewSuiteStatusParams().WithPagination(1, -1)) + if len(result.Results) != 0 { + t.Errorf("expected 0 results for negative page size, got %d", len(result.Results)) + } + }) + + t.Run("zero-pagesize", func(t *testing.T) { + result := ShallowCopySuiteStatus(suiteStatus, paging.NewSuiteStatusParams().WithPagination(1, 0)) + if len(result.Results) != 0 { + t.Errorf("expected 0 results for zero page size, got %d", len(result.Results)) + } + }) + + t.Run("nil-params", func(t *testing.T) { + result := ShallowCopySuiteStatus(suiteStatus, nil) + if len(result.Results) != 25 { + t.Errorf("expected 25 results for nil params, got %d", len(result.Results)) + } + }) + + t.Run("zero-params", func(t *testing.T) { + result := ShallowCopySuiteStatus(suiteStatus, &paging.SuiteStatusParams{Page: 0, PageSize: 0}) + if len(result.Results) != 25 { + t.Errorf("expected 25 results for zero-value params, got %d", len(result.Results)) + } + }) + + t.Run("first-page", func(t *testing.T) { + result := ShallowCopySuiteStatus(suiteStatus, paging.NewSuiteStatusParams().WithPagination(1, 10)) + if len(result.Results) != 10 { + t.Errorf("expected 10 results for page 1, size 10, got %d", len(result.Results)) + } + // Verify newest results are returned (reverse pagination) + if len(result.Results) > 0 && !result.Results[len(result.Results)-1].Timestamp.After(result.Results[0].Timestamp) { + t.Error("expected newest result to be at the end") + } + }) + + t.Run("second-page", func(t *testing.T) { + result := ShallowCopySuiteStatus(suiteStatus, paging.NewSuiteStatusParams().WithPagination(2, 10)) + if len(result.Results) != 10 { + t.Errorf("expected 10 results for page 2, size 10, got %d", len(result.Results)) + } + }) + + t.Run("last-partial-page", func(t *testing.T) { + result := ShallowCopySuiteStatus(suiteStatus, paging.NewSuiteStatusParams().WithPagination(3, 10)) + if len(result.Results) != 5 { + t.Errorf("expected 5 results for page 3, size 10, got %d", len(result.Results)) + } + }) + + t.Run("beyond-available-pages", func(t *testing.T) { + result := ShallowCopySuiteStatus(suiteStatus, paging.NewSuiteStatusParams().WithPagination(4, 10)) + if len(result.Results) != 0 { + t.Errorf("expected 0 results for page beyond available data, got %d", len(result.Results)) + } + }) + + t.Run("large-page-size", func(t *testing.T) { + result := ShallowCopySuiteStatus(suiteStatus, paging.NewSuiteStatusParams().WithPagination(1, 100)) + if len(result.Results) != 25 { + t.Errorf("expected 25 results for large page size, got %d", len(result.Results)) + } + }) +} + diff --git a/storage/store/sql/specific_postgres.go b/storage/store/sql/specific_postgres.go index aa8b5d5f..d87ba308 100644 --- a/storage/store/sql/specific_postgres.go +++ b/storage/store/sql/specific_postgres.go @@ -38,7 +38,8 @@ func (s *Store) createPostgresSchema() error { hostname TEXT NOT NULL, ip TEXT NOT NULL, duration BIGINT NOT NULL, - timestamp TIMESTAMP NOT NULL + timestamp TIMESTAMP NOT NULL, + suite_result_id BIGINT REFERENCES suite_results(suite_result_id) ON DELETE CASCADE ) `) if err != nil { @@ -79,7 +80,44 @@ func (s *Store) createPostgresSchema() error { UNIQUE(endpoint_id, configuration_checksum) ) `) + if err != nil { + return err + } + // Create suite tables + _, err = s.db.Exec(` + CREATE TABLE IF NOT EXISTS suites ( + suite_id BIGSERIAL PRIMARY KEY, + suite_key TEXT UNIQUE, + suite_name TEXT NOT NULL, + suite_group TEXT NOT NULL, + UNIQUE(suite_name, suite_group) + ) + `) + if err != nil { + return err + } + _, err = s.db.Exec(` + CREATE TABLE IF NOT EXISTS suite_results ( + suite_result_id BIGSERIAL PRIMARY KEY, + suite_id BIGINT NOT NULL REFERENCES suites(suite_id) ON DELETE CASCADE, + success BOOLEAN NOT NULL, + errors TEXT NOT NULL, + duration BIGINT NOT NULL, + timestamp TIMESTAMP NOT NULL + ) + `) + if err != nil { + return err + } + // Create index for suite_results + _, err = s.db.Exec(` + CREATE INDEX IF NOT EXISTS suite_results_suite_id_idx ON suite_results (suite_id); + `) // Silent table modifications TODO: Remove this in v6.0.0 _, _ = s.db.Exec(`ALTER TABLE endpoint_results ADD IF NOT EXISTS domain_expiration BIGINT NOT NULL DEFAULT 0`) + // Add suite_result_id to endpoint_results table for suite endpoint linkage + _, _ = s.db.Exec(`ALTER TABLE endpoint_results ADD COLUMN IF NOT EXISTS suite_result_id BIGINT REFERENCES suite_results(suite_result_id) ON DELETE CASCADE`) + // Create index for suite_result_id + _, _ = s.db.Exec(`CREATE INDEX IF NOT EXISTS endpoint_results_suite_result_id_idx ON endpoint_results(suite_result_id)`) return err } diff --git a/storage/store/sql/specific_sqlite.go b/storage/store/sql/specific_sqlite.go index 4dae436b..b167d70b 100644 --- a/storage/store/sql/specific_sqlite.go +++ b/storage/store/sql/specific_sqlite.go @@ -38,7 +38,8 @@ func (s *Store) createSQLiteSchema() error { hostname TEXT NOT NULL, ip TEXT NOT NULL, duration INTEGER NOT NULL, - timestamp TIMESTAMP NOT NULL + timestamp TIMESTAMP NOT NULL, + suite_result_id INTEGER REFERENCES suite_results(suite_result_id) ON DELETE CASCADE ) `) if err != nil { @@ -82,6 +83,32 @@ func (s *Store) createSQLiteSchema() error { if err != nil { return err } + // Create suite tables + _, err = s.db.Exec(` + CREATE TABLE IF NOT EXISTS suites ( + suite_id INTEGER PRIMARY KEY, + suite_key TEXT UNIQUE, + suite_name TEXT NOT NULL, + suite_group TEXT NOT NULL, + UNIQUE(suite_name, suite_group) + ) + `) + if err != nil { + return err + } + _, err = s.db.Exec(` + CREATE TABLE IF NOT EXISTS suite_results ( + suite_result_id INTEGER PRIMARY KEY, + suite_id INTEGER NOT NULL REFERENCES suites(suite_id) ON DELETE CASCADE, + success INTEGER NOT NULL, + errors TEXT NOT NULL, + duration INTEGER NOT NULL, + timestamp TIMESTAMP NOT NULL + ) + `) + if err != nil { + return err + } // Create indices for performance reasons _, err = s.db.Exec(` CREATE INDEX IF NOT EXISTS endpoint_results_endpoint_id_idx ON endpoint_results (endpoint_id); @@ -98,7 +125,23 @@ func (s *Store) createSQLiteSchema() error { _, err = s.db.Exec(` CREATE INDEX IF NOT EXISTS endpoint_result_conditions_endpoint_result_id_idx ON endpoint_result_conditions (endpoint_result_id); `) + if err != nil { + return err + } + // Create index for suite_results + _, err = s.db.Exec(` + CREATE INDEX IF NOT EXISTS suite_results_suite_id_idx ON suite_results (suite_id); + `) + if err != nil { + return err + } // Silent table modifications TODO: Remove this in v6.0.0 _, _ = s.db.Exec(`ALTER TABLE endpoint_results ADD domain_expiration INTEGER NOT NULL DEFAULT 0`) + // Add suite_result_id to endpoint_results table for suite endpoint linkage + _, _ = s.db.Exec(`ALTER TABLE endpoint_results ADD suite_result_id INTEGER REFERENCES suite_results(suite_result_id) ON DELETE CASCADE`) + // Create index for suite_result_id + _, _ = s.db.Exec(`CREATE INDEX IF NOT EXISTS endpoint_results_suite_result_id_idx ON endpoint_results(suite_result_id)`) + // Note: SQLite doesn't support DROP COLUMN in older versions, so we skip this cleanup + // The suite_id column in endpoints table will remain but unused return err } diff --git a/storage/store/sql/sql.go b/storage/store/sql/sql.go index 8d3312d2..cdd67dac 100644 --- a/storage/store/sql/sql.go +++ b/storage/store/sql/sql.go @@ -10,6 +10,8 @@ import ( "github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/config/endpoint" + "github.com/TwiN/gatus/v5/config/key" + "github.com/TwiN/gatus/v5/config/suite" "github.com/TwiN/gatus/v5/storage/store/common" "github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gocache/v2" @@ -138,7 +140,7 @@ func (s *Store) GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]* // GetEndpointStatus returns the endpoint status for a given endpoint name in the given group func (s *Store) GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*endpoint.Status, error) { - return s.GetEndpointStatusByKey(endpoint.ConvertGroupAndEndpointNameToKey(groupName, endpointName), params) + return s.GetEndpointStatusByKey(key.ConvertGroupAndNameToKey(groupName, endpointName), params) } // GetEndpointStatusByKey returns the endpoint status for a given key @@ -233,8 +235,8 @@ func (s *Store) GetHourlyAverageResponseTimeByKey(key string, from, to time.Time return hourlyAverageResponseTimes, nil } -// Insert adds the observed result for the specified endpoint into the store -func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error { +// InsertEndpointResult adds the observed result for the specified endpoint into the store +func (s *Store) InsertEndpointResult(ep *endpoint.Endpoint, result *endpoint.Result) error { tx, err := s.db.Begin() if err != nil { return err @@ -245,12 +247,12 @@ func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error { // Endpoint doesn't exist in the database, insert it if endpointID, err = s.insertEndpoint(tx, ep); err != nil { _ = tx.Rollback() - logr.Errorf("[sql.Insert] Failed to create endpoint with key=%s: %s", ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to create endpoint with key=%s: %s", ep.Key(), err.Error()) return err } } else { _ = tx.Rollback() - logr.Errorf("[sql.Insert] Failed to retrieve id of endpoint with key=%s: %s", ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to retrieve id of endpoint with key=%s: %s", ep.Key(), err.Error()) return err } } @@ -266,7 +268,7 @@ func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error { numberOfEvents, err := s.getNumberOfEventsByEndpointID(tx, endpointID) if err != nil { // Silently fail - logr.Errorf("[sql.Insert] Failed to retrieve total number of events for endpoint with key=%s: %s", ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to retrieve total number of events for endpoint with key=%s: %s", ep.Key(), err.Error()) } if numberOfEvents == 0 { // There's no events yet, which means we need to add the EventStart and the first healthy/unhealthy event @@ -276,18 +278,18 @@ func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error { }) if err != nil { // Silently fail - logr.Errorf("[sql.Insert] Failed to insert event=%s for endpoint with key=%s: %s", endpoint.EventStart, ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to insert event=%s for endpoint with key=%s: %s", endpoint.EventStart, ep.Key(), err.Error()) } event := endpoint.NewEventFromResult(result) if err = s.insertEndpointEvent(tx, endpointID, event); err != nil { // Silently fail - logr.Errorf("[sql.Insert] Failed to insert event=%s for endpoint with key=%s: %s", event.Type, ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to insert event=%s for endpoint with key=%s: %s", event.Type, ep.Key(), err.Error()) } } else { // Get the success value of the previous result var lastResultSuccess bool if lastResultSuccess, err = s.getLastEndpointResultSuccessValue(tx, endpointID); err != nil { - logr.Errorf("[sql.Insert] Failed to retrieve outcome of previous result for endpoint with key=%s: %s", ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to retrieve outcome of previous result for endpoint with key=%s: %s", ep.Key(), err.Error()) } else { // If we managed to retrieve the outcome of the previous result, we'll compare it with the new result. // If the final outcome (success or failure) of the previous and the new result aren't the same, it means @@ -297,7 +299,7 @@ func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error { event := endpoint.NewEventFromResult(result) if err = s.insertEndpointEvent(tx, endpointID, event); err != nil { // Silently fail - logr.Errorf("[sql.Insert] Failed to insert event=%s for endpoint with key=%s: %s", event.Type, ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to insert event=%s for endpoint with key=%s: %s", event.Type, ep.Key(), err.Error()) } } } @@ -306,42 +308,42 @@ func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error { // (since we're only deleting MaximumNumberOfEvents at a time instead of 1) if numberOfEvents > int64(s.maximumNumberOfEvents+eventsAboveMaximumCleanUpThreshold) { if err = s.deleteOldEndpointEvents(tx, endpointID); err != nil { - logr.Errorf("[sql.Insert] Failed to delete old events for endpoint with key=%s: %s", ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to delete old events for endpoint with key=%s: %s", ep.Key(), err.Error()) } } } // Second, we need to insert the result. if err = s.insertEndpointResult(tx, endpointID, result); err != nil { - logr.Errorf("[sql.Insert] Failed to insert result for endpoint with key=%s: %s", ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to insert result for endpoint with key=%s: %s", ep.Key(), err.Error()) _ = tx.Rollback() // If we can't insert the result, we'll rollback now since there's no point continuing return err } // Clean up old results numberOfResults, err := s.getNumberOfResultsByEndpointID(tx, endpointID) if err != nil { - logr.Errorf("[sql.Insert] Failed to retrieve total number of results for endpoint with key=%s: %s", ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to retrieve total number of results for endpoint with key=%s: %s", ep.Key(), err.Error()) } else { if numberOfResults > int64(s.maximumNumberOfResults+resultsAboveMaximumCleanUpThreshold) { if err = s.deleteOldEndpointResults(tx, endpointID); err != nil { - logr.Errorf("[sql.Insert] Failed to delete old results for endpoint with key=%s: %s", ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to delete old results for endpoint with key=%s: %s", ep.Key(), err.Error()) } } } // Finally, we need to insert the uptime data. // Because the uptime data significantly outlives the results, we can't rely on the results for determining the uptime if err = s.updateEndpointUptime(tx, endpointID, result); err != nil { - logr.Errorf("[sql.Insert] Failed to update uptime for endpoint with key=%s: %s", ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to update uptime for endpoint with key=%s: %s", ep.Key(), err.Error()) } // Merge hourly uptime entries that can be merged into daily entries and clean up old uptime entries numberOfUptimeEntries, err := s.getNumberOfUptimeEntriesByEndpointID(tx, endpointID) if err != nil { - logr.Errorf("[sql.Insert] Failed to retrieve total number of uptime entries for endpoint with key=%s: %s", ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to retrieve total number of uptime entries for endpoint with key=%s: %s", ep.Key(), err.Error()) } else { // Merge older hourly uptime entries into daily uptime entries if we have more than uptimeTotalEntriesMergeThreshold if numberOfUptimeEntries >= uptimeTotalEntriesMergeThreshold { - logr.Infof("[sql.Insert] Merging hourly uptime entries for endpoint with key=%s; This is a lot of work, it shouldn't happen too often", ep.Key()) + logr.Infof("[sql.InsertEndpointResult] Merging hourly uptime entries for endpoint with key=%s; This is a lot of work, it shouldn't happen too often", ep.Key()) if err = s.mergeHourlyUptimeEntriesOlderThanMergeThresholdIntoDailyUptimeEntries(tx, endpointID); err != nil { - logr.Errorf("[sql.Insert] Failed to merge hourly uptime entries for endpoint with key=%s: %s", ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to merge hourly uptime entries for endpoint with key=%s: %s", ep.Key(), err.Error()) } } } @@ -350,11 +352,11 @@ func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error { // but if Gatus was temporarily shut down, we might have some old entries that need to be cleaned up ageOfOldestUptimeEntry, err := s.getAgeOfOldestEndpointUptimeEntry(tx, endpointID) if err != nil { - logr.Errorf("[sql.Insert] Failed to retrieve oldest endpoint uptime entry for endpoint with key=%s: %s", ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to retrieve oldest endpoint uptime entry for endpoint with key=%s: %s", ep.Key(), err.Error()) } else { if ageOfOldestUptimeEntry > uptimeAgeCleanUpThreshold { if err = s.deleteOldUptimeEntries(tx, endpointID, time.Now().Add(-(uptimeRetention + time.Hour))); err != nil { - logr.Errorf("[sql.Insert] Failed to delete old uptime entries for endpoint with key=%s: %s", ep.Key(), err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Failed to delete old uptime entries for endpoint with key=%s: %s", ep.Key(), err.Error()) } } } @@ -364,7 +366,7 @@ func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error { s.writeThroughCache.Delete(cacheKey) endpointKey, params, err := extractKeyAndParamsFromCacheKey(cacheKey) if err != nil { - logr.Errorf("[sql.Insert] Silently deleting cache key %s instead of refreshing due to error: %s", cacheKey, err.Error()) + logr.Errorf("[sql.InsertEndpointResult] Silently deleting cache key %s instead of refreshing due to error: %s", cacheKey, err.Error()) continue } // Retrieve the endpoint status by key, which will in turn refresh the cache @@ -379,17 +381,43 @@ func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error { // DeleteAllEndpointStatusesNotInKeys removes all rows owned by an endpoint whose key is not within the keys provided func (s *Store) DeleteAllEndpointStatusesNotInKeys(keys []string) int { + logr.Debugf("[sql.DeleteAllEndpointStatusesNotInKeys] Called with %d keys", len(keys)) var err error var result sql.Result if len(keys) == 0 { // Delete everything + logr.Debugf("[sql.DeleteAllEndpointStatusesNotInKeys] No keys provided, deleting all endpoints") result, err = s.db.Exec("DELETE FROM endpoints") } else { + // First check what we're about to delete args := make([]interface{}, 0, len(keys)) + checkQuery := "SELECT endpoint_key FROM endpoints WHERE endpoint_key NOT IN (" + for i := range keys { + checkQuery += fmt.Sprintf("$%d,", i+1) + args = append(args, keys[i]) + } + checkQuery = checkQuery[:len(checkQuery)-1] + ")" + + rows, checkErr := s.db.Query(checkQuery, args...) + if checkErr == nil { + defer rows.Close() + var deletedKeys []string + for rows.Next() { + var key string + if err := rows.Scan(&key); err == nil { + deletedKeys = append(deletedKeys, key) + } + } + if len(deletedKeys) > 0 { + logr.Infof("[sql.DeleteAllEndpointStatusesNotInKeys] Deleting endpoints with keys: %v", deletedKeys) + } else { + logr.Debugf("[sql.DeleteAllEndpointStatusesNotInKeys] No endpoints to delete") + } + } + query := "DELETE FROM endpoints WHERE endpoint_key NOT IN (" for i := range keys { query += fmt.Sprintf("$%d,", i+1) - args = append(args, keys[i]) } query = query[:len(query)-1] + ")" // Remove the last comma and add the closing parenthesis result, err = s.db.Exec(query, args...) @@ -586,11 +614,16 @@ func (s *Store) insertEndpointEvent(tx *sql.Tx, endpointID int64, event *endpoin // insertEndpointResult inserts a result in the store func (s *Store) insertEndpointResult(tx *sql.Tx, endpointID int64, result *endpoint.Result) error { + return s.insertEndpointResultWithSuiteID(tx, endpointID, result, nil) +} + +// insertEndpointResultWithSuiteID inserts a result in the store with optional suite linkage +func (s *Store) insertEndpointResultWithSuiteID(tx *sql.Tx, endpointID int64, result *endpoint.Result, suiteResultID *int64) error { var endpointResultID int64 err := tx.QueryRow( ` - INSERT INTO endpoint_results (endpoint_id, success, errors, connected, status, dns_rcode, certificate_expiration, domain_expiration, hostname, ip, duration, timestamp) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + INSERT INTO endpoint_results (endpoint_id, success, errors, connected, status, dns_rcode, certificate_expiration, domain_expiration, hostname, ip, duration, timestamp, suite_result_id) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING endpoint_result_id `, endpointID, @@ -605,6 +638,7 @@ func (s *Store) insertEndpointResult(tx *sql.Tx, endpointID int64, result *endpo result.IP, result.Duration, result.Timestamp.UTC(), + suiteResultID, ).Scan(&endpointResultID) if err != nil { return err @@ -652,7 +686,16 @@ func (s *Store) updateEndpointUptime(tx *sql.Tx, endpointID int64, result *endpo } func (s *Store) getAllEndpointKeys(tx *sql.Tx) (keys []string, err error) { - rows, err := tx.Query("SELECT endpoint_key FROM endpoints ORDER BY endpoint_key") + // Only get endpoints that have at least one result not linked to a suite + // This excludes endpoints that only exist as part of suites + // Using JOIN for better performance than EXISTS subquery + rows, err := tx.Query(` + SELECT DISTINCT e.endpoint_key + FROM endpoints e + INNER JOIN endpoint_results er ON e.endpoint_id = er.endpoint_id + WHERE er.suite_result_id IS NULL + ORDER BY e.endpoint_key + `) if err != nil { return nil, err } @@ -1108,3 +1151,428 @@ func extractKeyAndParamsFromCacheKey(cacheKey string) (string, *paging.EndpointS } return strings.Join(parts[:len(parts)-4], "-"), params, nil } + +// GetAllSuiteStatuses returns all monitored suite statuses +func (s *Store) GetAllSuiteStatuses(params *paging.SuiteStatusParams) ([]*suite.Status, error) { + tx, err := s.db.Begin() + if err != nil { + return nil, err + } + defer tx.Rollback() + + // Get all suites + rows, err := tx.Query(` + SELECT suite_id, suite_key, suite_name, suite_group + FROM suites + ORDER BY suite_key + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var suiteStatuses []*suite.Status + for rows.Next() { + var suiteID int64 + var key, name, group string + if err = rows.Scan(&suiteID, &key, &name, &group); err != nil { + return nil, err + } + + status := &suite.Status{ + Name: name, + Group: group, + Key: key, + Results: []*suite.Result{}, + } + + // Get suite results with pagination + pageSize := 20 + page := 1 + if params != nil { + if params.PageSize > 0 { + pageSize = params.PageSize + } + if params.Page > 0 { + page = params.Page + } + } + + status.Results, err = s.getSuiteResults(tx, suiteID, page, pageSize) + if err != nil { + logr.Errorf("[sql.GetAllSuiteStatuses] Failed to retrieve results for suite_id=%d: %s", suiteID, err.Error()) + } + // Populate Name and Group fields on each result + for _, result := range status.Results { + result.Name = name + result.Group = group + } + + suiteStatuses = append(suiteStatuses, status) + } + + if err = tx.Commit(); err != nil { + return nil, err + } + return suiteStatuses, nil +} + +// GetSuiteStatusByKey returns the suite status for a given key +func (s *Store) GetSuiteStatusByKey(key string, params *paging.SuiteStatusParams) (*suite.Status, error) { + tx, err := s.db.Begin() + if err != nil { + return nil, err + } + defer tx.Rollback() + + var suiteID int64 + var name, group string + err = tx.QueryRow(` + SELECT suite_id, suite_name, suite_group + FROM suites + WHERE suite_key = $1 + `, key).Scan(&suiteID, &name, &group) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + + status := &suite.Status{ + Name: name, + Group: group, + Key: key, + Results: []*suite.Result{}, + } + + // Get suite results with pagination + pageSize := 20 + page := 1 + if params != nil { + if params.PageSize > 0 { + pageSize = params.PageSize + } + if params.Page > 0 { + page = params.Page + } + } + + status.Results, err = s.getSuiteResults(tx, suiteID, page, pageSize) + if err != nil { + logr.Errorf("[sql.GetSuiteStatusByKey] Failed to retrieve results for suite_id=%d: %s", suiteID, err.Error()) + } + // Populate Name and Group fields on each result + for _, result := range status.Results { + result.Name = name + result.Group = group + } + + if err = tx.Commit(); err != nil { + return nil, err + } + return status, nil +} + +// InsertSuiteResult adds the observed result for the specified suite into the store +func (s *Store) InsertSuiteResult(su *suite.Suite, result *suite.Result) error { + tx, err := s.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + // Get or create suite + suiteID, err := s.getSuiteID(tx, su) + if err != nil { + if errors.Is(err, common.ErrSuiteNotFound) { + // Suite doesn't exist in the database, insert it + if suiteID, err = s.insertSuite(tx, su); err != nil { + logr.Errorf("[sql.InsertSuiteResult] Failed to create suite with key=%s: %s", su.Key(), err.Error()) + return err + } + } else { + logr.Errorf("[sql.InsertSuiteResult] Failed to retrieve id of suite with key=%s: %s", su.Key(), err.Error()) + return err + } + } + // Insert suite result + var suiteResultID int64 + err = tx.QueryRow(` + INSERT INTO suite_results (suite_id, success, errors, duration, timestamp) + VALUES ($1, $2, $3, $4, $5) + RETURNING suite_result_id + `, + suiteID, + result.Success, + strings.Join(result.Errors, arraySeparator), + result.Duration.Nanoseconds(), + result.Timestamp.UTC(), // timestamp is the start time + ).Scan(&suiteResultID) + if err != nil { + return err + } + // For each endpoint result in the suite, we need to store them + for _, epResult := range result.EndpointResults { + // Create a temporary endpoint object for storage + ep := &endpoint.Endpoint{ + Name: epResult.Name, + Group: su.Group, + } + // Get or create the endpoint (without suite linkage in endpoints table) + epID, err := s.getEndpointID(tx, ep) + if err != nil { + if errors.Is(err, common.ErrEndpointNotFound) { + // Endpoint doesn't exist, create it + if epID, err = s.insertEndpoint(tx, ep); err != nil { + logr.Errorf("[sql.InsertSuiteResult] Failed to create endpoint %s: %s", epResult.Name, err.Error()) + continue + } + } else { + logr.Errorf("[sql.InsertSuiteResult] Failed to get endpoint %s: %s", epResult.Name, err.Error()) + continue + } + } + // InsertEndpointResult the endpoint result with suite linkage + err = s.insertEndpointResultWithSuiteID(tx, epID, epResult, &suiteResultID) + if err != nil { + logr.Errorf("[sql.InsertSuiteResult] Failed to insert endpoint result for %s: %s", epResult.Name, err.Error()) + } + } + // Clean up old suite results + numberOfResults, err := s.getNumberOfSuiteResultsByID(tx, suiteID) + if err != nil { + logr.Errorf("[sql.InsertSuiteResult] Failed to retrieve total number of results for suite with key=%s: %s", su.Key(), err.Error()) + } else { + if numberOfResults > int64(s.maximumNumberOfResults+resultsAboveMaximumCleanUpThreshold) { + if err = s.deleteOldSuiteResults(tx, suiteID); err != nil { + logr.Errorf("[sql.InsertSuiteResult] Failed to delete old results for suite with key=%s: %s", su.Key(), err.Error()) + } + } + } + if err = tx.Commit(); err != nil { + return err + } + return nil +} + +// DeleteAllSuiteStatusesNotInKeys removes all suite statuses that are not within the keys provided +func (s *Store) DeleteAllSuiteStatusesNotInKeys(keys []string) int { + logr.Debugf("[sql.DeleteAllSuiteStatusesNotInKeys] Called with %d keys", len(keys)) + if len(keys) == 0 { + // Delete all suites + logr.Debugf("[sql.DeleteAllSuiteStatusesNotInKeys] No keys provided, deleting all suites") + result, err := s.db.Exec("DELETE FROM suites") + if err != nil { + logr.Errorf("[sql.DeleteAllSuiteStatusesNotInKeys] Failed to delete all suites: %s", err.Error()) + return 0 + } + rowsAffected, _ := result.RowsAffected() + return int(rowsAffected) + } + args := make([]interface{}, 0, len(keys)) + query := "DELETE FROM suites WHERE suite_key NOT IN (" + for i := range keys { + if i > 0 { + query += "," + } + query += fmt.Sprintf("$%d", i+1) + args = append(args, keys[i]) + } + query += ")" + // First, let's see what we're about to delete + checkQuery := "SELECT suite_key FROM suites WHERE suite_key NOT IN (" + for i := range keys { + if i > 0 { + checkQuery += "," + } + checkQuery += fmt.Sprintf("$%d", i+1) + } + checkQuery += ")" + rows, err := s.db.Query(checkQuery, args...) + if err == nil { + defer rows.Close() + var deletedKeys []string + for rows.Next() { + var key string + if err := rows.Scan(&key); err == nil { + deletedKeys = append(deletedKeys, key) + } + } + if len(deletedKeys) > 0 { + logr.Infof("[sql.DeleteAllSuiteStatusesNotInKeys] Deleting suites with keys: %v", deletedKeys) + } + } + result, err := s.db.Exec(query, args...) + if err != nil { + logr.Errorf("[sql.DeleteAllSuiteStatusesNotInKeys] Failed to delete suites: %s", err.Error()) + return 0 + } + rowsAffected, _ := result.RowsAffected() + return int(rowsAffected) +} + +// Suite helper methods + +// getSuiteID retrieves the suite ID from the database by its key +func (s *Store) getSuiteID(tx *sql.Tx, su *suite.Suite) (int64, error) { + var id int64 + err := tx.QueryRow("SELECT suite_id FROM suites WHERE suite_key = $1", su.Key()).Scan(&id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return 0, common.ErrSuiteNotFound + } + return 0, err + } + return id, nil +} + +// insertSuite inserts a suite in the store and returns the generated id +func (s *Store) insertSuite(tx *sql.Tx, su *suite.Suite) (int64, error) { + var id int64 + err := tx.QueryRow( + "INSERT INTO suites (suite_key, suite_name, suite_group) VALUES ($1, $2, $3) RETURNING suite_id", + su.Key(), + su.Name, + su.Group, + ).Scan(&id) + if err != nil { + return 0, err + } + return id, nil +} + +// getSuiteResults retrieves paginated suite results +func (s *Store) getSuiteResults(tx *sql.Tx, suiteID int64, page, pageSize int) ([]*suite.Result, error) { + rows, err := tx.Query(` + SELECT suite_result_id, success, errors, duration, timestamp + FROM suite_results + WHERE suite_id = $1 + ORDER BY suite_result_id DESC + LIMIT $2 OFFSET $3 + `, + suiteID, + pageSize, + (page-1)*pageSize, + ) + if err != nil { + logr.Errorf("[sql.getSuiteResults] Query failed: %v", err) + return nil, err + } + defer rows.Close() + type suiteResultData struct { + result *suite.Result + id int64 + } + var resultsData []suiteResultData + for rows.Next() { + result := &suite.Result{ + EndpointResults: []*endpoint.Result{}, + } + var suiteResultID int64 + var joinedErrors string + var nanoseconds int64 + err = rows.Scan(&suiteResultID, &result.Success, &joinedErrors, &nanoseconds, &result.Timestamp) + if err != nil { + logr.Errorf("[sql.getSuiteResults] Failed to scan suite result: %s", err.Error()) + continue + } + result.Duration = time.Duration(nanoseconds) + if len(joinedErrors) > 0 { + result.Errors = strings.Split(joinedErrors, arraySeparator) + } + // Store both result and ID together + resultsData = append(resultsData, suiteResultData{ + result: result, + id: suiteResultID, + }) + } + + // Reverse the results to get chronological order (oldest to newest) + for i := len(resultsData)/2 - 1; i >= 0; i-- { + opp := len(resultsData) - 1 - i + resultsData[i], resultsData[opp] = resultsData[opp], resultsData[i] + } + // Fetch endpoint results for each suite result + for _, data := range resultsData { + result := data.result + resultID := data.id + // Query endpoint results for this suite result + epRows, err := tx.Query(` + SELECT + e.endpoint_name, + er.success, + er.errors, + er.duration, + er.timestamp + FROM endpoint_results er + JOIN endpoints e ON er.endpoint_id = e.endpoint_id + WHERE er.suite_result_id = $1 + ORDER BY er.endpoint_result_id + `, resultID) + if err != nil { + logr.Errorf("[sql.getSuiteResults] Failed to get endpoint results for suite_result_id=%d: %s", resultID, err.Error()) + continue + } + epCount := 0 + for epRows.Next() { + epCount++ + var name string + var success bool + var joinedErrors string + var duration int64 + var timestamp time.Time + err = epRows.Scan(&name, &success, &joinedErrors, &duration, ×tamp) + if err != nil { + logr.Errorf("[sql.getSuiteResults] Failed to scan endpoint result: %s", err.Error()) + continue + } + epResult := &endpoint.Result{ + Name: name, + Success: success, + Duration: time.Duration(duration), + Timestamp: timestamp, + } + if len(joinedErrors) > 0 { + epResult.Errors = strings.Split(joinedErrors, arraySeparator) + } + result.EndpointResults = append(result.EndpointResults, epResult) + } + epRows.Close() + if epCount > 0 { + logr.Debugf("[sql.getSuiteResults] Found %d endpoint results for suite_result_id=%d", epCount, resultID) + } + } + // Extract just the results for return + var results []*suite.Result + for _, data := range resultsData { + results = append(results, data.result) + } + return results, nil +} + +// getNumberOfSuiteResultsByID gets the count of results for a suite +func (s *Store) getNumberOfSuiteResultsByID(tx *sql.Tx, suiteID int64) (int64, error) { + var count int64 + err := tx.QueryRow("SELECT COUNT(1) FROM suite_results WHERE suite_id = $1", suiteID).Scan(&count) + return count, err +} + +// deleteOldSuiteResults deletes old suite results beyond the maximum +func (s *Store) deleteOldSuiteResults(tx *sql.Tx, suiteID int64) error { + _, err := tx.Exec(` + DELETE FROM suite_results + WHERE suite_id = $1 + AND suite_result_id NOT IN ( + SELECT suite_result_id + FROM suite_results + WHERE suite_id = $1 + ORDER BY suite_result_id DESC + LIMIT $2 + ) + `, + suiteID, + s.maximumNumberOfResults, + ) + return err +} diff --git a/storage/store/sql/sql_test.go b/storage/store/sql/sql_test.go index a9eb9ed1..13f25df6 100644 --- a/storage/store/sql/sql_test.go +++ b/storage/store/sql/sql_test.go @@ -103,7 +103,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) { now := time.Now().Truncate(time.Hour) now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) - store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-5 * time.Hour), Success: true}) + store.InsertEndpointResult(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-5 * time.Hour), Success: true}) tx, _ := store.db.Begin() oldest, _ := store.getAgeOfOldestEndpointUptimeEntry(tx, 1) @@ -113,7 +113,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) { } // The oldest cache entry should remain at ~5 hours old, because this entry is more recent - store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-3 * time.Hour), Success: true}) + store.InsertEndpointResult(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-3 * time.Hour), Success: true}) tx, _ = store.db.Begin() oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1) @@ -123,7 +123,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) { } // The oldest cache entry should now become at ~8 hours old, because this entry is older - store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-8 * time.Hour), Success: true}) + store.InsertEndpointResult(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-8 * time.Hour), Success: true}) tx, _ = store.db.Begin() oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1) @@ -133,7 +133,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) { } // Since this is one hour before reaching the clean up threshold, the oldest entry should now be this one - store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-(uptimeAgeCleanUpThreshold - time.Hour)), Success: true}) + store.InsertEndpointResult(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-(uptimeAgeCleanUpThreshold - time.Hour)), Success: true}) tx, _ = store.db.Begin() oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1) @@ -144,7 +144,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) { // Since this entry is after the uptimeAgeCleanUpThreshold, both this entry as well as the previous // one should be deleted since they both surpass uptimeRetention - store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-(uptimeAgeCleanUpThreshold + time.Hour)), Success: true}) + store.InsertEndpointResult(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-(uptimeAgeCleanUpThreshold + time.Hour)), Success: true}) tx, _ = store.db.Begin() oldest, _ = store.getAgeOfOldestEndpointUptimeEntry(tx, 1) @@ -182,7 +182,7 @@ func TestStore_HourlyUptimeEntriesAreMergedIntoDailyUptimeEntriesProperly(t *tes for i := scenario.numberOfHours; i > 0; i-- { //fmt.Printf("i: %d (%s)\n", i, now.Add(-time.Duration(i)*time.Hour)) // Create an uptime entry - err := store.Insert(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-time.Duration(i) * time.Hour), Success: true}) + err := store.InsertEndpointResult(&testEndpoint, &endpoint.Result{Timestamp: now.Add(-time.Duration(i) * time.Hour), Success: true}) if err != nil { t.Log(err) } @@ -218,7 +218,7 @@ func TestStore_getEndpointUptime(t *testing.T) { // Add 768 hourly entries (32 days) // Daily entries should be merged from hourly entries automatically for i := 768; i > 0; i-- { - err := store.Insert(&testEndpoint, &endpoint.Result{Timestamp: time.Now().Add(-time.Duration(i) * time.Hour), Duration: time.Second, Success: true}) + err := store.InsertEndpointResult(&testEndpoint, &endpoint.Result{Timestamp: time.Now().Add(-time.Duration(i) * time.Hour), Duration: time.Second, Success: true}) if err != nil { t.Log(err) } @@ -245,7 +245,7 @@ func TestStore_getEndpointUptime(t *testing.T) { t.Errorf("expected uptime to be 1, got %f", uptime) } // Add a new unsuccessful result, which should impact the uptime - err = store.Insert(&testEndpoint, &endpoint.Result{Timestamp: time.Now(), Duration: time.Second, Success: false}) + err = store.InsertEndpointResult(&testEndpoint, &endpoint.Result{Timestamp: time.Now(), Duration: time.Second, Success: false}) if err != nil { t.Log(err) } @@ -280,8 +280,8 @@ func TestStore_InsertCleansUpEventsAndResultsProperly(t *testing.T) { resultsCleanUpThreshold := store.maximumNumberOfResults + resultsAboveMaximumCleanUpThreshold eventsCleanUpThreshold := store.maximumNumberOfEvents + eventsAboveMaximumCleanUpThreshold for i := 0; i < resultsCleanUpThreshold+eventsCleanUpThreshold; i++ { - store.Insert(&testEndpoint, &testSuccessfulResult) - store.Insert(&testEndpoint, &testUnsuccessfulResult) + store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult) + store.InsertEndpointResult(&testEndpoint, &testUnsuccessfulResult) ss, _ := store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams().WithResults(1, storage.DefaultMaximumNumberOfResults*5).WithEvents(1, storage.DefaultMaximumNumberOfEvents*5)) if len(ss.Results) > resultsCleanUpThreshold+1 { t.Errorf("number of results shouldn't have exceeded %d, reached %d", resultsCleanUpThreshold, len(ss.Results)) @@ -296,8 +296,8 @@ func TestStore_InsertWithCaching(t *testing.T) { store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertWithCaching.db", true, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents) defer store.Close() // Add 2 results - store.Insert(&testEndpoint, &testSuccessfulResult) - store.Insert(&testEndpoint, &testSuccessfulResult) + store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult) + store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult) // Verify that they exist endpointStatuses, _ := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 20)) if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 1 { @@ -307,8 +307,8 @@ func TestStore_InsertWithCaching(t *testing.T) { t.Fatalf("expected 2 results, got %d", len(endpointStatuses[0].Results)) } // Add 2 more results - store.Insert(&testEndpoint, &testUnsuccessfulResult) - store.Insert(&testEndpoint, &testUnsuccessfulResult) + store.InsertEndpointResult(&testEndpoint, &testUnsuccessfulResult) + store.InsertEndpointResult(&testEndpoint, &testUnsuccessfulResult) // Verify that they exist endpointStatuses, _ = store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 20)) if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 1 { @@ -329,8 +329,8 @@ func TestStore_InsertWithCaching(t *testing.T) { func TestStore_Persistence(t *testing.T) { path := t.TempDir() + "/TestStore_Persistence.db" store, _ := NewStore("sqlite", path, false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents) - store.Insert(&testEndpoint, &testSuccessfulResult) - store.Insert(&testEndpoint, &testUnsuccessfulResult) + store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult) + store.InsertEndpointResult(&testEndpoint, &testUnsuccessfulResult) if uptime, _ := store.GetUptimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour), time.Now()); uptime != 0.5 { t.Errorf("the uptime over the past 1h should've been 0.5, got %f", uptime) } @@ -425,12 +425,12 @@ func TestStore_Save(t *testing.T) { func TestStore_SanityCheck(t *testing.T) { store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_SanityCheck.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents) defer store.Close() - store.Insert(&testEndpoint, &testSuccessfulResult) + store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult) endpointStatuses, _ := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams()) if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 1 { t.Fatalf("expected 1 EndpointStatus, got %d", numberOfEndpointStatuses) } - store.Insert(&testEndpoint, &testUnsuccessfulResult) + store.InsertEndpointResult(&testEndpoint, &testUnsuccessfulResult) // Both results inserted are for the same endpoint, therefore, the count shouldn't have increased endpointStatuses, _ = store.GetAllEndpointStatuses(paging.NewEndpointStatusParams()) if numberOfEndpointStatuses := len(endpointStatuses); numberOfEndpointStatuses != 1 { @@ -541,7 +541,7 @@ func TestStore_NoRows(t *testing.T) { func TestStore_BrokenSchema(t *testing.T) { store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_BrokenSchema.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents) defer store.Close() - if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil { + if err := store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult); err != nil { t.Fatal("expected no error, got", err.Error()) } if _, err := store.GetAverageResponseTimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour), time.Now()); err != nil { @@ -553,7 +553,7 @@ func TestStore_BrokenSchema(t *testing.T) { // Break _, _ = store.db.Exec("DROP TABLE endpoints") // And now we'll try to insert something in our broken schema - if err := store.Insert(&testEndpoint, &testSuccessfulResult); err == nil { + if err := store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult); err == nil { t.Fatal("expected an error") } if _, err := store.GetAverageResponseTimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour), time.Now()); err == nil { @@ -576,12 +576,12 @@ func TestStore_BrokenSchema(t *testing.T) { t.Fatal("schema should've been repaired") } store.Clear() - if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil { + if err := store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult); err != nil { t.Fatal("expected no error, got", err.Error()) } // Break _, _ = store.db.Exec("DROP TABLE endpoint_events") - if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil { + if err := store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult); err != nil { t.Fatal("expected no error, because this should silently fails, got", err.Error()) } if _, err := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 1).WithEvents(1, 1)); err != nil { @@ -592,28 +592,28 @@ func TestStore_BrokenSchema(t *testing.T) { t.Fatal("schema should've been repaired") } store.Clear() - if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil { + if err := store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult); err != nil { t.Fatal("expected no error, got", err.Error()) } // Break _, _ = store.db.Exec("DROP TABLE endpoint_results") - if err := store.Insert(&testEndpoint, &testSuccessfulResult); err == nil { + if err := store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult); err == nil { t.Fatal("expected an error") } - if _, err := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 1).WithEvents(1, 1)); err != nil { - t.Fatal("expected no error, because this should silently fail, got", err.Error()) + if _, err := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 1).WithEvents(1, 1)); err == nil { + t.Fatal("expected an error") } // Repair if err := store.createSchema(); err != nil { t.Fatal("schema should've been repaired") } store.Clear() - if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil { + if err := store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult); err != nil { t.Fatal("expected no error, got", err.Error()) } // Break _, _ = store.db.Exec("DROP TABLE endpoint_result_conditions") - if err := store.Insert(&testEndpoint, &testSuccessfulResult); err == nil { + if err := store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult); err == nil { t.Fatal("expected an error") } // Repair @@ -621,12 +621,12 @@ func TestStore_BrokenSchema(t *testing.T) { t.Fatal("schema should've been repaired") } store.Clear() - if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil { + if err := store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult); err != nil { t.Fatal("expected no error, got", err.Error()) } // Break _, _ = store.db.Exec("DROP TABLE endpoint_uptimes") - if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil { + if err := store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult); err != nil { t.Fatal("expected no error, because this should silently fails, got", err.Error()) } if _, err := store.GetAverageResponseTimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour), time.Now()); err == nil { @@ -857,8 +857,8 @@ func TestStore_DeleteAllTriggeredAlertsNotInChecksumsByEndpoint(t *testing.T) { func TestStore_HasEndpointStatusNewerThan(t *testing.T) { store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_HasEndpointStatusNewerThan.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents) defer store.Close() - // Insert an endpoint status - if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil { + // InsertEndpointResult an endpoint status + if err := store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult); err != nil { t.Fatal("expected no error, got", err.Error()) } // Check if it has a status newer than 1 hour ago diff --git a/storage/store/store.go b/storage/store/store.go index 7fb0ae06..693faa62 100644 --- a/storage/store/store.go +++ b/storage/store/store.go @@ -6,6 +6,7 @@ import ( "github.com/TwiN/gatus/v5/alerting/alert" "github.com/TwiN/gatus/v5/config/endpoint" + "github.com/TwiN/gatus/v5/config/suite" "github.com/TwiN/gatus/v5/storage" "github.com/TwiN/gatus/v5/storage/store/common/paging" "github.com/TwiN/gatus/v5/storage/store/memory" @@ -19,12 +20,18 @@ type Store interface { // with a subset of endpoint.Result defined by the page and pageSize parameters GetAllEndpointStatuses(params *paging.EndpointStatusParams) ([]*endpoint.Status, error) + // GetAllSuiteStatuses returns all monitored suite statuses + GetAllSuiteStatuses(params *paging.SuiteStatusParams) ([]*suite.Status, error) + // GetEndpointStatus returns the endpoint status for a given endpoint name in the given group GetEndpointStatus(groupName, endpointName string, params *paging.EndpointStatusParams) (*endpoint.Status, error) // GetEndpointStatusByKey returns the endpoint status for a given key GetEndpointStatusByKey(key string, params *paging.EndpointStatusParams) (*endpoint.Status, error) + // GetSuiteStatusByKey returns the suite status for a given key + GetSuiteStatusByKey(key string, params *paging.SuiteStatusParams) (*suite.Status, error) + // GetUptimeByKey returns the uptime percentage during a time range GetUptimeByKey(key string, from, to time.Time) (float64, error) @@ -34,14 +41,20 @@ type Store interface { // GetHourlyAverageResponseTimeByKey returns a map of hourly (key) average response time in milliseconds (value) during a time range GetHourlyAverageResponseTimeByKey(key string, from, to time.Time) (map[int64]int, error) - // Insert adds the observed result for the specified endpoint into the store - Insert(ep *endpoint.Endpoint, result *endpoint.Result) error + // InsertEndpointResult adds the observed result for the specified endpoint into the store + InsertEndpointResult(ep *endpoint.Endpoint, result *endpoint.Result) error + + // InsertSuiteResult adds the observed result for the specified suite into the store + InsertSuiteResult(s *suite.Suite, result *suite.Result) error // DeleteAllEndpointStatusesNotInKeys removes all Status that are not within the keys provided // // Used to delete endpoints that have been persisted but are no longer part of the configured endpoints DeleteAllEndpointStatusesNotInKeys(keys []string) int + // DeleteAllSuiteStatusesNotInKeys removes all suite statuses that are not within the keys provided + DeleteAllSuiteStatusesNotInKeys(keys []string) int + // GetTriggeredEndpointAlert returns whether the triggered alert for the specified endpoint as well as the necessary information to resolve it GetTriggeredEndpointAlert(ep *endpoint.Endpoint, alert *alert.Alert) (exists bool, resolveKey string, numberOfSuccessesInARow int, err error) diff --git a/storage/store/store_bench_test.go b/storage/store/store_bench_test.go index dd4a0dd3..c3f53f75 100644 --- a/storage/store/store_bench_test.go +++ b/storage/store/store_bench_test.go @@ -56,9 +56,9 @@ func BenchmarkStore_GetAllEndpointStatuses(b *testing.B) { for i := 0; i < numberOfEndpointsToCreate; i++ { ep := testEndpoint ep.Name = "endpoint" + strconv.Itoa(i) - // Insert 20 results for each endpoint + // InsertEndpointResult 20 results for each endpoint for j := 0; j < 20; j++ { - scenario.Store.Insert(&ep, &testSuccessfulResult) + scenario.Store.InsertEndpointResult(&ep, &testSuccessfulResult) } } // Run the scenarios @@ -131,7 +131,7 @@ func BenchmarkStore_Insert(b *testing.B) { result = testSuccessfulResult } result.Timestamp = time.Now() - scenario.Store.Insert(&testEndpoint, &result) + scenario.Store.InsertEndpointResult(&testEndpoint, &result) n++ } }) @@ -144,7 +144,7 @@ func BenchmarkStore_Insert(b *testing.B) { result = testSuccessfulResult } result.Timestamp = time.Now() - scenario.Store.Insert(&testEndpoint, &result) + scenario.Store.InsertEndpointResult(&testEndpoint, &result) } } b.ReportAllocs() @@ -192,8 +192,8 @@ func BenchmarkStore_GetEndpointStatusByKey(b *testing.B) { } for _, scenario := range scenarios { for i := 0; i < 50; i++ { - scenario.Store.Insert(&testEndpoint, &testSuccessfulResult) - scenario.Store.Insert(&testEndpoint, &testUnsuccessfulResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &testUnsuccessfulResult) } b.Run(scenario.Name, func(b *testing.B) { if scenario.Parallel { diff --git a/storage/store/store_test.go b/storage/store/store_test.go index 1dcfb5ac..42cdc5dd 100644 --- a/storage/store/store_test.go +++ b/storage/store/store_test.go @@ -136,8 +136,8 @@ func TestStore_GetEndpointStatusByKey(t *testing.T) { thirdResult.Timestamp = now for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { - scenario.Store.Insert(&testEndpoint, &firstResult) - scenario.Store.Insert(&testEndpoint, &secondResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &firstResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &secondResult) endpointStatus, err := scenario.Store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams().WithEvents(1, storage.DefaultMaximumNumberOfEvents).WithResults(1, storage.DefaultMaximumNumberOfResults)) if err != nil { t.Fatal("shouldn't have returned an error, got", err.Error()) @@ -157,7 +157,7 @@ func TestStore_GetEndpointStatusByKey(t *testing.T) { if endpointStatus.Results[0].Timestamp.After(endpointStatus.Results[1].Timestamp) { t.Error("The result at index 0 should've been older than the result at index 1") } - scenario.Store.Insert(&testEndpoint, &thirdResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &thirdResult) endpointStatus, err = scenario.Store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams().WithEvents(1, storage.DefaultMaximumNumberOfEvents).WithResults(1, storage.DefaultMaximumNumberOfResults)) if err != nil { t.Fatal("shouldn't have returned an error, got", err.Error()) @@ -175,7 +175,7 @@ func TestStore_GetEndpointStatusForMissingStatusReturnsNil(t *testing.T) { defer cleanUp(scenarios) for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { - scenario.Store.Insert(&testEndpoint, &testSuccessfulResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult) endpointStatus, err := scenario.Store.GetEndpointStatus("nonexistantgroup", "nonexistantname", paging.NewEndpointStatusParams().WithEvents(1, storage.DefaultMaximumNumberOfEvents).WithResults(1, storage.DefaultMaximumNumberOfResults)) if !errors.Is(err, common.ErrEndpointNotFound) { t.Error("should've returned ErrEndpointNotFound, got", err) @@ -206,8 +206,8 @@ func TestStore_GetAllEndpointStatuses(t *testing.T) { defer cleanUp(scenarios) for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { - scenario.Store.Insert(&testEndpoint, &testSuccessfulResult) - scenario.Store.Insert(&testEndpoint, &testUnsuccessfulResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &testUnsuccessfulResult) endpointStatuses, err := scenario.Store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 20)) if err != nil { t.Error("shouldn't have returned an error, got", err.Error()) @@ -230,10 +230,10 @@ func TestStore_GetAllEndpointStatuses(t *testing.T) { t.Run(scenario.Name+"-page-2", func(t *testing.T) { otherEndpoint := testEndpoint otherEndpoint.Name = testEndpoint.Name + "-other" - scenario.Store.Insert(&testEndpoint, &testSuccessfulResult) - scenario.Store.Insert(&otherEndpoint, &testSuccessfulResult) - scenario.Store.Insert(&otherEndpoint, &testSuccessfulResult) - scenario.Store.Insert(&otherEndpoint, &testSuccessfulResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult) + scenario.Store.InsertEndpointResult(&otherEndpoint, &testSuccessfulResult) + scenario.Store.InsertEndpointResult(&otherEndpoint, &testSuccessfulResult) + scenario.Store.InsertEndpointResult(&otherEndpoint, &testSuccessfulResult) endpointStatuses, err := scenario.Store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(2, 2)) if err != nil { t.Error("shouldn't have returned an error, got", err.Error()) @@ -268,8 +268,8 @@ func TestStore_GetAllEndpointStatusesWithResultsAndEvents(t *testing.T) { secondResult := testUnsuccessfulResult for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { - scenario.Store.Insert(&testEndpoint, &firstResult) - scenario.Store.Insert(&testEndpoint, &secondResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &firstResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &secondResult) // Can't be bothered dealing with timezone issues on the worker that runs the automated tests endpointStatuses, err := scenario.Store.GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(1, 20).WithEvents(1, 50)) if err != nil { @@ -302,8 +302,8 @@ func TestStore_GetEndpointStatusPage1IsHasMoreRecentResultsThanPage2(t *testing. secondResult.Timestamp = now for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { - scenario.Store.Insert(&testEndpoint, &firstResult) - scenario.Store.Insert(&testEndpoint, &secondResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &firstResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &secondResult) endpointStatusPage1, err := scenario.Store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams().WithResults(1, 1)) if err != nil { t.Error("shouldn't have returned an error, got", err.Error()) @@ -345,8 +345,8 @@ func TestStore_GetUptimeByKey(t *testing.T) { if _, err := scenario.Store.GetUptimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour), time.Now()); err != common.ErrEndpointNotFound { t.Errorf("should've returned not found because there's nothing yet, got %v", err) } - scenario.Store.Insert(&testEndpoint, &firstResult) - scenario.Store.Insert(&testEndpoint, &secondResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &firstResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &secondResult) if uptime, _ := scenario.Store.GetUptimeByKey(testEndpoint.Key(), now.Add(-time.Hour), time.Now()); uptime != 0.5 { t.Errorf("the uptime over the past 1h should've been 0.5, got %f", uptime) } @@ -380,10 +380,10 @@ func TestStore_GetAverageResponseTimeByKey(t *testing.T) { fourthResult.Timestamp = now for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { - scenario.Store.Insert(&testEndpoint, &firstResult) - scenario.Store.Insert(&testEndpoint, &secondResult) - scenario.Store.Insert(&testEndpoint, &thirdResult) - scenario.Store.Insert(&testEndpoint, &fourthResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &firstResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &secondResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &thirdResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &fourthResult) if averageResponseTime, err := scenario.Store.GetAverageResponseTimeByKey(testEndpoint.Key(), now.Add(-48*time.Hour), now.Add(-24*time.Hour)); err == nil { if averageResponseTime != 0 { t.Errorf("expected average response time to be 0ms, got %v", averageResponseTime) @@ -437,10 +437,10 @@ func TestStore_GetHourlyAverageResponseTimeByKey(t *testing.T) { fourthResult.Timestamp = now for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { - scenario.Store.Insert(&testEndpoint, &firstResult) - scenario.Store.Insert(&testEndpoint, &secondResult) - scenario.Store.Insert(&testEndpoint, &thirdResult) - scenario.Store.Insert(&testEndpoint, &fourthResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &firstResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &secondResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &thirdResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &fourthResult) hourlyAverageResponseTime, err := scenario.Store.GetHourlyAverageResponseTimeByKey(testEndpoint.Key(), now.Add(-24*time.Hour), now) if err != nil { t.Error("shouldn't have returned an error, got", err) @@ -468,8 +468,8 @@ func TestStore_Insert(t *testing.T) { secondResult.Timestamp = now for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { - scenario.Store.Insert(&testEndpoint, &firstResult) - scenario.Store.Insert(&testEndpoint, &secondResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &firstResult) + scenario.Store.InsertEndpointResult(&testEndpoint, &secondResult) ss, err := scenario.Store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams().WithEvents(1, storage.DefaultMaximumNumberOfEvents).WithResults(1, storage.DefaultMaximumNumberOfResults)) if err != nil { t.Error("shouldn't have returned an error, got", err) @@ -545,8 +545,8 @@ func TestStore_DeleteAllEndpointStatusesNotInKeys(t *testing.T) { r := &testSuccessfulResult for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { - scenario.Store.Insert(&firstEndpoint, r) - scenario.Store.Insert(&secondEndpoint, r) + scenario.Store.InsertEndpointResult(&firstEndpoint, r) + scenario.Store.InsertEndpointResult(&secondEndpoint, r) if ss, _ := scenario.Store.GetEndpointStatusByKey(firstEndpoint.Key(), paging.NewEndpointStatusParams()); ss == nil { t.Fatal("firstEndpoint should exist, got", ss) } diff --git a/watchdog/endpoint.go b/watchdog/endpoint.go new file mode 100644 index 00000000..0b1e7c23 --- /dev/null +++ b/watchdog/endpoint.go @@ -0,0 +1,80 @@ +package watchdog + +import ( + "context" + "time" + + "github.com/TwiN/gatus/v5/config" + "github.com/TwiN/gatus/v5/config/endpoint" + "github.com/TwiN/gatus/v5/metrics" + "github.com/TwiN/gatus/v5/storage/store" + "github.com/TwiN/logr" +) + +// monitorEndpoint a single endpoint in a loop +func monitorEndpoint(ep *endpoint.Endpoint, cfg *config.Config, extraLabels []string, ctx context.Context) { + // Run it immediately on start + executeEndpoint(ep, cfg, extraLabels) + // Loop for the next executions + ticker := time.NewTicker(ep.Interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + logr.Warnf("[watchdog.monitorEndpoint] Canceling current execution of group=%s; endpoint=%s; key=%s", ep.Group, ep.Name, ep.Key()) + return + case <-ticker.C: + executeEndpoint(ep, cfg, extraLabels) + } + } + // Just in case somebody wandered all the way to here and wonders, "what about ExternalEndpoints?" + // Alerting is checked every time an external endpoint is pushed to Gatus, so they're not monitored + // periodically like they are for normal endpoints. +} + +func executeEndpoint(ep *endpoint.Endpoint, cfg *config.Config, extraLabels []string) { + // Acquire semaphore to limit concurrent endpoint monitoring + if err := monitoringSemaphore.Acquire(ctx, 1); err != nil { + // Only fails if context is cancelled (during shutdown) + logr.Debugf("[watchdog.executeEndpoint] Context cancelled, skipping execution: %s", err.Error()) + return + } + defer monitoringSemaphore.Release(1) + // If there's a connectivity checker configured, check if Gatus has internet connectivity + if cfg.Connectivity != nil && cfg.Connectivity.Checker != nil && !cfg.Connectivity.Checker.IsConnected() { + logr.Infof("[watchdog.executeEndpoint] No connectivity; skipping execution") + return + } + logr.Debugf("[watchdog.executeEndpoint] Monitoring group=%s; endpoint=%s; key=%s", ep.Group, ep.Name, ep.Key()) + result := ep.EvaluateHealth() + if cfg.Metrics { + metrics.PublishMetricsForEndpoint(ep, result, extraLabels) + } + UpdateEndpointStatus(ep, result) + if logr.GetThreshold() == logr.LevelDebug && !result.Success { + logr.Debugf("[watchdog.executeEndpoint] Monitored group=%s; endpoint=%s; key=%s; success=%v; errors=%d; duration=%s; body=%s", ep.Group, ep.Name, ep.Key(), result.Success, len(result.Errors), result.Duration.Round(time.Millisecond), result.Body) + } else { + logr.Infof("[watchdog.executeEndpoint] Monitored group=%s; endpoint=%s; key=%s; success=%v; errors=%d; duration=%s", ep.Group, ep.Name, ep.Key(), result.Success, len(result.Errors), result.Duration.Round(time.Millisecond)) + } + inEndpointMaintenanceWindow := false + for _, maintenanceWindow := range ep.MaintenanceWindows { + if maintenanceWindow.IsUnderMaintenance() { + logr.Debug("[watchdog.executeEndpoint] Under endpoint maintenance window") + inEndpointMaintenanceWindow = true + } + } + if !cfg.Maintenance.IsUnderMaintenance() && !inEndpointMaintenanceWindow { + // TODO: Consider moving this after the monitoring lock is unlocked? I mean, how much noise can a single alerting provider cause... + HandleAlerting(ep, result, cfg.Alerting) + } else { + logr.Debug("[watchdog.executeEndpoint] Not handling alerting because currently in the maintenance window") + } + logr.Debugf("[watchdog.executeEndpoint] Waiting for interval=%s before monitoring group=%s endpoint=%s (key=%s) again", ep.Interval, ep.Group, ep.Name, ep.Key()) +} + +// UpdateEndpointStatus persists the endpoint result in the storage +func UpdateEndpointStatus(ep *endpoint.Endpoint, result *endpoint.Result) { + if err := store.Get().InsertEndpointResult(ep, result); err != nil { + logr.Errorf("[watchdog.UpdateEndpointStatus] Failed to insert result in storage: %s", err.Error()) + } +} diff --git a/watchdog/external_endpoint.go b/watchdog/external_endpoint.go new file mode 100644 index 00000000..3f8409b6 --- /dev/null +++ b/watchdog/external_endpoint.go @@ -0,0 +1,83 @@ +package watchdog + +import ( + "context" + "time" + + "github.com/TwiN/gatus/v5/config" + "github.com/TwiN/gatus/v5/config/endpoint" + "github.com/TwiN/gatus/v5/metrics" + "github.com/TwiN/gatus/v5/storage/store" + "github.com/TwiN/logr" +) + +func monitorExternalEndpointHeartbeat(ee *endpoint.ExternalEndpoint, cfg *config.Config, extraLabels []string, ctx context.Context) { + ticker := time.NewTicker(ee.Heartbeat.Interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + logr.Warnf("[watchdog.monitorExternalEndpointHeartbeat] Canceling current execution of group=%s; endpoint=%s; key=%s", ee.Group, ee.Name, ee.Key()) + return + case <-ticker.C: + executeExternalEndpointHeartbeat(ee, cfg, extraLabels) + } + } +} + +func executeExternalEndpointHeartbeat(ee *endpoint.ExternalEndpoint, cfg *config.Config, extraLabels []string) { + // Acquire semaphore to limit concurrent external endpoint monitoring + if err := monitoringSemaphore.Acquire(ctx, 1); err != nil { + // Only fails if context is cancelled (during shutdown) + logr.Debugf("[watchdog.executeExternalEndpointHeartbeat] Context cancelled, skipping execution: %s", err.Error()) + return + } + defer monitoringSemaphore.Release(1) + // If there's a connectivity checker configured, check if Gatus has internet connectivity + if cfg.Connectivity != nil && cfg.Connectivity.Checker != nil && !cfg.Connectivity.Checker.IsConnected() { + logr.Infof("[watchdog.monitorExternalEndpointHeartbeat] No connectivity; skipping execution") + return + } + logr.Debugf("[watchdog.monitorExternalEndpointHeartbeat] Checking heartbeat for group=%s; endpoint=%s; key=%s", ee.Group, ee.Name, ee.Key()) + convertedEndpoint := ee.ToEndpoint() + hasReceivedResultWithinHeartbeatInterval, err := store.Get().HasEndpointStatusNewerThan(ee.Key(), time.Now().Add(-ee.Heartbeat.Interval)) + if err != nil { + logr.Errorf("[watchdog.monitorExternalEndpointHeartbeat] Failed to check if endpoint has received a result within the heartbeat interval: %s", err.Error()) + return + } + if hasReceivedResultWithinHeartbeatInterval { + // If we received a result within the heartbeat interval, we don't want to create a successful result, so we + // skip the rest. We don't have to worry about alerting or metrics, because if the previous heartbeat failed + // while this one succeeds, it implies that there was a new result pushed, and that result being pushed + // should've resolved the alert. + logr.Infof("[watchdog.monitorExternalEndpointHeartbeat] Checked heartbeat for group=%s; endpoint=%s; key=%s; success=%v; errors=%d", ee.Group, ee.Name, ee.Key(), hasReceivedResultWithinHeartbeatInterval, 0) + return + } + // All code after this point assumes the heartbeat failed + result := &endpoint.Result{ + Timestamp: time.Now(), + Success: false, + Errors: []string{"heartbeat: no update received within " + ee.Heartbeat.Interval.String()}, + } + if cfg.Metrics { + metrics.PublishMetricsForEndpoint(convertedEndpoint, result, extraLabels) + } + UpdateEndpointStatus(convertedEndpoint, result) + logr.Infof("[watchdog.monitorExternalEndpointHeartbeat] Checked heartbeat for group=%s; endpoint=%s; key=%s; success=%v; errors=%d; duration=%s", ee.Group, ee.Name, ee.Key(), result.Success, len(result.Errors), result.Duration.Round(time.Millisecond)) + inEndpointMaintenanceWindow := false + for _, maintenanceWindow := range ee.MaintenanceWindows { + if maintenanceWindow.IsUnderMaintenance() { + logr.Debug("[watchdog.monitorExternalEndpointHeartbeat] Under endpoint maintenance window") + inEndpointMaintenanceWindow = true + } + } + if !cfg.Maintenance.IsUnderMaintenance() && !inEndpointMaintenanceWindow { + HandleAlerting(convertedEndpoint, result, cfg.Alerting) + // Sync the failure/success counters back to the external endpoint + ee.NumberOfSuccessesInARow = convertedEndpoint.NumberOfSuccessesInARow + ee.NumberOfFailuresInARow = convertedEndpoint.NumberOfFailuresInARow + } else { + logr.Debug("[watchdog.monitorExternalEndpointHeartbeat] Not handling alerting because currently in the maintenance window") + } + logr.Debugf("[watchdog.monitorExternalEndpointHeartbeat] Waiting for interval=%s before checking heartbeat for group=%s endpoint=%s (key=%s) again", ee.Heartbeat.Interval, ee.Group, ee.Name, ee.Key()) +} diff --git a/watchdog/suite.go b/watchdog/suite.go new file mode 100644 index 00000000..e48a5a77 --- /dev/null +++ b/watchdog/suite.go @@ -0,0 +1,86 @@ +package watchdog + +import ( + "context" + "time" + + "github.com/TwiN/gatus/v5/config" + "github.com/TwiN/gatus/v5/config/suite" + "github.com/TwiN/gatus/v5/metrics" + "github.com/TwiN/gatus/v5/storage/store" + "github.com/TwiN/logr" +) + +// monitorSuite monitors a suite by executing it at regular intervals +func monitorSuite(s *suite.Suite, cfg *config.Config, extraLabels []string, ctx context.Context) { + // Execute immediately on start + executeSuite(s, cfg, extraLabels) + // Set up ticker for periodic execution + ticker := time.NewTicker(s.Interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + logr.Warnf("[watchdog.monitorSuite] Canceling monitoring for suite=%s", s.Name) + return + case <-ticker.C: + executeSuite(s, cfg, extraLabels) + } + } +} + +// executeSuite executes a suite with proper concurrency control +func executeSuite(s *suite.Suite, cfg *config.Config, extraLabels []string) { + // Acquire semaphore to limit concurrent suite monitoring + if err := monitoringSemaphore.Acquire(ctx, 1); err != nil { + // Only fails if context is cancelled (during shutdown) + logr.Debugf("[watchdog.executeSuite] Context cancelled, skipping execution: %s", err.Error()) + return + } + defer monitoringSemaphore.Release(1) + // Check connectivity if configured + if cfg.Connectivity != nil && cfg.Connectivity.Checker != nil && !cfg.Connectivity.Checker.IsConnected() { + logr.Infof("[watchdog.executeSuite] No connectivity; skipping suite=%s", s.Name) + return + } + logr.Debugf("[watchdog.executeSuite] Monitoring group=%s; suite=%s; key=%s", s.Group, s.Name, s.Key()) + // Execute the suite using its Execute method + result := s.Execute() + // Publish metrics for the suite execution + if cfg.Metrics { + metrics.PublishMetricsForSuite(s, result, extraLabels) + } + // Store individual endpoint results and handle alerting + for i, ep := range s.Endpoints { + if i < len(result.EndpointResults) { + epResult := result.EndpointResults[i] + // Store the endpoint result + UpdateEndpointStatus(ep, epResult) + // Handle alerting if configured and not under maintenance + if cfg.Alerting != nil && !cfg.Maintenance.IsUnderMaintenance() { + // Check if endpoint is under maintenance + inEndpointMaintenanceWindow := false + for _, maintenanceWindow := range ep.MaintenanceWindows { + if maintenanceWindow.IsUnderMaintenance() { + logr.Debug("[watchdog.executeSuite] Endpoint under maintenance window") + inEndpointMaintenanceWindow = true + break + } + } + if !inEndpointMaintenanceWindow { + HandleAlerting(ep, epResult, cfg.Alerting) + } + } + } + } + logr.Infof("[watchdog.executeSuite] Completed suite=%s; success=%v; errors=%d; duration=%v; endpoints_executed=%d/%d", s.Name, result.Success, len(result.Errors), result.Duration, len(result.EndpointResults), len(s.Endpoints)) + // Store result in database + UpdateSuiteStatus(s, result) +} + +// UpdateSuiteStatus persists the suite result in the database +func UpdateSuiteStatus(s *suite.Suite, result *suite.Result) { + if err := store.Get().InsertSuiteResult(s, result); err != nil { + logr.Errorf("[watchdog.executeSuite] Failed to insert suite result for suite=%s: %v", s.Name, err) + } +} diff --git a/watchdog/watchdog.go b/watchdog/watchdog.go index be3ce607..c172cc5a 100644 --- a/watchdog/watchdog.go +++ b/watchdog/watchdog.go @@ -2,23 +2,22 @@ package watchdog import ( "context" - "sync" "time" - "github.com/TwiN/gatus/v5/alerting" "github.com/TwiN/gatus/v5/config" - "github.com/TwiN/gatus/v5/config/connectivity" - "github.com/TwiN/gatus/v5/config/endpoint" - "github.com/TwiN/gatus/v5/config/maintenance" - "github.com/TwiN/gatus/v5/metrics" - "github.com/TwiN/gatus/v5/storage/store" - "github.com/TwiN/logr" + "golang.org/x/sync/semaphore" +) + +const ( + // UnlimitedConcurrencyWeight is the semaphore weight used when concurrency is set to 0 (unlimited). + // This provides a practical upper limit while allowing very high concurrency for large deployments. + UnlimitedConcurrencyWeight = 10000 ) var ( - // monitoringMutex is used to prevent multiple endpoint from being evaluated at the same time. + // monitoringSemaphore is used to limit the number of endpoints/suites that can be evaluated concurrently. // Without this, conditions using response time may become inaccurate. - monitoringMutex sync.Mutex + monitoringSemaphore *semaphore.Weighted ctx context.Context cancelFunc context.CancelFunc @@ -27,12 +26,20 @@ var ( // Monitor loops over each endpoint and starts a goroutine to monitor each endpoint separately func Monitor(cfg *config.Config) { ctx, cancelFunc = context.WithCancel(context.Background()) + // Initialize semaphore based on concurrency configuration + if cfg.Concurrency == 0 { + // Unlimited concurrency - use a very high limit + monitoringSemaphore = semaphore.NewWeighted(UnlimitedConcurrencyWeight) + } else { + // Limited concurrency based on configuration + monitoringSemaphore = semaphore.NewWeighted(int64(cfg.Concurrency)) + } extraLabels := cfg.GetUniqueExtraMetricLabels() for _, endpoint := range cfg.Endpoints { if endpoint.IsEnabled() { // To prevent multiple requests from running at the same time, we'll wait for a little before each iteration - time.Sleep(777 * time.Millisecond) - go monitor(endpoint, cfg.Alerting, cfg.Maintenance, cfg.Connectivity, cfg.DisableMonitoringLock, cfg.Metrics, extraLabels, ctx) + time.Sleep(222 * time.Millisecond) + go monitorEndpoint(endpoint, cfg, extraLabels, ctx) } } for _, externalEndpoint := range cfg.ExternalEndpoints { @@ -40,153 +47,27 @@ func Monitor(cfg *config.Config) { // If the external endpoint does not use heartbeat, then it does not need to be monitored periodically, because // alerting is checked every time an external endpoint is pushed to Gatus, unlike normal endpoints. if externalEndpoint.IsEnabled() && externalEndpoint.Heartbeat.Interval > 0 { - go monitorExternalEndpointHeartbeat(externalEndpoint, cfg.Alerting, cfg.Maintenance, cfg.Connectivity, cfg.DisableMonitoringLock, cfg.Metrics, ctx, extraLabels) + go monitorExternalEndpointHeartbeat(externalEndpoint, cfg, extraLabels, ctx) } } -} - -// monitor a single endpoint in a loop -func monitor(ep *endpoint.Endpoint, alertingConfig *alerting.Config, maintenanceConfig *maintenance.Config, connectivityConfig *connectivity.Config, disableMonitoringLock bool, enabledMetrics bool, extraLabels []string, ctx context.Context) { - // Run it immediately on start - execute(ep, alertingConfig, maintenanceConfig, connectivityConfig, disableMonitoringLock, enabledMetrics, extraLabels) - // Loop for the next executions - ticker := time.NewTicker(ep.Interval) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - logr.Warnf("[watchdog.monitor] Canceling current execution of group=%s; endpoint=%s; key=%s", ep.Group, ep.Name, ep.Key()) - return - case <-ticker.C: - execute(ep, alertingConfig, maintenanceConfig, connectivityConfig, disableMonitoringLock, enabledMetrics, extraLabels) + for _, suite := range cfg.Suites { + if suite.IsEnabled() { + time.Sleep(222 * time.Millisecond) + go monitorSuite(suite, cfg, extraLabels, ctx) } } - // Just in case somebody wandered all the way to here and wonders, "what about ExternalEndpoints?" - // Alerting is checked every time an external endpoint is pushed to Gatus, so they're not monitored - // periodically like they are for normal endpoints. -} - -func execute(ep *endpoint.Endpoint, alertingConfig *alerting.Config, maintenanceConfig *maintenance.Config, connectivityConfig *connectivity.Config, disableMonitoringLock bool, enabledMetrics bool, extraLabels []string) { - if !disableMonitoringLock { - // By placing the lock here, we prevent multiple endpoints from being monitored at the exact same time, which - // could cause performance issues and return inaccurate results - monitoringMutex.Lock() - defer monitoringMutex.Unlock() - } - // If there's a connectivity checker configured, check if Gatus has internet connectivity - if connectivityConfig != nil && connectivityConfig.Checker != nil && !connectivityConfig.Checker.IsConnected() { - logr.Infof("[watchdog.execute] No connectivity; skipping execution") - return - } - logr.Debugf("[watchdog.execute] Monitoring group=%s; endpoint=%s; key=%s", ep.Group, ep.Name, ep.Key()) - result := ep.EvaluateHealth() - if enabledMetrics { - metrics.PublishMetricsForEndpoint(ep, result, extraLabels) - } - UpdateEndpointStatuses(ep, result) - if logr.GetThreshold() == logr.LevelDebug && !result.Success { - logr.Debugf("[watchdog.execute] Monitored group=%s; endpoint=%s; key=%s; success=%v; errors=%d; duration=%s; body=%s", ep.Group, ep.Name, ep.Key(), result.Success, len(result.Errors), result.Duration.Round(time.Millisecond), result.Body) - } else { - logr.Infof("[watchdog.execute] Monitored group=%s; endpoint=%s; key=%s; success=%v; errors=%d; duration=%s", ep.Group, ep.Name, ep.Key(), result.Success, len(result.Errors), result.Duration.Round(time.Millisecond)) - } - inEndpointMaintenanceWindow := false - for _, maintenanceWindow := range ep.MaintenanceWindows { - if maintenanceWindow.IsUnderMaintenance() { - logr.Debug("[watchdog.execute] Under endpoint maintenance window") - inEndpointMaintenanceWindow = true - } - } - if !maintenanceConfig.IsUnderMaintenance() && !inEndpointMaintenanceWindow { - // TODO: Consider moving this after the monitoring lock is unlocked? I mean, how much noise can a single alerting provider cause... - HandleAlerting(ep, result, alertingConfig) - } else { - logr.Debug("[watchdog.execute] Not handling alerting because currently in the maintenance window") - } - logr.Debugf("[watchdog.execute] Waiting for interval=%s before monitoring group=%s endpoint=%s (key=%s) again", ep.Interval, ep.Group, ep.Name, ep.Key()) -} - -func monitorExternalEndpointHeartbeat(ee *endpoint.ExternalEndpoint, alertingConfig *alerting.Config, maintenanceConfig *maintenance.Config, connectivityConfig *connectivity.Config, disableMonitoringLock bool, enabledMetrics bool, ctx context.Context, extraLabels []string) { - ticker := time.NewTicker(ee.Heartbeat.Interval) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - logr.Warnf("[watchdog.monitorExternalEndpointHeartbeat] Canceling current execution of group=%s; endpoint=%s; key=%s", ee.Group, ee.Name, ee.Key()) - return - case <-ticker.C: - executeExternalEndpointHeartbeat(ee, alertingConfig, maintenanceConfig, connectivityConfig, disableMonitoringLock, enabledMetrics, extraLabels) - } - } -} - -func executeExternalEndpointHeartbeat(ee *endpoint.ExternalEndpoint, alertingConfig *alerting.Config, maintenanceConfig *maintenance.Config, connectivityConfig *connectivity.Config, disableMonitoringLock bool, enabledMetrics bool, extraLabels []string) { - if !disableMonitoringLock { - // By placing the lock here, we prevent multiple endpoints from being monitored at the exact same time, which - // could cause performance issues and return inaccurate results - monitoringMutex.Lock() - defer monitoringMutex.Unlock() - } - // If there's a connectivity checker configured, check if Gatus has internet connectivity - if connectivityConfig != nil && connectivityConfig.Checker != nil && !connectivityConfig.Checker.IsConnected() { - logr.Infof("[watchdog.monitorExternalEndpointHeartbeat] No connectivity; skipping execution") - return - } - logr.Debugf("[watchdog.monitorExternalEndpointHeartbeat] Checking heartbeat for group=%s; endpoint=%s; key=%s", ee.Group, ee.Name, ee.Key()) - convertedEndpoint := ee.ToEndpoint() - hasReceivedResultWithinHeartbeatInterval, err := store.Get().HasEndpointStatusNewerThan(ee.Key(), time.Now().Add(-ee.Heartbeat.Interval)) - if err != nil { - logr.Errorf("[watchdog.monitorExternalEndpointHeartbeat] Failed to check if endpoint has received a result within the heartbeat interval: %s", err.Error()) - return - } - if hasReceivedResultWithinHeartbeatInterval { - // If we received a result within the heartbeat interval, we don't want to create a successful result, so we - // skip the rest. We don't have to worry about alerting or metrics, because if the previous heartbeat failed - // while this one succeeds, it implies that there was a new result pushed, and that result being pushed - // should've resolved the alert. - logr.Infof("[watchdog.monitorExternalEndpointHeartbeat] Checked heartbeat for group=%s; endpoint=%s; key=%s; success=%v; errors=%d", ee.Group, ee.Name, ee.Key(), hasReceivedResultWithinHeartbeatInterval, 0) - return - } - // All code after this point assumes the heartbeat failed - result := &endpoint.Result{ - Timestamp: time.Now(), - Success: false, - Errors: []string{"heartbeat: no update received within " + ee.Heartbeat.Interval.String()}, - } - if enabledMetrics { - metrics.PublishMetricsForEndpoint(convertedEndpoint, result, extraLabels) - } - UpdateEndpointStatuses(convertedEndpoint, result) - logr.Infof("[watchdog.monitorExternalEndpointHeartbeat] Checked heartbeat for group=%s; endpoint=%s; key=%s; success=%v; errors=%d; duration=%s", ee.Group, ee.Name, ee.Key(), result.Success, len(result.Errors), result.Duration.Round(time.Millisecond)) - inEndpointMaintenanceWindow := false - for _, maintenanceWindow := range ee.MaintenanceWindows { - if maintenanceWindow.IsUnderMaintenance() { - logr.Debug("[watchdog.monitorExternalEndpointHeartbeat] Under endpoint maintenance window") - inEndpointMaintenanceWindow = true - } - } - if !maintenanceConfig.IsUnderMaintenance() && !inEndpointMaintenanceWindow { - HandleAlerting(convertedEndpoint, result, alertingConfig) - // Sync the failure/success counters back to the external endpoint - ee.NumberOfSuccessesInARow = convertedEndpoint.NumberOfSuccessesInARow - ee.NumberOfFailuresInARow = convertedEndpoint.NumberOfFailuresInARow - } else { - logr.Debug("[watchdog.monitorExternalEndpointHeartbeat] Not handling alerting because currently in the maintenance window") - } - logr.Debugf("[watchdog.monitorExternalEndpointHeartbeat] Waiting for interval=%s before checking heartbeat for group=%s endpoint=%s (key=%s) again", ee.Heartbeat.Interval, ee.Group, ee.Name, ee.Key()) -} - -// UpdateEndpointStatuses updates the slice of endpoint statuses -func UpdateEndpointStatuses(ep *endpoint.Endpoint, result *endpoint.Result) { - if err := store.Get().Insert(ep, result); err != nil { - logr.Errorf("[watchdog.UpdateEndpointStatuses] Failed to insert result in storage: %s", err.Error()) - } } // Shutdown stops monitoring all endpoints func Shutdown(cfg *config.Config) { - // Disable all the old HTTP connections + // Stop in-flight HTTP connections for _, ep := range cfg.Endpoints { ep.Close() } + for _, s := range cfg.Suites { + for _, ep := range s.Endpoints { + ep.Close() + } + } cancelFunc() } diff --git a/web/app/src/components/EndpointCard.vue b/web/app/src/components/EndpointCard.vue index c9696555..2dfae1d1 100644 --- a/web/app/src/components/EndpointCard.vue +++ b/web/app/src/components/EndpointCard.vue @@ -61,7 +61,7 @@ import { computed } from 'vue' import { useRouter } from 'vue-router' import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' import StatusBadge from '@/components/StatusBadge.vue' -import { helper } from '@/mixins/helper' +import { generatePrettyTimeAgo } from '@/utils/time' const router = useRouter() @@ -145,12 +145,12 @@ const formattedResponseTime = computed(() => { const oldestResultTime = computed(() => { if (!props.endpoint.results || props.endpoint.results.length === 0) return '' - return helper.methods.generatePrettyTimeAgo(props.endpoint.results[0].timestamp) + return generatePrettyTimeAgo(props.endpoint.results[0].timestamp) }) const newestResultTime = computed(() => { if (!props.endpoint.results || props.endpoint.results.length === 0) return '' - return helper.methods.generatePrettyTimeAgo(props.endpoint.results[props.endpoint.results.length - 1].timestamp) + return generatePrettyTimeAgo(props.endpoint.results[props.endpoint.results.length - 1].timestamp) }) const navigateToDetails = () => { diff --git a/web/app/src/components/FlowStep.vue b/web/app/src/components/FlowStep.vue new file mode 100644 index 00000000..d861f096 --- /dev/null +++ b/web/app/src/components/FlowStep.vue @@ -0,0 +1,133 @@ + + + \ No newline at end of file diff --git a/web/app/src/components/SequentialFlowDiagram.vue b/web/app/src/components/SequentialFlowDiagram.vue new file mode 100644 index 00000000..df954b6c --- /dev/null +++ b/web/app/src/components/SequentialFlowDiagram.vue @@ -0,0 +1,124 @@ + + + \ No newline at end of file diff --git a/web/app/src/components/StepDetailsModal.vue b/web/app/src/components/StepDetailsModal.vue new file mode 100644 index 00000000..c9fe554b --- /dev/null +++ b/web/app/src/components/StepDetailsModal.vue @@ -0,0 +1,115 @@ + + + \ No newline at end of file diff --git a/web/app/src/components/SuiteCard.vue b/web/app/src/components/SuiteCard.vue new file mode 100644 index 00000000..5c4eb7c5 --- /dev/null +++ b/web/app/src/components/SuiteCard.vue @@ -0,0 +1,171 @@ + + + + + \ No newline at end of file diff --git a/web/app/src/components/Tooltip.vue b/web/app/src/components/Tooltip.vue index c9742368..7213b002 100644 --- a/web/app/src/components/Tooltip.vue +++ b/web/app/src/components/Tooltip.vue @@ -10,20 +10,62 @@ :style="`top: ${top}px; left: ${left}px;`" >
+ +
+ + + {{ result.success ? 'Suite Passed' : 'Suite Failed' }} + +
+
Timestamp
{{ prettifyTimestamp(result.timestamp) }}
+ +
+
Endpoints
+
+ + {{ successCount }}/{{ endpointCount }} passed + +
+ +
+
+ + {{ endpoint.success ? '✓' : '✗' }} + + {{ endpoint.name }} + ({{ (endpoint.duration / 1000000).toFixed(0) }}ms) +
+
+ ... and {{ result.endpointResults.length - 5 }} more +
+
+
+
-
Response Time
-
{{ (result.duration / 1000000).toFixed(0) }}ms
+
+ {{ isSuiteResult ? 'Total Duration' : 'Response Time' }} +
+
+ {{ isSuiteResult ? (result.duration / 1000000).toFixed(0) : (result.duration / 1000000).toFixed(0) }}ms +
- -
+ +
Conditions
/* eslint-disable no-undef */ -import { ref, watch, nextTick } from 'vue' -import { helper } from '@/mixins/helper' +import { ref, watch, nextTick, computed } from 'vue' +import { prettifyTimestamp } from '@/utils/time' const props = defineProps({ event: { @@ -74,8 +116,22 @@ const top = ref(0) const left = ref(0) const tooltip = ref(null) -// Methods from helper mixin -const { prettifyTimestamp } = helper.methods +// Computed properties +const isSuiteResult = computed(() => { + return props.result && props.result.endpointResults !== undefined +}) + +const endpointCount = computed(() => { + if (!isSuiteResult.value || !props.result.endpointResults) return 0 + return props.result.endpointResults.length +}) + +const successCount = computed(() => { + if (!isSuiteResult.value || !props.result.endpointResults) return 0 + return props.result.endpointResults.filter(e => e.success).length +}) + +// Methods are imported from utils/time const reposition = async () => { if (!props.event || !props.event.type) return diff --git a/web/app/src/mixins/helper.js b/web/app/src/mixins/helper.js deleted file mode 100644 index 48f6f6a1..00000000 --- a/web/app/src/mixins/helper.js +++ /dev/null @@ -1,38 +0,0 @@ -export const helper = { - methods: { - generatePrettyTimeAgo(t) { - let differenceInMs = new Date().getTime() - new Date(t).getTime(); - if (differenceInMs < 500) { - return "now"; - } - if (differenceInMs > 3 * 86400000) { // If it was more than 3 days ago, we'll display the number of days ago - let days = (differenceInMs / 86400000).toFixed(0); - return days + " day" + (days !== "1" ? "s" : "") + " ago"; - } - if (differenceInMs > 3600000) { // If it was more than 1h ago, display the number of hours ago - let hours = (differenceInMs / 3600000).toFixed(0); - return hours + " hour" + (hours !== "1" ? "s" : "") + " ago"; - } - if (differenceInMs > 60000) { - let minutes = (differenceInMs / 60000).toFixed(0); - return minutes + " minute" + (minutes !== "1" ? "s" : "") + " ago"; - } - let seconds = (differenceInMs / 1000).toFixed(0); - return seconds + " second" + (seconds !== "1" ? "s" : "") + " ago"; - }, - generatePrettyTimeDifference(start, end) { - let minutes = Math.ceil((new Date(start) - new Date(end)) / 1000 / 60); - return minutes + (minutes === 1 ? ' minute' : ' minutes'); - }, - prettifyTimestamp(timestamp) { - let date = new Date(timestamp); - let YYYY = date.getFullYear(); - let MM = ((date.getMonth() + 1) < 10 ? "0" : "") + "" + (date.getMonth() + 1); - let DD = ((date.getDate()) < 10 ? "0" : "") + "" + (date.getDate()); - let hh = ((date.getHours()) < 10 ? "0" : "") + "" + (date.getHours()); - let mm = ((date.getMinutes()) < 10 ? "0" : "") + "" + (date.getMinutes()); - let ss = ((date.getSeconds()) < 10 ? "0" : "") + "" + (date.getSeconds()); - return YYYY + "-" + MM + "-" + DD + " " + hh + ":" + mm + ":" + ss; - }, - } -} diff --git a/web/app/src/router/index.js b/web/app/src/router/index.js index b04bbd87..2e6b5840 100644 --- a/web/app/src/router/index.js +++ b/web/app/src/router/index.js @@ -1,6 +1,7 @@ import {createRouter, createWebHistory} from 'vue-router' import Home from '@/views/Home' -import Details from "@/views/Details"; +import EndpointDetails from "@/views/EndpointDetails"; +import SuiteDetails from '@/views/SuiteDetails'; const routes = [ { @@ -10,9 +11,14 @@ const routes = [ }, { path: '/endpoints/:key', - name: 'Details', - component: Details, + name: 'EndpointDetails', + component: EndpointDetails, }, + { + path: '/suites/:key', + name: 'SuiteDetails', + component: SuiteDetails + } ]; const router = createRouter({ diff --git a/web/app/src/utils/format.js b/web/app/src/utils/format.js new file mode 100644 index 00000000..883e0f0d --- /dev/null +++ b/web/app/src/utils/format.js @@ -0,0 +1,17 @@ +/** + * Formats a duration from nanoseconds to a human-readable string + * @param {number} duration - Duration in nanoseconds + * @returns {string} Formatted duration string (e.g., "123ms", "1.23s") + */ +export const formatDuration = (duration) => { + if (!duration && duration !== 0) return 'N/A' + + // Convert nanoseconds to milliseconds + const durationMs = duration / 1000000 + + if (durationMs < 1000) { + return `${durationMs.toFixed(0)}ms` + } else { + return `${(durationMs / 1000).toFixed(2)}s` + } +} \ No newline at end of file diff --git a/web/app/src/utils/time.js b/web/app/src/utils/time.js new file mode 100644 index 00000000..f757f0ea --- /dev/null +++ b/web/app/src/utils/time.js @@ -0,0 +1,52 @@ +/** + * Generates a human-readable relative time string (e.g., "2 hours ago") + * @param {string|Date} timestamp - The timestamp to convert + * @returns {string} Relative time string + */ +export const generatePrettyTimeAgo = (timestamp) => { + let differenceInMs = new Date().getTime() - new Date(timestamp).getTime(); + if (differenceInMs < 500) { + return "now"; + } + if (differenceInMs > 3 * 86400000) { // If it was more than 3 days ago, we'll display the number of days ago + let days = (differenceInMs / 86400000).toFixed(0); + return days + " day" + (days !== "1" ? "s" : "") + " ago"; + } + if (differenceInMs > 3600000) { // If it was more than 1h ago, display the number of hours ago + let hours = (differenceInMs / 3600000).toFixed(0); + return hours + " hour" + (hours !== "1" ? "s" : "") + " ago"; + } + if (differenceInMs > 60000) { + let minutes = (differenceInMs / 60000).toFixed(0); + return minutes + " minute" + (minutes !== "1" ? "s" : "") + " ago"; + } + let seconds = (differenceInMs / 1000).toFixed(0); + return seconds + " second" + (seconds !== "1" ? "s" : "") + " ago"; +} + +/** + * Generates a pretty time difference string between two timestamps + * @param {string|Date} start - Start timestamp + * @param {string|Date} end - End timestamp + * @returns {string} Time difference string + */ +export const generatePrettyTimeDifference = (start, end) => { + let minutes = Math.ceil((new Date(start) - new Date(end)) / 1000 / 60); + return minutes + (minutes === 1 ? ' minute' : ' minutes'); +} + +/** + * Formats a timestamp into YYYY-MM-DD HH:mm:ss format + * @param {string|Date} timestamp - The timestamp to format + * @returns {string} Formatted timestamp + */ +export const prettifyTimestamp = (timestamp) => { + let date = new Date(timestamp); + let YYYY = date.getFullYear(); + let MM = ((date.getMonth() + 1) < 10 ? "0" : "") + "" + (date.getMonth() + 1); + let DD = ((date.getDate()) < 10 ? "0" : "") + "" + (date.getDate()); + let hh = ((date.getHours()) < 10 ? "0" : "") + "" + (date.getHours()); + let mm = ((date.getMinutes()) < 10 ? "0" : "") + "" + (date.getMinutes()); + let ss = ((date.getSeconds()) < 10 ? "0" : "") + "" + (date.getSeconds()); + return YYYY + "-" + MM + "-" + DD + " " + hh + ":" + mm + ":" + ss; +} \ No newline at end of file diff --git a/web/app/src/views/Details.vue b/web/app/src/views/EndpointDetails.vue similarity index 97% rename from web/app/src/views/Details.vue rename to web/app/src/views/EndpointDetails.vue index ed7f82a6..55fdcea6 100644 --- a/web/app/src/views/Details.vue +++ b/web/app/src/views/EndpointDetails.vue @@ -207,7 +207,7 @@ import Settings from '@/components/Settings.vue' import Pagination from '@/components/Pagination.vue' import Loading from '@/components/Loading.vue' import { SERVER_URL } from '@/main.js' -import { helper } from '@/mixins/helper' +import { generatePrettyTimeAgo, generatePrettyTimeDifference } from '@/utils/time' const router = useRouter() const route = useRoute() @@ -290,7 +290,7 @@ const lastCheckTime = computed(() => { if (!currentStatus.value || !currentStatus.value.results || currentStatus.value.results.length === 0) { return 'Never' } - return helper.methods.generatePrettyTimeAgo(currentStatus.value.results[currentStatus.value.results.length - 1].timestamp) + return generatePrettyTimeAgo(currentStatus.value.results[currentStatus.value.results.length - 1].timestamp) }) @@ -328,7 +328,7 @@ const fetchData = async () => { event.fancyText = 'Endpoint became healthy' } else if (event.type === 'UNHEALTHY') { if (nextEvent) { - event.fancyText = 'Endpoint was unhealthy for ' + helper.methods.generatePrettyTimeDifference(nextEvent.timestamp, event.timestamp) + event.fancyText = 'Endpoint was unhealthy for ' + generatePrettyTimeDifference(nextEvent.timestamp, event.timestamp) } else { event.fancyText = 'Endpoint became unhealthy' } @@ -336,7 +336,7 @@ const fetchData = async () => { event.fancyText = 'Monitoring started' } } - event.fancyTimeAgo = helper.methods.generatePrettyTimeAgo(event.timestamp) + event.fancyTimeAgo = generatePrettyTimeAgo(event.timestamp) processedEvents.push(event) } } diff --git a/web/app/src/views/Home.vue b/web/app/src/views/Home.vue index 28182eae..779c6ffa 100644 --- a/web/app/src/views/Home.vue +++ b/web/app/src/views/Home.vue @@ -39,20 +39,20 @@
-
+
-

No endpoints found

+

No endpoints or suites found

{{ searchQuery || showOnlyFailing || showRecentFailures ? 'Try adjusting your filters' - : 'No endpoints are configured' }} + : 'No endpoints or suites are configured' }}

-
+
{{ group }}
- - {{ calculateUnhealthyCount(endpoints) }} + {{ calculateUnhealthyCount(items.endpoints) + calculateFailingSuitesCount(items.suites) }}
@@ -74,30 +74,68 @@
-
- + +
+

Suites

+
+ +
+
+ + +
+

Endpoints

+
+ +
-
- +
+ +
+

Suites

+
+ +
+
+ + +
+

Endpoints

+
+ +
+
@@ -144,6 +182,7 @@ import { ref, computed, onMounted } from 'vue' import { Activity, Timer, RefreshCw, AlertCircle, ChevronLeft, ChevronRight, ChevronDown, ChevronUp, CheckCircle } from 'lucide-vue-next' import { Button } from '@/components/ui/button' import EndpointCard from '@/components/EndpointCard.vue' +import SuiteCard from '@/components/SuiteCard.vue' import SearchBar from '@/components/SearchBar.vue' import Settings from '@/components/Settings.vue' import Loading from '@/components/Loading.vue' @@ -160,6 +199,7 @@ const props = defineProps({ const emit = defineEmits(['showTooltip']) const endpointStatuses = ref([]) +const suiteStatuses = ref([]) const loading = ref(false) const currentPage = ref(1) const itemsPerPage = 96 @@ -215,8 +255,51 @@ const filteredEndpoints = computed(() => { return filtered }) +const filteredSuites = computed(() => { + let filtered = [...suiteStatuses.value] + + if (searchQuery.value) { + const query = searchQuery.value.toLowerCase() + filtered = filtered.filter(suite => + suite.name.toLowerCase().includes(query) || + (suite.group && suite.group.toLowerCase().includes(query)) + ) + } + + if (showOnlyFailing.value) { + filtered = filtered.filter(suite => { + if (!suite.results || suite.results.length === 0) return false + return !suite.results[suite.results.length - 1].success + }) + } + + if (showRecentFailures.value) { + filtered = filtered.filter(suite => { + if (!suite.results || suite.results.length === 0) return false + return suite.results.some(result => !result.success) + }) + } + + // Sort by health if selected + if (sortBy.value === 'health') { + filtered.sort((a, b) => { + const aHealthy = a.results && a.results.length > 0 && a.results[a.results.length - 1].success + const bHealthy = b.results && b.results.length > 0 && b.results[b.results.length - 1].success + + // Unhealthy first + if (!aHealthy && bHealthy) return -1 + if (aHealthy && !bHealthy) return 1 + + // Then sort by name + return a.name.localeCompare(b.name) + }) + } + + return filtered +}) + const totalPages = computed(() => { - return Math.ceil(filteredEndpoints.value.length / itemsPerPage) + return Math.ceil((filteredEndpoints.value.length + filteredSuites.value.length) / itemsPerPage) }) const groupedEndpoints = computed(() => { @@ -248,6 +331,46 @@ const groupedEndpoints = computed(() => { return result }) +const combinedGroups = computed(() => { + if (!groupByGroup.value) { + return null + } + + const combined = {} + + // Add endpoints + filteredEndpoints.value.forEach(endpoint => { + const group = endpoint.group || 'No Group' + if (!combined[group]) { + combined[group] = { endpoints: [], suites: [] } + } + combined[group].endpoints.push(endpoint) + }) + + // Add suites + filteredSuites.value.forEach(suite => { + const group = suite.group || 'No Group' + if (!combined[group]) { + combined[group] = { endpoints: [], suites: [] } + } + combined[group].suites.push(suite) + }) + + // Sort groups alphabetically, with 'No Group' at the end + const sortedGroups = Object.keys(combined).sort((a, b) => { + if (a === 'No Group') return 1 + if (b === 'No Group') return -1 + return a.localeCompare(b) + }) + + const result = {} + sortedGroups.forEach(group => { + result[group] = combined[group] + }) + + return result +}) + const paginatedEndpoints = computed(() => { if (groupByGroup.value) { // When grouping, we don't paginate @@ -259,6 +382,17 @@ const paginatedEndpoints = computed(() => { return filteredEndpoints.value.slice(start, end) }) +const paginatedSuites = computed(() => { + if (groupByGroup.value) { + // When grouping, we don't paginate + return filteredSuites.value + } + + const start = (currentPage.value - 1) * itemsPerPage + const end = start + itemsPerPage + return filteredSuites.value.slice(start, end) +}) + const visiblePages = computed(() => { const pages = [] const maxVisible = 5 @@ -278,42 +412,31 @@ const visiblePages = computed(() => { const fetchData = async () => { // Don't show loading state on refresh to prevent UI flicker - const isInitialLoad = endpointStatuses.value.length === 0 + const isInitialLoad = endpointStatuses.value.length === 0 && suiteStatuses.value.length === 0 if (isInitialLoad) { loading.value = true } try { - const response = await fetch(`${SERVER_URL}/api/v1/endpoints/statuses?page=1&pageSize=100`, { + // Fetch endpoints + const endpointResponse = await fetch(`${SERVER_URL}/api/v1/endpoints/statuses?page=1&pageSize=100`, { credentials: 'include' }) - if (response.status === 200) { - const data = await response.json() - // If this is the initial load, just set the data - if (isInitialLoad) { - endpointStatuses.value = data - } else { - // Check if endpoints have been added or removed - const currentKeys = new Set(endpointStatuses.value.map(ep => ep.key)) - const newKeys = new Set(data.map(ep => ep.key)) - const hasAdditions = data.some(ep => !currentKeys.has(ep.key)) - const hasRemovals = endpointStatuses.value.some(ep => !newKeys.has(ep.key)) - if (hasAdditions || hasRemovals) { - // Endpoints have changed, reset the array to maintain proper order - endpointStatuses.value = data - } else { - // Only statuses/results have changed, update in place to preserve scroll - const endpointMap = new Map(data.map(ep => [ep.key, ep])) - endpointStatuses.value.forEach((endpoint, index) => { - const updated = endpointMap.get(endpoint.key) - if (updated) { - // Update in place to preserve Vue's reactivity and scroll position - Object.assign(endpointStatuses.value[index], updated) - } - }) - } - } + if (endpointResponse.status === 200) { + const data = await endpointResponse.json() + endpointStatuses.value = data } else { - console.error('[Home][fetchData] Error:', await response.text()) + console.error('[Home][fetchData] Error fetching endpoints:', await endpointResponse.text()) + } + + // Fetch suites + const suiteResponse = await fetch(`${SERVER_URL}/api/v1/suites/statuses?page=1&pageSize=100`, { + credentials: 'include' + }) + if (suiteResponse.status === 200) { + const suiteData = await suiteResponse.json() + suiteStatuses.value = suiteData + } else { + console.error('[Home][fetchData] Error fetching suites:', await suiteResponse.text()) } } catch (error) { console.error('[Home][fetchData] Error:', error) @@ -355,6 +478,13 @@ const calculateUnhealthyCount = (endpoints) => { }).length } +const calculateFailingSuitesCount = (suites) => { + return suites.filter(suite => { + if (!suite.results || suite.results.length === 0) return false + return !suite.results[suite.results.length - 1].success + }).length +} + const toggleGroupCollapse = (groupName) => { if (uncollapsedGroups.value.has(groupName)) { uncollapsedGroups.value.delete(groupName) diff --git a/web/app/src/views/SuiteDetails.vue b/web/app/src/views/SuiteDetails.vue new file mode 100644 index 00000000..fec849ad --- /dev/null +++ b/web/app/src/views/SuiteDetails.vue @@ -0,0 +1,334 @@ + + + + + \ No newline at end of file diff --git a/web/static/css/app.css b/web/static/css/app.css index 8654df51..d0559217 100644 --- a/web/static/css/app.css +++ b/web/static/css/app.css @@ -2,4 +2,4 @@ /* ! tailwindcss v3.1.8 | MIT License | https://tailwindcss.com -*/*,:after,:before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Consolas,Monaco,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}:root{--background:0 0% 100%;--foreground:222.2 84% 4.9%;--card:0 0% 100%;--card-foreground:222.2 84% 4.9%;--popover:0 0% 100%;--popover-foreground:222.2 84% 4.9%;--primary:222.2 47.4% 11.2%;--primary-foreground:210 40% 98%;--secondary:210 40% 96.1%;--secondary-foreground:222.2 47.4% 11.2%;--muted:210 40% 96.1%;--muted-foreground:215.4 16.3% 46.9%;--accent:210 40% 96.1%;--accent-foreground:222.2 47.4% 11.2%;--destructive:0 84.2% 60.2%;--destructive-foreground:210 40% 98%;--border:214.3 31.8% 91.4%;--input:214.3 31.8% 91.4%;--ring:222.2 84% 4.9%;--radius:0.5rem}:root.dark{--background:222.2 84% 4.9%;--foreground:210 40% 98%;--card:222.2 84% 4.9%;--card-foreground:210 40% 98%;--popover:222.2 84% 4.9%;--popover-foreground:210 40% 98%;--primary:210 40% 98%;--primary-foreground:222.2 47.4% 11.2%;--secondary:217.2 32.6% 17.5%;--secondary-foreground:210 40% 98%;--muted:217.2 32.6% 17.5%;--muted-foreground:215 20.2% 65.1%;--accent:217.2 32.6% 17.5%;--accent-foreground:210 40% 98%;--destructive:0 62.8% 30.6%;--destructive-foreground:210 40% 98%;--border:217.2 32.6% 17.5%;--input:217.2 32.6% 17.5%;--ring:212.7 26.8% 83.9%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground))}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.left-3{left:.75rem}.-left-\[26px\]{left:-26px}.top-1\/2{top:50%}.bottom-4{bottom:1rem}.left-4{left:1rem}.bottom-full{bottom:100%}.left-0{left:0}.left-1\/2{left:50%}.top-full{top:100%}.left-1\.5{left:.375rem}.left-1{left:.25rem}.z-10{z-index:10}.z-50{z-index:50}.mx-auto{margin-left:auto;margin-right:auto}.mt-4{margin-top:1rem}.mt-auto{margin-top:auto}.mb-4{margin-bottom:1rem}.mt-2{margin-top:.5rem}.mb-6{margin-bottom:1.5rem}.mr-2{margin-right:.5rem}.mb-2{margin-bottom:.5rem}.ml-7{margin-left:1.75rem}.ml-2{margin-left:.5rem}.mb-1{margin-bottom:.25rem}.mt-1{margin-top:.25rem}.mt-8{margin-top:2rem}.ml-1{margin-left:.25rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.\!hidden{display:none!important}.h-12{height:3rem}.h-full{height:100%}.h-5{height:1.25rem}.h-20{height:5rem}.h-11{height:2.75rem}.h-4{height:1rem}.h-3{height:.75rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-16{height:4rem}.h-3\.5{height:.875rem}.h-2{height:.5rem}.h-10{height:2.5rem}.h-9{height:2.25rem}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-full{width:100%}.w-5{width:1.25rem}.w-20{width:5rem}.w-4{width:1rem}.w-0\.5{width:.125rem}.w-0{width:0}.w-3{width:.75rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-16{width:4rem}.w-3\.5{width:.875rem}.w-px{width:1px}.w-2{width:.5rem}.w-10{width:2.5rem}.min-w-0{min-width:0}.max-w-7xl{max-width:80rem}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-x-1\/2,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-1\/2{--tw-translate-x:-50%}.-rotate-90{--tw-rotate:-90deg}.-rotate-90,.rotate-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-0{--tw-rotate:0deg}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-4{gap:1rem}.gap-3{gap:.75rem}.gap-2{gap:.5rem}.gap-1{gap:.25rem}.gap-0\.5{gap:.125rem}.gap-0{gap:0}.gap-1\.5{gap:.375rem}.gap-6{gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.125rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.overflow-hidden,.truncate{overflow:hidden}.truncate{text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-lg{border-radius:var(--radius)}.rounded-full{border-radius:9999px}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded-t-lg{border-top-left-radius:var(--radius);border-top-right-radius:var(--radius)}.rounded-b-lg{border-bottom-right-radius:var(--radius);border-bottom-left-radius:var(--radius)}.border{border-width:1px}.border-0{border-width:0}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-l-4{border-left-width:4px}.border-destructive\/20{border-color:hsl(var(--destructive)/.2)}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgb(239 68 68/var(--tw-border-opacity))}.border-yellow-500{--tw-border-opacity:1;border-color:rgb(234 179 8/var(--tw-border-opacity))}.border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.border-green-500{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}.border-border{border-color:hsl(var(--border))}.border-transparent{border-color:transparent}.border-input{border-color:hsl(var(--input))}.bg-background{background-color:hsl(var(--background))}.bg-card\/50{background-color:hsl(var(--card)/.5)}.bg-destructive\/10{background-color:hsl(var(--destructive)/.1)}.bg-primary{background-color:hsl(var(--primary))}.bg-card{background-color:hsl(var(--card))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-background\/95{background-color:hsl(var(--background)/.95)}.bg-popover{background-color:hsl(var(--popover))}.bg-accent{background-color:hsl(var(--accent))}.bg-border\/50{background-color:hsl(var(--border)/.5)}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(74 222 128/var(--tw-bg-opacity))}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity))}.bg-yellow-400{--tw-bg-opacity:1;background-color:rgb(250 204 21/var(--tw-bg-opacity))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity))}.bg-secondary{background-color:hsl(var(--secondary))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity))}.object-contain{-o-object-fit:contain;object-fit:contain}.p-4{padding:1rem}.p-3{padding:.75rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-0{padding:0}.p-6{padding:1.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-8{padding-top:2rem;padding-bottom:2rem}.py-20{padding-top:5rem;padding-bottom:5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-0{padding-top:0;padding-bottom:0}.pt-4{padding-top:1rem}.pt-3{padding-top:.75rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pt-2{padding-top:.5rem}.pl-10{padding-left:2.5rem}.pb-4{padding-bottom:1rem}.pt-0{padding-top:0}.pl-6{padding-left:1.5rem}.pr-2{padding-right:.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:Consolas,Monaco,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-xs{font-size:.75rem;line-height:1rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem}.text-lg,.text-xl{line-height:1.75rem}.text-xl{font-size:1.25rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-relaxed{line-height:1.625}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-foreground{color:hsl(var(--foreground))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-emerald-800{--tw-text-opacity:1;color:rgb(6 95 70/var(--tw-text-opacity))}.text-destructive{color:hsl(var(--destructive))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-popover-foreground{color:hsl(var(--popover-foreground))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-primary{color:hsl(var(--primary))}.text-accent-foreground{color:hsl(var(--accent-foreground))}.underline-offset-4{text-underline-offset:4px}.opacity-60{opacity:.6}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-none{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-offset-background{--tw-ring-offset-color:hsl(var(--background))}.grayscale{--tw-grayscale:grayscale(100%)}.filter,.grayscale{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur:blur(8px)}.backdrop-blur,.backdrop-blur-sm{backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-filter{backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.bg-success{background-color:#28a745}html{height:100%}body{min-height:100vh}@media screen and (max-width:1279px){body{padding-top:0;padding-bottom:0}}.file\:border-0::file-selector-button{border-width:0}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.last\:border-0:last-child{border-width:0}@media (hover:hover) and (pointer:fine){.hover\:scale-\[1\.01\]:hover{--tw-scale-x:1.01;--tw-scale-y:1.01}.hover\:scale-110:hover,.hover\:scale-\[1\.01\]:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary)/.9)}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity))}.hover\:bg-accent\/50:hover{background-color:hsl(var(--accent)/.5)}.hover\:bg-primary\/80:hover{background-color:hsl(var(--primary)/.8)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary)/.8)}.hover\:bg-destructive\/80:hover{background-color:hsl(var(--destructive)/.8)}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive)/.9)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-emerald-600:hover{--tw-text-opacity:1;color:rgb(5 150 105/var(--tw-text-opacity))}.hover\:text-primary:hover{color:hsl(var(--primary))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}.hover\:shadow-sm:hover{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.hover\:shadow-lg:hover,.hover\:shadow-sm:hover{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color:hsl(var(--ring))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:hsl(var(--ring))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (hover:hover) and (pointer:fine){.group:hover .group-hover\:opacity-100{opacity:1}}.dark .dark\:border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}.dark .dark\:bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}.dark .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.dark .dark\:bg-red-900\/50{background-color:rgba(127,29,29,.5)}.dark .dark\:bg-yellow-900\/50{background-color:rgba(113,63,18,.5)}.dark .dark\:bg-blue-900\/50{background-color:rgba(30,58,138,.5)}.dark .dark\:bg-green-900\/50{background-color:rgba(20,83,45,.5)}.dark .dark\:bg-gray-800\/50{background-color:rgba(31,41,55,.5)}.dark .dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.dark .dark\:text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}.dark .dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark .dark\:text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.dark .dark\:text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.dark .dark\:text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity))}.dark .dark\:text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity))}.dark .dark\:text-yellow-300{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity))}.dark .dark\:text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity))}.dark .dark\:text-blue-300{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity))}.dark .dark\:text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity))}.dark .dark\:text-green-300{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity))}@media (hover:hover) and (pointer:fine){.dark .dark\:hover\:border-gray-700:hover{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.dark .dark\:hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}}@media (min-width:640px){.sm\:left-2{left:.5rem}.sm\:h-8{height:2rem}.sm\:h-10{height:2.5rem}.sm\:h-4{height:1rem}.sm\:w-\[140px\]{width:140px}.sm\:w-\[90px\]{width:90px}.sm\:w-4{width:1rem}.sm\:flex-initial{flex:0 1 auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:gap-3{gap:.75rem}.sm\:gap-4{gap:1rem}.sm\:p-4{padding:1rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:px-3{padding-left:.75rem;padding-right:.75rem}.sm\:py-2{padding-top:.5rem;padding-bottom:.5rem}.sm\:pt-6{padding-top:1.5rem}.sm\:pb-4{padding-bottom:1rem}.sm\:pl-8{padding-left:2rem}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}.sm\:text-base{font-size:1rem;line-height:1.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-\[160px\]{width:160px}.md\:w-\[100px\]{width:100px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:gap-4{gap:1rem}}@keyframes slideIn-482756f8{0%{transform:translateX(-20px);opacity:0}to{transform:translateX(0);opacity:1}}#settings[data-v-482756f8]{animation:slideIn-482756f8 .3s ease-out}#settings>div[data-v-482756f8]{transition:all .2s ease}#settings>div[data-v-482756f8]:hover{transform:translateY(-2px);box-shadow:0 10px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1)}.announcement-container[data-v-48763619]{animation:slideDown-48763619 .3s ease-out}@keyframes slideDown-48763619{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@media (max-width:640px){.announcement-container .ml-7[data-v-48763619]{margin-left:1.5rem}} \ No newline at end of file +*/*,:after,:before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Consolas,Monaco,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}:root{--background:0 0% 100%;--foreground:222.2 84% 4.9%;--card:0 0% 100%;--card-foreground:222.2 84% 4.9%;--popover:0 0% 100%;--popover-foreground:222.2 84% 4.9%;--primary:222.2 47.4% 11.2%;--primary-foreground:210 40% 98%;--secondary:210 40% 96.1%;--secondary-foreground:222.2 47.4% 11.2%;--muted:210 40% 96.1%;--muted-foreground:215.4 16.3% 46.9%;--accent:210 40% 96.1%;--accent-foreground:222.2 47.4% 11.2%;--destructive:0 84.2% 60.2%;--destructive-foreground:210 40% 98%;--border:214.3 31.8% 91.4%;--input:214.3 31.8% 91.4%;--ring:222.2 84% 4.9%;--radius:0.5rem}:root.dark{--background:222.2 84% 4.9%;--foreground:210 40% 98%;--card:222.2 84% 4.9%;--card-foreground:210 40% 98%;--popover:222.2 84% 4.9%;--popover-foreground:210 40% 98%;--primary:210 40% 98%;--primary-foreground:222.2 47.4% 11.2%;--secondary:217.2 32.6% 17.5%;--secondary-foreground:210 40% 98%;--muted:217.2 32.6% 17.5%;--muted-foreground:215 20.2% 65.1%;--accent:217.2 32.6% 17.5%;--accent-foreground:210 40% 98%;--destructive:0 62.8% 30.6%;--destructive-foreground:210 40% 98%;--border:217.2 32.6% 17.5%;--input:217.2 32.6% 17.5%;--ring:212.7 26.8% 83.9%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground))}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.left-3{left:.75rem}.-left-\[26px\]{left:-26px}.top-1\/2{top:50%}.left-1\/2{left:50%}.bottom-8{bottom:2rem}.top-8{top:2rem}.bottom-4{bottom:1rem}.left-4{left:1rem}.bottom-full{bottom:100%}.left-0{left:0}.top-full{top:100%}.left-1\.5{left:.375rem}.left-1{left:.25rem}.z-10{z-index:10}.z-50{z-index:50}.-m-2{margin:-.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mt-4{margin-top:1rem}.mt-auto{margin-top:auto}.mb-4{margin-bottom:1rem}.mt-2{margin-top:.5rem}.mb-6{margin-bottom:1.5rem}.mr-2{margin-right:.5rem}.mb-2{margin-bottom:.5rem}.ml-7{margin-left:1.75rem}.ml-2{margin-left:.5rem}.mb-1{margin-bottom:.25rem}.mt-1{margin-top:.25rem}.mt-6{margin-top:1.5rem}.mb-3{margin-bottom:.75rem}.mt-8{margin-top:2rem}.ml-1{margin-left:.25rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.\!hidden{display:none!important}.h-12{height:3rem}.h-full{height:100%}.h-5{height:1.25rem}.h-20{height:5rem}.h-11{height:2.75rem}.h-4{height:1rem}.h-3{height:.75rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-16{height:4rem}.h-1{height:.25rem}.h-3\.5{height:.875rem}.h-2{height:.5rem}.h-10{height:2.5rem}.h-9{height:2.25rem}.max-h-\[80vh\]{max-height:80vh}.max-h-\[60vh\]{max-height:60vh}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-full{width:100%}.w-5{width:1.25rem}.w-20{width:5rem}.w-4{width:1rem}.w-0\.5{width:.125rem}.w-0{width:0}.w-3{width:.75rem}.w-8{width:2rem}.w-6{width:1.5rem}.w-16{width:4rem}.w-3\.5{width:.875rem}.w-px{width:1px}.w-2{width:.5rem}.w-10{width:2.5rem}.min-w-0{min-width:0}.max-w-7xl{max-width:80rem}.max-w-md{max-width:28rem}.max-w-2xl{max-width:42rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-x-px,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-px{--tw-translate-x:-1px}.-translate-x-1\/2{--tw-translate-x:-50%}.-rotate-90,.-translate-x-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-90{--tw-rotate:-90deg}.rotate-0{--tw-rotate:0deg}.rotate-0,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-4{gap:1rem}.gap-3{gap:.75rem}.gap-2{gap:.5rem}.gap-1{gap:.25rem}.gap-0\.5{gap:.125rem}.gap-0{gap:0}.gap-1\.5{gap:.375rem}.gap-6{gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.125rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-lg{border-radius:var(--radius)}.rounded-full{border-radius:9999px}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded{border-radius:.25rem}.rounded-t-lg{border-top-left-radius:var(--radius);border-top-right-radius:var(--radius)}.rounded-b-lg{border-bottom-right-radius:var(--radius);border-bottom-left-radius:var(--radius)}.border{border-width:1px}.border-2{border-width:2px}.border-0{border-width:0}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-l-4{border-left-width:4px}.border-l-2{border-left-width:2px}.border-dashed{border-style:dashed}.border-destructive\/20{border-color:hsl(var(--destructive)/.2)}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgb(239 68 68/var(--tw-border-opacity))}.border-yellow-500{--tw-border-opacity:1;border-color:rgb(234 179 8/var(--tw-border-opacity))}.border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.border-green-500{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}.border-green-600{--tw-border-opacity:1;border-color:rgb(22 163 74/var(--tw-border-opacity))}.border-red-600{--tw-border-opacity:1;border-color:rgb(220 38 38/var(--tw-border-opacity))}.border-blue-600{--tw-border-opacity:1;border-color:rgb(37 99 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.border-blue-200{--tw-border-opacity:1;border-color:rgb(191 219 254/var(--tw-border-opacity))}.border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202/var(--tw-border-opacity))}.border-border{border-color:hsl(var(--border))}.border-transparent{border-color:transparent}.border-input{border-color:hsl(var(--input))}.bg-background{background-color:hsl(var(--background))}.bg-card\/50{background-color:hsl(var(--card)/.5)}.bg-destructive\/10{background-color:hsl(var(--destructive)/.1)}.bg-primary{background-color:hsl(var(--primary))}.bg-card{background-color:hsl(var(--card))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-background\/95{background-color:hsl(var(--background)/.95)}.bg-popover{background-color:hsl(var(--popover))}.bg-accent{background-color:hsl(var(--accent))}.bg-border\/50{background-color:hsl(var(--border)/.5)}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(74 222 128/var(--tw-bg-opacity))}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity))}.bg-yellow-400{--tw-bg-opacity:1;background-color:rgb(250 204 21/var(--tw-bg-opacity))}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity))}.bg-secondary{background-color:hsl(var(--secondary))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity))}.object-contain{-o-object-fit:contain;object-fit:contain}.p-4{padding:1rem}.p-3{padding:.75rem}.p-2{padding:.5rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-0{padding:0}.p-6{padding:1.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-8{padding-top:2rem;padding-bottom:2rem}.py-20{padding-top:5rem;padding-bottom:5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-0{padding-top:0;padding-bottom:0}.pt-4{padding-top:1rem}.pt-3{padding-top:.75rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pt-2{padding-top:.5rem}.pt-1{padding-top:.25rem}.pl-10{padding-left:2.5rem}.pb-4{padding-bottom:1rem}.pt-0{padding-top:0}.pl-6{padding-left:1.5rem}.pr-2{padding-right:.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:Consolas,Monaco,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-xs{font-size:.75rem;line-height:1rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-relaxed{line-height:1.625}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-foreground{color:hsl(var(--foreground))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-emerald-800{--tw-text-opacity:1;color:rgb(6 95 70/var(--tw-text-opacity))}.text-destructive{color:hsl(var(--destructive))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-popover-foreground{color:hsl(var(--popover-foreground))}.text-blue-900{--tw-text-opacity:1;color:rgb(30 58 138/var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgb(234 179 8/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-primary{color:hsl(var(--primary))}.text-accent-foreground{color:hsl(var(--accent-foreground))}.underline-offset-4{text-underline-offset:4px}.opacity-60{opacity:.6}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-none{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring,.ring-2{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-blue-200{--tw-ring-opacity:1;--tw-ring-color:rgb(191 219 254/var(--tw-ring-opacity))}.ring-offset-background{--tw-ring-offset-color:hsl(var(--background))}.grayscale{--tw-grayscale:grayscale(100%)}.filter,.grayscale{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur:blur(8px)}.backdrop-blur,.backdrop-blur-sm{backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-filter{backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.bg-success{background-color:#28a745}html{height:100%}body{min-height:100vh}@media screen and (max-width:1279px){body{padding-top:0;padding-bottom:0}}.file\:border-0::file-selector-button{border-width:0}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.last\:border-0:last-child{border-width:0}@media (hover:hover) and (pointer:fine){.hover\:scale-\[1\.01\]:hover{--tw-scale-x:1.01;--tw-scale-y:1.01}.hover\:scale-110:hover,.hover\:scale-\[1\.01\]:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary)/.9)}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity))}.hover\:bg-accent\/30:hover{background-color:hsl(var(--accent)/.3)}.hover\:bg-accent\/50:hover{background-color:hsl(var(--accent)/.5)}.hover\:bg-primary\/80:hover{background-color:hsl(var(--primary)/.8)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary)/.8)}.hover\:bg-destructive\/80:hover{background-color:hsl(var(--destructive)/.8)}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive)/.9)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-emerald-600:hover{--tw-text-opacity:1;color:rgb(5 150 105/var(--tw-text-opacity))}.hover\:text-primary:hover{color:hsl(var(--primary))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}.hover\:shadow-sm:hover{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.hover\:shadow-lg:hover,.hover\:shadow-sm:hover{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color:hsl(var(--ring))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:hsl(var(--ring))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (hover:hover) and (pointer:fine){.group:hover .group-hover\:opacity-100{opacity:1}}.dark .dark\:border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}.dark .dark\:border-blue-800{--tw-border-opacity:1;border-color:rgb(30 64 175/var(--tw-border-opacity))}.dark .dark\:border-blue-700{--tw-border-opacity:1;border-color:rgb(29 78 216/var(--tw-border-opacity))}.dark .dark\:border-red-700{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity))}.dark .dark\:bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}.dark .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.dark .dark\:bg-red-900\/50{background-color:rgba(127,29,29,.5)}.dark .dark\:bg-yellow-900\/50{background-color:rgba(113,63,18,.5)}.dark .dark\:bg-blue-900\/50{background-color:rgba(30,58,138,.5)}.dark .dark\:bg-green-900\/50{background-color:rgba(20,83,45,.5)}.dark .dark\:bg-gray-800\/50{background-color:rgba(31,41,55,.5)}.dark .dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.dark .dark\:bg-blue-900{--tw-bg-opacity:1;background-color:rgb(30 58 138/var(--tw-bg-opacity))}.dark .dark\:bg-red-900{--tw-bg-opacity:1;background-color:rgb(127 29 29/var(--tw-bg-opacity))}.dark .dark\:bg-green-600{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity))}.dark .dark\:bg-blue-900\/30{background-color:rgba(30,58,138,.3)}.dark .dark\:text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}.dark .dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark .dark\:text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.dark .dark\:text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.dark .dark\:text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity))}.dark .dark\:text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity))}.dark .dark\:text-yellow-300{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity))}.dark .dark\:text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity))}.dark .dark\:text-blue-300{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity))}.dark .dark\:text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity))}.dark .dark\:text-green-300{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity))}.dark .dark\:text-blue-200{--tw-text-opacity:1;color:rgb(191 219 254/var(--tw-text-opacity))}.dark .dark\:text-red-200{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity))}.dark .dark\:ring-blue-800{--tw-ring-opacity:1;--tw-ring-color:rgb(30 64 175/var(--tw-ring-opacity))}@media (hover:hover) and (pointer:fine){.dark .dark\:hover\:border-gray-700:hover{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.dark .dark\:hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}}@media (min-width:640px){.sm\:left-2{left:.5rem}.sm\:h-8{height:2rem}.sm\:h-10{height:2.5rem}.sm\:h-4{height:1rem}.sm\:w-\[140px\]{width:140px}.sm\:w-\[90px\]{width:90px}.sm\:w-4{width:1rem}.sm\:flex-initial{flex:0 1 auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:gap-3{gap:.75rem}.sm\:gap-4{gap:1rem}.sm\:p-4{padding:1rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:px-3{padding-left:.75rem;padding-right:.75rem}.sm\:py-2{padding-top:.5rem;padding-bottom:.5rem}.sm\:pt-6{padding-top:1.5rem}.sm\:pb-4{padding-bottom:1rem}.sm\:pl-8{padding-left:2rem}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}.sm\:text-base{font-size:1rem;line-height:1.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-\[160px\]{width:160px}.md\:w-\[100px\]{width:100px}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:gap-4{gap:1rem}}.suite[data-v-648070e3]{transition:all .2s ease}.suite[data-v-648070e3]:hover{transform:translateY(-2px)}.suite-header[data-v-648070e3]{border-bottom:1px solid rgba(0,0,0,.05)}.dark .suite-header[data-v-648070e3]{border-bottom:1px solid hsla(0,0%,100%,.05)}@keyframes slideIn-482756f8{0%{transform:translateX(-20px);opacity:0}to{transform:translateX(0);opacity:1}}#settings[data-v-482756f8]{animation:slideIn-482756f8 .3s ease-out}#settings>div[data-v-482756f8]{transition:all .2s ease}#settings>div[data-v-482756f8]:hover{transform:translateY(-2px);box-shadow:0 10px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1)}.announcement-container[data-v-48763619]{animation:slideDown-48763619 .3s ease-out}@keyframes slideDown-48763619{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@media (max-width:640px){.announcement-container .ml-7[data-v-48763619]{margin-left:1.5rem}}.suite-details-container[data-v-0c28bc83]{min-height:100vh} \ No newline at end of file diff --git a/web/static/js/app.js b/web/static/js/app.js index dc560b44..0abf2aef 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -1 +1 @@ -(function(){"use strict";var e={256:function(e,t,a){a.d(t,{L:function(){return Al}});var l=a(963),s=a(252),n=a(577),r=a(262),o=a.p+"img/logo.svg",i=a(201),u=a(507),d=a(970),c=a(135),g=a(3),m=a(512),v=a(388);function p(...e){return(0,v.m6)((0,m.W)(e))}const f=["disabled"];var w={__name:"Button",props:{variant:{type:String,default:"default"},size:{type:String,default:"default"},disabled:{type:Boolean,default:!1}},setup(e){const t=(0,g.j)("inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",{variants:{variant:{default:"bg-primary text-primary-foreground hover:bg-primary/90",destructive:"bg-destructive text-destructive-foreground hover:bg-destructive/90",outline:"border border-input bg-background hover:bg-accent hover:text-accent-foreground",secondary:"bg-secondary text-secondary-foreground hover:bg-secondary/80",ghost:"hover:bg-accent hover:text-accent-foreground",link:"text-primary underline-offset-4 hover:underline"},size:{default:"h-10 px-4 py-2",sm:"h-9 rounded-md px-3",lg:"h-11 rounded-md px-8",icon:"h-10 w-10"}},defaultVariants:{variant:"default",size:"default"}});return(a,l)=>((0,s.wg)(),(0,s.iD)("button",{class:(0,n.C_)((0,r.SU)(p)((0,r.SU)(t)({variant:e.variant,size:e.size}),a.$attrs.class??"")),disabled:e.disabled},[(0,s.WI)(a.$slots,"default")],10,f))}};const h=w;var y=h,b={__name:"Card",setup(e){return(e,t)=>((0,s.wg)(),(0,s.iD)("div",{class:(0,n.C_)((0,r.SU)(p)("rounded-lg border bg-card text-card-foreground shadow-sm",e.$attrs.class??""))},[(0,s.WI)(e.$slots,"default")],2))}};const x=b;var _=x,k={__name:"CardHeader",setup(e){return(e,t)=>((0,s.wg)(),(0,s.iD)("div",{class:(0,n.C_)((0,r.SU)(p)("flex flex-col space-y-1.5 p-6",e.$attrs.class??""))},[(0,s.WI)(e.$slots,"default")],2))}};const S=k;var U=S,D={__name:"CardTitle",setup(e){return(e,t)=>((0,s.wg)(),(0,s.iD)("h3",{class:(0,n.C_)((0,r.SU)(p)("text-2xl font-semibold leading-none tracking-tight",e.$attrs.class??""))},[(0,s.WI)(e.$slots,"default")],2))}};const W=D;var H=W,C={__name:"CardContent",setup(e){return(e,t)=>((0,s.wg)(),(0,s.iD)("div",{class:(0,n.C_)((0,r.SU)(p)("p-6 pt-0",e.$attrs.class??""))},[(0,s.WI)(e.$slots,"default")],2))}};const z=C;var j=z;const T={id:"social"};function F(e,t){return(0,s.wg)(),(0,s.iD)("div",T,t[0]||(t[0]=[(0,s._)("a",{href:"https://github.com/TwiN/gatus",target:"_blank",title:"Gatus on GitHub"},[(0,s._)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"32",height:"32",viewBox:"0 0 16 16",class:"hover:scale-110"},[(0,s._)("path",{fill:"gray",d:"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"})])],-1)]))}var $=a(744);const R={},A=(0,$.Z)(R,[["render",F],["__scopeId","data-v-788af9ce"]]);var E=A;const L={methods:{generatePrettyTimeAgo(e){let t=(new Date).getTime()-new Date(e).getTime();if(t<500)return"now";if(t>2592e5){let e=(t/864e5).toFixed(0);return e+" day"+("1"!==e?"s":"")+" ago"}if(t>36e5){let e=(t/36e5).toFixed(0);return e+" hour"+("1"!==e?"s":"")+" ago"}if(t>6e4){let e=(t/6e4).toFixed(0);return e+" minute"+("1"!==e?"s":"")+" ago"}let a=(t/1e3).toFixed(0);return a+" second"+("1"!==a?"s":"")+" ago"},generatePrettyTimeDifference(e,t){let a=Math.ceil((new Date(e)-new Date(t))/1e3/60);return a+(1===a?" minute":" minutes")},prettifyTimestamp(e){let t=new Date(e),a=t.getFullYear(),l=(t.getMonth()+1<10?"0":"")+(t.getMonth()+1),s=(t.getDate()<10?"0":"")+t.getDate(),n=(t.getHours()<10?"0":"")+t.getHours(),r=(t.getMinutes()<10?"0":"")+t.getMinutes(),o=(t.getSeconds()<10?"0":"")+t.getSeconds();return a+"-"+l+"-"+s+" "+n+":"+r+":"+o}}},I={key:0,class:"space-y-2"},M={class:"font-mono text-xs"},Z={class:"font-mono text-xs"},N={key:0},q={class:"font-mono text-xs space-y-0.5"},Y={class:"break-all"},P={key:1},O={class:"font-mono text-xs space-y-0.5"};var V={__name:"Tooltip",props:{event:{type:[Event,Object],default:null},result:{type:Object,default:null}},setup(e){const t=e,a=(0,r.iH)(!0),l=(0,r.iH)(0),o=(0,r.iH)(0),i=(0,r.iH)(null),{prettifyTimestamp:u}=L.methods,d=async()=>{if(t.event&&t.event.type)if(await(0,s.Y3)(),"mouseenter"===t.event.type&&i.value){const e=t.event.target,n=e.getBoundingClientRect();a.value=!1,await(0,s.Y3)();const r=i.value.getBoundingClientRect();let u=n.bottom+8,d=n.left;const c=window.innerHeight-n.bottom,g=n.top;cr.height+20?n.top-r.height-8:g>c?10:window.innerHeight-r.height-10);const m=window.innerWidth-n.left;mt.event),(e=>{e&&e.type&&("mouseenter"===e.type?(a.value=!1,(0,s.Y3)((()=>d()))):"mouseleave"===e.type&&(a.value=!0))}),{immediate:!0}),(0,s.YP)((()=>t.result),(()=>{a.value||(0,s.Y3)((()=>d()))})),(t,d)=>((0,s.wg)(),(0,s.iD)("div",{id:"tooltip",ref_key:"tooltip",ref:i,class:(0,n.C_)(["fixed z-50 px-3 py-2 text-sm rounded-md shadow-lg border transition-all duration-200","bg-popover text-popover-foreground border-border",a.value?"invisible opacity-0":"visible opacity-100"]),style:(0,n.j5)(`top: ${l.value}px; left: ${o.value}px;`)},[e.result?((0,s.wg)(),(0,s.iD)("div",I,[(0,s._)("div",null,[d[0]||(d[0]=(0,s._)("div",{class:"text-xs font-semibold text-muted-foreground uppercase tracking-wider"},"Timestamp",-1)),(0,s._)("div",M,(0,n.zw)((0,r.SU)(u)(e.result.timestamp)),1)]),(0,s._)("div",null,[d[1]||(d[1]=(0,s._)("div",{class:"text-xs font-semibold text-muted-foreground uppercase tracking-wider"},"Response Time",-1)),(0,s._)("div",Z,(0,n.zw)((e.result.duration/1e6).toFixed(0))+"ms",1)]),e.result.conditionResults&&e.result.conditionResults.length?((0,s.wg)(),(0,s.iD)("div",N,[d[2]||(d[2]=(0,s._)("div",{class:"text-xs font-semibold text-muted-foreground uppercase tracking-wider"},"Conditions",-1)),(0,s._)("div",q,[((0,s.wg)(!0),(0,s.iD)(s.HY,null,(0,s.Ko)(e.result.conditionResults,((e,t)=>((0,s.wg)(),(0,s.iD)("div",{key:t,class:"flex items-start gap-1"},[(0,s._)("span",{class:(0,n.C_)(e.success?"text-green-500":"text-red-500")},(0,n.zw)(e.success?"✓":"✗"),3),(0,s._)("span",Y,(0,n.zw)(e.condition),1)])))),128))])])):(0,s.kq)("",!0),e.result.errors&&e.result.errors.length?((0,s.wg)(),(0,s.iD)("div",P,[d[3]||(d[3]=(0,s._)("div",{class:"text-xs font-semibold text-muted-foreground uppercase tracking-wider"},"Errors",-1)),(0,s._)("div",O,[((0,s.wg)(!0),(0,s.iD)(s.HY,null,(0,s.Ko)(e.result.errors,((e,t)=>((0,s.wg)(),(0,s.iD)("div",{key:t,class:"text-red-500"}," • "+(0,n.zw)(e),1)))),128))])])):(0,s.kq)("",!0)])):(0,s.kq)("",!0)],6))}};const B=V;var G=B;const K={class:"flex justify-center items-center"};var J={__name:"Loading",props:{size:{type:String,default:"md",validator:e=>["xs","sm","md","lg","xl"].includes(e)}},setup(e){const t=e,a=(0,s.Fl)((()=>{const e={xs:"w-4 h-4",sm:"w-6 h-6",md:"w-8 h-8",lg:"w-12 h-12",xl:"w-16 h-16"};return e[t.size]||e.md}));return(e,t)=>((0,s.wg)(),(0,s.iD)("div",K,[(0,s._)("img",{class:(0,n.C_)(["animate-spin rounded-full opacity-60 grayscale",a.value]),src:o,alt:"Gatus logo"},null,2)]))}};const Q=J;var X=Q;const ee={id:"global",class:"bg-background text-foreground"},te={key:0,class:"flex items-center justify-center min-h-screen"},ae={key:1,class:"relative"},le={class:"border-b bg-card/50 backdrop-blur supports-[backdrop-filter]:bg-card/60"},se={class:"container mx-auto px-4 py-4 max-w-7xl"},ne={class:"flex items-center justify-between"},re={class:"flex items-center gap-4"},oe={class:"w-12 h-12 flex items-center justify-center"},ie=["src"],ue={key:1,src:o,alt:"Gatus",class:"w-full h-full object-contain"},de={class:"text-2xl font-bold tracking-tight"},ce={key:0,class:"text-sm text-muted-foreground"},ge={class:"flex items-center gap-2"},me={key:0,class:"hidden md:flex items-center gap-1"},ve=["href"],pe={key:0,class:"md:hidden mt-4 pt-4 border-t space-y-1"},fe=["href"],we={class:"relative"},he={class:"border-t mt-auto"},ye={class:"container mx-auto px-4 py-6 max-w-7xl"},be={class:"flex flex-col items-center gap-4"},xe={key:2,id:"login-container",class:"flex items-center justify-center min-h-screen p-4"},_e={key:0,class:"mb-6"},ke={class:"p-3 rounded-md bg-destructive/10 border border-destructive/20"},Se={class:"text-sm text-destructive text-center"},Ue={key:0},De={key:1},We=["href"];var He={__name:"App",setup(e){const t=(0,i.yj)(),a=(0,r.iH)(!1),l=(0,r.iH)({oidc:!1,authenticated:!0}),g=(0,r.iH)([]),m=(0,r.iH)({}),v=(0,r.iH)(!1),p=(0,r.iH)(!1);let f=null;const w=(0,s.Fl)((()=>window.config&&window.config.logo&&"{{ .UI.Logo }}"!==window.config.logo?window.config.logo:"")),h=(0,s.Fl)((()=>window.config&&window.config.header&&"{{ .UI.Header }}"!==window.config.header?window.config.header:"Gatus")),b=(0,s.Fl)((()=>window.config&&window.config.link&&"{{ .UI.Link }}"!==window.config.link?window.config.link:null)),x=(0,s.Fl)((()=>window.config&&window.config.buttons?window.config.buttons:[])),k=async()=>{try{const e=await fetch(`${Al}/api/v1/config`,{credentials:"include"});if(a.value=!0,200===e.status){const t=await e.json();l.value=t,g.value=t.announcements||[]}}catch(e){console.error("Failed to fetch config:",e),a.value=!0}},S=(e,t)=>{m.value={result:e,event:t}};return(0,s.bv)((()=>{k(),f=setInterval(k,6e5)})),(0,s.Ah)((()=>{f&&(clearInterval(f),f=null)})),(e,i)=>{const f=(0,s.up)("router-view");return(0,s.wg)(),(0,s.iD)("div",ee,[a.value?l.value&&l.value.oidc&&!l.value.authenticated?((0,s.wg)(),(0,s.iD)("div",xe,[(0,s.Wm)((0,r.SU)(_),{class:"w-full max-w-md"},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(U),{class:"text-center"},{default:(0,s.w5)((()=>[i[5]||(i[5]=(0,s._)("img",{src:o,alt:"Gatus",class:"w-20 h-20 mx-auto mb-4"},null,-1)),(0,s.Wm)((0,r.SU)(H),{class:"text-3xl"},{default:(0,s.w5)((()=>i[4]||(i[4]=[(0,s.Uk)("Gatus",-1)]))),_:1,__:[4]}),i[6]||(i[6]=(0,s._)("p",{class:"text-muted-foreground mt-2"},"System Monitoring Dashboard",-1))])),_:1,__:[5,6]}),(0,s.Wm)((0,r.SU)(j),null,{default:(0,s.w5)((()=>[(0,r.SU)(t)&&(0,r.SU)(t).query.error?((0,s.wg)(),(0,s.iD)("div",_e,[(0,s._)("div",ke,[(0,s._)("p",Se,["access_denied"===(0,r.SU)(t).query.error?((0,s.wg)(),(0,s.iD)("span",Ue," You do not have access to this status page ")):((0,s.wg)(),(0,s.iD)("span",De,(0,n.zw)((0,r.SU)(t).query.error),1))])])])):(0,s.kq)("",!0),(0,s._)("a",{href:`${(0,r.SU)(Al)}/oidc/login`,class:"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-11 px-8 w-full",onClick:i[2]||(i[2]=e=>p.value=!0)},[p.value?((0,s.wg)(),(0,s.j4)(X,{key:0,size:"xs"})):((0,s.wg)(),(0,s.iD)(s.HY,{key:1},[(0,s.Wm)((0,r.SU)(c.Z),{class:"mr-2 h-4 w-4"}),i[7]||(i[7]=(0,s.Uk)(" Login with OIDC ",-1))],64))],8,We)])),_:1})])),_:1})])):((0,s.wg)(),(0,s.iD)("div",ae,[(0,s._)("header",le,[(0,s._)("div",se,[(0,s._)("div",ne,[(0,s._)("div",re,[((0,s.wg)(),(0,s.j4)((0,s.LL)(b.value?"a":"div"),{href:b.value,target:"_blank",class:"flex items-center gap-3 hover:opacity-80 transition-opacity"},{default:(0,s.w5)((()=>[(0,s._)("div",oe,[w.value?((0,s.wg)(),(0,s.iD)("img",{key:0,src:w.value,alt:"Gatus",class:"w-full h-full object-contain"},null,8,ie)):((0,s.wg)(),(0,s.iD)("img",ue))]),(0,s._)("div",null,[(0,s._)("h1",de,(0,n.zw)(h.value),1),x.value&&x.value.length?((0,s.wg)(),(0,s.iD)("p",ce," System Monitoring Dashboard ")):(0,s.kq)("",!0)])])),_:1},8,["href"]))]),(0,s._)("div",ge,[x.value&&x.value.length?((0,s.wg)(),(0,s.iD)("nav",me,[((0,s.wg)(!0),(0,s.iD)(s.HY,null,(0,s.Ko)(x.value,(e=>((0,s.wg)(),(0,s.iD)("a",{key:e.name,href:e.link,target:"_blank",class:"px-3 py-2 text-sm font-medium rounded-md hover:bg-accent hover:text-accent-foreground transition-colors"},(0,n.zw)(e.name),9,ve)))),128))])):(0,s.kq)("",!0),x.value&&x.value.length?((0,s.wg)(),(0,s.j4)((0,r.SU)(y),{key:1,variant:"ghost",size:"icon",class:"md:hidden",onClick:i[0]||(i[0]=e=>v.value=!v.value)},{default:(0,s.w5)((()=>[v.value?((0,s.wg)(),(0,s.j4)((0,r.SU)(d.Z),{key:1,class:"h-5 w-5"})):((0,s.wg)(),(0,s.j4)((0,r.SU)(u.Z),{key:0,class:"h-5 w-5"}))])),_:1})):(0,s.kq)("",!0)])]),x.value&&x.value.length&&v.value?((0,s.wg)(),(0,s.iD)("nav",pe,[((0,s.wg)(!0),(0,s.iD)(s.HY,null,(0,s.Ko)(x.value,(e=>((0,s.wg)(),(0,s.iD)("a",{key:e.name,href:e.link,target:"_blank",class:"block px-3 py-2 text-sm font-medium rounded-md hover:bg-accent hover:text-accent-foreground transition-colors",onClick:i[1]||(i[1]=e=>v.value=!1)},(0,n.zw)(e.name),9,fe)))),128))])):(0,s.kq)("",!0)])]),(0,s._)("main",we,[(0,s.Wm)(f,{onShowTooltip:S,announcements:g.value},null,8,["announcements"])]),(0,s._)("footer",he,[(0,s._)("div",ye,[(0,s._)("div",be,[i[3]||(i[3]=(0,s._)("div",{class:"text-sm text-muted-foreground text-center"},[(0,s.Uk)(" Powered by "),(0,s._)("a",{href:"https://gatus.io",target:"_blank",class:"font-medium text-emerald-800 hover:text-emerald-600"},"Gatus")],-1)),(0,s.Wm)(E)])])])])):((0,s.wg)(),(0,s.iD)("div",te,[(0,s.Wm)(X,{size:"lg"})])),(0,s.Wm)(G,{result:m.value.result,event:m.value.event},null,8,["result","event"])])}}};const Ce=He;var ze=Ce,je=a(793),Te=a(138),Fe=a(254),$e=a(146),Re=a(485),Ae=a(893),Ee=a(89),Le=a(372),Ie=a(981),Me={__name:"Badge",props:{variant:{type:String,default:"default"}},setup(e){const t=(0,g.j)("inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",{variants:{variant:{default:"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",secondary:"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",destructive:"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",outline:"text-foreground",success:"border-transparent bg-green-500 text-white",warning:"border-transparent bg-yellow-500 text-white"}},defaultVariants:{variant:"default"}});return(a,l)=>((0,s.wg)(),(0,s.iD)("div",{class:(0,n.C_)((0,r.SU)(p)((0,r.SU)(t)({variant:e.variant}),a.$attrs.class??""))},[(0,s.WI)(a.$slots,"default")],2))}};const Ze=Me;var Ne=Ze,qe={__name:"StatusBadge",props:{status:{type:String,required:!0,validator:e=>["healthy","unhealthy","degraded","unknown"].includes(e)}},setup(e){const t=e,a=(0,s.Fl)((()=>{switch(t.status){case"healthy":return"success";case"unhealthy":return"destructive";case"degraded":return"warning";default:return"secondary"}})),l=(0,s.Fl)((()=>{switch(t.status){case"healthy":return"Healthy";case"unhealthy":return"Unhealthy";case"degraded":return"Degraded";default:return"Unknown"}})),o=(0,s.Fl)((()=>{switch(t.status){case"healthy":return"bg-green-400";case"unhealthy":return"bg-red-400";case"degraded":return"bg-yellow-400";default:return"bg-gray-400"}}));return(e,t)=>((0,s.wg)(),(0,s.j4)((0,r.SU)(Ne),{variant:a.value,class:"flex items-center gap-1"},{default:(0,s.w5)((()=>[(0,s._)("span",{class:(0,n.C_)(["w-2 h-2 rounded-full",o.value])},null,2),(0,s.Uk)(" "+(0,n.zw)(l.value),1)])),_:1},8,["variant"]))}};const Ye=qe;var Pe=Ye;const Oe={class:"flex items-start justify-between gap-2 sm:gap-3"},Ve={class:"flex-1 min-w-0 overflow-hidden"},Be=["title","aria-label"],Ge={class:"flex items-center gap-2 text-xs sm:text-sm text-muted-foreground"},Ke=["title"],Je={key:1},Qe=["title"],Xe={class:"flex-shrink-0 ml-2"},et={class:"space-y-2"},tt={class:"flex items-center justify-between mb-1"},at=["title"],lt={class:"flex gap-0.5"},st=["onMouseenter","onMouseleave"],nt={class:"flex items-center justify-between text-xs text-muted-foreground mt-1"};var rt={__name:"EndpointCard",props:{endpoint:{type:Object,required:!0},maxResults:{type:Number,default:50},showAverageResponseTime:{type:Boolean,default:!0}},emits:["showTooltip"],setup(e,{emit:t}){const a=(0,i.tv)(),o=e,u=t,d=(0,s.Fl)((()=>o.endpoint.results&&0!==o.endpoint.results.length?o.endpoint.results[o.endpoint.results.length-1]:null)),c=(0,s.Fl)((()=>d.value?d.value.success?"healthy":"unhealthy":"unknown")),g=(0,s.Fl)((()=>d.value?.hostname||null)),m=(0,s.Fl)((()=>{const e=[...o.endpoint.results||[]];while(e.length{if(!o.endpoint.results||0===o.endpoint.results.length)return"N/A";let e=0,t=0,a=1/0,l=0;for(const s of o.endpoint.results)if(s.duration){const n=s.duration/1e6;e+=n,t++,a=Math.min(a,n),l=Math.max(l,n)}if(0===t)return"N/A";if(o.showAverageResponseTime){const a=Math.round(e/t);return`~${a}ms`}{const e=Math.round(a),t=Math.round(l);return e===t?`${e}ms`:`${e}-${t}ms`}})),p=(0,s.Fl)((()=>o.endpoint.results&&0!==o.endpoint.results.length?L.methods.generatePrettyTimeAgo(o.endpoint.results[0].timestamp):"")),f=(0,s.Fl)((()=>o.endpoint.results&&0!==o.endpoint.results.length?L.methods.generatePrettyTimeAgo(o.endpoint.results[o.endpoint.results.length-1].timestamp):"")),w=()=>{a.push(`/endpoints/${o.endpoint.key}`)};return(t,a)=>((0,s.wg)(),(0,s.j4)((0,r.SU)(_),{class:"endpoint h-full flex flex-col transition hover:shadow-lg hover:scale-[1.01] dark:hover:border-gray-700"},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(U),{class:"endpoint-header px-3 sm:px-6 pt-3 sm:pt-6 pb-2 space-y-0"},{default:(0,s.w5)((()=>[(0,s._)("div",Oe,[(0,s._)("div",Ve,[(0,s.Wm)((0,r.SU)(H),{class:"text-base sm:text-lg truncate"},{default:(0,s.w5)((()=>[(0,s._)("span",{class:"hover:text-primary cursor-pointer hover:underline text-sm sm:text-base block truncate",onClick:w,onKeydown:(0,l.D2)(w,["enter"]),title:e.endpoint.name,role:"link",tabindex:"0","aria-label":`View details for ${e.endpoint.name}`},(0,n.zw)(e.endpoint.name),41,Be)])),_:1}),(0,s._)("div",Ge,[e.endpoint.group?((0,s.wg)(),(0,s.iD)("span",{key:0,class:"truncate",title:e.endpoint.group},(0,n.zw)(e.endpoint.group),9,Ke)):(0,s.kq)("",!0),e.endpoint.group&&g.value?((0,s.wg)(),(0,s.iD)("span",Je,"•")):(0,s.kq)("",!0),g.value?((0,s.wg)(),(0,s.iD)("span",{key:2,class:"truncate",title:g.value},(0,n.zw)(g.value),9,Qe)):(0,s.kq)("",!0)])]),(0,s._)("div",Xe,[(0,s.Wm)(Pe,{status:c.value},null,8,["status"])])])])),_:1}),(0,s.Wm)((0,r.SU)(j),{class:"endpoint-content flex-1 pb-3 sm:pb-4 px-3 sm:px-6 pt-2"},{default:(0,s.w5)((()=>[(0,s._)("div",et,[(0,s._)("div",null,[(0,s._)("div",tt,[a[0]||(a[0]=(0,s._)("div",{class:"flex-1"},null,-1)),(0,s._)("p",{class:"text-xs text-muted-foreground",title:e.showAverageResponseTime?"Average response time":"Minimum and maximum response time"},(0,n.zw)(v.value),9,at)]),(0,s._)("div",lt,[((0,s.wg)(!0),(0,s.iD)(s.HY,null,(0,s.Ko)(m.value,((e,t)=>((0,s.wg)(),(0,s.iD)("div",{key:t,class:(0,n.C_)(["flex-1 h-6 sm:h-8 rounded-sm transition-all",e?e.success?"bg-green-500 hover:bg-green-700":"bg-red-500 hover:bg-red-700":"bg-gray-200 dark:bg-gray-700"]),onMouseenter:t=>e&&u("showTooltip",e,t),onMouseleave:t=>e&&u("showTooltip",null,t)},null,42,st)))),128))]),(0,s._)("div",nt,[(0,s._)("span",null,(0,n.zw)(p.value),1),(0,s._)("span",null,(0,n.zw)(f.value),1)])])])])),_:1})])),_:1}))}};const ot=rt;var it=ot,ut=a(275);const dt=["value"];var ct={__name:"Input",props:{modelValue:{type:[String,Number],default:""}},emits:["update:modelValue"],setup(e){return(t,a)=>((0,s.wg)(),(0,s.iD)("input",{class:(0,n.C_)((0,r.SU)(p)("flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",t.$attrs.class??"")),value:e.modelValue,onInput:a[0]||(a[0]=e=>t.$emit("update:modelValue",e.target.value))},null,42,dt))}};const gt=ct;var mt=gt,vt=a(368);const pt=["aria-expanded","aria-label"],ft={class:"truncate"},wt={key:0,role:"listbox",class:"absolute top-full left-0 z-50 mt-1 w-full rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95"},ht={class:"p-1"},yt=["onClick","aria-selected"],bt={class:"absolute left-1.5 sm:left-2 flex h-3.5 w-3.5 items-center justify-center"};var xt={__name:"Select",props:{modelValue:{type:String,default:""},options:{type:Array,required:!0},placeholder:{type:String,default:"Select..."},class:{type:String,default:""}},emits:["update:modelValue"],setup(e,{emit:t}){const a=e,l=t,o=(0,r.iH)(!1),i=(0,r.iH)(null),u=(0,r.iH)(-1),d=(0,s.Fl)((()=>a.options.find((e=>e.value===a.modelValue))||{label:a.placeholder,value:""})),c=e=>{l("update:modelValue",e.value),o.value=!1},g=()=>{if(o.value=!o.value,o.value){const e=a.options.findIndex((e=>e.value===a.modelValue));u.value=e>=0?e:0}else u.value=-1},m=e=>{i.value&&!i.value.contains(e.target)&&(o.value=!1,u.value=-1)},v=e=>{if(o.value)switch(e.key){case"ArrowDown":e.preventDefault(),u.value=Math.min(u.value+1,a.options.length-1);break;case"ArrowUp":e.preventDefault(),u.value=Math.max(u.value-1,0);break;case"Enter":case" ":e.preventDefault(),u.value>=0&&u.value{document.addEventListener("click",m)})),(0,s.Ah)((()=>{document.removeEventListener("click",m)})),(t,l)=>((0,s.wg)(),(0,s.iD)("div",{ref_key:"selectRef",ref:i,class:(0,n.C_)(["relative",a.class])},[(0,s._)("button",{onClick:g,onKeydown:v,"aria-expanded":o.value,"aria-haspopup":!0,"aria-label":d.value.label||a.placeholder,class:"flex h-9 sm:h-10 w-full items-center justify-between rounded-md border border-input bg-background px-2 sm:px-3 py-1.5 sm:py-2 text-xs sm:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"},[(0,s._)("span",ft,(0,n.zw)(d.value.label),1),(0,s.Wm)((0,r.SU)(Re.Z),{class:"h-3 w-3 sm:h-4 sm:w-4 opacity-50 flex-shrink-0 ml-1"})],40,pt),o.value?((0,s.wg)(),(0,s.iD)("div",wt,[(0,s._)("div",ht,[((0,s.wg)(!0),(0,s.iD)(s.HY,null,(0,s.Ko)(e.options,((t,a)=>((0,s.wg)(),(0,s.iD)("div",{key:t.value,onClick:e=>c(t),class:(0,n.C_)(["relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-6 sm:pl-8 pr-2 text-xs sm:text-sm outline-none hover:bg-accent hover:text-accent-foreground",a===u.value&&"bg-accent text-accent-foreground"]),role:"option","aria-selected":e.modelValue===t.value},[(0,s._)("span",bt,[e.modelValue===t.value?((0,s.wg)(),(0,s.j4)((0,r.SU)(vt.Z),{key:0,class:"h-3 w-3 sm:h-4 sm:w-4"})):(0,s.kq)("",!0)]),(0,s.Uk)(" "+(0,n.zw)(t.label),1)],10,yt)))),128))])])):(0,s.kq)("",!0)],2))}};const _t=xt;var kt=_t;const St={class:"flex flex-col lg:flex-row gap-3 lg:gap-4 p-3 sm:p-4 bg-card rounded-lg border"},Ut={class:"flex-1"},Dt={class:"relative"},Wt={class:"flex flex-col sm:flex-row gap-3 sm:gap-4"},Ht={class:"flex items-center gap-2 flex-1 sm:flex-initial"},Ct={class:"flex items-center gap-2 flex-1 sm:flex-initial"};var zt={__name:"SearchBar",emits:["search","update:showOnlyFailing","update:showRecentFailures","update:groupByGroup","update:sortBy","initializeCollapsedGroups"],setup(e,{emit:t}){const a=(0,r.iH)(""),l=(0,r.iH)(localStorage.getItem("gatus:filter-by")||"undefined"!==typeof window&&window.config?.defaultFilterBy||"none"),n=(0,r.iH)(localStorage.getItem("gatus:sort-by")||"undefined"!==typeof window&&window.config?.defaultSortBy||"name"),o=[{label:"None",value:"none"},{label:"Failing",value:"failing"},{label:"Unstable",value:"unstable"}],i=[{label:"Name",value:"name"},{label:"Group",value:"group"},{label:"Health",value:"health"}],u=t,d=e=>{l.value=e,localStorage.setItem("gatus:filter-by",e),u("update:showOnlyFailing",!1),u("update:showRecentFailures",!1),"failing"===e?u("update:showOnlyFailing",!0):"unstable"===e&&u("update:showRecentFailures",!0)},c=e=>{n.value=e,localStorage.setItem("gatus:sort-by",e),u("update:sortBy",e),u("update:groupByGroup","group"===e),"group"===e&&u("initializeCollapsedGroups")};return(0,s.bv)((()=>{d(l.value),c(n.value)})),(e,t)=>((0,s.wg)(),(0,s.iD)("div",St,[(0,s._)("div",Ut,[(0,s._)("div",Dt,[(0,s.Wm)((0,r.SU)(ut.Z),{class:"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground"}),t[4]||(t[4]=(0,s._)("label",{for:"search-input",class:"sr-only"},"Search endpoints",-1)),(0,s.Wm)((0,r.SU)(mt),{id:"search-input",modelValue:a.value,"onUpdate:modelValue":t[0]||(t[0]=e=>a.value=e),type:"text",placeholder:"Search endpoints...",class:"pl-10 text-sm sm:text-base",onInput:t[1]||(t[1]=t=>e.$emit("search",a.value))},null,8,["modelValue"])])]),(0,s._)("div",Wt,[(0,s._)("div",Ht,[t[5]||(t[5]=(0,s._)("label",{class:"text-xs sm:text-sm font-medium text-muted-foreground whitespace-nowrap"},"Filter by:",-1)),(0,s.Wm)((0,r.SU)(kt),{modelValue:l.value,"onUpdate:modelValue":[t[2]||(t[2]=e=>l.value=e),d],options:o,placeholder:"None",class:"flex-1 sm:w-[140px] md:w-[160px]"},null,8,["modelValue"])]),(0,s._)("div",Ct,[t[6]||(t[6]=(0,s._)("label",{class:"text-xs sm:text-sm font-medium text-muted-foreground whitespace-nowrap"},"Sort by:",-1)),(0,s.Wm)((0,r.SU)(kt),{modelValue:n.value,"onUpdate:modelValue":[t[3]||(t[3]=e=>n.value=e),c],options:i,placeholder:"Name",class:"flex-1 sm:w-[90px] md:w-[100px]"},null,8,["modelValue"])])])]))}};const jt=zt;var Tt=jt,Ft=a(789),$t=a(679);const Rt={id:"settings",class:"fixed bottom-4 left-4 z-50"},At={class:"flex items-center gap-1 bg-background/95 backdrop-blur-sm border rounded-full shadow-md p-1"},Et=["aria-label","aria-expanded"],Lt={class:"text-xs font-medium"},It=["onClick"],Mt=["aria-label"],Zt={class:"absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded-md shadow-md opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap"},Nt="300",qt="theme",Yt=31536e3;var Pt={__name:"Settings",emits:["refreshData"],setup(e,{emit:t}){const a=t,o=[{value:"10",label:"10s"},{value:"30",label:"30s"},{value:"60",label:"1m"},{value:"120",label:"2m"},{value:"300",label:"5m"},{value:"600",label:"10m"}],i={REFRESH_INTERVAL:"gatus:refresh-interval"};function u(){const e=document.cookie.match(new RegExp(`${qt}=(dark|light);?`))?.[1];return"dark"===e||!e&&(window.matchMedia("(prefers-color-scheme: dark)").matches||document.documentElement.classList.contains("dark"))}function d(){const e=localStorage.getItem(i.REFRESH_INTERVAL),t=e&&parseInt(e),a=t&&t>=10&&o.some((t=>t.value===e));return a?e:Nt}const c=(0,r.iH)(d()),g=(0,r.iH)(u()),m=(0,r.iH)(!1);let v=null;const p=e=>{const t=o.find((t=>t.value===e));return t?t.label:`${e}s`},f=e=>{localStorage.setItem(i.REFRESH_INTERVAL,e),v&&clearInterval(v),v=setInterval((()=>{w()}),1e3*e)},w=()=>{a("refreshData")},h=e=>{c.value=e,m.value=!1,w(),f(e)},y=e=>{const t=document.getElementById("settings");t&&!t.contains(e.target)&&(m.value=!1)},b=e=>{document.cookie=`${qt}=${e}; path=/; max-age=${Yt}; samesite=strict`},x=()=>{const e=u()?"light":"dark";b(e),_()},_=()=>{const e=u();g.value=e,document.documentElement.classList.toggle("dark",e)};return(0,s.bv)((()=>{f(c.value),_(),document.addEventListener("click",y)})),(0,s.Ah)((()=>{v&&clearInterval(v),document.removeEventListener("click",y)})),(e,t)=>((0,s.wg)(),(0,s.iD)("div",Rt,[(0,s._)("div",At,[(0,s._)("button",{onClick:t[1]||(t[1]=e=>m.value=!m.value),"aria-label":`Refresh interval: ${p(c.value)}`,"aria-expanded":m.value,class:"flex items-center gap-1.5 px-3 py-1.5 rounded-full hover:bg-accent transition-colors relative"},[(0,s.Wm)((0,r.SU)(Fe.Z),{class:"w-3.5 h-3.5 text-muted-foreground"}),(0,s._)("span",Lt,(0,n.zw)(p(c.value)),1),m.value?((0,s.wg)(),(0,s.iD)("div",{key:0,onClick:t[0]||(t[0]=(0,l.iM)((()=>{}),["stop"])),class:"absolute bottom-full left-0 mb-2 bg-popover border rounded-lg shadow-lg overflow-hidden"},[((0,s.wg)(),(0,s.iD)(s.HY,null,(0,s.Ko)(o,(e=>(0,s._)("button",{key:e.value,onClick:t=>h(e.value),class:(0,n.C_)(["block w-full px-4 py-2 text-xs text-left hover:bg-accent transition-colors",c.value===e.value&&"bg-accent"])},(0,n.zw)(e.label),11,It))),64))])):(0,s.kq)("",!0)],8,Et),t[2]||(t[2]=(0,s._)("div",{class:"h-5 w-px bg-border/50"},null,-1)),(0,s._)("button",{onClick:x,"aria-label":g.value?"Switch to light mode":"Switch to dark mode",class:"p-1.5 rounded-full hover:bg-accent transition-colors group relative"},[g.value?((0,s.wg)(),(0,s.j4)((0,r.SU)(Ft.Z),{key:0,class:"h-3.5 w-3.5 transition-all"})):((0,s.wg)(),(0,s.j4)((0,r.SU)($t.Z),{key:1,class:"h-3.5 w-3.5 transition-all"})),(0,s._)("div",Zt,(0,n.zw)(g.value?"Light mode":"Dark mode"),1)],8,Mt)])]))}};const Ot=(0,$.Z)(Pt,[["__scopeId","data-v-482756f8"]]);var Vt=Ot,Bt=a(691),Gt=a(446),Kt=a(5),Jt=a(337);const Qt={key:0,class:"announcement-container mb-6"},Xt={class:"flex items-center justify-between"},ea={class:"flex items-center gap-2"},ta={class:"text-xs text-gray-500 dark:text-gray-400"},aa={key:0,class:"announcement-content p-4 transition-all duration-200 rounded-b-lg"},la={class:"relative"},sa={class:"space-y-3"},na={class:"flex items-center gap-3 mb-2 relative"},ra={class:"relative z-10 bg-white dark:bg-gray-800 px-2 py-1 rounded-md border border-gray-200 dark:border-gray-600"},oa={class:"text-xs font-medium text-gray-600 dark:text-gray-300"},ia={class:"space-y-2 ml-7 relative"},ua={class:"flex items-center justify-between gap-3"},da={class:"flex-1 min-w-0"},ca={class:"text-sm leading-relaxed text-gray-900 dark:text-gray-100"},ga=["title"];var ma={__name:"AnnouncementBanner",props:{announcements:{type:Array,default:()=>[]}},setup(e){const t=e,a=(0,r.iH)(!1),l=()=>{a.value=!a.value},o={outage:{icon:Bt.Z,background:"bg-red-50 border-gray-200 dark:bg-red-900/50 dark:border-gray-600",border:"border-red-500",iconColor:"text-red-600 dark:text-red-400",text:"text-red-700 dark:text-red-300"},warning:{icon:Gt.Z,background:"bg-yellow-50 border-gray-200 dark:bg-yellow-900/50 dark:border-gray-600",border:"border-yellow-500",iconColor:"text-yellow-600 dark:text-yellow-400",text:"text-yellow-700 dark:text-yellow-300"},information:{icon:Kt.Z,background:"bg-blue-50 border-gray-200 dark:bg-blue-900/50 dark:border-gray-600",border:"border-blue-500",iconColor:"text-blue-600 dark:text-blue-400",text:"text-blue-700 dark:text-blue-300"},operational:{icon:Ee.Z,background:"bg-green-50 border-gray-200 dark:bg-green-900/50 dark:border-gray-600",border:"border-green-500",iconColor:"text-green-600 dark:text-green-400",text:"text-green-700 dark:text-green-300"},none:{icon:Jt.Z,background:"bg-gray-50 border-gray-200 dark:bg-gray-800/50 dark:border-gray-600",border:"border-gray-500",iconColor:"text-gray-600 dark:text-gray-400",text:"text-gray-700 dark:text-gray-300"}},i=(0,s.Fl)((()=>t.announcements&&t.announcements.length>0?t.announcements[0]:null)),u=(0,s.Fl)((()=>{const e=i.value?.type||"none";return o[e]?.icon||Jt.Z})),d=(0,s.Fl)((()=>{const e=i.value?.type||"none";return o[e]?.iconColor||"text-gray-600 dark:text-gray-400"})),c=(0,s.Fl)((()=>{const e=i.value?.type||"none",t=o[e];return`border-l-4 ${t.border.replace("border-","border-l-")}`})),g=(0,s.Fl)((()=>{if(!t.announcements||0===t.announcements.length)return{};const e={};return t.announcements.forEach((t=>{const a=new Date(t.timestamp).toDateString();e[a]||(e[a]=[]),e[a].push(t)})),e})),m=e=>o[e]?.icon||Jt.Z,v=e=>o[e]||o.none,p=e=>{const t=1===e.length?"2rem":2+3.5*(e.length-1)+"rem";return{top:"1.5rem",height:t}},f=e=>{const t=new Date(e),a=new Date,l=new Date(a);return l.setDate(l.getDate()-1),t.toDateString()===a.toDateString()?"Today":t.toDateString()===l.toDateString()?"Yesterday":t.toLocaleDateString("en-US",{weekday:"long",year:"numeric",month:"long",day:"numeric"})},w=e=>new Date(e).toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit",hour12:!1}),h=e=>new Date(e).toLocaleString("en-US",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"});return(t,o)=>e.announcements&&e.announcements.length?((0,s.wg)(),(0,s.iD)("div",Qt,[(0,s._)("div",{class:(0,n.C_)(["rounded-lg border bg-card text-card-foreground shadow-sm transition-all duration-200",c.value])},[(0,s._)("div",{class:(0,n.C_)(["announcement-header px-4 py-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors",a.value?"rounded-lg":"rounded-t-lg border-b border-gray-200 dark:border-gray-600"]),onClick:l},[(0,s._)("div",Xt,[(0,s._)("div",ea,[((0,s.wg)(),(0,s.j4)((0,s.LL)(u.value),{class:(0,n.C_)(["w-5 h-5",d.value])},null,8,["class"])),o[0]||(o[0]=(0,s._)("h2",{class:"text-base font-semibold text-gray-900 dark:text-gray-100"},"Announcements",-1)),(0,s._)("span",ta," ("+(0,n.zw)(e.announcements.length)+") ",1)]),(0,s.Wm)((0,r.SU)(Re.Z),{class:(0,n.C_)(["w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform duration-200",a.value?"-rotate-90":"rotate-0"])},null,8,["class"])])],2),a.value?(0,s.kq)("",!0):((0,s.wg)(),(0,s.iD)("div",aa,[(0,s._)("div",la,[(0,s._)("div",sa,[((0,s.wg)(!0),(0,s.iD)(s.HY,null,(0,s.Ko)(g.value,((e,t)=>((0,s.wg)(),(0,s.iD)("div",{key:t,class:"relative"},[e.length>0?((0,s.wg)(),(0,s.iD)("div",{key:0,class:"absolute left-3 w-0.5 bg-gray-300 dark:bg-gray-600 pointer-events-none",style:(0,n.j5)(p(e))},null,4)):(0,s.kq)("",!0),(0,s._)("div",na,[(0,s._)("div",ra,[(0,s._)("time",oa,(0,n.zw)(f(t)),1)]),o[1]||(o[1]=(0,s._)("div",{class:"flex-1 border-t border-gray-200 dark:border-gray-600"},null,-1))]),(0,s._)("div",ia,[((0,s.wg)(!0),(0,s.iD)(s.HY,null,(0,s.Ko)(e,((e,a)=>((0,s.wg)(),(0,s.iD)("div",{key:`${t}-${a}-${e.timestamp}`,class:"relative"},[(0,s._)("div",{class:(0,n.C_)(["absolute -left-[26px] top-1/2 -translate-y-1/2 w-5 h-5 rounded-full border bg-white dark:bg-gray-800 flex items-center justify-center z-10",v(e.type).border])},[((0,s.wg)(),(0,s.j4)((0,s.LL)(m(e.type)),{class:(0,n.C_)(["w-3 h-3",v(e.type).iconColor])},null,8,["class"]))],2),(0,s._)("div",{class:(0,n.C_)(["rounded-md border p-3 transition-all duration-200 hover:shadow-sm",v(e.type).background])},[(0,s._)("div",ua,[(0,s._)("div",da,[(0,s._)("p",ca,(0,n.zw)(e.message),1)]),(0,s._)("time",{class:(0,n.C_)(["text-xs font-mono whitespace-nowrap",v(e.type).text]),title:h(e.timestamp)},(0,n.zw)(w(e.timestamp)),11,ga)])],2)])))),128))])])))),128))])])]))],2)])):(0,s.kq)("",!0)}};const va=(0,$.Z)(ma,[["__scopeId","data-v-48763619"]]);var pa=va;const fa={class:"dashboard-container bg-background"},wa={class:"container mx-auto px-4 py-8 max-w-7xl"},ha={class:"mb-6"},ya={class:"flex items-center justify-between mb-6"},ba={class:"flex items-center gap-4"},xa={key:0,class:"flex items-center justify-center py-20"},_a={key:1,class:"text-center py-20"},ka={class:"text-muted-foreground"},Sa={key:2},Ua={key:0,class:"space-y-6"},Da=["onClick"],Wa={class:"flex items-center gap-3"},Ha={class:"text-xl font-semibold text-foreground"},Ca={class:"flex items-center gap-2"},za={key:0,class:"bg-red-600 text-white px-2 py-1 rounded-full text-sm font-medium"},ja={key:0,class:"endpoint-group-content p-4"},Ta={class:"grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"},Fa={key:1,class:"grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"},$a={key:2,class:"mt-8 flex items-center justify-center gap-2"},Ra={class:"flex gap-1"},Aa=96;var Ea={__name:"Home",props:{announcements:{type:Array,default:()=>[]}},emits:["showTooltip"],setup(e,{emit:t}){const a=e,l=t,o=(0,r.iH)([]),i=(0,r.iH)(!1),u=(0,r.iH)(1),d=(0,r.iH)(""),c=(0,r.iH)(!1),g=(0,r.iH)(!1),m=(0,r.iH)(!0),v=(0,r.iH)(!1),p=(0,r.iH)(localStorage.getItem("gatus:sort-by")||"name"),f=(0,r.iH)(new Set),w=(0,s.Fl)((()=>{let e=[...o.value];if(d.value){const t=d.value.toLowerCase();e=e.filter((e=>e.name.toLowerCase().includes(t)||e.group&&e.group.toLowerCase().includes(t)))}return c.value&&(e=e.filter((e=>{if(!e.results||0===e.results.length)return!1;const t=e.results[e.results.length-1];return!t.success}))),g.value&&(e=e.filter((e=>!(!e.results||0===e.results.length)&&e.results.some((e=>!e.success))))),"health"===p.value&&e.sort(((e,t)=>{const a=e.results&&e.results.length>0&&e.results[e.results.length-1].success,l=t.results&&t.results.length>0&&t.results[t.results.length-1].success;return!a&&l?-1:a&&!l?1:e.name.localeCompare(t.name)})),e})),h=(0,s.Fl)((()=>Math.ceil(w.value.length/Aa))),b=(0,s.Fl)((()=>{if(!v.value)return null;const e={};w.value.forEach((t=>{const a=t.group||"No Group";e[a]||(e[a]=[]),e[a].push(t)}));const t=Object.keys(e).sort(((e,t)=>"No Group"===e?1:"No Group"===t?-1:e.localeCompare(t))),a={};return t.forEach((t=>{a[t]=e[t]})),a})),x=(0,s.Fl)((()=>{if(v.value)return b.value;const e=(u.value-1)*Aa,t=e+Aa;return w.value.slice(e,t)})),_=(0,s.Fl)((()=>{const e=[],t=5;let a=Math.max(1,u.value-Math.floor(t/2)),l=Math.min(h.value,a+t-1);l-a{const e=0===o.value.length;e&&(i.value=!0);try{const t=await fetch(`${Al}/api/v1/endpoints/statuses?page=1&pageSize=100`,{credentials:"include"});if(200===t.status){const a=await t.json();if(e)o.value=a;else{const e=new Set(o.value.map((e=>e.key))),t=new Set(a.map((e=>e.key))),l=a.some((t=>!e.has(t.key))),s=o.value.some((e=>!t.has(e.key)));if(l||s)o.value=a;else{const e=new Map(a.map((e=>[e.key,e])));o.value.forEach(((t,a)=>{const l=e.get(t.key);l&&Object.assign(o.value[a],l)}))}}}else console.error("[Home][fetchData] Error:",await t.text())}catch(t){console.error("[Home][fetchData] Error:",t)}finally{e&&(i.value=!1)}},S=()=>{o.value=[],k()},U=e=>{d.value=e,u.value=1},D=e=>{u.value=e,window.scrollTo({top:0,behavior:"smooth"})},W=()=>{m.value=!m.value},H=(e,t)=>{l("showTooltip",e,t)},C=e=>e.filter((e=>{if(!e.results||0===e.results.length)return!1;const t=e.results[e.results.length-1];return!t.success})).length,z=e=>{f.value.has(e)?f.value.delete(e):f.value.add(e);const t=Array.from(f.value);localStorage.setItem("gatus:uncollapsed-groups",JSON.stringify(t)),localStorage.removeItem("gatus:collapsed-groups")},j=()=>{try{const e=localStorage.getItem("gatus:uncollapsed-groups");e&&(f.value=new Set(JSON.parse(e)))}catch(e){console.warn("Failed to parse saved uncollapsed groups:",e),localStorage.removeItem("gatus:uncollapsed-groups")}};return(0,s.bv)((()=>{k()})),(e,t)=>((0,s.wg)(),(0,s.iD)("div",fa,[(0,s._)("div",wa,[(0,s._)("div",ha,[(0,s._)("div",ya,[t[6]||(t[6]=(0,s._)("div",null,[(0,s._)("h1",{class:"text-4xl font-bold tracking-tight"},"Health Dashboard"),(0,s._)("p",{class:"text-muted-foreground mt-2"},"Monitor the health of your endpoints in real-time")],-1)),(0,s._)("div",ba,[(0,s.Wm)((0,r.SU)(y),{variant:"ghost",size:"icon",onClick:W,title:m.value?"Show min-max response time":"Show average response time"},{default:(0,s.w5)((()=>[m.value?((0,s.wg)(),(0,s.j4)((0,r.SU)(je.Z),{key:0,class:"h-5 w-5"})):((0,s.wg)(),(0,s.j4)((0,r.SU)(Te.Z),{key:1,class:"h-5 w-5"}))])),_:1},8,["title"]),(0,s.Wm)((0,r.SU)(y),{variant:"ghost",size:"icon",onClick:S,title:"Refresh data"},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(Fe.Z),{class:"h-5 w-5"})])),_:1})])]),(0,s.Wm)(pa,{announcements:a.announcements},null,8,["announcements"]),(0,s.Wm)(Tt,{onSearch:U,"onUpdate:showOnlyFailing":t[0]||(t[0]=e=>c.value=e),"onUpdate:showRecentFailures":t[1]||(t[1]=e=>g.value=e),"onUpdate:groupByGroup":t[2]||(t[2]=e=>v.value=e),"onUpdate:sortBy":t[3]||(t[3]=e=>p.value=e),onInitializeCollapsedGroups:j})]),i.value?((0,s.wg)(),(0,s.iD)("div",xa,[(0,s.Wm)(X,{size:"lg"})])):0===w.value.length?((0,s.wg)(),(0,s.iD)("div",_a,[(0,s.Wm)((0,r.SU)($e.Z),{class:"h-12 w-12 text-muted-foreground mx-auto mb-4"}),t[7]||(t[7]=(0,s._)("h3",{class:"text-lg font-semibold mb-2"},"No endpoints found",-1)),(0,s._)("p",ka,(0,n.zw)(d.value||c.value||g.value?"Try adjusting your filters":"No endpoints are configured"),1)])):((0,s.wg)(),(0,s.iD)("div",Sa,[v.value?((0,s.wg)(),(0,s.iD)("div",Ua,[((0,s.wg)(!0),(0,s.iD)(s.HY,null,(0,s.Ko)(x.value,((e,t)=>((0,s.wg)(),(0,s.iD)("div",{key:t,class:"endpoint-group border rounded-lg overflow-hidden"},[(0,s._)("div",{onClick:e=>z(t),class:"endpoint-group-header flex items-center justify-between p-4 bg-card border-b cursor-pointer hover:bg-accent/50 transition-colors"},[(0,s._)("div",Wa,[f.value.has(t)?((0,s.wg)(),(0,s.j4)((0,r.SU)(Re.Z),{key:0,class:"h-5 w-5 text-muted-foreground"})):((0,s.wg)(),(0,s.j4)((0,r.SU)(Ae.Z),{key:1,class:"h-5 w-5 text-muted-foreground"})),(0,s._)("h2",Ha,(0,n.zw)(t),1)]),(0,s._)("div",Ca,[C(e)>0?((0,s.wg)(),(0,s.iD)("span",za,(0,n.zw)(C(e)),1)):((0,s.wg)(),(0,s.j4)((0,r.SU)(Ee.Z),{key:1,class:"h-6 w-6 text-green-600"}))])],8,Da),f.value.has(t)?((0,s.wg)(),(0,s.iD)("div",ja,[(0,s._)("div",Ta,[((0,s.wg)(!0),(0,s.iD)(s.HY,null,(0,s.Ko)(e,(e=>((0,s.wg)(),(0,s.j4)(it,{key:e.key,endpoint:e,maxResults:50,showAverageResponseTime:m.value,onShowTooltip:H},null,8,["endpoint","showAverageResponseTime"])))),128))])])):(0,s.kq)("",!0)])))),128))])):((0,s.wg)(),(0,s.iD)("div",Fa,[((0,s.wg)(!0),(0,s.iD)(s.HY,null,(0,s.Ko)(x.value,(e=>((0,s.wg)(),(0,s.j4)(it,{key:e.key,endpoint:e,maxResults:50,showAverageResponseTime:m.value,onShowTooltip:H},null,8,["endpoint","showAverageResponseTime"])))),128))])),!v.value&&h.value>1?((0,s.wg)(),(0,s.iD)("div",$a,[(0,s.Wm)((0,r.SU)(y),{variant:"outline",size:"icon",disabled:1===u.value,onClick:t[4]||(t[4]=e=>D(u.value-1))},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(Le.Z),{class:"h-4 w-4"})])),_:1},8,["disabled"]),(0,s._)("div",Ra,[((0,s.wg)(!0),(0,s.iD)(s.HY,null,(0,s.Ko)(_.value,(e=>((0,s.wg)(),(0,s.j4)((0,r.SU)(y),{key:e,variant:e===u.value?"default":"outline",size:"sm",onClick:t=>D(e)},{default:(0,s.w5)((()=>[(0,s.Uk)((0,n.zw)(e),1)])),_:2},1032,["variant","onClick"])))),128))]),(0,s.Wm)((0,r.SU)(y),{variant:"outline",size:"icon",disabled:u.value===h.value,onClick:t[5]||(t[5]=e=>D(u.value+1))},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(Ie.Z),{class:"h-4 w-4"})])),_:1},8,["disabled"])])):(0,s.kq)("",!0)]))]),(0,s.Wm)(Vt,{onRefreshData:k})]))}};const La=Ea;var Ia=La,Ma=a(318),Za=a(779),Na=a(141),qa=a(478);const Ya={class:"flex items-center justify-between"},Pa={class:"text-sm text-muted-foreground"};var Oa={__name:"Pagination",props:{numberOfResultsPerPage:Number,currentPageProp:{type:Number,default:1}},emits:["page"],setup(e,{emit:t}){const a=e,l=t,o=(0,r.iH)(a.currentPageProp),i=(0,s.Fl)((()=>{let e=100;if("undefined"!==typeof window&&window.config&&window.config.maximumNumberOfResults){const t=parseInt(window.config.maximumNumberOfResults);isNaN(t)||(e=t)}return Math.ceil(e/a.numberOfResultsPerPage)})),u=()=>{o.value--,l("page",o.value)},d=()=>{o.value++,l("page",o.value)};return(e,t)=>((0,s.wg)(),(0,s.iD)("div",Ya,[(0,s.Wm)((0,r.SU)(y),{variant:"outline",size:"sm",disabled:o.value>=i.value,onClick:d,class:"flex items-center gap-1"},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(Le.Z),{class:"h-4 w-4"}),t[0]||(t[0]=(0,s.Uk)(" Previous ",-1))])),_:1,__:[0]},8,["disabled"]),(0,s._)("span",Pa," Page "+(0,n.zw)(o.value)+" of "+(0,n.zw)(i.value),1),(0,s.Wm)((0,r.SU)(y),{variant:"outline",size:"sm",disabled:o.value<=1,onClick:u,class:"flex items-center gap-1"},{default:(0,s.w5)((()=>[t[1]||(t[1]=(0,s.Uk)(" Next ",-1)),(0,s.Wm)((0,r.SU)(Ie.Z),{class:"h-4 w-4"})])),_:1,__:[1]},8,["disabled"])]))}};const Va=Oa;var Ba=Va;const Ga={class:"dashboard-container bg-background"},Ka={class:"container mx-auto px-4 py-8 max-w-7xl"},Ja={class:"mb-6"},Qa={key:0,class:"space-y-6"},Xa={class:"flex items-start justify-between"},el={class:"text-4xl font-bold tracking-tight"},tl={class:"flex items-center gap-3 text-muted-foreground mt-2"},al={key:0},ll={key:1},sl={key:2},nl={class:"grid gap-6 md:grid-cols-2 lg:grid-cols-4"},rl={class:"text-2xl font-bold"},ol={class:"text-2xl font-bold"},il={class:"text-2xl font-bold"},ul={class:"text-2xl font-bold"},dl={class:"flex items-center justify-between"},cl={class:"flex items-center gap-2"},gl={class:"space-y-4"},ml={key:1,class:"pt-4 border-t"},vl={key:0,class:"space-y-6"},pl={class:"flex items-center justify-between"},fl=["src"],wl={class:"grid gap-4 md:grid-cols-2 lg:grid-cols-4"},hl=["src","alt"],yl={class:"grid gap-4 md:grid-cols-2 lg:grid-cols-4"},bl={class:"text-sm text-muted-foreground mb-2"},xl=["src","alt"],_l={class:"text-center"},kl=["src"],Sl={class:"space-y-4"},Ul={class:"mt-1"},Dl={class:"flex-1"},Wl={class:"font-medium"},Hl={class:"text-sm text-muted-foreground"},Cl={key:1,class:"flex items-center justify-center py-20"};var zl={__name:"Details",emits:["showTooltip"],setup(e,{emit:t}){const a=(0,i.tv)(),o=(0,i.yj)(),u=t,d=(0,r.iH)(null),c=(0,r.iH)(null),g=(0,r.iH)([]),m=(0,r.iH)(1),v=(0,r.iH)(!1),p=(0,r.iH)(!1),f=(0,r.iH)("24h"),w="."===Al?"..":Al,h=(0,r.iH)(!1),b=(0,s.Fl)((()=>c.value&&c.value.results&&0!==c.value.results.length?c.value.results[c.value.results.length-1]:null)),x=(0,s.Fl)((()=>b.value?b.value.success?"healthy":"unhealthy":"unknown")),k=(0,s.Fl)((()=>b.value?.hostname||null)),S=(0,s.Fl)((()=>{if(!d.value||!d.value.results||0===d.value.results.length)return"N/A";let e=0,t=0;for(const a of d.value.results)a.duration&&(e+=a.duration,t++);return 0===t?"N/A":Math.round(e/t/1e6)})),D=(0,s.Fl)((()=>{if(!d.value||!d.value.results||0===d.value.results.length)return"N/A";let e=1/0,t=0,a=!1;for(const n of d.value.results)if(n.duration){const l=n.duration/1e6;e=Math.min(e,l),t=Math.max(t,l),a=!0}if(!a)return"N/A";const l=Math.round(e),s=Math.round(t);return l===s?`${l}ms`:`${l}-${s}ms`})),W=(0,s.Fl)((()=>c.value&&c.value.results&&0!==c.value.results.length?L.methods.generatePrettyTimeAgo(c.value.results[c.value.results.length-1].timestamp):"Never")),C=async()=>{h.value=!0;try{const e=await fetch(`${w}/api/v1/endpoints/${o.params.key}/statuses?page=${m.value}&pageSize=50`,{credentials:"include"});if(200===e.status){const t=await e.json();d.value=t,1===m.value&&(c.value=t);let a=[];if(t.events&&t.events.length>0)for(let e=t.events.length-1;e>=0;e--){let l=t.events[e];if(e===t.events.length-1)"UNHEALTHY"===l.type?l.fancyText="Endpoint is unhealthy":"HEALTHY"===l.type?l.fancyText="Endpoint is healthy":"START"===l.type&&(l.fancyText="Monitoring started");else{let a=t.events[e+1];"HEALTHY"===l.type?l.fancyText="Endpoint became healthy":"UNHEALTHY"===l.type?l.fancyText=a?"Endpoint was unhealthy for "+L.methods.generatePrettyTimeDifference(a.timestamp,l.timestamp):"Endpoint became unhealthy":"START"===l.type&&(l.fancyText="Monitoring started")}l.fancyTimeAgo=L.methods.generatePrettyTimeAgo(l.timestamp),a.push(l)}if(g.value=a,t.results&&t.results.length>0)for(let e=0;e0){v.value=!0;break}}else console.error("[Details][fetchData] Error:",await e.text())}catch(e){console.error("[Details][fetchData] Error:",e)}finally{h.value=!1}},z=()=>{a.push("/")},T=e=>{m.value=e,C()},F=(e,t)=>{u("showTooltip",e,t)},$=e=>new Date(e).toLocaleString(),R=()=>`${w}/api/v1/endpoints/${d.value.key}/health/badge.svg`,A=e=>`${w}/api/v1/endpoints/${d.value.key}/uptimes/${e}/badge.svg`,E=e=>`${w}/api/v1/endpoints/${d.value.key}/response-times/${e}/badge.svg`,I=e=>`${w}/api/v1/endpoints/${d.value.key}/response-times/${e}/chart.svg`;return(0,s.bv)((()=>{C()})),(e,t)=>((0,s.wg)(),(0,s.iD)("div",Ga,[(0,s._)("div",Ka,[(0,s._)("div",Ja,[(0,s.Wm)((0,r.SU)(y),{variant:"ghost",class:"mb-4",onClick:z},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(Ma.Z),{class:"h-4 w-4 mr-2"}),t[2]||(t[2]=(0,s.Uk)(" Back to Dashboard ",-1))])),_:1,__:[2]}),d.value&&d.value.name?((0,s.wg)(),(0,s.iD)("div",Qa,[(0,s._)("div",Xa,[(0,s._)("div",null,[(0,s._)("h1",el,(0,n.zw)(d.value.name),1),(0,s._)("div",tl,[d.value.group?((0,s.wg)(),(0,s.iD)("span",al,"Group: "+(0,n.zw)(d.value.group),1)):(0,s.kq)("",!0),d.value.group&&k.value?((0,s.wg)(),(0,s.iD)("span",ll,"•")):(0,s.kq)("",!0),k.value?((0,s.wg)(),(0,s.iD)("span",sl,(0,n.zw)(k.value),1)):(0,s.kq)("",!0)])]),(0,s.Wm)(Pe,{status:x.value},null,8,["status"])]),(0,s._)("div",nl,[(0,s.Wm)((0,r.SU)(_),null,{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(U),{class:"pb-2"},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(H),{class:"text-sm font-medium text-muted-foreground"},{default:(0,s.w5)((()=>t[3]||(t[3]=[(0,s.Uk)("Current Status",-1)]))),_:1,__:[3]})])),_:1}),(0,s.Wm)((0,r.SU)(j),null,{default:(0,s.w5)((()=>[(0,s._)("div",rl,(0,n.zw)("healthy"===x.value?"Operational":"Issues Detected"),1)])),_:1})])),_:1}),(0,s.Wm)((0,r.SU)(_),null,{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(U),{class:"pb-2"},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(H),{class:"text-sm font-medium text-muted-foreground"},{default:(0,s.w5)((()=>t[4]||(t[4]=[(0,s.Uk)("Avg Response Time",-1)]))),_:1,__:[4]})])),_:1}),(0,s.Wm)((0,r.SU)(j),null,{default:(0,s.w5)((()=>[(0,s._)("div",ol,(0,n.zw)(S.value)+"ms",1)])),_:1})])),_:1}),(0,s.Wm)((0,r.SU)(_),null,{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(U),{class:"pb-2"},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(H),{class:"text-sm font-medium text-muted-foreground"},{default:(0,s.w5)((()=>t[5]||(t[5]=[(0,s.Uk)("Response Time Range",-1)]))),_:1,__:[5]})])),_:1}),(0,s.Wm)((0,r.SU)(j),null,{default:(0,s.w5)((()=>[(0,s._)("div",il,(0,n.zw)(D.value),1)])),_:1})])),_:1}),(0,s.Wm)((0,r.SU)(_),null,{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(U),{class:"pb-2"},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(H),{class:"text-sm font-medium text-muted-foreground"},{default:(0,s.w5)((()=>t[6]||(t[6]=[(0,s.Uk)("Last Check",-1)]))),_:1,__:[6]})])),_:1}),(0,s.Wm)((0,r.SU)(j),null,{default:(0,s.w5)((()=>[(0,s._)("div",ul,(0,n.zw)(W.value),1)])),_:1})])),_:1})]),(0,s.Wm)((0,r.SU)(_),null,{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(U),null,{default:(0,s.w5)((()=>[(0,s._)("div",dl,[(0,s.Wm)((0,r.SU)(H),null,{default:(0,s.w5)((()=>t[7]||(t[7]=[(0,s.Uk)("Recent Checks",-1)]))),_:1,__:[7]}),(0,s._)("div",cl,[(0,s.Wm)((0,r.SU)(y),{variant:"ghost",size:"icon",onClick:t[0]||(t[0]=e=>p.value=!p.value),title:p.value?"Show min-max response time":"Show average response time"},{default:(0,s.w5)((()=>[p.value?((0,s.wg)(),(0,s.j4)((0,r.SU)(je.Z),{key:0,class:"h-5 w-5"})):((0,s.wg)(),(0,s.j4)((0,r.SU)(Te.Z),{key:1,class:"h-5 w-5"}))])),_:1},8,["title"]),(0,s.Wm)((0,r.SU)(y),{variant:"ghost",size:"icon",onClick:C,title:"Refresh data",disabled:h.value},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(Fe.Z),{class:(0,n.C_)(["h-4 w-4",h.value&&"animate-spin"])},null,8,["class"])])),_:1},8,["disabled"])])])])),_:1}),(0,s.Wm)((0,r.SU)(j),null,{default:(0,s.w5)((()=>[(0,s._)("div",gl,[d.value?((0,s.wg)(),(0,s.j4)(it,{key:0,endpoint:d.value,maxResults:50,showAverageResponseTime:p.value,onShowTooltip:F,class:"border-0 shadow-none bg-transparent p-0"},null,8,["endpoint","showAverageResponseTime"])):(0,s.kq)("",!0),d.value&&d.value.key?((0,s.wg)(),(0,s.iD)("div",ml,[(0,s.Wm)(Ba,{onPage:T,numberOfResultsPerPage:50,currentPageProp:m.value},null,8,["currentPageProp"])])):(0,s.kq)("",!0)])])),_:1})])),_:1}),v.value?((0,s.wg)(),(0,s.iD)("div",vl,[(0,s.Wm)((0,r.SU)(_),null,{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(U),null,{default:(0,s.w5)((()=>[(0,s._)("div",pl,[(0,s.Wm)((0,r.SU)(H),null,{default:(0,s.w5)((()=>t[8]||(t[8]=[(0,s.Uk)("Response Time Trend",-1)]))),_:1,__:[8]}),(0,s.wy)((0,s._)("select",{"onUpdate:modelValue":t[1]||(t[1]=e=>f.value=e),class:"text-sm bg-background border rounded-md px-3 py-1 focus:outline-none focus:ring-2 focus:ring-ring"},t[9]||(t[9]=[(0,s._)("option",{value:"24h"},"24 hours",-1),(0,s._)("option",{value:"7d"},"7 days",-1),(0,s._)("option",{value:"30d"},"30 days",-1)]),512),[[l.bM,f.value]])])])),_:1}),(0,s.Wm)((0,r.SU)(j),null,{default:(0,s.w5)((()=>[(0,s._)("img",{src:I(f.value),alt:"Response time chart",class:"w-full"},null,8,fl)])),_:1})])),_:1}),(0,s._)("div",wl,[((0,s.wg)(),(0,s.iD)(s.HY,null,(0,s.Ko)(["30d","7d","24h","1h"],(e=>(0,s.Wm)((0,r.SU)(_),{key:e},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(U),{class:"pb-2"},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(H),{class:"text-sm font-medium text-muted-foreground text-center"},{default:(0,s.w5)((()=>[(0,s.Uk)((0,n.zw)("30d"===e?"Last 30 days":"7d"===e?"Last 7 days":"24h"===e?"Last 24 hours":"Last hour"),1)])),_:2},1024)])),_:2},1024),(0,s.Wm)((0,r.SU)(j),null,{default:(0,s.w5)((()=>[(0,s._)("img",{src:E(e),alt:`${e} response time`,class:"mx-auto mt-2"},null,8,hl)])),_:2},1024)])),_:2},1024))),64))])])):(0,s.kq)("",!0),(0,s.Wm)((0,r.SU)(_),null,{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(U),null,{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(H),null,{default:(0,s.w5)((()=>t[10]||(t[10]=[(0,s.Uk)("Uptime Statistics",-1)]))),_:1,__:[10]})])),_:1}),(0,s.Wm)((0,r.SU)(j),null,{default:(0,s.w5)((()=>[(0,s._)("div",yl,[((0,s.wg)(),(0,s.iD)(s.HY,null,(0,s.Ko)(["30d","7d","24h","1h"],(e=>(0,s._)("div",{key:e,class:"text-center"},[(0,s._)("p",bl,(0,n.zw)("30d"===e?"Last 30 days":"7d"===e?"Last 7 days":"24h"===e?"Last 24 hours":"Last hour"),1),(0,s._)("img",{src:A(e),alt:`${e} uptime`,class:"mx-auto"},null,8,xl)]))),64))])])),_:1})])),_:1}),(0,s.Wm)((0,r.SU)(_),null,{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(U),null,{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(H),null,{default:(0,s.w5)((()=>t[11]||(t[11]=[(0,s.Uk)("Current Health",-1)]))),_:1,__:[11]})])),_:1}),(0,s.Wm)((0,r.SU)(j),null,{default:(0,s.w5)((()=>[(0,s._)("div",_l,[(0,s._)("img",{src:R(),alt:"health badge",class:"mx-auto"},null,8,kl)])])),_:1})])),_:1}),g.value&&g.value.length>0?((0,s.wg)(),(0,s.j4)((0,r.SU)(_),{key:1},{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(U),null,{default:(0,s.w5)((()=>[(0,s.Wm)((0,r.SU)(H),null,{default:(0,s.w5)((()=>t[12]||(t[12]=[(0,s.Uk)("Events",-1)]))),_:1,__:[12]})])),_:1}),(0,s.Wm)((0,r.SU)(j),null,{default:(0,s.w5)((()=>[(0,s._)("div",Sl,[((0,s.wg)(!0),(0,s.iD)(s.HY,null,(0,s.Ko)(g.value,(e=>((0,s.wg)(),(0,s.iD)("div",{key:e.timestamp,class:"flex items-start gap-4 pb-4 border-b last:border-0"},[(0,s._)("div",Ul,["HEALTHY"===e.type?((0,s.wg)(),(0,s.j4)((0,r.SU)(Za.Z),{key:0,class:"h-5 w-5 text-green-500"})):"UNHEALTHY"===e.type?((0,s.wg)(),(0,s.j4)((0,r.SU)(Na.Z),{key:1,class:"h-5 w-5 text-red-500"})):((0,s.wg)(),(0,s.j4)((0,r.SU)(qa.Z),{key:2,class:"h-5 w-5 text-muted-foreground"}))]),(0,s._)("div",Dl,[(0,s._)("p",Wl,(0,n.zw)(e.fancyText),1),(0,s._)("p",Hl,(0,n.zw)($(e.timestamp))+" • "+(0,n.zw)(e.fancyTimeAgo),1)])])))),128))])])),_:1})])),_:1})):(0,s.kq)("",!0)])):((0,s.wg)(),(0,s.iD)("div",Cl,[(0,s.Wm)(X,{size:"lg"})]))])]),(0,s.Wm)(Vt,{onRefreshData:C})]))}};const jl=zl;var Tl=jl;const Fl=[{path:"/",name:"Home",component:Ia},{path:"/endpoints/:key",name:"Details",component:Tl}],$l=(0,i.p7)({history:(0,i.PO)("/"),routes:Fl});var Rl=$l;const Al="";(0,l.ri)(ze).use(Rl).mount("#app")}},t={};function a(l){var s=t[l];if(void 0!==s)return s.exports;var n=t[l]={exports:{}};return e[l](n,n.exports,a),n.exports}a.m=e,function(){var e=[];a.O=function(t,l,s,n){if(!l){var r=1/0;for(d=0;d=n)&&Object.keys(a.O).every((function(e){return a.O[e](l[i])}))?l.splice(i--,1):(o=!1,n0&&e[d-1][2]>n;d--)e[d]=e[d-1];e[d]=[l,s,n]}}(),function(){a.d=function(e,t){for(var l in t)a.o(t,l)&&!a.o(e,l)&&Object.defineProperty(e,l,{enumerable:!0,get:t[l]})}}(),function(){a.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"===typeof window)return window}}()}(),function(){a.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)}}(),function(){a.p="/"}(),function(){var e={143:0};a.O.j=function(t){return 0===e[t]};var t=function(t,l){var s,n,r=l[0],o=l[1],i=l[2],u=0;if(r.some((function(t){return 0!==e[t]}))){for(s in o)a.o(o,s)&&(a.m[s]=o[s]);if(i)var d=i(a)}for(t&&t(l);u((0,a.wg)(),(0,a.iD)("button",{class:(0,n.C_)((0,r.SU)(v)((0,r.SU)(t)({variant:e.variant,size:e.size}),s.$attrs.class??"")),disabled:e.disabled},[(0,a.WI)(s.$slots,"default")],10,f))}};const h=w;var x=h,b={__name:"Card",setup(e){return(e,t)=>((0,a.wg)(),(0,a.iD)("div",{class:(0,n.C_)((0,r.SU)(v)("rounded-lg border bg-card text-card-foreground shadow-sm",e.$attrs.class??""))},[(0,a.WI)(e.$slots,"default")],2))}};const y=b;var _=y,k={__name:"CardHeader",setup(e){return(e,t)=>((0,a.wg)(),(0,a.iD)("div",{class:(0,n.C_)((0,r.SU)(v)("flex flex-col space-y-1.5 p-6",e.$attrs.class??""))},[(0,a.WI)(e.$slots,"default")],2))}};const S=k;var D=S,U={__name:"CardTitle",setup(e){return(e,t)=>((0,a.wg)(),(0,a.iD)("h3",{class:(0,n.C_)((0,r.SU)(v)("text-2xl font-semibold leading-none tracking-tight",e.$attrs.class??""))},[(0,a.WI)(e.$slots,"default")],2))}};const z=U;var W=z,C={__name:"CardContent",setup(e){return(e,t)=>((0,a.wg)(),(0,a.iD)("div",{class:(0,n.C_)((0,r.SU)(v)("p-6 pt-0",e.$attrs.class??""))},[(0,a.WI)(e.$slots,"default")],2))}};const j=C;var H=j;const F={id:"social"};function R(e,t){return(0,a.wg)(),(0,a.iD)("div",F,t[0]||(t[0]=[(0,a._)("a",{href:"https://github.com/TwiN/gatus",target:"_blank",title:"Gatus on GitHub"},[(0,a._)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"32",height:"32",viewBox:"0 0 16 16",class:"hover:scale-110"},[(0,a._)("path",{fill:"gray",d:"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"})])],-1)]))}var $=s(744);const q={},T=(0,$.Z)(q,[["render",R],["__scopeId","data-v-788af9ce"]]);var Z=T;const E=e=>{let t=(new Date).getTime()-new Date(e).getTime();if(t<500)return"now";if(t>2592e5){let e=(t/864e5).toFixed(0);return e+" day"+("1"!==e?"s":"")+" ago"}if(t>36e5){let e=(t/36e5).toFixed(0);return e+" hour"+("1"!==e?"s":"")+" ago"}if(t>6e4){let e=(t/6e4).toFixed(0);return e+" minute"+("1"!==e?"s":"")+" ago"}let s=(t/1e3).toFixed(0);return s+" second"+("1"!==s?"s":"")+" ago"},A=(e,t)=>{let s=Math.ceil((new Date(e)-new Date(t))/1e3/60);return s+(1===s?" minute":" minutes")},L=e=>{let t=new Date(e),s=t.getFullYear(),l=(t.getMonth()+1<10?"0":"")+(t.getMonth()+1),a=(t.getDate()<10?"0":"")+t.getDate(),n=(t.getHours()<10?"0":"")+t.getHours(),r=(t.getMinutes()<10?"0":"")+t.getMinutes(),o=(t.getSeconds()<10?"0":"")+t.getSeconds();return s+"-"+l+"-"+a+" "+n+":"+r+":"+o},N={key:0,class:"space-y-2"},M={key:0,class:"flex items-center gap-2"},I={class:"text-xs font-semibold"},Y={class:"font-mono text-xs"},O={key:1},K={class:"font-mono text-xs"},P={key:0,class:"mt-1 space-y-0.5"},V={class:"truncate"},G={class:"text-muted-foreground"},B={key:0,class:"text-xs text-muted-foreground"},J={class:"text-xs font-semibold text-muted-foreground uppercase tracking-wider"},Q={class:"font-mono text-xs"},X={key:2},ee={class:"font-mono text-xs space-y-0.5"},te={class:"break-all"},se={key:3},le={class:"font-mono text-xs space-y-0.5"};var ae={__name:"Tooltip",props:{event:{type:[Event,Object],default:null},result:{type:Object,default:null}},setup(e){const t=e,s=(0,r.iH)(!0),l=(0,r.iH)(0),o=(0,r.iH)(0),u=(0,r.iH)(null),i=(0,a.Fl)((()=>t.result&&void 0!==t.result.endpointResults)),d=(0,a.Fl)((()=>i.value&&t.result.endpointResults?t.result.endpointResults.length:0)),c=(0,a.Fl)((()=>i.value&&t.result.endpointResults?t.result.endpointResults.filter((e=>e.success)).length:0)),g=async()=>{if(t.event&&t.event.type)if(await(0,a.Y3)(),"mouseenter"===t.event.type&&u.value){const e=t.event.target,n=e.getBoundingClientRect();s.value=!1,await(0,a.Y3)();const r=u.value.getBoundingClientRect();let i=n.bottom+8,d=n.left;const c=window.innerHeight-n.bottom,g=n.top;cr.height+20?n.top-r.height-8:g>c?10:window.innerHeight-r.height-10);const m=window.innerWidth-n.left;mt.event),(e=>{e&&e.type&&("mouseenter"===e.type?(s.value=!1,(0,a.Y3)((()=>g()))):"mouseleave"===e.type&&(s.value=!0))}),{immediate:!0}),(0,a.YP)((()=>t.result),(()=>{s.value||(0,a.Y3)((()=>g()))})),(t,g)=>((0,a.wg)(),(0,a.iD)("div",{id:"tooltip",ref_key:"tooltip",ref:u,class:(0,n.C_)(["fixed z-50 px-3 py-2 text-sm rounded-md shadow-lg border transition-all duration-200","bg-popover text-popover-foreground border-border",s.value?"invisible opacity-0":"visible opacity-100"]),style:(0,n.j5)(`top: ${l.value}px; left: ${o.value}px;`)},[e.result?((0,a.wg)(),(0,a.iD)("div",N,[i.value?((0,a.wg)(),(0,a.iD)("div",M,[(0,a._)("span",{class:(0,n.C_)(["inline-block w-2 h-2 rounded-full",e.result.success?"bg-green-500":"bg-red-500"])},null,2),(0,a._)("span",I,(0,n.zw)(e.result.success?"Suite Passed":"Suite Failed"),1)])):(0,a.kq)("",!0),(0,a._)("div",null,[g[0]||(g[0]=(0,a._)("div",{class:"text-xs font-semibold text-muted-foreground uppercase tracking-wider"},"Timestamp",-1)),(0,a._)("div",Y,(0,n.zw)((0,r.SU)(L)(e.result.timestamp)),1)]),i.value&&e.result.endpointResults?((0,a.wg)(),(0,a.iD)("div",O,[g[1]||(g[1]=(0,a._)("div",{class:"text-xs font-semibold text-muted-foreground uppercase tracking-wider"},"Endpoints",-1)),(0,a._)("div",K,[(0,a._)("span",{class:(0,n.C_)(c.value===d.value?"text-green-500":"text-yellow-500")},(0,n.zw)(c.value)+"/"+(0,n.zw)(d.value)+" passed ",3)]),e.result.endpointResults.length>0?((0,a.wg)(),(0,a.iD)("div",P,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(e.result.endpointResults.slice(0,5),((e,t)=>((0,a.wg)(),(0,a.iD)("div",{key:t,class:"flex items-center gap-1 text-xs"},[(0,a._)("span",{class:(0,n.C_)(e.success?"text-green-500":"text-red-500")},(0,n.zw)(e.success?"✓":"✗"),3),(0,a._)("span",V,(0,n.zw)(e.name),1),(0,a._)("span",G,"("+(0,n.zw)((e.duration/1e6).toFixed(0))+"ms)",1)])))),128)),e.result.endpointResults.length>5?((0,a.wg)(),(0,a.iD)("div",B," ... and "+(0,n.zw)(e.result.endpointResults.length-5)+" more ",1)):(0,a.kq)("",!0)])):(0,a.kq)("",!0)])):(0,a.kq)("",!0),(0,a._)("div",null,[(0,a._)("div",J,(0,n.zw)(i.value?"Total Duration":"Response Time"),1),(0,a._)("div",Q,(0,n.zw)((i.value,(e.result.duration/1e6).toFixed(0)))+"ms ",1)]),!i.value&&e.result.conditionResults&&e.result.conditionResults.length?((0,a.wg)(),(0,a.iD)("div",X,[g[2]||(g[2]=(0,a._)("div",{class:"text-xs font-semibold text-muted-foreground uppercase tracking-wider"},"Conditions",-1)),(0,a._)("div",ee,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(e.result.conditionResults,((e,t)=>((0,a.wg)(),(0,a.iD)("div",{key:t,class:"flex items-start gap-1"},[(0,a._)("span",{class:(0,n.C_)(e.success?"text-green-500":"text-red-500")},(0,n.zw)(e.success?"✓":"✗"),3),(0,a._)("span",te,(0,n.zw)(e.condition),1)])))),128))])])):(0,a.kq)("",!0),e.result.errors&&e.result.errors.length?((0,a.wg)(),(0,a.iD)("div",se,[g[3]||(g[3]=(0,a._)("div",{class:"text-xs font-semibold text-muted-foreground uppercase tracking-wider"},"Errors",-1)),(0,a._)("div",le,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(e.result.errors,((e,t)=>((0,a.wg)(),(0,a.iD)("div",{key:t,class:"text-red-500"}," • "+(0,n.zw)(e),1)))),128))])])):(0,a.kq)("",!0)])):(0,a.kq)("",!0)],6))}};const ne=ae;var re=ne;const oe={class:"flex justify-center items-center"};var ue={__name:"Loading",props:{size:{type:String,default:"md",validator:e=>["xs","sm","md","lg","xl"].includes(e)}},setup(e){const t=e,s=(0,a.Fl)((()=>{const e={xs:"w-4 h-4",sm:"w-6 h-6",md:"w-8 h-8",lg:"w-12 h-12",xl:"w-16 h-16"};return e[t.size]||e.md}));return(e,t)=>((0,a.wg)(),(0,a.iD)("div",oe,[(0,a._)("img",{class:(0,n.C_)(["animate-spin rounded-full opacity-60 grayscale",s.value]),src:o,alt:"Gatus logo"},null,2)]))}};const ie=ue;var de=ie;const ce={id:"global",class:"bg-background text-foreground"},ge={key:0,class:"flex items-center justify-center min-h-screen"},me={key:1,class:"relative"},pe={class:"border-b bg-card/50 backdrop-blur supports-[backdrop-filter]:bg-card/60"},ve={class:"container mx-auto px-4 py-4 max-w-7xl"},fe={class:"flex items-center justify-between"},we={class:"flex items-center gap-4"},he={class:"w-12 h-12 flex items-center justify-center"},xe=["src"],be={key:1,src:o,alt:"Gatus",class:"w-full h-full object-contain"},ye={class:"text-2xl font-bold tracking-tight"},_e={key:0,class:"text-sm text-muted-foreground"},ke={class:"flex items-center gap-2"},Se={key:0,class:"hidden md:flex items-center gap-1"},De=["href"],Ue={key:0,class:"md:hidden mt-4 pt-4 border-t space-y-1"},ze=["href"],We={class:"relative"},Ce={class:"border-t mt-auto"},je={class:"container mx-auto px-4 py-6 max-w-7xl"},He={class:"flex flex-col items-center gap-4"},Fe={key:2,id:"login-container",class:"flex items-center justify-center min-h-screen p-4"},Re={key:0,class:"mb-6"},$e={class:"p-3 rounded-md bg-destructive/10 border border-destructive/20"},qe={class:"text-sm text-destructive text-center"},Te={key:0},Ze={key:1},Ee=["href"];var Ae={__name:"App",setup(e){const t=(0,u.yj)(),s=(0,r.iH)(!1),l=(0,r.iH)({oidc:!1,authenticated:!0}),g=(0,r.iH)([]),m=(0,r.iH)({}),p=(0,r.iH)(!1),v=(0,r.iH)(!1);let f=null;const w=(0,a.Fl)((()=>window.config&&window.config.logo&&"{{ .UI.Logo }}"!==window.config.logo?window.config.logo:"")),h=(0,a.Fl)((()=>window.config&&window.config.header&&"{{ .UI.Header }}"!==window.config.header?window.config.header:"Gatus")),b=(0,a.Fl)((()=>window.config&&window.config.link&&"{{ .UI.Link }}"!==window.config.link?window.config.link:null)),y=(0,a.Fl)((()=>window.config&&window.config.buttons?window.config.buttons:[])),k=async()=>{try{const e=await fetch(`${Qn}/api/v1/config`,{credentials:"include"});if(s.value=!0,200===e.status){const t=await e.json();l.value=t,g.value=t.announcements||[]}}catch(e){console.error("Failed to fetch config:",e),s.value=!0}},S=(e,t)=>{m.value={result:e,event:t}};return(0,a.bv)((()=>{k(),f=setInterval(k,6e5)})),(0,a.Ah)((()=>{f&&(clearInterval(f),f=null)})),(e,u)=>{const f=(0,a.up)("router-view");return(0,a.wg)(),(0,a.iD)("div",ce,[s.value?l.value&&l.value.oidc&&!l.value.authenticated?((0,a.wg)(),(0,a.iD)("div",Fe,[(0,a.Wm)((0,r.SU)(_),{class:"w-full max-w-md"},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),{class:"text-center"},{default:(0,a.w5)((()=>[u[5]||(u[5]=(0,a._)("img",{src:o,alt:"Gatus",class:"w-20 h-20 mx-auto mb-4"},null,-1)),(0,a.Wm)((0,r.SU)(W),{class:"text-3xl"},{default:(0,a.w5)((()=>u[4]||(u[4]=[(0,a.Uk)("Gatus",-1)]))),_:1,__:[4]}),u[6]||(u[6]=(0,a._)("p",{class:"text-muted-foreground mt-2"},"System Monitoring Dashboard",-1))])),_:1,__:[5,6]}),(0,a.Wm)((0,r.SU)(H),null,{default:(0,a.w5)((()=>[(0,r.SU)(t)&&(0,r.SU)(t).query.error?((0,a.wg)(),(0,a.iD)("div",Re,[(0,a._)("div",$e,[(0,a._)("p",qe,["access_denied"===(0,r.SU)(t).query.error?((0,a.wg)(),(0,a.iD)("span",Te," You do not have access to this status page ")):((0,a.wg)(),(0,a.iD)("span",Ze,(0,n.zw)((0,r.SU)(t).query.error),1))])])])):(0,a.kq)("",!0),(0,a._)("a",{href:`${(0,r.SU)(Qn)}/oidc/login`,class:"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-11 px-8 w-full",onClick:u[2]||(u[2]=e=>v.value=!0)},[v.value?((0,a.wg)(),(0,a.j4)(de,{key:0,size:"xs"})):((0,a.wg)(),(0,a.iD)(a.HY,{key:1},[(0,a.Wm)((0,r.SU)(c.Z),{class:"mr-2 h-4 w-4"}),u[7]||(u[7]=(0,a.Uk)(" Login with OIDC ",-1))],64))],8,Ee)])),_:1})])),_:1})])):((0,a.wg)(),(0,a.iD)("div",me,[(0,a._)("header",pe,[(0,a._)("div",ve,[(0,a._)("div",fe,[(0,a._)("div",we,[((0,a.wg)(),(0,a.j4)((0,a.LL)(b.value?"a":"div"),{href:b.value,target:"_blank",class:"flex items-center gap-3 hover:opacity-80 transition-opacity"},{default:(0,a.w5)((()=>[(0,a._)("div",he,[w.value?((0,a.wg)(),(0,a.iD)("img",{key:0,src:w.value,alt:"Gatus",class:"w-full h-full object-contain"},null,8,xe)):((0,a.wg)(),(0,a.iD)("img",be))]),(0,a._)("div",null,[(0,a._)("h1",ye,(0,n.zw)(h.value),1),y.value&&y.value.length?((0,a.wg)(),(0,a.iD)("p",_e," System Monitoring Dashboard ")):(0,a.kq)("",!0)])])),_:1},8,["href"]))]),(0,a._)("div",ke,[y.value&&y.value.length?((0,a.wg)(),(0,a.iD)("nav",Se,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(y.value,(e=>((0,a.wg)(),(0,a.iD)("a",{key:e.name,href:e.link,target:"_blank",class:"px-3 py-2 text-sm font-medium rounded-md hover:bg-accent hover:text-accent-foreground transition-colors"},(0,n.zw)(e.name),9,De)))),128))])):(0,a.kq)("",!0),y.value&&y.value.length?((0,a.wg)(),(0,a.j4)((0,r.SU)(x),{key:1,variant:"ghost",size:"icon",class:"md:hidden",onClick:u[0]||(u[0]=e=>p.value=!p.value)},{default:(0,a.w5)((()=>[p.value?((0,a.wg)(),(0,a.j4)((0,r.SU)(d.Z),{key:1,class:"h-5 w-5"})):((0,a.wg)(),(0,a.j4)((0,r.SU)(i.Z),{key:0,class:"h-5 w-5"}))])),_:1})):(0,a.kq)("",!0)])]),y.value&&y.value.length&&p.value?((0,a.wg)(),(0,a.iD)("nav",Ue,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(y.value,(e=>((0,a.wg)(),(0,a.iD)("a",{key:e.name,href:e.link,target:"_blank",class:"block px-3 py-2 text-sm font-medium rounded-md hover:bg-accent hover:text-accent-foreground transition-colors",onClick:u[1]||(u[1]=e=>p.value=!1)},(0,n.zw)(e.name),9,ze)))),128))])):(0,a.kq)("",!0)])]),(0,a._)("main",We,[(0,a.Wm)(f,{onShowTooltip:S,announcements:g.value},null,8,["announcements"])]),(0,a._)("footer",Ce,[(0,a._)("div",je,[(0,a._)("div",He,[u[3]||(u[3]=(0,a._)("div",{class:"text-sm text-muted-foreground text-center"},[(0,a.Uk)(" Powered by "),(0,a._)("a",{href:"https://gatus.io",target:"_blank",class:"font-medium text-emerald-800 hover:text-emerald-600"},"Gatus")],-1)),(0,a.Wm)(Z)])])])])):((0,a.wg)(),(0,a.iD)("div",ge,[(0,a.Wm)(de,{size:"lg"})])),(0,a.Wm)(re,{result:m.value.result,event:m.value.event},null,8,["result","event"])])}}};const Le=Ae;var Ne=Le,Me=s(793),Ie=s(138),Ye=s(254),Oe=s(146),Ke=s(485),Pe=s(893),Ve=s(89),Ge=s(372),Be=s(981),Je={__name:"Badge",props:{variant:{type:String,default:"default"}},setup(e){const t=(0,g.j)("inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",{variants:{variant:{default:"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",secondary:"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",destructive:"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",outline:"text-foreground",success:"border-transparent bg-green-500 text-white",warning:"border-transparent bg-yellow-500 text-white"}},defaultVariants:{variant:"default"}});return(s,l)=>((0,a.wg)(),(0,a.iD)("div",{class:(0,n.C_)((0,r.SU)(v)((0,r.SU)(t)({variant:e.variant}),s.$attrs.class??""))},[(0,a.WI)(s.$slots,"default")],2))}};const Qe=Je;var Xe=Qe,et={__name:"StatusBadge",props:{status:{type:String,required:!0,validator:e=>["healthy","unhealthy","degraded","unknown"].includes(e)}},setup(e){const t=e,s=(0,a.Fl)((()=>{switch(t.status){case"healthy":return"success";case"unhealthy":return"destructive";case"degraded":return"warning";default:return"secondary"}})),l=(0,a.Fl)((()=>{switch(t.status){case"healthy":return"Healthy";case"unhealthy":return"Unhealthy";case"degraded":return"Degraded";default:return"Unknown"}})),o=(0,a.Fl)((()=>{switch(t.status){case"healthy":return"bg-green-400";case"unhealthy":return"bg-red-400";case"degraded":return"bg-yellow-400";default:return"bg-gray-400"}}));return(e,t)=>((0,a.wg)(),(0,a.j4)((0,r.SU)(Xe),{variant:s.value,class:"flex items-center gap-1"},{default:(0,a.w5)((()=>[(0,a._)("span",{class:(0,n.C_)(["w-2 h-2 rounded-full",o.value])},null,2),(0,a.Uk)(" "+(0,n.zw)(l.value),1)])),_:1},8,["variant"]))}};const tt=et;var st=tt;const lt={class:"flex items-start justify-between gap-2 sm:gap-3"},at={class:"flex-1 min-w-0 overflow-hidden"},nt=["title","aria-label"],rt={class:"flex items-center gap-2 text-xs sm:text-sm text-muted-foreground"},ot=["title"],ut={key:1},it=["title"],dt={class:"flex-shrink-0 ml-2"},ct={class:"space-y-2"},gt={class:"flex items-center justify-between mb-1"},mt=["title"],pt={class:"flex gap-0.5"},vt=["onMouseenter","onMouseleave"],ft={class:"flex items-center justify-between text-xs text-muted-foreground mt-1"};var wt={__name:"EndpointCard",props:{endpoint:{type:Object,required:!0},maxResults:{type:Number,default:50},showAverageResponseTime:{type:Boolean,default:!0}},emits:["showTooltip"],setup(e,{emit:t}){const s=(0,u.tv)(),o=e,i=t,d=(0,a.Fl)((()=>o.endpoint.results&&0!==o.endpoint.results.length?o.endpoint.results[o.endpoint.results.length-1]:null)),c=(0,a.Fl)((()=>d.value?d.value.success?"healthy":"unhealthy":"unknown")),g=(0,a.Fl)((()=>d.value?.hostname||null)),m=(0,a.Fl)((()=>{const e=[...o.endpoint.results||[]];while(e.length{if(!o.endpoint.results||0===o.endpoint.results.length)return"N/A";let e=0,t=0,s=1/0,l=0;for(const a of o.endpoint.results)if(a.duration){const n=a.duration/1e6;e+=n,t++,s=Math.min(s,n),l=Math.max(l,n)}if(0===t)return"N/A";if(o.showAverageResponseTime){const s=Math.round(e/t);return`~${s}ms`}{const e=Math.round(s),t=Math.round(l);return e===t?`${e}ms`:`${e}-${t}ms`}})),v=(0,a.Fl)((()=>o.endpoint.results&&0!==o.endpoint.results.length?E(o.endpoint.results[0].timestamp):"")),f=(0,a.Fl)((()=>o.endpoint.results&&0!==o.endpoint.results.length?E(o.endpoint.results[o.endpoint.results.length-1].timestamp):"")),w=()=>{s.push(`/endpoints/${o.endpoint.key}`)};return(t,s)=>((0,a.wg)(),(0,a.j4)((0,r.SU)(_),{class:"endpoint h-full flex flex-col transition hover:shadow-lg hover:scale-[1.01] dark:hover:border-gray-700"},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),{class:"endpoint-header px-3 sm:px-6 pt-3 sm:pt-6 pb-2 space-y-0"},{default:(0,a.w5)((()=>[(0,a._)("div",lt,[(0,a._)("div",at,[(0,a.Wm)((0,r.SU)(W),{class:"text-base sm:text-lg truncate"},{default:(0,a.w5)((()=>[(0,a._)("span",{class:"hover:text-primary cursor-pointer hover:underline text-sm sm:text-base block truncate",onClick:w,onKeydown:(0,l.D2)(w,["enter"]),title:e.endpoint.name,role:"link",tabindex:"0","aria-label":`View details for ${e.endpoint.name}`},(0,n.zw)(e.endpoint.name),41,nt)])),_:1}),(0,a._)("div",rt,[e.endpoint.group?((0,a.wg)(),(0,a.iD)("span",{key:0,class:"truncate",title:e.endpoint.group},(0,n.zw)(e.endpoint.group),9,ot)):(0,a.kq)("",!0),e.endpoint.group&&g.value?((0,a.wg)(),(0,a.iD)("span",ut,"•")):(0,a.kq)("",!0),g.value?((0,a.wg)(),(0,a.iD)("span",{key:2,class:"truncate",title:g.value},(0,n.zw)(g.value),9,it)):(0,a.kq)("",!0)])]),(0,a._)("div",dt,[(0,a.Wm)(st,{status:c.value},null,8,["status"])])])])),_:1}),(0,a.Wm)((0,r.SU)(H),{class:"endpoint-content flex-1 pb-3 sm:pb-4 px-3 sm:px-6 pt-2"},{default:(0,a.w5)((()=>[(0,a._)("div",ct,[(0,a._)("div",null,[(0,a._)("div",gt,[s[0]||(s[0]=(0,a._)("div",{class:"flex-1"},null,-1)),(0,a._)("p",{class:"text-xs text-muted-foreground",title:e.showAverageResponseTime?"Average response time":"Minimum and maximum response time"},(0,n.zw)(p.value),9,mt)]),(0,a._)("div",pt,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(m.value,((e,t)=>((0,a.wg)(),(0,a.iD)("div",{key:t,class:(0,n.C_)(["flex-1 h-6 sm:h-8 rounded-sm transition-all",e?e.success?"bg-green-500 hover:bg-green-700":"bg-red-500 hover:bg-red-700":"bg-gray-200 dark:bg-gray-700"]),onMouseenter:t=>e&&i("showTooltip",e,t),onMouseleave:t=>e&&i("showTooltip",null,t)},null,42,vt)))),128))]),(0,a._)("div",ft,[(0,a._)("span",null,(0,n.zw)(v.value),1),(0,a._)("span",null,(0,n.zw)(f.value),1)])])])])),_:1})])),_:1}))}};const ht=wt;var xt=ht;const bt={class:"flex items-start justify-between gap-2 sm:gap-3"},yt={class:"flex-1 min-w-0 overflow-hidden"},_t=["title","aria-label"],kt={class:"flex items-center gap-2 text-xs sm:text-sm text-muted-foreground"},St=["title"],Dt={key:1},Ut={key:2},zt={class:"flex-shrink-0 ml-2"},Wt={class:"space-y-2"},Ct={class:"flex items-center justify-between mb-1"},jt={class:"text-xs text-muted-foreground"},Ht={key:0,class:"text-xs text-muted-foreground"},Ft={class:"flex gap-0.5"},Rt=["onMouseenter"],$t={class:"flex items-center justify-between text-xs text-muted-foreground mt-1"};var qt={__name:"SuiteCard",props:{suite:{type:Object,required:!0},maxResults:{type:Number,default:50}},emits:["showTooltip"],setup(e,{emit:t}){const s=(0,u.tv)(),o=e,i=t,d=(0,a.Fl)((()=>{const e=[...o.suite.results||[]];while(e.lengtho.suite.results&&0!==o.suite.results.length?o.suite.results[o.suite.results.length-1].success?"healthy":"unhealthy":"unknown")),g=(0,a.Fl)((()=>{if(!o.suite.results||0===o.suite.results.length)return 0;const e=o.suite.results[o.suite.results.length-1];return e.endpointResults?e.endpointResults.length:0})),m=(0,a.Fl)((()=>{if(!o.suite.results||0===o.suite.results.length)return 0;const e=o.suite.results.filter((e=>e.success)).length;return Math.round(e/o.suite.results.length*100)})),p=(0,a.Fl)((()=>{if(!o.suite.results||0===o.suite.results.length)return null;const e=o.suite.results.reduce(((e,t)=>e+(t.duration||0)),0);return Math.round(e/o.suite.results.length/1e6)})),v=(0,a.Fl)((()=>{if(!o.suite.results||0===o.suite.results.length)return"N/A";const e=o.suite.results[0];return E(e.timestamp)})),f=(0,a.Fl)((()=>{if(!o.suite.results||0===o.suite.results.length)return"Now";const e=o.suite.results[o.suite.results.length-1];return E(e.timestamp)})),w=()=>{s.push(`/suites/${o.suite.key}`)},h=(e,t)=>{i("showTooltip",e,t)},x=e=>{i("showTooltip",null,e)};return(t,s)=>((0,a.wg)(),(0,a.j4)((0,r.SU)(_),{class:"suite h-full flex flex-col transition hover:shadow-lg hover:scale-[1.01] dark:hover:border-gray-700"},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),{class:"suite-header px-3 sm:px-6 pt-3 sm:pt-6 pb-2 space-y-0"},{default:(0,a.w5)((()=>[(0,a._)("div",bt,[(0,a._)("div",yt,[(0,a.Wm)((0,r.SU)(W),{class:"text-base sm:text-lg truncate"},{default:(0,a.w5)((()=>[(0,a._)("span",{class:"hover:text-primary cursor-pointer hover:underline text-sm sm:text-base block truncate",onClick:w,onKeydown:(0,l.D2)(w,["enter"]),title:e.suite.name,role:"link",tabindex:"0","aria-label":`View details for suite ${e.suite.name}`},(0,n.zw)(e.suite.name),41,_t)])),_:1}),(0,a._)("div",kt,[e.suite.group?((0,a.wg)(),(0,a.iD)("span",{key:0,class:"truncate",title:e.suite.group},(0,n.zw)(e.suite.group),9,St)):(0,a.kq)("",!0),e.suite.group&&g.value?((0,a.wg)(),(0,a.iD)("span",Dt,"•")):(0,a.kq)("",!0),g.value?((0,a.wg)(),(0,a.iD)("span",Ut,(0,n.zw)(g.value)+" endpoint"+(0,n.zw)(1!==g.value?"s":""),1)):(0,a.kq)("",!0)])]),(0,a._)("div",zt,[(0,a.Wm)(st,{status:c.value},null,8,["status"])])])])),_:1}),(0,a.Wm)((0,r.SU)(H),{class:"suite-content flex-1 pb-3 sm:pb-4 px-3 sm:px-6 pt-2"},{default:(0,a.w5)((()=>[(0,a._)("div",Wt,[(0,a._)("div",null,[(0,a._)("div",Ct,[(0,a._)("p",jt,"Success Rate: "+(0,n.zw)(m.value)+"%",1),p.value?((0,a.wg)(),(0,a.iD)("p",Ht,(0,n.zw)(p.value)+"ms avg",1)):(0,a.kq)("",!0)]),(0,a._)("div",Ft,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(d.value,((e,t)=>((0,a.wg)(),(0,a.iD)("div",{key:t,class:(0,n.C_)(["flex-1 h-6 sm:h-8 rounded-sm transition-all",e?e.success?"bg-green-500 hover:bg-green-700":"bg-red-500 hover:bg-red-700":"bg-gray-200 dark:bg-gray-700"]),onMouseenter:t=>e&&h(e,t),onMouseleave:s[0]||(s[0]=e=>x(e))},null,42,Rt)))),128))]),(0,a._)("div",$t,[(0,a._)("span",null,(0,n.zw)(f.value),1),(0,a._)("span",null,(0,n.zw)(v.value),1)])])])])),_:1})])),_:1}))}};const Tt=(0,$.Z)(qt,[["__scopeId","data-v-648070e3"]]);var Zt=Tt,Et=s(275);const At=["value"];var Lt={__name:"Input",props:{modelValue:{type:[String,Number],default:""}},emits:["update:modelValue"],setup(e){return(t,s)=>((0,a.wg)(),(0,a.iD)("input",{class:(0,n.C_)((0,r.SU)(v)("flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",t.$attrs.class??"")),value:e.modelValue,onInput:s[0]||(s[0]=e=>t.$emit("update:modelValue",e.target.value))},null,42,At))}};const Nt=Lt;var Mt=Nt,It=s(368);const Yt=["aria-expanded","aria-label"],Ot={class:"truncate"},Kt={key:0,role:"listbox",class:"absolute top-full left-0 z-50 mt-1 w-full rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95"},Pt={class:"p-1"},Vt=["onClick","aria-selected"],Gt={class:"absolute left-1.5 sm:left-2 flex h-3.5 w-3.5 items-center justify-center"};var Bt={__name:"Select",props:{modelValue:{type:String,default:""},options:{type:Array,required:!0},placeholder:{type:String,default:"Select..."},class:{type:String,default:""}},emits:["update:modelValue"],setup(e,{emit:t}){const s=e,l=t,o=(0,r.iH)(!1),u=(0,r.iH)(null),i=(0,r.iH)(-1),d=(0,a.Fl)((()=>s.options.find((e=>e.value===s.modelValue))||{label:s.placeholder,value:""})),c=e=>{l("update:modelValue",e.value),o.value=!1},g=()=>{if(o.value=!o.value,o.value){const e=s.options.findIndex((e=>e.value===s.modelValue));i.value=e>=0?e:0}else i.value=-1},m=e=>{u.value&&!u.value.contains(e.target)&&(o.value=!1,i.value=-1)},p=e=>{if(o.value)switch(e.key){case"ArrowDown":e.preventDefault(),i.value=Math.min(i.value+1,s.options.length-1);break;case"ArrowUp":e.preventDefault(),i.value=Math.max(i.value-1,0);break;case"Enter":case" ":e.preventDefault(),i.value>=0&&i.value{document.addEventListener("click",m)})),(0,a.Ah)((()=>{document.removeEventListener("click",m)})),(t,l)=>((0,a.wg)(),(0,a.iD)("div",{ref_key:"selectRef",ref:u,class:(0,n.C_)(["relative",s.class])},[(0,a._)("button",{onClick:g,onKeydown:p,"aria-expanded":o.value,"aria-haspopup":!0,"aria-label":d.value.label||s.placeholder,class:"flex h-9 sm:h-10 w-full items-center justify-between rounded-md border border-input bg-background px-2 sm:px-3 py-1.5 sm:py-2 text-xs sm:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"},[(0,a._)("span",Ot,(0,n.zw)(d.value.label),1),(0,a.Wm)((0,r.SU)(Ke.Z),{class:"h-3 w-3 sm:h-4 sm:w-4 opacity-50 flex-shrink-0 ml-1"})],40,Yt),o.value?((0,a.wg)(),(0,a.iD)("div",Kt,[(0,a._)("div",Pt,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(e.options,((t,s)=>((0,a.wg)(),(0,a.iD)("div",{key:t.value,onClick:e=>c(t),class:(0,n.C_)(["relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-6 sm:pl-8 pr-2 text-xs sm:text-sm outline-none hover:bg-accent hover:text-accent-foreground",s===i.value&&"bg-accent text-accent-foreground"]),role:"option","aria-selected":e.modelValue===t.value},[(0,a._)("span",Gt,[e.modelValue===t.value?((0,a.wg)(),(0,a.j4)((0,r.SU)(It.Z),{key:0,class:"h-3 w-3 sm:h-4 sm:w-4"})):(0,a.kq)("",!0)]),(0,a.Uk)(" "+(0,n.zw)(t.label),1)],10,Vt)))),128))])])):(0,a.kq)("",!0)],2))}};const Jt=Bt;var Qt=Jt;const Xt={class:"flex flex-col lg:flex-row gap-3 lg:gap-4 p-3 sm:p-4 bg-card rounded-lg border"},es={class:"flex-1"},ts={class:"relative"},ss={class:"flex flex-col sm:flex-row gap-3 sm:gap-4"},ls={class:"flex items-center gap-2 flex-1 sm:flex-initial"},as={class:"flex items-center gap-2 flex-1 sm:flex-initial"};var ns={__name:"SearchBar",emits:["search","update:showOnlyFailing","update:showRecentFailures","update:groupByGroup","update:sortBy","initializeCollapsedGroups"],setup(e,{emit:t}){const s=(0,r.iH)(""),l=(0,r.iH)(localStorage.getItem("gatus:filter-by")||"undefined"!==typeof window&&window.config?.defaultFilterBy||"none"),n=(0,r.iH)(localStorage.getItem("gatus:sort-by")||"undefined"!==typeof window&&window.config?.defaultSortBy||"name"),o=[{label:"None",value:"none"},{label:"Failing",value:"failing"},{label:"Unstable",value:"unstable"}],u=[{label:"Name",value:"name"},{label:"Group",value:"group"},{label:"Health",value:"health"}],i=t,d=e=>{l.value=e,localStorage.setItem("gatus:filter-by",e),i("update:showOnlyFailing",!1),i("update:showRecentFailures",!1),"failing"===e?i("update:showOnlyFailing",!0):"unstable"===e&&i("update:showRecentFailures",!0)},c=e=>{n.value=e,localStorage.setItem("gatus:sort-by",e),i("update:sortBy",e),i("update:groupByGroup","group"===e),"group"===e&&i("initializeCollapsedGroups")};return(0,a.bv)((()=>{d(l.value),c(n.value)})),(e,t)=>((0,a.wg)(),(0,a.iD)("div",Xt,[(0,a._)("div",es,[(0,a._)("div",ts,[(0,a.Wm)((0,r.SU)(Et.Z),{class:"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground"}),t[4]||(t[4]=(0,a._)("label",{for:"search-input",class:"sr-only"},"Search endpoints",-1)),(0,a.Wm)((0,r.SU)(Mt),{id:"search-input",modelValue:s.value,"onUpdate:modelValue":t[0]||(t[0]=e=>s.value=e),type:"text",placeholder:"Search endpoints...",class:"pl-10 text-sm sm:text-base",onInput:t[1]||(t[1]=t=>e.$emit("search",s.value))},null,8,["modelValue"])])]),(0,a._)("div",ss,[(0,a._)("div",ls,[t[5]||(t[5]=(0,a._)("label",{class:"text-xs sm:text-sm font-medium text-muted-foreground whitespace-nowrap"},"Filter by:",-1)),(0,a.Wm)((0,r.SU)(Qt),{modelValue:l.value,"onUpdate:modelValue":[t[2]||(t[2]=e=>l.value=e),d],options:o,placeholder:"None",class:"flex-1 sm:w-[140px] md:w-[160px]"},null,8,["modelValue"])]),(0,a._)("div",as,[t[6]||(t[6]=(0,a._)("label",{class:"text-xs sm:text-sm font-medium text-muted-foreground whitespace-nowrap"},"Sort by:",-1)),(0,a.Wm)((0,r.SU)(Qt),{modelValue:n.value,"onUpdate:modelValue":[t[3]||(t[3]=e=>n.value=e),c],options:u,placeholder:"Name",class:"flex-1 sm:w-[90px] md:w-[100px]"},null,8,["modelValue"])])])]))}};const rs=ns;var os=rs,us=s(789),is=s(679);const ds={id:"settings",class:"fixed bottom-4 left-4 z-50"},cs={class:"flex items-center gap-1 bg-background/95 backdrop-blur-sm border rounded-full shadow-md p-1"},gs=["aria-label","aria-expanded"],ms={class:"text-xs font-medium"},ps=["onClick"],vs=["aria-label"],fs={class:"absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded-md shadow-md opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap"},ws="300",hs="theme",xs=31536e3;var bs={__name:"Settings",emits:["refreshData"],setup(e,{emit:t}){const s=t,o=[{value:"10",label:"10s"},{value:"30",label:"30s"},{value:"60",label:"1m"},{value:"120",label:"2m"},{value:"300",label:"5m"},{value:"600",label:"10m"}],u={REFRESH_INTERVAL:"gatus:refresh-interval"};function i(){const e=document.cookie.match(new RegExp(`${hs}=(dark|light);?`))?.[1];return"dark"===e||!e&&(window.matchMedia("(prefers-color-scheme: dark)").matches||document.documentElement.classList.contains("dark"))}function d(){const e=localStorage.getItem(u.REFRESH_INTERVAL),t=e&&parseInt(e),s=t&&t>=10&&o.some((t=>t.value===e));return s?e:ws}const c=(0,r.iH)(d()),g=(0,r.iH)(i()),m=(0,r.iH)(!1);let p=null;const v=e=>{const t=o.find((t=>t.value===e));return t?t.label:`${e}s`},f=e=>{localStorage.setItem(u.REFRESH_INTERVAL,e),p&&clearInterval(p),p=setInterval((()=>{w()}),1e3*e)},w=()=>{s("refreshData")},h=e=>{c.value=e,m.value=!1,w(),f(e)},x=e=>{const t=document.getElementById("settings");t&&!t.contains(e.target)&&(m.value=!1)},b=e=>{document.cookie=`${hs}=${e}; path=/; max-age=${xs}; samesite=strict`},y=()=>{const e=i()?"light":"dark";b(e),_()},_=()=>{const e=i();g.value=e,document.documentElement.classList.toggle("dark",e)};return(0,a.bv)((()=>{f(c.value),_(),document.addEventListener("click",x)})),(0,a.Ah)((()=>{p&&clearInterval(p),document.removeEventListener("click",x)})),(e,t)=>((0,a.wg)(),(0,a.iD)("div",ds,[(0,a._)("div",cs,[(0,a._)("button",{onClick:t[1]||(t[1]=e=>m.value=!m.value),"aria-label":`Refresh interval: ${v(c.value)}`,"aria-expanded":m.value,class:"flex items-center gap-1.5 px-3 py-1.5 rounded-full hover:bg-accent transition-colors relative"},[(0,a.Wm)((0,r.SU)(Ye.Z),{class:"w-3.5 h-3.5 text-muted-foreground"}),(0,a._)("span",ms,(0,n.zw)(v(c.value)),1),m.value?((0,a.wg)(),(0,a.iD)("div",{key:0,onClick:t[0]||(t[0]=(0,l.iM)((()=>{}),["stop"])),class:"absolute bottom-full left-0 mb-2 bg-popover border rounded-lg shadow-lg overflow-hidden"},[((0,a.wg)(),(0,a.iD)(a.HY,null,(0,a.Ko)(o,(e=>(0,a._)("button",{key:e.value,onClick:t=>h(e.value),class:(0,n.C_)(["block w-full px-4 py-2 text-xs text-left hover:bg-accent transition-colors",c.value===e.value&&"bg-accent"])},(0,n.zw)(e.label),11,ps))),64))])):(0,a.kq)("",!0)],8,gs),t[2]||(t[2]=(0,a._)("div",{class:"h-5 w-px bg-border/50"},null,-1)),(0,a._)("button",{onClick:y,"aria-label":g.value?"Switch to light mode":"Switch to dark mode",class:"p-1.5 rounded-full hover:bg-accent transition-colors group relative"},[g.value?((0,a.wg)(),(0,a.j4)((0,r.SU)(us.Z),{key:0,class:"h-3.5 w-3.5 transition-all"})):((0,a.wg)(),(0,a.j4)((0,r.SU)(is.Z),{key:1,class:"h-3.5 w-3.5 transition-all"})),(0,a._)("div",fs,(0,n.zw)(g.value?"Light mode":"Dark mode"),1)],8,vs)])]))}};const ys=(0,$.Z)(bs,[["__scopeId","data-v-482756f8"]]);var _s=ys,ks=s(691),Ss=s(446),Ds=s(5),Us=s(337);const zs={key:0,class:"announcement-container mb-6"},Ws={class:"flex items-center justify-between"},Cs={class:"flex items-center gap-2"},js={class:"text-xs text-gray-500 dark:text-gray-400"},Hs={key:0,class:"announcement-content p-4 transition-all duration-200 rounded-b-lg"},Fs={class:"relative"},Rs={class:"space-y-3"},$s={class:"flex items-center gap-3 mb-2 relative"},qs={class:"relative z-10 bg-white dark:bg-gray-800 px-2 py-1 rounded-md border border-gray-200 dark:border-gray-600"},Ts={class:"text-xs font-medium text-gray-600 dark:text-gray-300"},Zs={class:"space-y-2 ml-7 relative"},Es={class:"flex items-center justify-between gap-3"},As={class:"flex-1 min-w-0"},Ls={class:"text-sm leading-relaxed text-gray-900 dark:text-gray-100"},Ns=["title"];var Ms={__name:"AnnouncementBanner",props:{announcements:{type:Array,default:()=>[]}},setup(e){const t=e,s=(0,r.iH)(!1),l=()=>{s.value=!s.value},o={outage:{icon:ks.Z,background:"bg-red-50 border-gray-200 dark:bg-red-900/50 dark:border-gray-600",border:"border-red-500",iconColor:"text-red-600 dark:text-red-400",text:"text-red-700 dark:text-red-300"},warning:{icon:Ss.Z,background:"bg-yellow-50 border-gray-200 dark:bg-yellow-900/50 dark:border-gray-600",border:"border-yellow-500",iconColor:"text-yellow-600 dark:text-yellow-400",text:"text-yellow-700 dark:text-yellow-300"},information:{icon:Ds.Z,background:"bg-blue-50 border-gray-200 dark:bg-blue-900/50 dark:border-gray-600",border:"border-blue-500",iconColor:"text-blue-600 dark:text-blue-400",text:"text-blue-700 dark:text-blue-300"},operational:{icon:Ve.Z,background:"bg-green-50 border-gray-200 dark:bg-green-900/50 dark:border-gray-600",border:"border-green-500",iconColor:"text-green-600 dark:text-green-400",text:"text-green-700 dark:text-green-300"},none:{icon:Us.Z,background:"bg-gray-50 border-gray-200 dark:bg-gray-800/50 dark:border-gray-600",border:"border-gray-500",iconColor:"text-gray-600 dark:text-gray-400",text:"text-gray-700 dark:text-gray-300"}},u=(0,a.Fl)((()=>t.announcements&&t.announcements.length>0?t.announcements[0]:null)),i=(0,a.Fl)((()=>{const e=u.value?.type||"none";return o[e]?.icon||Us.Z})),d=(0,a.Fl)((()=>{const e=u.value?.type||"none";return o[e]?.iconColor||"text-gray-600 dark:text-gray-400"})),c=(0,a.Fl)((()=>{const e=u.value?.type||"none",t=o[e];return`border-l-4 ${t.border.replace("border-","border-l-")}`})),g=(0,a.Fl)((()=>{if(!t.announcements||0===t.announcements.length)return{};const e={};return t.announcements.forEach((t=>{const s=new Date(t.timestamp).toDateString();e[s]||(e[s]=[]),e[s].push(t)})),e})),m=e=>o[e]?.icon||Us.Z,p=e=>o[e]||o.none,v=e=>{const t=1===e.length?"2rem":2+3.5*(e.length-1)+"rem";return{top:"1.5rem",height:t}},f=e=>{const t=new Date(e),s=new Date,l=new Date(s);return l.setDate(l.getDate()-1),t.toDateString()===s.toDateString()?"Today":t.toDateString()===l.toDateString()?"Yesterday":t.toLocaleDateString("en-US",{weekday:"long",year:"numeric",month:"long",day:"numeric"})},w=e=>new Date(e).toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit",hour12:!1}),h=e=>new Date(e).toLocaleString("en-US",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"});return(t,o)=>e.announcements&&e.announcements.length?((0,a.wg)(),(0,a.iD)("div",zs,[(0,a._)("div",{class:(0,n.C_)(["rounded-lg border bg-card text-card-foreground shadow-sm transition-all duration-200",c.value])},[(0,a._)("div",{class:(0,n.C_)(["announcement-header px-4 py-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors",s.value?"rounded-lg":"rounded-t-lg border-b border-gray-200 dark:border-gray-600"]),onClick:l},[(0,a._)("div",Ws,[(0,a._)("div",Cs,[((0,a.wg)(),(0,a.j4)((0,a.LL)(i.value),{class:(0,n.C_)(["w-5 h-5",d.value])},null,8,["class"])),o[0]||(o[0]=(0,a._)("h2",{class:"text-base font-semibold text-gray-900 dark:text-gray-100"},"Announcements",-1)),(0,a._)("span",js," ("+(0,n.zw)(e.announcements.length)+") ",1)]),(0,a.Wm)((0,r.SU)(Ke.Z),{class:(0,n.C_)(["w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform duration-200",s.value?"-rotate-90":"rotate-0"])},null,8,["class"])])],2),s.value?(0,a.kq)("",!0):((0,a.wg)(),(0,a.iD)("div",Hs,[(0,a._)("div",Fs,[(0,a._)("div",Rs,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(g.value,((e,t)=>((0,a.wg)(),(0,a.iD)("div",{key:t,class:"relative"},[e.length>0?((0,a.wg)(),(0,a.iD)("div",{key:0,class:"absolute left-3 w-0.5 bg-gray-300 dark:bg-gray-600 pointer-events-none",style:(0,n.j5)(v(e))},null,4)):(0,a.kq)("",!0),(0,a._)("div",$s,[(0,a._)("div",qs,[(0,a._)("time",Ts,(0,n.zw)(f(t)),1)]),o[1]||(o[1]=(0,a._)("div",{class:"flex-1 border-t border-gray-200 dark:border-gray-600"},null,-1))]),(0,a._)("div",Zs,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(e,((e,s)=>((0,a.wg)(),(0,a.iD)("div",{key:`${t}-${s}-${e.timestamp}`,class:"relative"},[(0,a._)("div",{class:(0,n.C_)(["absolute -left-[26px] top-1/2 -translate-y-1/2 w-5 h-5 rounded-full border bg-white dark:bg-gray-800 flex items-center justify-center z-10",p(e.type).border])},[((0,a.wg)(),(0,a.j4)((0,a.LL)(m(e.type)),{class:(0,n.C_)(["w-3 h-3",p(e.type).iconColor])},null,8,["class"]))],2),(0,a._)("div",{class:(0,n.C_)(["rounded-md border p-3 transition-all duration-200 hover:shadow-sm",p(e.type).background])},[(0,a._)("div",Es,[(0,a._)("div",As,[(0,a._)("p",Ls,(0,n.zw)(e.message),1)]),(0,a._)("time",{class:(0,n.C_)(["text-xs font-mono whitespace-nowrap",p(e.type).text]),title:h(e.timestamp)},(0,n.zw)(w(e.timestamp)),11,Ns)])],2)])))),128))])])))),128))])])]))],2)])):(0,a.kq)("",!0)}};const Is=(0,$.Z)(Ms,[["__scopeId","data-v-48763619"]]);var Ys=Is;const Os={class:"dashboard-container bg-background"},Ks={class:"container mx-auto px-4 py-8 max-w-7xl"},Ps={class:"mb-6"},Vs={class:"flex items-center justify-between mb-6"},Gs={class:"flex items-center gap-4"},Bs={key:0,class:"flex items-center justify-center py-20"},Js={key:1,class:"text-center py-20"},Qs={class:"text-muted-foreground"},Xs={key:2},el={key:0,class:"space-y-6"},tl=["onClick"],sl={class:"flex items-center gap-3"},ll={class:"text-xl font-semibold text-foreground"},al={class:"flex items-center gap-2"},nl={key:0,class:"bg-red-600 text-white px-2 py-1 rounded-full text-sm font-medium"},rl={key:0,class:"endpoint-group-content p-4"},ol={key:0,class:"mb-4"},ul={class:"grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"},il={key:1},dl={key:0,class:"text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3"},cl={class:"grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"},gl={key:1},ml={key:0,class:"mb-6"},pl={class:"grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"},vl={key:1},fl={key:0,class:"text-lg font-semibold text-foreground mb-3"},wl={class:"grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"},hl={key:2,class:"mt-8 flex items-center justify-center gap-2"},xl={class:"flex gap-1"},bl=96;var yl={__name:"Home",props:{announcements:{type:Array,default:()=>[]}},emits:["showTooltip"],setup(e,{emit:t}){const s=e,l=t,o=(0,r.iH)([]),u=(0,r.iH)([]),i=(0,r.iH)(!1),d=(0,r.iH)(1),c=(0,r.iH)(""),g=(0,r.iH)(!1),m=(0,r.iH)(!1),p=(0,r.iH)(!0),v=(0,r.iH)(!1),f=(0,r.iH)(localStorage.getItem("gatus:sort-by")||"name"),w=(0,r.iH)(new Set),h=(0,a.Fl)((()=>{let e=[...o.value];if(c.value){const t=c.value.toLowerCase();e=e.filter((e=>e.name.toLowerCase().includes(t)||e.group&&e.group.toLowerCase().includes(t)))}return g.value&&(e=e.filter((e=>{if(!e.results||0===e.results.length)return!1;const t=e.results[e.results.length-1];return!t.success}))),m.value&&(e=e.filter((e=>!(!e.results||0===e.results.length)&&e.results.some((e=>!e.success))))),"health"===f.value&&e.sort(((e,t)=>{const s=e.results&&e.results.length>0&&e.results[e.results.length-1].success,l=t.results&&t.results.length>0&&t.results[t.results.length-1].success;return!s&&l?-1:s&&!l?1:e.name.localeCompare(t.name)})),e})),b=(0,a.Fl)((()=>{let e=[...u.value];if(c.value){const t=c.value.toLowerCase();e=e.filter((e=>e.name.toLowerCase().includes(t)||e.group&&e.group.toLowerCase().includes(t)))}return g.value&&(e=e.filter((e=>!(!e.results||0===e.results.length)&&!e.results[e.results.length-1].success))),m.value&&(e=e.filter((e=>!(!e.results||0===e.results.length)&&e.results.some((e=>!e.success))))),"health"===f.value&&e.sort(((e,t)=>{const s=e.results&&e.results.length>0&&e.results[e.results.length-1].success,l=t.results&&t.results.length>0&&t.results[t.results.length-1].success;return!s&&l?-1:s&&!l?1:e.name.localeCompare(t.name)})),e})),y=(0,a.Fl)((()=>Math.ceil((h.value.length+b.value.length)/bl))),_=(0,a.Fl)((()=>{if(!v.value)return null;const e={};h.value.forEach((t=>{const s=t.group||"No Group";e[s]||(e[s]=[]),e[s].push(t)}));const t=Object.keys(e).sort(((e,t)=>"No Group"===e?1:"No Group"===t?-1:e.localeCompare(t))),s={};return t.forEach((t=>{s[t]=e[t]})),s})),k=(0,a.Fl)((()=>{if(!v.value)return null;const e={};h.value.forEach((t=>{const s=t.group||"No Group";e[s]||(e[s]={endpoints:[],suites:[]}),e[s].endpoints.push(t)})),b.value.forEach((t=>{const s=t.group||"No Group";e[s]||(e[s]={endpoints:[],suites:[]}),e[s].suites.push(t)}));const t=Object.keys(e).sort(((e,t)=>"No Group"===e?1:"No Group"===t?-1:e.localeCompare(t))),s={};return t.forEach((t=>{s[t]=e[t]})),s})),S=(0,a.Fl)((()=>{if(v.value)return _.value;const e=(d.value-1)*bl,t=e+bl;return h.value.slice(e,t)})),D=(0,a.Fl)((()=>{if(v.value)return b.value;const e=(d.value-1)*bl,t=e+bl;return b.value.slice(e,t)})),U=(0,a.Fl)((()=>{const e=[],t=5;let s=Math.max(1,d.value-Math.floor(t/2)),l=Math.min(y.value,s+t-1);l-s{const e=0===o.value.length&&0===u.value.length;e&&(i.value=!0);try{const t=await fetch(`${Qn}/api/v1/endpoints/statuses?page=1&pageSize=100`,{credentials:"include"});if(200===t.status){const e=await t.json();o.value=e}else console.error("[Home][fetchData] Error fetching endpoints:",await t.text());const s=await fetch(`${Qn}/api/v1/suites/statuses?page=1&pageSize=100`,{credentials:"include"});if(200===s.status){const e=await s.json();u.value=e}else console.error("[Home][fetchData] Error fetching suites:",await s.text())}catch(t){console.error("[Home][fetchData] Error:",t)}finally{e&&(i.value=!1)}},W=()=>{o.value=[],z()},C=e=>{c.value=e,d.value=1},j=e=>{d.value=e,window.scrollTo({top:0,behavior:"smooth"})},H=()=>{p.value=!p.value},F=(e,t)=>{l("showTooltip",e,t)},R=e=>e.filter((e=>{if(!e.results||0===e.results.length)return!1;const t=e.results[e.results.length-1];return!t.success})).length,$=e=>e.filter((e=>!(!e.results||0===e.results.length)&&!e.results[e.results.length-1].success)).length,q=e=>{w.value.has(e)?w.value.delete(e):w.value.add(e);const t=Array.from(w.value);localStorage.setItem("gatus:uncollapsed-groups",JSON.stringify(t)),localStorage.removeItem("gatus:collapsed-groups")},T=()=>{try{const e=localStorage.getItem("gatus:uncollapsed-groups");e&&(w.value=new Set(JSON.parse(e)))}catch(e){console.warn("Failed to parse saved uncollapsed groups:",e),localStorage.removeItem("gatus:uncollapsed-groups")}};return(0,a.bv)((()=>{z()})),(e,t)=>((0,a.wg)(),(0,a.iD)("div",Os,[(0,a._)("div",Ks,[(0,a._)("div",Ps,[(0,a._)("div",Vs,[t[6]||(t[6]=(0,a._)("div",null,[(0,a._)("h1",{class:"text-4xl font-bold tracking-tight"},"Health Dashboard"),(0,a._)("p",{class:"text-muted-foreground mt-2"},"Monitor the health of your endpoints in real-time")],-1)),(0,a._)("div",Gs,[(0,a.Wm)((0,r.SU)(x),{variant:"ghost",size:"icon",onClick:H,title:p.value?"Show min-max response time":"Show average response time"},{default:(0,a.w5)((()=>[p.value?((0,a.wg)(),(0,a.j4)((0,r.SU)(Me.Z),{key:0,class:"h-5 w-5"})):((0,a.wg)(),(0,a.j4)((0,r.SU)(Ie.Z),{key:1,class:"h-5 w-5"}))])),_:1},8,["title"]),(0,a.Wm)((0,r.SU)(x),{variant:"ghost",size:"icon",onClick:W,title:"Refresh data"},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(Ye.Z),{class:"h-5 w-5"})])),_:1})])]),(0,a.Wm)(Ys,{announcements:s.announcements},null,8,["announcements"]),(0,a.Wm)(os,{onSearch:C,"onUpdate:showOnlyFailing":t[0]||(t[0]=e=>g.value=e),"onUpdate:showRecentFailures":t[1]||(t[1]=e=>m.value=e),"onUpdate:groupByGroup":t[2]||(t[2]=e=>v.value=e),"onUpdate:sortBy":t[3]||(t[3]=e=>f.value=e),onInitializeCollapsedGroups:T})]),i.value?((0,a.wg)(),(0,a.iD)("div",Bs,[(0,a.Wm)(de,{size:"lg"})])):0===h.value.length&&0===b.value.length?((0,a.wg)(),(0,a.iD)("div",Js,[(0,a.Wm)((0,r.SU)(Oe.Z),{class:"h-12 w-12 text-muted-foreground mx-auto mb-4"}),t[7]||(t[7]=(0,a._)("h3",{class:"text-lg font-semibold mb-2"},"No endpoints or suites found",-1)),(0,a._)("p",Qs,(0,n.zw)(c.value||g.value||m.value?"Try adjusting your filters":"No endpoints or suites are configured"),1)])):((0,a.wg)(),(0,a.iD)("div",Xs,[v.value?((0,a.wg)(),(0,a.iD)("div",el,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(k.value,((e,s)=>((0,a.wg)(),(0,a.iD)("div",{key:s,class:"endpoint-group border rounded-lg overflow-hidden"},[(0,a._)("div",{onClick:e=>q(s),class:"endpoint-group-header flex items-center justify-between p-4 bg-card border-b cursor-pointer hover:bg-accent/50 transition-colors"},[(0,a._)("div",sl,[w.value.has(s)?((0,a.wg)(),(0,a.j4)((0,r.SU)(Ke.Z),{key:0,class:"h-5 w-5 text-muted-foreground"})):((0,a.wg)(),(0,a.j4)((0,r.SU)(Pe.Z),{key:1,class:"h-5 w-5 text-muted-foreground"})),(0,a._)("h2",ll,(0,n.zw)(s),1)]),(0,a._)("div",al,[R(e.endpoints)+$(e.suites)>0?((0,a.wg)(),(0,a.iD)("span",nl,(0,n.zw)(R(e.endpoints)+$(e.suites)),1)):((0,a.wg)(),(0,a.j4)((0,r.SU)(Ve.Z),{key:1,class:"h-6 w-6 text-green-600"}))])],8,tl),w.value.has(s)?((0,a.wg)(),(0,a.iD)("div",rl,[e.suites.length>0?((0,a.wg)(),(0,a.iD)("div",ol,[t[8]||(t[8]=(0,a._)("h3",{class:"text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3"},"Suites",-1)),(0,a._)("div",ul,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(e.suites,(e=>((0,a.wg)(),(0,a.j4)(Zt,{key:e.key,suite:e,maxResults:50,onShowTooltip:F},null,8,["suite"])))),128))])])):(0,a.kq)("",!0),e.endpoints.length>0?((0,a.wg)(),(0,a.iD)("div",il,[e.suites.length>0?((0,a.wg)(),(0,a.iD)("h3",dl,"Endpoints")):(0,a.kq)("",!0),(0,a._)("div",cl,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(e.endpoints,(e=>((0,a.wg)(),(0,a.j4)(xt,{key:e.key,endpoint:e,maxResults:50,showAverageResponseTime:p.value,onShowTooltip:F},null,8,["endpoint","showAverageResponseTime"])))),128))])])):(0,a.kq)("",!0)])):(0,a.kq)("",!0)])))),128))])):((0,a.wg)(),(0,a.iD)("div",gl,[b.value.length>0?((0,a.wg)(),(0,a.iD)("div",ml,[t[9]||(t[9]=(0,a._)("h2",{class:"text-lg font-semibold text-foreground mb-3"},"Suites",-1)),(0,a._)("div",pl,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(D.value,(e=>((0,a.wg)(),(0,a.j4)(Zt,{key:e.key,suite:e,maxResults:50,onShowTooltip:F},null,8,["suite"])))),128))])])):(0,a.kq)("",!0),h.value.length>0?((0,a.wg)(),(0,a.iD)("div",vl,[b.value.length>0?((0,a.wg)(),(0,a.iD)("h2",fl,"Endpoints")):(0,a.kq)("",!0),(0,a._)("div",wl,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(S.value,(e=>((0,a.wg)(),(0,a.j4)(xt,{key:e.key,endpoint:e,maxResults:50,showAverageResponseTime:p.value,onShowTooltip:F},null,8,["endpoint","showAverageResponseTime"])))),128))])])):(0,a.kq)("",!0)])),!v.value&&y.value>1?((0,a.wg)(),(0,a.iD)("div",hl,[(0,a.Wm)((0,r.SU)(x),{variant:"outline",size:"icon",disabled:1===d.value,onClick:t[4]||(t[4]=e=>j(d.value-1))},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(Ge.Z),{class:"h-4 w-4"})])),_:1},8,["disabled"]),(0,a._)("div",xl,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(U.value,(e=>((0,a.wg)(),(0,a.j4)((0,r.SU)(x),{key:e,variant:e===d.value?"default":"outline",size:"sm",onClick:t=>j(e)},{default:(0,a.w5)((()=>[(0,a.Uk)((0,n.zw)(e),1)])),_:2},1032,["variant","onClick"])))),128))]),(0,a.Wm)((0,r.SU)(x),{variant:"outline",size:"icon",disabled:d.value===y.value,onClick:t[5]||(t[5]=e=>j(d.value+1))},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(Be.Z),{class:"h-4 w-4"})])),_:1},8,["disabled"])])):(0,a.kq)("",!0)]))]),(0,a.Wm)(_s,{onRefreshData:z})]))}};const _l=yl;var kl=_l,Sl=s(318),Dl=s(779),Ul=s(141),zl=s(478);const Wl={class:"flex items-center justify-between"},Cl={class:"text-sm text-muted-foreground"};var jl={__name:"Pagination",props:{numberOfResultsPerPage:Number,currentPageProp:{type:Number,default:1}},emits:["page"],setup(e,{emit:t}){const s=e,l=t,o=(0,r.iH)(s.currentPageProp),u=(0,a.Fl)((()=>{let e=100;if("undefined"!==typeof window&&window.config&&window.config.maximumNumberOfResults){const t=parseInt(window.config.maximumNumberOfResults);isNaN(t)||(e=t)}return Math.ceil(e/s.numberOfResultsPerPage)})),i=()=>{o.value--,l("page",o.value)},d=()=>{o.value++,l("page",o.value)};return(e,t)=>((0,a.wg)(),(0,a.iD)("div",Wl,[(0,a.Wm)((0,r.SU)(x),{variant:"outline",size:"sm",disabled:o.value>=u.value,onClick:d,class:"flex items-center gap-1"},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(Ge.Z),{class:"h-4 w-4"}),t[0]||(t[0]=(0,a.Uk)(" Previous ",-1))])),_:1,__:[0]},8,["disabled"]),(0,a._)("span",Cl," Page "+(0,n.zw)(o.value)+" of "+(0,n.zw)(u.value),1),(0,a.Wm)((0,r.SU)(x),{variant:"outline",size:"sm",disabled:o.value<=1,onClick:i,class:"flex items-center gap-1"},{default:(0,a.w5)((()=>[t[1]||(t[1]=(0,a.Uk)(" Next ",-1)),(0,a.Wm)((0,r.SU)(Be.Z),{class:"h-4 w-4"})])),_:1,__:[1]},8,["disabled"])]))}};const Hl=jl;var Fl=Hl;const Rl={class:"dashboard-container bg-background"},$l={class:"container mx-auto px-4 py-8 max-w-7xl"},ql={class:"mb-6"},Tl={key:0,class:"space-y-6"},Zl={class:"flex items-start justify-between"},El={class:"text-4xl font-bold tracking-tight"},Al={class:"flex items-center gap-3 text-muted-foreground mt-2"},Ll={key:0},Nl={key:1},Ml={key:2},Il={class:"grid gap-6 md:grid-cols-2 lg:grid-cols-4"},Yl={class:"text-2xl font-bold"},Ol={class:"text-2xl font-bold"},Kl={class:"text-2xl font-bold"},Pl={class:"text-2xl font-bold"},Vl={class:"flex items-center justify-between"},Gl={class:"flex items-center gap-2"},Bl={class:"space-y-4"},Jl={key:1,class:"pt-4 border-t"},Ql={key:0,class:"space-y-6"},Xl={class:"flex items-center justify-between"},ea=["src"],ta={class:"grid gap-4 md:grid-cols-2 lg:grid-cols-4"},sa=["src","alt"],la={class:"grid gap-4 md:grid-cols-2 lg:grid-cols-4"},aa={class:"text-sm text-muted-foreground mb-2"},na=["src","alt"],ra={class:"text-center"},oa=["src"],ua={class:"space-y-4"},ia={class:"mt-1"},da={class:"flex-1"},ca={class:"font-medium"},ga={class:"text-sm text-muted-foreground"},ma={key:1,class:"flex items-center justify-center py-20"};var pa={__name:"EndpointDetails",emits:["showTooltip"],setup(e,{emit:t}){const s=(0,u.tv)(),o=(0,u.yj)(),i=t,d=(0,r.iH)(null),c=(0,r.iH)(null),g=(0,r.iH)([]),m=(0,r.iH)(1),p=(0,r.iH)(!1),v=(0,r.iH)(!1),f=(0,r.iH)("24h"),w="."===Qn?"..":Qn,h=(0,r.iH)(!1),b=(0,a.Fl)((()=>c.value&&c.value.results&&0!==c.value.results.length?c.value.results[c.value.results.length-1]:null)),y=(0,a.Fl)((()=>b.value?b.value.success?"healthy":"unhealthy":"unknown")),k=(0,a.Fl)((()=>b.value?.hostname||null)),S=(0,a.Fl)((()=>{if(!d.value||!d.value.results||0===d.value.results.length)return"N/A";let e=0,t=0;for(const s of d.value.results)s.duration&&(e+=s.duration,t++);return 0===t?"N/A":Math.round(e/t/1e6)})),U=(0,a.Fl)((()=>{if(!d.value||!d.value.results||0===d.value.results.length)return"N/A";let e=1/0,t=0,s=!1;for(const n of d.value.results)if(n.duration){const l=n.duration/1e6;e=Math.min(e,l),t=Math.max(t,l),s=!0}if(!s)return"N/A";const l=Math.round(e),a=Math.round(t);return l===a?`${l}ms`:`${l}-${a}ms`})),z=(0,a.Fl)((()=>c.value&&c.value.results&&0!==c.value.results.length?E(c.value.results[c.value.results.length-1].timestamp):"Never")),C=async()=>{h.value=!0;try{const e=await fetch(`${w}/api/v1/endpoints/${o.params.key}/statuses?page=${m.value}&pageSize=50`,{credentials:"include"});if(200===e.status){const t=await e.json();d.value=t,1===m.value&&(c.value=t);let s=[];if(t.events&&t.events.length>0)for(let e=t.events.length-1;e>=0;e--){let l=t.events[e];if(e===t.events.length-1)"UNHEALTHY"===l.type?l.fancyText="Endpoint is unhealthy":"HEALTHY"===l.type?l.fancyText="Endpoint is healthy":"START"===l.type&&(l.fancyText="Monitoring started");else{let s=t.events[e+1];"HEALTHY"===l.type?l.fancyText="Endpoint became healthy":"UNHEALTHY"===l.type?l.fancyText=s?"Endpoint was unhealthy for "+A(s.timestamp,l.timestamp):"Endpoint became unhealthy":"START"===l.type&&(l.fancyText="Monitoring started")}l.fancyTimeAgo=E(l.timestamp),s.push(l)}if(g.value=s,t.results&&t.results.length>0)for(let e=0;e0){p.value=!0;break}}else console.error("[Details][fetchData] Error:",await e.text())}catch(e){console.error("[Details][fetchData] Error:",e)}finally{h.value=!1}},j=()=>{s.push("/")},F=e=>{m.value=e,C()},R=(e,t)=>{i("showTooltip",e,t)},$=e=>new Date(e).toLocaleString(),q=()=>`${w}/api/v1/endpoints/${d.value.key}/health/badge.svg`,T=e=>`${w}/api/v1/endpoints/${d.value.key}/uptimes/${e}/badge.svg`,Z=e=>`${w}/api/v1/endpoints/${d.value.key}/response-times/${e}/badge.svg`,L=e=>`${w}/api/v1/endpoints/${d.value.key}/response-times/${e}/chart.svg`;return(0,a.bv)((()=>{C()})),(e,t)=>((0,a.wg)(),(0,a.iD)("div",Rl,[(0,a._)("div",$l,[(0,a._)("div",ql,[(0,a.Wm)((0,r.SU)(x),{variant:"ghost",class:"mb-4",onClick:j},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(Sl.Z),{class:"h-4 w-4 mr-2"}),t[2]||(t[2]=(0,a.Uk)(" Back to Dashboard ",-1))])),_:1,__:[2]}),d.value&&d.value.name?((0,a.wg)(),(0,a.iD)("div",Tl,[(0,a._)("div",Zl,[(0,a._)("div",null,[(0,a._)("h1",El,(0,n.zw)(d.value.name),1),(0,a._)("div",Al,[d.value.group?((0,a.wg)(),(0,a.iD)("span",Ll,"Group: "+(0,n.zw)(d.value.group),1)):(0,a.kq)("",!0),d.value.group&&k.value?((0,a.wg)(),(0,a.iD)("span",Nl,"•")):(0,a.kq)("",!0),k.value?((0,a.wg)(),(0,a.iD)("span",Ml,(0,n.zw)(k.value),1)):(0,a.kq)("",!0)])]),(0,a.Wm)(st,{status:y.value},null,8,["status"])]),(0,a._)("div",Il,[(0,a.Wm)((0,r.SU)(_),null,{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),{class:"pb-2"},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(W),{class:"text-sm font-medium text-muted-foreground"},{default:(0,a.w5)((()=>t[3]||(t[3]=[(0,a.Uk)("Current Status",-1)]))),_:1,__:[3]})])),_:1}),(0,a.Wm)((0,r.SU)(H),null,{default:(0,a.w5)((()=>[(0,a._)("div",Yl,(0,n.zw)("healthy"===y.value?"Operational":"Issues Detected"),1)])),_:1})])),_:1}),(0,a.Wm)((0,r.SU)(_),null,{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),{class:"pb-2"},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(W),{class:"text-sm font-medium text-muted-foreground"},{default:(0,a.w5)((()=>t[4]||(t[4]=[(0,a.Uk)("Avg Response Time",-1)]))),_:1,__:[4]})])),_:1}),(0,a.Wm)((0,r.SU)(H),null,{default:(0,a.w5)((()=>[(0,a._)("div",Ol,(0,n.zw)(S.value)+"ms",1)])),_:1})])),_:1}),(0,a.Wm)((0,r.SU)(_),null,{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),{class:"pb-2"},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(W),{class:"text-sm font-medium text-muted-foreground"},{default:(0,a.w5)((()=>t[5]||(t[5]=[(0,a.Uk)("Response Time Range",-1)]))),_:1,__:[5]})])),_:1}),(0,a.Wm)((0,r.SU)(H),null,{default:(0,a.w5)((()=>[(0,a._)("div",Kl,(0,n.zw)(U.value),1)])),_:1})])),_:1}),(0,a.Wm)((0,r.SU)(_),null,{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),{class:"pb-2"},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(W),{class:"text-sm font-medium text-muted-foreground"},{default:(0,a.w5)((()=>t[6]||(t[6]=[(0,a.Uk)("Last Check",-1)]))),_:1,__:[6]})])),_:1}),(0,a.Wm)((0,r.SU)(H),null,{default:(0,a.w5)((()=>[(0,a._)("div",Pl,(0,n.zw)(z.value),1)])),_:1})])),_:1})]),(0,a.Wm)((0,r.SU)(_),null,{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),null,{default:(0,a.w5)((()=>[(0,a._)("div",Vl,[(0,a.Wm)((0,r.SU)(W),null,{default:(0,a.w5)((()=>t[7]||(t[7]=[(0,a.Uk)("Recent Checks",-1)]))),_:1,__:[7]}),(0,a._)("div",Gl,[(0,a.Wm)((0,r.SU)(x),{variant:"ghost",size:"icon",onClick:t[0]||(t[0]=e=>v.value=!v.value),title:v.value?"Show min-max response time":"Show average response time"},{default:(0,a.w5)((()=>[v.value?((0,a.wg)(),(0,a.j4)((0,r.SU)(Me.Z),{key:0,class:"h-5 w-5"})):((0,a.wg)(),(0,a.j4)((0,r.SU)(Ie.Z),{key:1,class:"h-5 w-5"}))])),_:1},8,["title"]),(0,a.Wm)((0,r.SU)(x),{variant:"ghost",size:"icon",onClick:C,title:"Refresh data",disabled:h.value},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(Ye.Z),{class:(0,n.C_)(["h-4 w-4",h.value&&"animate-spin"])},null,8,["class"])])),_:1},8,["disabled"])])])])),_:1}),(0,a.Wm)((0,r.SU)(H),null,{default:(0,a.w5)((()=>[(0,a._)("div",Bl,[d.value?((0,a.wg)(),(0,a.j4)(xt,{key:0,endpoint:d.value,maxResults:50,showAverageResponseTime:v.value,onShowTooltip:R,class:"border-0 shadow-none bg-transparent p-0"},null,8,["endpoint","showAverageResponseTime"])):(0,a.kq)("",!0),d.value&&d.value.key?((0,a.wg)(),(0,a.iD)("div",Jl,[(0,a.Wm)(Fl,{onPage:F,numberOfResultsPerPage:50,currentPageProp:m.value},null,8,["currentPageProp"])])):(0,a.kq)("",!0)])])),_:1})])),_:1}),p.value?((0,a.wg)(),(0,a.iD)("div",Ql,[(0,a.Wm)((0,r.SU)(_),null,{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),null,{default:(0,a.w5)((()=>[(0,a._)("div",Xl,[(0,a.Wm)((0,r.SU)(W),null,{default:(0,a.w5)((()=>t[8]||(t[8]=[(0,a.Uk)("Response Time Trend",-1)]))),_:1,__:[8]}),(0,a.wy)((0,a._)("select",{"onUpdate:modelValue":t[1]||(t[1]=e=>f.value=e),class:"text-sm bg-background border rounded-md px-3 py-1 focus:outline-none focus:ring-2 focus:ring-ring"},t[9]||(t[9]=[(0,a._)("option",{value:"24h"},"24 hours",-1),(0,a._)("option",{value:"7d"},"7 days",-1),(0,a._)("option",{value:"30d"},"30 days",-1)]),512),[[l.bM,f.value]])])])),_:1}),(0,a.Wm)((0,r.SU)(H),null,{default:(0,a.w5)((()=>[(0,a._)("img",{src:L(f.value),alt:"Response time chart",class:"w-full"},null,8,ea)])),_:1})])),_:1}),(0,a._)("div",ta,[((0,a.wg)(),(0,a.iD)(a.HY,null,(0,a.Ko)(["30d","7d","24h","1h"],(e=>(0,a.Wm)((0,r.SU)(_),{key:e},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),{class:"pb-2"},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(W),{class:"text-sm font-medium text-muted-foreground text-center"},{default:(0,a.w5)((()=>[(0,a.Uk)((0,n.zw)("30d"===e?"Last 30 days":"7d"===e?"Last 7 days":"24h"===e?"Last 24 hours":"Last hour"),1)])),_:2},1024)])),_:2},1024),(0,a.Wm)((0,r.SU)(H),null,{default:(0,a.w5)((()=>[(0,a._)("img",{src:Z(e),alt:`${e} response time`,class:"mx-auto mt-2"},null,8,sa)])),_:2},1024)])),_:2},1024))),64))])])):(0,a.kq)("",!0),(0,a.Wm)((0,r.SU)(_),null,{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),null,{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(W),null,{default:(0,a.w5)((()=>t[10]||(t[10]=[(0,a.Uk)("Uptime Statistics",-1)]))),_:1,__:[10]})])),_:1}),(0,a.Wm)((0,r.SU)(H),null,{default:(0,a.w5)((()=>[(0,a._)("div",la,[((0,a.wg)(),(0,a.iD)(a.HY,null,(0,a.Ko)(["30d","7d","24h","1h"],(e=>(0,a._)("div",{key:e,class:"text-center"},[(0,a._)("p",aa,(0,n.zw)("30d"===e?"Last 30 days":"7d"===e?"Last 7 days":"24h"===e?"Last 24 hours":"Last hour"),1),(0,a._)("img",{src:T(e),alt:`${e} uptime`,class:"mx-auto"},null,8,na)]))),64))])])),_:1})])),_:1}),(0,a.Wm)((0,r.SU)(_),null,{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),null,{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(W),null,{default:(0,a.w5)((()=>t[11]||(t[11]=[(0,a.Uk)("Current Health",-1)]))),_:1,__:[11]})])),_:1}),(0,a.Wm)((0,r.SU)(H),null,{default:(0,a.w5)((()=>[(0,a._)("div",ra,[(0,a._)("img",{src:q(),alt:"health badge",class:"mx-auto"},null,8,oa)])])),_:1})])),_:1}),g.value&&g.value.length>0?((0,a.wg)(),(0,a.j4)((0,r.SU)(_),{key:1},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),null,{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(W),null,{default:(0,a.w5)((()=>t[12]||(t[12]=[(0,a.Uk)("Events",-1)]))),_:1,__:[12]})])),_:1}),(0,a.Wm)((0,r.SU)(H),null,{default:(0,a.w5)((()=>[(0,a._)("div",ua,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(g.value,(e=>((0,a.wg)(),(0,a.iD)("div",{key:e.timestamp,class:"flex items-start gap-4 pb-4 border-b last:border-0"},[(0,a._)("div",ia,["HEALTHY"===e.type?((0,a.wg)(),(0,a.j4)((0,r.SU)(Dl.Z),{key:0,class:"h-5 w-5 text-green-500"})):"UNHEALTHY"===e.type?((0,a.wg)(),(0,a.j4)((0,r.SU)(Ul.Z),{key:1,class:"h-5 w-5 text-red-500"})):((0,a.wg)(),(0,a.j4)((0,r.SU)(zl.Z),{key:2,class:"h-5 w-5 text-muted-foreground"}))]),(0,a._)("div",da,[(0,a._)("p",ca,(0,n.zw)(e.fancyText),1),(0,a._)("p",ga,(0,n.zw)($(e.timestamp))+" • "+(0,n.zw)(e.fancyTimeAgo),1)])])))),128))])])),_:1})])),_:1})):(0,a.kq)("",!0)])):((0,a.wg)(),(0,a.iD)("div",ma,[(0,a.Wm)(de,{size:"lg"})]))])]),(0,a.Wm)(_s,{onRefreshData:C})]))}};const va=pa;var fa=va,wa=s(469),ha=s(399),xa=s(167);const ba=e=>{if(!e&&0!==e)return"N/A";const t=e/1e6;return t<1e3?`${t.toFixed(0)}ms`:`${(t/1e3).toFixed(2)}s`},ya={class:"relative flex-shrink-0"},_a={class:"flex-1 min-w-0 pt-1"},ka={class:"flex items-center justify-between gap-2 mb-1"},Sa={class:"font-medium text-sm truncate"},Da={class:"text-xs text-muted-foreground whitespace-nowrap"},Ua={class:"flex flex-wrap gap-1"},za={key:0,class:"inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 rounded-md"},Wa={key:1,class:"inline-flex items-center px-2 py-1 text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 rounded-md"};var Ca={__name:"FlowStep",props:{step:{type:Object,required:!0},index:{type:Number,required:!0},isLast:{type:Boolean,default:!1},previousStep:{type:Object,default:null}},emits:["step-click"],setup(e){const t=e,s=(0,a.Fl)((()=>{switch(t.step.status){case"success":return Ve.Z;case"failed":return ks.Z;case"skipped":return wa.Z;case"not-started":return xa.Z;default:return xa.Z}})),l=(0,a.Fl)((()=>{const e="border-2";if(t.step.isAlwaysRun)switch(t.step.status){case"success":return`${e} bg-green-500 text-white border-green-600 ring-2 ring-blue-200 dark:ring-blue-800`;case"failed":return`${e} bg-red-500 text-white border-red-600 ring-2 ring-blue-200 dark:ring-blue-800`;default:return`${e} bg-blue-500 text-white border-blue-600 ring-2 ring-blue-200 dark:ring-blue-800`}switch(t.step.status){case"success":return`${e} bg-green-500 text-white border-green-600`;case"failed":return`${e} bg-red-500 text-white border-red-600`;case"skipped":return`${e} bg-gray-400 text-white border-gray-500`;case"not-started":return`${e} bg-gray-200 text-gray-500 border-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600`;default:return`${e} bg-gray-200 text-gray-500 border-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600`}})),o=(0,a.Fl)((()=>{if(!t.previousStep)return"bg-gray-300 dark:bg-gray-600";if("skipped"===t.step.status)return"border-l-2 border-dashed border-gray-400 bg-transparent";switch(t.previousStep.status){case"success":return"bg-green-500";case"failed":return"bg-red-500";default:return"bg-gray-300 dark:bg-gray-600"}})),u=(0,a.Fl)((()=>{const e=t.step.nextStepStatus;switch(t.step.status){case"success":return"skipped"===e?"bg-gray-300 dark:bg-gray-600":"bg-green-500";case"failed":return"skipped"===e?"border-l-2 border-dashed border-gray-400 bg-transparent":"bg-red-500";default:return"bg-gray-300 dark:bg-gray-600"}}));return(t,i)=>((0,a.wg)(),(0,a.iD)("div",{class:"flex items-start gap-4 relative group hover:bg-accent/30 rounded-lg p-2 -m-2 transition-colors cursor-pointer",onClick:i[0]||(i[0]=e=>t.$emit("step-click"))},[(0,a._)("div",ya,[e.index>0?((0,a.wg)(),(0,a.iD)("div",{key:0,class:(0,n.C_)([o.value,"absolute left-1/2 bottom-8 w-0.5 h-4 -translate-x-px"])},null,2)):(0,a.kq)("",!0),(0,a._)("div",{class:(0,n.C_)([l.value,"w-8 h-8 rounded-full flex items-center justify-center"])},[((0,a.wg)(),(0,a.j4)((0,a.LL)(s.value),{class:"w-4 h-4"}))],2),e.isLast?(0,a.kq)("",!0):((0,a.wg)(),(0,a.iD)("div",{key:1,class:(0,n.C_)([u.value,"absolute left-1/2 top-8 w-0.5 h-4 -translate-x-px"])},null,2))]),(0,a._)("div",_a,[(0,a._)("div",ka,[(0,a._)("h4",Sa,(0,n.zw)(e.step.name),1),(0,a._)("span",Da,(0,n.zw)((0,r.SU)(ba)(e.step.duration)),1)]),(0,a._)("div",Ua,[e.step.isAlwaysRun?((0,a.wg)(),(0,a.iD)("span",za,[(0,a.Wm)((0,r.SU)(ha.Z),{class:"w-3 h-3"}),i[1]||(i[1]=(0,a.Uk)(" Always Run ",-1))])):(0,a.kq)("",!0),e.step.errors?.length?((0,a.wg)(),(0,a.iD)("span",Wa,(0,n.zw)(e.step.errors.length)+" error"+(0,n.zw)(1!==e.step.errors.length?"s":""),1)):(0,a.kq)("",!0)])])]))}};const ja=Ca;var Ha=ja;const Fa={class:"space-y-4"},Ra={class:"flex items-center gap-4"},$a={class:"flex-1 h-1 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden"},qa={class:"flex items-center justify-between text-xs text-muted-foreground"},Ta={key:0},Za={class:"space-y-2"},Ea={class:"mt-6 pt-4 border-t"},Aa={class:"grid grid-cols-2 md:grid-cols-4 gap-3 text-xs"},La={key:0,class:"flex items-center gap-2"},Na={class:"w-4 h-4 rounded-full bg-green-500 flex items-center justify-center"},Ma={key:1,class:"flex items-center gap-2"},Ia={class:"w-4 h-4 rounded-full bg-red-500 flex items-center justify-center"},Ya={key:2,class:"flex items-center gap-2"},Oa={class:"w-4 h-4 rounded-full bg-gray-400 flex items-center justify-center"},Ka={key:3,class:"flex items-center gap-2"},Pa={class:"w-4 h-4 rounded-full bg-blue-500 border-2 border-blue-200 dark:border-blue-800 flex items-center justify-center"};var Va={__name:"SequentialFlowDiagram",props:{flowSteps:{type:Array,default:()=>[]},progressPercentage:{type:Number,default:0},completedSteps:{type:Number,default:0},totalSteps:{type:Number,default:0}},emits:["step-selected"],setup(e){const t=e,s=(0,a.Fl)((()=>t.completedSteps)),l=(0,a.Fl)((()=>t.totalSteps)),o=(0,a.Fl)((()=>t.flowSteps.reduce(((e,t)=>e+(t.duration||0)),0))),u=(0,a.Fl)((()=>t.flowSteps.some((e=>"success"===e.status)))),i=(0,a.Fl)((()=>t.flowSteps.some((e=>"failed"===e.status)))),d=(0,a.Fl)((()=>t.flowSteps.some((e=>"skipped"===e.status)))),c=(0,a.Fl)((()=>t.flowSteps.some((e=>!0===e.isAlwaysRun))));return(t,g)=>((0,a.wg)(),(0,a.iD)("div",Fa,[(0,a._)("div",Ra,[g[0]||(g[0]=(0,a._)("div",{class:"text-sm font-medium text-muted-foreground"},"Start",-1)),(0,a._)("div",$a,[(0,a._)("div",{class:"h-full bg-green-500 dark:bg-green-600 rounded-full transition-all duration-300 ease-out",style:(0,n.j5)({width:e.progressPercentage+"%"})},null,4)]),g[1]||(g[1]=(0,a._)("div",{class:"text-sm font-medium text-muted-foreground"},"End",-1))]),(0,a._)("div",qa,[(0,a._)("span",null,(0,n.zw)(s.value)+"/"+(0,n.zw)(l.value)+" steps successful",1),o.value>0?((0,a.wg)(),(0,a.iD)("span",Ta,(0,n.zw)((0,r.SU)(ba)(o.value))+" total",1)):(0,a.kq)("",!0)]),(0,a._)("div",Za,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(e.flowSteps,((s,l)=>((0,a.wg)(),(0,a.j4)(Ha,{key:l,step:s,index:l,"is-last":l===e.flowSteps.length-1,"previous-step":l>0?e.flowSteps[l-1]:null,onStepClick:e=>t.$emit("step-selected",s,l)},null,8,["step","index","is-last","previous-step","onStepClick"])))),128))]),(0,a._)("div",Ea,[g[6]||(g[6]=(0,a._)("div",{class:"text-sm font-medium text-muted-foreground mb-2"},"Status Legend",-1)),(0,a._)("div",Aa,[u.value?((0,a.wg)(),(0,a.iD)("div",La,[(0,a._)("div",Na,[(0,a.Wm)((0,r.SU)(Ve.Z),{class:"w-3 h-3 text-white"})]),g[2]||(g[2]=(0,a._)("span",{class:"text-muted-foreground"},"Success",-1))])):(0,a.kq)("",!0),i.value?((0,a.wg)(),(0,a.iD)("div",Ma,[(0,a._)("div",Ia,[(0,a.Wm)((0,r.SU)(ks.Z),{class:"w-3 h-3 text-white"})]),g[3]||(g[3]=(0,a._)("span",{class:"text-muted-foreground"},"Failed",-1))])):(0,a.kq)("",!0),d.value?((0,a.wg)(),(0,a.iD)("div",Ya,[(0,a._)("div",Oa,[(0,a.Wm)((0,r.SU)(wa.Z),{class:"w-3 h-3 text-white"})]),g[4]||(g[4]=(0,a._)("span",{class:"text-muted-foreground"},"Skipped",-1))])):(0,a.kq)("",!0),c.value?((0,a.wg)(),(0,a.iD)("div",Ka,[(0,a._)("div",Pa,[(0,a.Wm)((0,r.SU)(ha.Z),{class:"w-3 h-3 text-white"})]),g[5]||(g[5]=(0,a._)("span",{class:"text-muted-foreground"},"Always Run",-1))])):(0,a.kq)("",!0)])])]))}};const Ga=Va;var Ba=Ga,Ja=s(293),Qa=s(322);const Xa={class:"flex items-center justify-between p-4 border-b"},en={class:"text-lg font-semibold flex items-center gap-2"},tn={class:"text-sm text-muted-foreground mt-1"},sn={class:"p-4 space-y-4 overflow-y-auto max-h-[60vh]"},ln={key:0,class:"flex flex-wrap gap-2"},an={class:"flex items-center gap-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/30 rounded-lg border border-blue-200 dark:border-blue-700"},nn={key:1,class:"space-y-2"},rn={class:"text-sm font-medium flex items-center gap-2 text-red-600 dark:text-red-400"},on={class:"space-y-2"},un={key:2,class:"space-y-2"},dn={class:"text-sm font-medium flex items-center gap-2"},cn={class:"text-xs font-mono text-muted-foreground"},gn={key:3,class:"space-y-2"},mn={class:"text-sm font-medium flex items-center gap-2"},pn={class:"grid grid-cols-2 gap-4 text-xs"},vn={class:"font-mono mt-1"};var fn={__name:"StepDetailsModal",props:{step:{type:Object,required:!0},index:{type:Number,required:!0}},emits:["close"],setup(e){const t=e,s=(0,a.Fl)((()=>{switch(t.step.status){case"success":return Ve.Z;case"failed":return ks.Z;case"skipped":return wa.Z;case"not-started":return xa.Z;default:return xa.Z}})),o=(0,a.Fl)((()=>{switch(t.step.status){case"success":return"text-green-600 dark:text-green-400";case"failed":return"text-red-600 dark:text-red-400";case"skipped":return"text-gray-600 dark:text-gray-400";default:return"text-blue-600 dark:text-blue-400"}}));return(t,u)=>((0,a.wg)(),(0,a.iD)("div",{class:"fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50",onClick:u[2]||(u[2]=e=>t.$emit("close"))},[(0,a._)("div",{class:"bg-background border rounded-lg shadow-lg max-w-2xl w-full max-h-[80vh] overflow-hidden",onClick:u[1]||(u[1]=(0,l.iM)((()=>{}),["stop"]))},[(0,a._)("div",Xa,[(0,a._)("div",null,[(0,a._)("h2",en,[((0,a.wg)(),(0,a.j4)((0,a.LL)(s.value),{class:(0,n.C_)([o.value,"w-5 h-5"])},null,8,["class"])),(0,a.Uk)(" "+(0,n.zw)(e.step.name),1)]),(0,a._)("p",tn," Step "+(0,n.zw)(e.index+1)+" • "+(0,n.zw)((0,r.SU)(ba)(e.step.duration)),1)]),(0,a.Wm)((0,r.SU)(x),{variant:"ghost",size:"icon",onClick:u[0]||(u[0]=e=>t.$emit("close"))},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(d.Z),{class:"w-4 h-4"})])),_:1})]),(0,a._)("div",sn,[e.step.isAlwaysRun?((0,a.wg)(),(0,a.iD)("div",ln,[(0,a._)("div",an,[(0,a.Wm)((0,r.SU)(ha.Z),{class:"w-4 h-4 text-blue-600 dark:text-blue-400"}),u[3]||(u[3]=(0,a._)("div",null,[(0,a._)("p",{class:"text-sm font-medium text-blue-900 dark:text-blue-200"},"Always Run"),(0,a._)("p",{class:"text-xs text-blue-600 dark:text-blue-400"},"This endpoint is configured to executes even after failures")],-1))])])):(0,a.kq)("",!0),e.step.errors?.length?((0,a.wg)(),(0,a.iD)("div",nn,[(0,a._)("h3",rn,[(0,a.Wm)((0,r.SU)(Oe.Z),{class:"w-4 h-4"}),(0,a.Uk)(" Errors ("+(0,n.zw)(e.step.errors.length)+") ",1)]),(0,a._)("div",on,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(e.step.errors,((e,t)=>((0,a.wg)(),(0,a.iD)("div",{key:t,class:"p-3 bg-red-50 dark:bg-red-900/50 border border-red-200 dark:border-red-700 rounded text-sm font-mono text-red-800 dark:text-red-300 break-all"},(0,n.zw)(e),1)))),128))])])):(0,a.kq)("",!0),e.step.result&&e.step.result.timestamp?((0,a.wg)(),(0,a.iD)("div",un,[(0,a._)("h3",dn,[(0,a.Wm)((0,r.SU)(Ja.Z),{class:"w-4 h-4"}),u[4]||(u[4]=(0,a.Uk)(" Timestamp ",-1))]),(0,a._)("p",cn,(0,n.zw)((0,r.SU)(L)(e.step.result.timestamp)),1)])):(0,a.kq)("",!0),e.step.result?((0,a.wg)(),(0,a.iD)("div",gn,[(0,a._)("h3",mn,[(0,a.Wm)((0,r.SU)(Qa.Z),{class:"w-4 h-4"}),u[5]||(u[5]=(0,a.Uk)(" Response ",-1))]),(0,a._)("div",pn,[(0,a._)("div",null,[u[6]||(u[6]=(0,a._)("span",{class:"text-muted-foreground"},"Duration:",-1)),(0,a._)("p",vn,(0,n.zw)((0,r.SU)(ba)(e.step.result.duration)),1)]),(0,a._)("div",null,[u[7]||(u[7]=(0,a._)("span",{class:"text-muted-foreground"},"Success:",-1)),(0,a._)("p",{class:(0,n.C_)(["mt-1",e.step.result.success?"text-green-600 dark:text-green-400":"text-red-600 dark:text-red-400"])},(0,n.zw)(e.step.result.success?"Yes":"No"),3)])])])):(0,a.kq)("",!0)])])]))}};const wn=fn;var hn=wn;const xn={class:"suite-details-container bg-background min-h-screen"},bn={class:"container mx-auto px-4 py-8 max-w-7xl"},yn={class:"mb-6"},_n={class:"flex items-start justify-between"},kn={class:"text-3xl font-bold tracking-tight"},Sn={class:"text-muted-foreground mt-2"},Dn={key:0},Un={key:1},zn={class:"flex items-center gap-2"},Wn={key:0,class:"flex items-center justify-center py-20"},Cn={key:1,class:"text-center py-20"},jn={key:2,class:"space-y-6"},Hn={class:"space-y-4"},Fn={class:"grid grid-cols-2 md:grid-cols-4 gap-4"},Rn={class:"text-lg font-medium"},$n={class:"text-lg font-medium"},qn={class:"text-lg font-medium"},Tn={class:"text-lg font-medium"},Zn={class:"mt-6"},En={key:0,class:"mt-6"},An={class:"space-y-2"},Ln={key:0,class:"space-y-2"},Nn=["onClick"],Mn={class:"flex items-center gap-3"},In={class:"text-sm font-medium"},Yn={class:"text-xs text-muted-foreground"},On={key:1,class:"text-center py-8 text-muted-foreground"};var Kn={__name:"SuiteDetails",setup(e){const t=(0,u.tv)(),s=(0,u.yj)(),l=(0,r.iH)(!1),o=(0,r.iH)(null),i=(0,r.iH)(null),d=(0,r.iH)(null),c=(0,r.iH)(0),g=(0,a.Fl)((()=>o.value&&o.value.results&&0!==o.value.results.length?[...o.value.results].sort(((e,t)=>new Date(t.timestamp)-new Date(e.timestamp))):[])),m=(0,a.Fl)((()=>o.value&&o.value.results&&0!==o.value.results.length?i.value||g.value[0]:null)),p=async()=>{l.value=!0;try{const e=await fetch(`${Qn}/api/v1/suites/${s.params.key}/statuses`,{credentials:"include"});if(200===e.status){const t=await e.json();if(o.value=t,t.results&&t.results.length>0&&!i.value){const e=[...t.results].sort(((e,t)=>new Date(t.timestamp)-new Date(e.timestamp)));i.value=e[0]}}else 404===e.status?o.value=null:console.error("[SuiteDetails][fetchData] Error:",await e.text())}catch(e){console.error("[SuiteDetails][fetchData] Error:",e)}finally{l.value=!1}},v=()=>{p()},f=()=>{t.push("/")},w=e=>E(e),h=e=>{const t=new Date(e);return t.toLocaleString()},b=e=>{if(!e&&0!==e)return"N/A";const t=e/1e6;return t<1e3?`${t.toFixed(0)}ms`:`${(t/1e3).toFixed(2)}s`},y=e=>{if(!e||!e.endpointResults||0===e.endpointResults.length)return 0;const t=e.endpointResults.filter((e=>e.success)).length;return Math.round(t/e.endpointResults.length*100)},k=(0,a.Fl)((()=>{if(!m.value||!m.value.endpointResults)return[];const e=m.value.endpointResults;return e.map(((t,s)=>{const l=o.value?.endpoints?.[s],a=e[s+1];let n=!1;for(let r=0;rk.value.filter((e=>"success"===e.status)).length)),U=(0,a.Fl)((()=>k.value.length?Math.round(S.value/k.value.length*100):0)),z=e=>e?e.conditionResults&&e.conditionResults.some((e=>e.condition.includes("SKIP")))?"skipped":e.success?"success":"failed":"not-started",C=(e,t)=>{d.value=e,c.value=t};return(0,a.bv)((()=>{p()})),(e,t)=>((0,a.wg)(),(0,a.iD)("div",xn,[(0,a._)("div",bn,[(0,a._)("div",yn,[(0,a.Wm)((0,r.SU)(x),{variant:"ghost",size:"sm",onClick:f,class:"mb-4"},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(Sl.Z),{class:"h-4 w-4 mr-2"}),t[1]||(t[1]=(0,a.Uk)(" Back to Dashboard ",-1))])),_:1,__:[1]}),(0,a._)("div",_n,[(0,a._)("div",null,[(0,a._)("h1",kn,(0,n.zw)(o.value?.name||"Loading..."),1),(0,a._)("p",Sn,[o.value?.group?((0,a.wg)(),(0,a.iD)("span",Dn,(0,n.zw)(o.value.group)+" • ",1)):(0,a.kq)("",!0),m.value?((0,a.wg)(),(0,a.iD)("span",Un,(0,n.zw)(i.value&&i.value!==g.value[0]?"Ran":"Last run")+" "+(0,n.zw)(w(m.value.timestamp)),1)):(0,a.kq)("",!0)])]),(0,a._)("div",zn,[m.value?((0,a.wg)(),(0,a.j4)(st,{key:0,status:m.value.success?"healthy":"unhealthy"},null,8,["status"])):(0,a.kq)("",!0),(0,a.Wm)((0,r.SU)(x),{variant:"ghost",size:"icon",onClick:v,title:"Refresh"},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(Ye.Z),{class:"h-5 w-5"})])),_:1})])])]),l.value?((0,a.wg)(),(0,a.iD)("div",Wn,[(0,a.Wm)(de,{size:"lg"})])):o.value?((0,a.wg)(),(0,a.iD)("div",jn,[m.value?((0,a.wg)(),(0,a.j4)((0,r.SU)(_),{key:0},{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),null,{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(W),null,{default:(0,a.w5)((()=>t[4]||(t[4]=[(0,a.Uk)("Latest Execution",-1)]))),_:1,__:[4]})])),_:1}),(0,a.Wm)((0,r.SU)(H),null,{default:(0,a.w5)((()=>[(0,a._)("div",Hn,[(0,a._)("div",Fn,[(0,a._)("div",null,[t[5]||(t[5]=(0,a._)("p",{class:"text-sm text-muted-foreground"},"Status",-1)),(0,a._)("p",Rn,(0,n.zw)(m.value.success?"Success":"Failed"),1)]),(0,a._)("div",null,[t[6]||(t[6]=(0,a._)("p",{class:"text-sm text-muted-foreground"},"Duration",-1)),(0,a._)("p",$n,(0,n.zw)(b(m.value.duration)),1)]),(0,a._)("div",null,[t[7]||(t[7]=(0,a._)("p",{class:"text-sm text-muted-foreground"},"Endpoints",-1)),(0,a._)("p",qn,(0,n.zw)(m.value.endpointResults?.length||0),1)]),(0,a._)("div",null,[t[8]||(t[8]=(0,a._)("p",{class:"text-sm text-muted-foreground"},"Success Rate",-1)),(0,a._)("p",Tn,(0,n.zw)(y(m.value))+"%",1)])]),(0,a._)("div",Zn,[t[9]||(t[9]=(0,a._)("h3",{class:"text-lg font-semibold mb-4"},"Execution Flow",-1)),(0,a.Wm)(Ba,{"flow-steps":k.value,"progress-percentage":U.value,"completed-steps":S.value,"total-steps":k.value.length,onStepSelected:C},null,8,["flow-steps","progress-percentage","completed-steps","total-steps"])]),m.value.errors&&m.value.errors.length>0?((0,a.wg)(),(0,a.iD)("div",En,[t[10]||(t[10]=(0,a._)("h3",{class:"text-lg font-semibold mb-3 text-red-500"},"Suite Errors",-1)),(0,a._)("div",An,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(m.value.errors,((e,t)=>((0,a.wg)(),(0,a.iD)("div",{key:t,class:"bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300 p-3 rounded-md text-sm"},(0,n.zw)(e),1)))),128))])])):(0,a.kq)("",!0)])])),_:1})])),_:1})):(0,a.kq)("",!0),(0,a.Wm)((0,r.SU)(_),null,{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(D),null,{default:(0,a.w5)((()=>[(0,a.Wm)((0,r.SU)(W),null,{default:(0,a.w5)((()=>t[11]||(t[11]=[(0,a.Uk)("Execution History",-1)]))),_:1,__:[11]})])),_:1}),(0,a.Wm)((0,r.SU)(H),null,{default:(0,a.w5)((()=>[g.value.length>0?((0,a.wg)(),(0,a.iD)("div",Ln,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(g.value,((e,t)=>((0,a.wg)(),(0,a.iD)("div",{key:t,class:(0,n.C_)(["flex items-center justify-between p-3 border rounded-lg hover:bg-accent/50 transition-colors cursor-pointer",{"bg-accent":i.value===e}]),onClick:t=>i.value=e},[(0,a._)("div",Mn,[(0,a.Wm)(st,{status:e.success?"healthy":"unhealthy",size:"sm"},null,8,["status"]),(0,a._)("div",null,[(0,a._)("p",In,(0,n.zw)(h(e.timestamp)),1),(0,a._)("p",Yn,(0,n.zw)(e.endpointResults?.length||0)+" endpoints • "+(0,n.zw)(b(e.duration)),1)])]),(0,a.Wm)((0,r.SU)(Be.Z),{class:"h-4 w-4 text-muted-foreground"})],10,Nn)))),128))])):((0,a.wg)(),(0,a.iD)("div",On," No execution history available "))])),_:1})])),_:1})])):((0,a.wg)(),(0,a.iD)("div",Cn,[(0,a.Wm)((0,r.SU)(Oe.Z),{class:"h-12 w-12 text-muted-foreground mx-auto mb-4"}),t[2]||(t[2]=(0,a._)("h3",{class:"text-lg font-semibold mb-2"},"Suite not found",-1)),t[3]||(t[3]=(0,a._)("p",{class:"text-muted-foreground"},"The requested suite could not be found.",-1))]))]),(0,a.Wm)(_s,{onRefreshData:p}),d.value?((0,a.wg)(),(0,a.j4)(hn,{key:0,step:d.value,index:c.value,onClose:t[0]||(t[0]=e=>d.value=null)},null,8,["step","index"])):(0,a.kq)("",!0)]))}};const Pn=(0,$.Z)(Kn,[["__scopeId","data-v-0c28bc83"]]);var Vn=Pn;const Gn=[{path:"/",name:"Home",component:kl},{path:"/endpoints/:key",name:"EndpointDetails",component:fa},{path:"/suites/:key",name:"SuiteDetails",component:Vn}],Bn=(0,u.p7)({history:(0,u.PO)("/"),routes:Gn});var Jn=Bn;const Qn="";(0,l.ri)(Ne).use(Jn).mount("#app")}},t={};function s(l){var a=t[l];if(void 0!==a)return a.exports;var n=t[l]={exports:{}};return e[l](n,n.exports,s),n.exports}s.m=e,function(){var e=[];s.O=function(t,l,a,n){if(!l){var r=1/0;for(d=0;d=n)&&Object.keys(s.O).every((function(e){return s.O[e](l[u])}))?l.splice(u--,1):(o=!1,n0&&e[d-1][2]>n;d--)e[d]=e[d-1];e[d]=[l,a,n]}}(),function(){s.d=function(e,t){for(var l in t)s.o(t,l)&&!s.o(e,l)&&Object.defineProperty(e,l,{enumerable:!0,get:t[l]})}}(),function(){s.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"===typeof window)return window}}()}(),function(){s.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)}}(),function(){s.p="/"}(),function(){var e={143:0};s.O.j=function(t){return 0===e[t]};var t=function(t,l){var a,n,r=l[0],o=l[1],u=l[2],i=0;if(r.some((function(t){return 0!==e[t]}))){for(a in o)s.o(o,a)&&(s.m[a]=o[a]);if(u)var d=u(s)}for(t&&t(l);i0&&0===--this._on&&(o=this.prevScope,this.prevScope=void 0)}stop(e){if(this._active){let t,n;for(this._active=!1,t=0,n=this.effects.length;t0)return;if(f){let e=f;f=void 0;while(e){const t=e.next;e.next=void 0,e.flags&=-9,e=t}}let e;while(u){let n=u;u=void 0;while(n){const r=n.next;if(n.next=void 0,n.flags&=-9,1&n.flags)try{n.trigger()}catch(t){e||(e=t)}n=r}}if(e)throw e}function g(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function v(e){let t,n=e.depsTail,r=n;while(r){const e=r.prevDep;-1===r.version?(r===n&&(n=e),k(r),w(r)):t=r,r.dep.activeLink=r.prevActiveLink,r.prevActiveLink=void 0,r=e}e.deps=t,e.depsTail=n}function b(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(y(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function y(e){if(4&e.flags&&!(16&e.flags))return;if(e.flags&=-17,e.globalVersion===O)return;if(e.globalVersion=O,!e.isSSR&&128&e.flags&&(!e.deps&&!e._dirty||!b(e)))return;e.flags|=2;const t=e.dep,n=s,o=x;s=e,x=!0;try{g(e);const i=e.fn(e._value);(0===t.version||(0,r.aU)(i,e._value))&&(e.flags|=128,e._value=i,t.version++)}catch(i){throw t.version++,i}finally{s=n,x=o,v(e),e.flags&=-3}}function k(e,t=!1){const{dep:n,prevSub:r,nextSub:o}=e;if(r&&(r.nextSub=o,e.prevSub=void 0),o&&(o.prevSub=r,e.nextSub=void 0),n.subs===e&&(n.subs=r,!r&&n.computed)){n.computed.flags&=-5;for(let e=n.computed.deps;e;e=e.nextDep)k(e,!0)}t||--n.sc||!n.map||n.map.delete(n.key)}function w(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let x=!0;const _=[];function S(){_.push(x),x=!1}function C(){const e=_.pop();x=void 0===e||e}function E(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const e=s;s=void 0;try{t()}finally{s=e}}}let O=0;class R{constructor(e,t){this.sub=e,this.dep=t,this.version=t.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class j{constructor(e){this.computed=e,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(e){if(!s||!x||s===this.computed)return;let t=this.activeLink;if(void 0===t||t.sub!==s)t=this.activeLink=new R(s,this),s.deps?(t.prevDep=s.depsTail,s.depsTail.nextDep=t,s.depsTail=t):s.deps=s.depsTail=t,M(t);else if(-1===t.version&&(t.version=this.version,t.nextDep)){const e=t.nextDep;e.prevDep=t.prevDep,t.prevDep&&(t.prevDep.nextDep=e),t.prevDep=s.depsTail,t.nextDep=void 0,s.depsTail.nextDep=t,s.depsTail=t,s.deps===t&&(s.deps=e)}return t}trigger(e){this.version++,O++,this.notify(e)}notify(e){h();try{0;for(let e=this.subs;e;e=e.prevSub)e.sub.notify()&&e.sub.dep.notify()}finally{m()}}}function M(e){if(e.dep.sc++,4&e.sub.flags){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let e=t.deps;e;e=e.nextDep)M(e)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const A=new WeakMap,I=Symbol(""),T=Symbol(""),P=Symbol("");function F(e,t,n){if(x&&s){let t=A.get(e);t||A.set(e,t=new Map);let r=t.get(n);r||(t.set(n,r=new j),r.map=t,r.key=n),r.track()}}function D(e,t,n,o,s,i){const l=A.get(e);if(!l)return void O++;const c=e=>{e&&e.trigger()};if(h(),"clear"===t)l.forEach(c);else{const s=(0,r.kJ)(e),i=s&&(0,r.S0)(n);if(s&&"length"===n){const e=Number(o);l.forEach(((t,n)=>{("length"===n||n===P||!(0,r.yk)(n)&&n>=e)&&c(t)}))}else switch((void 0!==n||l.has(void 0))&&c(l.get(n)),i&&c(l.get(P)),t){case"add":s?i&&c(l.get("length")):(c(l.get(I)),(0,r._N)(e)&&c(l.get(T)));break;case"delete":s||(c(l.get(I)),(0,r._N)(e)&&c(l.get(T)));break;case"set":(0,r._N)(e)&&c(l.get(I));break}}m()}function N(e){const t=Se(e);return t===e?t:(F(t,"iterate",P),xe(e)?t:t.map(Ee))}function $(e){return F(e=Se(e),"iterate",P),e}const L={__proto__:null,[Symbol.iterator](){return U(this,Symbol.iterator,Ee)},concat(...e){return N(this).concat(...e.map((e=>(0,r.kJ)(e)?N(e):e)))},entries(){return U(this,"entries",(e=>(e[1]=Ee(e[1]),e)))},every(e,t){return J(this,"every",e,t,void 0,arguments)},filter(e,t){return J(this,"filter",e,t,(e=>e.map(Ee)),arguments)},find(e,t){return J(this,"find",e,t,Ee,arguments)},findIndex(e,t){return J(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return J(this,"findLast",e,t,Ee,arguments)},findLastIndex(e,t){return J(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return J(this,"forEach",e,t,void 0,arguments)},includes(...e){return V(this,"includes",e)},indexOf(...e){return V(this,"indexOf",e)},join(e){return N(this).join(e)},lastIndexOf(...e){return V(this,"lastIndexOf",e)},map(e,t){return J(this,"map",e,t,void 0,arguments)},pop(){return H(this,"pop")},push(...e){return H(this,"push",e)},reduce(e,...t){return Z(this,"reduce",e,t)},reduceRight(e,...t){return Z(this,"reduceRight",e,t)},shift(){return H(this,"shift")},some(e,t){return J(this,"some",e,t,void 0,arguments)},splice(...e){return H(this,"splice",e)},toReversed(){return N(this).toReversed()},toSorted(e){return N(this).toSorted(e)},toSpliced(...e){return N(this).toSpliced(...e)},unshift(...e){return H(this,"unshift",e)},values(){return U(this,"values",Ee)}};function U(e,t,n){const r=$(e),o=r[t]();return r===e||xe(e)||(o._next=o.next,o.next=()=>{const e=o._next();return e.value&&(e.value=n(e.value)),e}),o}const z=Array.prototype;function J(e,t,n,r,o,s){const i=$(e),l=i!==e&&!xe(e),c=i[t];if(c!==z[t]){const t=c.apply(e,s);return l?Ee(t):t}let a=n;i!==e&&(l?a=function(t,r){return n.call(this,Ee(t),r,e)}:n.length>2&&(a=function(t,r){return n.call(this,t,r,e)}));const u=c.call(i,a,r);return l&&o?o(u):u}function Z(e,t,n,r){const o=$(e);let s=n;return o!==e&&(xe(e)?n.length>3&&(s=function(t,r,o){return n.call(this,t,r,o,e)}):s=function(t,r,o){return n.call(this,t,Ee(r),o,e)}),o[t](s,...r)}function V(e,t,n){const r=Se(e);F(r,"iterate",P);const o=r[t](...n);return-1!==o&&!1!==o||!_e(n[0])?o:(n[0]=Se(n[0]),r[t](...n))}function H(e,t,n=[]){S(),h();const r=Se(e)[t].apply(e,n);return m(),C(),r}const B=(0,r.fY)("__proto__,__v_isRef,__isVue"),G=new Set(Object.getOwnPropertyNames(Symbol).filter((e=>"arguments"!==e&&"caller"!==e)).map((e=>Symbol[e])).filter(r.yk));function q(e){(0,r.yk)(e)||(e=String(e));const t=Se(this);return F(t,"has",e),t.hasOwnProperty(e)}class W{constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}get(e,t,n){if("__v_skip"===t)return e["__v_skip"];const o=this._isReadonly,s=this._isShallow;if("__v_isReactive"===t)return!o;if("__v_isReadonly"===t)return o;if("__v_isShallow"===t)return s;if("__v_raw"===t)return n===(o?s?pe:de:s?fe:ue).get(e)||Object.getPrototypeOf(e)===Object.getPrototypeOf(n)?e:void 0;const i=(0,r.kJ)(e);if(!o){let e;if(i&&(e=L[t]))return e;if("hasOwnProperty"===t)return q}const l=Reflect.get(e,t,Re(e)?e:n);return((0,r.yk)(t)?G.has(t):B(t))?l:(o||F(e,"get",t),s?l:Re(l)?i&&(0,r.S0)(t)?l:l.value:(0,r.Kn)(l)?o?be(l):ge(l):l)}}class K extends W{constructor(e=!1){super(!1,e)}set(e,t,n,o){let s=e[t];if(!this._isShallow){const t=we(s);if(xe(n)||we(n)||(s=Se(s),n=Se(n)),!(0,r.kJ)(e)&&Re(s)&&!Re(n))return!t&&(s.value=n,!0)}const i=(0,r.kJ)(e)&&(0,r.S0)(t)?Number(t)e,ne=e=>Reflect.getPrototypeOf(e);function re(e,t,n){return function(...o){const s=this["__v_raw"],i=Se(s),l=(0,r._N)(i),c="entries"===e||e===Symbol.iterator&&l,a="keys"===e&&l,u=s[e](...o),f=n?te:t?Oe:Ee;return!t&&F(i,"iterate",a?T:I),{next(){const{value:e,done:t}=u.next();return t?{value:e,done:t}:{value:c?[f(e[0]),f(e[1])]:f(e),done:t}},[Symbol.iterator](){return this}}}}function oe(e){return function(...t){return"delete"!==e&&("clear"===e?void 0:this)}}function se(e,t){const n={get(n){const o=this["__v_raw"],s=Se(o),i=Se(n);e||((0,r.aU)(n,i)&&F(s,"get",n),F(s,"get",i));const{has:l}=ne(s),c=t?te:e?Oe:Ee;return l.call(s,n)?c(o.get(n)):l.call(s,i)?c(o.get(i)):void(o!==s&&o.get(n))},get size(){const t=this["__v_raw"];return!e&&F(Se(t),"iterate",I),Reflect.get(t,"size",t)},has(t){const n=this["__v_raw"],o=Se(n),s=Se(t);return e||((0,r.aU)(t,s)&&F(o,"has",t),F(o,"has",s)),t===s?n.has(t):n.has(t)||n.has(s)},forEach(n,r){const o=this,s=o["__v_raw"],i=Se(s),l=t?te:e?Oe:Ee;return!e&&F(i,"iterate",I),s.forEach(((e,t)=>n.call(r,l(e),l(t),o)))}};(0,r.l7)(n,e?{add:oe("add"),set:oe("set"),delete:oe("delete"),clear:oe("clear")}:{add(e){t||xe(e)||we(e)||(e=Se(e));const n=Se(this),r=ne(n),o=r.has.call(n,e);return o||(n.add(e),D(n,"add",e,e)),this},set(e,n){t||xe(n)||we(n)||(n=Se(n));const o=Se(this),{has:s,get:i}=ne(o);let l=s.call(o,e);l||(e=Se(e),l=s.call(o,e));const c=i.call(o,e);return o.set(e,n),l?(0,r.aU)(n,c)&&D(o,"set",e,n,c):D(o,"add",e,n),this},delete(e){const t=Se(this),{has:n,get:r}=ne(t);let o=n.call(t,e);o||(e=Se(e),o=n.call(t,e));const s=r?r.call(t,e):void 0,i=t.delete(e);return o&&D(t,"delete",e,void 0,s),i},clear(){const e=Se(this),t=0!==e.size,n=void 0,r=e.clear();return t&&D(e,"clear",void 0,void 0,n),r}});const o=["keys","values","entries",Symbol.iterator];return o.forEach((r=>{n[r]=re(r,e,t)})),n}function ie(e,t){const n=se(e,t);return(t,o,s)=>"__v_isReactive"===o?!e:"__v_isReadonly"===o?e:"__v_raw"===o?t:Reflect.get((0,r.RI)(n,o)&&o in t?n:t,o,s)}const le={get:ie(!1,!1)},ce={get:ie(!1,!0)},ae={get:ie(!0,!1)};const ue=new WeakMap,fe=new WeakMap,de=new WeakMap,pe=new WeakMap;function he(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function me(e){return e["__v_skip"]||!Object.isExtensible(e)?0:he((0,r.W7)(e))}function ge(e){return we(e)?e:ye(e,!1,X,le,ue)}function ve(e){return ye(e,!1,ee,ce,fe)}function be(e){return ye(e,!0,Q,ae,de)}function ye(e,t,n,o,s){if(!(0,r.Kn)(e))return e;if(e["__v_raw"]&&(!t||!e["__v_isReactive"]))return e;const i=me(e);if(0===i)return e;const l=s.get(e);if(l)return l;const c=new Proxy(e,2===i?o:n);return s.set(e,c),c}function ke(e){return we(e)?ke(e["__v_raw"]):!(!e||!e["__v_isReactive"])}function we(e){return!(!e||!e["__v_isReadonly"])}function xe(e){return!(!e||!e["__v_isShallow"])}function _e(e){return!!e&&!!e["__v_raw"]}function Se(e){const t=e&&e["__v_raw"];return t?Se(t):e}function Ce(e){return!(0,r.RI)(e,"__v_skip")&&Object.isExtensible(e)&&(0,r.Nj)(e,"__v_skip",!0),e}const Ee=e=>(0,r.Kn)(e)?ge(e):e,Oe=e=>(0,r.Kn)(e)?be(e):e;function Re(e){return!!e&&!0===e["__v_isRef"]}function je(e){return Ae(e,!1)}function Me(e){return Ae(e,!0)}function Ae(e,t){return Re(e)?e:new Ie(e,t)}class Ie{constructor(e,t){this.dep=new j,this["__v_isRef"]=!0,this["__v_isShallow"]=!1,this._rawValue=t?e:Se(e),this._value=t?e:Ee(e),this["__v_isShallow"]=t}get value(){return this.dep.track(),this._value}set value(e){const t=this._rawValue,n=this["__v_isShallow"]||xe(e)||we(e);e=n?e:Se(e),(0,r.aU)(e,t)&&(this._rawValue=e,this._value=n?e:Ee(e),this.dep.trigger())}}function Te(e){return Re(e)?e.value:e}const Pe={get:(e,t,n)=>"__v_raw"===t?e:Te(Reflect.get(e,t,n)),set:(e,t,n,r)=>{const o=e[t];return Re(o)&&!Re(n)?(o.value=n,!0):Reflect.set(e,t,n,r)}};function Fe(e){return ke(e)?e:new Proxy(e,Pe)}class De{constructor(e,t,n){this.fn=e,this.setter=t,this._value=void 0,this.dep=new j(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=O-1,this.next=void 0,this.effect=this,this["__v_isReadonly"]=!t,this.isSSR=n}notify(){if(this.flags|=16,!(8&this.flags||s===this))return p(this,!0),!0}get value(){const e=this.dep.track();return y(this),e&&(e.version=this.dep.version),this._value}set value(e){this.setter&&this.setter(e)}}function Ne(e,t,n=!1){let o,s;(0,r.mf)(e)?o=e:(o=e.get,s=e.set);const i=new De(o,s,n);return i}const $e={},Le=new WeakMap;let Ue;function ze(e,t=!1,n=Ue){if(n){let t=Le.get(n);t||Le.set(n,t=[]),t.push(e)}else 0}function Je(e,t,n=r.kT){const{immediate:o,deep:s,once:i,scheduler:c,augmentJob:u,call:f}=n,d=e=>s?e:xe(e)||!1===s||0===s?Ze(e,1):Ze(e);let p,h,m,g,v=!1,b=!1;if(Re(e)?(h=()=>e.value,v=xe(e)):ke(e)?(h=()=>d(e),v=!0):(0,r.kJ)(e)?(b=!0,v=e.some((e=>ke(e)||xe(e))),h=()=>e.map((e=>Re(e)?e.value:ke(e)?d(e):(0,r.mf)(e)?f?f(e,2):e():void 0))):h=(0,r.mf)(e)?t?f?()=>f(e,2):e:()=>{if(m){S();try{m()}finally{C()}}const t=Ue;Ue=p;try{return f?f(e,3,[g]):e(g)}finally{Ue=t}}:r.dG,t&&s){const e=h,t=!0===s?1/0:s;h=()=>Ze(e(),t)}const y=l(),k=()=>{p.stop(),y&&y.active&&(0,r.Od)(y.effects,p)};if(i&&t){const e=t;t=(...t)=>{e(...t),k()}}let w=b?new Array(e.length).fill($e):$e;const x=e=>{if(1&p.flags&&(p.dirty||e))if(t){const e=p.run();if(s||v||(b?e.some(((e,t)=>(0,r.aU)(e,w[t]))):(0,r.aU)(e,w))){m&&m();const n=Ue;Ue=p;try{const r=[e,w===$e?void 0:b&&w[0]===$e?[]:w,g];w=e,f?f(t,3,r):t(...r)}finally{Ue=n}}}else p.run()};return u&&u(x),p=new a(h),p.scheduler=c?()=>c(x,!1):x,g=e=>ze(e,!1,p),m=p.onStop=()=>{const e=Le.get(p);if(e){if(f)f(e,4);else for(const t of e)t();Le.delete(p)}},t?o?x(!0):w=p.run():c?c(x.bind(null,!0),!0):p.run(),k.pause=p.pause.bind(p),k.resume=p.resume.bind(p),k.stop=k,k}function Ze(e,t=1/0,n){if(t<=0||!(0,r.Kn)(e)||e["__v_skip"])return e;if(n=n||new Set,n.has(e))return e;if(n.add(e),t--,Re(e))Ze(e.value,t,n);else if((0,r.kJ)(e))for(let r=0;r{Ze(e,t,n)}));else if((0,r.PO)(e)){for(const r in e)Ze(e[r],t,n);for(const r of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,r)&&Ze(e[r],t,n)}return e}},252:function(e,t,n){n.d(t,{$d:function(){return i},Ah:function(){return le},FN:function(){return kn},Fl:function(){return $n},HY:function(){return Ut},JJ:function(){return He},Ko:function(){return be},LL:function(){return me},Q6:function(){return z},U2:function(){return L},Uk:function(){return an},Us:function(){return pt},WI:function(){return ye},Wm:function(){return on},Y3:function(){return g},Y8:function(){return F},YP:function(){return St},_:function(){return rn},aZ:function(){return J},bv:function(){return re},f3:function(){return Be},h:function(){return Ln},iD:function(){return Yt},ic:function(){return se},j4:function(){return Xt},kq:function(){return un},nJ:function(){return N},nK:function(){return U},up:function(){return pe},w5:function(){return R},wg:function(){return Bt},wy:function(){return j}});var r=n(262),o=n(577);function s(e,t,n,r){try{return r?e(...r):e()}catch(o){l(o,t,n)}}function i(e,t,n,r){if((0,o.mf)(e)){const i=s(e,t,n,r);return i&&(0,o.tI)(i)&&i.catch((e=>{l(e,t,n)})),i}if((0,o.kJ)(e)){const o=[];for(let s=0;s>>1,o=a[r],s=_(o);s=_(n)?a.push(e):a.splice(v(t),0,e),e.flags|=1,y()}}function y(){m||(m=h.then(S))}function k(e){(0,o.kJ)(e)?f.push(...e):d&&-1===e.id?d.splice(p+1,0,e):1&e.flags||(f.push(e),e.flags|=1),y()}function w(e,t,n=u+1){for(0;n_(e)-_(t)));if(f.length=0,d)return void d.push(...e);for(d=e,p=0;pnull==e.id?2&e.flags?-1:1/0:e.id;function S(e){o.dG;try{for(u=0;u{r._d&&Wt(-1);const o=O(t);let s;try{s=e(...n)}finally{O(o),r._d&&Wt(1)}return s};return r._n=!0,r._c=!0,r._d=!0,r}function j(e,t){if(null===C)return e;const n=Fn(C),s=e.dirs||(e.dirs=[]);for(let i=0;ie.__isTeleport;const T=Symbol("_leaveCb"),P=Symbol("_enterCb");function F(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return re((()=>{e.isMounted=!0})),ie((()=>{e.isUnmounting=!0})),e}const D=[Function,Array],N={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:D,onEnter:D,onAfterEnter:D,onEnterCancelled:D,onBeforeLeave:D,onLeave:D,onAfterLeave:D,onLeaveCancelled:D,onBeforeAppear:D,onAppear:D,onAfterAppear:D,onAppearCancelled:D};function $(e,t){const{leavingVNodes:n}=e;let r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function L(e,t,n,r,s){const{appear:l,mode:c,persisted:a=!1,onBeforeEnter:u,onEnter:f,onAfterEnter:d,onEnterCancelled:p,onBeforeLeave:h,onLeave:m,onAfterLeave:g,onLeaveCancelled:v,onBeforeAppear:b,onAppear:y,onAfterAppear:k,onAppearCancelled:w}=t,x=String(e.key),_=$(n,e),S=(e,t)=>{e&&i(e,r,9,t)},C=(e,t)=>{const n=t[1];S(e,t),(0,o.kJ)(e)?e.every((e=>e.length<=1))&&n():e.length<=1&&n()},E={mode:c,persisted:a,beforeEnter(t){let r=u;if(!n.isMounted){if(!l)return;r=b||u}t[T]&&t[T](!0);const o=_[x];o&&en(e,o)&&o.el[T]&&o.el[T](),S(r,[t])},enter(e){let t=f,r=d,o=p;if(!n.isMounted){if(!l)return;t=y||f,r=k||d,o=w||p}let s=!1;const i=e[P]=t=>{s||(s=!0,S(t?o:r,[e]),E.delayedLeave&&E.delayedLeave(),e[P]=void 0)};t?C(t,[e,i]):i()},leave(t,r){const o=String(e.key);if(t[P]&&t[P](!0),n.isUnmounting)return r();S(h,[t]);let s=!1;const i=t[T]=n=>{s||(s=!0,r(),S(n?v:g,[t]),t[T]=void 0,_[o]===e&&delete _[o])};_[o]=e,m?C(m,[t,i]):i()},clone(e){const o=L(e,t,n,r,s);return s&&s(o),o}};return E}function U(e,t){6&e.shapeFlag&&e.component?(e.transition=t,U(e.component.subTree,t)):128&e.shapeFlag?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function z(e,t=!1,n){let r=[],o=0;for(let s=0;s1)for(let s=0;s(0,o.l7)({name:e.name},t,{setup:e}))():e}function Z(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}function V(e,t,n,i,l=!1){if((0,o.kJ)(e))return void e.forEach(((e,r)=>V(e,t&&((0,o.kJ)(t)?t[r]:t),n,i,l)));if(H(i)&&!l)return void(512&i.shapeFlag&&i.type.__asyncResolved&&i.component.subTree.component&&V(e,t,n,i.component.subTree));const c=4&i.shapeFlag?Fn(i.component):i.el,a=l?null:c,{i:u,r:f}=e;const d=t&&t.r,p=u.refs===o.kT?u.refs={}:u.refs,h=u.setupState,m=(0,r.IU)(h),g=h===o.kT?()=>!1:e=>(0,o.RI)(m,e);if(null!=d&&d!==f&&((0,o.HD)(d)?(p[d]=null,g(d)&&(h[d]=null)):(0,r.dq)(d)&&(d.value=null)),(0,o.mf)(f))s(f,u,12,[a,p]);else{const t=(0,o.HD)(f),s=(0,r.dq)(f);if(t||s){const r=()=>{if(e.f){const n=t?g(f)?h[f]:p[f]:f.value;l?(0,o.kJ)(n)&&(0,o.Od)(n,c):(0,o.kJ)(n)?n.includes(c)||n.push(c):t?(p[f]=[c],g(f)&&(h[f]=p[f])):(f.value=[c],e.k&&(p[e.k]=f.value))}else t?(p[f]=a,g(f)&&(h[f]=a)):s&&(f.value=a,e.k&&(p[e.k]=a))};a?(r.id=-1,dt(r,n)):r()}else 0}}(0,o.E9)().requestIdleCallback,(0,o.E9)().cancelIdleCallback;const H=e=>!!e.type.__asyncLoader -/*! #__NO_SIDE_EFFECTS__ */;const B=e=>e.type.__isKeepAlive;RegExp,RegExp;function G(e,t){return(0,o.kJ)(e)?e.some((e=>G(e,t))):(0,o.HD)(e)?e.split(",").includes(t):!!(0,o.Kj)(e)&&(e.lastIndex=0,e.test(t))}function q(e,t){K(e,"a",t)}function W(e,t){K(e,"da",t)}function K(e,t,n=yn){const r=e.__wdc||(e.__wdc=()=>{let t=n;while(t){if(t.isDeactivated)return;t=t.parent}return e()});if(ee(t,r,n),n){let e=n.parent;while(e&&e.parent)B(e.parent.vnode)&&Y(r,t,n,e),e=e.parent}}function Y(e,t,n,r){const s=ee(t,e,r,!0);le((()=>{(0,o.Od)(r[t],s)}),n)}function X(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function Q(e){return 128&e.shapeFlag?e.ssContent:e}function ee(e,t,n=yn,o=!1){if(n){const s=n[e]||(n[e]=[]),l=t.__weh||(t.__weh=(...o)=>{(0,r.Jd)();const s=_n(n),l=i(t,n,e,o);return s(),(0,r.lk)(),l});return o?s.unshift(l):s.push(l),l}}const te=e=>(t,n=yn)=>{Rn&&"sp"!==e||ee(e,((...e)=>t(...e)),n)},ne=te("bm"),re=te("m"),oe=te("bu"),se=te("u"),ie=te("bum"),le=te("um"),ce=te("sp"),ae=te("rtg"),ue=te("rtc");function fe(e,t=yn){ee("ec",e,t)}const de="components";function pe(e,t){return ge(de,e,!0,t)||e}const he=Symbol.for("v-ndc");function me(e){return(0,o.HD)(e)?ge(de,e,!1)||e:e||he}function ge(e,t,n=!0,r=!1){const s=C||yn;if(s){const n=s.type;if(e===de){const e=Dn(n,!1);if(e&&(e===t||e===(0,o._A)(t)||e===(0,o.kC)((0,o._A)(t))))return n}const i=ve(s[e]||n[e],t)||ve(s.appContext[e],t);return!i&&r?n:i}}function ve(e,t){return e&&(e[t]||e[(0,o._A)(t)]||e[(0,o.kC)((0,o._A)(t))])}function be(e,t,n,s){let i;const l=n&&n[s],c=(0,o.kJ)(e);if(c||(0,o.HD)(e)){const n=c&&(0,r.PG)(e);let o=!1,s=!1;n&&(o=!(0,r.yT)(e),s=(0,r.$y)(e),e=(0,r.XB)(e)),i=new Array(e.length);for(let c=0,a=e.length;ct(e,n,void 0,l&&l[n])));else{const n=Object.keys(e);i=new Array(n.length);for(let r=0,o=n.length;r!Qt(e)||e.type!==Jt&&!(e.type===Ut&&!ke(e.children))))?e:null}const we=e=>e?Cn(e)?Fn(e):we(e.parent):null,xe=(0,o.l7)(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>we(e.parent),$root:e=>we(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Ae(e),$forceUpdate:e=>e.f||(e.f=()=>{b(e.update)}),$nextTick:e=>e.n||(e.n=g.bind(e.proxy)),$watch:e=>Et.bind(e)}),_e=(e,t)=>e!==o.kT&&!e.__isScriptSetup&&(0,o.RI)(e,t),Se={get({_:e},t){if("__v_skip"===t)return!0;const{ctx:n,setupState:s,data:i,props:l,accessCache:c,type:a,appContext:u}=e;let f;if("$"!==t[0]){const r=c[t];if(void 0!==r)switch(r){case 1:return s[t];case 2:return i[t];case 4:return n[t];case 3:return l[t]}else{if(_e(s,t))return c[t]=1,s[t];if(i!==o.kT&&(0,o.RI)(i,t))return c[t]=2,i[t];if((f=e.propsOptions[0])&&(0,o.RI)(f,t))return c[t]=3,l[t];if(n!==o.kT&&(0,o.RI)(n,t))return c[t]=4,n[t];Ee&&(c[t]=0)}}const d=xe[t];let p,h;return d?("$attrs"===t&&(0,r.j)(e.attrs,"get",""),d(e)):(p=a.__cssModules)&&(p=p[t])?p:n!==o.kT&&(0,o.RI)(n,t)?(c[t]=4,n[t]):(h=u.config.globalProperties,(0,o.RI)(h,t)?h[t]:void 0)},set({_:e},t,n){const{data:r,setupState:s,ctx:i}=e;return _e(s,t)?(s[t]=n,!0):r!==o.kT&&(0,o.RI)(r,t)?(r[t]=n,!0):!(0,o.RI)(e.props,t)&&(("$"!==t[0]||!(t.slice(1)in e))&&(i[t]=n,!0))},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:s,propsOptions:i}},l){let c;return!!n[l]||e!==o.kT&&(0,o.RI)(e,l)||_e(t,l)||(c=i[0])&&(0,o.RI)(c,l)||(0,o.RI)(r,l)||(0,o.RI)(xe,l)||(0,o.RI)(s.config.globalProperties,l)},defineProperty(e,t,n){return null!=n.get?e._.accessCache[t]=0:(0,o.RI)(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Ce(e){return(0,o.kJ)(e)?e.reduce(((e,t)=>(e[t]=null,e)),{}):e}let Ee=!0;function Oe(e){const t=Ae(e),n=e.proxy,s=e.ctx;Ee=!1,t.beforeCreate&&je(t.beforeCreate,e,"bc");const{data:i,computed:l,methods:c,watch:a,provide:u,inject:f,created:d,beforeMount:p,mounted:h,beforeUpdate:m,updated:g,activated:v,deactivated:b,beforeDestroy:y,beforeUnmount:k,destroyed:w,unmounted:x,render:_,renderTracked:S,renderTriggered:C,errorCaptured:E,serverPrefetch:O,expose:R,inheritAttrs:j,components:M,directives:A,filters:I}=t,T=null;if(f&&Re(f,s,T),c)for(const r in c){const e=c[r];(0,o.mf)(e)&&(s[r]=e.bind(n))}if(i){0;const t=i.call(n,n);0,(0,o.Kn)(t)&&(e.data=(0,r.qj)(t))}if(Ee=!0,l)for(const r in l){const e=l[r],t=(0,o.mf)(e)?e.bind(n,n):(0,o.mf)(e.get)?e.get.bind(n,n):o.dG;0;const i=!(0,o.mf)(e)&&(0,o.mf)(e.set)?e.set.bind(n):o.dG,c=$n({get:t,set:i});Object.defineProperty(s,r,{enumerable:!0,configurable:!0,get:()=>c.value,set:e=>c.value=e})}if(a)for(const r in a)Me(a[r],s,n,r);if(u){const e=(0,o.mf)(u)?u.call(n):u;Reflect.ownKeys(e).forEach((t=>{He(t,e[t])}))}function P(e,t){(0,o.kJ)(t)?t.forEach((t=>e(t.bind(n)))):t&&e(t.bind(n))}if(d&&je(d,e,"c"),P(ne,p),P(re,h),P(oe,m),P(se,g),P(q,v),P(W,b),P(fe,E),P(ue,S),P(ae,C),P(ie,k),P(le,x),P(ce,O),(0,o.kJ)(R))if(R.length){const t=e.exposed||(e.exposed={});R.forEach((e=>{Object.defineProperty(t,e,{get:()=>n[e],set:t=>n[e]=t,enumerable:!0})}))}else e.exposed||(e.exposed={});_&&e.render===o.dG&&(e.render=_),null!=j&&(e.inheritAttrs=j),M&&(e.components=M),A&&(e.directives=A),O&&Z(e)}function Re(e,t,n=o.dG){(0,o.kJ)(e)&&(e=De(e));for(const s in e){const n=e[s];let i;i=(0,o.Kn)(n)?"default"in n?Be(n.from||s,n.default,!0):Be(n.from||s):Be(n),(0,r.dq)(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:e=>i.value=e}):t[s]=i}}function je(e,t,n){i((0,o.kJ)(e)?e.map((e=>e.bind(t.proxy))):e.bind(t.proxy),t,n)}function Me(e,t,n,r){let s=r.includes(".")?Ot(n,r):()=>n[r];if((0,o.HD)(e)){const n=t[e];(0,o.mf)(n)&&St(s,n)}else if((0,o.mf)(e))St(s,e.bind(n));else if((0,o.Kn)(e))if((0,o.kJ)(e))e.forEach((e=>Me(e,t,n,r)));else{const r=(0,o.mf)(e.handler)?e.handler.bind(n):t[e.handler];(0,o.mf)(r)&&St(s,r,e)}else 0}function Ae(e){const t=e.type,{mixins:n,extends:r}=t,{mixins:s,optionsCache:i,config:{optionMergeStrategies:l}}=e.appContext,c=i.get(t);let a;return c?a=c:s.length||n||r?(a={},s.length&&s.forEach((e=>Ie(a,e,l,!0))),Ie(a,t,l)):a=t,(0,o.Kn)(t)&&i.set(t,a),a}function Ie(e,t,n,r=!1){const{mixins:o,extends:s}=t;s&&Ie(e,s,n,!0),o&&o.forEach((t=>Ie(e,t,n,!0)));for(const i in t)if(r&&"expose"===i);else{const r=Te[i]||n&&n[i];e[i]=r?r(e[i],t[i]):t[i]}return e}const Te={data:Pe,props:Le,emits:Le,methods:$e,computed:$e,beforeCreate:Ne,created:Ne,beforeMount:Ne,mounted:Ne,beforeUpdate:Ne,updated:Ne,beforeDestroy:Ne,beforeUnmount:Ne,destroyed:Ne,unmounted:Ne,activated:Ne,deactivated:Ne,errorCaptured:Ne,serverPrefetch:Ne,components:$e,directives:$e,watch:Ue,provide:Pe,inject:Fe};function Pe(e,t){return t?e?function(){return(0,o.l7)((0,o.mf)(e)?e.call(this,this):e,(0,o.mf)(t)?t.call(this,this):t)}:t:e}function Fe(e,t){return $e(De(e),De(t))}function De(e){if((0,o.kJ)(e)){const t={};for(let n=0;n1)return n&&(0,o.mf)(t)?t.call(r&&r.proxy):t}else 0}const Ge={},qe=()=>Object.create(Ge),We=e=>Object.getPrototypeOf(e)===Ge;function Ke(e,t,n,o=!1){const s={},i=qe();e.propsDefaults=Object.create(null),Xe(e,t,s,i);for(const r in e.propsOptions[0])r in s||(s[r]=void 0);n?e.props=o?s:(0,r.Um)(s):e.type.props?e.props=s:e.props=i,e.attrs=i}function Ye(e,t,n,s){const{props:i,attrs:l,vnode:{patchFlag:c}}=e,a=(0,r.IU)(i),[u]=e.propsOptions;let f=!1;if(!(s||c>0)||16&c){let r;Xe(e,t,i,l)&&(f=!0);for(const s in a)t&&((0,o.RI)(t,s)||(r=(0,o.rs)(s))!==s&&(0,o.RI)(t,r))||(u?!n||void 0===n[s]&&void 0===n[r]||(i[s]=Qe(u,a,s,void 0,e,!0)):delete i[s]);if(l!==a)for(const e in l)t&&(0,o.RI)(t,e)||(delete l[e],f=!0)}else if(8&c){const n=e.vnode.dynamicProps;for(let r=0;r{a=!0;const[n,r]=tt(e,t,!0);(0,o.l7)(l,n),r&&c.push(...r)};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}if(!i&&!a)return(0,o.Kn)(e)&&r.set(e,o.Z6),o.Z6;if((0,o.kJ)(i))for(let f=0;f"_"===e||"__"===e||"_ctx"===e||"$stable"===e,ot=e=>(0,o.kJ)(e)?e.map(fn):[fn(e)],st=(e,t,n)=>{if(t._n)return t;const r=R(((...e)=>ot(t(...e))),n);return r._c=!1,r},it=(e,t,n)=>{const r=e._ctx;for(const s in e){if(rt(s))continue;const n=e[s];if((0,o.mf)(n))t[s]=st(s,n,r);else if(null!=n){0;const e=ot(n);t[s]=()=>e}}},lt=(e,t)=>{const n=ot(t);e.slots.default=()=>n},ct=(e,t,n)=>{for(const r in t)!n&&rt(r)||(e[r]=t[r])},at=(e,t,n)=>{const r=e.slots=qe();if(32&e.vnode.shapeFlag){const e=t.__;e&&(0,o.Nj)(r,"__",e,!0);const s=t._;s?(ct(r,t,n),n&&(0,o.Nj)(r,"_",s,!0)):it(t,r)}else t&<(e,t)},ut=(e,t,n)=>{const{vnode:r,slots:s}=e;let i=!0,l=o.kT;if(32&r.shapeFlag){const e=t._;e?n&&1===e?i=!1:ct(s,t,n):(i=!t.$stable,it(t,s)),l=t}else t&&(lt(e,t),l={default:1});if(i)for(const o in s)rt(o)||null!=l[o]||delete s[o]};function ft(){"boolean"!==typeof __VUE_PROD_HYDRATION_MISMATCH_DETAILS__&&((0,o.E9)().__VUE_PROD_HYDRATION_MISMATCH_DETAILS__=!1)}const dt=Lt;function pt(e){return ht(e)}function ht(e,t){ft();const n=(0,o.E9)();n.__VUE__=!0;const{insert:s,remove:i,patchProp:l,createElement:c,createText:a,createComment:u,setText:f,setElementText:d,parentNode:p,nextSibling:h,setScopeId:m=o.dG,insertStaticContent:g}=e,v=(e,t,n,r=null,o=null,s=null,i,l=null,c=!!t.dynamicChildren)=>{if(e===t)return;e&&!en(e,t)&&(r=Q(e),q(e,o,s,!0),e=null),-2===t.patchFlag&&(c=!1,t.dynamicChildren=null);const{type:a,ref:u,shapeFlag:f}=t;switch(a){case zt:y(e,t,n,r);break;case Jt:k(e,t,n,r);break;case Zt:null==e&&_(t,n,r,i);break;case Ut:F(e,t,n,r,o,s,i,l,c);break;default:1&f?E(e,t,n,r,o,s,i,l,c):6&f?D(e,t,n,r,o,s,i,l,c):(64&f||128&f)&&a.process(e,t,n,r,o,s,i,l,c,ne)}null!=u&&o?V(u,e&&e.ref,s,t||e,!t):null==u&&e&&null!=e.ref&&V(e.ref,null,s,e,!0)},y=(e,t,n,r)=>{if(null==e)s(t.el=a(t.children),n,r);else{const n=t.el=e.el;t.children!==e.children&&f(n,t.children)}},k=(e,t,n,r)=>{null==e?s(t.el=u(t.children||""),n,r):t.el=e.el},_=(e,t,n,r)=>{[e.el,e.anchor]=g(e.children,t,n,r,e.el,e.anchor)},S=({el:e,anchor:t},n,r)=>{let o;while(e&&e!==t)o=h(e),s(e,n,r),e=o;s(t,n,r)},C=({el:e,anchor:t})=>{let n;while(e&&e!==t)n=h(e),i(e),e=n;i(t)},E=(e,t,n,r,o,s,i,l,c)=>{"svg"===t.type?i="svg":"math"===t.type&&(i="mathml"),null==e?O(t,n,r,o,s,i,l,c):I(e,t,o,s,i,l,c)},O=(e,t,n,r,i,a,u,f)=>{let p,h;const{props:m,shapeFlag:g,transition:v,dirs:b}=e;if(p=e.el=c(e.type,a,m&&m.is,m),8&g?d(p,e.children):16&g&&j(e.children,p,null,r,i,mt(e,a),u,f),b&&M(e,null,r,"created"),R(p,e,e.scopeId,u,r),m){for(const e in m)"value"===e||(0,o.Gg)(e)||l(p,e,null,m[e],a,r);"value"in m&&l(p,"value",null,m.value,a),(h=m.onVnodeBeforeMount)&&mn(h,r,e)}b&&M(e,null,r,"beforeMount");const y=vt(i,v);y&&v.beforeEnter(p),s(p,t,n),((h=m&&m.onVnodeMounted)||y||b)&&dt((()=>{h&&mn(h,r,e),y&&v.enter(p),b&&M(e,null,r,"mounted")}),i)},R=(e,t,n,r,o)=>{if(n&&m(e,n),r)for(let s=0;s{for(let a=c;a{const a=t.el=e.el;let{patchFlag:u,dynamicChildren:f,dirs:p}=t;u|=16&e.patchFlag;const h=e.props||o.kT,m=t.props||o.kT;let g;if(n&>(n,!1),(g=m.onVnodeBeforeUpdate)&&mn(g,n,t,e),p&&M(t,e,n,"beforeUpdate"),n&>(n,!0),(h.innerHTML&&null==m.innerHTML||h.textContent&&null==m.textContent)&&d(a,""),f?T(e.dynamicChildren,f,a,n,r,mt(t,s),i):c||z(e,t,a,null,n,r,mt(t,s),i,!1),u>0){if(16&u)P(a,h,m,n,s);else if(2&u&&h.class!==m.class&&l(a,"class",null,m.class,s),4&u&&l(a,"style",h.style,m.style,s),8&u){const e=t.dynamicProps;for(let t=0;t{g&&mn(g,n,t,e),p&&M(t,e,n,"updated")}),r)},T=(e,t,n,r,o,s,i)=>{for(let l=0;l{if(t!==n){if(t!==o.kT)for(const i in t)(0,o.Gg)(i)||i in n||l(e,i,t[i],null,s,r);for(const i in n){if((0,o.Gg)(i))continue;const c=n[i],a=t[i];c!==a&&"value"!==i&&l(e,i,a,c,s,r)}"value"in n&&l(e,"value",t.value,n.value,s)}},F=(e,t,n,r,o,i,l,c,u)=>{const f=t.el=e?e.el:a(""),d=t.anchor=e?e.anchor:a("");let{patchFlag:p,dynamicChildren:h,slotScopeIds:m}=t;m&&(c=c?c.concat(m):m),null==e?(s(f,n,r),s(d,n,r),j(t.children||[],n,d,o,i,l,c,u)):p>0&&64&p&&h&&e.dynamicChildren?(T(e.dynamicChildren,h,n,o,i,l,c),(null!=t.key||o&&t===o.subTree)&&bt(e,t,!0)):z(e,t,n,d,o,i,l,c,u)},D=(e,t,n,r,o,s,i,l,c)=>{t.slotScopeIds=l,null==e?512&t.shapeFlag?o.ctx.activate(t,n,r,i,c):N(t,n,r,o,s,i,c):$(e,t,c)},N=(e,t,n,r,o,s,i)=>{const l=e.component=bn(e,r,o);if(B(e)&&(l.ctx.renderer=ne),jn(l,!1,i),l.asyncDep){if(o&&o.registerDep(l,L,i),!e.el){const r=l.subTree=on(Jt);k(null,r,t,n),e.placeholder=r.el}}else L(l,e,t,n,o,s,i)},$=(e,t,n)=>{const r=t.component=e.component;if(Ft(e,t,n)){if(r.asyncDep&&!r.asyncResolved)return void U(r,t,n);r.next=t,r.update()}else t.el=e.el,r.vnode=t},L=(e,t,n,s,i,l,c)=>{const a=()=>{if(e.isMounted){let{next:t,bu:n,u:r,parent:s,vnode:u}=e;{const n=kt(e);if(n)return t&&(t.el=u.el,U(e,t,c)),void n.asyncDep.then((()=>{e.isUnmounted||a()}))}let f,d=t;0,gt(e,!1),t?(t.el=u.el,U(e,t,c)):t=u,n&&(0,o.ir)(n),(f=t.props&&t.props.onVnodeBeforeUpdate)&&mn(f,s,t,u),gt(e,!0);const h=It(e);0;const m=e.subTree;e.subTree=h,v(m,h,p(m.el),Q(m),e,i,l),t.el=h.el,null===d&&Nt(e,h.el),r&&dt(r,i),(f=t.props&&t.props.onVnodeUpdated)&&dt((()=>mn(f,s,t,u)),i)}else{let r;const{el:c,props:a}=t,{bm:u,m:f,parent:d,root:p,type:h}=e,m=H(t);if(gt(e,!1),u&&(0,o.ir)(u),!m&&(r=a&&a.onVnodeBeforeMount)&&mn(r,d,t),gt(e,!0),c&&oe){const t=()=>{e.subTree=It(e),oe(c,e.subTree,e,i,null)};m&&h.__asyncHydrate?h.__asyncHydrate(c,e,t):t()}else{p.ce&&!1!==p.ce._def.shadowRoot&&p.ce._injectChildStyle(h);const r=e.subTree=It(e);0,v(null,r,n,s,e,i,l),t.el=r.el}if(f&&dt(f,i),!m&&(r=a&&a.onVnodeMounted)){const e=t;dt((()=>mn(r,d,e)),i)}(256&t.shapeFlag||d&&H(d.vnode)&&256&d.vnode.shapeFlag)&&e.a&&dt(e.a,i),e.isMounted=!0,t=n=s=null}};e.scope.on();const u=e.effect=new r.qq(a);e.scope.off();const f=e.update=u.run.bind(u),d=e.job=u.runIfDirty.bind(u);d.i=e,d.id=e.uid,u.scheduler=()=>b(d),gt(e,!0),f()},U=(e,t,n)=>{t.component=e;const o=e.vnode.props;e.vnode=t,e.next=null,Ye(e,t.props,o,n),ut(e,t.children,n),(0,r.Jd)(),w(e),(0,r.lk)()},z=(e,t,n,r,o,s,i,l,c=!1)=>{const a=e&&e.children,u=e?e.shapeFlag:0,f=t.children,{patchFlag:p,shapeFlag:h}=t;if(p>0){if(128&p)return void Z(a,f,n,r,o,s,i,l,c);if(256&p)return void J(a,f,n,r,o,s,i,l,c)}8&h?(16&u&&X(a,o,s),f!==a&&d(n,f)):16&u?16&h?Z(a,f,n,r,o,s,i,l,c):X(a,o,s,!0):(8&u&&d(n,""),16&h&&j(f,n,r,o,s,i,l,c))},J=(e,t,n,r,s,i,l,c,a)=>{e=e||o.Z6,t=t||o.Z6;const u=e.length,f=t.length,d=Math.min(u,f);let p;for(p=0;pf?X(e,s,i,!0,!1,d):j(t,n,r,s,i,l,c,a,d)},Z=(e,t,n,r,s,i,l,c,a)=>{let u=0;const f=t.length;let d=e.length-1,p=f-1;while(u<=d&&u<=p){const r=e[u],o=t[u]=a?dn(t[u]):fn(t[u]);if(!en(r,o))break;v(r,o,n,null,s,i,l,c,a),u++}while(u<=d&&u<=p){const r=e[d],o=t[p]=a?dn(t[p]):fn(t[p]);if(!en(r,o))break;v(r,o,n,null,s,i,l,c,a),d--,p--}if(u>d){if(u<=p){const e=p+1,o=ep)while(u<=d)q(e[u],s,i,!0),u++;else{const h=u,m=u,g=new Map;for(u=m;u<=p;u++){const e=t[u]=a?dn(t[u]):fn(t[u]);null!=e.key&&g.set(e.key,u)}let b,y=0;const k=p-m+1;let w=!1,x=0;const _=new Array(k);for(u=0;u=k){q(r,s,i,!0);continue}let o;if(null!=r.key)o=g.get(r.key);else for(b=m;b<=p;b++)if(0===_[b-m]&&en(r,t[b])){o=b;break}void 0===o?q(r,s,i,!0):(_[o-m]=u+1,o>=x?x=o:w=!0,v(r,t[o],n,null,s,i,l,c,a),y++)}const S=w?yt(_):o.Z6;for(b=S.length-1,u=k-1;u>=0;u--){const e=m+u,o=t[e],d=t[e+1],p=e+1{const{el:l,type:c,transition:a,children:u,shapeFlag:f}=e;if(6&f)return void G(e.component.subTree,t,n,r);if(128&f)return void e.suspense.move(t,n,r);if(64&f)return void c.move(e,t,n,ne);if(c===Ut){s(l,t,n);for(let e=0;ea.enter(l)),o);else{const{leave:r,delayLeave:o,afterLeave:c}=a,u=()=>{e.ctx.isUnmounted?i(l):s(l,t,n)},f=()=>{r(l,(()=>{u(),c&&c()}))};o?o(l,u,f):f()}else s(l,t,n)},q=(e,t,n,o=!1,s=!1)=>{const{type:i,props:l,ref:c,children:a,dynamicChildren:u,shapeFlag:f,patchFlag:d,dirs:p,cacheIndex:h}=e;if(-2===d&&(s=!1),null!=c&&((0,r.Jd)(),V(c,null,n,e,!0),(0,r.lk)()),null!=h&&(t.renderCache[h]=void 0),256&f)return void t.ctx.deactivate(e);const m=1&f&&p,g=!H(e);let v;if(g&&(v=l&&l.onVnodeBeforeUnmount)&&mn(v,t,e),6&f)Y(e.component,n,o);else{if(128&f)return void e.suspense.unmount(n,o);m&&M(e,null,t,"beforeUnmount"),64&f?e.type.remove(e,t,n,ne,o):u&&!u.hasOnce&&(i!==Ut||d>0&&64&d)?X(u,t,n,!1,!0):(i===Ut&&384&d||!s&&16&f)&&X(a,t,n),o&&W(e)}(g&&(v=l&&l.onVnodeUnmounted)||m)&&dt((()=>{v&&mn(v,t,e),m&&M(e,null,t,"unmounted")}),n)},W=e=>{const{type:t,el:n,anchor:r,transition:o}=e;if(t===Ut)return void K(n,r);if(t===Zt)return void C(e);const s=()=>{i(n),o&&!o.persisted&&o.afterLeave&&o.afterLeave()};if(1&e.shapeFlag&&o&&!o.persisted){const{leave:t,delayLeave:r}=o,i=()=>t(n,s);r?r(e.el,s,i):i()}else s()},K=(e,t)=>{let n;while(e!==t)n=h(e),i(e),e=n;i(t)},Y=(e,t,n)=>{const{bum:r,scope:s,job:i,subTree:l,um:c,m:a,a:u,parent:f,slots:{__:d}}=e;wt(a),wt(u),r&&(0,o.ir)(r),f&&(0,o.kJ)(d)&&d.forEach((e=>{f.renderCache[e]=void 0})),s.stop(),i&&(i.flags|=8,q(l,e,t,n)),c&&dt(c,t),dt((()=>{e.isUnmounted=!0}),t),t&&t.pendingBranch&&!t.isUnmounted&&e.asyncDep&&!e.asyncResolved&&e.suspenseId===t.pendingId&&(t.deps--,0===t.deps&&t.resolve())},X=(e,t,n,r=!1,o=!1,s=0)=>{for(let i=s;i{if(6&e.shapeFlag)return Q(e.component.subTree);if(128&e.shapeFlag)return e.suspense.next();const t=h(e.anchor||e.el),n=t&&t[A];return n?h(n):t};let ee=!1;const te=(e,t,n)=>{null==e?t._vnode&&q(t._vnode,null,null,!0):v(t._vnode||null,e,t,null,null,null,n),t._vnode=e,ee||(ee=!0,w(),x(),ee=!1)},ne={p:v,um:q,m:G,r:W,mt:N,mc:j,pc:z,pbc:T,n:Q,o:e};let re,oe;return t&&([re,oe]=t(ne)),{render:te,hydrate:re,createApp:Ze(te,re)}}function mt({type:e,props:t},n){return"svg"===n&&"foreignObject"===e||"mathml"===n&&"annotation-xml"===e&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function gt({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function vt(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function bt(e,t,n=!1){const r=e.children,s=t.children;if((0,o.kJ)(r)&&(0,o.kJ)(s))for(let o=0;o>1,e[n[l]]0&&(t[r]=n[s-1]),n[s]=r)}}s=n.length,i=n[s-1];while(s-- >0)n[s]=i,i=t[i];return n}function kt(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:kt(t)}function wt(e){if(e)for(let t=0;t{{const e=Be(xt);return e}};function St(e,t,n){return Ct(e,t,n)}function Ct(e,t,n=o.kT){const{immediate:s,deep:l,flush:c,once:a}=n;const u=(0,o.l7)({},n);const f=t&&s||!t&&"post"!==c;let d;if(Rn)if("sync"===c){const e=_t();d=e.__watcherHandles||(e.__watcherHandles=[])}else if(!f){const e=()=>{};return e.stop=o.dG,e.resume=o.dG,e.pause=o.dG,e}const p=yn;u.call=(e,t,n)=>i(e,p,t,n);let h=!1;"post"===c?u.scheduler=e=>{dt(e,p&&p.suspense)}:"sync"!==c&&(h=!0,u.scheduler=(e,t)=>{t?e():b(e)}),u.augmentJob=e=>{t&&(e.flags|=4),h&&(e.flags|=2,p&&(e.id=p.uid,e.i=p))};const m=(0,r.YP)(e,t,u);return Rn&&(d?d.push(m):f&&m()),m}function Et(e,t,n){const r=this.proxy,s=(0,o.HD)(e)?e.includes(".")?Ot(r,e):()=>r[e]:e.bind(r,r);let i;(0,o.mf)(t)?i=t:(i=t.handler,n=t);const l=_n(this),c=Ct(s,i.bind(r),n);return l(),c}function Ot(e,t){const n=t.split(".");return()=>{let t=e;for(let e=0;e"modelValue"===t||"model-value"===t?e.modelModifiers:e[`${t}Modifiers`]||e[`${(0,o._A)(t)}Modifiers`]||e[`${(0,o.rs)(t)}Modifiers`];function jt(e,t,...n){if(e.isUnmounted)return;const r=e.vnode.props||o.kT;let s=n;const l=t.startsWith("update:"),c=l&&Rt(r,t.slice(7));let a;c&&(c.trim&&(s=n.map((e=>(0,o.HD)(e)?e.trim():e))),c.number&&(s=n.map(o.h5)));let u=r[a=(0,o.hR)(t)]||r[a=(0,o.hR)((0,o._A)(t))];!u&&l&&(u=r[a=(0,o.hR)((0,o.rs)(t))]),u&&i(u,e,6,s);const f=r[a+"Once"];if(f){if(e.emitted){if(e.emitted[a])return}else e.emitted={};e.emitted[a]=!0,i(f,e,6,s)}}function Mt(e,t,n=!1){const r=t.emitsCache,s=r.get(e);if(void 0!==s)return s;const i=e.emits;let l={},c=!1;if(!(0,o.mf)(e)){const r=e=>{const n=Mt(e,t,!0);n&&(c=!0,(0,o.l7)(l,n))};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}return i||c?((0,o.kJ)(i)?i.forEach((e=>l[e]=null)):(0,o.l7)(l,i),(0,o.Kn)(e)&&r.set(e,l),l):((0,o.Kn)(e)&&r.set(e,null),null)}function At(e,t){return!(!e||!(0,o.F7)(t))&&(t=t.slice(2).replace(/Once$/,""),(0,o.RI)(e,t[0].toLowerCase()+t.slice(1))||(0,o.RI)(e,(0,o.rs)(t))||(0,o.RI)(e,t))}function It(e){const{type:t,vnode:n,proxy:r,withProxy:s,propsOptions:[i],slots:c,attrs:a,emit:u,render:f,renderCache:d,props:p,data:h,setupState:m,ctx:g,inheritAttrs:v}=e,b=O(e);let y,k;try{if(4&n.shapeFlag){const e=s||r,t=e;y=fn(f.call(t,e,d,p,m,h,g)),k=a}else{const e=t;0,y=fn(e.length>1?e(p,{attrs:a,slots:c,emit:u}):e(p,null)),k=t.props?a:Tt(a)}}catch(x){Vt.length=0,l(x,e,1),y=on(Jt)}let w=y;if(k&&!1!==v){const e=Object.keys(k),{shapeFlag:t}=w;e.length&&7&t&&(i&&e.some(o.tR)&&(k=Pt(k,i)),w=cn(w,k,!1,!0))}return n.dirs&&(w=cn(w,null,!1,!0),w.dirs=w.dirs?w.dirs.concat(n.dirs):n.dirs),n.transition&&U(w,n.transition),y=w,O(b),y}const Tt=e=>{let t;for(const n in e)("class"===n||"style"===n||(0,o.F7)(n))&&((t||(t={}))[n]=e[n]);return t},Pt=(e,t)=>{const n={};for(const r in e)(0,o.tR)(r)&&r.slice(9)in t||(n[r]=e[r]);return n};function Ft(e,t,n){const{props:r,children:o,component:s}=e,{props:i,children:l,patchFlag:c}=t,a=s.emitsOptions;if(t.dirs||t.transition)return!0;if(!(n&&c>=0))return!(!o&&!l||l&&l.$stable)||r!==i&&(r?!i||Dt(r,i,a):!!i);if(1024&c)return!0;if(16&c)return r?Dt(r,i,a):!!i;if(8&c){const e=t.dynamicProps;for(let t=0;te.__isSuspense;function Lt(e,t){t&&t.pendingBranch?(0,o.kJ)(e)?t.effects.push(...e):t.effects.push(e):k(e)}const Ut=Symbol.for("v-fgt"),zt=Symbol.for("v-txt"),Jt=Symbol.for("v-cmt"),Zt=Symbol.for("v-stc"),Vt=[];let Ht=null;function Bt(e=!1){Vt.push(Ht=e?null:[])}function Gt(){Vt.pop(),Ht=Vt[Vt.length-1]||null}let qt=1;function Wt(e,t=!1){qt+=e,e<0&&Ht&&t&&(Ht.hasOnce=!0)}function Kt(e){return e.dynamicChildren=qt>0?Ht||o.Z6:null,Gt(),qt>0&&Ht&&Ht.push(e),e}function Yt(e,t,n,r,o,s){return Kt(rn(e,t,n,r,o,s,!0))}function Xt(e,t,n,r,o){return Kt(on(e,t,n,r,o,!0))}function Qt(e){return!!e&&!0===e.__v_isVNode}function en(e,t){return e.type===t.type&&e.key===t.key}const tn=({key:e})=>null!=e?e:null,nn=({ref:e,ref_key:t,ref_for:n})=>("number"===typeof e&&(e=""+e),null!=e?(0,o.HD)(e)||(0,r.dq)(e)||(0,o.mf)(e)?{i:C,r:e,k:t,f:!!n}:e:null);function rn(e,t=null,n=null,r=0,s=null,i=(e===Ut?0:1),l=!1,c=!1){const a={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&tn(t),ref:t&&nn(t),scopeId:E,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:r,dynamicProps:s,dynamicChildren:null,appContext:null,ctx:C};return c?(pn(a,n),128&i&&e.normalize(a)):n&&(a.shapeFlag|=(0,o.HD)(n)?8:16),qt>0&&!l&&Ht&&(a.patchFlag>0||6&i)&&32!==a.patchFlag&&Ht.push(a),a}const on=sn;function sn(e,t=null,n=null,s=0,i=null,l=!1){if(e&&e!==he||(e=Jt),Qt(e)){const r=cn(e,t,!0);return n&&pn(r,n),qt>0&&!l&&Ht&&(6&r.shapeFlag?Ht[Ht.indexOf(e)]=r:Ht.push(r)),r.patchFlag=-2,r}if(Nn(e)&&(e=e.__vccOpts),t){t=ln(t);let{class:e,style:n}=t;e&&!(0,o.HD)(e)&&(t.class=(0,o.C_)(e)),(0,o.Kn)(n)&&((0,r.X3)(n)&&!(0,o.kJ)(n)&&(n=(0,o.l7)({},n)),t.style=(0,o.j5)(n))}const c=(0,o.HD)(e)?1:$t(e)?128:I(e)?64:(0,o.Kn)(e)?4:(0,o.mf)(e)?2:0;return rn(e,t,n,s,i,c,l,!0)}function ln(e){return e?(0,r.X3)(e)||We(e)?(0,o.l7)({},e):e:null}function cn(e,t,n=!1,r=!1){const{props:s,ref:i,patchFlag:l,children:c,transition:a}=e,u=t?hn(s||{},t):s,f={__v_isVNode:!0,__v_skip:!0,type:e.type,props:u,key:u&&tn(u),ref:t&&t.ref?n&&i?(0,o.kJ)(i)?i.concat(nn(t)):[i,nn(t)]:nn(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:c,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Ut?-1===l?16:16|l:l,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:a,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&cn(e.ssContent),ssFallback:e.ssFallback&&cn(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return a&&r&&U(f,a.clone(f)),f}function an(e=" ",t=0){return on(zt,null,e,t)}function un(e="",t=!1){return t?(Bt(),Xt(Jt,null,e)):on(Jt,null,e)}function fn(e){return null==e||"boolean"===typeof e?on(Jt):(0,o.kJ)(e)?on(Ut,null,e.slice()):Qt(e)?dn(e):on(zt,null,String(e))}function dn(e){return null===e.el&&-1!==e.patchFlag||e.memo?e:cn(e)}function pn(e,t){let n=0;const{shapeFlag:r}=e;if(null==t)t=null;else if((0,o.kJ)(t))n=16;else if("object"===typeof t){if(65&r){const n=t.default;return void(n&&(n._c&&(n._d=!1),pn(e,n()),n._c&&(n._d=!0)))}{n=32;const r=t._;r||We(t)?3===r&&C&&(1===C.slots._?t._=1:(t._=2,e.patchFlag|=1024)):t._ctx=C}}else(0,o.mf)(t)?(t={default:t,_ctx:C},n=32):(t=String(t),64&r?(n=16,t=[an(t)]):n=8);e.children=t,e.shapeFlag|=n}function hn(...e){const t={};for(let n=0;nyn||C;let wn,xn;{const e=(0,o.E9)(),t=(t,n)=>{let r;return(r=e[t])||(r=e[t]=[]),r.push(n),e=>{r.length>1?r.forEach((t=>t(e))):r[0](e)}};wn=t("__VUE_INSTANCE_SETTERS__",(e=>yn=e)),xn=t("__VUE_SSR_SETTERS__",(e=>Rn=e))}const _n=e=>{const t=yn;return wn(e),e.scope.on(),()=>{e.scope.off(),wn(t)}},Sn=()=>{yn&&yn.scope.off(),wn(null)};function Cn(e){return 4&e.vnode.shapeFlag}let En,On,Rn=!1;function jn(e,t=!1,n=!1){t&&xn(t);const{props:r,children:o}=e.vnode,s=Cn(e);Ke(e,r,s,t),at(e,o,n||t);const i=s?Mn(e,t):void 0;return t&&xn(!1),i}function Mn(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Se);const{setup:i}=n;if(i){(0,r.Jd)();const n=e.setupContext=i.length>1?Pn(e):null,c=_n(e),a=s(i,e,0,[e.props,n]),u=(0,o.tI)(a);if((0,r.lk)(),c(),!u&&!e.sp||H(e)||Z(e),u){if(a.then(Sn,Sn),t)return a.then((n=>{An(e,n,t)})).catch((t=>{l(t,e,0)}));e.asyncDep=a}else An(e,a,t)}else In(e,t)}function An(e,t,n){(0,o.mf)(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:(0,o.Kn)(t)&&(e.setupState=(0,r.WL)(t)),In(e,n)}function In(e,t,n){const s=e.type;if(!e.render){if(!t&&En&&!s.render){const t=s.template||Ae(e).template;if(t){0;const{isCustomElement:n,compilerOptions:r}=e.appContext.config,{delimiters:i,compilerOptions:l}=s,c=(0,o.l7)((0,o.l7)({isCustomElement:n,delimiters:i},r),l);s.render=En(t,c)}}e.render=s.render||o.dG,On&&On(e)}{const t=_n(e);(0,r.Jd)();try{Oe(e)}finally{(0,r.lk)(),t()}}}const Tn={get(e,t){return(0,r.j)(e,"get",""),e[t]}};function Pn(e){const t=t=>{e.exposed=t||{}};return{attrs:new Proxy(e.attrs,Tn),slots:e.slots,emit:e.emit,expose:t}}function Fn(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy((0,r.WL)((0,r.Xl)(e.exposed)),{get(t,n){return n in t?t[n]:n in xe?xe[n](e):void 0},has(e,t){return t in e||t in xe}})):e.proxy}function Dn(e,t=!0){return(0,o.mf)(e)?e.displayName||e.name:e.name||t&&e.__name}function Nn(e){return(0,o.mf)(e)&&"__vccOpts"in e}const $n=(e,t)=>{const n=(0,r.Fl)(e,t,Rn);return n};function Ln(e,t,n){const r=arguments.length;return 2===r?(0,o.Kn)(t)&&!(0,o.kJ)(t)?Qt(t)?on(e,null,[t]):on(e,t):on(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):3===r&&Qt(n)&&(n=[n]),on(e,t,n))}const Un="3.5.18"},963:function(e,t,n){n.d(t,{D2:function(){return X},bM:function(){return H},iM:function(){return K},ri:function(){return ne}});var r=n(252),o=n(577);n(262); +**/let o,s;class i{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=o,!e&&o&&(this.index=(o.scopes||(o.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){let e,t;if(this._isPaused=!0,this.scopes)for(e=0,t=this.scopes.length;e0&&0===--this._on&&(o=this.prevScope,this.prevScope=void 0)}stop(e){if(this._active){let t,n;for(this._active=!1,t=0,n=this.effects.length;t0)return;if(f){let e=f;f=void 0;while(e){const t=e.next;e.next=void 0,e.flags&=-9,e=t}}let e;while(u){let n=u;u=void 0;while(n){const r=n.next;if(n.next=void 0,n.flags&=-9,1&n.flags)try{n.trigger()}catch(t){e||(e=t)}n=r}}if(e)throw e}function g(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function v(e){let t,n=e.depsTail,r=n;while(r){const e=r.prevDep;-1===r.version?(r===n&&(n=e),k(r),w(r)):t=r,r.dep.activeLink=r.prevActiveLink,r.prevActiveLink=void 0,r=e}e.deps=t,e.depsTail=n}function y(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(b(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function b(e){if(4&e.flags&&!(16&e.flags))return;if(e.flags&=-17,e.globalVersion===O)return;if(e.globalVersion=O,!e.isSSR&&128&e.flags&&(!e.deps&&!e._dirty||!y(e)))return;e.flags|=2;const t=e.dep,n=s,o=x;s=e,x=!0;try{g(e);const i=e.fn(e._value);(0===t.version||(0,r.aU)(i,e._value))&&(e.flags|=128,e._value=i,t.version++)}catch(i){throw t.version++,i}finally{s=n,x=o,v(e),e.flags&=-3}}function k(e,t=!1){const{dep:n,prevSub:r,nextSub:o}=e;if(r&&(r.nextSub=o,e.prevSub=void 0),o&&(o.prevSub=r,e.nextSub=void 0),n.subs===e&&(n.subs=r,!r&&n.computed)){n.computed.flags&=-5;for(let e=n.computed.deps;e;e=e.nextDep)k(e,!0)}t||--n.sc||!n.map||n.map.delete(n.key)}function w(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let x=!0;const _=[];function S(){_.push(x),x=!1}function C(){const e=_.pop();x=void 0===e||e}function E(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const e=s;s=void 0;try{t()}finally{s=e}}}let O=0;class R{constructor(e,t){this.sub=e,this.dep=t,this.version=t.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class M{constructor(e){this.computed=e,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(e){if(!s||!x||s===this.computed)return;let t=this.activeLink;if(void 0===t||t.sub!==s)t=this.activeLink=new R(s,this),s.deps?(t.prevDep=s.depsTail,s.depsTail.nextDep=t,s.depsTail=t):s.deps=s.depsTail=t,j(t);else if(-1===t.version&&(t.version=this.version,t.nextDep)){const e=t.nextDep;e.prevDep=t.prevDep,t.prevDep&&(t.prevDep.nextDep=e),t.prevDep=s.depsTail,t.nextDep=void 0,s.depsTail.nextDep=t,s.depsTail=t,s.deps===t&&(s.deps=e)}return t}trigger(e){this.version++,O++,this.notify(e)}notify(e){h();try{0;for(let e=this.subs;e;e=e.prevSub)e.sub.notify()&&e.sub.dep.notify()}finally{m()}}}function j(e){if(e.dep.sc++,4&e.sub.flags){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let e=t.deps;e;e=e.nextDep)j(e)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const A=new WeakMap,I=Symbol(""),T=Symbol(""),P=Symbol("");function F(e,t,n){if(x&&s){let t=A.get(e);t||A.set(e,t=new Map);let r=t.get(n);r||(t.set(n,r=new M),r.map=t,r.key=n),r.track()}}function D(e,t,n,o,s,i){const l=A.get(e);if(!l)return void O++;const c=e=>{e&&e.trigger()};if(h(),"clear"===t)l.forEach(c);else{const s=(0,r.kJ)(e),i=s&&(0,r.S0)(n);if(s&&"length"===n){const e=Number(o);l.forEach(((t,n)=>{("length"===n||n===P||!(0,r.yk)(n)&&n>=e)&&c(t)}))}else switch((void 0!==n||l.has(void 0))&&c(l.get(n)),i&&c(l.get(P)),t){case"add":s?i&&c(l.get("length")):(c(l.get(I)),(0,r._N)(e)&&c(l.get(T)));break;case"delete":s||(c(l.get(I)),(0,r._N)(e)&&c(l.get(T)));break;case"set":(0,r._N)(e)&&c(l.get(I));break}}m()}function N(e){const t=Se(e);return t===e?t:(F(t,"iterate",P),xe(e)?t:t.map(Ee))}function $(e){return F(e=Se(e),"iterate",P),e}const z={__proto__:null,[Symbol.iterator](){return L(this,Symbol.iterator,Ee)},concat(...e){return N(this).concat(...e.map((e=>(0,r.kJ)(e)?N(e):e)))},entries(){return L(this,"entries",(e=>(e[1]=Ee(e[1]),e)))},every(e,t){return Z(this,"every",e,t,void 0,arguments)},filter(e,t){return Z(this,"filter",e,t,(e=>e.map(Ee)),arguments)},find(e,t){return Z(this,"find",e,t,Ee,arguments)},findIndex(e,t){return Z(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Z(this,"findLast",e,t,Ee,arguments)},findLastIndex(e,t){return Z(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Z(this,"forEach",e,t,void 0,arguments)},includes(...e){return V(this,"includes",e)},indexOf(...e){return V(this,"indexOf",e)},join(e){return N(this).join(e)},lastIndexOf(...e){return V(this,"lastIndexOf",e)},map(e,t){return Z(this,"map",e,t,void 0,arguments)},pop(){return H(this,"pop")},push(...e){return H(this,"push",e)},reduce(e,...t){return J(this,"reduce",e,t)},reduceRight(e,...t){return J(this,"reduceRight",e,t)},shift(){return H(this,"shift")},some(e,t){return Z(this,"some",e,t,void 0,arguments)},splice(...e){return H(this,"splice",e)},toReversed(){return N(this).toReversed()},toSorted(e){return N(this).toSorted(e)},toSpliced(...e){return N(this).toSpliced(...e)},unshift(...e){return H(this,"unshift",e)},values(){return L(this,"values",Ee)}};function L(e,t,n){const r=$(e),o=r[t]();return r===e||xe(e)||(o._next=o.next,o.next=()=>{const e=o._next();return e.value&&(e.value=n(e.value)),e}),o}const U=Array.prototype;function Z(e,t,n,r,o,s){const i=$(e),l=i!==e&&!xe(e),c=i[t];if(c!==U[t]){const t=c.apply(e,s);return l?Ee(t):t}let a=n;i!==e&&(l?a=function(t,r){return n.call(this,Ee(t),r,e)}:n.length>2&&(a=function(t,r){return n.call(this,t,r,e)}));const u=c.call(i,a,r);return l&&o?o(u):u}function J(e,t,n,r){const o=$(e);let s=n;return o!==e&&(xe(e)?n.length>3&&(s=function(t,r,o){return n.call(this,t,r,o,e)}):s=function(t,r,o){return n.call(this,t,Ee(r),o,e)}),o[t](s,...r)}function V(e,t,n){const r=Se(e);F(r,"iterate",P);const o=r[t](...n);return-1!==o&&!1!==o||!_e(n[0])?o:(n[0]=Se(n[0]),r[t](...n))}function H(e,t,n=[]){S(),h();const r=Se(e)[t].apply(e,n);return m(),C(),r}const B=(0,r.fY)("__proto__,__v_isRef,__isVue"),G=new Set(Object.getOwnPropertyNames(Symbol).filter((e=>"arguments"!==e&&"caller"!==e)).map((e=>Symbol[e])).filter(r.yk));function q(e){(0,r.yk)(e)||(e=String(e));const t=Se(this);return F(t,"has",e),t.hasOwnProperty(e)}class W{constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}get(e,t,n){if("__v_skip"===t)return e["__v_skip"];const o=this._isReadonly,s=this._isShallow;if("__v_isReactive"===t)return!o;if("__v_isReadonly"===t)return o;if("__v_isShallow"===t)return s;if("__v_raw"===t)return n===(o?s?pe:de:s?fe:ue).get(e)||Object.getPrototypeOf(e)===Object.getPrototypeOf(n)?e:void 0;const i=(0,r.kJ)(e);if(!o){let e;if(i&&(e=z[t]))return e;if("hasOwnProperty"===t)return q}const l=Reflect.get(e,t,Re(e)?e:n);return((0,r.yk)(t)?G.has(t):B(t))?l:(o||F(e,"get",t),s?l:Re(l)?i&&(0,r.S0)(t)?l:l.value:(0,r.Kn)(l)?o?ye(l):ge(l):l)}}class K extends W{constructor(e=!1){super(!1,e)}set(e,t,n,o){let s=e[t];if(!this._isShallow){const t=we(s);if(xe(n)||we(n)||(s=Se(s),n=Se(n)),!(0,r.kJ)(e)&&Re(s)&&!Re(n))return!t&&(s.value=n,!0)}const i=(0,r.kJ)(e)&&(0,r.S0)(t)?Number(t)e,ne=e=>Reflect.getPrototypeOf(e);function re(e,t,n){return function(...o){const s=this["__v_raw"],i=Se(s),l=(0,r._N)(i),c="entries"===e||e===Symbol.iterator&&l,a="keys"===e&&l,u=s[e](...o),f=n?te:t?Oe:Ee;return!t&&F(i,"iterate",a?T:I),{next(){const{value:e,done:t}=u.next();return t?{value:e,done:t}:{value:c?[f(e[0]),f(e[1])]:f(e),done:t}},[Symbol.iterator](){return this}}}}function oe(e){return function(...t){return"delete"!==e&&("clear"===e?void 0:this)}}function se(e,t){const n={get(n){const o=this["__v_raw"],s=Se(o),i=Se(n);e||((0,r.aU)(n,i)&&F(s,"get",n),F(s,"get",i));const{has:l}=ne(s),c=t?te:e?Oe:Ee;return l.call(s,n)?c(o.get(n)):l.call(s,i)?c(o.get(i)):void(o!==s&&o.get(n))},get size(){const t=this["__v_raw"];return!e&&F(Se(t),"iterate",I),Reflect.get(t,"size",t)},has(t){const n=this["__v_raw"],o=Se(n),s=Se(t);return e||((0,r.aU)(t,s)&&F(o,"has",t),F(o,"has",s)),t===s?n.has(t):n.has(t)||n.has(s)},forEach(n,r){const o=this,s=o["__v_raw"],i=Se(s),l=t?te:e?Oe:Ee;return!e&&F(i,"iterate",I),s.forEach(((e,t)=>n.call(r,l(e),l(t),o)))}};(0,r.l7)(n,e?{add:oe("add"),set:oe("set"),delete:oe("delete"),clear:oe("clear")}:{add(e){t||xe(e)||we(e)||(e=Se(e));const n=Se(this),r=ne(n),o=r.has.call(n,e);return o||(n.add(e),D(n,"add",e,e)),this},set(e,n){t||xe(n)||we(n)||(n=Se(n));const o=Se(this),{has:s,get:i}=ne(o);let l=s.call(o,e);l||(e=Se(e),l=s.call(o,e));const c=i.call(o,e);return o.set(e,n),l?(0,r.aU)(n,c)&&D(o,"set",e,n,c):D(o,"add",e,n),this},delete(e){const t=Se(this),{has:n,get:r}=ne(t);let o=n.call(t,e);o||(e=Se(e),o=n.call(t,e));const s=r?r.call(t,e):void 0,i=t.delete(e);return o&&D(t,"delete",e,void 0,s),i},clear(){const e=Se(this),t=0!==e.size,n=void 0,r=e.clear();return t&&D(e,"clear",void 0,void 0,n),r}});const o=["keys","values","entries",Symbol.iterator];return o.forEach((r=>{n[r]=re(r,e,t)})),n}function ie(e,t){const n=se(e,t);return(t,o,s)=>"__v_isReactive"===o?!e:"__v_isReadonly"===o?e:"__v_raw"===o?t:Reflect.get((0,r.RI)(n,o)&&o in t?n:t,o,s)}const le={get:ie(!1,!1)},ce={get:ie(!1,!0)},ae={get:ie(!0,!1)};const ue=new WeakMap,fe=new WeakMap,de=new WeakMap,pe=new WeakMap;function he(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function me(e){return e["__v_skip"]||!Object.isExtensible(e)?0:he((0,r.W7)(e))}function ge(e){return we(e)?e:be(e,!1,X,le,ue)}function ve(e){return be(e,!1,ee,ce,fe)}function ye(e){return be(e,!0,Q,ae,de)}function be(e,t,n,o,s){if(!(0,r.Kn)(e))return e;if(e["__v_raw"]&&(!t||!e["__v_isReactive"]))return e;const i=me(e);if(0===i)return e;const l=s.get(e);if(l)return l;const c=new Proxy(e,2===i?o:n);return s.set(e,c),c}function ke(e){return we(e)?ke(e["__v_raw"]):!(!e||!e["__v_isReactive"])}function we(e){return!(!e||!e["__v_isReadonly"])}function xe(e){return!(!e||!e["__v_isShallow"])}function _e(e){return!!e&&!!e["__v_raw"]}function Se(e){const t=e&&e["__v_raw"];return t?Se(t):e}function Ce(e){return!(0,r.RI)(e,"__v_skip")&&Object.isExtensible(e)&&(0,r.Nj)(e,"__v_skip",!0),e}const Ee=e=>(0,r.Kn)(e)?ge(e):e,Oe=e=>(0,r.Kn)(e)?ye(e):e;function Re(e){return!!e&&!0===e["__v_isRef"]}function Me(e){return Ae(e,!1)}function je(e){return Ae(e,!0)}function Ae(e,t){return Re(e)?e:new Ie(e,t)}class Ie{constructor(e,t){this.dep=new M,this["__v_isRef"]=!0,this["__v_isShallow"]=!1,this._rawValue=t?e:Se(e),this._value=t?e:Ee(e),this["__v_isShallow"]=t}get value(){return this.dep.track(),this._value}set value(e){const t=this._rawValue,n=this["__v_isShallow"]||xe(e)||we(e);e=n?e:Se(e),(0,r.aU)(e,t)&&(this._rawValue=e,this._value=n?e:Ee(e),this.dep.trigger())}}function Te(e){return Re(e)?e.value:e}const Pe={get:(e,t,n)=>"__v_raw"===t?e:Te(Reflect.get(e,t,n)),set:(e,t,n,r)=>{const o=e[t];return Re(o)&&!Re(n)?(o.value=n,!0):Reflect.set(e,t,n,r)}};function Fe(e){return ke(e)?e:new Proxy(e,Pe)}class De{constructor(e,t,n){this.fn=e,this.setter=t,this._value=void 0,this.dep=new M(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=O-1,this.next=void 0,this.effect=this,this["__v_isReadonly"]=!t,this.isSSR=n}notify(){if(this.flags|=16,!(8&this.flags||s===this))return p(this,!0),!0}get value(){const e=this.dep.track();return b(this),e&&(e.version=this.dep.version),this._value}set value(e){this.setter&&this.setter(e)}}function Ne(e,t,n=!1){let o,s;(0,r.mf)(e)?o=e:(o=e.get,s=e.set);const i=new De(o,s,n);return i}const $e={},ze=new WeakMap;let Le;function Ue(e,t=!1,n=Le){if(n){let t=ze.get(n);t||ze.set(n,t=[]),t.push(e)}else 0}function Ze(e,t,n=r.kT){const{immediate:o,deep:s,once:i,scheduler:c,augmentJob:u,call:f}=n,d=e=>s?e:xe(e)||!1===s||0===s?Je(e,1):Je(e);let p,h,m,g,v=!1,y=!1;if(Re(e)?(h=()=>e.value,v=xe(e)):ke(e)?(h=()=>d(e),v=!0):(0,r.kJ)(e)?(y=!0,v=e.some((e=>ke(e)||xe(e))),h=()=>e.map((e=>Re(e)?e.value:ke(e)?d(e):(0,r.mf)(e)?f?f(e,2):e():void 0))):h=(0,r.mf)(e)?t?f?()=>f(e,2):e:()=>{if(m){S();try{m()}finally{C()}}const t=Le;Le=p;try{return f?f(e,3,[g]):e(g)}finally{Le=t}}:r.dG,t&&s){const e=h,t=!0===s?1/0:s;h=()=>Je(e(),t)}const b=l(),k=()=>{p.stop(),b&&b.active&&(0,r.Od)(b.effects,p)};if(i&&t){const e=t;t=(...t)=>{e(...t),k()}}let w=y?new Array(e.length).fill($e):$e;const x=e=>{if(1&p.flags&&(p.dirty||e))if(t){const e=p.run();if(s||v||(y?e.some(((e,t)=>(0,r.aU)(e,w[t]))):(0,r.aU)(e,w))){m&&m();const n=Le;Le=p;try{const r=[e,w===$e?void 0:y&&w[0]===$e?[]:w,g];w=e,f?f(t,3,r):t(...r)}finally{Le=n}}}else p.run()};return u&&u(x),p=new a(h),p.scheduler=c?()=>c(x,!1):x,g=e=>Ue(e,!1,p),m=p.onStop=()=>{const e=ze.get(p);if(e){if(f)f(e,4);else for(const t of e)t();ze.delete(p)}},t?o?x(!0):w=p.run():c?c(x.bind(null,!0),!0):p.run(),k.pause=p.pause.bind(p),k.resume=p.resume.bind(p),k.stop=k,k}function Je(e,t=1/0,n){if(t<=0||!(0,r.Kn)(e)||e["__v_skip"])return e;if(n=n||new Set,n.has(e))return e;if(n.add(e),t--,Re(e))Je(e.value,t,n);else if((0,r.kJ)(e))for(let r=0;r{Je(e,t,n)}));else if((0,r.PO)(e)){for(const r in e)Je(e[r],t,n);for(const r of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,r)&&Je(e[r],t,n)}return e}},252:function(e,t,n){n.d(t,{$d:function(){return i},Ah:function(){return le},FN:function(){return kn},Fl:function(){return $n},HY:function(){return Lt},JJ:function(){return He},Ko:function(){return ye},LL:function(){return me},Q6:function(){return U},U2:function(){return z},Uk:function(){return an},Us:function(){return pt},WI:function(){return be},Wm:function(){return on},Y3:function(){return g},Y8:function(){return F},YP:function(){return St},_:function(){return rn},aZ:function(){return Z},bv:function(){return re},f3:function(){return Be},h:function(){return zn},iD:function(){return Yt},ic:function(){return se},j4:function(){return Xt},kq:function(){return un},nJ:function(){return N},nK:function(){return L},up:function(){return pe},w5:function(){return R},wg:function(){return Bt},wy:function(){return M}});var r=n(262),o=n(577);function s(e,t,n,r){try{return r?e(...r):e()}catch(o){l(o,t,n)}}function i(e,t,n,r){if((0,o.mf)(e)){const i=s(e,t,n,r);return i&&(0,o.tI)(i)&&i.catch((e=>{l(e,t,n)})),i}if((0,o.kJ)(e)){const o=[];for(let s=0;s>>1,o=a[r],s=_(o);s=_(n)?a.push(e):a.splice(v(t),0,e),e.flags|=1,b()}}function b(){m||(m=h.then(S))}function k(e){(0,o.kJ)(e)?f.push(...e):d&&-1===e.id?d.splice(p+1,0,e):1&e.flags||(f.push(e),e.flags|=1),b()}function w(e,t,n=u+1){for(0;n_(e)-_(t)));if(f.length=0,d)return void d.push(...e);for(d=e,p=0;pnull==e.id?2&e.flags?-1:1/0:e.id;function S(e){o.dG;try{for(u=0;u{r._d&&Wt(-1);const o=O(t);let s;try{s=e(...n)}finally{O(o),r._d&&Wt(1)}return s};return r._n=!0,r._c=!0,r._d=!0,r}function M(e,t){if(null===C)return e;const n=Fn(C),s=e.dirs||(e.dirs=[]);for(let i=0;ie.__isTeleport;const T=Symbol("_leaveCb"),P=Symbol("_enterCb");function F(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return re((()=>{e.isMounted=!0})),ie((()=>{e.isUnmounting=!0})),e}const D=[Function,Array],N={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:D,onEnter:D,onAfterEnter:D,onEnterCancelled:D,onBeforeLeave:D,onLeave:D,onAfterLeave:D,onLeaveCancelled:D,onBeforeAppear:D,onAppear:D,onAfterAppear:D,onAppearCancelled:D};function $(e,t){const{leavingVNodes:n}=e;let r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function z(e,t,n,r,s){const{appear:l,mode:c,persisted:a=!1,onBeforeEnter:u,onEnter:f,onAfterEnter:d,onEnterCancelled:p,onBeforeLeave:h,onLeave:m,onAfterLeave:g,onLeaveCancelled:v,onBeforeAppear:y,onAppear:b,onAfterAppear:k,onAppearCancelled:w}=t,x=String(e.key),_=$(n,e),S=(e,t)=>{e&&i(e,r,9,t)},C=(e,t)=>{const n=t[1];S(e,t),(0,o.kJ)(e)?e.every((e=>e.length<=1))&&n():e.length<=1&&n()},E={mode:c,persisted:a,beforeEnter(t){let r=u;if(!n.isMounted){if(!l)return;r=y||u}t[T]&&t[T](!0);const o=_[x];o&&en(e,o)&&o.el[T]&&o.el[T](),S(r,[t])},enter(e){let t=f,r=d,o=p;if(!n.isMounted){if(!l)return;t=b||f,r=k||d,o=w||p}let s=!1;const i=e[P]=t=>{s||(s=!0,S(t?o:r,[e]),E.delayedLeave&&E.delayedLeave(),e[P]=void 0)};t?C(t,[e,i]):i()},leave(t,r){const o=String(e.key);if(t[P]&&t[P](!0),n.isUnmounting)return r();S(h,[t]);let s=!1;const i=t[T]=n=>{s||(s=!0,r(),S(n?v:g,[t]),t[T]=void 0,_[o]===e&&delete _[o])};_[o]=e,m?C(m,[t,i]):i()},clone(e){const o=z(e,t,n,r,s);return s&&s(o),o}};return E}function L(e,t){6&e.shapeFlag&&e.component?(e.transition=t,L(e.component.subTree,t)):128&e.shapeFlag?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function U(e,t=!1,n){let r=[],o=0;for(let s=0;s1)for(let s=0;s(0,o.l7)({name:e.name},t,{setup:e}))():e}function J(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}function V(e,t,n,i,l=!1){if((0,o.kJ)(e))return void e.forEach(((e,r)=>V(e,t&&((0,o.kJ)(t)?t[r]:t),n,i,l)));if(H(i)&&!l)return void(512&i.shapeFlag&&i.type.__asyncResolved&&i.component.subTree.component&&V(e,t,n,i.component.subTree));const c=4&i.shapeFlag?Fn(i.component):i.el,a=l?null:c,{i:u,r:f}=e;const d=t&&t.r,p=u.refs===o.kT?u.refs={}:u.refs,h=u.setupState,m=(0,r.IU)(h),g=h===o.kT?()=>!1:e=>(0,o.RI)(m,e);if(null!=d&&d!==f&&((0,o.HD)(d)?(p[d]=null,g(d)&&(h[d]=null)):(0,r.dq)(d)&&(d.value=null)),(0,o.mf)(f))s(f,u,12,[a,p]);else{const t=(0,o.HD)(f),s=(0,r.dq)(f);if(t||s){const r=()=>{if(e.f){const n=t?g(f)?h[f]:p[f]:f.value;l?(0,o.kJ)(n)&&(0,o.Od)(n,c):(0,o.kJ)(n)?n.includes(c)||n.push(c):t?(p[f]=[c],g(f)&&(h[f]=p[f])):(f.value=[c],e.k&&(p[e.k]=f.value))}else t?(p[f]=a,g(f)&&(h[f]=a)):s&&(f.value=a,e.k&&(p[e.k]=a))};a?(r.id=-1,dt(r,n)):r()}else 0}}(0,o.E9)().requestIdleCallback,(0,o.E9)().cancelIdleCallback;const H=e=>!!e.type.__asyncLoader +/*! #__NO_SIDE_EFFECTS__ */;const B=e=>e.type.__isKeepAlive;RegExp,RegExp;function G(e,t){return(0,o.kJ)(e)?e.some((e=>G(e,t))):(0,o.HD)(e)?e.split(",").includes(t):!!(0,o.Kj)(e)&&(e.lastIndex=0,e.test(t))}function q(e,t){K(e,"a",t)}function W(e,t){K(e,"da",t)}function K(e,t,n=bn){const r=e.__wdc||(e.__wdc=()=>{let t=n;while(t){if(t.isDeactivated)return;t=t.parent}return e()});if(ee(t,r,n),n){let e=n.parent;while(e&&e.parent)B(e.parent.vnode)&&Y(r,t,n,e),e=e.parent}}function Y(e,t,n,r){const s=ee(t,e,r,!0);le((()=>{(0,o.Od)(r[t],s)}),n)}function X(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function Q(e){return 128&e.shapeFlag?e.ssContent:e}function ee(e,t,n=bn,o=!1){if(n){const s=n[e]||(n[e]=[]),l=t.__weh||(t.__weh=(...o)=>{(0,r.Jd)();const s=_n(n),l=i(t,n,e,o);return s(),(0,r.lk)(),l});return o?s.unshift(l):s.push(l),l}}const te=e=>(t,n=bn)=>{Rn&&"sp"!==e||ee(e,((...e)=>t(...e)),n)},ne=te("bm"),re=te("m"),oe=te("bu"),se=te("u"),ie=te("bum"),le=te("um"),ce=te("sp"),ae=te("rtg"),ue=te("rtc");function fe(e,t=bn){ee("ec",e,t)}const de="components";function pe(e,t){return ge(de,e,!0,t)||e}const he=Symbol.for("v-ndc");function me(e){return(0,o.HD)(e)?ge(de,e,!1)||e:e||he}function ge(e,t,n=!0,r=!1){const s=C||bn;if(s){const n=s.type;if(e===de){const e=Dn(n,!1);if(e&&(e===t||e===(0,o._A)(t)||e===(0,o.kC)((0,o._A)(t))))return n}const i=ve(s[e]||n[e],t)||ve(s.appContext[e],t);return!i&&r?n:i}}function ve(e,t){return e&&(e[t]||e[(0,o._A)(t)]||e[(0,o.kC)((0,o._A)(t))])}function ye(e,t,n,s){let i;const l=n&&n[s],c=(0,o.kJ)(e);if(c||(0,o.HD)(e)){const n=c&&(0,r.PG)(e);let o=!1,s=!1;n&&(o=!(0,r.yT)(e),s=(0,r.$y)(e),e=(0,r.XB)(e)),i=new Array(e.length);for(let c=0,a=e.length;ct(e,n,void 0,l&&l[n])));else{const n=Object.keys(e);i=new Array(n.length);for(let r=0,o=n.length;r!Qt(e)||e.type!==Zt&&!(e.type===Lt&&!ke(e.children))))?e:null}const we=e=>e?Cn(e)?Fn(e):we(e.parent):null,xe=(0,o.l7)(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>we(e.parent),$root:e=>we(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Ae(e),$forceUpdate:e=>e.f||(e.f=()=>{y(e.update)}),$nextTick:e=>e.n||(e.n=g.bind(e.proxy)),$watch:e=>Et.bind(e)}),_e=(e,t)=>e!==o.kT&&!e.__isScriptSetup&&(0,o.RI)(e,t),Se={get({_:e},t){if("__v_skip"===t)return!0;const{ctx:n,setupState:s,data:i,props:l,accessCache:c,type:a,appContext:u}=e;let f;if("$"!==t[0]){const r=c[t];if(void 0!==r)switch(r){case 1:return s[t];case 2:return i[t];case 4:return n[t];case 3:return l[t]}else{if(_e(s,t))return c[t]=1,s[t];if(i!==o.kT&&(0,o.RI)(i,t))return c[t]=2,i[t];if((f=e.propsOptions[0])&&(0,o.RI)(f,t))return c[t]=3,l[t];if(n!==o.kT&&(0,o.RI)(n,t))return c[t]=4,n[t];Ee&&(c[t]=0)}}const d=xe[t];let p,h;return d?("$attrs"===t&&(0,r.j)(e.attrs,"get",""),d(e)):(p=a.__cssModules)&&(p=p[t])?p:n!==o.kT&&(0,o.RI)(n,t)?(c[t]=4,n[t]):(h=u.config.globalProperties,(0,o.RI)(h,t)?h[t]:void 0)},set({_:e},t,n){const{data:r,setupState:s,ctx:i}=e;return _e(s,t)?(s[t]=n,!0):r!==o.kT&&(0,o.RI)(r,t)?(r[t]=n,!0):!(0,o.RI)(e.props,t)&&(("$"!==t[0]||!(t.slice(1)in e))&&(i[t]=n,!0))},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:s,propsOptions:i}},l){let c;return!!n[l]||e!==o.kT&&(0,o.RI)(e,l)||_e(t,l)||(c=i[0])&&(0,o.RI)(c,l)||(0,o.RI)(r,l)||(0,o.RI)(xe,l)||(0,o.RI)(s.config.globalProperties,l)},defineProperty(e,t,n){return null!=n.get?e._.accessCache[t]=0:(0,o.RI)(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Ce(e){return(0,o.kJ)(e)?e.reduce(((e,t)=>(e[t]=null,e)),{}):e}let Ee=!0;function Oe(e){const t=Ae(e),n=e.proxy,s=e.ctx;Ee=!1,t.beforeCreate&&Me(t.beforeCreate,e,"bc");const{data:i,computed:l,methods:c,watch:a,provide:u,inject:f,created:d,beforeMount:p,mounted:h,beforeUpdate:m,updated:g,activated:v,deactivated:y,beforeDestroy:b,beforeUnmount:k,destroyed:w,unmounted:x,render:_,renderTracked:S,renderTriggered:C,errorCaptured:E,serverPrefetch:O,expose:R,inheritAttrs:M,components:j,directives:A,filters:I}=t,T=null;if(f&&Re(f,s,T),c)for(const r in c){const e=c[r];(0,o.mf)(e)&&(s[r]=e.bind(n))}if(i){0;const t=i.call(n,n);0,(0,o.Kn)(t)&&(e.data=(0,r.qj)(t))}if(Ee=!0,l)for(const r in l){const e=l[r],t=(0,o.mf)(e)?e.bind(n,n):(0,o.mf)(e.get)?e.get.bind(n,n):o.dG;0;const i=!(0,o.mf)(e)&&(0,o.mf)(e.set)?e.set.bind(n):o.dG,c=$n({get:t,set:i});Object.defineProperty(s,r,{enumerable:!0,configurable:!0,get:()=>c.value,set:e=>c.value=e})}if(a)for(const r in a)je(a[r],s,n,r);if(u){const e=(0,o.mf)(u)?u.call(n):u;Reflect.ownKeys(e).forEach((t=>{He(t,e[t])}))}function P(e,t){(0,o.kJ)(t)?t.forEach((t=>e(t.bind(n)))):t&&e(t.bind(n))}if(d&&Me(d,e,"c"),P(ne,p),P(re,h),P(oe,m),P(se,g),P(q,v),P(W,y),P(fe,E),P(ue,S),P(ae,C),P(ie,k),P(le,x),P(ce,O),(0,o.kJ)(R))if(R.length){const t=e.exposed||(e.exposed={});R.forEach((e=>{Object.defineProperty(t,e,{get:()=>n[e],set:t=>n[e]=t,enumerable:!0})}))}else e.exposed||(e.exposed={});_&&e.render===o.dG&&(e.render=_),null!=M&&(e.inheritAttrs=M),j&&(e.components=j),A&&(e.directives=A),O&&J(e)}function Re(e,t,n=o.dG){(0,o.kJ)(e)&&(e=De(e));for(const s in e){const n=e[s];let i;i=(0,o.Kn)(n)?"default"in n?Be(n.from||s,n.default,!0):Be(n.from||s):Be(n),(0,r.dq)(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:e=>i.value=e}):t[s]=i}}function Me(e,t,n){i((0,o.kJ)(e)?e.map((e=>e.bind(t.proxy))):e.bind(t.proxy),t,n)}function je(e,t,n,r){let s=r.includes(".")?Ot(n,r):()=>n[r];if((0,o.HD)(e)){const n=t[e];(0,o.mf)(n)&&St(s,n)}else if((0,o.mf)(e))St(s,e.bind(n));else if((0,o.Kn)(e))if((0,o.kJ)(e))e.forEach((e=>je(e,t,n,r)));else{const r=(0,o.mf)(e.handler)?e.handler.bind(n):t[e.handler];(0,o.mf)(r)&&St(s,r,e)}else 0}function Ae(e){const t=e.type,{mixins:n,extends:r}=t,{mixins:s,optionsCache:i,config:{optionMergeStrategies:l}}=e.appContext,c=i.get(t);let a;return c?a=c:s.length||n||r?(a={},s.length&&s.forEach((e=>Ie(a,e,l,!0))),Ie(a,t,l)):a=t,(0,o.Kn)(t)&&i.set(t,a),a}function Ie(e,t,n,r=!1){const{mixins:o,extends:s}=t;s&&Ie(e,s,n,!0),o&&o.forEach((t=>Ie(e,t,n,!0)));for(const i in t)if(r&&"expose"===i);else{const r=Te[i]||n&&n[i];e[i]=r?r(e[i],t[i]):t[i]}return e}const Te={data:Pe,props:ze,emits:ze,methods:$e,computed:$e,beforeCreate:Ne,created:Ne,beforeMount:Ne,mounted:Ne,beforeUpdate:Ne,updated:Ne,beforeDestroy:Ne,beforeUnmount:Ne,destroyed:Ne,unmounted:Ne,activated:Ne,deactivated:Ne,errorCaptured:Ne,serverPrefetch:Ne,components:$e,directives:$e,watch:Le,provide:Pe,inject:Fe};function Pe(e,t){return t?e?function(){return(0,o.l7)((0,o.mf)(e)?e.call(this,this):e,(0,o.mf)(t)?t.call(this,this):t)}:t:e}function Fe(e,t){return $e(De(e),De(t))}function De(e){if((0,o.kJ)(e)){const t={};for(let n=0;n1)return n&&(0,o.mf)(t)?t.call(r&&r.proxy):t}else 0}const Ge={},qe=()=>Object.create(Ge),We=e=>Object.getPrototypeOf(e)===Ge;function Ke(e,t,n,o=!1){const s={},i=qe();e.propsDefaults=Object.create(null),Xe(e,t,s,i);for(const r in e.propsOptions[0])r in s||(s[r]=void 0);n?e.props=o?s:(0,r.Um)(s):e.type.props?e.props=s:e.props=i,e.attrs=i}function Ye(e,t,n,s){const{props:i,attrs:l,vnode:{patchFlag:c}}=e,a=(0,r.IU)(i),[u]=e.propsOptions;let f=!1;if(!(s||c>0)||16&c){let r;Xe(e,t,i,l)&&(f=!0);for(const s in a)t&&((0,o.RI)(t,s)||(r=(0,o.rs)(s))!==s&&(0,o.RI)(t,r))||(u?!n||void 0===n[s]&&void 0===n[r]||(i[s]=Qe(u,a,s,void 0,e,!0)):delete i[s]);if(l!==a)for(const e in l)t&&(0,o.RI)(t,e)||(delete l[e],f=!0)}else if(8&c){const n=e.vnode.dynamicProps;for(let r=0;r{a=!0;const[n,r]=tt(e,t,!0);(0,o.l7)(l,n),r&&c.push(...r)};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}if(!i&&!a)return(0,o.Kn)(e)&&r.set(e,o.Z6),o.Z6;if((0,o.kJ)(i))for(let f=0;f"_"===e||"__"===e||"_ctx"===e||"$stable"===e,ot=e=>(0,o.kJ)(e)?e.map(fn):[fn(e)],st=(e,t,n)=>{if(t._n)return t;const r=R(((...e)=>ot(t(...e))),n);return r._c=!1,r},it=(e,t,n)=>{const r=e._ctx;for(const s in e){if(rt(s))continue;const n=e[s];if((0,o.mf)(n))t[s]=st(s,n,r);else if(null!=n){0;const e=ot(n);t[s]=()=>e}}},lt=(e,t)=>{const n=ot(t);e.slots.default=()=>n},ct=(e,t,n)=>{for(const r in t)!n&&rt(r)||(e[r]=t[r])},at=(e,t,n)=>{const r=e.slots=qe();if(32&e.vnode.shapeFlag){const e=t.__;e&&(0,o.Nj)(r,"__",e,!0);const s=t._;s?(ct(r,t,n),n&&(0,o.Nj)(r,"_",s,!0)):it(t,r)}else t&<(e,t)},ut=(e,t,n)=>{const{vnode:r,slots:s}=e;let i=!0,l=o.kT;if(32&r.shapeFlag){const e=t._;e?n&&1===e?i=!1:ct(s,t,n):(i=!t.$stable,it(t,s)),l=t}else t&&(lt(e,t),l={default:1});if(i)for(const o in s)rt(o)||null!=l[o]||delete s[o]};function ft(){"boolean"!==typeof __VUE_PROD_HYDRATION_MISMATCH_DETAILS__&&((0,o.E9)().__VUE_PROD_HYDRATION_MISMATCH_DETAILS__=!1)}const dt=zt;function pt(e){return ht(e)}function ht(e,t){ft();const n=(0,o.E9)();n.__VUE__=!0;const{insert:s,remove:i,patchProp:l,createElement:c,createText:a,createComment:u,setText:f,setElementText:d,parentNode:p,nextSibling:h,setScopeId:m=o.dG,insertStaticContent:g}=e,v=(e,t,n,r=null,o=null,s=null,i,l=null,c=!!t.dynamicChildren)=>{if(e===t)return;e&&!en(e,t)&&(r=Q(e),q(e,o,s,!0),e=null),-2===t.patchFlag&&(c=!1,t.dynamicChildren=null);const{type:a,ref:u,shapeFlag:f}=t;switch(a){case Ut:b(e,t,n,r);break;case Zt:k(e,t,n,r);break;case Jt:null==e&&_(t,n,r,i);break;case Lt:F(e,t,n,r,o,s,i,l,c);break;default:1&f?E(e,t,n,r,o,s,i,l,c):6&f?D(e,t,n,r,o,s,i,l,c):(64&f||128&f)&&a.process(e,t,n,r,o,s,i,l,c,ne)}null!=u&&o?V(u,e&&e.ref,s,t||e,!t):null==u&&e&&null!=e.ref&&V(e.ref,null,s,e,!0)},b=(e,t,n,r)=>{if(null==e)s(t.el=a(t.children),n,r);else{const n=t.el=e.el;t.children!==e.children&&f(n,t.children)}},k=(e,t,n,r)=>{null==e?s(t.el=u(t.children||""),n,r):t.el=e.el},_=(e,t,n,r)=>{[e.el,e.anchor]=g(e.children,t,n,r,e.el,e.anchor)},S=({el:e,anchor:t},n,r)=>{let o;while(e&&e!==t)o=h(e),s(e,n,r),e=o;s(t,n,r)},C=({el:e,anchor:t})=>{let n;while(e&&e!==t)n=h(e),i(e),e=n;i(t)},E=(e,t,n,r,o,s,i,l,c)=>{"svg"===t.type?i="svg":"math"===t.type&&(i="mathml"),null==e?O(t,n,r,o,s,i,l,c):I(e,t,o,s,i,l,c)},O=(e,t,n,r,i,a,u,f)=>{let p,h;const{props:m,shapeFlag:g,transition:v,dirs:y}=e;if(p=e.el=c(e.type,a,m&&m.is,m),8&g?d(p,e.children):16&g&&M(e.children,p,null,r,i,mt(e,a),u,f),y&&j(e,null,r,"created"),R(p,e,e.scopeId,u,r),m){for(const e in m)"value"===e||(0,o.Gg)(e)||l(p,e,null,m[e],a,r);"value"in m&&l(p,"value",null,m.value,a),(h=m.onVnodeBeforeMount)&&mn(h,r,e)}y&&j(e,null,r,"beforeMount");const b=vt(i,v);b&&v.beforeEnter(p),s(p,t,n),((h=m&&m.onVnodeMounted)||b||y)&&dt((()=>{h&&mn(h,r,e),b&&v.enter(p),y&&j(e,null,r,"mounted")}),i)},R=(e,t,n,r,o)=>{if(n&&m(e,n),r)for(let s=0;s{for(let a=c;a{const a=t.el=e.el;let{patchFlag:u,dynamicChildren:f,dirs:p}=t;u|=16&e.patchFlag;const h=e.props||o.kT,m=t.props||o.kT;let g;if(n&>(n,!1),(g=m.onVnodeBeforeUpdate)&&mn(g,n,t,e),p&&j(t,e,n,"beforeUpdate"),n&>(n,!0),(h.innerHTML&&null==m.innerHTML||h.textContent&&null==m.textContent)&&d(a,""),f?T(e.dynamicChildren,f,a,n,r,mt(t,s),i):c||U(e,t,a,null,n,r,mt(t,s),i,!1),u>0){if(16&u)P(a,h,m,n,s);else if(2&u&&h.class!==m.class&&l(a,"class",null,m.class,s),4&u&&l(a,"style",h.style,m.style,s),8&u){const e=t.dynamicProps;for(let t=0;t{g&&mn(g,n,t,e),p&&j(t,e,n,"updated")}),r)},T=(e,t,n,r,o,s,i)=>{for(let l=0;l{if(t!==n){if(t!==o.kT)for(const i in t)(0,o.Gg)(i)||i in n||l(e,i,t[i],null,s,r);for(const i in n){if((0,o.Gg)(i))continue;const c=n[i],a=t[i];c!==a&&"value"!==i&&l(e,i,a,c,s,r)}"value"in n&&l(e,"value",t.value,n.value,s)}},F=(e,t,n,r,o,i,l,c,u)=>{const f=t.el=e?e.el:a(""),d=t.anchor=e?e.anchor:a("");let{patchFlag:p,dynamicChildren:h,slotScopeIds:m}=t;m&&(c=c?c.concat(m):m),null==e?(s(f,n,r),s(d,n,r),M(t.children||[],n,d,o,i,l,c,u)):p>0&&64&p&&h&&e.dynamicChildren?(T(e.dynamicChildren,h,n,o,i,l,c),(null!=t.key||o&&t===o.subTree)&&yt(e,t,!0)):U(e,t,n,d,o,i,l,c,u)},D=(e,t,n,r,o,s,i,l,c)=>{t.slotScopeIds=l,null==e?512&t.shapeFlag?o.ctx.activate(t,n,r,i,c):N(t,n,r,o,s,i,c):$(e,t,c)},N=(e,t,n,r,o,s,i)=>{const l=e.component=yn(e,r,o);if(B(e)&&(l.ctx.renderer=ne),Mn(l,!1,i),l.asyncDep){if(o&&o.registerDep(l,z,i),!e.el){const r=l.subTree=on(Zt);k(null,r,t,n),e.placeholder=r.el}}else z(l,e,t,n,o,s,i)},$=(e,t,n)=>{const r=t.component=e.component;if(Ft(e,t,n)){if(r.asyncDep&&!r.asyncResolved)return void L(r,t,n);r.next=t,r.update()}else t.el=e.el,r.vnode=t},z=(e,t,n,s,i,l,c)=>{const a=()=>{if(e.isMounted){let{next:t,bu:n,u:r,parent:s,vnode:u}=e;{const n=kt(e);if(n)return t&&(t.el=u.el,L(e,t,c)),void n.asyncDep.then((()=>{e.isUnmounted||a()}))}let f,d=t;0,gt(e,!1),t?(t.el=u.el,L(e,t,c)):t=u,n&&(0,o.ir)(n),(f=t.props&&t.props.onVnodeBeforeUpdate)&&mn(f,s,t,u),gt(e,!0);const h=It(e);0;const m=e.subTree;e.subTree=h,v(m,h,p(m.el),Q(m),e,i,l),t.el=h.el,null===d&&Nt(e,h.el),r&&dt(r,i),(f=t.props&&t.props.onVnodeUpdated)&&dt((()=>mn(f,s,t,u)),i)}else{let r;const{el:c,props:a}=t,{bm:u,m:f,parent:d,root:p,type:h}=e,m=H(t);if(gt(e,!1),u&&(0,o.ir)(u),!m&&(r=a&&a.onVnodeBeforeMount)&&mn(r,d,t),gt(e,!0),c&&oe){const t=()=>{e.subTree=It(e),oe(c,e.subTree,e,i,null)};m&&h.__asyncHydrate?h.__asyncHydrate(c,e,t):t()}else{p.ce&&!1!==p.ce._def.shadowRoot&&p.ce._injectChildStyle(h);const r=e.subTree=It(e);0,v(null,r,n,s,e,i,l),t.el=r.el}if(f&&dt(f,i),!m&&(r=a&&a.onVnodeMounted)){const e=t;dt((()=>mn(r,d,e)),i)}(256&t.shapeFlag||d&&H(d.vnode)&&256&d.vnode.shapeFlag)&&e.a&&dt(e.a,i),e.isMounted=!0,t=n=s=null}};e.scope.on();const u=e.effect=new r.qq(a);e.scope.off();const f=e.update=u.run.bind(u),d=e.job=u.runIfDirty.bind(u);d.i=e,d.id=e.uid,u.scheduler=()=>y(d),gt(e,!0),f()},L=(e,t,n)=>{t.component=e;const o=e.vnode.props;e.vnode=t,e.next=null,Ye(e,t.props,o,n),ut(e,t.children,n),(0,r.Jd)(),w(e),(0,r.lk)()},U=(e,t,n,r,o,s,i,l,c=!1)=>{const a=e&&e.children,u=e?e.shapeFlag:0,f=t.children,{patchFlag:p,shapeFlag:h}=t;if(p>0){if(128&p)return void J(a,f,n,r,o,s,i,l,c);if(256&p)return void Z(a,f,n,r,o,s,i,l,c)}8&h?(16&u&&X(a,o,s),f!==a&&d(n,f)):16&u?16&h?J(a,f,n,r,o,s,i,l,c):X(a,o,s,!0):(8&u&&d(n,""),16&h&&M(f,n,r,o,s,i,l,c))},Z=(e,t,n,r,s,i,l,c,a)=>{e=e||o.Z6,t=t||o.Z6;const u=e.length,f=t.length,d=Math.min(u,f);let p;for(p=0;pf?X(e,s,i,!0,!1,d):M(t,n,r,s,i,l,c,a,d)},J=(e,t,n,r,s,i,l,c,a)=>{let u=0;const f=t.length;let d=e.length-1,p=f-1;while(u<=d&&u<=p){const r=e[u],o=t[u]=a?dn(t[u]):fn(t[u]);if(!en(r,o))break;v(r,o,n,null,s,i,l,c,a),u++}while(u<=d&&u<=p){const r=e[d],o=t[p]=a?dn(t[p]):fn(t[p]);if(!en(r,o))break;v(r,o,n,null,s,i,l,c,a),d--,p--}if(u>d){if(u<=p){const e=p+1,o=ep)while(u<=d)q(e[u],s,i,!0),u++;else{const h=u,m=u,g=new Map;for(u=m;u<=p;u++){const e=t[u]=a?dn(t[u]):fn(t[u]);null!=e.key&&g.set(e.key,u)}let y,b=0;const k=p-m+1;let w=!1,x=0;const _=new Array(k);for(u=0;u=k){q(r,s,i,!0);continue}let o;if(null!=r.key)o=g.get(r.key);else for(y=m;y<=p;y++)if(0===_[y-m]&&en(r,t[y])){o=y;break}void 0===o?q(r,s,i,!0):(_[o-m]=u+1,o>=x?x=o:w=!0,v(r,t[o],n,null,s,i,l,c,a),b++)}const S=w?bt(_):o.Z6;for(y=S.length-1,u=k-1;u>=0;u--){const e=m+u,o=t[e],d=t[e+1],p=e+1{const{el:l,type:c,transition:a,children:u,shapeFlag:f}=e;if(6&f)return void G(e.component.subTree,t,n,r);if(128&f)return void e.suspense.move(t,n,r);if(64&f)return void c.move(e,t,n,ne);if(c===Lt){s(l,t,n);for(let e=0;ea.enter(l)),o);else{const{leave:r,delayLeave:o,afterLeave:c}=a,u=()=>{e.ctx.isUnmounted?i(l):s(l,t,n)},f=()=>{r(l,(()=>{u(),c&&c()}))};o?o(l,u,f):f()}else s(l,t,n)},q=(e,t,n,o=!1,s=!1)=>{const{type:i,props:l,ref:c,children:a,dynamicChildren:u,shapeFlag:f,patchFlag:d,dirs:p,cacheIndex:h}=e;if(-2===d&&(s=!1),null!=c&&((0,r.Jd)(),V(c,null,n,e,!0),(0,r.lk)()),null!=h&&(t.renderCache[h]=void 0),256&f)return void t.ctx.deactivate(e);const m=1&f&&p,g=!H(e);let v;if(g&&(v=l&&l.onVnodeBeforeUnmount)&&mn(v,t,e),6&f)Y(e.component,n,o);else{if(128&f)return void e.suspense.unmount(n,o);m&&j(e,null,t,"beforeUnmount"),64&f?e.type.remove(e,t,n,ne,o):u&&!u.hasOnce&&(i!==Lt||d>0&&64&d)?X(u,t,n,!1,!0):(i===Lt&&384&d||!s&&16&f)&&X(a,t,n),o&&W(e)}(g&&(v=l&&l.onVnodeUnmounted)||m)&&dt((()=>{v&&mn(v,t,e),m&&j(e,null,t,"unmounted")}),n)},W=e=>{const{type:t,el:n,anchor:r,transition:o}=e;if(t===Lt)return void K(n,r);if(t===Jt)return void C(e);const s=()=>{i(n),o&&!o.persisted&&o.afterLeave&&o.afterLeave()};if(1&e.shapeFlag&&o&&!o.persisted){const{leave:t,delayLeave:r}=o,i=()=>t(n,s);r?r(e.el,s,i):i()}else s()},K=(e,t)=>{let n;while(e!==t)n=h(e),i(e),e=n;i(t)},Y=(e,t,n)=>{const{bum:r,scope:s,job:i,subTree:l,um:c,m:a,a:u,parent:f,slots:{__:d}}=e;wt(a),wt(u),r&&(0,o.ir)(r),f&&(0,o.kJ)(d)&&d.forEach((e=>{f.renderCache[e]=void 0})),s.stop(),i&&(i.flags|=8,q(l,e,t,n)),c&&dt(c,t),dt((()=>{e.isUnmounted=!0}),t),t&&t.pendingBranch&&!t.isUnmounted&&e.asyncDep&&!e.asyncResolved&&e.suspenseId===t.pendingId&&(t.deps--,0===t.deps&&t.resolve())},X=(e,t,n,r=!1,o=!1,s=0)=>{for(let i=s;i{if(6&e.shapeFlag)return Q(e.component.subTree);if(128&e.shapeFlag)return e.suspense.next();const t=h(e.anchor||e.el),n=t&&t[A];return n?h(n):t};let ee=!1;const te=(e,t,n)=>{null==e?t._vnode&&q(t._vnode,null,null,!0):v(t._vnode||null,e,t,null,null,null,n),t._vnode=e,ee||(ee=!0,w(),x(),ee=!1)},ne={p:v,um:q,m:G,r:W,mt:N,mc:M,pc:U,pbc:T,n:Q,o:e};let re,oe;return t&&([re,oe]=t(ne)),{render:te,hydrate:re,createApp:Je(te,re)}}function mt({type:e,props:t},n){return"svg"===n&&"foreignObject"===e||"mathml"===n&&"annotation-xml"===e&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function gt({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function vt(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function yt(e,t,n=!1){const r=e.children,s=t.children;if((0,o.kJ)(r)&&(0,o.kJ)(s))for(let o=0;o>1,e[n[l]]0&&(t[r]=n[s-1]),n[s]=r)}}s=n.length,i=n[s-1];while(s-- >0)n[s]=i,i=t[i];return n}function kt(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:kt(t)}function wt(e){if(e)for(let t=0;t{{const e=Be(xt);return e}};function St(e,t,n){return Ct(e,t,n)}function Ct(e,t,n=o.kT){const{immediate:s,deep:l,flush:c,once:a}=n;const u=(0,o.l7)({},n);const f=t&&s||!t&&"post"!==c;let d;if(Rn)if("sync"===c){const e=_t();d=e.__watcherHandles||(e.__watcherHandles=[])}else if(!f){const e=()=>{};return e.stop=o.dG,e.resume=o.dG,e.pause=o.dG,e}const p=bn;u.call=(e,t,n)=>i(e,p,t,n);let h=!1;"post"===c?u.scheduler=e=>{dt(e,p&&p.suspense)}:"sync"!==c&&(h=!0,u.scheduler=(e,t)=>{t?e():y(e)}),u.augmentJob=e=>{t&&(e.flags|=4),h&&(e.flags|=2,p&&(e.id=p.uid,e.i=p))};const m=(0,r.YP)(e,t,u);return Rn&&(d?d.push(m):f&&m()),m}function Et(e,t,n){const r=this.proxy,s=(0,o.HD)(e)?e.includes(".")?Ot(r,e):()=>r[e]:e.bind(r,r);let i;(0,o.mf)(t)?i=t:(i=t.handler,n=t);const l=_n(this),c=Ct(s,i.bind(r),n);return l(),c}function Ot(e,t){const n=t.split(".");return()=>{let t=e;for(let e=0;e"modelValue"===t||"model-value"===t?e.modelModifiers:e[`${t}Modifiers`]||e[`${(0,o._A)(t)}Modifiers`]||e[`${(0,o.rs)(t)}Modifiers`];function Mt(e,t,...n){if(e.isUnmounted)return;const r=e.vnode.props||o.kT;let s=n;const l=t.startsWith("update:"),c=l&&Rt(r,t.slice(7));let a;c&&(c.trim&&(s=n.map((e=>(0,o.HD)(e)?e.trim():e))),c.number&&(s=n.map(o.h5)));let u=r[a=(0,o.hR)(t)]||r[a=(0,o.hR)((0,o._A)(t))];!u&&l&&(u=r[a=(0,o.hR)((0,o.rs)(t))]),u&&i(u,e,6,s);const f=r[a+"Once"];if(f){if(e.emitted){if(e.emitted[a])return}else e.emitted={};e.emitted[a]=!0,i(f,e,6,s)}}function jt(e,t,n=!1){const r=t.emitsCache,s=r.get(e);if(void 0!==s)return s;const i=e.emits;let l={},c=!1;if(!(0,o.mf)(e)){const r=e=>{const n=jt(e,t,!0);n&&(c=!0,(0,o.l7)(l,n))};!n&&t.mixins.length&&t.mixins.forEach(r),e.extends&&r(e.extends),e.mixins&&e.mixins.forEach(r)}return i||c?((0,o.kJ)(i)?i.forEach((e=>l[e]=null)):(0,o.l7)(l,i),(0,o.Kn)(e)&&r.set(e,l),l):((0,o.Kn)(e)&&r.set(e,null),null)}function At(e,t){return!(!e||!(0,o.F7)(t))&&(t=t.slice(2).replace(/Once$/,""),(0,o.RI)(e,t[0].toLowerCase()+t.slice(1))||(0,o.RI)(e,(0,o.rs)(t))||(0,o.RI)(e,t))}function It(e){const{type:t,vnode:n,proxy:r,withProxy:s,propsOptions:[i],slots:c,attrs:a,emit:u,render:f,renderCache:d,props:p,data:h,setupState:m,ctx:g,inheritAttrs:v}=e,y=O(e);let b,k;try{if(4&n.shapeFlag){const e=s||r,t=e;b=fn(f.call(t,e,d,p,m,h,g)),k=a}else{const e=t;0,b=fn(e.length>1?e(p,{attrs:a,slots:c,emit:u}):e(p,null)),k=t.props?a:Tt(a)}}catch(x){Vt.length=0,l(x,e,1),b=on(Zt)}let w=b;if(k&&!1!==v){const e=Object.keys(k),{shapeFlag:t}=w;e.length&&7&t&&(i&&e.some(o.tR)&&(k=Pt(k,i)),w=cn(w,k,!1,!0))}return n.dirs&&(w=cn(w,null,!1,!0),w.dirs=w.dirs?w.dirs.concat(n.dirs):n.dirs),n.transition&&L(w,n.transition),b=w,O(y),b}const Tt=e=>{let t;for(const n in e)("class"===n||"style"===n||(0,o.F7)(n))&&((t||(t={}))[n]=e[n]);return t},Pt=(e,t)=>{const n={};for(const r in e)(0,o.tR)(r)&&r.slice(9)in t||(n[r]=e[r]);return n};function Ft(e,t,n){const{props:r,children:o,component:s}=e,{props:i,children:l,patchFlag:c}=t,a=s.emitsOptions;if(t.dirs||t.transition)return!0;if(!(n&&c>=0))return!(!o&&!l||l&&l.$stable)||r!==i&&(r?!i||Dt(r,i,a):!!i);if(1024&c)return!0;if(16&c)return r?Dt(r,i,a):!!i;if(8&c){const e=t.dynamicProps;for(let t=0;te.__isSuspense;function zt(e,t){t&&t.pendingBranch?(0,o.kJ)(e)?t.effects.push(...e):t.effects.push(e):k(e)}const Lt=Symbol.for("v-fgt"),Ut=Symbol.for("v-txt"),Zt=Symbol.for("v-cmt"),Jt=Symbol.for("v-stc"),Vt=[];let Ht=null;function Bt(e=!1){Vt.push(Ht=e?null:[])}function Gt(){Vt.pop(),Ht=Vt[Vt.length-1]||null}let qt=1;function Wt(e,t=!1){qt+=e,e<0&&Ht&&t&&(Ht.hasOnce=!0)}function Kt(e){return e.dynamicChildren=qt>0?Ht||o.Z6:null,Gt(),qt>0&&Ht&&Ht.push(e),e}function Yt(e,t,n,r,o,s){return Kt(rn(e,t,n,r,o,s,!0))}function Xt(e,t,n,r,o){return Kt(on(e,t,n,r,o,!0))}function Qt(e){return!!e&&!0===e.__v_isVNode}function en(e,t){return e.type===t.type&&e.key===t.key}const tn=({key:e})=>null!=e?e:null,nn=({ref:e,ref_key:t,ref_for:n})=>("number"===typeof e&&(e=""+e),null!=e?(0,o.HD)(e)||(0,r.dq)(e)||(0,o.mf)(e)?{i:C,r:e,k:t,f:!!n}:e:null);function rn(e,t=null,n=null,r=0,s=null,i=(e===Lt?0:1),l=!1,c=!1){const a={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&tn(t),ref:t&&nn(t),scopeId:E,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:r,dynamicProps:s,dynamicChildren:null,appContext:null,ctx:C};return c?(pn(a,n),128&i&&e.normalize(a)):n&&(a.shapeFlag|=(0,o.HD)(n)?8:16),qt>0&&!l&&Ht&&(a.patchFlag>0||6&i)&&32!==a.patchFlag&&Ht.push(a),a}const on=sn;function sn(e,t=null,n=null,s=0,i=null,l=!1){if(e&&e!==he||(e=Zt),Qt(e)){const r=cn(e,t,!0);return n&&pn(r,n),qt>0&&!l&&Ht&&(6&r.shapeFlag?Ht[Ht.indexOf(e)]=r:Ht.push(r)),r.patchFlag=-2,r}if(Nn(e)&&(e=e.__vccOpts),t){t=ln(t);let{class:e,style:n}=t;e&&!(0,o.HD)(e)&&(t.class=(0,o.C_)(e)),(0,o.Kn)(n)&&((0,r.X3)(n)&&!(0,o.kJ)(n)&&(n=(0,o.l7)({},n)),t.style=(0,o.j5)(n))}const c=(0,o.HD)(e)?1:$t(e)?128:I(e)?64:(0,o.Kn)(e)?4:(0,o.mf)(e)?2:0;return rn(e,t,n,s,i,c,l,!0)}function ln(e){return e?(0,r.X3)(e)||We(e)?(0,o.l7)({},e):e:null}function cn(e,t,n=!1,r=!1){const{props:s,ref:i,patchFlag:l,children:c,transition:a}=e,u=t?hn(s||{},t):s,f={__v_isVNode:!0,__v_skip:!0,type:e.type,props:u,key:u&&tn(u),ref:t&&t.ref?n&&i?(0,o.kJ)(i)?i.concat(nn(t)):[i,nn(t)]:nn(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:c,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Lt?-1===l?16:16|l:l,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:a,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&cn(e.ssContent),ssFallback:e.ssFallback&&cn(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return a&&r&&L(f,a.clone(f)),f}function an(e=" ",t=0){return on(Ut,null,e,t)}function un(e="",t=!1){return t?(Bt(),Xt(Zt,null,e)):on(Zt,null,e)}function fn(e){return null==e||"boolean"===typeof e?on(Zt):(0,o.kJ)(e)?on(Lt,null,e.slice()):Qt(e)?dn(e):on(Ut,null,String(e))}function dn(e){return null===e.el&&-1!==e.patchFlag||e.memo?e:cn(e)}function pn(e,t){let n=0;const{shapeFlag:r}=e;if(null==t)t=null;else if((0,o.kJ)(t))n=16;else if("object"===typeof t){if(65&r){const n=t.default;return void(n&&(n._c&&(n._d=!1),pn(e,n()),n._c&&(n._d=!0)))}{n=32;const r=t._;r||We(t)?3===r&&C&&(1===C.slots._?t._=1:(t._=2,e.patchFlag|=1024)):t._ctx=C}}else(0,o.mf)(t)?(t={default:t,_ctx:C},n=32):(t=String(t),64&r?(n=16,t=[an(t)]):n=8);e.children=t,e.shapeFlag|=n}function hn(...e){const t={};for(let n=0;nbn||C;let wn,xn;{const e=(0,o.E9)(),t=(t,n)=>{let r;return(r=e[t])||(r=e[t]=[]),r.push(n),e=>{r.length>1?r.forEach((t=>t(e))):r[0](e)}};wn=t("__VUE_INSTANCE_SETTERS__",(e=>bn=e)),xn=t("__VUE_SSR_SETTERS__",(e=>Rn=e))}const _n=e=>{const t=bn;return wn(e),e.scope.on(),()=>{e.scope.off(),wn(t)}},Sn=()=>{bn&&bn.scope.off(),wn(null)};function Cn(e){return 4&e.vnode.shapeFlag}let En,On,Rn=!1;function Mn(e,t=!1,n=!1){t&&xn(t);const{props:r,children:o}=e.vnode,s=Cn(e);Ke(e,r,s,t),at(e,o,n||t);const i=s?jn(e,t):void 0;return t&&xn(!1),i}function jn(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Se);const{setup:i}=n;if(i){(0,r.Jd)();const n=e.setupContext=i.length>1?Pn(e):null,c=_n(e),a=s(i,e,0,[e.props,n]),u=(0,o.tI)(a);if((0,r.lk)(),c(),!u&&!e.sp||H(e)||J(e),u){if(a.then(Sn,Sn),t)return a.then((n=>{An(e,n,t)})).catch((t=>{l(t,e,0)}));e.asyncDep=a}else An(e,a,t)}else In(e,t)}function An(e,t,n){(0,o.mf)(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:(0,o.Kn)(t)&&(e.setupState=(0,r.WL)(t)),In(e,n)}function In(e,t,n){const s=e.type;if(!e.render){if(!t&&En&&!s.render){const t=s.template||Ae(e).template;if(t){0;const{isCustomElement:n,compilerOptions:r}=e.appContext.config,{delimiters:i,compilerOptions:l}=s,c=(0,o.l7)((0,o.l7)({isCustomElement:n,delimiters:i},r),l);s.render=En(t,c)}}e.render=s.render||o.dG,On&&On(e)}{const t=_n(e);(0,r.Jd)();try{Oe(e)}finally{(0,r.lk)(),t()}}}const Tn={get(e,t){return(0,r.j)(e,"get",""),e[t]}};function Pn(e){const t=t=>{e.exposed=t||{}};return{attrs:new Proxy(e.attrs,Tn),slots:e.slots,emit:e.emit,expose:t}}function Fn(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy((0,r.WL)((0,r.Xl)(e.exposed)),{get(t,n){return n in t?t[n]:n in xe?xe[n](e):void 0},has(e,t){return t in e||t in xe}})):e.proxy}function Dn(e,t=!0){return(0,o.mf)(e)?e.displayName||e.name:e.name||t&&e.__name}function Nn(e){return(0,o.mf)(e)&&"__vccOpts"in e}const $n=(e,t)=>{const n=(0,r.Fl)(e,t,Rn);return n};function zn(e,t,n){const r=arguments.length;return 2===r?(0,o.Kn)(t)&&!(0,o.kJ)(t)?Qt(t)?on(e,null,[t]):on(e,t):on(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):3===r&&Qt(n)&&(n=[n]),on(e,t,n))}const Ln="3.5.18"},963:function(e,t,n){n.d(t,{D2:function(){return X},bM:function(){return H},iM:function(){return K},ri:function(){return ne}});var r=n(252),o=n(577);n(262); /** * @vue/runtime-dom v3.5.18 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT **/ -let s;const i="undefined"!==typeof window&&window.trustedTypes;if(i)try{s=i.createPolicy("vue",{createHTML:e=>e})}catch(se){}const l=s?e=>s.createHTML(e):e=>e,c="http://www.w3.org/2000/svg",a="http://www.w3.org/1998/Math/MathML",u="undefined"!==typeof document?document:null,f=u&&u.createElement("template"),d={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const o="svg"===t?u.createElementNS(c,e):"mathml"===t?u.createElementNS(a,e):n?u.createElement(e,{is:n}):u.createElement(e);return"select"===e&&r&&null!=r.multiple&&o.setAttribute("multiple",r.multiple),o},createText:e=>u.createTextNode(e),createComment:e=>u.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>u.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,o,s){const i=n?n.previousSibling:t.lastChild;if(o&&(o===s||o.nextSibling)){while(1)if(t.insertBefore(o.cloneNode(!0),n),o===s||!(o=o.nextSibling))break}else{f.innerHTML=l("svg"===r?`${e}`:"mathml"===r?`${e}`:e);const o=f.content;if("svg"===r||"mathml"===r){const e=o.firstChild;while(e.firstChild)o.appendChild(e.firstChild);o.removeChild(e)}t.insertBefore(o,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},p=Symbol("_vtc"),h={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};r.nJ;function m(e,t,n){const r=e[p];r&&(t=(t?[t,...r]:[...r]).join(" ")),null==t?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const g=Symbol("_vod"),v=Symbol("_vsh");const b=Symbol("");const y=/(^|;)\s*display\s*:/;function k(e,t,n){const r=e.style,s=(0,o.HD)(n);let i=!1;if(n&&!s){if(t)if((0,o.HD)(t))for(const e of t.split(";")){const t=e.slice(0,e.indexOf(":")).trim();null==n[t]&&x(r,t,"")}else for(const e in t)null==n[e]&&x(r,e,"");for(const e in n)"display"===e&&(i=!0),x(r,e,n[e])}else if(s){if(t!==n){const e=r[b];e&&(n+=";"+e),r.cssText=n,i=y.test(n)}}else t&&e.removeAttribute("style");g in e&&(e[g]=i?r.display:"",e[v]&&(r.display="none"))}const w=/\s*!important$/;function x(e,t,n){if((0,o.kJ)(n))n.forEach((n=>x(e,t,n)));else if(null==n&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=C(e,t);w.test(n)?e.setProperty((0,o.rs)(r),n.replace(w,""),"important"):e[r]=n}}const _=["Webkit","Moz","ms"],S={};function C(e,t){const n=S[t];if(n)return n;let r=(0,o._A)(t);if("filter"!==r&&r in e)return S[t]=r;r=(0,o.kC)(r);for(let o=0;o<_.length;o++){const n=_[o]+r;if(n in e)return S[t]=n}return t}const E="http://www.w3.org/1999/xlink";function O(e,t,n,r,s,i=(0,o.Pq)(t)){r&&t.startsWith("xlink:")?null==n?e.removeAttributeNS(E,t.slice(6,t.length)):e.setAttributeNS(E,t,n):null==n||i&&!(0,o.yA)(n)?e.removeAttribute(t):e.setAttribute(t,i?"":(0,o.yk)(n)?String(n):n)}function R(e,t,n,r,s){if("innerHTML"===t||"textContent"===t)return void(null!=n&&(e[t]="innerHTML"===t?l(n):n));const i=e.tagName;if("value"===t&&"PROGRESS"!==i&&!i.includes("-")){const r="OPTION"===i?e.getAttribute("value")||"":e.value,o=null==n?"checkbox"===e.type?"on":"":String(n);return r===o&&"_value"in e||(e.value=o),null==n&&e.removeAttribute(t),void(e._value=n)}let c=!1;if(""===n||null==n){const r=typeof e[t];"boolean"===r?n=(0,o.yA)(n):null==n&&"string"===r?(n="",c=!0):"number"===r&&(n=0,c=!0)}try{e[t]=n}catch(se){0}c&&e.removeAttribute(s||t)}function j(e,t,n,r){e.addEventListener(t,n,r)}function M(e,t,n,r){e.removeEventListener(t,n,r)}const A=Symbol("_vei");function I(e,t,n,r,o=null){const s=e[A]||(e[A]={}),i=s[t];if(r&&i)i.value=r;else{const[n,l]=P(t);if(r){const i=s[t]=$(r,o);j(e,n,i,l)}else i&&(M(e,n,i,l),s[t]=void 0)}}const T=/(?:Once|Passive|Capture)$/;function P(e){let t;if(T.test(e)){let n;t={};while(n=e.match(T))e=e.slice(0,e.length-n[0].length),t[n[0].toLowerCase()]=!0}const n=":"===e[2]?e.slice(3):(0,o.rs)(e.slice(2));return[n,t]}let F=0;const D=Promise.resolve(),N=()=>F||(D.then((()=>F=0)),F=Date.now());function $(e,t){const n=e=>{if(e._vts){if(e._vts<=n.attached)return}else e._vts=Date.now();(0,r.$d)(L(e,n.value),t,5,[e])};return n.value=e,n.attached=N(),n}function L(e,t){if((0,o.kJ)(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map((e=>t=>!t._stopped&&e&&e(t)))}return t}const U=e=>111===e.charCodeAt(0)&&110===e.charCodeAt(1)&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,z=(e,t,n,r,s,i)=>{const l="svg"===s;"class"===t?m(e,r,l):"style"===t?k(e,n,r):(0,o.F7)(t)?(0,o.tR)(t)||I(e,t,n,r,i):("."===t[0]?(t=t.slice(1),1):"^"===t[0]?(t=t.slice(1),0):J(e,t,r,l))?(R(e,t,r),e.tagName.includes("-")||"value"!==t&&"checked"!==t&&"selected"!==t||O(e,t,r,l,i,"value"!==t)):!e._isVueCE||!/[A-Z]/.test(t)&&(0,o.HD)(r)?("true-value"===t?e._trueValue=r:"false-value"===t&&(e._falseValue=r),O(e,t,r,l)):R(e,(0,o._A)(t),r,i,t)};function J(e,t,n,r){if(r)return"innerHTML"===t||"textContent"===t||!!(t in e&&U(t)&&(0,o.mf)(n));if("spellcheck"===t||"draggable"===t||"translate"===t||"autocorrect"===t)return!1;if("form"===t)return!1;if("list"===t&&"INPUT"===e.tagName)return!1;if("type"===t&&"TEXTAREA"===e.tagName)return!1;if("width"===t||"height"===t){const t=e.tagName;if("IMG"===t||"VIDEO"===t||"CANVAS"===t||"SOURCE"===t)return!1}return(!U(t)||!(0,o.HD)(n))&&t in e} +let s;const i="undefined"!==typeof window&&window.trustedTypes;if(i)try{s=i.createPolicy("vue",{createHTML:e=>e})}catch(se){}const l=s?e=>s.createHTML(e):e=>e,c="http://www.w3.org/2000/svg",a="http://www.w3.org/1998/Math/MathML",u="undefined"!==typeof document?document:null,f=u&&u.createElement("template"),d={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const o="svg"===t?u.createElementNS(c,e):"mathml"===t?u.createElementNS(a,e):n?u.createElement(e,{is:n}):u.createElement(e);return"select"===e&&r&&null!=r.multiple&&o.setAttribute("multiple",r.multiple),o},createText:e=>u.createTextNode(e),createComment:e=>u.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>u.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,o,s){const i=n?n.previousSibling:t.lastChild;if(o&&(o===s||o.nextSibling)){while(1)if(t.insertBefore(o.cloneNode(!0),n),o===s||!(o=o.nextSibling))break}else{f.innerHTML=l("svg"===r?`${e}`:"mathml"===r?`${e}`:e);const o=f.content;if("svg"===r||"mathml"===r){const e=o.firstChild;while(e.firstChild)o.appendChild(e.firstChild);o.removeChild(e)}t.insertBefore(o,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},p=Symbol("_vtc"),h={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};r.nJ;function m(e,t,n){const r=e[p];r&&(t=(t?[t,...r]:[...r]).join(" ")),null==t?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const g=Symbol("_vod"),v=Symbol("_vsh");const y=Symbol("");const b=/(^|;)\s*display\s*:/;function k(e,t,n){const r=e.style,s=(0,o.HD)(n);let i=!1;if(n&&!s){if(t)if((0,o.HD)(t))for(const e of t.split(";")){const t=e.slice(0,e.indexOf(":")).trim();null==n[t]&&x(r,t,"")}else for(const e in t)null==n[e]&&x(r,e,"");for(const e in n)"display"===e&&(i=!0),x(r,e,n[e])}else if(s){if(t!==n){const e=r[y];e&&(n+=";"+e),r.cssText=n,i=b.test(n)}}else t&&e.removeAttribute("style");g in e&&(e[g]=i?r.display:"",e[v]&&(r.display="none"))}const w=/\s*!important$/;function x(e,t,n){if((0,o.kJ)(n))n.forEach((n=>x(e,t,n)));else if(null==n&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=C(e,t);w.test(n)?e.setProperty((0,o.rs)(r),n.replace(w,""),"important"):e[r]=n}}const _=["Webkit","Moz","ms"],S={};function C(e,t){const n=S[t];if(n)return n;let r=(0,o._A)(t);if("filter"!==r&&r in e)return S[t]=r;r=(0,o.kC)(r);for(let o=0;o<_.length;o++){const n=_[o]+r;if(n in e)return S[t]=n}return t}const E="http://www.w3.org/1999/xlink";function O(e,t,n,r,s,i=(0,o.Pq)(t)){r&&t.startsWith("xlink:")?null==n?e.removeAttributeNS(E,t.slice(6,t.length)):e.setAttributeNS(E,t,n):null==n||i&&!(0,o.yA)(n)?e.removeAttribute(t):e.setAttribute(t,i?"":(0,o.yk)(n)?String(n):n)}function R(e,t,n,r,s){if("innerHTML"===t||"textContent"===t)return void(null!=n&&(e[t]="innerHTML"===t?l(n):n));const i=e.tagName;if("value"===t&&"PROGRESS"!==i&&!i.includes("-")){const r="OPTION"===i?e.getAttribute("value")||"":e.value,o=null==n?"checkbox"===e.type?"on":"":String(n);return r===o&&"_value"in e||(e.value=o),null==n&&e.removeAttribute(t),void(e._value=n)}let c=!1;if(""===n||null==n){const r=typeof e[t];"boolean"===r?n=(0,o.yA)(n):null==n&&"string"===r?(n="",c=!0):"number"===r&&(n=0,c=!0)}try{e[t]=n}catch(se){0}c&&e.removeAttribute(s||t)}function M(e,t,n,r){e.addEventListener(t,n,r)}function j(e,t,n,r){e.removeEventListener(t,n,r)}const A=Symbol("_vei");function I(e,t,n,r,o=null){const s=e[A]||(e[A]={}),i=s[t];if(r&&i)i.value=r;else{const[n,l]=P(t);if(r){const i=s[t]=$(r,o);M(e,n,i,l)}else i&&(j(e,n,i,l),s[t]=void 0)}}const T=/(?:Once|Passive|Capture)$/;function P(e){let t;if(T.test(e)){let n;t={};while(n=e.match(T))e=e.slice(0,e.length-n[0].length),t[n[0].toLowerCase()]=!0}const n=":"===e[2]?e.slice(3):(0,o.rs)(e.slice(2));return[n,t]}let F=0;const D=Promise.resolve(),N=()=>F||(D.then((()=>F=0)),F=Date.now());function $(e,t){const n=e=>{if(e._vts){if(e._vts<=n.attached)return}else e._vts=Date.now();(0,r.$d)(z(e,n.value),t,5,[e])};return n.value=e,n.attached=N(),n}function z(e,t){if((0,o.kJ)(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map((e=>t=>!t._stopped&&e&&e(t)))}return t}const L=e=>111===e.charCodeAt(0)&&110===e.charCodeAt(1)&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,U=(e,t,n,r,s,i)=>{const l="svg"===s;"class"===t?m(e,r,l):"style"===t?k(e,n,r):(0,o.F7)(t)?(0,o.tR)(t)||I(e,t,n,r,i):("."===t[0]?(t=t.slice(1),1):"^"===t[0]?(t=t.slice(1),0):Z(e,t,r,l))?(R(e,t,r),e.tagName.includes("-")||"value"!==t&&"checked"!==t&&"selected"!==t||O(e,t,r,l,i,"value"!==t)):!e._isVueCE||!/[A-Z]/.test(t)&&(0,o.HD)(r)?("true-value"===t?e._trueValue=r:"false-value"===t&&(e._falseValue=r),O(e,t,r,l)):R(e,(0,o._A)(t),r,i,t)};function Z(e,t,n,r){if(r)return"innerHTML"===t||"textContent"===t||!!(t in e&&L(t)&&(0,o.mf)(n));if("spellcheck"===t||"draggable"===t||"translate"===t||"autocorrect"===t)return!1;if("form"===t)return!1;if("list"===t&&"INPUT"===e.tagName)return!1;if("type"===t&&"TEXTAREA"===e.tagName)return!1;if("width"===t||"height"===t){const t=e.tagName;if("IMG"===t||"VIDEO"===t||"CANVAS"===t||"SOURCE"===t)return!1}return(!L(t)||!(0,o.HD)(n))&&t in e} /*! #__NO_SIDE_EFFECTS__ */ -"undefined"!==typeof HTMLElement&&HTMLElement;Symbol("_moveCb"),Symbol("_enterCb");const Z=e=>{const t=e.props["onUpdate:modelValue"]||!1;return(0,o.kJ)(t)?e=>(0,o.ir)(t,e):t};const V=Symbol("_assign");const H={deep:!0,created(e,{value:t,modifiers:{number:n}},s){const i=(0,o.DM)(t);j(e,"change",(()=>{const t=Array.prototype.filter.call(e.options,(e=>e.selected)).map((e=>n?(0,o.h5)(G(e)):G(e)));e[V](e.multiple?i?new Set(t):t:t[0]),e._assigning=!0,(0,r.Y3)((()=>{e._assigning=!1}))})),e[V]=Z(s)},mounted(e,{value:t}){B(e,t)},beforeUpdate(e,t,n){e[V]=Z(n)},updated(e,{value:t}){e._assigning||B(e,t)}};function B(e,t){const n=e.multiple,r=(0,o.kJ)(t);if(!n||r||(0,o.DM)(t)){for(let s=0,i=e.options.length;sString(e)===String(l))):(0,o.hq)(t,l)>-1}else i.selected=t.has(l);else if((0,o.WV)(G(i),t))return void(e.selectedIndex!==s&&(e.selectedIndex=s))}n||-1===e.selectedIndex||(e.selectedIndex=-1)}}function G(e){return"_value"in e?e._value:e.value}const q=["ctrl","shift","alt","meta"],W={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&0!==e.button,middle:e=>"button"in e&&1!==e.button,right:e=>"button"in e&&2!==e.button,exact:(e,t)=>q.some((n=>e[`${n}Key`]&&!t.includes(n)))},K=(e,t)=>{const n=e._withMods||(e._withMods={}),r=t.join(".");return n[r]||(n[r]=(n,...r)=>{for(let e=0;e{const n=e._withKeys||(e._withKeys={}),r=t.join(".");return n[r]||(n[r]=n=>{if(!("key"in n))return;const r=(0,o.rs)(n.key);return t.some((e=>e===r||Y[e]===r))?e(n):void 0})},Q=(0,o.l7)({patchProp:z},d);let ee;function te(){return ee||(ee=(0,r.Us)(Q))}const ne=(...e)=>{const t=te().createApp(...e);const{mount:n}=t;return t.mount=e=>{const r=oe(e);if(!r)return;const s=t._component;(0,o.mf)(s)||s.render||s.template||(s.template=r.innerHTML),1===r.nodeType&&(r.textContent="");const i=n(r,!1,re(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),i},t};function re(e){return e instanceof SVGElement?"svg":"function"===typeof MathMLElement&&e instanceof MathMLElement?"mathml":void 0}function oe(e){if((0,o.HD)(e)){const t=document.querySelector(e);return t}return e}},577:function(e,t,n){ +"undefined"!==typeof HTMLElement&&HTMLElement;Symbol("_moveCb"),Symbol("_enterCb");const J=e=>{const t=e.props["onUpdate:modelValue"]||!1;return(0,o.kJ)(t)?e=>(0,o.ir)(t,e):t};const V=Symbol("_assign");const H={deep:!0,created(e,{value:t,modifiers:{number:n}},s){const i=(0,o.DM)(t);M(e,"change",(()=>{const t=Array.prototype.filter.call(e.options,(e=>e.selected)).map((e=>n?(0,o.h5)(G(e)):G(e)));e[V](e.multiple?i?new Set(t):t:t[0]),e._assigning=!0,(0,r.Y3)((()=>{e._assigning=!1}))})),e[V]=J(s)},mounted(e,{value:t}){B(e,t)},beforeUpdate(e,t,n){e[V]=J(n)},updated(e,{value:t}){e._assigning||B(e,t)}};function B(e,t){const n=e.multiple,r=(0,o.kJ)(t);if(!n||r||(0,o.DM)(t)){for(let s=0,i=e.options.length;sString(e)===String(l))):(0,o.hq)(t,l)>-1}else i.selected=t.has(l);else if((0,o.WV)(G(i),t))return void(e.selectedIndex!==s&&(e.selectedIndex=s))}n||-1===e.selectedIndex||(e.selectedIndex=-1)}}function G(e){return"_value"in e?e._value:e.value}const q=["ctrl","shift","alt","meta"],W={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&0!==e.button,middle:e=>"button"in e&&1!==e.button,right:e=>"button"in e&&2!==e.button,exact:(e,t)=>q.some((n=>e[`${n}Key`]&&!t.includes(n)))},K=(e,t)=>{const n=e._withMods||(e._withMods={}),r=t.join(".");return n[r]||(n[r]=(n,...r)=>{for(let e=0;e{const n=e._withKeys||(e._withKeys={}),r=t.join(".");return n[r]||(n[r]=n=>{if(!("key"in n))return;const r=(0,o.rs)(n.key);return t.some((e=>e===r||Y[e]===r))?e(n):void 0})},Q=(0,o.l7)({patchProp:U},d);let ee;function te(){return ee||(ee=(0,r.Us)(Q))}const ne=(...e)=>{const t=te().createApp(...e);const{mount:n}=t;return t.mount=e=>{const r=oe(e);if(!r)return;const s=t._component;(0,o.mf)(s)||s.render||s.template||(s.template=r.innerHTML),1===r.nodeType&&(r.textContent="");const i=n(r,!1,re(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),i},t};function re(e){return e instanceof SVGElement?"svg":"function"===typeof MathMLElement&&e instanceof MathMLElement?"mathml":void 0}function oe(e){if((0,o.HD)(e)){const t=document.querySelector(e);return t}return e}},577:function(e,t,n){ /** * @vue/shared v3.5.18 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT **/ /*! #__NO_SIDE_EFFECTS__ */ -function r(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return e=>e in t}n.d(t,{C_:function(){return Y},DM:function(){return g},E9:function(){return Z},F7:function(){return c},Gg:function(){return j},HD:function(){return k},He:function(){return z},Kj:function(){return b},Kn:function(){return x},NO:function(){return l},Nj:function(){return L},Od:function(){return f},PO:function(){return O},Pq:function(){return Q},RI:function(){return p},S0:function(){return R},W7:function(){return E},WV:function(){return ne},Z6:function(){return s},_A:function(){return I},_N:function(){return m},aU:function(){return N},dG:function(){return i},fY:function(){return r},h5:function(){return U},hR:function(){return D},hq:function(){return re},ir:function(){return $},j5:function(){return B},kC:function(){return F},kJ:function(){return h},kT:function(){return o},l7:function(){return u},mf:function(){return y},rs:function(){return P},tI:function(){return _},tR:function(){return a},yA:function(){return ee},yk:function(){return w},yl:function(){return H},zw:function(){return se}});const o={},s=[],i=()=>{},l=()=>!1,c=e=>111===e.charCodeAt(0)&&110===e.charCodeAt(1)&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),a=e=>e.startsWith("onUpdate:"),u=Object.assign,f=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},d=Object.prototype.hasOwnProperty,p=(e,t)=>d.call(e,t),h=Array.isArray,m=e=>"[object Map]"===C(e),g=e=>"[object Set]"===C(e),v=e=>"[object Date]"===C(e),b=e=>"[object RegExp]"===C(e),y=e=>"function"===typeof e,k=e=>"string"===typeof e,w=e=>"symbol"===typeof e,x=e=>null!==e&&"object"===typeof e,_=e=>(x(e)||y(e))&&y(e.then)&&y(e.catch),S=Object.prototype.toString,C=e=>S.call(e),E=e=>C(e).slice(8,-1),O=e=>"[object Object]"===C(e),R=e=>k(e)&&"NaN"!==e&&"-"!==e[0]&&""+parseInt(e,10)===e,j=r(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),M=e=>{const t=Object.create(null);return n=>{const r=t[n];return r||(t[n]=e(n))}},A=/-(\w)/g,I=M((e=>e.replace(A,((e,t)=>t?t.toUpperCase():"")))),T=/\B([A-Z])/g,P=M((e=>e.replace(T,"-$1").toLowerCase())),F=M((e=>e.charAt(0).toUpperCase()+e.slice(1))),D=M((e=>{const t=e?`on${F(e)}`:"";return t})),N=(e,t)=>!Object.is(e,t),$=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},U=e=>{const t=parseFloat(e);return isNaN(t)?e:t},z=e=>{const t=k(e)?Number(e):NaN;return isNaN(t)?e:t};let J;const Z=()=>J||(J="undefined"!==typeof globalThis?globalThis:"undefined"!==typeof self?self:"undefined"!==typeof window?window:"undefined"!==typeof n.g?n.g:{});const V="Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol",H=r(V);function B(e){if(h(e)){const t={};for(let n=0;n{if(e){const n=e.split(q);n.length>1&&(t[n[0].trim()]=n[1].trim())}})),t}function Y(e){let t="";if(k(e))t=e;else if(h(e))for(let n=0;nne(e,t)))}const oe=e=>!(!e||!0!==e["__v_isRef"]),se=e=>k(e)?e:null==e?"":h(e)||x(e)&&(e.toString===S||!y(e.toString))?oe(e)?se(e.value):JSON.stringify(e,ie,2):String(e),ie=(e,t)=>oe(t)?ie(e,t.value):m(t)?{[`Map(${t.size})`]:[...t.entries()].reduce(((e,[t,n],r)=>(e[le(t,r)+" =>"]=n,e)),{})}:g(t)?{[`Set(${t.size})`]:[...t.values()].map((e=>le(e)))}:w(t)?le(t):!x(t)||h(t)||O(t)?t:String(t),le=(e,t="")=>{var n;return w(e)?`Symbol(${null!=(n=e.description)?n:t})`:e}},264:function(e,t,n){n.d(t,{Z:function(){return f}});var r=n(252); +function r(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return e=>e in t}n.d(t,{C_:function(){return Y},DM:function(){return g},E9:function(){return J},F7:function(){return c},Gg:function(){return M},HD:function(){return k},He:function(){return U},Kj:function(){return y},Kn:function(){return x},NO:function(){return l},Nj:function(){return z},Od:function(){return f},PO:function(){return O},Pq:function(){return Q},RI:function(){return p},S0:function(){return R},W7:function(){return E},WV:function(){return ne},Z6:function(){return s},_A:function(){return I},_N:function(){return m},aU:function(){return N},dG:function(){return i},fY:function(){return r},h5:function(){return L},hR:function(){return D},hq:function(){return re},ir:function(){return $},j5:function(){return B},kC:function(){return F},kJ:function(){return h},kT:function(){return o},l7:function(){return u},mf:function(){return b},rs:function(){return P},tI:function(){return _},tR:function(){return a},yA:function(){return ee},yk:function(){return w},yl:function(){return H},zw:function(){return se}});const o={},s=[],i=()=>{},l=()=>!1,c=e=>111===e.charCodeAt(0)&&110===e.charCodeAt(1)&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),a=e=>e.startsWith("onUpdate:"),u=Object.assign,f=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},d=Object.prototype.hasOwnProperty,p=(e,t)=>d.call(e,t),h=Array.isArray,m=e=>"[object Map]"===C(e),g=e=>"[object Set]"===C(e),v=e=>"[object Date]"===C(e),y=e=>"[object RegExp]"===C(e),b=e=>"function"===typeof e,k=e=>"string"===typeof e,w=e=>"symbol"===typeof e,x=e=>null!==e&&"object"===typeof e,_=e=>(x(e)||b(e))&&b(e.then)&&b(e.catch),S=Object.prototype.toString,C=e=>S.call(e),E=e=>C(e).slice(8,-1),O=e=>"[object Object]"===C(e),R=e=>k(e)&&"NaN"!==e&&"-"!==e[0]&&""+parseInt(e,10)===e,M=r(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),j=e=>{const t=Object.create(null);return n=>{const r=t[n];return r||(t[n]=e(n))}},A=/-(\w)/g,I=j((e=>e.replace(A,((e,t)=>t?t.toUpperCase():"")))),T=/\B([A-Z])/g,P=j((e=>e.replace(T,"-$1").toLowerCase())),F=j((e=>e.charAt(0).toUpperCase()+e.slice(1))),D=j((e=>{const t=e?`on${F(e)}`:"";return t})),N=(e,t)=>!Object.is(e,t),$=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},L=e=>{const t=parseFloat(e);return isNaN(t)?e:t},U=e=>{const t=k(e)?Number(e):NaN;return isNaN(t)?e:t};let Z;const J=()=>Z||(Z="undefined"!==typeof globalThis?globalThis:"undefined"!==typeof self?self:"undefined"!==typeof window?window:"undefined"!==typeof n.g?n.g:{});const V="Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol",H=r(V);function B(e){if(h(e)){const t={};for(let n=0;n{if(e){const n=e.split(q);n.length>1&&(t[n[0].trim()]=n[1].trim())}})),t}function Y(e){let t="";if(k(e))t=e;else if(h(e))for(let n=0;nne(e,t)))}const oe=e=>!(!e||!0!==e["__v_isRef"]),se=e=>k(e)?e:null==e?"":h(e)||x(e)&&(e.toString===S||!b(e.toString))?oe(e)?se(e.value):JSON.stringify(e,ie,2):String(e),ie=(e,t)=>oe(t)?ie(e,t.value):m(t)?{[`Map(${t.size})`]:[...t.entries()].reduce(((e,[t,n],r)=>(e[le(t,r)+" =>"]=n,e)),{})}:g(t)?{[`Set(${t.size})`]:[...t.values()].map((e=>le(e)))}:w(t)?le(t):!x(t)||h(t)||O(t)?t:String(t),le=(e,t="")=>{var n;return w(e)?`Symbol(${null!=(n=e.description)?n:t})`:e}},264:function(e,t,n){n.d(t,{Z:function(){return f}});var r=n(252); /** * @license lucide-vue-next v0.539.0 - ISC * @@ -125,7 +125,19 @@ const u=({name:e,iconNode:t,absoluteStrokeWidth:n,"absolute-stroke-width":s,stro * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const o=(0,r.Z)("circle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]])},5:function(e,t,n){n.d(t,{Z:function(){return o}});var r=n(264); + */const o=(0,r.Z)("circle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]])},293:function(e,t,n){n.d(t,{Z:function(){return o}});var r=n(264); +/** + * @license lucide-vue-next v0.539.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const o=(0,r.Z)("clock",[["path",{d:"M12 6v6l4 2",key:"mmk7yg"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]])},322:function(e,t,n){n.d(t,{Z:function(){return o}});var r=n(264); +/** + * @license lucide-vue-next v0.539.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const o=(0,r.Z)("download",[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]])},5:function(e,t,n){n.d(t,{Z:function(){return o}});var r=n(264); /** * @license lucide-vue-next v0.539.0 - ISC * @@ -149,19 +161,37 @@ const u=({name:e,iconNode:t,absoluteStrokeWidth:n,"absolute-stroke-width":s,stro * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const o=(0,r.Z)("moon",[["path",{d:"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401",key:"kfwtm"}]])},254:function(e,t,n){n.d(t,{Z:function(){return o}});var r=n(264); + */const o=(0,r.Z)("moon",[["path",{d:"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401",key:"kfwtm"}]])},167:function(e,t,n){n.d(t,{Z:function(){return o}});var r=n(264); /** * @license lucide-vue-next v0.539.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const o=(0,r.Z)("refresh-cw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]])},275:function(e,t,n){n.d(t,{Z:function(){return o}});var r=n(264); + */const o=(0,r.Z)("pause",[["rect",{x:"14",y:"3",width:"5",height:"18",rx:"1",key:"kaeet6"}],["rect",{x:"5",y:"3",width:"5",height:"18",rx:"1",key:"1wsw3u"}]])},254:function(e,t,n){n.d(t,{Z:function(){return o}});var r=n(264); /** * @license lucide-vue-next v0.539.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const o=(0,r.Z)("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]])},789:function(e,t,n){n.d(t,{Z:function(){return o}});var r=n(264); + */const o=(0,r.Z)("refresh-cw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]])},399:function(e,t,n){n.d(t,{Z:function(){return o}});var r=n(264); +/** + * @license lucide-vue-next v0.539.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const o=(0,r.Z)("rotate-ccw",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}]])},275:function(e,t,n){n.d(t,{Z:function(){return o}});var r=n(264); +/** + * @license lucide-vue-next v0.539.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const o=(0,r.Z)("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]])},469:function(e,t,n){n.d(t,{Z:function(){return o}});var r=n(264); +/** + * @license lucide-vue-next v0.539.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const o=(0,r.Z)("skip-forward",[["path",{d:"M21 4v16",key:"7j8fe9"}],["path",{d:"M6.029 4.285A2 2 0 0 0 3 6v12a2 2 0 0 0 3.029 1.715l9.997-5.998a2 2 0 0 0 .003-3.432z",key:"zs4d6"}]])},789:function(e,t,n){n.d(t,{Z:function(){return o}});var r=n(264); /** * @license lucide-vue-next v0.539.0 - ISC * @@ -185,10 +215,10 @@ const u=({name:e,iconNode:t,absoluteStrokeWidth:n,"absolute-stroke-width":s,stro * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const o=(0,r.Z)("x",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]])},744:function(e,t){t.Z=(e,t)=>{const n=e.__vccOpts||e;for(const[r,o]of t)n[r]=o;return n}},3:function(e,t,n){n.d(t,{j:function(){return i}});var r=n(512);const o=e=>"boolean"===typeof e?`${e}`:0===e?"0":e,s=r.W,i=(e,t)=>n=>{var r;if(null==(null===t||void 0===t?void 0:t.variants))return s(e,null===n||void 0===n?void 0:n.class,null===n||void 0===n?void 0:n.className);const{variants:i,defaultVariants:l}=t,c=Object.keys(i).map((e=>{const t=null===n||void 0===n?void 0:n[e],r=null===l||void 0===l?void 0:l[e];if(null===t)return null;const s=o(t)||o(r);return i[e][s]})),a=n&&Object.entries(n).reduce(((e,t)=>{let[n,r]=t;return void 0===r||(e[n]=r),e}),{}),u=null===t||void 0===t||null===(r=t.compoundVariants)||void 0===r?void 0:r.reduce(((e,t)=>{let{class:n,className:r,...o}=t;return Object.entries(o).every((e=>{let[t,n]=e;return Array.isArray(n)?n.includes({...l,...a}[t]):{...l,...a}[t]===n}))?[...e,n,r]:e}),[]);return s(e,c,u,null===n||void 0===n?void 0:n.class,null===n||void 0===n?void 0:n.className)}},512:function(e,t,n){function r(e){var t,n,o="";if("string"==typeof e||"number"==typeof e)o+=e;else if("object"==typeof e)if(Array.isArray(e)){var s=e.length;for(t=0;t{const t=c(e),{conflictingClassGroups:n,conflictingClassGroupModifiers:o}=e,i=e=>{const n=e.split(r);return""===n[0]&&1!==n.length&&n.shift(),s(n,t)||l(e)},a=(e,t)=>{const r=n[e]||[];return t&&o[e]?[...r,...o[e]]:r};return{getClassGroupId:i,getConflictingClassGroupIds:a}},s=(e,t)=>{if(0===e.length)return t.classGroupId;const n=e[0],o=t.nextPart.get(n),i=o?s(e.slice(1),o):void 0;if(i)return i;if(0===t.validators.length)return;const l=e.join(r);return t.validators.find((({validator:e})=>e(l)))?.classGroupId},i=/^\[(.+)\]$/,l=e=>{if(i.test(e)){const t=i.exec(e)[1],n=t?.substring(0,t.indexOf(":"));if(n)return"arbitrary.."+n}},c=e=>{const{theme:t,classGroups:n}=e,r={nextPart:new Map,validators:[]};for(const o in n)a(n[o],r,o,t);return r},a=(e,t,n,r)=>{e.forEach((e=>{if("string"!==typeof e){if("function"===typeof e)return f(e)?void a(e(r),t,n,r):void t.validators.push({validator:e,classGroupId:n});Object.entries(e).forEach((([e,o])=>{a(o,u(t,e),n,r)}))}else{const r=""===e?t:u(t,e);r.classGroupId=n}}))},u=(e,t)=>{let n=e;return t.split(r).forEach((e=>{n.nextPart.has(e)||n.nextPart.set(e,{nextPart:new Map,validators:[]}),n=n.nextPart.get(e)})),n},f=e=>e.isThemeGetter,d=e=>{if(e<1)return{get:()=>{},set:()=>{}};let t=0,n=new Map,r=new Map;const o=(o,s)=>{n.set(o,s),t++,t>e&&(t=0,r=n,n=new Map)};return{get(e){let t=n.get(e);return void 0!==t?t:void 0!==(t=r.get(e))?(o(e,t),t):void 0},set(e,t){n.has(e)?n.set(e,t):o(e,t)}}},p="!",h=":",m=h.length,g=e=>{const{prefix:t,experimentalParseClassName:n}=e;let r=e=>{const t=[];let n,r=0,o=0,s=0;for(let u=0;us?n-s:void 0;return{modifiers:t,hasImportantModifier:c,baseClassName:l,maybePostfixModifierPosition:a}};if(t){const e=t+h,n=r;r=t=>t.startsWith(e)?n(t.substring(e.length)):{isExternal:!0,modifiers:[],hasImportantModifier:!1,baseClassName:t,maybePostfixModifierPosition:void 0}}if(n){const e=r;r=t=>n({className:t,parseClassName:e})}return r},v=e=>e.endsWith(p)?e.substring(0,e.length-1):e.startsWith(p)?e.substring(1):e,b=e=>{const t=Object.fromEntries(e.orderSensitiveModifiers.map((e=>[e,!0]))),n=e=>{if(e.length<=1)return e;const n=[];let r=[];return e.forEach((e=>{const o="["===e[0]||t[e];o?(n.push(...r.sort(),e),r=[]):r.push(e)})),n.push(...r.sort()),n};return n},y=e=>({cache:d(e.cacheSize),parseClassName:g(e),sortModifiers:b(e),...o(e)}),k=/\s+/,w=(e,t)=>{const{parseClassName:n,getClassGroupId:r,getConflictingClassGroupIds:o,sortModifiers:s}=t,i=[],l=e.trim().split(k);let c="";for(let a=l.length-1;a>=0;a-=1){const e=l[a],{isExternal:t,modifiers:u,hasImportantModifier:f,baseClassName:d,maybePostfixModifierPosition:h}=n(e);if(t){c=e+(c.length>0?" "+c:c);continue}let m=!!h,g=r(m?d.substring(0,h):d);if(!g){if(!m){c=e+(c.length>0?" "+c:c);continue}if(g=r(d),!g){c=e+(c.length>0?" "+c:c);continue}m=!1}const v=s(u).join(":"),b=f?v+p:v,y=b+g;if(i.includes(y))continue;i.push(y);const k=o(g,m);for(let n=0;n0?" "+c:c)}return c};function x(){let e,t,n=0,r="";while(n{if("string"===typeof e)return e;let t,n="";for(let r=0;rt(e)),e());return n=y(c),r=n.cache.get,o=n.cache.set,s=l,l(i)}function l(e){const t=r(e);if(t)return t;const s=w(e,n);return o(e,s),s}return function(){return s(x.apply(null,arguments))}}const C=e=>{const t=t=>t[e]||[];return t.isThemeGetter=!0,t},E=/^\[(?:(\w[\w-]*):)?(.+)\]$/i,O=/^\((?:(\w[\w-]*):)?(.+)\)$/i,R=/^\d+\/\d+$/,j=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,M=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,A=/^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/,I=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,T=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,P=e=>R.test(e),F=e=>!!e&&!Number.isNaN(Number(e)),D=e=>!!e&&Number.isInteger(Number(e)),N=e=>e.endsWith("%")&&F(e.slice(0,-1)),$=e=>j.test(e),L=()=>!0,U=e=>M.test(e)&&!A.test(e),z=()=>!1,J=e=>I.test(e),Z=e=>T.test(e),V=e=>!B(e)&&!X(e),H=e=>se(e,ae,z),B=e=>E.test(e),G=e=>se(e,ue,U),q=e=>se(e,fe,F),W=e=>se(e,le,z),K=e=>se(e,ce,Z),Y=e=>se(e,pe,J),X=e=>O.test(e),Q=e=>ie(e,ue),ee=e=>ie(e,de),te=e=>ie(e,le),ne=e=>ie(e,ae),re=e=>ie(e,ce),oe=e=>ie(e,pe,!0),se=(e,t,n)=>{const r=E.exec(e);return!!r&&(r[1]?t(r[1]):n(r[2]))},ie=(e,t,n=!1)=>{const r=O.exec(e);return!!r&&(r[1]?t(r[1]):n)},le=e=>"position"===e||"percentage"===e,ce=e=>"image"===e||"url"===e,ae=e=>"length"===e||"size"===e||"bg-size"===e,ue=e=>"length"===e,fe=e=>"number"===e,de=e=>"family-name"===e,pe=e=>"shadow"===e,he=(Symbol.toStringTag,()=>{const e=C("color"),t=C("font"),n=C("text"),r=C("font-weight"),o=C("tracking"),s=C("leading"),i=C("breakpoint"),l=C("container"),c=C("spacing"),a=C("radius"),u=C("shadow"),f=C("inset-shadow"),d=C("text-shadow"),p=C("drop-shadow"),h=C("blur"),m=C("perspective"),g=C("aspect"),v=C("ease"),b=C("animate"),y=()=>["auto","avoid","all","avoid-page","page","left","right","column"],k=()=>["center","top","bottom","left","right","top-left","left-top","top-right","right-top","bottom-right","right-bottom","bottom-left","left-bottom"],w=()=>[...k(),X,B],x=()=>["auto","hidden","clip","visible","scroll"],_=()=>["auto","contain","none"],S=()=>[X,B,c],E=()=>[P,"full","auto",...S()],O=()=>[D,"none","subgrid",X,B],R=()=>["auto",{span:["full",D,X,B]},D,X,B],j=()=>[D,"auto",X,B],M=()=>["auto","min","max","fr",X,B],A=()=>["start","end","center","between","around","evenly","stretch","baseline","center-safe","end-safe"],I=()=>["start","end","center","stretch","center-safe","end-safe"],T=()=>["auto",...S()],U=()=>[P,"auto","full","dvw","dvh","lvw","lvh","svw","svh","min","max","fit",...S()],z=()=>[e,X,B],J=()=>[...k(),te,W,{position:[X,B]}],Z=()=>["no-repeat",{repeat:["","x","y","space","round"]}],se=()=>["auto","cover","contain",ne,H,{size:[X,B]}],ie=()=>[N,Q,G],le=()=>["","none","full",a,X,B],ce=()=>["",F,Q,G],ae=()=>["solid","dashed","dotted","double"],ue=()=>["normal","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity"],fe=()=>[F,N,te,W],de=()=>["","none",h,X,B],pe=()=>["none",F,X,B],he=()=>["none",F,X,B],me=()=>[F,X,B],ge=()=>[P,"full",...S()];return{cacheSize:500,theme:{animate:["spin","ping","pulse","bounce"],aspect:["video"],blur:[$],breakpoint:[$],color:[L],container:[$],"drop-shadow":[$],ease:["in","out","in-out"],font:[V],"font-weight":["thin","extralight","light","normal","medium","semibold","bold","extrabold","black"],"inset-shadow":[$],leading:["none","tight","snug","normal","relaxed","loose"],perspective:["dramatic","near","normal","midrange","distant","none"],radius:[$],shadow:[$],spacing:["px",F],text:[$],"text-shadow":[$],tracking:["tighter","tight","normal","wide","wider","widest"]},classGroups:{aspect:[{aspect:["auto","square",P,B,X,g]}],container:["container"],columns:[{columns:[F,B,X,l]}],"break-after":[{"break-after":y()}],"break-before":[{"break-before":y()}],"break-inside":[{"break-inside":["auto","avoid","avoid-page","avoid-column"]}],"box-decoration":[{"box-decoration":["slice","clone"]}],box:[{box:["border","content"]}],display:["block","inline-block","inline","flex","inline-flex","table","inline-table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row-group","table-row","flow-root","grid","inline-grid","contents","list-item","hidden"],sr:["sr-only","not-sr-only"],float:[{float:["right","left","none","start","end"]}],clear:[{clear:["left","right","both","none","start","end"]}],isolation:["isolate","isolation-auto"],"object-fit":[{object:["contain","cover","fill","none","scale-down"]}],"object-position":[{object:w()}],overflow:[{overflow:x()}],"overflow-x":[{"overflow-x":x()}],"overflow-y":[{"overflow-y":x()}],overscroll:[{overscroll:_()}],"overscroll-x":[{"overscroll-x":_()}],"overscroll-y":[{"overscroll-y":_()}],position:["static","fixed","absolute","relative","sticky"],inset:[{inset:E()}],"inset-x":[{"inset-x":E()}],"inset-y":[{"inset-y":E()}],start:[{start:E()}],end:[{end:E()}],top:[{top:E()}],right:[{right:E()}],bottom:[{bottom:E()}],left:[{left:E()}],visibility:["visible","invisible","collapse"],z:[{z:[D,"auto",X,B]}],basis:[{basis:[P,"full","auto",l,...S()]}],"flex-direction":[{flex:["row","row-reverse","col","col-reverse"]}],"flex-wrap":[{flex:["nowrap","wrap","wrap-reverse"]}],flex:[{flex:[F,P,"auto","initial","none",B]}],grow:[{grow:["",F,X,B]}],shrink:[{shrink:["",F,X,B]}],order:[{order:[D,"first","last","none",X,B]}],"grid-cols":[{"grid-cols":O()}],"col-start-end":[{col:R()}],"col-start":[{"col-start":j()}],"col-end":[{"col-end":j()}],"grid-rows":[{"grid-rows":O()}],"row-start-end":[{row:R()}],"row-start":[{"row-start":j()}],"row-end":[{"row-end":j()}],"grid-flow":[{"grid-flow":["row","col","dense","row-dense","col-dense"]}],"auto-cols":[{"auto-cols":M()}],"auto-rows":[{"auto-rows":M()}],gap:[{gap:S()}],"gap-x":[{"gap-x":S()}],"gap-y":[{"gap-y":S()}],"justify-content":[{justify:[...A(),"normal"]}],"justify-items":[{"justify-items":[...I(),"normal"]}],"justify-self":[{"justify-self":["auto",...I()]}],"align-content":[{content:["normal",...A()]}],"align-items":[{items:[...I(),{baseline:["","last"]}]}],"align-self":[{self:["auto",...I(),{baseline:["","last"]}]}],"place-content":[{"place-content":A()}],"place-items":[{"place-items":[...I(),"baseline"]}],"place-self":[{"place-self":["auto",...I()]}],p:[{p:S()}],px:[{px:S()}],py:[{py:S()}],ps:[{ps:S()}],pe:[{pe:S()}],pt:[{pt:S()}],pr:[{pr:S()}],pb:[{pb:S()}],pl:[{pl:S()}],m:[{m:T()}],mx:[{mx:T()}],my:[{my:T()}],ms:[{ms:T()}],me:[{me:T()}],mt:[{mt:T()}],mr:[{mr:T()}],mb:[{mb:T()}],ml:[{ml:T()}],"space-x":[{"space-x":S()}],"space-x-reverse":["space-x-reverse"],"space-y":[{"space-y":S()}],"space-y-reverse":["space-y-reverse"],size:[{size:U()}],w:[{w:[l,"screen",...U()]}],"min-w":[{"min-w":[l,"screen","none",...U()]}],"max-w":[{"max-w":[l,"screen","none","prose",{screen:[i]},...U()]}],h:[{h:["screen","lh",...U()]}],"min-h":[{"min-h":["screen","lh","none",...U()]}],"max-h":[{"max-h":["screen","lh",...U()]}],"font-size":[{text:["base",n,Q,G]}],"font-smoothing":["antialiased","subpixel-antialiased"],"font-style":["italic","not-italic"],"font-weight":[{font:[r,X,q]}],"font-stretch":[{"font-stretch":["ultra-condensed","extra-condensed","condensed","semi-condensed","normal","semi-expanded","expanded","extra-expanded","ultra-expanded",N,B]}],"font-family":[{font:[ee,B,t]}],"fvn-normal":["normal-nums"],"fvn-ordinal":["ordinal"],"fvn-slashed-zero":["slashed-zero"],"fvn-figure":["lining-nums","oldstyle-nums"],"fvn-spacing":["proportional-nums","tabular-nums"],"fvn-fraction":["diagonal-fractions","stacked-fractions"],tracking:[{tracking:[o,X,B]}],"line-clamp":[{"line-clamp":[F,"none",X,q]}],leading:[{leading:[s,...S()]}],"list-image":[{"list-image":["none",X,B]}],"list-style-position":[{list:["inside","outside"]}],"list-style-type":[{list:["disc","decimal","none",X,B]}],"text-alignment":[{text:["left","center","right","justify","start","end"]}],"placeholder-color":[{placeholder:z()}],"text-color":[{text:z()}],"text-decoration":["underline","overline","line-through","no-underline"],"text-decoration-style":[{decoration:[...ae(),"wavy"]}],"text-decoration-thickness":[{decoration:[F,"from-font","auto",X,G]}],"text-decoration-color":[{decoration:z()}],"underline-offset":[{"underline-offset":[F,"auto",X,B]}],"text-transform":["uppercase","lowercase","capitalize","normal-case"],"text-overflow":["truncate","text-ellipsis","text-clip"],"text-wrap":[{text:["wrap","nowrap","balance","pretty"]}],indent:[{indent:S()}],"vertical-align":[{align:["baseline","top","middle","bottom","text-top","text-bottom","sub","super",X,B]}],whitespace:[{whitespace:["normal","nowrap","pre","pre-line","pre-wrap","break-spaces"]}],break:[{break:["normal","words","all","keep"]}],wrap:[{wrap:["break-word","anywhere","normal"]}],hyphens:[{hyphens:["none","manual","auto"]}],content:[{content:["none",X,B]}],"bg-attachment":[{bg:["fixed","local","scroll"]}],"bg-clip":[{"bg-clip":["border","padding","content","text"]}],"bg-origin":[{"bg-origin":["border","padding","content"]}],"bg-position":[{bg:J()}],"bg-repeat":[{bg:Z()}],"bg-size":[{bg:se()}],"bg-image":[{bg:["none",{linear:[{to:["t","tr","r","br","b","bl","l","tl"]},D,X,B],radial:["",X,B],conic:[D,X,B]},re,K]}],"bg-color":[{bg:z()}],"gradient-from-pos":[{from:ie()}],"gradient-via-pos":[{via:ie()}],"gradient-to-pos":[{to:ie()}],"gradient-from":[{from:z()}],"gradient-via":[{via:z()}],"gradient-to":[{to:z()}],rounded:[{rounded:le()}],"rounded-s":[{"rounded-s":le()}],"rounded-e":[{"rounded-e":le()}],"rounded-t":[{"rounded-t":le()}],"rounded-r":[{"rounded-r":le()}],"rounded-b":[{"rounded-b":le()}],"rounded-l":[{"rounded-l":le()}],"rounded-ss":[{"rounded-ss":le()}],"rounded-se":[{"rounded-se":le()}],"rounded-ee":[{"rounded-ee":le()}],"rounded-es":[{"rounded-es":le()}],"rounded-tl":[{"rounded-tl":le()}],"rounded-tr":[{"rounded-tr":le()}],"rounded-br":[{"rounded-br":le()}],"rounded-bl":[{"rounded-bl":le()}],"border-w":[{border:ce()}],"border-w-x":[{"border-x":ce()}],"border-w-y":[{"border-y":ce()}],"border-w-s":[{"border-s":ce()}],"border-w-e":[{"border-e":ce()}],"border-w-t":[{"border-t":ce()}],"border-w-r":[{"border-r":ce()}],"border-w-b":[{"border-b":ce()}],"border-w-l":[{"border-l":ce()}],"divide-x":[{"divide-x":ce()}],"divide-x-reverse":["divide-x-reverse"],"divide-y":[{"divide-y":ce()}],"divide-y-reverse":["divide-y-reverse"],"border-style":[{border:[...ae(),"hidden","none"]}],"divide-style":[{divide:[...ae(),"hidden","none"]}],"border-color":[{border:z()}],"border-color-x":[{"border-x":z()}],"border-color-y":[{"border-y":z()}],"border-color-s":[{"border-s":z()}],"border-color-e":[{"border-e":z()}],"border-color-t":[{"border-t":z()}],"border-color-r":[{"border-r":z()}],"border-color-b":[{"border-b":z()}],"border-color-l":[{"border-l":z()}],"divide-color":[{divide:z()}],"outline-style":[{outline:[...ae(),"none","hidden"]}],"outline-offset":[{"outline-offset":[F,X,B]}],"outline-w":[{outline:["",F,Q,G]}],"outline-color":[{outline:z()}],shadow:[{shadow:["","none",u,oe,Y]}],"shadow-color":[{shadow:z()}],"inset-shadow":[{"inset-shadow":["none",f,oe,Y]}],"inset-shadow-color":[{"inset-shadow":z()}],"ring-w":[{ring:ce()}],"ring-w-inset":["ring-inset"],"ring-color":[{ring:z()}],"ring-offset-w":[{"ring-offset":[F,G]}],"ring-offset-color":[{"ring-offset":z()}],"inset-ring-w":[{"inset-ring":ce()}],"inset-ring-color":[{"inset-ring":z()}],"text-shadow":[{"text-shadow":["none",d,oe,Y]}],"text-shadow-color":[{"text-shadow":z()}],opacity:[{opacity:[F,X,B]}],"mix-blend":[{"mix-blend":[...ue(),"plus-darker","plus-lighter"]}],"bg-blend":[{"bg-blend":ue()}],"mask-clip":[{"mask-clip":["border","padding","content","fill","stroke","view"]},"mask-no-clip"],"mask-composite":[{mask:["add","subtract","intersect","exclude"]}],"mask-image-linear-pos":[{"mask-linear":[F]}],"mask-image-linear-from-pos":[{"mask-linear-from":fe()}],"mask-image-linear-to-pos":[{"mask-linear-to":fe()}],"mask-image-linear-from-color":[{"mask-linear-from":z()}],"mask-image-linear-to-color":[{"mask-linear-to":z()}],"mask-image-t-from-pos":[{"mask-t-from":fe()}],"mask-image-t-to-pos":[{"mask-t-to":fe()}],"mask-image-t-from-color":[{"mask-t-from":z()}],"mask-image-t-to-color":[{"mask-t-to":z()}],"mask-image-r-from-pos":[{"mask-r-from":fe()}],"mask-image-r-to-pos":[{"mask-r-to":fe()}],"mask-image-r-from-color":[{"mask-r-from":z()}],"mask-image-r-to-color":[{"mask-r-to":z()}],"mask-image-b-from-pos":[{"mask-b-from":fe()}],"mask-image-b-to-pos":[{"mask-b-to":fe()}],"mask-image-b-from-color":[{"mask-b-from":z()}],"mask-image-b-to-color":[{"mask-b-to":z()}],"mask-image-l-from-pos":[{"mask-l-from":fe()}],"mask-image-l-to-pos":[{"mask-l-to":fe()}],"mask-image-l-from-color":[{"mask-l-from":z()}],"mask-image-l-to-color":[{"mask-l-to":z()}],"mask-image-x-from-pos":[{"mask-x-from":fe()}],"mask-image-x-to-pos":[{"mask-x-to":fe()}],"mask-image-x-from-color":[{"mask-x-from":z()}],"mask-image-x-to-color":[{"mask-x-to":z()}],"mask-image-y-from-pos":[{"mask-y-from":fe()}],"mask-image-y-to-pos":[{"mask-y-to":fe()}],"mask-image-y-from-color":[{"mask-y-from":z()}],"mask-image-y-to-color":[{"mask-y-to":z()}],"mask-image-radial":[{"mask-radial":[X,B]}],"mask-image-radial-from-pos":[{"mask-radial-from":fe()}],"mask-image-radial-to-pos":[{"mask-radial-to":fe()}],"mask-image-radial-from-color":[{"mask-radial-from":z()}],"mask-image-radial-to-color":[{"mask-radial-to":z()}],"mask-image-radial-shape":[{"mask-radial":["circle","ellipse"]}],"mask-image-radial-size":[{"mask-radial":[{closest:["side","corner"],farthest:["side","corner"]}]}],"mask-image-radial-pos":[{"mask-radial-at":k()}],"mask-image-conic-pos":[{"mask-conic":[F]}],"mask-image-conic-from-pos":[{"mask-conic-from":fe()}],"mask-image-conic-to-pos":[{"mask-conic-to":fe()}],"mask-image-conic-from-color":[{"mask-conic-from":z()}],"mask-image-conic-to-color":[{"mask-conic-to":z()}],"mask-mode":[{mask:["alpha","luminance","match"]}],"mask-origin":[{"mask-origin":["border","padding","content","fill","stroke","view"]}],"mask-position":[{mask:J()}],"mask-repeat":[{mask:Z()}],"mask-size":[{mask:se()}],"mask-type":[{"mask-type":["alpha","luminance"]}],"mask-image":[{mask:["none",X,B]}],filter:[{filter:["","none",X,B]}],blur:[{blur:de()}],brightness:[{brightness:[F,X,B]}],contrast:[{contrast:[F,X,B]}],"drop-shadow":[{"drop-shadow":["","none",p,oe,Y]}],"drop-shadow-color":[{"drop-shadow":z()}],grayscale:[{grayscale:["",F,X,B]}],"hue-rotate":[{"hue-rotate":[F,X,B]}],invert:[{invert:["",F,X,B]}],saturate:[{saturate:[F,X,B]}],sepia:[{sepia:["",F,X,B]}],"backdrop-filter":[{"backdrop-filter":["","none",X,B]}],"backdrop-blur":[{"backdrop-blur":de()}],"backdrop-brightness":[{"backdrop-brightness":[F,X,B]}],"backdrop-contrast":[{"backdrop-contrast":[F,X,B]}],"backdrop-grayscale":[{"backdrop-grayscale":["",F,X,B]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[F,X,B]}],"backdrop-invert":[{"backdrop-invert":["",F,X,B]}],"backdrop-opacity":[{"backdrop-opacity":[F,X,B]}],"backdrop-saturate":[{"backdrop-saturate":[F,X,B]}],"backdrop-sepia":[{"backdrop-sepia":["",F,X,B]}],"border-collapse":[{border:["collapse","separate"]}],"border-spacing":[{"border-spacing":S()}],"border-spacing-x":[{"border-spacing-x":S()}],"border-spacing-y":[{"border-spacing-y":S()}],"table-layout":[{table:["auto","fixed"]}],caption:[{caption:["top","bottom"]}],transition:[{transition:["","all","colors","opacity","shadow","transform","none",X,B]}],"transition-behavior":[{transition:["normal","discrete"]}],duration:[{duration:[F,"initial",X,B]}],ease:[{ease:["linear","initial",v,X,B]}],delay:[{delay:[F,X,B]}],animate:[{animate:["none",b,X,B]}],backface:[{backface:["hidden","visible"]}],perspective:[{perspective:[m,X,B]}],"perspective-origin":[{"perspective-origin":w()}],rotate:[{rotate:pe()}],"rotate-x":[{"rotate-x":pe()}],"rotate-y":[{"rotate-y":pe()}],"rotate-z":[{"rotate-z":pe()}],scale:[{scale:he()}],"scale-x":[{"scale-x":he()}],"scale-y":[{"scale-y":he()}],"scale-z":[{"scale-z":he()}],"scale-3d":["scale-3d"],skew:[{skew:me()}],"skew-x":[{"skew-x":me()}],"skew-y":[{"skew-y":me()}],transform:[{transform:[X,B,"","none","gpu","cpu"]}],"transform-origin":[{origin:w()}],"transform-style":[{transform:["3d","flat"]}],translate:[{translate:ge()}],"translate-x":[{"translate-x":ge()}],"translate-y":[{"translate-y":ge()}],"translate-z":[{"translate-z":ge()}],"translate-none":["translate-none"],accent:[{accent:z()}],appearance:[{appearance:["none","auto"]}],"caret-color":[{caret:z()}],"color-scheme":[{scheme:["normal","dark","light","light-dark","only-dark","only-light"]}],cursor:[{cursor:["auto","default","pointer","wait","text","move","help","not-allowed","none","context-menu","progress","cell","crosshair","vertical-text","alias","copy","no-drop","grab","grabbing","all-scroll","col-resize","row-resize","n-resize","e-resize","s-resize","w-resize","ne-resize","nw-resize","se-resize","sw-resize","ew-resize","ns-resize","nesw-resize","nwse-resize","zoom-in","zoom-out",X,B]}],"field-sizing":[{"field-sizing":["fixed","content"]}],"pointer-events":[{"pointer-events":["auto","none"]}],resize:[{resize:["none","","y","x"]}],"scroll-behavior":[{scroll:["auto","smooth"]}],"scroll-m":[{"scroll-m":S()}],"scroll-mx":[{"scroll-mx":S()}],"scroll-my":[{"scroll-my":S()}],"scroll-ms":[{"scroll-ms":S()}],"scroll-me":[{"scroll-me":S()}],"scroll-mt":[{"scroll-mt":S()}],"scroll-mr":[{"scroll-mr":S()}],"scroll-mb":[{"scroll-mb":S()}],"scroll-ml":[{"scroll-ml":S()}],"scroll-p":[{"scroll-p":S()}],"scroll-px":[{"scroll-px":S()}],"scroll-py":[{"scroll-py":S()}],"scroll-ps":[{"scroll-ps":S()}],"scroll-pe":[{"scroll-pe":S()}],"scroll-pt":[{"scroll-pt":S()}],"scroll-pr":[{"scroll-pr":S()}],"scroll-pb":[{"scroll-pb":S()}],"scroll-pl":[{"scroll-pl":S()}],"snap-align":[{snap:["start","end","center","align-none"]}],"snap-stop":[{snap:["normal","always"]}],"snap-type":[{snap:["none","x","y","both"]}],"snap-strictness":[{snap:["mandatory","proximity"]}],touch:[{touch:["auto","none","manipulation"]}],"touch-x":[{"touch-pan":["x","left","right"]}],"touch-y":[{"touch-pan":["y","up","down"]}],"touch-pz":["touch-pinch-zoom"],select:[{select:["none","text","all","auto"]}],"will-change":[{"will-change":["auto","scroll","contents","transform",X,B]}],fill:[{fill:["none",...z()]}],"stroke-w":[{stroke:[F,Q,G,q]}],stroke:[{stroke:["none",...z()]}],"forced-color-adjust":[{"forced-color-adjust":["auto","none"]}]},conflictingClassGroups:{overflow:["overflow-x","overflow-y"],overscroll:["overscroll-x","overscroll-y"],inset:["inset-x","inset-y","start","end","top","right","bottom","left"],"inset-x":["right","left"],"inset-y":["top","bottom"],flex:["basis","grow","shrink"],gap:["gap-x","gap-y"],p:["px","py","ps","pe","pt","pr","pb","pl"],px:["pr","pl"],py:["pt","pb"],m:["mx","my","ms","me","mt","mr","mb","ml"],mx:["mr","ml"],my:["mt","mb"],size:["w","h"],"font-size":["leading"],"fvn-normal":["fvn-ordinal","fvn-slashed-zero","fvn-figure","fvn-spacing","fvn-fraction"],"fvn-ordinal":["fvn-normal"],"fvn-slashed-zero":["fvn-normal"],"fvn-figure":["fvn-normal"],"fvn-spacing":["fvn-normal"],"fvn-fraction":["fvn-normal"],"line-clamp":["display","overflow"],rounded:["rounded-s","rounded-e","rounded-t","rounded-r","rounded-b","rounded-l","rounded-ss","rounded-se","rounded-ee","rounded-es","rounded-tl","rounded-tr","rounded-br","rounded-bl"],"rounded-s":["rounded-ss","rounded-es"],"rounded-e":["rounded-se","rounded-ee"],"rounded-t":["rounded-tl","rounded-tr"],"rounded-r":["rounded-tr","rounded-br"],"rounded-b":["rounded-br","rounded-bl"],"rounded-l":["rounded-tl","rounded-bl"],"border-spacing":["border-spacing-x","border-spacing-y"],"border-w":["border-w-x","border-w-y","border-w-s","border-w-e","border-w-t","border-w-r","border-w-b","border-w-l"],"border-w-x":["border-w-r","border-w-l"],"border-w-y":["border-w-t","border-w-b"],"border-color":["border-color-x","border-color-y","border-color-s","border-color-e","border-color-t","border-color-r","border-color-b","border-color-l"],"border-color-x":["border-color-r","border-color-l"],"border-color-y":["border-color-t","border-color-b"],translate:["translate-x","translate-y","translate-none"],"translate-none":["translate","translate-x","translate-y","translate-z"],"scroll-m":["scroll-mx","scroll-my","scroll-ms","scroll-me","scroll-mt","scroll-mr","scroll-mb","scroll-ml"],"scroll-mx":["scroll-mr","scroll-ml"],"scroll-my":["scroll-mt","scroll-mb"],"scroll-p":["scroll-px","scroll-py","scroll-ps","scroll-pe","scroll-pt","scroll-pr","scroll-pb","scroll-pl"],"scroll-px":["scroll-pr","scroll-pl"],"scroll-py":["scroll-pt","scroll-pb"],touch:["touch-x","touch-y","touch-pz"],"touch-x":["touch"],"touch-y":["touch"],"touch-pz":["touch"]},conflictingClassGroupModifiers:{"font-size":["leading"]},orderSensitiveModifiers:["*","**","after","backdrop","before","details-content","file","first-letter","first-line","marker","placeholder","selection"]}}),me=S(he)},201:function(e,t,n){n.d(t,{PO:function(){return ce},p7:function(){return ot},tv:function(){return it},yj:function(){return lt}});var r=n(252),o=n(262); + */const o=(0,r.Z)("x",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]])},744:function(e,t){t.Z=(e,t)=>{const n=e.__vccOpts||e;for(const[r,o]of t)n[r]=o;return n}},3:function(e,t,n){n.d(t,{j:function(){return i}});var r=n(512);const o=e=>"boolean"===typeof e?`${e}`:0===e?"0":e,s=r.W,i=(e,t)=>n=>{var r;if(null==(null===t||void 0===t?void 0:t.variants))return s(e,null===n||void 0===n?void 0:n.class,null===n||void 0===n?void 0:n.className);const{variants:i,defaultVariants:l}=t,c=Object.keys(i).map((e=>{const t=null===n||void 0===n?void 0:n[e],r=null===l||void 0===l?void 0:l[e];if(null===t)return null;const s=o(t)||o(r);return i[e][s]})),a=n&&Object.entries(n).reduce(((e,t)=>{let[n,r]=t;return void 0===r||(e[n]=r),e}),{}),u=null===t||void 0===t||null===(r=t.compoundVariants)||void 0===r?void 0:r.reduce(((e,t)=>{let{class:n,className:r,...o}=t;return Object.entries(o).every((e=>{let[t,n]=e;return Array.isArray(n)?n.includes({...l,...a}[t]):{...l,...a}[t]===n}))?[...e,n,r]:e}),[]);return s(e,c,u,null===n||void 0===n?void 0:n.class,null===n||void 0===n?void 0:n.className)}},512:function(e,t,n){function r(e){var t,n,o="";if("string"==typeof e||"number"==typeof e)o+=e;else if("object"==typeof e)if(Array.isArray(e)){var s=e.length;for(t=0;t{const t=c(e),{conflictingClassGroups:n,conflictingClassGroupModifiers:o}=e,i=e=>{const n=e.split(r);return""===n[0]&&1!==n.length&&n.shift(),s(n,t)||l(e)},a=(e,t)=>{const r=n[e]||[];return t&&o[e]?[...r,...o[e]]:r};return{getClassGroupId:i,getConflictingClassGroupIds:a}},s=(e,t)=>{if(0===e.length)return t.classGroupId;const n=e[0],o=t.nextPart.get(n),i=o?s(e.slice(1),o):void 0;if(i)return i;if(0===t.validators.length)return;const l=e.join(r);return t.validators.find((({validator:e})=>e(l)))?.classGroupId},i=/^\[(.+)\]$/,l=e=>{if(i.test(e)){const t=i.exec(e)[1],n=t?.substring(0,t.indexOf(":"));if(n)return"arbitrary.."+n}},c=e=>{const{theme:t,classGroups:n}=e,r={nextPart:new Map,validators:[]};for(const o in n)a(n[o],r,o,t);return r},a=(e,t,n,r)=>{e.forEach((e=>{if("string"!==typeof e){if("function"===typeof e)return f(e)?void a(e(r),t,n,r):void t.validators.push({validator:e,classGroupId:n});Object.entries(e).forEach((([e,o])=>{a(o,u(t,e),n,r)}))}else{const r=""===e?t:u(t,e);r.classGroupId=n}}))},u=(e,t)=>{let n=e;return t.split(r).forEach((e=>{n.nextPart.has(e)||n.nextPart.set(e,{nextPart:new Map,validators:[]}),n=n.nextPart.get(e)})),n},f=e=>e.isThemeGetter,d=e=>{if(e<1)return{get:()=>{},set:()=>{}};let t=0,n=new Map,r=new Map;const o=(o,s)=>{n.set(o,s),t++,t>e&&(t=0,r=n,n=new Map)};return{get(e){let t=n.get(e);return void 0!==t?t:void 0!==(t=r.get(e))?(o(e,t),t):void 0},set(e,t){n.has(e)?n.set(e,t):o(e,t)}}},p="!",h=":",m=h.length,g=e=>{const{prefix:t,experimentalParseClassName:n}=e;let r=e=>{const t=[];let n,r=0,o=0,s=0;for(let u=0;us?n-s:void 0;return{modifiers:t,hasImportantModifier:c,baseClassName:l,maybePostfixModifierPosition:a}};if(t){const e=t+h,n=r;r=t=>t.startsWith(e)?n(t.substring(e.length)):{isExternal:!0,modifiers:[],hasImportantModifier:!1,baseClassName:t,maybePostfixModifierPosition:void 0}}if(n){const e=r;r=t=>n({className:t,parseClassName:e})}return r},v=e=>e.endsWith(p)?e.substring(0,e.length-1):e.startsWith(p)?e.substring(1):e,y=e=>{const t=Object.fromEntries(e.orderSensitiveModifiers.map((e=>[e,!0]))),n=e=>{if(e.length<=1)return e;const n=[];let r=[];return e.forEach((e=>{const o="["===e[0]||t[e];o?(n.push(...r.sort(),e),r=[]):r.push(e)})),n.push(...r.sort()),n};return n},b=e=>({cache:d(e.cacheSize),parseClassName:g(e),sortModifiers:y(e),...o(e)}),k=/\s+/,w=(e,t)=>{const{parseClassName:n,getClassGroupId:r,getConflictingClassGroupIds:o,sortModifiers:s}=t,i=[],l=e.trim().split(k);let c="";for(let a=l.length-1;a>=0;a-=1){const e=l[a],{isExternal:t,modifiers:u,hasImportantModifier:f,baseClassName:d,maybePostfixModifierPosition:h}=n(e);if(t){c=e+(c.length>0?" "+c:c);continue}let m=!!h,g=r(m?d.substring(0,h):d);if(!g){if(!m){c=e+(c.length>0?" "+c:c);continue}if(g=r(d),!g){c=e+(c.length>0?" "+c:c);continue}m=!1}const v=s(u).join(":"),y=f?v+p:v,b=y+g;if(i.includes(b))continue;i.push(b);const k=o(g,m);for(let n=0;n0?" "+c:c)}return c};function x(){let e,t,n=0,r="";while(n{if("string"===typeof e)return e;let t,n="";for(let r=0;rt(e)),e());return n=b(c),r=n.cache.get,o=n.cache.set,s=l,l(i)}function l(e){const t=r(e);if(t)return t;const s=w(e,n);return o(e,s),s}return function(){return s(x.apply(null,arguments))}}const C=e=>{const t=t=>t[e]||[];return t.isThemeGetter=!0,t},E=/^\[(?:(\w[\w-]*):)?(.+)\]$/i,O=/^\((?:(\w[\w-]*):)?(.+)\)$/i,R=/^\d+\/\d+$/,M=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,j=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,A=/^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/,I=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,T=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,P=e=>R.test(e),F=e=>!!e&&!Number.isNaN(Number(e)),D=e=>!!e&&Number.isInteger(Number(e)),N=e=>e.endsWith("%")&&F(e.slice(0,-1)),$=e=>M.test(e),z=()=>!0,L=e=>j.test(e)&&!A.test(e),U=()=>!1,Z=e=>I.test(e),J=e=>T.test(e),V=e=>!B(e)&&!X(e),H=e=>se(e,ae,U),B=e=>E.test(e),G=e=>se(e,ue,L),q=e=>se(e,fe,F),W=e=>se(e,le,U),K=e=>se(e,ce,J),Y=e=>se(e,pe,Z),X=e=>O.test(e),Q=e=>ie(e,ue),ee=e=>ie(e,de),te=e=>ie(e,le),ne=e=>ie(e,ae),re=e=>ie(e,ce),oe=e=>ie(e,pe,!0),se=(e,t,n)=>{const r=E.exec(e);return!!r&&(r[1]?t(r[1]):n(r[2]))},ie=(e,t,n=!1)=>{const r=O.exec(e);return!!r&&(r[1]?t(r[1]):n)},le=e=>"position"===e||"percentage"===e,ce=e=>"image"===e||"url"===e,ae=e=>"length"===e||"size"===e||"bg-size"===e,ue=e=>"length"===e,fe=e=>"number"===e,de=e=>"family-name"===e,pe=e=>"shadow"===e,he=(Symbol.toStringTag,()=>{const e=C("color"),t=C("font"),n=C("text"),r=C("font-weight"),o=C("tracking"),s=C("leading"),i=C("breakpoint"),l=C("container"),c=C("spacing"),a=C("radius"),u=C("shadow"),f=C("inset-shadow"),d=C("text-shadow"),p=C("drop-shadow"),h=C("blur"),m=C("perspective"),g=C("aspect"),v=C("ease"),y=C("animate"),b=()=>["auto","avoid","all","avoid-page","page","left","right","column"],k=()=>["center","top","bottom","left","right","top-left","left-top","top-right","right-top","bottom-right","right-bottom","bottom-left","left-bottom"],w=()=>[...k(),X,B],x=()=>["auto","hidden","clip","visible","scroll"],_=()=>["auto","contain","none"],S=()=>[X,B,c],E=()=>[P,"full","auto",...S()],O=()=>[D,"none","subgrid",X,B],R=()=>["auto",{span:["full",D,X,B]},D,X,B],M=()=>[D,"auto",X,B],j=()=>["auto","min","max","fr",X,B],A=()=>["start","end","center","between","around","evenly","stretch","baseline","center-safe","end-safe"],I=()=>["start","end","center","stretch","center-safe","end-safe"],T=()=>["auto",...S()],L=()=>[P,"auto","full","dvw","dvh","lvw","lvh","svw","svh","min","max","fit",...S()],U=()=>[e,X,B],Z=()=>[...k(),te,W,{position:[X,B]}],J=()=>["no-repeat",{repeat:["","x","y","space","round"]}],se=()=>["auto","cover","contain",ne,H,{size:[X,B]}],ie=()=>[N,Q,G],le=()=>["","none","full",a,X,B],ce=()=>["",F,Q,G],ae=()=>["solid","dashed","dotted","double"],ue=()=>["normal","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity"],fe=()=>[F,N,te,W],de=()=>["","none",h,X,B],pe=()=>["none",F,X,B],he=()=>["none",F,X,B],me=()=>[F,X,B],ge=()=>[P,"full",...S()];return{cacheSize:500,theme:{animate:["spin","ping","pulse","bounce"],aspect:["video"],blur:[$],breakpoint:[$],color:[z],container:[$],"drop-shadow":[$],ease:["in","out","in-out"],font:[V],"font-weight":["thin","extralight","light","normal","medium","semibold","bold","extrabold","black"],"inset-shadow":[$],leading:["none","tight","snug","normal","relaxed","loose"],perspective:["dramatic","near","normal","midrange","distant","none"],radius:[$],shadow:[$],spacing:["px",F],text:[$],"text-shadow":[$],tracking:["tighter","tight","normal","wide","wider","widest"]},classGroups:{aspect:[{aspect:["auto","square",P,B,X,g]}],container:["container"],columns:[{columns:[F,B,X,l]}],"break-after":[{"break-after":b()}],"break-before":[{"break-before":b()}],"break-inside":[{"break-inside":["auto","avoid","avoid-page","avoid-column"]}],"box-decoration":[{"box-decoration":["slice","clone"]}],box:[{box:["border","content"]}],display:["block","inline-block","inline","flex","inline-flex","table","inline-table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row-group","table-row","flow-root","grid","inline-grid","contents","list-item","hidden"],sr:["sr-only","not-sr-only"],float:[{float:["right","left","none","start","end"]}],clear:[{clear:["left","right","both","none","start","end"]}],isolation:["isolate","isolation-auto"],"object-fit":[{object:["contain","cover","fill","none","scale-down"]}],"object-position":[{object:w()}],overflow:[{overflow:x()}],"overflow-x":[{"overflow-x":x()}],"overflow-y":[{"overflow-y":x()}],overscroll:[{overscroll:_()}],"overscroll-x":[{"overscroll-x":_()}],"overscroll-y":[{"overscroll-y":_()}],position:["static","fixed","absolute","relative","sticky"],inset:[{inset:E()}],"inset-x":[{"inset-x":E()}],"inset-y":[{"inset-y":E()}],start:[{start:E()}],end:[{end:E()}],top:[{top:E()}],right:[{right:E()}],bottom:[{bottom:E()}],left:[{left:E()}],visibility:["visible","invisible","collapse"],z:[{z:[D,"auto",X,B]}],basis:[{basis:[P,"full","auto",l,...S()]}],"flex-direction":[{flex:["row","row-reverse","col","col-reverse"]}],"flex-wrap":[{flex:["nowrap","wrap","wrap-reverse"]}],flex:[{flex:[F,P,"auto","initial","none",B]}],grow:[{grow:["",F,X,B]}],shrink:[{shrink:["",F,X,B]}],order:[{order:[D,"first","last","none",X,B]}],"grid-cols":[{"grid-cols":O()}],"col-start-end":[{col:R()}],"col-start":[{"col-start":M()}],"col-end":[{"col-end":M()}],"grid-rows":[{"grid-rows":O()}],"row-start-end":[{row:R()}],"row-start":[{"row-start":M()}],"row-end":[{"row-end":M()}],"grid-flow":[{"grid-flow":["row","col","dense","row-dense","col-dense"]}],"auto-cols":[{"auto-cols":j()}],"auto-rows":[{"auto-rows":j()}],gap:[{gap:S()}],"gap-x":[{"gap-x":S()}],"gap-y":[{"gap-y":S()}],"justify-content":[{justify:[...A(),"normal"]}],"justify-items":[{"justify-items":[...I(),"normal"]}],"justify-self":[{"justify-self":["auto",...I()]}],"align-content":[{content:["normal",...A()]}],"align-items":[{items:[...I(),{baseline:["","last"]}]}],"align-self":[{self:["auto",...I(),{baseline:["","last"]}]}],"place-content":[{"place-content":A()}],"place-items":[{"place-items":[...I(),"baseline"]}],"place-self":[{"place-self":["auto",...I()]}],p:[{p:S()}],px:[{px:S()}],py:[{py:S()}],ps:[{ps:S()}],pe:[{pe:S()}],pt:[{pt:S()}],pr:[{pr:S()}],pb:[{pb:S()}],pl:[{pl:S()}],m:[{m:T()}],mx:[{mx:T()}],my:[{my:T()}],ms:[{ms:T()}],me:[{me:T()}],mt:[{mt:T()}],mr:[{mr:T()}],mb:[{mb:T()}],ml:[{ml:T()}],"space-x":[{"space-x":S()}],"space-x-reverse":["space-x-reverse"],"space-y":[{"space-y":S()}],"space-y-reverse":["space-y-reverse"],size:[{size:L()}],w:[{w:[l,"screen",...L()]}],"min-w":[{"min-w":[l,"screen","none",...L()]}],"max-w":[{"max-w":[l,"screen","none","prose",{screen:[i]},...L()]}],h:[{h:["screen","lh",...L()]}],"min-h":[{"min-h":["screen","lh","none",...L()]}],"max-h":[{"max-h":["screen","lh",...L()]}],"font-size":[{text:["base",n,Q,G]}],"font-smoothing":["antialiased","subpixel-antialiased"],"font-style":["italic","not-italic"],"font-weight":[{font:[r,X,q]}],"font-stretch":[{"font-stretch":["ultra-condensed","extra-condensed","condensed","semi-condensed","normal","semi-expanded","expanded","extra-expanded","ultra-expanded",N,B]}],"font-family":[{font:[ee,B,t]}],"fvn-normal":["normal-nums"],"fvn-ordinal":["ordinal"],"fvn-slashed-zero":["slashed-zero"],"fvn-figure":["lining-nums","oldstyle-nums"],"fvn-spacing":["proportional-nums","tabular-nums"],"fvn-fraction":["diagonal-fractions","stacked-fractions"],tracking:[{tracking:[o,X,B]}],"line-clamp":[{"line-clamp":[F,"none",X,q]}],leading:[{leading:[s,...S()]}],"list-image":[{"list-image":["none",X,B]}],"list-style-position":[{list:["inside","outside"]}],"list-style-type":[{list:["disc","decimal","none",X,B]}],"text-alignment":[{text:["left","center","right","justify","start","end"]}],"placeholder-color":[{placeholder:U()}],"text-color":[{text:U()}],"text-decoration":["underline","overline","line-through","no-underline"],"text-decoration-style":[{decoration:[...ae(),"wavy"]}],"text-decoration-thickness":[{decoration:[F,"from-font","auto",X,G]}],"text-decoration-color":[{decoration:U()}],"underline-offset":[{"underline-offset":[F,"auto",X,B]}],"text-transform":["uppercase","lowercase","capitalize","normal-case"],"text-overflow":["truncate","text-ellipsis","text-clip"],"text-wrap":[{text:["wrap","nowrap","balance","pretty"]}],indent:[{indent:S()}],"vertical-align":[{align:["baseline","top","middle","bottom","text-top","text-bottom","sub","super",X,B]}],whitespace:[{whitespace:["normal","nowrap","pre","pre-line","pre-wrap","break-spaces"]}],break:[{break:["normal","words","all","keep"]}],wrap:[{wrap:["break-word","anywhere","normal"]}],hyphens:[{hyphens:["none","manual","auto"]}],content:[{content:["none",X,B]}],"bg-attachment":[{bg:["fixed","local","scroll"]}],"bg-clip":[{"bg-clip":["border","padding","content","text"]}],"bg-origin":[{"bg-origin":["border","padding","content"]}],"bg-position":[{bg:Z()}],"bg-repeat":[{bg:J()}],"bg-size":[{bg:se()}],"bg-image":[{bg:["none",{linear:[{to:["t","tr","r","br","b","bl","l","tl"]},D,X,B],radial:["",X,B],conic:[D,X,B]},re,K]}],"bg-color":[{bg:U()}],"gradient-from-pos":[{from:ie()}],"gradient-via-pos":[{via:ie()}],"gradient-to-pos":[{to:ie()}],"gradient-from":[{from:U()}],"gradient-via":[{via:U()}],"gradient-to":[{to:U()}],rounded:[{rounded:le()}],"rounded-s":[{"rounded-s":le()}],"rounded-e":[{"rounded-e":le()}],"rounded-t":[{"rounded-t":le()}],"rounded-r":[{"rounded-r":le()}],"rounded-b":[{"rounded-b":le()}],"rounded-l":[{"rounded-l":le()}],"rounded-ss":[{"rounded-ss":le()}],"rounded-se":[{"rounded-se":le()}],"rounded-ee":[{"rounded-ee":le()}],"rounded-es":[{"rounded-es":le()}],"rounded-tl":[{"rounded-tl":le()}],"rounded-tr":[{"rounded-tr":le()}],"rounded-br":[{"rounded-br":le()}],"rounded-bl":[{"rounded-bl":le()}],"border-w":[{border:ce()}],"border-w-x":[{"border-x":ce()}],"border-w-y":[{"border-y":ce()}],"border-w-s":[{"border-s":ce()}],"border-w-e":[{"border-e":ce()}],"border-w-t":[{"border-t":ce()}],"border-w-r":[{"border-r":ce()}],"border-w-b":[{"border-b":ce()}],"border-w-l":[{"border-l":ce()}],"divide-x":[{"divide-x":ce()}],"divide-x-reverse":["divide-x-reverse"],"divide-y":[{"divide-y":ce()}],"divide-y-reverse":["divide-y-reverse"],"border-style":[{border:[...ae(),"hidden","none"]}],"divide-style":[{divide:[...ae(),"hidden","none"]}],"border-color":[{border:U()}],"border-color-x":[{"border-x":U()}],"border-color-y":[{"border-y":U()}],"border-color-s":[{"border-s":U()}],"border-color-e":[{"border-e":U()}],"border-color-t":[{"border-t":U()}],"border-color-r":[{"border-r":U()}],"border-color-b":[{"border-b":U()}],"border-color-l":[{"border-l":U()}],"divide-color":[{divide:U()}],"outline-style":[{outline:[...ae(),"none","hidden"]}],"outline-offset":[{"outline-offset":[F,X,B]}],"outline-w":[{outline:["",F,Q,G]}],"outline-color":[{outline:U()}],shadow:[{shadow:["","none",u,oe,Y]}],"shadow-color":[{shadow:U()}],"inset-shadow":[{"inset-shadow":["none",f,oe,Y]}],"inset-shadow-color":[{"inset-shadow":U()}],"ring-w":[{ring:ce()}],"ring-w-inset":["ring-inset"],"ring-color":[{ring:U()}],"ring-offset-w":[{"ring-offset":[F,G]}],"ring-offset-color":[{"ring-offset":U()}],"inset-ring-w":[{"inset-ring":ce()}],"inset-ring-color":[{"inset-ring":U()}],"text-shadow":[{"text-shadow":["none",d,oe,Y]}],"text-shadow-color":[{"text-shadow":U()}],opacity:[{opacity:[F,X,B]}],"mix-blend":[{"mix-blend":[...ue(),"plus-darker","plus-lighter"]}],"bg-blend":[{"bg-blend":ue()}],"mask-clip":[{"mask-clip":["border","padding","content","fill","stroke","view"]},"mask-no-clip"],"mask-composite":[{mask:["add","subtract","intersect","exclude"]}],"mask-image-linear-pos":[{"mask-linear":[F]}],"mask-image-linear-from-pos":[{"mask-linear-from":fe()}],"mask-image-linear-to-pos":[{"mask-linear-to":fe()}],"mask-image-linear-from-color":[{"mask-linear-from":U()}],"mask-image-linear-to-color":[{"mask-linear-to":U()}],"mask-image-t-from-pos":[{"mask-t-from":fe()}],"mask-image-t-to-pos":[{"mask-t-to":fe()}],"mask-image-t-from-color":[{"mask-t-from":U()}],"mask-image-t-to-color":[{"mask-t-to":U()}],"mask-image-r-from-pos":[{"mask-r-from":fe()}],"mask-image-r-to-pos":[{"mask-r-to":fe()}],"mask-image-r-from-color":[{"mask-r-from":U()}],"mask-image-r-to-color":[{"mask-r-to":U()}],"mask-image-b-from-pos":[{"mask-b-from":fe()}],"mask-image-b-to-pos":[{"mask-b-to":fe()}],"mask-image-b-from-color":[{"mask-b-from":U()}],"mask-image-b-to-color":[{"mask-b-to":U()}],"mask-image-l-from-pos":[{"mask-l-from":fe()}],"mask-image-l-to-pos":[{"mask-l-to":fe()}],"mask-image-l-from-color":[{"mask-l-from":U()}],"mask-image-l-to-color":[{"mask-l-to":U()}],"mask-image-x-from-pos":[{"mask-x-from":fe()}],"mask-image-x-to-pos":[{"mask-x-to":fe()}],"mask-image-x-from-color":[{"mask-x-from":U()}],"mask-image-x-to-color":[{"mask-x-to":U()}],"mask-image-y-from-pos":[{"mask-y-from":fe()}],"mask-image-y-to-pos":[{"mask-y-to":fe()}],"mask-image-y-from-color":[{"mask-y-from":U()}],"mask-image-y-to-color":[{"mask-y-to":U()}],"mask-image-radial":[{"mask-radial":[X,B]}],"mask-image-radial-from-pos":[{"mask-radial-from":fe()}],"mask-image-radial-to-pos":[{"mask-radial-to":fe()}],"mask-image-radial-from-color":[{"mask-radial-from":U()}],"mask-image-radial-to-color":[{"mask-radial-to":U()}],"mask-image-radial-shape":[{"mask-radial":["circle","ellipse"]}],"mask-image-radial-size":[{"mask-radial":[{closest:["side","corner"],farthest:["side","corner"]}]}],"mask-image-radial-pos":[{"mask-radial-at":k()}],"mask-image-conic-pos":[{"mask-conic":[F]}],"mask-image-conic-from-pos":[{"mask-conic-from":fe()}],"mask-image-conic-to-pos":[{"mask-conic-to":fe()}],"mask-image-conic-from-color":[{"mask-conic-from":U()}],"mask-image-conic-to-color":[{"mask-conic-to":U()}],"mask-mode":[{mask:["alpha","luminance","match"]}],"mask-origin":[{"mask-origin":["border","padding","content","fill","stroke","view"]}],"mask-position":[{mask:Z()}],"mask-repeat":[{mask:J()}],"mask-size":[{mask:se()}],"mask-type":[{"mask-type":["alpha","luminance"]}],"mask-image":[{mask:["none",X,B]}],filter:[{filter:["","none",X,B]}],blur:[{blur:de()}],brightness:[{brightness:[F,X,B]}],contrast:[{contrast:[F,X,B]}],"drop-shadow":[{"drop-shadow":["","none",p,oe,Y]}],"drop-shadow-color":[{"drop-shadow":U()}],grayscale:[{grayscale:["",F,X,B]}],"hue-rotate":[{"hue-rotate":[F,X,B]}],invert:[{invert:["",F,X,B]}],saturate:[{saturate:[F,X,B]}],sepia:[{sepia:["",F,X,B]}],"backdrop-filter":[{"backdrop-filter":["","none",X,B]}],"backdrop-blur":[{"backdrop-blur":de()}],"backdrop-brightness":[{"backdrop-brightness":[F,X,B]}],"backdrop-contrast":[{"backdrop-contrast":[F,X,B]}],"backdrop-grayscale":[{"backdrop-grayscale":["",F,X,B]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[F,X,B]}],"backdrop-invert":[{"backdrop-invert":["",F,X,B]}],"backdrop-opacity":[{"backdrop-opacity":[F,X,B]}],"backdrop-saturate":[{"backdrop-saturate":[F,X,B]}],"backdrop-sepia":[{"backdrop-sepia":["",F,X,B]}],"border-collapse":[{border:["collapse","separate"]}],"border-spacing":[{"border-spacing":S()}],"border-spacing-x":[{"border-spacing-x":S()}],"border-spacing-y":[{"border-spacing-y":S()}],"table-layout":[{table:["auto","fixed"]}],caption:[{caption:["top","bottom"]}],transition:[{transition:["","all","colors","opacity","shadow","transform","none",X,B]}],"transition-behavior":[{transition:["normal","discrete"]}],duration:[{duration:[F,"initial",X,B]}],ease:[{ease:["linear","initial",v,X,B]}],delay:[{delay:[F,X,B]}],animate:[{animate:["none",y,X,B]}],backface:[{backface:["hidden","visible"]}],perspective:[{perspective:[m,X,B]}],"perspective-origin":[{"perspective-origin":w()}],rotate:[{rotate:pe()}],"rotate-x":[{"rotate-x":pe()}],"rotate-y":[{"rotate-y":pe()}],"rotate-z":[{"rotate-z":pe()}],scale:[{scale:he()}],"scale-x":[{"scale-x":he()}],"scale-y":[{"scale-y":he()}],"scale-z":[{"scale-z":he()}],"scale-3d":["scale-3d"],skew:[{skew:me()}],"skew-x":[{"skew-x":me()}],"skew-y":[{"skew-y":me()}],transform:[{transform:[X,B,"","none","gpu","cpu"]}],"transform-origin":[{origin:w()}],"transform-style":[{transform:["3d","flat"]}],translate:[{translate:ge()}],"translate-x":[{"translate-x":ge()}],"translate-y":[{"translate-y":ge()}],"translate-z":[{"translate-z":ge()}],"translate-none":["translate-none"],accent:[{accent:U()}],appearance:[{appearance:["none","auto"]}],"caret-color":[{caret:U()}],"color-scheme":[{scheme:["normal","dark","light","light-dark","only-dark","only-light"]}],cursor:[{cursor:["auto","default","pointer","wait","text","move","help","not-allowed","none","context-menu","progress","cell","crosshair","vertical-text","alias","copy","no-drop","grab","grabbing","all-scroll","col-resize","row-resize","n-resize","e-resize","s-resize","w-resize","ne-resize","nw-resize","se-resize","sw-resize","ew-resize","ns-resize","nesw-resize","nwse-resize","zoom-in","zoom-out",X,B]}],"field-sizing":[{"field-sizing":["fixed","content"]}],"pointer-events":[{"pointer-events":["auto","none"]}],resize:[{resize:["none","","y","x"]}],"scroll-behavior":[{scroll:["auto","smooth"]}],"scroll-m":[{"scroll-m":S()}],"scroll-mx":[{"scroll-mx":S()}],"scroll-my":[{"scroll-my":S()}],"scroll-ms":[{"scroll-ms":S()}],"scroll-me":[{"scroll-me":S()}],"scroll-mt":[{"scroll-mt":S()}],"scroll-mr":[{"scroll-mr":S()}],"scroll-mb":[{"scroll-mb":S()}],"scroll-ml":[{"scroll-ml":S()}],"scroll-p":[{"scroll-p":S()}],"scroll-px":[{"scroll-px":S()}],"scroll-py":[{"scroll-py":S()}],"scroll-ps":[{"scroll-ps":S()}],"scroll-pe":[{"scroll-pe":S()}],"scroll-pt":[{"scroll-pt":S()}],"scroll-pr":[{"scroll-pr":S()}],"scroll-pb":[{"scroll-pb":S()}],"scroll-pl":[{"scroll-pl":S()}],"snap-align":[{snap:["start","end","center","align-none"]}],"snap-stop":[{snap:["normal","always"]}],"snap-type":[{snap:["none","x","y","both"]}],"snap-strictness":[{snap:["mandatory","proximity"]}],touch:[{touch:["auto","none","manipulation"]}],"touch-x":[{"touch-pan":["x","left","right"]}],"touch-y":[{"touch-pan":["y","up","down"]}],"touch-pz":["touch-pinch-zoom"],select:[{select:["none","text","all","auto"]}],"will-change":[{"will-change":["auto","scroll","contents","transform",X,B]}],fill:[{fill:["none",...U()]}],"stroke-w":[{stroke:[F,Q,G,q]}],stroke:[{stroke:["none",...U()]}],"forced-color-adjust":[{"forced-color-adjust":["auto","none"]}]},conflictingClassGroups:{overflow:["overflow-x","overflow-y"],overscroll:["overscroll-x","overscroll-y"],inset:["inset-x","inset-y","start","end","top","right","bottom","left"],"inset-x":["right","left"],"inset-y":["top","bottom"],flex:["basis","grow","shrink"],gap:["gap-x","gap-y"],p:["px","py","ps","pe","pt","pr","pb","pl"],px:["pr","pl"],py:["pt","pb"],m:["mx","my","ms","me","mt","mr","mb","ml"],mx:["mr","ml"],my:["mt","mb"],size:["w","h"],"font-size":["leading"],"fvn-normal":["fvn-ordinal","fvn-slashed-zero","fvn-figure","fvn-spacing","fvn-fraction"],"fvn-ordinal":["fvn-normal"],"fvn-slashed-zero":["fvn-normal"],"fvn-figure":["fvn-normal"],"fvn-spacing":["fvn-normal"],"fvn-fraction":["fvn-normal"],"line-clamp":["display","overflow"],rounded:["rounded-s","rounded-e","rounded-t","rounded-r","rounded-b","rounded-l","rounded-ss","rounded-se","rounded-ee","rounded-es","rounded-tl","rounded-tr","rounded-br","rounded-bl"],"rounded-s":["rounded-ss","rounded-es"],"rounded-e":["rounded-se","rounded-ee"],"rounded-t":["rounded-tl","rounded-tr"],"rounded-r":["rounded-tr","rounded-br"],"rounded-b":["rounded-br","rounded-bl"],"rounded-l":["rounded-tl","rounded-bl"],"border-spacing":["border-spacing-x","border-spacing-y"],"border-w":["border-w-x","border-w-y","border-w-s","border-w-e","border-w-t","border-w-r","border-w-b","border-w-l"],"border-w-x":["border-w-r","border-w-l"],"border-w-y":["border-w-t","border-w-b"],"border-color":["border-color-x","border-color-y","border-color-s","border-color-e","border-color-t","border-color-r","border-color-b","border-color-l"],"border-color-x":["border-color-r","border-color-l"],"border-color-y":["border-color-t","border-color-b"],translate:["translate-x","translate-y","translate-none"],"translate-none":["translate","translate-x","translate-y","translate-z"],"scroll-m":["scroll-mx","scroll-my","scroll-ms","scroll-me","scroll-mt","scroll-mr","scroll-mb","scroll-ml"],"scroll-mx":["scroll-mr","scroll-ml"],"scroll-my":["scroll-mt","scroll-mb"],"scroll-p":["scroll-px","scroll-py","scroll-ps","scroll-pe","scroll-pt","scroll-pr","scroll-pb","scroll-pl"],"scroll-px":["scroll-pr","scroll-pl"],"scroll-py":["scroll-pt","scroll-pb"],touch:["touch-x","touch-y","touch-pz"],"touch-x":["touch"],"touch-y":["touch"],"touch-pz":["touch"]},conflictingClassGroupModifiers:{"font-size":["leading"]},orderSensitiveModifiers:["*","**","after","backdrop","before","details-content","file","first-letter","first-line","marker","placeholder","selection"]}}),me=S(he)},201:function(e,t,n){n.d(t,{PO:function(){return ce},p7:function(){return ot},tv:function(){return it},yj:function(){return lt}});var r=n(252),o=n(262); /*! * vue-router v4.5.1 * (c) 2025 Eduardo San Martin Morote * @license MIT */ -const s="undefined"!==typeof document;function i(e){return"object"===typeof e||"displayName"in e||"props"in e||"__vccOpts"in e}function l(e){return e.__esModule||"Module"===e[Symbol.toStringTag]||e.default&&i(e.default)}const c=Object.assign;function a(e,t){const n={};for(const r in t){const o=t[r];n[r]=f(o)?o.map(e):e(o)}return n}const u=()=>{},f=Array.isArray;const d=/#/g,p=/&/g,h=/\//g,m=/=/g,g=/\?/g,v=/\+/g,b=/%5B/g,y=/%5D/g,k=/%5E/g,w=/%60/g,x=/%7B/g,_=/%7C/g,S=/%7D/g,C=/%20/g;function E(e){return encodeURI(""+e).replace(_,"|").replace(b,"[").replace(y,"]")}function O(e){return E(e).replace(x,"{").replace(S,"}").replace(k,"^")}function R(e){return E(e).replace(v,"%2B").replace(C,"+").replace(d,"%23").replace(p,"%26").replace(w,"`").replace(x,"{").replace(S,"}").replace(k,"^")}function j(e){return R(e).replace(m,"%3D")}function M(e){return E(e).replace(d,"%23").replace(g,"%3F")}function A(e){return null==e?"":M(e).replace(h,"%2F")}function I(e){try{return decodeURIComponent(""+e)}catch(t){}return""+e}const T=/\/$/,P=e=>e.replace(T,"");function F(e,t,n="/"){let r,o={},s="",i="";const l=t.indexOf("#");let c=t.indexOf("?");return l=0&&(c=-1),c>-1&&(r=t.slice(0,c),s=t.slice(c+1,l>-1?l:t.length),o=e(s)),l>-1&&(r=r||t.slice(0,l),i=t.slice(l,t.length)),r=Z(null!=r?r:t,n),{fullPath:r+(s&&"?")+s+i,path:r,query:o,hash:I(i)}}function D(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function N(e,t){return t&&e.toLowerCase().startsWith(t.toLowerCase())?e.slice(t.length)||"/":e}function $(e,t,n){const r=t.matched.length-1,o=n.matched.length-1;return r>-1&&r===o&&L(t.matched[r],n.matched[o])&&U(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function L(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function U(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(const n in e)if(!z(e[n],t[n]))return!1;return!0}function z(e,t){return f(e)?J(e,t):f(t)?J(t,e):e===t}function J(e,t){return f(t)?e.length===t.length&&e.every(((e,n)=>e===t[n])):1===e.length&&e[0]===t}function Z(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),r=e.split("/"),o=r[r.length-1];".."!==o&&"."!==o||r.push("");let s,i,l=n.length-1;for(s=0;s1&&l--}return n.slice(0,l).join("/")+"/"+r.slice(s).join("/")}const V={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};var H,B;(function(e){e["pop"]="pop",e["push"]="push"})(H||(H={})),function(e){e["back"]="back",e["forward"]="forward",e["unknown"]=""}(B||(B={}));function G(e){if(!e)if(s){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return"/"!==e[0]&&"#"!==e[0]&&(e="/"+e),P(e)}const q=/^[^#]+#/;function W(e,t){return e.replace(q,"#")+t}function K(e,t){const n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}const Y=()=>({left:window.scrollX,top:window.scrollY});function X(e){let t;if("el"in e){const n=e.el,r="string"===typeof n&&n.startsWith("#");0;const o="string"===typeof n?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!o)return;t=K(o,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(null!=t.left?t.left:window.scrollX,null!=t.top?t.top:window.scrollY)}function Q(e,t){const n=history.state?history.state.position-t:-1;return n+e}const ee=new Map;function te(e,t){ee.set(e,t)}function ne(e){const t=ee.get(e);return ee.delete(e),t}let re=()=>location.protocol+"//"+location.host;function oe(e,t){const{pathname:n,search:r,hash:o}=t,s=e.indexOf("#");if(s>-1){let t=o.includes(e.slice(s))?e.slice(s).length:1,n=o.slice(t);return"/"!==n[0]&&(n="/"+n),N(n,"")}const i=N(n,e);return i+r+o}function se(e,t,n,r){let o=[],s=[],i=null;const l=({state:s})=>{const l=oe(e,location),c=n.value,a=t.value;let u=0;if(s){if(n.value=l,t.value=s,i&&i===c)return void(i=null);u=a?s.position-a.position:0}else r(l);o.forEach((e=>{e(n.value,c,{delta:u,type:H.pop,direction:u?u>0?B.forward:B.back:B.unknown})}))};function a(){i=n.value}function u(e){o.push(e);const t=()=>{const t=o.indexOf(e);t>-1&&o.splice(t,1)};return s.push(t),t}function f(){const{history:e}=window;e.state&&e.replaceState(c({},e.state,{scroll:Y()}),"")}function d(){for(const e of s)e();s=[],window.removeEventListener("popstate",l),window.removeEventListener("beforeunload",f)}return window.addEventListener("popstate",l),window.addEventListener("beforeunload",f,{passive:!0}),{pauseListeners:a,listen:u,destroy:d}}function ie(e,t,n,r=!1,o=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:o?Y():null}}function le(e){const{history:t,location:n}=window,r={value:oe(e,n)},o={value:t.state};function s(r,s,i){const l=e.indexOf("#"),c=l>-1?(n.host&&document.querySelector("base")?e:e.slice(l))+r:re()+e+r;try{t[i?"replaceState":"pushState"](s,"",c),o.value=s}catch(a){console.error(a),n[i?"replace":"assign"](c)}}function i(e,n){const i=c({},t.state,ie(o.value.back,e,o.value.forward,!0),n,{position:o.value.position});s(e,i,!0),r.value=e}function l(e,n){const i=c({},o.value,t.state,{forward:e,scroll:Y()});s(i.current,i,!0);const l=c({},ie(r.value,e,null),{position:i.position+1},n);s(e,l,!1),r.value=e}return o.value||s(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0),{location:r,state:o,push:l,replace:i}}function ce(e){e=G(e);const t=le(e),n=se(e,t.state,t.location,t.replace);function r(e,t=!0){t||n.pauseListeners(),history.go(e)}const o=c({location:"",base:e,go:r,createHref:W.bind(null,e)},t,n);return Object.defineProperty(o,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(o,"state",{enumerable:!0,get:()=>t.state.value}),o}function ae(e){return"string"===typeof e||e&&"object"===typeof e}function ue(e){return"string"===typeof e||"symbol"===typeof e}const fe=Symbol("");var de;(function(e){e[e["aborted"]=4]="aborted",e[e["cancelled"]=8]="cancelled",e[e["duplicated"]=16]="duplicated"})(de||(de={}));function pe(e,t){return c(new Error,{type:e,[fe]:!0},t)}function he(e,t){return e instanceof Error&&fe in e&&(null==t||!!(e.type&t))}const me="[^/]+?",ge={sensitive:!1,strict:!1,start:!0,end:!0},ve=/[.+*?^${}()[\]/\\]/g;function be(e,t){const n=c({},ge,t),r=[];let o=n.start?"^":"";const s=[];for(const c of e){const e=c.length?[]:[90];n.strict&&!c.length&&(o+="/");for(let t=0;tt.length?1===t.length&&80===t[0]?1:-1:0}function ke(e,t){let n=0;const r=e.score,o=t.score;while(n0&&t[t.length-1]<0}const xe={type:0,value:""},_e=/[a-zA-Z0-9_]/;function Se(e){if(!e)return[[]];if("/"===e)return[[xe]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(e){throw new Error(`ERR (${n})/"${a}": ${e}`)}let n=0,r=n;const o=[];let s;function i(){s&&o.push(s),s=[]}let l,c=0,a="",u="";function f(){a&&(0===n?s.push({type:0,value:a}):1===n||2===n||3===n?(s.length>1&&("*"===l||"+"===l)&&t(`A repeatable param (${a}) must be alone in its segment. eg: '/:ids+.`),s.push({type:1,value:a,regexp:u,repeatable:"*"===l||"+"===l,optional:"*"===l||"?"===l})):t("Invalid state to consume buffer"),a="")}function d(){a+=l}while(c{i(h)}:u}function i(e){if(ue(e)){const t=r.get(e);t&&(r.delete(e),n.splice(n.indexOf(t),1),t.children.forEach(i),t.alias.forEach(i))}else{const t=n.indexOf(e);t>-1&&(n.splice(t,1),e.record.name&&r.delete(e.record.name),e.children.forEach(i),e.alias.forEach(i))}}function l(){return n}function a(e){const t=Te(e,n);n.splice(t,0,e),e.record.name&&!Me(e)&&r.set(e.record.name,e)}function f(e,t){let o,s,i,l={};if("name"in e&&e.name){if(o=r.get(e.name),!o)throw pe(1,{location:e});0,i=o.record.name,l=c(Oe(t.params,o.keys.filter((e=>!e.optional)).concat(o.parent?o.parent.keys.filter((e=>e.optional)):[]).map((e=>e.name))),e.params&&Oe(e.params,o.keys.map((e=>e.name)))),s=o.stringify(l)}else if(null!=e.path)s=e.path,o=n.find((e=>e.re.test(s))),o&&(l=o.parse(s),i=o.record.name);else{if(o=t.name?r.get(t.name):n.find((e=>e.re.test(t.path))),!o)throw pe(1,{location:e,currentLocation:t});i=o.record.name,l=c({},t.params,e.params),s=o.stringify(l)}const a=[];let u=o;while(u)a.unshift(u.record),u=u.parent;return{name:i,path:s,params:l,matched:a,meta:Ae(a)}}function d(){n.length=0,r.clear()}return t=Ie({strict:!1,end:!0,sensitive:!1},t),e.forEach((e=>s(e))),{addRoute:s,resolve:f,removeRoute:i,clearRoutes:d,getRoutes:l,getRecordMatcher:o}}function Oe(e,t){const n={};for(const r of t)r in e&&(n[r]=e[r]);return n}function Re(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:je(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function je(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const r in e.components)t[r]="object"===typeof n?n[r]:n;return t}function Me(e){while(e){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Ae(e){return e.reduce(((e,t)=>c(e,t.meta)),{})}function Ie(e,t){const n={};for(const r in e)n[r]=r in t?t[r]:e[r];return n}function Te(e,t){let n=0,r=t.length;while(n!==r){const o=n+r>>1,s=ke(e,t[o]);s<0?r=o:n=o+1}const o=Pe(e);return o&&(r=t.lastIndexOf(o,r-1)),r}function Pe(e){let t=e;while(t=t.parent)if(Fe(t)&&0===ke(e,t))return t}function Fe({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function De(e){const t={};if(""===e||"?"===e)return t;const n="?"===e[0],r=(n?e.slice(1):e).split("&");for(let o=0;oe&&R(e))):[r&&R(r)];o.forEach((e=>{void 0!==e&&(t+=(t.length?"&":"")+n,null!=e&&(t+="="+e))}))}return t}function $e(e){const t={};for(const n in e){const r=e[n];void 0!==r&&(t[n]=f(r)?r.map((e=>null==e?null:""+e)):null==r?r:""+r)}return t}const Le=Symbol(""),Ue=Symbol(""),ze=Symbol(""),Je=Symbol(""),Ze=Symbol("");function Ve(){let e=[];function t(t){return e.push(t),()=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function He(e,t,n,r,o,s=(e=>e())){const i=r&&(r.enterCallbacks[o]=r.enterCallbacks[o]||[]);return()=>new Promise(((l,c)=>{const a=e=>{!1===e?c(pe(4,{from:n,to:t})):e instanceof Error?c(e):ae(e)?c(pe(2,{from:t,to:e})):(i&&r.enterCallbacks[o]===i&&"function"===typeof e&&i.push(e),l())},u=s((()=>e.call(r&&r.instances[o],t,n,a)));let f=Promise.resolve(u);e.length<3&&(f=f.then(a)),f.catch((e=>c(e)))}))}function Be(e,t,n,r,o=(e=>e())){const s=[];for(const c of e){0;for(const e in c.components){let a=c.components[e];if("beforeRouteEnter"===t||c.instances[e])if(i(a)){const i=a.__vccOpts||a,l=i[t];l&&s.push(He(l,n,r,c,e,o))}else{let i=a();0,s.push((()=>i.then((s=>{if(!s)throw new Error(`Couldn't resolve component "${e}" at "${c.path}"`);const i=l(s)?s.default:s;c.mods[e]=s,c.components[e]=i;const a=i.__vccOpts||i,u=a[t];return u&&He(u,n,r,c,e,o)()}))))}}}return s}function Ge(e){const t=(0,r.f3)(ze),n=(0,r.f3)(Je);const s=(0,r.Fl)((()=>{const n=(0,o.SU)(e.to);return t.resolve(n)})),i=(0,r.Fl)((()=>{const{matched:e}=s.value,{length:t}=e,r=e[t-1],o=n.matched;if(!r||!o.length)return-1;const i=o.findIndex(L.bind(null,r));if(i>-1)return i;const l=Qe(e[t-2]);return t>1&&Qe(r)===l&&o[o.length-1].path!==l?o.findIndex(L.bind(null,e[t-2])):i})),l=(0,r.Fl)((()=>i.value>-1&&Xe(n.params,s.value.params))),c=(0,r.Fl)((()=>i.value>-1&&i.value===n.matched.length-1&&U(n.params,s.value.params)));function a(n={}){if(Ye(n)){const n=t[(0,o.SU)(e.replace)?"replace":"push"]((0,o.SU)(e.to)).catch(u);return e.viewTransition&&"undefined"!==typeof document&&"startViewTransition"in document&&document.startViewTransition((()=>n)),n}return Promise.resolve()}return{route:s,href:(0,r.Fl)((()=>s.value.href)),isActive:l,isExactActive:c,navigate:a}}function qe(e){return 1===e.length?e[0]:e}const We=(0,r.aZ)({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"},viewTransition:Boolean},useLink:Ge,setup(e,{slots:t}){const n=(0,o.qj)(Ge(e)),{options:s}=(0,r.f3)(ze),i=(0,r.Fl)((()=>({[et(e.activeClass,s.linkActiveClass,"router-link-active")]:n.isActive,[et(e.exactActiveClass,s.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive})));return()=>{const o=t.default&&qe(t.default(n));return e.custom?o:(0,r.h)("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:i.value},o)}}}),Ke=We;function Ye(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&(void 0===e.button||0===e.button)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function Xe(e,t){for(const n in t){const r=t[n],o=e[n];if("string"===typeof r){if(r!==o)return!1}else if(!f(o)||o.length!==r.length||r.some(((e,t)=>e!==o[t])))return!1}return!0}function Qe(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const et=(e,t,n)=>null!=e?e:null!=t?t:n,tt=(0,r.aZ)({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const s=(0,r.f3)(Ze),i=(0,r.Fl)((()=>e.route||s.value)),l=(0,r.f3)(Ue,0),a=(0,r.Fl)((()=>{let e=(0,o.SU)(l);const{matched:t}=i.value;let n;while((n=t[e])&&!n.components)e++;return e})),u=(0,r.Fl)((()=>i.value.matched[a.value]));(0,r.JJ)(Ue,(0,r.Fl)((()=>a.value+1))),(0,r.JJ)(Le,u),(0,r.JJ)(Ze,i);const f=(0,o.iH)();return(0,r.YP)((()=>[f.value,u.value,e.name]),(([e,t,n],[r,o,s])=>{t&&(t.instances[n]=e,o&&o!==t&&e&&e===r&&(t.leaveGuards.size||(t.leaveGuards=o.leaveGuards),t.updateGuards.size||(t.updateGuards=o.updateGuards))),!e||!t||o&&L(t,o)&&r||(t.enterCallbacks[n]||[]).forEach((t=>t(e)))}),{flush:"post"}),()=>{const o=i.value,s=e.name,l=u.value,a=l&&l.components[s];if(!a)return nt(n.default,{Component:a,route:o});const d=l.props[s],p=d?!0===d?o.params:"function"===typeof d?d(o):d:null,h=e=>{e.component.isUnmounted&&(l.instances[s]=null)},m=(0,r.h)(a,c({},p,t,{onVnodeUnmounted:h,ref:f}));return nt(n.default,{Component:m,route:o})||m}}});function nt(e,t){if(!e)return null;const n=e(t);return 1===n.length?n[0]:n}const rt=tt;function ot(e){const t=Ee(e.routes,e),n=e.parseQuery||De,i=e.stringifyQuery||Ne,l=e.history;const d=Ve(),p=Ve(),h=Ve(),m=(0,o.XI)(V);let g=V;s&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const v=a.bind(null,(e=>""+e)),b=a.bind(null,A),y=a.bind(null,I);function k(e,n){let r,o;return ue(e)?(r=t.getRecordMatcher(e),o=n):o=e,t.addRoute(o,r)}function w(e){const n=t.getRecordMatcher(e);n&&t.removeRoute(n)}function x(){return t.getRoutes().map((e=>e.record))}function _(e){return!!t.getRecordMatcher(e)}function S(e,r){if(r=c({},r||m.value),"string"===typeof e){const o=F(n,e,r.path),s=t.resolve({path:o.path},r),i=l.createHref(o.fullPath);return c(o,s,{params:y(s.params),hash:I(o.hash),redirectedFrom:void 0,href:i})}let o;if(null!=e.path)o=c({},e,{path:F(n,e.path,r.path).path});else{const t=c({},e.params);for(const e in t)null==t[e]&&delete t[e];o=c({},e,{params:b(t)}),r.params=b(r.params)}const s=t.resolve(o,r),a=e.hash||"";s.params=v(y(s.params));const u=D(i,c({},e,{hash:O(a),path:s.path})),f=l.createHref(u);return c({fullPath:u,hash:a,query:i===Ne?$e(e.query):e.query||{}},s,{redirectedFrom:void 0,href:f})}function C(e){return"string"===typeof e?F(n,e,m.value.path):c({},e)}function E(e,t){if(g!==e)return pe(8,{from:t,to:e})}function R(e){return T(e)}function j(e){return R(c(C(e),{replace:!0}))}function M(e){const t=e.matched[e.matched.length-1];if(t&&t.redirect){const{redirect:n}=t;let r="function"===typeof n?n(e):n;return"string"===typeof r&&(r=r.includes("?")||r.includes("#")?r=C(r):{path:r},r.params={}),c({query:e.query,hash:e.hash,params:null!=r.path?{}:e.params},r)}}function T(e,t){const n=g=S(e),r=m.value,o=e.state,s=e.force,l=!0===e.replace,a=M(n);if(a)return T(c(C(a),{state:"object"===typeof a?c({},o,a.state):o,force:s,replace:l}),t||n);const u=n;let f;return u.redirectedFrom=t,!s&&$(i,r,n)&&(f=pe(16,{to:u,from:r}),re(r,r,!0,!1)),(f?Promise.resolve(f):L(u,r)).catch((e=>he(e)?he(e,2)?e:ee(e):W(e,u,r))).then((e=>{if(e){if(he(e,2))return T(c({replace:l},C(e.to),{state:"object"===typeof e.to?c({},o,e.to.state):o,force:s}),t||u)}else e=z(u,r,!0,l,o);return U(u,r,e),e}))}function P(e,t){const n=E(e,t);return n?Promise.reject(n):Promise.resolve()}function N(e){const t=ie.values().next().value;return t&&"function"===typeof t.runWithContext?t.runWithContext(e):e()}function L(e,t){let n;const[r,o,s]=st(e,t);n=Be(r.reverse(),"beforeRouteLeave",e,t);for(const l of r)l.leaveGuards.forEach((r=>{n.push(He(r,e,t))}));const i=P.bind(null,e,t);return n.push(i),ce(n).then((()=>{n=[];for(const r of d.list())n.push(He(r,e,t));return n.push(i),ce(n)})).then((()=>{n=Be(o,"beforeRouteUpdate",e,t);for(const r of o)r.updateGuards.forEach((r=>{n.push(He(r,e,t))}));return n.push(i),ce(n)})).then((()=>{n=[];for(const r of s)if(r.beforeEnter)if(f(r.beforeEnter))for(const o of r.beforeEnter)n.push(He(o,e,t));else n.push(He(r.beforeEnter,e,t));return n.push(i),ce(n)})).then((()=>(e.matched.forEach((e=>e.enterCallbacks={})),n=Be(s,"beforeRouteEnter",e,t,N),n.push(i),ce(n)))).then((()=>{n=[];for(const r of p.list())n.push(He(r,e,t));return n.push(i),ce(n)})).catch((e=>he(e,8)?e:Promise.reject(e)))}function U(e,t,n){h.list().forEach((r=>N((()=>r(e,t,n)))))}function z(e,t,n,r,o){const i=E(e,t);if(i)return i;const a=t===V,u=s?history.state:{};n&&(r||a?l.replace(e.fullPath,c({scroll:a&&u&&u.scroll},o)):l.push(e.fullPath,o)),m.value=e,re(e,t,n,a),ee()}let J;function Z(){J||(J=l.listen(((e,t,n)=>{if(!le.listening)return;const r=S(e),o=M(r);if(o)return void T(c(o,{replace:!0,force:!0}),r).catch(u);g=r;const i=m.value;s&&te(Q(i.fullPath,n.delta),Y()),L(r,i).catch((e=>he(e,12)?e:he(e,2)?(T(c(C(e.to),{force:!0}),r).then((e=>{he(e,20)&&!n.delta&&n.type===H.pop&&l.go(-1,!1)})).catch(u),Promise.reject()):(n.delta&&l.go(-n.delta,!1),W(e,r,i)))).then((e=>{e=e||z(r,i,!1),e&&(n.delta&&!he(e,8)?l.go(-n.delta,!1):n.type===H.pop&&he(e,20)&&l.go(-1,!1)),U(r,i,e)})).catch(u)})))}let B,G=Ve(),q=Ve();function W(e,t,n){ee(e);const r=q.list();return r.length?r.forEach((r=>r(e,t,n))):console.error(e),Promise.reject(e)}function K(){return B&&m.value!==V?Promise.resolve():new Promise(((e,t)=>{G.add([e,t])}))}function ee(e){return B||(B=!e,Z(),G.list().forEach((([t,n])=>e?n(e):t())),G.reset()),e}function re(t,n,o,i){const{scrollBehavior:l}=e;if(!s||!l)return Promise.resolve();const c=!o&&ne(Q(t.fullPath,0))||(i||!o)&&history.state&&history.state.scroll||null;return(0,r.Y3)().then((()=>l(t,n,c))).then((e=>e&&X(e))).catch((e=>W(e,t,n)))}const oe=e=>l.go(e);let se;const ie=new Set,le={currentRoute:m,listening:!0,addRoute:k,removeRoute:w,clearRoutes:t.clearRoutes,hasRoute:_,getRoutes:x,resolve:S,options:e,push:R,replace:j,go:oe,back:()=>oe(-1),forward:()=>oe(1),beforeEach:d.add,beforeResolve:p.add,afterEach:h.add,onError:q.add,isReady:K,install(e){const t=this;e.component("RouterLink",Ke),e.component("RouterView",rt),e.config.globalProperties.$router=t,Object.defineProperty(e.config.globalProperties,"$route",{enumerable:!0,get:()=>(0,o.SU)(m)}),s&&!se&&m.value===V&&(se=!0,R(l.location).catch((e=>{0})));const n={};for(const o in V)Object.defineProperty(n,o,{get:()=>m.value[o],enumerable:!0});e.provide(ze,t),e.provide(Je,(0,o.Um)(n)),e.provide(Ze,m);const r=e.unmount;ie.add(e),e.unmount=function(){ie.delete(e),ie.size<1&&(g=V,J&&J(),J=null,m.value=V,se=!1,B=!1),r()}}};function ce(e){return e.reduce(((e,t)=>e.then((()=>N(t)))),Promise.resolve())}return le}function st(e,t){const n=[],r=[],o=[],s=Math.max(t.matched.length,e.matched.length);for(let i=0;iL(e,s)))?r.push(s):n.push(s));const l=e.matched[i];l&&(t.matched.find((e=>L(e,l)))||o.push(l))}return[n,r,o]}function it(){return(0,r.f3)(ze)}function lt(e){return(0,r.f3)(Je)}}}]); \ No newline at end of file +const s="undefined"!==typeof document;function i(e){return"object"===typeof e||"displayName"in e||"props"in e||"__vccOpts"in e}function l(e){return e.__esModule||"Module"===e[Symbol.toStringTag]||e.default&&i(e.default)}const c=Object.assign;function a(e,t){const n={};for(const r in t){const o=t[r];n[r]=f(o)?o.map(e):e(o)}return n}const u=()=>{},f=Array.isArray;const d=/#/g,p=/&/g,h=/\//g,m=/=/g,g=/\?/g,v=/\+/g,y=/%5B/g,b=/%5D/g,k=/%5E/g,w=/%60/g,x=/%7B/g,_=/%7C/g,S=/%7D/g,C=/%20/g;function E(e){return encodeURI(""+e).replace(_,"|").replace(y,"[").replace(b,"]")}function O(e){return E(e).replace(x,"{").replace(S,"}").replace(k,"^")}function R(e){return E(e).replace(v,"%2B").replace(C,"+").replace(d,"%23").replace(p,"%26").replace(w,"`").replace(x,"{").replace(S,"}").replace(k,"^")}function M(e){return R(e).replace(m,"%3D")}function j(e){return E(e).replace(d,"%23").replace(g,"%3F")}function A(e){return null==e?"":j(e).replace(h,"%2F")}function I(e){try{return decodeURIComponent(""+e)}catch(t){}return""+e}const T=/\/$/,P=e=>e.replace(T,"");function F(e,t,n="/"){let r,o={},s="",i="";const l=t.indexOf("#");let c=t.indexOf("?");return l=0&&(c=-1),c>-1&&(r=t.slice(0,c),s=t.slice(c+1,l>-1?l:t.length),o=e(s)),l>-1&&(r=r||t.slice(0,l),i=t.slice(l,t.length)),r=J(null!=r?r:t,n),{fullPath:r+(s&&"?")+s+i,path:r,query:o,hash:I(i)}}function D(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function N(e,t){return t&&e.toLowerCase().startsWith(t.toLowerCase())?e.slice(t.length)||"/":e}function $(e,t,n){const r=t.matched.length-1,o=n.matched.length-1;return r>-1&&r===o&&z(t.matched[r],n.matched[o])&&L(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function z(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function L(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(const n in e)if(!U(e[n],t[n]))return!1;return!0}function U(e,t){return f(e)?Z(e,t):f(t)?Z(t,e):e===t}function Z(e,t){return f(t)?e.length===t.length&&e.every(((e,n)=>e===t[n])):1===e.length&&e[0]===t}function J(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),r=e.split("/"),o=r[r.length-1];".."!==o&&"."!==o||r.push("");let s,i,l=n.length-1;for(s=0;s1&&l--}return n.slice(0,l).join("/")+"/"+r.slice(s).join("/")}const V={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};var H,B;(function(e){e["pop"]="pop",e["push"]="push"})(H||(H={})),function(e){e["back"]="back",e["forward"]="forward",e["unknown"]=""}(B||(B={}));function G(e){if(!e)if(s){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return"/"!==e[0]&&"#"!==e[0]&&(e="/"+e),P(e)}const q=/^[^#]+#/;function W(e,t){return e.replace(q,"#")+t}function K(e,t){const n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}const Y=()=>({left:window.scrollX,top:window.scrollY});function X(e){let t;if("el"in e){const n=e.el,r="string"===typeof n&&n.startsWith("#");0;const o="string"===typeof n?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!o)return;t=K(o,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(null!=t.left?t.left:window.scrollX,null!=t.top?t.top:window.scrollY)}function Q(e,t){const n=history.state?history.state.position-t:-1;return n+e}const ee=new Map;function te(e,t){ee.set(e,t)}function ne(e){const t=ee.get(e);return ee.delete(e),t}let re=()=>location.protocol+"//"+location.host;function oe(e,t){const{pathname:n,search:r,hash:o}=t,s=e.indexOf("#");if(s>-1){let t=o.includes(e.slice(s))?e.slice(s).length:1,n=o.slice(t);return"/"!==n[0]&&(n="/"+n),N(n,"")}const i=N(n,e);return i+r+o}function se(e,t,n,r){let o=[],s=[],i=null;const l=({state:s})=>{const l=oe(e,location),c=n.value,a=t.value;let u=0;if(s){if(n.value=l,t.value=s,i&&i===c)return void(i=null);u=a?s.position-a.position:0}else r(l);o.forEach((e=>{e(n.value,c,{delta:u,type:H.pop,direction:u?u>0?B.forward:B.back:B.unknown})}))};function a(){i=n.value}function u(e){o.push(e);const t=()=>{const t=o.indexOf(e);t>-1&&o.splice(t,1)};return s.push(t),t}function f(){const{history:e}=window;e.state&&e.replaceState(c({},e.state,{scroll:Y()}),"")}function d(){for(const e of s)e();s=[],window.removeEventListener("popstate",l),window.removeEventListener("beforeunload",f)}return window.addEventListener("popstate",l),window.addEventListener("beforeunload",f,{passive:!0}),{pauseListeners:a,listen:u,destroy:d}}function ie(e,t,n,r=!1,o=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:o?Y():null}}function le(e){const{history:t,location:n}=window,r={value:oe(e,n)},o={value:t.state};function s(r,s,i){const l=e.indexOf("#"),c=l>-1?(n.host&&document.querySelector("base")?e:e.slice(l))+r:re()+e+r;try{t[i?"replaceState":"pushState"](s,"",c),o.value=s}catch(a){console.error(a),n[i?"replace":"assign"](c)}}function i(e,n){const i=c({},t.state,ie(o.value.back,e,o.value.forward,!0),n,{position:o.value.position});s(e,i,!0),r.value=e}function l(e,n){const i=c({},o.value,t.state,{forward:e,scroll:Y()});s(i.current,i,!0);const l=c({},ie(r.value,e,null),{position:i.position+1},n);s(e,l,!1),r.value=e}return o.value||s(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0),{location:r,state:o,push:l,replace:i}}function ce(e){e=G(e);const t=le(e),n=se(e,t.state,t.location,t.replace);function r(e,t=!0){t||n.pauseListeners(),history.go(e)}const o=c({location:"",base:e,go:r,createHref:W.bind(null,e)},t,n);return Object.defineProperty(o,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(o,"state",{enumerable:!0,get:()=>t.state.value}),o}function ae(e){return"string"===typeof e||e&&"object"===typeof e}function ue(e){return"string"===typeof e||"symbol"===typeof e}const fe=Symbol("");var de;(function(e){e[e["aborted"]=4]="aborted",e[e["cancelled"]=8]="cancelled",e[e["duplicated"]=16]="duplicated"})(de||(de={}));function pe(e,t){return c(new Error,{type:e,[fe]:!0},t)}function he(e,t){return e instanceof Error&&fe in e&&(null==t||!!(e.type&t))}const me="[^/]+?",ge={sensitive:!1,strict:!1,start:!0,end:!0},ve=/[.+*?^${}()[\]/\\]/g;function ye(e,t){const n=c({},ge,t),r=[];let o=n.start?"^":"";const s=[];for(const c of e){const e=c.length?[]:[90];n.strict&&!c.length&&(o+="/");for(let t=0;tt.length?1===t.length&&80===t[0]?1:-1:0}function ke(e,t){let n=0;const r=e.score,o=t.score;while(n0&&t[t.length-1]<0}const xe={type:0,value:""},_e=/[a-zA-Z0-9_]/;function Se(e){if(!e)return[[]];if("/"===e)return[[xe]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(e){throw new Error(`ERR (${n})/"${a}": ${e}`)}let n=0,r=n;const o=[];let s;function i(){s&&o.push(s),s=[]}let l,c=0,a="",u="";function f(){a&&(0===n?s.push({type:0,value:a}):1===n||2===n||3===n?(s.length>1&&("*"===l||"+"===l)&&t(`A repeatable param (${a}) must be alone in its segment. eg: '/:ids+.`),s.push({type:1,value:a,regexp:u,repeatable:"*"===l||"+"===l,optional:"*"===l||"?"===l})):t("Invalid state to consume buffer"),a="")}function d(){a+=l}while(c{i(h)}:u}function i(e){if(ue(e)){const t=r.get(e);t&&(r.delete(e),n.splice(n.indexOf(t),1),t.children.forEach(i),t.alias.forEach(i))}else{const t=n.indexOf(e);t>-1&&(n.splice(t,1),e.record.name&&r.delete(e.record.name),e.children.forEach(i),e.alias.forEach(i))}}function l(){return n}function a(e){const t=Te(e,n);n.splice(t,0,e),e.record.name&&!je(e)&&r.set(e.record.name,e)}function f(e,t){let o,s,i,l={};if("name"in e&&e.name){if(o=r.get(e.name),!o)throw pe(1,{location:e});0,i=o.record.name,l=c(Oe(t.params,o.keys.filter((e=>!e.optional)).concat(o.parent?o.parent.keys.filter((e=>e.optional)):[]).map((e=>e.name))),e.params&&Oe(e.params,o.keys.map((e=>e.name)))),s=o.stringify(l)}else if(null!=e.path)s=e.path,o=n.find((e=>e.re.test(s))),o&&(l=o.parse(s),i=o.record.name);else{if(o=t.name?r.get(t.name):n.find((e=>e.re.test(t.path))),!o)throw pe(1,{location:e,currentLocation:t});i=o.record.name,l=c({},t.params,e.params),s=o.stringify(l)}const a=[];let u=o;while(u)a.unshift(u.record),u=u.parent;return{name:i,path:s,params:l,matched:a,meta:Ae(a)}}function d(){n.length=0,r.clear()}return t=Ie({strict:!1,end:!0,sensitive:!1},t),e.forEach((e=>s(e))),{addRoute:s,resolve:f,removeRoute:i,clearRoutes:d,getRoutes:l,getRecordMatcher:o}}function Oe(e,t){const n={};for(const r of t)r in e&&(n[r]=e[r]);return n}function Re(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:Me(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function Me(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const r in e.components)t[r]="object"===typeof n?n[r]:n;return t}function je(e){while(e){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Ae(e){return e.reduce(((e,t)=>c(e,t.meta)),{})}function Ie(e,t){const n={};for(const r in e)n[r]=r in t?t[r]:e[r];return n}function Te(e,t){let n=0,r=t.length;while(n!==r){const o=n+r>>1,s=ke(e,t[o]);s<0?r=o:n=o+1}const o=Pe(e);return o&&(r=t.lastIndexOf(o,r-1)),r}function Pe(e){let t=e;while(t=t.parent)if(Fe(t)&&0===ke(e,t))return t}function Fe({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function De(e){const t={};if(""===e||"?"===e)return t;const n="?"===e[0],r=(n?e.slice(1):e).split("&");for(let o=0;oe&&R(e))):[r&&R(r)];o.forEach((e=>{void 0!==e&&(t+=(t.length?"&":"")+n,null!=e&&(t+="="+e))}))}return t}function $e(e){const t={};for(const n in e){const r=e[n];void 0!==r&&(t[n]=f(r)?r.map((e=>null==e?null:""+e)):null==r?r:""+r)}return t}const ze=Symbol(""),Le=Symbol(""),Ue=Symbol(""),Ze=Symbol(""),Je=Symbol("");function Ve(){let e=[];function t(t){return e.push(t),()=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function He(e,t,n,r,o,s=(e=>e())){const i=r&&(r.enterCallbacks[o]=r.enterCallbacks[o]||[]);return()=>new Promise(((l,c)=>{const a=e=>{!1===e?c(pe(4,{from:n,to:t})):e instanceof Error?c(e):ae(e)?c(pe(2,{from:t,to:e})):(i&&r.enterCallbacks[o]===i&&"function"===typeof e&&i.push(e),l())},u=s((()=>e.call(r&&r.instances[o],t,n,a)));let f=Promise.resolve(u);e.length<3&&(f=f.then(a)),f.catch((e=>c(e)))}))}function Be(e,t,n,r,o=(e=>e())){const s=[];for(const c of e){0;for(const e in c.components){let a=c.components[e];if("beforeRouteEnter"===t||c.instances[e])if(i(a)){const i=a.__vccOpts||a,l=i[t];l&&s.push(He(l,n,r,c,e,o))}else{let i=a();0,s.push((()=>i.then((s=>{if(!s)throw new Error(`Couldn't resolve component "${e}" at "${c.path}"`);const i=l(s)?s.default:s;c.mods[e]=s,c.components[e]=i;const a=i.__vccOpts||i,u=a[t];return u&&He(u,n,r,c,e,o)()}))))}}}return s}function Ge(e){const t=(0,r.f3)(Ue),n=(0,r.f3)(Ze);const s=(0,r.Fl)((()=>{const n=(0,o.SU)(e.to);return t.resolve(n)})),i=(0,r.Fl)((()=>{const{matched:e}=s.value,{length:t}=e,r=e[t-1],o=n.matched;if(!r||!o.length)return-1;const i=o.findIndex(z.bind(null,r));if(i>-1)return i;const l=Qe(e[t-2]);return t>1&&Qe(r)===l&&o[o.length-1].path!==l?o.findIndex(z.bind(null,e[t-2])):i})),l=(0,r.Fl)((()=>i.value>-1&&Xe(n.params,s.value.params))),c=(0,r.Fl)((()=>i.value>-1&&i.value===n.matched.length-1&&L(n.params,s.value.params)));function a(n={}){if(Ye(n)){const n=t[(0,o.SU)(e.replace)?"replace":"push"]((0,o.SU)(e.to)).catch(u);return e.viewTransition&&"undefined"!==typeof document&&"startViewTransition"in document&&document.startViewTransition((()=>n)),n}return Promise.resolve()}return{route:s,href:(0,r.Fl)((()=>s.value.href)),isActive:l,isExactActive:c,navigate:a}}function qe(e){return 1===e.length?e[0]:e}const We=(0,r.aZ)({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"},viewTransition:Boolean},useLink:Ge,setup(e,{slots:t}){const n=(0,o.qj)(Ge(e)),{options:s}=(0,r.f3)(Ue),i=(0,r.Fl)((()=>({[et(e.activeClass,s.linkActiveClass,"router-link-active")]:n.isActive,[et(e.exactActiveClass,s.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive})));return()=>{const o=t.default&&qe(t.default(n));return e.custom?o:(0,r.h)("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:i.value},o)}}}),Ke=We;function Ye(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&(void 0===e.button||0===e.button)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function Xe(e,t){for(const n in t){const r=t[n],o=e[n];if("string"===typeof r){if(r!==o)return!1}else if(!f(o)||o.length!==r.length||r.some(((e,t)=>e!==o[t])))return!1}return!0}function Qe(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const et=(e,t,n)=>null!=e?e:null!=t?t:n,tt=(0,r.aZ)({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const s=(0,r.f3)(Je),i=(0,r.Fl)((()=>e.route||s.value)),l=(0,r.f3)(Le,0),a=(0,r.Fl)((()=>{let e=(0,o.SU)(l);const{matched:t}=i.value;let n;while((n=t[e])&&!n.components)e++;return e})),u=(0,r.Fl)((()=>i.value.matched[a.value]));(0,r.JJ)(Le,(0,r.Fl)((()=>a.value+1))),(0,r.JJ)(ze,u),(0,r.JJ)(Je,i);const f=(0,o.iH)();return(0,r.YP)((()=>[f.value,u.value,e.name]),(([e,t,n],[r,o,s])=>{t&&(t.instances[n]=e,o&&o!==t&&e&&e===r&&(t.leaveGuards.size||(t.leaveGuards=o.leaveGuards),t.updateGuards.size||(t.updateGuards=o.updateGuards))),!e||!t||o&&z(t,o)&&r||(t.enterCallbacks[n]||[]).forEach((t=>t(e)))}),{flush:"post"}),()=>{const o=i.value,s=e.name,l=u.value,a=l&&l.components[s];if(!a)return nt(n.default,{Component:a,route:o});const d=l.props[s],p=d?!0===d?o.params:"function"===typeof d?d(o):d:null,h=e=>{e.component.isUnmounted&&(l.instances[s]=null)},m=(0,r.h)(a,c({},p,t,{onVnodeUnmounted:h,ref:f}));return nt(n.default,{Component:m,route:o})||m}}});function nt(e,t){if(!e)return null;const n=e(t);return 1===n.length?n[0]:n}const rt=tt;function ot(e){const t=Ee(e.routes,e),n=e.parseQuery||De,i=e.stringifyQuery||Ne,l=e.history;const d=Ve(),p=Ve(),h=Ve(),m=(0,o.XI)(V);let g=V;s&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const v=a.bind(null,(e=>""+e)),y=a.bind(null,A),b=a.bind(null,I);function k(e,n){let r,o;return ue(e)?(r=t.getRecordMatcher(e),o=n):o=e,t.addRoute(o,r)}function w(e){const n=t.getRecordMatcher(e);n&&t.removeRoute(n)}function x(){return t.getRoutes().map((e=>e.record))}function _(e){return!!t.getRecordMatcher(e)}function S(e,r){if(r=c({},r||m.value),"string"===typeof e){const o=F(n,e,r.path),s=t.resolve({path:o.path},r),i=l.createHref(o.fullPath);return c(o,s,{params:b(s.params),hash:I(o.hash),redirectedFrom:void 0,href:i})}let o;if(null!=e.path)o=c({},e,{path:F(n,e.path,r.path).path});else{const t=c({},e.params);for(const e in t)null==t[e]&&delete t[e];o=c({},e,{params:y(t)}),r.params=y(r.params)}const s=t.resolve(o,r),a=e.hash||"";s.params=v(b(s.params));const u=D(i,c({},e,{hash:O(a),path:s.path})),f=l.createHref(u);return c({fullPath:u,hash:a,query:i===Ne?$e(e.query):e.query||{}},s,{redirectedFrom:void 0,href:f})}function C(e){return"string"===typeof e?F(n,e,m.value.path):c({},e)}function E(e,t){if(g!==e)return pe(8,{from:t,to:e})}function R(e){return T(e)}function M(e){return R(c(C(e),{replace:!0}))}function j(e){const t=e.matched[e.matched.length-1];if(t&&t.redirect){const{redirect:n}=t;let r="function"===typeof n?n(e):n;return"string"===typeof r&&(r=r.includes("?")||r.includes("#")?r=C(r):{path:r},r.params={}),c({query:e.query,hash:e.hash,params:null!=r.path?{}:e.params},r)}}function T(e,t){const n=g=S(e),r=m.value,o=e.state,s=e.force,l=!0===e.replace,a=j(n);if(a)return T(c(C(a),{state:"object"===typeof a?c({},o,a.state):o,force:s,replace:l}),t||n);const u=n;let f;return u.redirectedFrom=t,!s&&$(i,r,n)&&(f=pe(16,{to:u,from:r}),re(r,r,!0,!1)),(f?Promise.resolve(f):z(u,r)).catch((e=>he(e)?he(e,2)?e:ee(e):W(e,u,r))).then((e=>{if(e){if(he(e,2))return T(c({replace:l},C(e.to),{state:"object"===typeof e.to?c({},o,e.to.state):o,force:s}),t||u)}else e=U(u,r,!0,l,o);return L(u,r,e),e}))}function P(e,t){const n=E(e,t);return n?Promise.reject(n):Promise.resolve()}function N(e){const t=ie.values().next().value;return t&&"function"===typeof t.runWithContext?t.runWithContext(e):e()}function z(e,t){let n;const[r,o,s]=st(e,t);n=Be(r.reverse(),"beforeRouteLeave",e,t);for(const l of r)l.leaveGuards.forEach((r=>{n.push(He(r,e,t))}));const i=P.bind(null,e,t);return n.push(i),ce(n).then((()=>{n=[];for(const r of d.list())n.push(He(r,e,t));return n.push(i),ce(n)})).then((()=>{n=Be(o,"beforeRouteUpdate",e,t);for(const r of o)r.updateGuards.forEach((r=>{n.push(He(r,e,t))}));return n.push(i),ce(n)})).then((()=>{n=[];for(const r of s)if(r.beforeEnter)if(f(r.beforeEnter))for(const o of r.beforeEnter)n.push(He(o,e,t));else n.push(He(r.beforeEnter,e,t));return n.push(i),ce(n)})).then((()=>(e.matched.forEach((e=>e.enterCallbacks={})),n=Be(s,"beforeRouteEnter",e,t,N),n.push(i),ce(n)))).then((()=>{n=[];for(const r of p.list())n.push(He(r,e,t));return n.push(i),ce(n)})).catch((e=>he(e,8)?e:Promise.reject(e)))}function L(e,t,n){h.list().forEach((r=>N((()=>r(e,t,n)))))}function U(e,t,n,r,o){const i=E(e,t);if(i)return i;const a=t===V,u=s?history.state:{};n&&(r||a?l.replace(e.fullPath,c({scroll:a&&u&&u.scroll},o)):l.push(e.fullPath,o)),m.value=e,re(e,t,n,a),ee()}let Z;function J(){Z||(Z=l.listen(((e,t,n)=>{if(!le.listening)return;const r=S(e),o=j(r);if(o)return void T(c(o,{replace:!0,force:!0}),r).catch(u);g=r;const i=m.value;s&&te(Q(i.fullPath,n.delta),Y()),z(r,i).catch((e=>he(e,12)?e:he(e,2)?(T(c(C(e.to),{force:!0}),r).then((e=>{he(e,20)&&!n.delta&&n.type===H.pop&&l.go(-1,!1)})).catch(u),Promise.reject()):(n.delta&&l.go(-n.delta,!1),W(e,r,i)))).then((e=>{e=e||U(r,i,!1),e&&(n.delta&&!he(e,8)?l.go(-n.delta,!1):n.type===H.pop&&he(e,20)&&l.go(-1,!1)),L(r,i,e)})).catch(u)})))}let B,G=Ve(),q=Ve();function W(e,t,n){ee(e);const r=q.list();return r.length?r.forEach((r=>r(e,t,n))):console.error(e),Promise.reject(e)}function K(){return B&&m.value!==V?Promise.resolve():new Promise(((e,t)=>{G.add([e,t])}))}function ee(e){return B||(B=!e,J(),G.list().forEach((([t,n])=>e?n(e):t())),G.reset()),e}function re(t,n,o,i){const{scrollBehavior:l}=e;if(!s||!l)return Promise.resolve();const c=!o&&ne(Q(t.fullPath,0))||(i||!o)&&history.state&&history.state.scroll||null;return(0,r.Y3)().then((()=>l(t,n,c))).then((e=>e&&X(e))).catch((e=>W(e,t,n)))}const oe=e=>l.go(e);let se;const ie=new Set,le={currentRoute:m,listening:!0,addRoute:k,removeRoute:w,clearRoutes:t.clearRoutes,hasRoute:_,getRoutes:x,resolve:S,options:e,push:R,replace:M,go:oe,back:()=>oe(-1),forward:()=>oe(1),beforeEach:d.add,beforeResolve:p.add,afterEach:h.add,onError:q.add,isReady:K,install(e){const t=this;e.component("RouterLink",Ke),e.component("RouterView",rt),e.config.globalProperties.$router=t,Object.defineProperty(e.config.globalProperties,"$route",{enumerable:!0,get:()=>(0,o.SU)(m)}),s&&!se&&m.value===V&&(se=!0,R(l.location).catch((e=>{0})));const n={};for(const o in V)Object.defineProperty(n,o,{get:()=>m.value[o],enumerable:!0});e.provide(Ue,t),e.provide(Ze,(0,o.Um)(n)),e.provide(Je,m);const r=e.unmount;ie.add(e),e.unmount=function(){ie.delete(e),ie.size<1&&(g=V,Z&&Z(),Z=null,m.value=V,se=!1,B=!1),r()}}};function ce(e){return e.reduce(((e,t)=>e.then((()=>N(t)))),Promise.resolve())}return le}function st(e,t){const n=[],r=[],o=[],s=Math.max(t.matched.length,e.matched.length);for(let i=0;iz(e,s)))?r.push(s):n.push(s));const l=e.matched[i];l&&(t.matched.find((e=>z(e,l)))||o.push(l))}return[n,r,o]}function it(){return(0,r.f3)(Ue)}function lt(e){return(0,r.f3)(Ze)}}}]); \ No newline at end of file