Compare commits

...
20 Commits
Author SHA1 Message Date
TwiN 91931e48b4 fix(ui): Clear selected result before toggling new one
relevant: #1236
2025-10-17 22:06:54 -04:00
TwiN 386a4d2cb7 fix(ui): Implement toggleable tooltip for suites too
relevant: #1236
2025-10-17 21:41:40 -04:00
TwiN 4d9eb0572c docs(alerting): Link n8n-nodes-gatus-trigger for n8n alerting provider 2025-10-17 21:12:53 -04:00
1586b3cc0b feat(alerting): Add message-content parameter for Discord pings (#1335)
* feat(discord-alerts): add option for prefix-messages outside of embeds

* chore(docs): add discord prefix-message to README

* chore(discord-alerts): rename prefix-message to message-content

---------

Co-authored-by: TwiN <twin@linux.com>
2025-10-17 20:22:26 -04:00
981e082d0c feat(ui): Make tooltips toggleable (#1236)
* feat(results): allow for data points in checks to be "clicked"

asdf

* feat(ui): resolve merge conflicts

feat(dev): put back package.lock

* fix(ui): make sure the datapoint stays "fixed"

* fix(ui): watch for url changes to make tooltip go away

* feat(ui): add compiled app.css and app.js

* fix(ui): lengthen the tooltipElement name

---------

Co-authored-by: TwiN <twin@linux.com>
2025-10-17 16:09:47 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
91daaf92aa chore(deps): bump github.com/gofiber/fiber/v2 from 2.52.8 to 2.52.9 (#1338)
Bumps [github.com/gofiber/fiber/v2](https://github.com/gofiber/fiber) from 2.52.8 to 2.52.9.
- [Release notes](https://github.com/gofiber/fiber/releases)
- [Commits](https://github.com/gofiber/fiber/compare/v2.52.8...v2.52.9)

---
updated-dependencies:
- dependency-name: github.com/gofiber/fiber/v2
  dependency-version: 2.52.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-17 10:09:03 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a1bb07c556 chore(deps): bump golang.org/x/net from 0.45.0 to 0.46.0 (#1333)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.45.0 to 0.46.0.
- [Commits](https://github.com/golang/net/compare/v0.45.0...v0.46.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-17 10:08:48 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
258175dec3 chore(deps): bump code.gitea.io/sdk/gitea from 0.21.0 to 0.22.0 (#1341)
Bumps code.gitea.io/sdk/gitea from 0.21.0 to 0.22.0.

---
updated-dependencies:
- dependency-name: code.gitea.io/sdk/gitea
  dependency-version: 0.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-17 10:08:31 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>TwiN
ef6159e420 chore(deps): bump github.com/prometheus/client_golang from 1.23.0 to 1.23.2 (#1337)
chore(deps): bump github.com/prometheus/client_golang

Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.23.0 to 1.23.2.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.23.0...v1.23.2)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.23.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: TwiN <twin@linux.com>
2025-10-16 20:45:36 -04:00
TwiNandGitHub ebd4068aac fix(key): Support (, ), + and & as name/group (#1340)
fix(key): Support (, ), + and & as name/group

Relevant: #1339
2025-10-16 16:47:11 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
39981de54b chore(deps): bump golang.org/x/crypto from 0.42.0 to 0.43.0 (#1332)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.42.0 to 0.43.0.
- [Commits](https://github.com/golang/crypto/compare/v0.42.0...v0.43.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 08:49:52 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7dce07e47f chore(deps): bump modernc.org/sqlite from 1.38.2 to 1.39.1 (#1331)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.38.2 to 1.39.1.
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.38.2...v1.39.1)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.39.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 08:23:57 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6a83857db4 chore(deps): bump github.com/coreos/go-oidc/v3 from 3.14.1 to 3.16.0 (#1313)
Bumps [github.com/coreos/go-oidc/v3](https://github.com/coreos/go-oidc) from 3.14.1 to 3.16.0.
- [Release notes](https://github.com/coreos/go-oidc/releases)
- [Commits](https://github.com/coreos/go-oidc/compare/v3.14.1...v3.16.0)

---
updated-dependencies:
- dependency-name: github.com/coreos/go-oidc/v3
  dependency-version: 3.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-14 16:42:57 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
50702bd1d4 chore(deps): bump google.golang.org/api from 0.242.0 to 0.252.0 (#1315)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.242.0 to 0.252.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.242.0...v0.252.0)

---
updated-dependencies:
- dependency-name: google.golang.org/api
  dependency-version: 0.252.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-14 10:19:44 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5bf95fe4f7 chore(deps): bump github.com/valyala/fasthttp from 1.64.0 to 1.67.0 (#1330)
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.64.0 to 1.67.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.64.0...v1.67.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-version: 1.67.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-14 09:56:20 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
20d8ef966b chore(deps): bump golang.org/x/oauth2 from 0.30.0 to 0.32.0 (#1317)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.30.0 to 0.32.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.30.0...v0.32.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 22:11:52 -04:00
michael-barabooandGitHub 8f15346fb7 fix(alerting)!: remove discontinued jetbrains space alerting provider (#1329)
remove alerting provider for discontinued jetbrains space
2025-10-13 20:47:05 -04:00
TwiNandGitHub 8920bdd301 fix(ui): Handle refresh properly on SuiteDetails.vue (#1324) 2025-10-12 14:27:43 -04:00
TwiN e37024dfc6 docs: Use working websocket example 2025-10-09 13:28:34 -04:00
TwiN ac4374b1e3 docs: Replace Docker Hub with GHCR as primary container registry 2025-10-06 14:15:14 -04:00
26 changed files with 515 additions and 888 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 89.76 89.75"><defs><style>.cls-1{fill:#3cad4b;}.cls-2{fill:#017400;}.cls-3{fill:#1e9025;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M33.67,65.35a23.35,23.35,0,0,1,.08-41,22.94,22.94,0,0,1,3.8-1.64A23,23,0,0,0,53.6,1C53,0,51,0,44.89,0c-9.08,0-9.21.17-8.81,3.22,1.07,8.12-9.42,12.5-14.45,6-1.94-2.52-2.1-2.52-8.68,4.16-6.22,6.3-6.33,6.28-3.77,8.25a8.09,8.09,0,0,1,2.56,9.53A8.15,8.15,0,0,1,3.08,36C0,35.63,0,35.73,0,45.2.08,53.81,0,54,3.3,53.63A8.06,8.06,0,0,1,9.76,67.52c-3,2.83-2.84,2.61,2.84,8.48,5.43,5.62,6.33,6.73,8.16,5.24L34,68A1.63,1.63,0,0,0,33.67,65.35Z"/><path class="cls-2" d="M85.43,36.13a8.11,8.11,0,0,1-5.27-14.21c2.85-2.5,2.82-2.37-3.55-8.75-4.31-4.31-5.71-5.75-6.87-5.4l-14,14a1.65,1.65,0,0,0,.36,2.61,23.35,23.35,0,0,1-.1,41,24.5,24.5,0,0,1-5.11,2c-8.54,2.28-14.73,9.63-14.73,18.47v1.27c.15,2.54,1.19,2.42,8.06,2.52,9.32.14,9.1.35,9.38-4.66a8.11,8.11,0,0,1,14-5.09c3,3.15,2.39,3.11,8.73-3.14,6.56-6.47,6.86-6.25,3.68-9.14a8.1,8.1,0,0,1,6.06-14.07c3.68.27,3.51.06,3.63-8.09C89.85,36.27,90,36.16,85.43,36.13Z"/><path class="cls-3" d="M41.11,59h8a.76.76,0,0,0,.77-.76V50.43a.76.76,0,0,1,.77-.76h7.84a.78.78,0,0,0,.77-.77V40.84a.77.77,0,0,0-.77-.76H50.7a.76.76,0,0,1-.77-.77V31.47a.76.76,0,0,0-.77-.77h-8a.76.76,0,0,0-.77.77v7.84a.76.76,0,0,1-.77.77H31.73a.77.77,0,0,0-.77.76V48.9a.78.78,0,0,0,.77.77h7.84a.76.76,0,0,1,.77.76v7.85A.76.76,0,0,0,41.11,59Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+28 -65
View File
@@ -20,13 +20,14 @@ _Looking for a managed solution? Check out [Gatus.io](https://gatus.io)._
<details> <details>
<summary><b>Quick start</b></summary> <summary><b>Quick start</b></summary>
```console
docker run -p 8080:8080 --name gatus twinproduction/gatus:stable
```
You can also use GitHub Container Registry if you prefer:
```console ```console
docker run -p 8080:8080 --name gatus ghcr.io/twin/gatus:stable docker run -p 8080:8080 --name gatus ghcr.io/twin/gatus:stable
``` ```
You can also use Docker Hub if you prefer:
```console
docker run -p 8080:8080 --name gatus twinproduction/gatus:stable
```
For more details, see [Usage](#usage) For more details, see [Usage](#usage)
</details> </details>
@@ -66,7 +67,6 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Configuring IFTTT alerts](#configuring-ifttt-alerts) - [Configuring IFTTT alerts](#configuring-ifttt-alerts)
- [Configuring Ilert alerts](#configuring-ilert-alerts) - [Configuring Ilert alerts](#configuring-ilert-alerts)
- [Configuring Incident.io alerts](#configuring-incidentio-alerts) - [Configuring Incident.io alerts](#configuring-incidentio-alerts)
- [Configuring JetBrains Space alerts](#configuring-jetbrains-space-alerts)
- [Configuring Line alerts](#configuring-line-alerts) - [Configuring Line alerts](#configuring-line-alerts)
- [Configuring Matrix alerts](#configuring-matrix-alerts) - [Configuring Matrix alerts](#configuring-matrix-alerts)
- [Configuring Mattermost alerts](#configuring-mattermost-alerts) - [Configuring Mattermost alerts](#configuring-mattermost-alerts)
@@ -186,18 +186,15 @@ The main features of Gatus are:
## Usage ## Usage
<details>
<summary><b>Quick start</b></summary>
```console ```console
docker run -p 8080:8080 --name gatus twinproduction/gatus docker run -p 8080:8080 --name gatus ghcr.io/twin/gatus:stable
``` ```
You can also use GitHub Container Registry if you prefer:
You can also use Docker Hub if you prefer:
```console ```console
docker run -p 8080:8080 --name gatus ghcr.io/twin/gatus docker run -p 8080:8080 --name gatus twinproduction/gatus:stable
``` ```
If you want to create your own configuration, see [Docker](#docker) for information on how to mount a configuration file. If you want to create your own configuration, see [Docker](#docker) for information on how to mount a configuration file.
</details>
Here's a simple example: Here's a simple example:
```yaml ```yaml
@@ -369,7 +366,7 @@ or send an HTTP request:
POST /api/v1/endpoints/{key}/external?success={success}&error={error}&duration={duration} POST /api/v1/endpoints/{key}/external?success={success}&error={error}&duration={duration}
``` ```
Where: Where:
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`. - `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
- Using the example configuration above, the key would be `core_ext-ep-test`. - Using the example configuration above, the key would be `core_ext-ep-test`.
- `{success}` is a boolean (`true` or `false`) value indicating whether the health check was successful or not. - `{success}` is a boolean (`true` or `false`) value indicating whether the health check was successful or not.
- `{error}` (optional): a string describing the reason for a failed health check. If {success} is false, this should contain the error message; if the check is successful. - `{error}` (optional): a string describing the reason for a failed health check. If {success} is false, this should contain the error message; if the check is successful.
@@ -813,7 +810,6 @@ endpoints:
| `alerting.ifttt` | Configuration for alerts of type `ifttt`. <br />See [Configuring IFTTT alerts](#configuring-ifttt-alerts). | `{}` | | `alerting.ifttt` | Configuration for alerts of type `ifttt`. <br />See [Configuring IFTTT alerts](#configuring-ifttt-alerts). | `{}` |
| `alerting.ilert` | Configuration for alerts of type `ilert`. <br />See [Configuring ilert alerts](#configuring-ilert-alerts). | `{}` | | `alerting.ilert` | Configuration for alerts of type `ilert`. <br />See [Configuring ilert alerts](#configuring-ilert-alerts). | `{}` |
| `alerting.incident-io` | Configuration for alerts of type `incident-io`. <br />See [Configuring Incident.io alerts](#configuring-incidentio-alerts). | `{}` | | `alerting.incident-io` | Configuration for alerts of type `incident-io`. <br />See [Configuring Incident.io alerts](#configuring-incidentio-alerts). | `{}` |
| `alerting.jetbrainsspace` | Configuration for alerts of type `jetbrainsspace`. <br />See [Configuring JetBrains Space alerts](#configuring-jetbrains-space-alerts). | `{}` |
| `alerting.line` | Configuration for alerts of type `line`. <br />See [Configuring Line alerts](#configuring-line-alerts). | `{}` | | `alerting.line` | Configuration for alerts of type `line`. <br />See [Configuring Line alerts](#configuring-line-alerts). | `{}` |
| `alerting.matrix` | Configuration for alerts of type `matrix`. <br />See [Configuring Matrix alerts](#configuring-matrix-alerts). | `{}` | | `alerting.matrix` | Configuration for alerts of type `matrix`. <br />See [Configuring Matrix alerts](#configuring-matrix-alerts). | `{}` |
| `alerting.mattermost` | Configuration for alerts of type `mattermost`. <br />See [Configuring Mattermost alerts](#configuring-mattermost-alerts). | `{}` | | `alerting.mattermost` | Configuration for alerts of type `mattermost`. <br />See [Configuring Mattermost alerts](#configuring-mattermost-alerts). | `{}` |
@@ -924,6 +920,7 @@ endpoints:
| `alerting.discord` | Configuration for alerts of type `discord` | `{}` | | `alerting.discord` | Configuration for alerts of type `discord` | `{}` |
| `alerting.discord.webhook-url` | Discord Webhook URL | Required `""` | | `alerting.discord.webhook-url` | Discord Webhook URL | Required `""` |
| `alerting.discord.title` | Title of the notification | `":helmet_with_white_cross: Gatus"` | | `alerting.discord.title` | Title of the notification | `":helmet_with_white_cross: Gatus"` |
| `alerting.discord.message-content` | Message content to send before the embed (useful for pinging users/roles, e.g. `<@123>`) | `""` |
| `alerting.discord.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A | | `alerting.discord.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.discord.overrides` | List of overrides that may be prioritized over the default configuration | `[]` | | `alerting.discord.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
| `alerting.discord.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` | | `alerting.discord.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
@@ -1385,42 +1382,6 @@ In order to get the required alert source config id and authentication token, yo
> **_NOTE:_** the source config id is of the form `https://api.incident.io/v2/alert_events/http/$ID` and the token is expected to be passed as a bearer token like so: `Authorization: Bearer $TOKEN` > **_NOTE:_** the source config id is of the form `https://api.incident.io/v2/alert_events/http/$ID` and the token is expected to be passed as a bearer token like so: `Authorization: Bearer $TOKEN`
#### Configuring JetBrains Space alerts
| Parameter | Description | Default |
|:--------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
| `alerting.jetbrainsspace` | Configuration for alerts of type `jetbrainsspace` | `{}` |
| `alerting.jetbrainsspace.project` | JetBrains Space project name | Required `""` |
| `alerting.jetbrainsspace.channel-id` | JetBrains Space Chat Channel ID | Required `""` |
| `alerting.jetbrainsspace.token` | Token that is used for authentication. | Required `""` |
| `alerting.jetbrainsspace.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.jetbrainsspace.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
| `alerting.jetbrainsspace.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
| `alerting.jetbrainsspace.overrides[].*` | See `alerting.jetbrainsspace.*` parameters | `{}` |
```yaml
alerting:
jetbrainsspace:
project: myproject
channel-id: ABCDE12345
token: "**************"
endpoints:
- name: website
url: "https://twin.sh/health"
interval: 5m
conditions:
- "[STATUS] == 200"
alerts:
- type: jetbrainsspace
description: "healthcheck failed"
send-on-resolved: true
```
Here's an example of what the notifications look like:
![JetBrains Space notifications](.github/assets/jetbrains-space-alerts.png)
#### Configuring Line alerts #### Configuring Line alerts
| Parameter | Description | Default | | Parameter | Description | Default |
@@ -1605,6 +1566,8 @@ endpoints:
[n8n](https://n8n.io/) is a workflow automation platform that allows you to automate tasks across different applications and services using webhooks. [n8n](https://n8n.io/) is a workflow automation platform that allows you to automate tasks across different applications and services using webhooks.
See [n8n-nodes-gatus-trigger](https://github.com/TwiN/n8n-nodes-gatus-trigger) for a n8n community node that can be used as trigger.
Example: Example:
```yaml ```yaml
alerting: alerting:
@@ -2789,24 +2752,24 @@ Many examples can be found in the [.examples](.examples) folder, but this sectio
### Docker ### Docker
To run Gatus locally with Docker: To run Gatus locally with Docker:
```console ```console
docker run -p 8080:8080 --name gatus twinproduction/gatus docker run -p 8080:8080 --name gatus ghcr.io/twin/gatus:stable
``` ```
Other than using one of the examples provided in the [.examples](.examples) folder, you can also try it out locally by Other than using one of the examples provided in the [.examples](.examples) folder, you can also try it out locally by
creating a configuration file, we'll call it `config.yaml` for this example, and running the following creating a configuration file, we'll call it `config.yaml` for this example, and running the following
command: command:
```console ```console
docker run -p 8080:8080 --mount type=bind,source="$(pwd)"/config.yaml,target=/config/config.yaml --name gatus twinproduction/gatus docker run -p 8080:8080 --mount type=bind,source="$(pwd)"/config.yaml,target=/config/config.yaml --name gatus ghcr.io/twin/gatus:stable
``` ```
If you're on Windows, replace `"$(pwd)"` by the absolute path to your current directory, e.g.: If you're on Windows, replace `"$(pwd)"` by the absolute path to your current directory, e.g.:
```console ```console
docker run -p 8080:8080 --mount type=bind,source=C:/Users/Chris/Desktop/config.yaml,target=/config/config.yaml --name gatus twinproduction/gatus docker run -p 8080:8080 --mount type=bind,source=C:/Users/Chris/Desktop/config.yaml,target=/config/config.yaml --name gatus ghcr.io/twin/gatus:stable
``` ```
To build the image locally: To build the image locally:
```console ```console
docker build . -t twinproduction/gatus docker build . -t ghcr.io/twin/gatus:stable
``` ```
@@ -2967,20 +2930,20 @@ This works for SCTP based application.
### Monitoring a WebSocket endpoint ### Monitoring a WebSocket endpoint
By prefixing `endpoints[].url` with `ws://` or `wss://`, you can monitor WebSocket endpoints at a very basic level: By prefixing `endpoints[].url` with `ws://` or `wss://`, you can monitor WebSocket endpoints:
```yaml ```yaml
endpoints: endpoints:
- name: example - name: example
url: "wss://example.com/" url: "wss://echo.websocket.org/"
body: "status" body: "status"
conditions: conditions:
- "[CONNECTED] == true" - "[CONNECTED] == true"
- "[BODY].result >= 0" - "[BODY] == pat(*served by*)"
``` ```
The `[BODY]` placeholder contains the output of the query, and `[CONNECTED]` The `[BODY]` placeholder contains the output of the query, and `[CONNECTED]`
shows whether the connection was successfully established. You can use Go template shows whether the connection was successfully established. You can use Go template
syntax. The functions LocalAddr and RandomString with a length can be used. syntax.
### Monitoring an endpoint using ICMP ### Monitoring an endpoint using ICMP
@@ -3334,7 +3297,7 @@ The path to generate a badge is the following:
``` ```
Where: Where:
- `{duration}` is `30d`, `7d`, `24h` or `1h` - `{duration}` is `30d`, `7d`, `24h` or `1h`
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`. - `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
For instance, if you want the uptime during the last 24 hours from the endpoint `frontend` in the group `core`, For instance, if you want the uptime during the last 24 hours from the endpoint `frontend` in the group `core`,
the URL would look like this: the URL would look like this:
@@ -3360,7 +3323,7 @@ The path to generate a badge is the following:
/api/v1/endpoints/{key}/health/badge.svg /api/v1/endpoints/{key}/health/badge.svg
``` ```
Where: Where:
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`. - `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
For instance, if you want the current status of the endpoint `frontend` in the group `core`, For instance, if you want the current status of the endpoint `frontend` in the group `core`,
the URL would look like this: the URL would look like this:
@@ -3377,7 +3340,7 @@ The path to generate a badge is the following:
/api/v1/endpoints/{key}/health/badge.shields /api/v1/endpoints/{key}/health/badge.shields
``` ```
Where: Where:
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`. - `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
For instance, if you want the current status of the endpoint `frontend` in the group `core`, For instance, if you want the current status of the endpoint `frontend` in the group `core`,
the URL would look like this: the URL would look like this:
@@ -3400,7 +3363,7 @@ The endpoint to generate a badge is the following:
``` ```
Where: Where:
- `{duration}` is `30d`, `7d`, `24h` or `1h` - `{duration}` is `30d`, `7d`, `24h` or `1h`
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`. - `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
#### Response time (chart) #### Response time (chart)
![Response time 24h](https://status.twin.sh/api/v1/endpoints/core_blog-external/response-times/24h/chart.svg) ![Response time 24h](https://status.twin.sh/api/v1/endpoints/core_blog-external/response-times/24h/chart.svg)
@@ -3413,7 +3376,7 @@ The endpoint to generate a response time chart is the following:
``` ```
Where: Where:
- `{duration}` is `30d`, `7d`, or `24h` - `{duration}` is `30d`, `7d`, or `24h`
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`. - `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
##### How to change the color thresholds of the response time badge ##### How to change the color thresholds of the response time badge
To change the response time badges' threshold, a corresponding configuration can be added to an endpoint. To change the response time badges' threshold, a corresponding configuration can be added to an endpoint.
@@ -3471,7 +3434,7 @@ The path to get raw uptime data for an endpoint is:
``` ```
Where: Where:
- `{duration}` is `30d`, `7d`, `24h` or `1h` - `{duration}` is `30d`, `7d`, `24h` or `1h`
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`. - `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
For instance, if you want the raw uptime data for the last 24 hours from the endpoint `frontend` in the group `core`, the URL would look like this: For instance, if you want the raw uptime data for the last 24 hours from the endpoint `frontend` in the group `core`, the URL would look like this:
``` ```
@@ -3485,7 +3448,7 @@ The path to get raw response time data for an endpoint is:
``` ```
Where: Where:
- `{duration}` is `30d`, `7d`, `24h` or `1h` - `{duration}` is `30d`, `7d`, `24h` or `1h`
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`. - `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
For instance, if you want the raw response time data for the last 24 hours from the endpoint `frontend` in the group `core`, the URL would look like this: For instance, if you want the raw response time data for the last 24 hours from the endpoint `frontend` in the group `core`, the URL would look like this:
``` ```
-3
View File
@@ -47,9 +47,6 @@ const (
// TypeIncidentIO is the Type for the incident-io alerting provider // TypeIncidentIO is the Type for the incident-io alerting provider
TypeIncidentIO Type = "incident-io" TypeIncidentIO Type = "incident-io"
// TypeJetBrainsSpace is the Type for the jetbrains alerting provider
TypeJetBrainsSpace Type = "jetbrainsspace"
// TypeLine is the Type for the line alerting provider // TypeLine is the Type for the line alerting provider
TypeLine Type = "line" TypeLine Type = "line"
-4
View File
@@ -20,7 +20,6 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/ifttt" "github.com/TwiN/gatus/v5/alerting/provider/ifttt"
"github.com/TwiN/gatus/v5/alerting/provider/ilert" "github.com/TwiN/gatus/v5/alerting/provider/ilert"
"github.com/TwiN/gatus/v5/alerting/provider/incidentio" "github.com/TwiN/gatus/v5/alerting/provider/incidentio"
"github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace"
"github.com/TwiN/gatus/v5/alerting/provider/line" "github.com/TwiN/gatus/v5/alerting/provider/line"
"github.com/TwiN/gatus/v5/alerting/provider/matrix" "github.com/TwiN/gatus/v5/alerting/provider/matrix"
"github.com/TwiN/gatus/v5/alerting/provider/mattermost" "github.com/TwiN/gatus/v5/alerting/provider/mattermost"
@@ -94,9 +93,6 @@ type Config struct {
// IncidentIO is the configuration for the incident-io alerting provider // IncidentIO is the configuration for the incident-io alerting provider
IncidentIO *incidentio.AlertProvider `yaml:"incident-io,omitempty"` IncidentIO *incidentio.AlertProvider `yaml:"incident-io,omitempty"`
// JetBrainsSpace is the configuration for the jetbrains space alerting provider
JetBrainsSpace *jetbrainsspace.AlertProvider `yaml:"jetbrainsspace,omitempty"`
// Line is the configuration for the line alerting provider // Line is the configuration for the line alerting provider
Line *line.AlertProvider `yaml:"line,omitempty"` Line *line.AlertProvider `yaml:"line,omitempty"`
+7 -3
View File
@@ -20,8 +20,9 @@ var (
) )
type Config struct { type Config struct {
WebhookURL string `yaml:"webhook-url"` WebhookURL string `yaml:"webhook-url"`
Title string `yaml:"title,omitempty"` // Title of the message that will be sent Title string `yaml:"title,omitempty"` // Title of the message that will be sent
MessageContent string `yaml:"message-content,omitempty"` // Message content for pinging users or groups (e.g. "<@123456789>" or "<@&987654321>")
} }
func (cfg *Config) Validate() error { func (cfg *Config) Validate() error {
@@ -38,6 +39,9 @@ func (cfg *Config) Merge(override *Config) {
if len(override.Title) > 0 { if len(override.Title) > 0 {
cfg.Title = override.Title cfg.Title = override.Title
} }
if len(override.MessageContent) > 0 {
cfg.MessageContent = override.MessageContent
}
} }
// AlertProvider is the configuration necessary for sending an alert using Discord // AlertProvider is the configuration necessary for sending an alert using Discord
@@ -142,7 +146,7 @@ func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoi
title = cfg.Title title = cfg.Title
} }
body := Body{ body := Body{
Content: "", Content: cfg.MessageContent,
Embeds: []Embed{ Embeds: []Embed{
{ {
Title: title, Title: title,
+67
View File
@@ -134,6 +134,16 @@ func TestAlertProvider_Send(t *testing.T) {
}), }),
ExpectedError: false, ExpectedError: false,
}, },
{
Name: "triggered-with-message-content",
Provider: AlertProvider{DefaultConfig: Config{WebhookURL: "http://example.com", MessageContent: "<@123456789>"}},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
}),
ExpectedError: false,
},
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
@@ -200,6 +210,27 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Resolved: false, Resolved: false,
ExpectedBody: "{\"content\":\"\",\"embeds\":[{\"title\":\"provider-title\",\"description\":\"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"color\":15158332}]}", ExpectedBody: "{\"content\":\"\",\"embeds\":[{\"title\":\"provider-title\",\"description\":\"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"color\":15158332}]}",
}, },
{
Name: "triggered-with-message-content-user-mention",
Provider: AlertProvider{DefaultConfig: Config{MessageContent: "<@123456789>"}},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
ExpectedBody: "{\"content\":\"\\u003c@123456789\\u003e\",\"embeds\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"description\":\"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"color\":15158332,\"fields\":[{\"name\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n:x: - `[BODY] != \\\"\\\"`\\n\",\"inline\":false}]}]}",
},
{
Name: "triggered-with-message-content-role-mention",
Provider: AlertProvider{DefaultConfig: Config{MessageContent: "<@&987654321>"}},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
ExpectedBody: "{\"content\":\"\\u003c@\\u0026987654321\\u003e\",\"embeds\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"description\":\"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"color\":15158332,\"fields\":[{\"name\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n:x: - `[BODY] != \\\"\\\"`\\n\",\"inline\":false}]}]}",
},
{
Name: "resolved-with-message-content",
Provider: AlertProvider{DefaultConfig: Config{MessageContent: "<@123456789>"}},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedBody: "{\"content\":\"\\u003c@123456789\\u003e\",\"embeds\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"description\":\"An alert for **endpoint-name** has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"color\":3066993,\"fields\":[{\"name\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n:white_check_mark: - `[BODY] != \\\"\\\"`\\n\",\"inline\":false}]}]}",
},
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
@@ -313,6 +344,39 @@ func TestAlertProvider_GetConfig(t *testing.T) {
InputAlert: alert.Alert{ProviderOverride: map[string]any{"webhook-url": "http://alert-example.com"}}, InputAlert: alert.Alert{ProviderOverride: map[string]any{"webhook-url": "http://alert-example.com"}},
ExpectedOutput: Config{WebhookURL: "http://alert-example.com"}, ExpectedOutput: Config{WebhookURL: "http://alert-example.com"},
}, },
{
Name: "provider-with-message-content-default",
Provider: AlertProvider{
DefaultConfig: Config{WebhookURL: "http://example.com", MessageContent: "<@123456789>"},
},
InputGroup: "",
InputAlert: alert.Alert{},
ExpectedOutput: Config{WebhookURL: "http://example.com", MessageContent: "<@123456789>"},
},
{
Name: "provider-with-message-content-group-override",
Provider: AlertProvider{
DefaultConfig: Config{WebhookURL: "http://example.com", MessageContent: "<@123456789>"},
Overrides: []Override{
{
Group: "group",
Config: Config{WebhookURL: "http://group-example.com", MessageContent: "<@&987654321>"},
},
},
},
InputGroup: "group",
InputAlert: alert.Alert{},
ExpectedOutput: Config{WebhookURL: "http://group-example.com", MessageContent: "<@&987654321>"},
},
{
Name: "provider-with-message-content-alert-override",
Provider: AlertProvider{
DefaultConfig: Config{WebhookURL: "http://example.com", MessageContent: "<@123456789>"},
},
InputGroup: "",
InputAlert: alert.Alert{ProviderOverride: map[string]any{"message-content": "<@999999999>"}},
ExpectedOutput: Config{WebhookURL: "http://example.com", MessageContent: "<@999999999>"},
},
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
@@ -323,6 +387,9 @@ func TestAlertProvider_GetConfig(t *testing.T) {
if got.WebhookURL != scenario.ExpectedOutput.WebhookURL { if got.WebhookURL != scenario.ExpectedOutput.WebhookURL {
t.Errorf("expected webhook URL to be %s, got %s", scenario.ExpectedOutput.WebhookURL, got.WebhookURL) t.Errorf("expected webhook URL to be %s, got %s", scenario.ExpectedOutput.WebhookURL, got.WebhookURL)
} }
if got.MessageContent != scenario.ExpectedOutput.MessageContent {
t.Errorf("expected message content to be %s, got %s", scenario.ExpectedOutput.MessageContent, got.MessageContent)
}
// Test ValidateOverrides as well, since it really just calls GetConfig // Test ValidateOverrides as well, since it really just calls GetConfig
if err = scenario.Provider.ValidateOverrides(scenario.InputGroup, &scenario.InputAlert); err != nil { if err = scenario.Provider.ValidateOverrides(scenario.InputGroup, &scenario.InputAlert); err != nil {
t.Errorf("unexpected error: %s", err) t.Errorf("unexpected error: %s", err)
@@ -1,223 +0,0 @@
package jetbrainsspace
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
"gopkg.in/yaml.v3"
)
var (
ErrProjectNotSet = errors.New("project not set")
ErrChannelIDNotSet = errors.New("channel-id not set")
ErrTokenNotSet = errors.New("token not set")
ErrDuplicateGroupOverride = errors.New("duplicate group override")
)
type Config struct {
Project string `yaml:"project"` // Project name
ChannelID string `yaml:"channel-id"` // Chat Channel ID
Token string `yaml:"token"` // Bearer Token
}
func (cfg *Config) Validate() error {
if len(cfg.Project) == 0 {
return ErrProjectNotSet
}
if len(cfg.ChannelID) == 0 {
return ErrChannelIDNotSet
}
if len(cfg.Token) == 0 {
return ErrTokenNotSet
}
return nil
}
func (cfg *Config) Merge(override *Config) {
if len(override.Project) > 0 {
cfg.Project = override.Project
}
if len(override.ChannelID) > 0 {
cfg.ChannelID = override.ChannelID
}
if len(override.Token) > 0 {
cfg.Token = override.Token
}
}
// AlertProvider is the configuration necessary for sending an alert using JetBrains Space
type AlertProvider struct {
DefaultConfig Config `yaml:",inline"`
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
// Overrides is a list of Override that may be prioritized over the default configuration
Overrides []Override `yaml:"overrides,omitempty"`
}
// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
Config `yaml:",inline"`
}
// Validate the provider's configuration
func (provider *AlertProvider) Validate() error {
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" {
return ErrDuplicateGroupOverride
}
registeredGroups[override.Group] = true
}
}
return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
cfg, err := provider.GetConfig(ep.Group, alert)
if err != nil {
return err
}
buffer := bytes.NewBuffer(provider.buildRequestBody(cfg, ep, alert, result, resolved))
url := fmt.Sprintf("https://%s.jetbrains.space/api/http/chats/messages/send-message", cfg.Project)
request, err := http.NewRequest(http.MethodPost, url, buffer)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Authorization", "Bearer "+cfg.Token)
response, err := client.GetHTTPClient(nil).Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode > 399 {
body, _ := io.ReadAll(response.Body)
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
}
return err
}
type Body struct {
Channel string `json:"channel"`
Content Content `json:"content"`
}
type Content struct {
ClassName string `json:"className"`
Style string `json:"style"`
Sections []Section `json:"sections,omitempty"`
}
type Section struct {
ClassName string `json:"className"`
Elements []Element `json:"elements"`
Header string `json:"header"`
}
type Element struct {
ClassName string `json:"className"`
Accessory Accessory `json:"accessory"`
Style string `json:"style"`
Size string `json:"size"`
Content string `json:"content"`
}
type Accessory struct {
ClassName string `json:"className"`
Icon Icon `json:"icon"`
Style string `json:"style"`
}
type Icon struct {
Icon string `json:"icon"`
}
// buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
body := Body{
Channel: "id:" + cfg.ChannelID,
Content: Content{
ClassName: "ChatMessage.Block",
Sections: []Section{{
ClassName: "MessageSection",
Elements: []Element{},
}},
},
}
if resolved {
body.Content.Style = "SUCCESS"
body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
} else {
body.Content.Style = "WARNING"
body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
}
for _, conditionResult := range result.ConditionResults {
icon := "warning"
style := "WARNING"
if conditionResult.Success {
icon = "success"
style = "SUCCESS"
}
body.Content.Sections[0].Elements = append(body.Content.Sections[0].Elements, Element{
ClassName: "MessageText",
Accessory: Accessory{
ClassName: "MessageIcon",
Icon: Icon{Icon: icon},
Style: style,
},
Style: style,
Size: "REGULAR",
Content: conditionResult.Condition,
})
}
bodyAsJSON, _ := json.Marshal(body)
return bodyAsJSON
}
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
// GetConfig returns the configuration for the provider with the overrides applied
func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
cfg := provider.DefaultConfig
// Handle group overrides
if provider.Overrides != nil {
for _, override := range provider.Overrides {
if group == override.Group {
cfg.Merge(&override.Config)
break
}
}
}
// Handle alert overrides
if len(alert.ProviderOverride) != 0 {
overrideConfig := Config{}
if err := yaml.Unmarshal(alert.ProviderOverrideAsBytes(), &overrideConfig); err != nil {
return nil, err
}
cfg.Merge(&overrideConfig)
}
// Validate the configuration
err := cfg.Validate()
return &cfg, err
}
// ValidateOverrides validates the alert's provider override and, if present, the group override
func (provider *AlertProvider) ValidateOverrides(group string, alert *alert.Alert) error {
_, err := provider.GetConfig(group, alert)
return err
}
@@ -1,318 +0,0 @@
package jetbrainsspace
import (
"encoding/json"
"net/http"
"testing"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/test"
)
func TestAlertProvider_Validate(t *testing.T) {
invalidProvider := AlertProvider{DefaultConfig: Config{Project: ""}}
if err := invalidProvider.Validate(); err == nil {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{DefaultConfig: Config{Project: "foo", ChannelID: "bar", Token: "baz"}}
if err := validProvider.Validate(); err != nil {
t.Error("provider should've been valid")
}
}
func TestAlertProvider_ValidateWithOverride(t *testing.T) {
providerWithInvalidOverrideGroup := AlertProvider{
DefaultConfig: Config{Project: "foobar"},
Overrides: []Override{
{
Config: Config{ChannelID: "http://example.com"},
Group: "",
},
},
}
if err := providerWithInvalidOverrideGroup.Validate(); err == nil {
t.Error("provider Group shouldn't have been valid")
}
providerWithInvalidOverrideTo := AlertProvider{
DefaultConfig: Config{Project: "foobar"},
Overrides: []Override{
{
Config: Config{ChannelID: ""},
Group: "group",
},
},
}
if err := providerWithInvalidOverrideTo.Validate(); err == nil {
t.Error("provider integration key shouldn't have been valid")
}
providerWithValidOverride := AlertProvider{
DefaultConfig: Config{
Project: "foo",
ChannelID: "bar",
Token: "baz",
},
Overrides: []Override{
{
Config: Config{ChannelID: "foobar"},
Group: "group",
},
},
}
if err := providerWithValidOverride.Validate(); err != nil {
t.Error("provider should've been valid")
}
}
func TestAlertProvider_Send(t *testing.T) {
defer client.InjectHTTPClient(nil)
firstDescription := "description-1"
secondDescription := "description-2"
scenarios := []struct {
Name string
Provider AlertProvider
Alert alert.Alert
Resolved bool
MockRoundTripper test.MockRoundTripper
ExpectedError bool
}{
{
Name: "triggered",
Provider: AlertProvider{DefaultConfig: Config{ChannelID: "1", Project: "project", Token: "token"}},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
}),
ExpectedError: false,
},
{
Name: "triggered-error",
Provider: AlertProvider{DefaultConfig: Config{ChannelID: "1", Project: "project", Token: "token"}},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusInternalServerError, Body: http.NoBody}
}),
ExpectedError: true,
},
{
Name: "resolved",
Provider: AlertProvider{DefaultConfig: Config{ChannelID: "1", Project: "project", Token: "token"}},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
}),
ExpectedError: false,
},
{
Name: "resolved-error",
Provider: AlertProvider{DefaultConfig: Config{ChannelID: "1", Project: "project", Token: "token"}},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusInternalServerError, Body: http.NoBody}
}),
ExpectedError: true,
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
err := scenario.Provider.Send(
&endpoint.Endpoint{Name: "endpoint-name"},
&scenario.Alert,
&endpoint.Result{
ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
},
},
scenario.Resolved,
)
if scenario.ExpectedError && err == nil {
t.Error("expected error, got none")
}
if !scenario.ExpectedError && err != nil {
t.Error("expected no error, got", err.Error())
}
})
}
}
func TestAlertProvider_buildRequestBody(t *testing.T) {
firstDescription := "description-1"
secondDescription := "description-2"
scenarios := []struct {
Name string
Provider AlertProvider
Endpoint endpoint.Endpoint
Alert alert.Alert
Resolved bool
ExpectedBody string
}{
{
Name: "triggered",
Provider: AlertProvider{DefaultConfig: Config{ChannelID: "1", Project: "project"}},
Endpoint: endpoint.Endpoint{Name: "name"},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
ExpectedBody: `{"channel":"id:1","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been triggered due to having failed 3 time(s) in a row"}]}}`,
},
{
Name: "triggered-with-group",
Provider: AlertProvider{DefaultConfig: Config{ChannelID: "1", Project: "project"}},
Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
ExpectedBody: `{"channel":"id:1","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row"}]}}`,
},
{
Name: "resolved",
Provider: AlertProvider{DefaultConfig: Config{ChannelID: "1", Project: "project"}},
Endpoint: endpoint.Endpoint{Name: "name"},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedBody: `{"channel":"id:1","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been resolved after passing successfully 5 time(s) in a row"}]}}`,
},
{
Name: "resolved-with-group",
Provider: AlertProvider{DefaultConfig: Config{ChannelID: "1", Project: "project"}},
Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedBody: `{"channel":"id:1","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row"}]}}`,
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
body := scenario.Provider.buildRequestBody(
&scenario.Provider.DefaultConfig,
&scenario.Endpoint,
&scenario.Alert,
&endpoint.Result{
ConditionResults: []*endpoint.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
},
},
scenario.Resolved,
)
if string(body) != scenario.ExpectedBody {
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
}
out := make(map[string]interface{})
if err := json.Unmarshal(body, &out); err != nil {
t.Error("expected body to be valid JSON, got error:", err.Error())
}
})
}
}
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
t.Error("expected default alert to be not nil")
}
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
t.Error("expected default alert to be nil")
}
}
func TestAlertProvider_GetConfig(t *testing.T) {
scenarios := []struct {
Name string
Provider AlertProvider
InputGroup string
InputAlert alert.Alert
ExpectedOutput Config
}{
{
Name: "provider-no-override-specify-no-group-should-default",
Provider: AlertProvider{
DefaultConfig: Config{ChannelID: "default", Project: "project", Token: "token"},
Overrides: nil,
},
InputGroup: "",
InputAlert: alert.Alert{},
ExpectedOutput: Config{ChannelID: "default", Project: "project", Token: "token"},
},
{
Name: "provider-no-override-specify-group-should-default",
Provider: AlertProvider{
DefaultConfig: Config{ChannelID: "default", Project: "project", Token: "token"},
Overrides: nil,
},
InputGroup: "group",
InputAlert: alert.Alert{},
ExpectedOutput: Config{ChannelID: "default", Project: "project", Token: "token"},
},
{
Name: "provider-with-override-specify-no-group-should-default",
Provider: AlertProvider{
DefaultConfig: Config{ChannelID: "default", Project: "project", Token: "token"},
Overrides: []Override{
{
Group: "group",
Config: Config{ChannelID: "group-channel"},
},
},
},
InputGroup: "",
InputAlert: alert.Alert{},
ExpectedOutput: Config{ChannelID: "default", Project: "project", Token: "token"},
},
{
Name: "provider-with-override-specify-group-should-override",
Provider: AlertProvider{
DefaultConfig: Config{ChannelID: "default", Project: "project", Token: "token"},
Overrides: []Override{
{
Group: "group",
Config: Config{ChannelID: "group-channel"},
},
},
},
InputGroup: "group",
InputAlert: alert.Alert{},
ExpectedOutput: Config{ChannelID: "group-channel", Project: "project", Token: "token"},
},
{
Name: "provider-with-group-override-and-alert-override--alert-override-should-take-precedence",
Provider: AlertProvider{
DefaultConfig: Config{ChannelID: "default", Project: "project", Token: "token"},
Overrides: []Override{
{
Group: "group",
Config: Config{ChannelID: "group-channel"},
},
},
},
InputGroup: "group",
InputAlert: alert.Alert{ProviderOverride: map[string]any{"channel-id": "alert-channel", "project": "alert-project", "token": "alert-token"}},
ExpectedOutput: Config{ChannelID: "alert-channel", Project: "alert-project", Token: "alert-token"},
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
got, err := scenario.Provider.GetConfig(scenario.InputGroup, &scenario.InputAlert)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if got.ChannelID != scenario.ExpectedOutput.ChannelID {
t.Errorf("expected %s, got %s", scenario.ExpectedOutput.ChannelID, got.ChannelID)
}
if got.Project != scenario.ExpectedOutput.Project {
t.Errorf("expected %s, got %s", scenario.ExpectedOutput.Project, got.Project)
}
if got.Token != scenario.ExpectedOutput.Token {
t.Errorf("expected %s, got %s", scenario.ExpectedOutput.Token, got.Token)
}
// Test ValidateOverrides as well, since it really just calls GetConfig
if err = scenario.Provider.ValidateOverrides(scenario.InputGroup, &scenario.InputAlert); err != nil {
t.Errorf("unexpected error: %s", err)
}
})
}
}
-3
View File
@@ -16,7 +16,6 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/ifttt" "github.com/TwiN/gatus/v5/alerting/provider/ifttt"
"github.com/TwiN/gatus/v5/alerting/provider/ilert" "github.com/TwiN/gatus/v5/alerting/provider/ilert"
"github.com/TwiN/gatus/v5/alerting/provider/incidentio" "github.com/TwiN/gatus/v5/alerting/provider/incidentio"
"github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace"
"github.com/TwiN/gatus/v5/alerting/provider/line" "github.com/TwiN/gatus/v5/alerting/provider/line"
"github.com/TwiN/gatus/v5/alerting/provider/matrix" "github.com/TwiN/gatus/v5/alerting/provider/matrix"
"github.com/TwiN/gatus/v5/alerting/provider/mattermost" "github.com/TwiN/gatus/v5/alerting/provider/mattermost"
@@ -106,7 +105,6 @@ var (
_ AlertProvider = (*ifttt.AlertProvider)(nil) _ AlertProvider = (*ifttt.AlertProvider)(nil)
_ AlertProvider = (*ilert.AlertProvider)(nil) _ AlertProvider = (*ilert.AlertProvider)(nil)
_ AlertProvider = (*incidentio.AlertProvider)(nil) _ AlertProvider = (*incidentio.AlertProvider)(nil)
_ AlertProvider = (*jetbrainsspace.AlertProvider)(nil)
_ AlertProvider = (*line.AlertProvider)(nil) _ AlertProvider = (*line.AlertProvider)(nil)
_ AlertProvider = (*matrix.AlertProvider)(nil) _ AlertProvider = (*matrix.AlertProvider)(nil)
_ AlertProvider = (*mattermost.AlertProvider)(nil) _ AlertProvider = (*mattermost.AlertProvider)(nil)
@@ -148,7 +146,6 @@ var (
_ Config[ifttt.Config] = (*ifttt.Config)(nil) _ Config[ifttt.Config] = (*ifttt.Config)(nil)
_ Config[ilert.Config] = (*ilert.Config)(nil) _ Config[ilert.Config] = (*ilert.Config)(nil)
_ Config[incidentio.Config] = (*incidentio.Config)(nil) _ Config[incidentio.Config] = (*incidentio.Config)(nil)
_ Config[jetbrainsspace.Config] = (*jetbrainsspace.Config)(nil)
_ Config[line.Config] = (*line.Config)(nil) _ Config[line.Config] = (*line.Config)(nil)
_ Config[matrix.Config] = (*matrix.Config)(nil) _ Config[matrix.Config] = (*matrix.Config)(nil)
_ Config[mattermost.Config] = (*mattermost.Config)(nil) _ Config[mattermost.Config] = (*mattermost.Config)(nil)
-1
View File
@@ -607,7 +607,6 @@ func ValidateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoi
alert.TypeIFTTT, alert.TypeIFTTT,
alert.TypeIlert, alert.TypeIlert,
alert.TypeIncidentIO, alert.TypeIncidentIO,
alert.TypeJetBrainsSpace,
alert.TypeLine, alert.TypeLine,
alert.TypeMatrix, alert.TypeMatrix,
alert.TypeMattermost, alert.TypeMattermost,
+12 -64
View File
@@ -25,7 +25,6 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/ifttt" "github.com/TwiN/gatus/v5/alerting/provider/ifttt"
"github.com/TwiN/gatus/v5/alerting/provider/ilert" "github.com/TwiN/gatus/v5/alerting/provider/ilert"
"github.com/TwiN/gatus/v5/alerting/provider/incidentio" "github.com/TwiN/gatus/v5/alerting/provider/incidentio"
"github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace"
"github.com/TwiN/gatus/v5/alerting/provider/line" "github.com/TwiN/gatus/v5/alerting/provider/line"
"github.com/TwiN/gatus/v5/alerting/provider/matrix" "github.com/TwiN/gatus/v5/alerting/provider/matrix"
"github.com/TwiN/gatus/v5/alerting/provider/mattermost" "github.com/TwiN/gatus/v5/alerting/provider/mattermost"
@@ -775,10 +774,6 @@ alerting:
to: "+1-234-567-8901" to: "+1-234-567-8901"
teams: teams:
webhook-url: "http://example.com" webhook-url: "http://example.com"
jetbrainsspace:
project: "foo"
channel-id: "bar"
token: "baz"
endpoints: endpoints:
- name: website - name: website
@@ -801,7 +796,6 @@ endpoints:
success-threshold: 15 success-threshold: 15
- type: teams - type: teams
- type: pushover - type: pushover
- type: jetbrainsspace
conditions: conditions:
- "[STATUS] == 200" - "[STATUS] == 200"
`)) `))
@@ -828,8 +822,8 @@ endpoints:
if config.Endpoints[0].Interval != 60*time.Second { if config.Endpoints[0].Interval != 60*time.Second {
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second) t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
} }
if len(config.Endpoints[0].Alerts) != 10 { if len(config.Endpoints[0].Alerts) != 9 {
t.Fatal("There should've been 10 alerts configured") t.Fatal("There should've been 9 alerts configured")
} }
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack { if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
@@ -936,12 +930,6 @@ endpoints:
if !config.Endpoints[0].Alerts[8].IsEnabled() { if !config.Endpoints[0].Alerts[8].IsEnabled() {
t.Error("The alert should've been enabled") t.Error("The alert should've been enabled")
} }
if config.Endpoints[0].Alerts[9].Type != alert.TypeJetBrainsSpace {
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeJetBrainsSpace, config.Endpoints[0].Alerts[9].Type)
}
if !config.Endpoints[0].Alerts[9].IsEnabled() {
t.Error("The alert should've been enabled")
}
} }
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlert(t *testing.T) { func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlert(t *testing.T) {
@@ -1001,14 +989,6 @@ alerting:
webhook-url: "http://example.com" webhook-url: "http://example.com"
default-alert: default-alert:
enabled: true enabled: true
jetbrainsspace:
project: "foo"
channel-id: "bar"
token: "baz"
default-alert:
enabled: true
failure-threshold: 5
success-threshold: 3
email: email:
from: "from@example.com" from: "from@example.com"
username: "from@example.com" username: "from@example.com"
@@ -1047,7 +1027,6 @@ endpoints:
- type: twilio - type: twilio
- type: teams - type: teams
- type: pushover - type: pushover
- type: jetbrainsspace
- type: email - type: email
- type: gotify - type: gotify
conditions: conditions:
@@ -1169,22 +1148,6 @@ endpoints:
t.Fatal("Teams.GetDefaultAlert() shouldn't have returned nil") t.Fatal("Teams.GetDefaultAlert() shouldn't have returned nil")
} }
if config.Alerting.JetBrainsSpace == nil || config.Alerting.JetBrainsSpace.Validate() != nil {
t.Fatal("JetBrainsSpace alerting config should've been valid")
}
if config.Alerting.JetBrainsSpace.GetDefaultAlert() == nil {
t.Fatal("JetBrainsSpace.GetDefaultAlert() shouldn't have returned nil")
}
if config.Alerting.JetBrainsSpace.DefaultConfig.Project != "foo" {
t.Errorf("JetBrainsSpace webhook should've been %s, but was %s", "foo", config.Alerting.JetBrainsSpace.DefaultConfig.Project)
}
if config.Alerting.JetBrainsSpace.DefaultConfig.ChannelID != "bar" {
t.Errorf("JetBrainsSpace webhook should've been %s, but was %s", "bar", config.Alerting.JetBrainsSpace.DefaultConfig.ChannelID)
}
if config.Alerting.JetBrainsSpace.DefaultConfig.Token != "baz" {
t.Errorf("JetBrainsSpace webhook should've been %s, but was %s", "baz", config.Alerting.JetBrainsSpace.DefaultConfig.Token)
}
if config.Alerting.Email == nil || config.Alerting.Email.Validate() != nil { if config.Alerting.Email == nil || config.Alerting.Email.Validate() != nil {
t.Fatal("Email alerting config should've been valid") t.Fatal("Email alerting config should've been valid")
} }
@@ -1256,8 +1219,8 @@ endpoints:
if config.Endpoints[0].Interval != 60*time.Second { if config.Endpoints[0].Interval != 60*time.Second {
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second) t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
} }
if len(config.Endpoints[0].Alerts) != 12 { if len(config.Endpoints[0].Alerts) != 11 {
t.Fatalf("There should've been 12 alerts configured, got %d", len(config.Endpoints[0].Alerts)) t.Fatalf("There should've been 11 alerts configured, got %d", len(config.Endpoints[0].Alerts))
} }
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack { if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
@@ -1374,21 +1337,21 @@ endpoints:
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[8].SuccessThreshold) t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[8].SuccessThreshold)
} }
if config.Endpoints[0].Alerts[9].Type != alert.TypeJetBrainsSpace { if config.Endpoints[0].Alerts[9].Type != alert.TypeEmail {
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeJetBrainsSpace, config.Endpoints[0].Alerts[9].Type) t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeEmail, config.Endpoints[0].Alerts[9].Type)
} }
if !config.Endpoints[0].Alerts[9].IsEnabled() { if !config.Endpoints[0].Alerts[9].IsEnabled() {
t.Error("The alert should've been enabled") t.Error("The alert should've been enabled")
} }
if config.Endpoints[0].Alerts[9].FailureThreshold != 5 { if config.Endpoints[0].Alerts[9].FailureThreshold != 3 {
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 5, config.Endpoints[0].Alerts[9].FailureThreshold) t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[9].FailureThreshold)
} }
if config.Endpoints[0].Alerts[9].SuccessThreshold != 3 { if config.Endpoints[0].Alerts[9].SuccessThreshold != 2 {
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[9].SuccessThreshold) t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[9].SuccessThreshold)
} }
if config.Endpoints[0].Alerts[10].Type != alert.TypeEmail { if config.Endpoints[0].Alerts[10].Type != alert.TypeGotify {
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeEmail, config.Endpoints[0].Alerts[10].Type) t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeGotify, config.Endpoints[0].Alerts[10].Type)
} }
if !config.Endpoints[0].Alerts[10].IsEnabled() { if !config.Endpoints[0].Alerts[10].IsEnabled() {
t.Error("The alert should've been enabled") t.Error("The alert should've been enabled")
@@ -1399,19 +1362,6 @@ endpoints:
if config.Endpoints[0].Alerts[10].SuccessThreshold != 2 { if config.Endpoints[0].Alerts[10].SuccessThreshold != 2 {
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[10].SuccessThreshold) t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[10].SuccessThreshold)
} }
if config.Endpoints[0].Alerts[11].Type != alert.TypeGotify {
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeGotify, config.Endpoints[0].Alerts[11].Type)
}
if !config.Endpoints[0].Alerts[11].IsEnabled() {
t.Error("The alert should've been enabled")
}
if config.Endpoints[0].Alerts[11].FailureThreshold != 3 {
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[11].FailureThreshold)
}
if config.Endpoints[0].Alerts[11].SuccessThreshold != 2 {
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[11].SuccessThreshold)
}
} }
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlertAndMultipleAlertsOfSameTypeWithOverriddenParameters(t *testing.T) { func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlertAndMultipleAlertsOfSameTypeWithOverriddenParameters(t *testing.T) {
@@ -1917,7 +1867,6 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
IFTTT: &ifttt.AlertProvider{}, IFTTT: &ifttt.AlertProvider{},
Ilert: &ilert.AlertProvider{}, Ilert: &ilert.AlertProvider{},
IncidentIO: &incidentio.AlertProvider{}, IncidentIO: &incidentio.AlertProvider{},
JetBrainsSpace: &jetbrainsspace.AlertProvider{},
Line: &line.AlertProvider{}, Line: &line.AlertProvider{},
Matrix: &matrix.AlertProvider{}, Matrix: &matrix.AlertProvider{},
Mattermost: &mattermost.AlertProvider{}, Mattermost: &mattermost.AlertProvider{},
@@ -1962,7 +1911,6 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
{alertType: alert.TypeIFTTT, expected: alertingConfig.IFTTT}, {alertType: alert.TypeIFTTT, expected: alertingConfig.IFTTT},
{alertType: alert.TypeIlert, expected: alertingConfig.Ilert}, {alertType: alert.TypeIlert, expected: alertingConfig.Ilert},
{alertType: alert.TypeIncidentIO, expected: alertingConfig.IncidentIO}, {alertType: alert.TypeIncidentIO, expected: alertingConfig.IncidentIO},
{alertType: alert.TypeJetBrainsSpace, expected: alertingConfig.JetBrainsSpace},
{alertType: alert.TypeLine, expected: alertingConfig.Line}, {alertType: alert.TypeLine, expected: alertingConfig.Line},
{alertType: alert.TypeMatrix, expected: alertingConfig.Matrix}, {alertType: alert.TypeMatrix, expected: alertingConfig.Matrix},
{alertType: alert.TypeMattermost, expected: alertingConfig.Mattermost}, {alertType: alert.TypeMattermost, expected: alertingConfig.Mattermost},
+5 -1
View File
@@ -15,5 +15,9 @@ func sanitize(s string) string {
s = strings.ReplaceAll(s, ",", "-") s = strings.ReplaceAll(s, ",", "-")
s = strings.ReplaceAll(s, " ", "-") s = strings.ReplaceAll(s, " ", "-")
s = strings.ReplaceAll(s, "#", "-") s = strings.ReplaceAll(s, "#", "-")
s = strings.ReplaceAll(s, "(", "-")
s = strings.ReplaceAll(s, ")", "-")
s = strings.ReplaceAll(s, "+", "-")
s = strings.ReplaceAll(s, "&", "-")
return s return s
} }
+15
View File
@@ -29,6 +29,21 @@ func TestConvertGroupAndNameToKey(t *testing.T) {
Name: "name", Name: "name",
ExpectedOutput: "_name", ExpectedOutput: "_name",
}, },
{
GroupName: "API (v1)",
Name: "endpoint",
ExpectedOutput: "api--v1-_endpoint",
},
{
GroupName: "website (admin)",
Name: "test",
ExpectedOutput: "website--admin-_test",
},
{
GroupName: "search",
Name: "query&filter",
ExpectedOutput: "search_query-filter",
},
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.ExpectedOutput, func(t *testing.T) { t.Run(scenario.ExpectedOutput, func(t *testing.T) {
+29 -28
View File
@@ -5,7 +5,7 @@ go 1.24.4
toolchain go1.24.7 toolchain go1.24.7
require ( require (
code.gitea.io/sdk/gitea v0.21.0 code.gitea.io/sdk/gitea v0.22.0
github.com/TwiN/deepmerge v0.2.2 github.com/TwiN/deepmerge v0.2.2
github.com/TwiN/g8/v2 v2.0.0 github.com/TwiN/g8/v2 v2.0.0
github.com/TwiN/gocache/v2 v2.4.0 github.com/TwiN/gocache/v2 v2.4.0
@@ -16,33 +16,33 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.31.12 github.com/aws/aws-sdk-go-v2/config v1.31.12
github.com/aws/aws-sdk-go-v2/credentials v1.18.16 github.com/aws/aws-sdk-go-v2/credentials v1.18.16
github.com/aws/aws-sdk-go-v2/service/ses v1.34.5 github.com/aws/aws-sdk-go-v2/service/ses v1.34.5
github.com/coreos/go-oidc/v3 v3.14.1 github.com/coreos/go-oidc/v3 v3.16.0
github.com/gofiber/fiber/v2 v2.52.8 github.com/gofiber/fiber/v2 v2.52.9
github.com/google/go-github/v48 v48.2.0 github.com/google/go-github/v48 v48.2.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/miekg/dns v1.1.68 github.com/miekg/dns v1.1.68
github.com/prometheus-community/pro-bing v0.7.0 github.com/prometheus-community/pro-bing v0.7.0
github.com/prometheus/client_golang v1.23.0 github.com/prometheus/client_golang v1.23.2
github.com/registrobr/rdap v1.1.8 github.com/registrobr/rdap v1.1.8
github.com/valyala/fasthttp v1.64.0 github.com/valyala/fasthttp v1.67.0
github.com/wcharczuk/go-chart/v2 v2.1.2 github.com/wcharczuk/go-chart/v2 v2.1.2
golang.org/x/crypto v0.40.0 golang.org/x/crypto v0.43.0
golang.org/x/net v0.42.0 golang.org/x/net v0.46.0
golang.org/x/oauth2 v0.30.0 golang.org/x/oauth2 v0.32.0
golang.org/x/sync v0.17.0 golang.org/x/sync v0.17.0
google.golang.org/api v0.242.0 google.golang.org/api v0.252.0
gopkg.in/mail.v2 v2.3.1 gopkg.in/mail.v2 v2.3.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.38.2 modernc.org/sqlite v1.39.1
) )
require ( require (
cloud.google.com/go/auth v0.16.2 // indirect cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/42wim/httpsig v1.2.2 // indirect github.com/42wim/httpsig v1.2.3 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect
@@ -60,14 +60,14 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect
@@ -77,27 +77,28 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/image v0.18.0 // indirect golang.org/x/image v0.18.0 // indirect
golang.org/x/mod v0.25.0 // indirect golang.org/x/mod v0.28.0 // indirect
golang.org/x/sys v0.34.0 // indirect golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.27.0 // indirect golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.34.0 // indirect golang.org/x/tools v0.37.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect
google.golang.org/grpc v1.73.0 // indirect google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
modernc.org/libc v1.66.3 // indirect modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
) )
+76 -72
View File
@@ -1,13 +1,13 @@
cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4= code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0=
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA= code.gitea.io/sdk/gitea v0.22.0/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
github.com/42wim/httpsig v1.2.2 h1:ofAYoHUNs/MJOLqQ8hIxeyz2QxOz8qdSVvp3PX/oPgA= github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
github.com/42wim/httpsig v1.2.2/go.mod h1:P/UYo7ytNBFwc+dg35IubuAUIs8zj5zzFIgUCEl55WY= github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
github.com/TwiN/deepmerge v0.2.2 h1:FUG9QMIYg/j2aQyPPhA3XTFJwXSNHI/swaR4Lbyxwg4= github.com/TwiN/deepmerge v0.2.2 h1:FUG9QMIYg/j2aQyPPhA3XTFJwXSNHI/swaR4Lbyxwg4=
github.com/TwiN/deepmerge v0.2.2/go.mod h1:4OHvjV3pPNJCJZBHswYAwk6rxiD8h8YZ+9cPo7nu4oI= github.com/TwiN/deepmerge v0.2.2/go.mod h1:4OHvjV3pPNJCJZBHswYAwk6rxiD8h8YZ+9cPo7nu4oI=
github.com/TwiN/g8/v2 v2.0.0 h1:+hwIbRLMhDd2iwHzkZUPp2FkX7yTx8ddYOnS91HkDqQ= github.com/TwiN/g8/v2 v2.0.0 h1:+hwIbRLMhDd2iwHzkZUPp2FkX7yTx8ddYOnS91HkDqQ=
@@ -56,8 +56,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
@@ -68,15 +68,15 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4= github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -97,8 +97,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 h1:i2fYnDurfLlJH8AyyMOnkLHnHeP8Ff/DDpuZA/D3bPo= github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 h1:i2fYnDurfLlJH8AyyMOnkLHnHeP8Ff/DDpuZA/D3bPo=
@@ -130,12 +130,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA= github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA=
github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE= github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/registrobr/rdap v1.1.8 h1:7egYAM8MsuencdP9mvF/892f8OjXvUFSyp5cT1Lg45U= github.com/registrobr/rdap v1.1.8 h1:7egYAM8MsuencdP9mvF/892f8OjXvUFSyp5cT1Lg45U=
@@ -147,12 +147,12 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og= github.com/valyala/fasthttp v1.67.0 h1:tqKlJMUP6iuNG8hGjK/s9J4kadH7HLV4ijEcPGsezac=
github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA= github.com/valyala/fasthttp v1.67.0/go.mod h1:qYSIpqt/0XNmShgo/8Aq8E3UYWVVwNS2QYmzd8WIEPM=
github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E= github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ= github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
@@ -164,18 +164,20 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
@@ -183,8 +185,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
@@ -194,8 +196,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -205,10 +207,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -230,8 +232,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -240,8 +242,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -251,28 +253,30 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= google.golang.org/api v0.252.0 h1:xfKJeAJaMwb8OC9fesr369rjciQ704AjU/psjkKURSI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/api v0.252.0/go.mod h1:dnHOv81x5RAmumZ7BWLShB/u7JZNeyalImxHmtTHxqw=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -282,18 +286,18 @@ gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -302,8 +306,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
-14
View File
@@ -12,7 +12,6 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/discord" "github.com/TwiN/gatus/v5/alerting/provider/discord"
"github.com/TwiN/gatus/v5/alerting/provider/email" "github.com/TwiN/gatus/v5/alerting/provider/email"
"github.com/TwiN/gatus/v5/alerting/provider/ifttt" "github.com/TwiN/gatus/v5/alerting/provider/ifttt"
"github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace"
"github.com/TwiN/gatus/v5/alerting/provider/line" "github.com/TwiN/gatus/v5/alerting/provider/line"
"github.com/TwiN/gatus/v5/alerting/provider/matrix" "github.com/TwiN/gatus/v5/alerting/provider/matrix"
"github.com/TwiN/gatus/v5/alerting/provider/mattermost" "github.com/TwiN/gatus/v5/alerting/provider/mattermost"
@@ -325,19 +324,6 @@ func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) {
}, },
}, },
}, },
{
Name: "jetbrainsspace",
AlertType: alert.TypeJetBrainsSpace,
AlertingConfig: &alerting.Config{
JetBrainsSpace: &jetbrainsspace.AlertProvider{
DefaultConfig: jetbrainsspace.Config{
Project: "foo",
ChannelID: "bar",
Token: "baz",
},
},
},
},
{ {
Name: "line", Name: "line",
AlertType: alert.TypeLine, AlertType: alert.TypeLine,
+39 -3
View File
@@ -148,7 +148,7 @@
</div> </div>
<!-- Tooltip --> <!-- Tooltip -->
<Tooltip :result="tooltip.result" :event="tooltip.event" /> <Tooltip :result="tooltip.result" :event="tooltip.event" :isPersistent="tooltipIsPersistent" />
</div> </div>
</template> </template>
@@ -173,6 +173,7 @@ const announcements = ref([])
const tooltip = ref({}) const tooltip = ref({})
const mobileMenuOpen = ref(false) const mobileMenuOpen = ref(false)
const isOidcLoading = ref(false) const isOidcLoading = ref(false)
const tooltipIsPersistent = ref(false)
let configInterval = null let configInterval = null
// Computed properties // Computed properties
@@ -209,8 +210,39 @@ const fetchConfig = async () => {
} }
} }
const showTooltip = (result, event) => { const showTooltip = (result, event, action = 'hover') => {
tooltip.value = { result, event } if (action === 'click') {
if (!result) {
// Deselecting
tooltip.value = {}
tooltipIsPersistent.value = false
} else {
// Selecting new data point
tooltip.value = { result, event }
tooltipIsPersistent.value = true
}
} else if (action === 'hover') {
// Only update tooltip on hover if not in persistent mode
if (!tooltipIsPersistent.value) {
tooltip.value = { result, event }
}
}
}
const handleDocumentClick = (event) => {
// Close persistent tooltip when clicking outside
if (tooltipIsPersistent.value) {
const tooltipElement = document.getElementById('tooltip')
// Check if click is on a data point bar or inside tooltip
const clickedDataPoint = event.target.closest('.flex-1.h-6, .flex-1.h-8')
if (tooltipElement && !tooltipElement.contains(event.target) && !clickedDataPoint) {
tooltip.value = {}
tooltipIsPersistent.value = false
// Emit event to clear selections in child components
window.dispatchEvent(new CustomEvent('clear-data-point-selection'))
}
}
} }
// Fetch config on mount and set up interval // Fetch config on mount and set up interval
@@ -218,6 +250,8 @@ onMounted(() => {
fetchConfig() fetchConfig()
// Refresh config every 10 minutes for announcements // Refresh config every 10 minutes for announcements
configInterval = setInterval(fetchConfig, 600000) configInterval = setInterval(fetchConfig, 600000)
// Add click listener for closing persistent tooltips
document.addEventListener('click', handleDocumentClick)
}) })
// Clean up interval on unmount // Clean up interval on unmount
@@ -226,5 +260,7 @@ onUnmounted(() => {
clearInterval(configInterval) clearInterval(configInterval)
configInterval = null configInterval = null
} }
// Remove click listener
document.removeEventListener('click', handleDocumentClick)
}) })
</script> </script>
+47 -4
View File
@@ -39,10 +39,16 @@
:key="index" :key="index"
:class="[ :class="[
'flex-1 h-6 sm:h-8 rounded-sm transition-all', 'flex-1 h-6 sm:h-8 rounded-sm transition-all',
result ? (result.success ? 'bg-green-500 hover:bg-green-700' : 'bg-red-500 hover:bg-red-700') : 'bg-gray-200 dark:bg-gray-700' result ? 'cursor-pointer' : '',
result ? (
result.success
? (selectedResultIndex === index ? 'bg-green-700' : 'bg-green-500 hover:bg-green-700')
: (selectedResultIndex === index ? 'bg-red-700' : 'bg-red-500 hover:bg-red-700')
) : 'bg-gray-200 dark:bg-gray-700'
]" ]"
@mouseenter="result && emit('showTooltip', result, $event)" @mouseenter="result && handleMouseEnter(result, $event)"
@mouseleave="result && emit('showTooltip', null, $event)" @mouseleave="result && handleMouseLeave(result, $event)"
@click.stop="result && handleClick(result, $event, index)"
/> />
</div> </div>
<div class="flex items-center justify-between text-xs text-muted-foreground mt-1"> <div class="flex items-center justify-between text-xs text-muted-foreground mt-1">
@@ -57,7 +63,7 @@
<script setup> <script setup>
/* eslint-disable no-undef */ /* eslint-disable no-undef */
import { computed } from 'vue' import { computed, ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import StatusBadge from '@/components/StatusBadge.vue' import StatusBadge from '@/components/StatusBadge.vue'
@@ -82,6 +88,9 @@ const props = defineProps({
const emit = defineEmits(['showTooltip']) const emit = defineEmits(['showTooltip'])
// Track selected data point
const selectedResultIndex = ref(null)
const latestResult = computed(() => { const latestResult = computed(() => {
if (!props.endpoint.results || props.endpoint.results.length === 0) { if (!props.endpoint.results || props.endpoint.results.length === 0) {
return null return null
@@ -156,4 +165,38 @@ const newestResultTime = computed(() => {
const navigateToDetails = () => { const navigateToDetails = () => {
router.push(`/endpoints/${props.endpoint.key}`) router.push(`/endpoints/${props.endpoint.key}`)
} }
const handleMouseEnter = (result, event) => {
emit('showTooltip', result, event, 'hover')
}
const handleMouseLeave = (result, event) => {
emit('showTooltip', null, event, 'hover')
}
const handleClick = (result, event, index) => {
// Clear selections in other cards first
window.dispatchEvent(new CustomEvent('clear-data-point-selection'))
// Then toggle this card's selection
if (selectedResultIndex.value === index) {
selectedResultIndex.value = null
emit('showTooltip', null, event, 'click')
} else {
selectedResultIndex.value = index
emit('showTooltip', result, event, 'click')
}
}
// Listen for clear selection event
const handleClearSelection = () => {
selectedResultIndex.value = null
}
onMounted(() => {
window.addEventListener('clear-data-point-selection', handleClearSelection)
})
onUnmounted(() => {
window.removeEventListener('clear-data-point-selection', handleClearSelection)
})
</script> </script>
+43 -8
View File
@@ -39,10 +39,16 @@
:key="index" :key="index"
:class="[ :class="[
'flex-1 h-6 sm:h-8 rounded-sm transition-all', 'flex-1 h-6 sm:h-8 rounded-sm transition-all',
result ? (result.success ? 'bg-green-500 hover:bg-green-700' : 'bg-red-500 hover:bg-red-700') : 'bg-gray-200 dark:bg-gray-700' result ? 'cursor-pointer' : '',
result ? (
result.success
? (selectedResultIndex === index ? 'bg-green-700' : 'bg-green-500 hover:bg-green-700')
: (selectedResultIndex === index ? 'bg-red-700' : 'bg-red-500 hover:bg-red-700')
) : 'bg-gray-200 dark:bg-gray-700'
]" ]"
@mouseenter="result && showTooltip(result, $event)" @mouseenter="result && handleMouseEnter(result, $event)"
@mouseleave="hideTooltip($event)" @mouseleave="result && handleMouseLeave(result, $event)"
@click.stop="result && handleClick(result, $event, index)"
/> />
</div> </div>
<div class="flex items-center justify-between text-xs text-muted-foreground mt-1"> <div class="flex items-center justify-between text-xs text-muted-foreground mt-1">
@@ -56,7 +62,7 @@
</template> </template>
<script setup> <script setup>
import { computed } from 'vue' import { computed, ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import StatusBadge from '@/components/StatusBadge.vue' import StatusBadge from '@/components/StatusBadge.vue'
@@ -77,6 +83,9 @@ const props = defineProps({
const emit = defineEmits(['showTooltip']) const emit = defineEmits(['showTooltip'])
// Track selected data point
const selectedResultIndex = ref(null)
// Computed properties // Computed properties
const displayResults = computed(() => { const displayResults = computed(() => {
const results = [...(props.suite.results || [])] const results = [...(props.suite.results || [])]
@@ -143,13 +152,39 @@ const navigateToDetails = () => {
router.push(`/suites/${props.suite.key}`) router.push(`/suites/${props.suite.key}`)
} }
const showTooltip = (result, event) => { const handleMouseEnter = (result, event) => {
emit('showTooltip', result, event) emit('showTooltip', result, event, 'hover')
} }
const hideTooltip = (event) => { const handleMouseLeave = (result, event) => {
emit('showTooltip', null, event) emit('showTooltip', null, event, 'hover')
} }
const handleClick = (result, event, index) => {
// Clear selections in other cards first
window.dispatchEvent(new CustomEvent('clear-data-point-selection'))
// Then toggle this card's selection
if (selectedResultIndex.value === index) {
selectedResultIndex.value = null
emit('showTooltip', null, event, 'click')
} else {
selectedResultIndex.value = index
emit('showTooltip', result, event, 'click')
}
}
// Listen for clear selection event
const handleClearSelection = () => {
selectedResultIndex.value = null
}
onMounted(() => {
window.addEventListener('clear-data-point-selection', handleClearSelection)
})
onUnmounted(() => {
window.removeEventListener('clear-data-point-selection', handleClearSelection)
})
</script> </script>
<style scoped> <style scoped>
+119 -59
View File
@@ -1,12 +1,12 @@
<template> <template>
<div <div
id="tooltip" id="tooltip"
ref="tooltip" ref="tooltip"
:class="[ :class="[
'fixed z-50 px-3 py-2 text-sm rounded-md shadow-lg border transition-all duration-200', 'absolute z-50 px-3 py-2 text-sm rounded-md shadow-lg border transition-all duration-200',
'bg-popover text-popover-foreground border-border', 'bg-popover text-popover-foreground border-border',
hidden ? 'invisible opacity-0' : 'visible opacity-100' hidden ? 'invisible opacity-0' : 'visible opacity-100'
]" ]"
:style="`top: ${top}px; left: ${left}px;`" :style="`top: ${top}px; left: ${left}px;`"
> >
<div v-if="result" class="space-y-2"> <div v-if="result" class="space-y-2">
@@ -96,9 +96,12 @@
<script setup> <script setup>
/* eslint-disable no-undef */ /* eslint-disable no-undef */
import { ref, watch, nextTick, computed } from 'vue' import { ref, watch, nextTick, computed, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { prettifyTimestamp } from '@/utils/time' import { prettifyTimestamp } from '@/utils/time'
const route = useRoute()
const props = defineProps({ const props = defineProps({
event: { event: {
type: [Event, Object], type: [Event, Object],
@@ -107,6 +110,10 @@ const props = defineProps({
result: { result: {
type: Object, type: Object,
default: null default: null
},
isPersistent: {
type: Boolean,
default: false
} }
}) })
@@ -115,6 +122,7 @@ const hidden = ref(true)
const top = ref(0) const top = ref(0)
const left = ref(0) const left = ref(0)
const tooltip = ref(null) const tooltip = ref(null)
const targetElement = ref(null)
// Computed properties // Computed properties
const isSuiteResult = computed(() => { const isSuiteResult = computed(() => {
@@ -133,75 +141,109 @@ const successCount = computed(() => {
// Methods are imported from utils/time // Methods are imported from utils/time
// Update tooltip position based on target element's current position
const updatePosition = async () => {
if (!targetElement.value || !tooltip.value || hidden.value) return
await nextTick()
const targetRect = targetElement.value.getBoundingClientRect()
const tooltipRect = tooltip.value.getBoundingClientRect()
// For absolute positioning, we need to add scroll offsets
const scrollTop = window.pageYOffset || document.documentElement.scrollTop
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft
// Default position: below the target (viewport coords + scroll offset)
let newTop = targetRect.bottom + scrollTop + 8
let newLeft = targetRect.left + scrollLeft
// Check if tooltip would overflow the viewport bottom
const spaceBelow = window.innerHeight - targetRect.bottom
const spaceAbove = targetRect.top
if (spaceBelow < tooltipRect.height + 20) {
// Not enough space below, try above
if (spaceAbove > tooltipRect.height + 20) {
// Position above
newTop = targetRect.top + scrollTop - tooltipRect.height - 8
} else {
// Not enough space above either, position at the best spot
if (spaceAbove > spaceBelow) {
// More space above
newTop = scrollTop + 10
} else {
// More space below or equal, keep below but adjust
newTop = scrollTop + window.innerHeight - tooltipRect.height - 10
}
}
}
// Adjust horizontal position if tooltip would overflow right edge
const spaceRight = window.innerWidth - targetRect.left
if (spaceRight < tooltipRect.width + 20) {
// Align right edge of tooltip with right edge of target
newLeft = targetRect.right + scrollLeft - tooltipRect.width
// Make sure it doesn't go off the left edge
if (newLeft < scrollLeft + 10) {
newLeft = scrollLeft + 10
}
}
top.value = Math.round(newTop)
left.value = Math.round(newLeft)
}
const reposition = async () => { const reposition = async () => {
if (!props.event || !props.event.type) return if (!props.event || !props.event.type) return
await nextTick() await nextTick()
if (props.event.type === 'mouseenter' && tooltip.value) { if ((props.event.type === 'mouseenter' || props.event.type === 'click') && tooltip.value) {
const target = props.event.target const target = props.event.target
const targetRect = target.getBoundingClientRect() // Store the target element for scroll updates
targetElement.value = target
// First, position tooltip to get its dimensions
// First, make tooltip visible to get its dimensions
hidden.value = false hidden.value = false
await nextTick() await nextTick()
const tooltipRect = tooltip.value.getBoundingClientRect() // Update position
await updatePosition()
// Since tooltip uses position: fixed, we work with viewport coordinates
// getBoundingClientRect() already gives us viewport-relative positions
// Default position: below the target
let newTop = targetRect.bottom + 8
let newLeft = targetRect.left
// Check if tooltip would overflow the viewport bottom
const spaceBelow = window.innerHeight - targetRect.bottom
const spaceAbove = targetRect.top
if (spaceBelow < tooltipRect.height + 20) {
// Not enough space below, try above
if (spaceAbove > tooltipRect.height + 20) {
// Position above
newTop = targetRect.top - tooltipRect.height - 8
} else {
// Not enough space above either, position at the best spot
if (spaceAbove > spaceBelow) {
// More space above
newTop = 10
} else {
// More space below or equal, keep below but adjust
newTop = window.innerHeight - tooltipRect.height - 10
}
}
}
// Adjust horizontal position if tooltip would overflow right edge
const spaceRight = window.innerWidth - targetRect.left
if (spaceRight < tooltipRect.width + 20) {
// Align right edge of tooltip with right edge of target
newLeft = targetRect.right - tooltipRect.width
// Make sure it doesn't go off the left edge
if (newLeft < 10) {
newLeft = 10
}
}
top.value = Math.round(newTop)
left.value = Math.round(newLeft)
} else if (props.event.type === 'mouseleave') { } else if (props.event.type === 'mouseleave') {
hidden.value = true // Only hide on mouseleave if not in persistent mode
if (!props.isPersistent) {
hidden.value = true
targetElement.value = null
}
} }
} }
// Handle resize events (still needed for viewport size changes)
const handleResize = () => {
updatePosition()
}
// Lifecycle hooks
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
// Watchers // Watchers
watch(() => props.event, (newEvent) => { watch(() => props.event, (newEvent) => {
if (newEvent && newEvent.type) { if (newEvent && newEvent.type) {
if (newEvent.type === 'mouseenter') { if (newEvent.type === 'mouseenter' || newEvent.type === 'click') {
hidden.value = false hidden.value = false
nextTick(() => reposition()) nextTick(() => reposition())
} else if (newEvent.type === 'mouseleave') { } else if (newEvent.type === 'mouseleave') {
hidden.value = true // Only hide on mouseleave if not in persistent mode
if (!props.isPersistent) {
hidden.value = true
}
} }
} }
}, { immediate: true }) }, { immediate: true })
@@ -211,4 +253,22 @@ watch(() => props.result, () => {
nextTick(() => reposition()) nextTick(() => reposition())
} }
}) })
// Watch for persistent state changes and result changes
watch(() => [props.isPersistent, props.result], ([isPersistent, result]) => {
if (!isPersistent && !result) {
// Hide tooltip when both persistent mode is off and no result
hidden.value = true
} else if (result && (isPersistent || props.event?.type === 'mouseenter')) {
// Show tooltip when there's a result and either persistent or hovering
hidden.value = false
nextTick(() => reposition())
}
})
// Watch for route changes and hide tooltip
watch(() => route.path, () => {
hidden.value = true
targetElement.value = null
})
</script> </script>
+2 -2
View File
@@ -369,8 +369,8 @@ const changePage = (page) => {
fetchData() fetchData()
} }
const showTooltip = (result, event) => { const showTooltip = (result, event, action = 'hover') => {
emit('showTooltip', result, event) emit('showTooltip', result, event, action)
} }
const prettifyTimestamp = (timestamp) => { const prettifyTimestamp = (timestamp) => {
+2 -2
View File
@@ -471,8 +471,8 @@ const toggleShowAverageResponseTime = () => {
showAverageResponseTime.value = !showAverageResponseTime.value showAverageResponseTime.value = !showAverageResponseTime.value
} }
const showTooltip = (result, event) => { const showTooltip = (result, event, action = 'hover') => {
emit('showTooltip', result, event) emit('showTooltip', result, event, action)
} }
const calculateUnhealthyCount = (endpoints) => { const calculateUnhealthyCount = (endpoints) => {
+21 -9
View File
@@ -14,7 +14,7 @@
<p class="text-muted-foreground mt-2"> <p class="text-muted-foreground mt-2">
<span v-if="suite?.group">{{ suite.group }} • </span> <span v-if="suite?.group">{{ suite.group }} • </span>
<span v-if="latestResult"> <span v-if="latestResult">
{{ selectedResult && selectedResult !== sortedResults[0] ? 'Ran' : 'Last run' }} {{ formatRelativeTime(latestResult.timestamp) }} {{ selectedResult && selectedResult.timestamp !== sortedResults[0]?.timestamp ? 'Ran' : 'Last run' }} {{ formatRelativeTime(latestResult.timestamp) }}
</span> </span>
</p> </p>
</div> </div>
@@ -41,7 +41,7 @@
<!-- Latest Execution --> <!-- Latest Execution -->
<Card v-if="latestResult"> <Card v-if="latestResult">
<CardHeader> <CardHeader>
<CardTitle>Latest Execution</CardTitle> <CardTitle>{{ selectedResult?.timestamp === sortedResults[0]?.timestamp ? 'Latest Execution' : `Execution at ${formatTimestamp(selectedResult.timestamp)}` }}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div class="space-y-4"> <div class="space-y-4">
@@ -107,7 +107,7 @@
:key="index" :key="index"
class="flex items-center justify-between p-3 border rounded-lg hover:bg-accent/50 transition-colors cursor-pointer" class="flex items-center justify-between p-3 border rounded-lg hover:bg-accent/50 transition-colors cursor-pointer"
@click="selectedResult = result" @click="selectedResult = result"
:class="{ 'bg-accent': selectedResult === result }" :class="{ 'bg-accent': selectedResult && selectedResult.timestamp === result.timestamp }"
> >
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<StatusBadge :status="result.success ? 'healthy' : 'unhealthy'" size="sm" /> <StatusBadge :status="result.success ? 'healthy' : 'unhealthy'" size="sm" />
@@ -184,20 +184,30 @@ const latestResult = computed(() => {
// Methods // Methods
const fetchData = async () => { const fetchData = async () => {
loading.value = true // Don't show loading state on refresh to prevent UI flicker
const isInitialLoad = !suite.value
if (isInitialLoad) {
loading.value = true
}
try { try {
const response = await fetch(`${SERVER_URL}/api/v1/suites/${route.params.key}/statuses`, { const response = await fetch(`${SERVER_URL}/api/v1/suites/${route.params.key}/statuses`, {
credentials: 'include' credentials: 'include'
}) })
if (response.status === 200) { if (response.status === 200) {
const data = await response.json() const data = await response.json()
const oldSuite = suite.value
suite.value = data suite.value = data
if (data.results && data.results.length > 0 && !selectedResult.value) { if (data.results && data.results.length > 0) {
// Sort results by timestamp to get the most recent one // Sort results by timestamp to get the most recent one
const sorted = [...data.results].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)) const sorted = [...data.results].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
selectedResult.value = sorted[0] // Update selectedResult if: no result selected, or currently viewing the latest result
const wasViewingLatest = !selectedResult.value ||
(oldSuite?.results && selectedResult.value.timestamp === [...oldSuite.results].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))[0]?.timestamp)
if (wasViewingLatest) {
selectedResult.value = sorted[0]
}
} }
} else if (response.status === 404) { } else if (response.status === 404) {
suite.value = null suite.value = null
@@ -207,7 +217,9 @@ const fetchData = async () => {
} catch (error) { } catch (error) {
console.error('[SuiteDetails][fetchData] Error:', error) console.error('[SuiteDetails][fetchData] Error:', error)
} finally { } finally {
loading.value = false if (isInitialLoad) {
loading.value = false
}
} }
} }
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long