diff --git a/.air.toml b/.air.toml index 34b3c928..56844df4 100644 --- a/.air.toml +++ b/.air.toml @@ -1,16 +1,16 @@ -# Docs for .air.toml -# https://github.com/cosmtrek/air/blob/master/air_example.toml - -root = "." - -[build] -cmd = "task build" -full_bin = "task serve" -delay = 100 -exclude_dir = ["tmp", "dist", "internal/database/dbgen"] -exclude_file = ["internal/view/static/css/style.css"] -exclude_regex = ["_test.go", "_generated.go", ".sql.go", ".gen.go"] -include_ext = ["go", "sql", "js", "css", "json"] - -[log] -main_only = true +# Docs for .air.toml +# https://github.com/cosmtrek/air/blob/master/air_example.toml + +root = "." + +[build] +cmd = "task build" +full_bin = "task serve" +delay = 100 +exclude_dir = ["tmp", "dist", "internal/database/dbgen"] +exclude_file = ["internal/view/static/css/style.css"] +exclude_regex = ["_test.go", "_generated.go", ".sql.go", ".gen.go"] +include_ext = ["go", "sql", "js", "css", "json"] + +[log] +main_only = true diff --git a/.env.dev b/.env.dev index 37d7ced2..dcbe8622 100644 --- a/.env.dev +++ b/.env.dev @@ -1,14 +1,14 @@ -############################################################################### -# ⚠️ Do not use this file in production. This is only for development purposes. -############################################################################### - -# This values are configured by default in the compose.yaml file, so you can -# copy this file to .env and use it as is (only for development). - -# Encryption key is used to encrypt and decrypt the sensitive data stored -# in the database such as database credentials, secret keys, etc. -PBW_ENCRYPTION_KEY="encryption-key" - -# Database connection string for a PostgreSQL database where the pgbackweb -# will store its data. -PBW_POSTGRES_CONN_STRING="postgresql://postgres:password@host.docker.internal:5432/pgbackweb?sslmode=disable" +############################################################################### +# ⚠️ Do not use this file in production. This is only for development purposes. +############################################################################### + +# This values are configured by default in the compose.yaml file, so you can +# copy this file to .env and use it as is (only for development). + +# Encryption key is used to encrypt and decrypt the sensitive data stored +# in the database such as database credentials, secret keys, etc. +PBW_ENCRYPTION_KEY="encryption-key" + +# Database connection string for a PostgreSQL database where the pgbackweb +# will store its data. +PBW_POSTGRES_CONN_STRING="postgresql://postgres:password@host.docker.internal:5432/pgbackweb?sslmode=disable" diff --git a/.env.example b/.env.example index 32689436..bb311f77 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ -# Encryption key is used to encrypt and decrypt the sensitive data stored -# in the database such as database credentials, secret keys, etc. -PBW_ENCRYPTION_KEY="" - -# Database connection string for a PostgreSQL database where the pgbackweb -# will store its data. -PBW_POSTGRES_CONN_STRING="" +# Encryption key is used to encrypt and decrypt the sensitive data stored +# in the database such as database credentials, secret keys, etc. +PBW_ENCRYPTION_KEY="" + +# Database connection string for a PostgreSQL database where the pgbackweb +# will store its data. +PBW_POSTGRES_CONN_STRING="" diff --git a/.github/workflows/lint-test-build.yaml b/.github/workflows/lint-test-build.yaml index 479975ee..f89b9ad1 100644 --- a/.github/workflows/lint-test-build.yaml +++ b/.github/workflows/lint-test-build.yaml @@ -1,29 +1,29 @@ -name: Lint, test & build - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - run_all: - name: Run all - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Build docker image - run: docker build -t pbw-test -f ./docker/Dockerfile.cicd . - - - name: Run linter - run: docker run --rm pbw-test task lint-only - - - name: Run tests - run: docker run --rm pbw-test task test-only - - - name: Run build - run: docker run --rm pbw-test task build +name: Lint, test & build + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + run_all: + name: Run all + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Build docker image + run: docker build -t pbw-test -f ./docker/Dockerfile.cicd . + + - name: Run linter + run: docker run --rm pbw-test task lint-only + + - name: Run tests + run: docker run --rm pbw-test task test-only + + - name: Run build + run: docker run --rm pbw-test task build diff --git a/.github/workflows/sync-docker-hub-readme.yaml b/.github/workflows/sync-docker-hub-readme.yaml index b6241d0f..a3866496 100644 --- a/.github/workflows/sync-docker-hub-readme.yaml +++ b/.github/workflows/sync-docker-hub-readme.yaml @@ -1,24 +1,24 @@ -name: Sync Docker Hub README - -on: - push: - branches: - - main - paths: - - README.md - workflow_dispatch: - -jobs: - run_sync: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Sync Docker Hub README - uses: ms-jpq/sync-dockerhub-readme@v1 - with: - username: ${{ secrets.DOCKERHUB_USER }} - password: ${{ secrets.DOCKERHUB_PASS }} - repository: eduardolat/pgbackweb - readme: "./README.md" +name: Sync Docker Hub README + +on: + push: + branches: + - main + paths: + - README.md + workflow_dispatch: + +jobs: + run_sync: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Sync Docker Hub README + uses: ms-jpq/sync-dockerhub-readme@v1 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_PASS }} + repository: eduardolat/pgbackweb + readme: "./README.md" diff --git a/.gitignore b/.gitignore index 35fbd590..ae20df57 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,31 @@ -# Environment -.env -.env.* -!.env.dev -!.env.example - -# NodeJS -node_modules/ - -# Distribution folder -dist/ -.idea/ - -# Temp files/folders -tmp/ - -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Go workspace file -go.work -go.work.sum \ No newline at end of file +# Environment +!.env.dev +!.env.example + +# NodeJS +node_modules/ + +# Distribution folder +dist/ +.idea/ + +# Temp files/folders +tmp/ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Go workspace file +go.work +go.work.sum +config.bat diff --git a/.golangci.yaml b/.golangci.yaml index 2a2b2b43..ec7c8884 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,12 +1,12 @@ -run: - timeout: "5m" - -linters: - disable-all: true - enable: - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck - - unused +run: + timeout: "5m" + +linters: + disable-all: true + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 99b38527..c115617c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,7 @@ -{ - "recommendations": [ - "golang.go", - "bradlc.vscode-tailwindcss", - "standard.vscode-standard" - ] +{ + "recommendations": [ + "golang.go", + "bradlc.vscode-tailwindcss", + "standard.vscode-standard" + ] } \ No newline at end of file diff --git a/.vscode/project.code-snippets b/.vscode/project.code-snippets index 17a2898b..d1ef2b33 100644 --- a/.vscode/project.code-snippets +++ b/.vscode/project.code-snippets @@ -1,34 +1,34 @@ -{ - "If error return error": { - "prefix": "rerr", - "body": [ - "if err != nil {", - "\treturn err", - "}", - "" - ], - "description": "Inserts code to check for error and return it" - }, - "Get request context": { - "prefix": "grcr", // Get Request Context Request - "body": [ - "ctx := c.Request().Context()" - ], - "description": "Get's the request from echo context" - }, - "Get request echo context": { - "prefix": "grce", // Get Request Context Echo - "body": [ - "reqCtx := reqctx.GetCtx(c)" - ], - "description": "Inserts code to get echo context from a request" - }, - "Get both request and echo context": { - "prefix": "grcb", // Get Request Context Both - "body": [ - "ctx := c.Request().Context()", - "reqCtx := reqctx.GetCtx(c)" - ], - "description": "Inserts code to get both request and echo context" - }, +{ + "If error return error": { + "prefix": "rerr", + "body": [ + "if err != nil {", + "\treturn err", + "}", + "" + ], + "description": "Inserts code to check for error and return it" + }, + "Get request context": { + "prefix": "grcr", // Get Request Context Request + "body": [ + "ctx := c.Request().Context()" + ], + "description": "Get's the request from echo context" + }, + "Get request echo context": { + "prefix": "grce", // Get Request Context Echo + "body": [ + "reqCtx := reqctx.GetCtx(c)" + ], + "description": "Inserts code to get echo context from a request" + }, + "Get both request and echo context": { + "prefix": "grcb", // Get Request Context Both + "body": [ + "ctx := c.Request().Context()", + "reqCtx := reqctx.GetCtx(c)" + ], + "description": "Inserts code to get both request and echo context" + }, } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 2db35ef6..8f0eb1c6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,69 +1,69 @@ -{ - ///// - // Editor settings - ///// - "editor.formatOnSave": true, - "editor.foldingStrategy": "indentation", - "editor.rulers": [ - 80 - ], - "files.eol": "\n", - ///// - // Language specific settings - ///// - "[json]": { - "editor.defaultFormatter": "vscode.json-language-features" - }, - "[jsonc]": { - "editor.defaultFormatter": "vscode.json-language-features" - }, - // JavaScript & Standard - "standard.enable": true, - "standard.autoFixOnSave": true, - "[javascript]": { - "editor.defaultFormatter": "standard.vscode-standard" - }, - "[css]": { - "editor.defaultFormatter": "vscode.css-language-features" - }, - "[html]": { - "editor.defaultFormatter": "vscode.html-language-features" - }, - "[markdown]": { - "editor.defaultFormatter": "vscode.markdown-language-features" - }, - "[go]": { - "editor.defaultFormatter": "golang.go" - }, - ///// - // Tailwind CSS + Gomponents - // https://gist.github.com/eduardolat/438a1de077ccac6b9792153e708c1824 - ///// - "tailwindCSS.includeLanguages": { - "go": "go", - }, - "tailwindCSS.experimental.classRegex": [ - [ - "Class\\(([^)]*)\\)", - "[\"`]([^\"`]*)[\"`]" - ], // Class("...") or Class(`...`) - [ - "Classes\\(([^)]*)\\)", - "[\"`]([^\"`]*)[\"`]" - ], // Classes("...") or Classes(`...`) - [ - "Class\\{([^)]*)\\}", - "[\"`]([^\"`]*)[\"`]" - ], // Class{"..."} or Class{`...`} - [ - "Classes\\{([^)]*)\\}", - "[\"`]([^\"`]*)[\"`]" - ], // Classes{"..."} or Classes{`...`} - [ - "Class:\\s*[\"`]([^\"`]*)[\"`]" - ], // Class: "..." or Class: `...` - [ - "Classes:\\s*[\"`]([^\"`]*)[\"`]" - ], // Classes: "..." or Classes: `...` - ] +{ + ///// + // Editor settings + ///// + "editor.formatOnSave": true, + "editor.foldingStrategy": "indentation", + "editor.rulers": [ + 80 + ], + "files.eol": "\n", + ///// + // Language specific settings + ///// + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + // JavaScript & Standard + "standard.enable": true, + "standard.autoFixOnSave": true, + "[javascript]": { + "editor.defaultFormatter": "standard.vscode-standard" + }, + "[css]": { + "editor.defaultFormatter": "vscode.css-language-features" + }, + "[html]": { + "editor.defaultFormatter": "vscode.html-language-features" + }, + "[markdown]": { + "editor.defaultFormatter": "vscode.markdown-language-features" + }, + "[go]": { + "editor.defaultFormatter": "golang.go" + }, + ///// + // Tailwind CSS + Gomponents + // https://gist.github.com/eduardolat/438a1de077ccac6b9792153e708c1824 + ///// + "tailwindCSS.includeLanguages": { + "go": "go", + }, + "tailwindCSS.experimental.classRegex": [ + [ + "Class\\(([^)]*)\\)", + "[\"`]([^\"`]*)[\"`]" + ], // Class("...") or Class(`...`) + [ + "Classes\\(([^)]*)\\)", + "[\"`]([^\"`]*)[\"`]" + ], // Classes("...") or Classes(`...`) + [ + "Class\\{([^)]*)\\}", + "[\"`]([^\"`]*)[\"`]" + ], // Class{"..."} or Class{`...`} + [ + "Classes\\{([^)]*)\\}", + "[\"`]([^\"`]*)[\"`]" + ], // Classes{"..."} or Classes{`...`} + [ + "Class:\\s*[\"`]([^\"`]*)[\"`]" + ], // Class: "..." or Class: `...` + [ + "Classes:\\s*[\"`]([^\"`]*)[\"`]" + ], // Classes: "..." or Classes: `...` + ] } \ No newline at end of file diff --git a/LICENSE b/LICENSE index 05520d99..8006f728 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2024 Luis Eduardo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2024 Luis Eduardo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 26eae564..a9be5e7b 100644 --- a/README.md +++ b/README.md @@ -1,134 +1,134 @@ -

-

PG Back Web

-

- -

-

- 🐘 Effortless PostgreSQL backups with a user-friendly web interface! 🌐💾 -

-

-

- - Tests Status - - - Go Report Card - - - Release Version - - - Docker Pulls - - - License - - - - -

- -## Why PG Back Web? - -PG Back Web isn't just another backup tool. It's your trusted ally in ensuring the security and availability of your PostgreSQL data: - -- 🎯 **Designed for everyone**: From individual developers to teams. -- ⏱️ **Save time**: Automate your backups and forget about manual tasks. -- ⚡ **Plug and play**: Don't waste time with complex configurations. - -## Features - -- 📦 **Intuitive web interface**: Manage your backups with ease, no database expertise required. -- 📅 **Scheduled backups**: Set it and forget it. PG Back Web takes care of the rest. -- 📈 **Backup monitoring**: Visualize the status of your backups with execution logs. -- 📤 **Instant download & restore**: Restore and download your backups when you need them, directly from the web interface. -- 🖥 **Multi-version support**: Compatible with PostgreSQL 13, 14, 15, and 16. -- 📁 **Local & S3 storage**: Store backups locally or add as many S3 buckets as you want for greater flexibility. -- 🔒 **Security first**: PGP encryption to protect your sensitive information. -- 🛡️ **Open-source trust**: Open-source code under MIT license, backed by the robust pg_dump tool. -- 🌚 **Dark mode**: Because we all love dark mode. - -## Installation - -PG Back Web is available as a Docker image. You just need to set 3 environment variables and you're good to go! - -Here's an example of how you can run PG Back Web with Docker Compose, feel free to adapt it to your needs: - -```yaml -services: - pgbackweb: - image: eduardolat/pgbackweb:latest - ports: - - "8085:8085" # Access the web interface at http://localhost:8085 - volumes: - - ./backups:/backups # If you only use S3 destinations, you don't need this volume - environment: - PBW_ENCRYPTION_KEY: "my_secret_key" # Change this to a strong key - PBW_POSTGRES_CONN_STRING: "postgresql://postgres:password@postgres:5432/pgbackweb?sslmode=disable" - TZ: "America/Guatemala" # Set your timezone, optional - depends_on: - postgres: - condition: service_healthy - - postgres: - image: postgres:16 - environment: - POSTGRES_USER: postgres - POSTGRES_DB: pgbackweb - POSTGRES_PASSWORD: password - ports: - - "5432:5432" - volumes: - - ./data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 5s - timeout: 5s - retries: 5 -``` - -## Configuration - -You only need to configure the following environment variables: - -- `PBW_ENCRYPTION_KEY`: Your encryption key. Generate a strong one and store it in a safe place, as PG Back Web uses it to encrypt sensitive data. - -- `PBW_POSTGRES_CONN_STRING`: The connection string for the PostgreSQL database that will store PG Back Web data. - -- `TZ`: Your [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) (optional). Default is `UTC`. This impacts logging, backup filenames and default timezone in the web interface. - -## Screenshots - - - - - -## Reset password - -You can reset your PG Back Web password by running the following command in the server where PG Back Web is running: - -```bash -docker exec -it sh -c change-password -``` - -You should replace `` with the name or ID of the PG Back Web container, then just follow the instructions. - -## Next steps - -In this link you can see a list of features that have been confirmed for future updates: - - - Next steps ⏭️ - - -## Join the Community - -Got ideas to improve PG Back Web? Contribute to the project! Every suggestion and pull request is welcome. - -## License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - ---- - +

+

PG Back Web

+

+ +

+

+ 🐘 Effortless PostgreSQL backups with a user-friendly web interface! 🌐💾 +

+

+

+ + Tests Status + + + Go Report Card + + + Release Version + + + Docker Pulls + + + License + + + + +

+ +## Why PG Back Web? + +PG Back Web isn't just another backup tool. It's your trusted ally in ensuring the security and availability of your PostgreSQL data: + +- 🎯 **Designed for everyone**: From individual developers to teams. +- ⏱️ **Save time**: Automate your backups and forget about manual tasks. +- ⚡ **Plug and play**: Don't waste time with complex configurations. + +## Features + +- 📦 **Intuitive web interface**: Manage your backups with ease, no database expertise required. +- 📅 **Scheduled backups**: Set it and forget it. PG Back Web takes care of the rest. +- 📈 **Backup monitoring**: Visualize the status of your backups with execution logs. +- 📤 **Instant download & restore**: Restore and download your backups when you need them, directly from the web interface. +- 🖥 **Multi-version support**: Compatible with PostgreSQL 13, 14, 15, and 16. +- 📁 **Local & S3 storage**: Store backups locally or add as many S3 buckets as you want for greater flexibility. +- 🔒 **Security first**: PGP encryption to protect your sensitive information. +- 🛡️ **Open-source trust**: Open-source code under MIT license, backed by the robust pg_dump tool. +- 🌚 **Dark mode**: Because we all love dark mode. + +## Installation + +PG Back Web is available as a Docker image. You just need to set 3 environment variables and you're good to go! + +Here's an example of how you can run PG Back Web with Docker Compose, feel free to adapt it to your needs: + +```yaml +services: + pgbackweb: + image: eduardolat/pgbackweb:latest + ports: + - "8085:8085" # Access the web interface at http://localhost:8085 + volumes: + - ./backups:/backups # If you only use S3 destinations, you don't need this volume + environment: + PBW_ENCRYPTION_KEY: "my_secret_key" # Change this to a strong key + PBW_POSTGRES_CONN_STRING: "postgresql://postgres:password@postgres:5432/pgbackweb?sslmode=disable" + TZ: "America/Guatemala" # Set your timezone, optional + depends_on: + postgres: + condition: service_healthy + + postgres: + image: postgres:16 + environment: + POSTGRES_USER: postgres + POSTGRES_DB: pgbackweb + POSTGRES_PASSWORD: password + ports: + - "5432:5432" + volumes: + - ./data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 +``` + +## Configuration + +You only need to configure the following environment variables: + +- `PBW_ENCRYPTION_KEY`: Your encryption key. Generate a strong one and store it in a safe place, as PG Back Web uses it to encrypt sensitive data. + +- `PBW_POSTGRES_CONN_STRING`: The connection string for the PostgreSQL database that will store PG Back Web data. + +- `TZ`: Your [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) (optional). Default is `UTC`. This impacts logging, backup filenames and default timezone in the web interface. + +## Screenshots + + + + + +## Reset password + +You can reset your PG Back Web password by running the following command in the server where PG Back Web is running: + +```bash +docker exec -it sh -c change-password +``` + +You should replace `` with the name or ID of the PG Back Web container, then just follow the instructions. + +## Next steps + +In this link you can see a list of features that have been confirmed for future updates: + + + Next steps ⏭️ + + +## Join the Community + +Got ideas to improve PG Back Web? Contribute to the project! Every suggestion and pull request is welcome. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +--- + 💖 **Love PG Back Web?** Give us a ⭐ on GitHub and share the project with your colleagues. Together, we can make PostgreSQL backups more accessible to everyone! \ No newline at end of file diff --git a/chart/pgbackweb/.helmignore b/chart/pgbackweb/.helmignore new file mode 100644 index 00000000..f82e96d4 --- /dev/null +++ b/chart/pgbackweb/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/chart/pgbackweb/Chart.yaml b/chart/pgbackweb/Chart.yaml new file mode 100644 index 00000000..f0d62f2a --- /dev/null +++ b/chart/pgbackweb/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: pgbackweb +description: A Helm chart for Kubernetes +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.0" diff --git a/chart/pgbackweb/templates/_helpers.tpl b/chart/pgbackweb/templates/_helpers.tpl new file mode 100644 index 00000000..d2596f92 --- /dev/null +++ b/chart/pgbackweb/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "pgbackweb.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "pgbackweb.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "pgbackweb.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "pgbackweb.labels" -}} +helm.sh/chart: {{ include "pgbackweb.chart" . }} +{{ include "pgbackweb.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "pgbackweb.selectorLabels" -}} +app.kubernetes.io/name: {{ include "pgbackweb.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "pgbackweb.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "pgbackweb.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/chart/pgbackweb/templates/config.yaml b/chart/pgbackweb/templates/config.yaml new file mode 100644 index 00000000..eed475eb --- /dev/null +++ b/chart/pgbackweb/templates/config.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "pgbackweb.fullname" . }}-config + labels: + {{- include "pgbackweb.labels" . | nindent 4 }} +data: + PBW_ENCRYPTION_KEY: {{ .Values.config.pbwEncryptionKey | quote }} + TZ: {{ .Values.config.tz | quote }} + PBW_POSTGRES_CONN_STRING: {{ .Values.config.pbwPostgresConnectionString | quote }} \ No newline at end of file diff --git a/chart/pgbackweb/templates/deployment.yaml b/chart/pgbackweb/templates/deployment.yaml new file mode 100644 index 00000000..06b1db4d --- /dev/null +++ b/chart/pgbackweb/templates/deployment.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "pgbackweb.fullname" . }}-pgbackweb + labels: + {{- include "pgbackweb.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.pgbackweb.replicas }} + selector: + matchLabels: + app: pgbackweb + {{- include "pgbackweb.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + app: pgbackweb + {{- include "pgbackweb.selectorLabels" . | nindent 8 }} + spec: + containers: + - env: + - name: PBW_ENCRYPTION_KEY + valueFrom: + configMapKeyRef: + key: PBW_ENCRYPTION_KEY + name: {{ include "pgbackweb.fullname" . }}-config + - name: PBW_POSTGRES_CONN_STRING + valueFrom: + configMapKeyRef: + key: PBW_POSTGRES_CONN_STRING + name: {{ include "pgbackweb.fullname" . }}-config + - name: TZ + valueFrom: + configMapKeyRef: + key: TZ + name: {{ include "pgbackweb.fullname" . }}-config + - name: KUBERNETES_CLUSTER_DOMAIN + value: {{ quote .Values.kubernetesClusterDomain }} + image: {{ .Values.pgbackweb.pgbackweb.image.repository }}:{{ .Values.pgbackweb.pgbackweb.image.tag + | default .Chart.AppVersion }} + name: pgbackweb + ports: + - containerPort: 8085 + resources: {} \ No newline at end of file diff --git a/chart/pgbackweb/templates/ingress.yaml b/chart/pgbackweb/templates/ingress.yaml new file mode 100644 index 00000000..7e7986c2 --- /dev/null +++ b/chart/pgbackweb/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := "pgbackweb-service" -}} +{{- $ingressPathType := .Values.ingress.pathType -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "pgbackweb.fullname" . }}-ingress + labels: + {{- include "pgbackweb.labels" . | nindent 4 }} + annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" +spec: +{{- if .Values.ingress.ingressClassName }} + ingressClassName: {{ .Values.ingress.ingressClassName | quote }} +{{- end }} +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . }} + pathType: {{ $ingressPathType }} + backend: + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- end }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/chart/pgbackweb/templates/service.yaml b/chart/pgbackweb/templates/service.yaml new file mode 100644 index 00000000..9f8fe12a --- /dev/null +++ b/chart/pgbackweb/templates/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "pgbackweb.fullname" . }}-service + labels: + {{- include "pgbackweb.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + selector: + app: pgbackweb + {{- include "pgbackweb.selectorLabels" . | nindent 4 }} + ports: + {{- .Values.service.ports | toYaml | nindent 2 }} \ No newline at end of file diff --git a/chart/pgbackweb/values.yaml b/chart/pgbackweb/values.yaml new file mode 100644 index 00000000..dc02ca05 --- /dev/null +++ b/chart/pgbackweb/values.yaml @@ -0,0 +1,31 @@ +config: + pbwEncryptionKey: my_secret_key + tz: America/Guatemala + pbwPostgresConnectionString: postgresql://USERNAME:PASSWORD@HOST:5432/DB_NAME?sslmode=disable +kubernetesClusterDomain: cluster.local +pgbackweb: + pgbackweb: + image: + repository: eduardolat/pgbackweb + tag: latest + replicas: 1 +ingress: + ingressClassName: nginx + annotations: + kubernetes.io/tls-acme: "true" + enabled: true + hosts: + - host: pgbackweb.example.com + paths: + - / + pathType: Prefix + tls: + - hosts: + - pgbackweb.example.com + secretName: pgbackweb.example.com-tls +service: + port: 8085 + ports: + - port: 8085 + targetPort: 8085 + type: ClusterIP diff --git a/cmd/app/main.go b/cmd/app/main.go index 3db3a789..00105fc2 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -1,50 +1,50 @@ -package main - -import ( - "github.com/eduardolat/pgbackweb/internal/config" - "github.com/eduardolat/pgbackweb/internal/cron" - "github.com/eduardolat/pgbackweb/internal/database" - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/eduardolat/pgbackweb/internal/integration" - "github.com/eduardolat/pgbackweb/internal/logger" - "github.com/eduardolat/pgbackweb/internal/service" - "github.com/eduardolat/pgbackweb/internal/view" - "github.com/labstack/echo/v4" -) - -func main() { - env := config.GetEnv() - - cr, err := cron.New() - if err != nil { - logger.FatalError("error initializing cron scheduler", logger.KV{"error": err}) - } - cr.Start() - defer func() { - if err := cr.Shutdown(); err != nil { - logger.Error("error shutting down cron scheduler", logger.KV{"error": err}) - } - }() - - db := database.Connect(env) - defer func() { - if err := db.Close(); err != nil { - logger.Error("error closing database connection", logger.KV{"error": err}) - } - }() - dbgen := dbgen.New(db) - - ints := integration.New() - servs := service.New(env, dbgen, cr, ints) - initSchedule(cr, servs) - - app := echo.New() - app.HideBanner = true - app.HidePort = true - view.MountRouter(app, servs) - - logger.Info("server started at http://localhost:8085") - if err := app.Start(":8085"); err != nil { - logger.FatalError("error starting server", logger.KV{"error": err}) - } -} +package main + +import ( + "github.com/eduardolat/pgbackweb/internal/config" + "github.com/eduardolat/pgbackweb/internal/cron" + "github.com/eduardolat/pgbackweb/internal/database" + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/integration" + "github.com/eduardolat/pgbackweb/internal/logger" + "github.com/eduardolat/pgbackweb/internal/service" + "github.com/eduardolat/pgbackweb/internal/view" + "github.com/labstack/echo/v4" +) + +func main() { + env := config.GetEnv() + + cr, err := cron.New() + if err != nil { + logger.FatalError("error initializing cron scheduler", logger.KV{"error": err}) + } + cr.Start() + defer func() { + if err := cr.Shutdown(); err != nil { + logger.Error("error shutting down cron scheduler", logger.KV{"error": err}) + } + }() + + db := database.Connect(env) + defer func() { + if err := db.Close(); err != nil { + logger.Error("error closing database connection", logger.KV{"error": err}) + } + }() + dbgen := dbgen.New(db) + + ints := integration.New() + servs := service.New(env, dbgen, cr, ints) + initSchedule(cr, servs) + + app := echo.New() + app.HideBanner = true + app.HidePort = true + view.MountRouter(app, servs) + + logger.Info("server started at http://localhost:8085") + if err := app.Start(":8085"); err != nil { + logger.FatalError("error starting server", logger.KV{"error": err}) + } +} diff --git a/cmd/changepw/main.go b/cmd/changepw/main.go index 7a43214a..b88c5332 100644 --- a/cmd/changepw/main.go +++ b/cmd/changepw/main.go @@ -1,73 +1,73 @@ -package main - -import ( - "context" - "database/sql" - "errors" - "fmt" - - "github.com/eduardolat/pgbackweb/internal/config" - "github.com/eduardolat/pgbackweb/internal/database" - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/eduardolat/pgbackweb/internal/util/cryptoutil" - "github.com/google/uuid" -) - -func main() { - env := config.GetEnv() - - db := database.Connect(env) - defer db.Close() - dbg := dbgen.New(db) - - fmt.Println() - fmt.Println() - fmt.Println("PG Back Web - Password Reset") - fmt.Println("---") - fmt.Print("User email: ") - var userID uuid.UUID - - for { - var email string - if _, err := fmt.Scanln(&email); err != nil { - panic(err) - } - - user, err := dbg.UsersServiceGetUserByEmail( - context.Background(), email, - ) - if err != nil && errors.Is(err, sql.ErrNoRows) { - fmt.Print("User not found. Enter new email: ") - continue - } - if err != nil { - panic(err) - } - - userID = user.ID - break - } - - newPassword := uuid.NewString() - hashedPassword, err := cryptoutil.CreateBcryptHash(newPassword) - if err != nil { - panic(err) - } - - err = dbg.UsersServiceChangePassword( - context.Background(), dbgen.UsersServiceChangePasswordParams{ - ID: userID, - Password: hashedPassword, - }, - ) - if err != nil { - panic(err) - } - - fmt.Println() - fmt.Println("Password reset successfully") - fmt.Println("New password: ", newPassword) - fmt.Println() - fmt.Println("You can change your password after login") - fmt.Println() -} +package main + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "github.com/eduardolat/pgbackweb/internal/config" + "github.com/eduardolat/pgbackweb/internal/database" + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/cryptoutil" + "github.com/google/uuid" +) + +func main() { + env := config.GetEnv() + + db := database.Connect(env) + defer db.Close() + dbg := dbgen.New(db) + + fmt.Println() + fmt.Println() + fmt.Println("PG Back Web - Password Reset") + fmt.Println("---") + fmt.Print("User email: ") + var userID uuid.UUID + + for { + var email string + if _, err := fmt.Scanln(&email); err != nil { + panic(err) + } + + user, err := dbg.UsersServiceGetUserByEmail( + context.Background(), email, + ) + if err != nil && errors.Is(err, sql.ErrNoRows) { + fmt.Print("User not found. Enter new email: ") + continue + } + if err != nil { + panic(err) + } + + userID = user.ID + break + } + + newPassword := uuid.NewString() + hashedPassword, err := cryptoutil.CreateBcryptHash(newPassword) + if err != nil { + panic(err) + } + + err = dbg.UsersServiceChangePassword( + context.Background(), dbgen.UsersServiceChangePasswordParams{ + ID: userID, + Password: hashedPassword, + }, + ) + if err != nil { + panic(err) + } + + fmt.Println() + fmt.Println("Password reset successfully") + fmt.Println("New password: ", newPassword) + fmt.Println() + fmt.Println("You can change your password after login") + fmt.Println() +} diff --git a/cmd/goose/main.go b/cmd/goose/main.go index 63c576fa..dd36ae24 100644 --- a/cmd/goose/main.go +++ b/cmd/goose/main.go @@ -1,37 +1,37 @@ -package main - -import ( - "fmt" - "os" - - "github.com/eduardolat/pgbackweb/internal/config" -) - -const migrationsFolder string = "./internal/database/migrations" - -func main() { - if len(os.Args) < 2 { - fmt.Println("goose command is required") - fmt.Println("example: task goose -- up") - return - } - - gooseCmd := "" - for i, arg := range os.Args { - if i == 0 { - continue - } - gooseCmd += arg + " " - } - - env := config.GetEnv(true) - - cmd := fmt.Sprintf( - "goose -dir %s postgres \"%s\" %s", - migrationsFolder, - *env.PBW_POSTGRES_CONN_STRING, - gooseCmd, - ) - - fmt.Println(cmd) -} +package main + +import ( + "fmt" + "os" + + "github.com/eduardolat/pgbackweb/internal/config" +) + +const migrationsFolder string = "./internal/database/migrations" + +func main() { + if len(os.Args) < 2 { + fmt.Println("goose command is required") + fmt.Println("example: task goose -- up") + return + } + + gooseCmd := "" + for i, arg := range os.Args { + if i == 0 { + continue + } + gooseCmd += arg + " " + } + + env := config.GetEnv(true) + + cmd := fmt.Sprintf( + "goose -dir %s postgres \"%s\" %s", + migrationsFolder, + *env.PBW_POSTGRES_CONN_STRING, + gooseCmd, + ) + + fmt.Println(cmd) +} diff --git a/cmd/resetdb/main.go b/cmd/resetdb/main.go index 357e7a96..4aae23ee 100644 --- a/cmd/resetdb/main.go +++ b/cmd/resetdb/main.go @@ -1,57 +1,57 @@ -package main - -import ( - "database/sql" - "fmt" - "log" - - "github.com/eduardolat/pgbackweb/internal/config" - _ "github.com/lib/pq" -) - -func main() { - log.Println("Are you sure you want to reset the database? (yes/no)") - var answer string - - for answer != "yes" && answer != "no" { - if _, err := fmt.Scanln(&answer); err != nil { - panic(err) - } - if answer == "no" { - log.Println("Exiting...") - return - } - if answer == "yes" { - log.Println("Resetting database...") - break - } - log.Println("Please enter 'yes' or 'no'") - } - - env := config.GetEnv() - db := connectDB(env) - - _, err := db.Exec("DROP SCHEMA public CASCADE; CREATE SCHEMA public;") - if err != nil { - panic(fmt.Errorf("❌ Could not reset DB: %w", err)) - } - - log.Println("✅ Database reset") -} - -func connectDB(env *config.Env) *sql.DB { - db, err := sql.Open("postgres", *env.PBW_POSTGRES_CONN_STRING) - if err != nil { - panic(fmt.Errorf("❌ Could not connect to DB: %w", err)) - } - - err = db.Ping() - if err != nil { - panic(fmt.Errorf("❌ Could not ping DB: %w", err)) - } - - db.SetMaxOpenConns(1) - log.Println("✅ Connected to DB") - - return db -} +package main + +import ( + "database/sql" + "fmt" + "log" + + "github.com/eduardolat/pgbackweb/internal/config" + _ "github.com/lib/pq" +) + +func main() { + log.Println("Are you sure you want to reset the database? (yes/no)") + var answer string + + for answer != "yes" && answer != "no" { + if _, err := fmt.Scanln(&answer); err != nil { + panic(err) + } + if answer == "no" { + log.Println("Exiting...") + return + } + if answer == "yes" { + log.Println("Resetting database...") + break + } + log.Println("Please enter 'yes' or 'no'") + } + + env := config.GetEnv() + db := connectDB(env) + + _, err := db.Exec("DROP SCHEMA public CASCADE; CREATE SCHEMA public;") + if err != nil { + panic(fmt.Errorf("❌ Could not reset DB: %w", err)) + } + + log.Println("✅ Database reset") +} + +func connectDB(env *config.Env) *sql.DB { + db, err := sql.Open("postgres", *env.PBW_POSTGRES_CONN_STRING) + if err != nil { + panic(fmt.Errorf("❌ Could not connect to DB: %w", err)) + } + + err = db.Ping() + if err != nil { + panic(fmt.Errorf("❌ Could not ping DB: %w", err)) + } + + db.SetMaxOpenConns(1) + log.Println("✅ Connected to DB") + + return db +} diff --git a/compose.yaml b/compose.yaml index a9b54078..b4a71a78 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,72 +1,72 @@ -name: pgbackweb-dev - -services: - app: - container_name: pbw_app - build: - context: . - dockerfile: ./docker/Dockerfile.dev - ports: - - "8085:8085" - volumes: - - ./:/app - - pbw_vol_app_ssh:/root/.ssh - - pbw_vol_app_gh:/root/.config/gh - - pbw_vol_app_go_mod_cache:/root/go/pkg/mod - networks: - - pbw_network - depends_on: - - postgres - - adminer - - minio - - postgres: - container_name: pbw_postgres - image: postgres:16 - environment: - POSTGRES_USER: postgres - POSTGRES_DB: pgbackweb - POSTGRES_PASSWORD: password - volumes: - - pbw_vol_postgres:/var/lib/postgresql/data - ports: - - "5432:5432" - networks: - - pbw_network - - adminer: - container_name: pbw_adminer - image: adminer:4.8.1 - ports: - - "8080:8080" - environment: - ADMINER_DEFAULT_SERVER: pbw_postgres - ADMINER_DESIGN: "lucas-sandery" - networks: - - pbw_network - - minio: - container_name: pbw_minio - image: minio/minio:RELEASE.2024-02-12T21-02-27Z - ports: - - "9000:9000" - - "9090:9090" - environment: - MINIO_ROOT_USER: "root" - MINIO_ROOT_PASSWORD: "password" - volumes: - - pbw_vol_minio:/data - command: minio server /data/minio --console-address ":9090" - networks: - - pbw_network - -volumes: - pbw_vol_app_ssh: - pbw_vol_app_gh: - pbw_vol_app_go_mod_cache: - pbw_vol_postgres: - pbw_vol_minio: - -networks: - pbw_network: - driver: bridge +name: pgbackweb-dev + +services: + app: + container_name: pbw_app + build: + context: . + dockerfile: ./docker/Dockerfile.dev + ports: + - "8085:8085" + volumes: + - ./:/app + - pbw_vol_app_ssh:/root/.ssh + - pbw_vol_app_gh:/root/.config/gh + - pbw_vol_app_go_mod_cache:/root/go/pkg/mod + networks: + - pbw_network + depends_on: + - postgres + - adminer + - minio + + postgres: + container_name: pbw_postgres + image: postgres:16 + environment: + POSTGRES_USER: postgres + POSTGRES_DB: pgbackweb + POSTGRES_PASSWORD: password + volumes: + - pbw_vol_postgres:/var/lib/postgresql/data + ports: + - "5432:5432" + networks: + - pbw_network + + adminer: + container_name: pbw_adminer + image: adminer:4.8.1 + ports: + - "8080:8080" + environment: + ADMINER_DEFAULT_SERVER: pbw_postgres + ADMINER_DESIGN: "lucas-sandery" + networks: + - pbw_network + + minio: + container_name: pbw_minio + image: minio/minio:RELEASE.2024-02-12T21-02-27Z + ports: + - "9000:9000" + - "9090:9090" + environment: + MINIO_ROOT_USER: "root" + MINIO_ROOT_PASSWORD: "password" + volumes: + - pbw_vol_minio:/data + command: minio server /data/minio --console-address ":9090" + networks: + - pbw_network + +volumes: + pbw_vol_app_ssh: + pbw_vol_app_gh: + pbw_vol_app_go_mod_cache: + pbw_vol_postgres: + pbw_vol_minio: + +networks: + pbw_network: + driver: bridge diff --git a/docker/Dockerfile b/docker/Dockerfile index 293929a5..9bdf4af3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,112 +1,112 @@ -# To make sure we have the node, npm, and golang binaries -FROM node:21.5.0-slim AS node -FROM golang:1.22.5 AS golang - -# This is the actual image we will use -FROM ubuntu:24.04 - -# Copy node and npm binaries from the node image -COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules -COPY --from=node /usr/local/bin/node /usr/local/bin/node -RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm - -# Copy the golang binaries from the golang image -COPY --from=golang /usr/local/go /usr/local/go -ENV PATH="$PATH:/usr/local/go/bin" - -# Set general environment variables -ENV DEBIAN_FRONTEND=noninteractive - -# Install system dependencies -RUN apt-get update && \ - apt-get install -y \ - wget=1.21.4-1ubuntu4.1 unzip=6.0-28ubuntu4 git=1:2.43.0-1ubuntu7.1 \ - gnupg2=2.4.4-2ubuntu17 lsb-release=12.0-2 tzdata=2024a-2ubuntu1 && \ - rm -rf /var/lib/apt/lists/* - -# Add PostgreSQL repository and key -RUN echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - - -# Install supported PostgreSQL client versions -RUN apt-get update && \ - apt-get install -y postgresql-client-13 postgresql-client-14 \ - postgresql-client-15 postgresql-client-16 && \ - rm -rf /var/lib/apt/lists/* - -# Create aliases for the different PostgreSQL client versions -RUN echo 'alias pg_dump_13="/usr/lib/postgresql/13/bin/pg_dump"' >> /etc/bash.bashrc && \ - echo 'alias pg_dump_14="/usr/lib/postgresql/14/bin/pg_dump"' >> /etc/bash.bashrc && \ - echo 'alias pg_dump_15="/usr/lib/postgresql/15/bin/pg_dump"' >> /etc/bash.bashrc && \ - echo 'alias pg_dump_16="/usr/lib/postgresql/16/bin/pg_dump"' >> /etc/bash.bashrc - -# Move to temporary directory to install additional tools -WORKDIR /app/temp - -# Install task -RUN wget --no-verbose https://github.com/go-task/task/releases/download/v3.37.2/task_linux_amd64.tar.gz && \ - tar -xzf task_linux_amd64.tar.gz && \ - mv ./task /usr/local/bin/task && \ - chmod 777 /usr/local/bin/task - -# Install GitHub CLI -RUN wget --no-verbose https://github.com/cli/cli/releases/download/v2.52.0/gh_2.52.0_linux_amd64.tar.gz && \ - tar -xzf gh_2.52.0_linux_amd64.tar.gz && \ - mv gh_2.52.0_linux_amd64/bin/gh /usr/local/bin/gh && \ - chmod 777 /usr/local/bin/gh - -# Install air -RUN wget --no-verbose https://github.com/cosmtrek/air/releases/download/v1.52.3/air_1.52.3_linux_amd64.tar.gz && \ - tar -xzf air_1.52.3_linux_amd64.tar.gz && \ - mv ./air /usr/local/bin/air && \ - chmod 777 /usr/local/bin/air - -# Install goose -RUN wget --no-verbose https://github.com/pressly/goose/releases/download/v3.21.1/goose_linux_x86_64 && \ - mv ./goose_linux_x86_64 /usr/local/bin/goose && \ - chmod 777 /usr/local/bin/goose - -# Install sqlc -RUN wget --no-verbose https://github.com/sqlc-dev/sqlc/releases/download/v1.26.0/sqlc_1.26.0_linux_amd64.tar.gz && \ - tar -xzf sqlc_1.26.0_linux_amd64.tar.gz && \ - mv ./sqlc /usr/local/bin/sqlc && \ - chmod 777 /usr/local/bin/sqlc - -# Install golangci-lint -RUN wget --no-verbose https://github.com/golangci/golangci-lint/releases/download/v1.59.1/golangci-lint-1.59.1-linux-amd64.tar.gz && \ - tar -xzf golangci-lint-1.59.1-linux-amd64.tar.gz && \ - mv ./golangci-lint-1.59.1-linux-amd64/golangci-lint /usr/local/bin/golangci-lint && \ - chmod 777 /usr/local/bin/golangci-lint - -# Go to the app dir, delete the temporary dir and create backups dir -WORKDIR /app -RUN rm -rf /app/temp && \ - mkdir /backups && \ - chmod 777 /backups - -############## -# START HERE # -############## - -# Copy and install nodejs dependencies -COPY package.json . -RUN npm install - -# Copy and install go dependencies -COPY go.mod . -COPY go.sum . -RUN go mod download - -# Copy the rest of the files -COPY . . - -# Build the app -RUN task build - -# Copy change-password binary -RUN cp ./dist/change-password /usr/local/bin/change-password && \ - chmod 777 /usr/local/bin/change-password - -# Run the app -EXPOSE 8085 -CMD ["task", "migrate-serve"] +# To make sure we have the node, npm, and golang binaries +FROM node:21.5.0-slim AS node +FROM golang:1.22.5 AS golang + +# This is the actual image we will use +FROM ubuntu:24.04 + +# Copy node and npm binaries from the node image +COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules +COPY --from=node /usr/local/bin/node /usr/local/bin/node +RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm + +# Copy the golang binaries from the golang image +COPY --from=golang /usr/local/go /usr/local/go +ENV PATH="$PATH:/usr/local/go/bin" + +# Set general environment variables +ENV DEBIAN_FRONTEND=noninteractive + +# Install system dependencies +RUN apt-get update && \ + apt-get install -y \ + wget=1.21.4-1ubuntu4.1 unzip=6.0-28ubuntu4 git=1:2.43.0-1ubuntu7.1 \ + gnupg2=2.4.4-2ubuntu17 lsb-release=12.0-2 tzdata=2024a-2ubuntu1 && \ + rm -rf /var/lib/apt/lists/* + +# Add PostgreSQL repository and key +RUN echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ + wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - + +# Install supported PostgreSQL client versions +RUN apt-get update && \ + apt-get install -y postgresql-client-13 postgresql-client-14 \ + postgresql-client-15 postgresql-client-16 && \ + rm -rf /var/lib/apt/lists/* + +# Create aliases for the different PostgreSQL client versions +RUN echo 'alias pg_dump_13="/usr/lib/postgresql/13/bin/pg_dump"' >> /etc/bash.bashrc && \ + echo 'alias pg_dump_14="/usr/lib/postgresql/14/bin/pg_dump"' >> /etc/bash.bashrc && \ + echo 'alias pg_dump_15="/usr/lib/postgresql/15/bin/pg_dump"' >> /etc/bash.bashrc && \ + echo 'alias pg_dump_16="/usr/lib/postgresql/16/bin/pg_dump"' >> /etc/bash.bashrc + +# Move to temporary directory to install additional tools +WORKDIR /app/temp + +# Install task +RUN wget --no-verbose https://github.com/go-task/task/releases/download/v3.37.2/task_linux_amd64.tar.gz && \ + tar -xzf task_linux_amd64.tar.gz && \ + mv ./task /usr/local/bin/task && \ + chmod 777 /usr/local/bin/task + +# Install GitHub CLI +RUN wget --no-verbose https://github.com/cli/cli/releases/download/v2.52.0/gh_2.52.0_linux_amd64.tar.gz && \ + tar -xzf gh_2.52.0_linux_amd64.tar.gz && \ + mv gh_2.52.0_linux_amd64/bin/gh /usr/local/bin/gh && \ + chmod 777 /usr/local/bin/gh + +# Install air +RUN wget --no-verbose https://github.com/cosmtrek/air/releases/download/v1.52.3/air_1.52.3_linux_amd64.tar.gz && \ + tar -xzf air_1.52.3_linux_amd64.tar.gz && \ + mv ./air /usr/local/bin/air && \ + chmod 777 /usr/local/bin/air + +# Install goose +RUN wget --no-verbose https://github.com/pressly/goose/releases/download/v3.21.1/goose_linux_x86_64 && \ + mv ./goose_linux_x86_64 /usr/local/bin/goose && \ + chmod 777 /usr/local/bin/goose + +# Install sqlc +RUN wget --no-verbose https://github.com/sqlc-dev/sqlc/releases/download/v1.26.0/sqlc_1.26.0_linux_amd64.tar.gz && \ + tar -xzf sqlc_1.26.0_linux_amd64.tar.gz && \ + mv ./sqlc /usr/local/bin/sqlc && \ + chmod 777 /usr/local/bin/sqlc + +# Install golangci-lint +RUN wget --no-verbose https://github.com/golangci/golangci-lint/releases/download/v1.59.1/golangci-lint-1.59.1-linux-amd64.tar.gz && \ + tar -xzf golangci-lint-1.59.1-linux-amd64.tar.gz && \ + mv ./golangci-lint-1.59.1-linux-amd64/golangci-lint /usr/local/bin/golangci-lint && \ + chmod 777 /usr/local/bin/golangci-lint + +# Go to the app dir, delete the temporary dir and create backups dir +WORKDIR /app +RUN rm -rf /app/temp && \ + mkdir /backups && \ + chmod 777 /backups + +############## +# START HERE # +############## + +# Copy and install nodejs dependencies +COPY package.json . +RUN npm install + +# Copy and install go dependencies +COPY go.mod . +COPY go.sum . +RUN go mod download + +# Copy the rest of the files +COPY . . + +# Build the app +RUN task build + +# Copy change-password binary +RUN cp ./dist/change-password /usr/local/bin/change-password && \ + chmod 777 /usr/local/bin/change-password + +# Run the app +EXPOSE 8085 +CMD ["task", "migrate-serve"] diff --git a/docker/Dockerfile.cicd b/docker/Dockerfile.cicd index 05aa341a..b2498838 100644 --- a/docker/Dockerfile.cicd +++ b/docker/Dockerfile.cicd @@ -1,101 +1,101 @@ -# To make sure we have the node, npm, and golang binaries -FROM node:21.5.0-slim AS node -FROM golang:1.22.5 AS golang - -# This is the actual image we will use -FROM ubuntu:24.04 - -# Copy node and npm binaries from the node image -COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules -COPY --from=node /usr/local/bin/node /usr/local/bin/node -RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm - -# Copy the golang binaries from the golang image -COPY --from=golang /usr/local/go /usr/local/go -ENV PATH="$PATH:/usr/local/go/bin" - -# Set general environment variables -ENV DEBIAN_FRONTEND=noninteractive - -# Install system dependencies -RUN apt-get update && \ - apt-get install -y \ - wget=1.21.4-1ubuntu4.1 unzip=6.0-28ubuntu4 git=1:2.43.0-1ubuntu7.1 \ - gnupg2=2.4.4-2ubuntu17 lsb-release=12.0-2 tzdata=2024a-2ubuntu1 && \ - rm -rf /var/lib/apt/lists/* - -# Add PostgreSQL repository and key -RUN echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - - -# Install supported PostgreSQL client versions -RUN apt-get update && \ - apt-get install -y postgresql-client-13 postgresql-client-14 \ - postgresql-client-15 postgresql-client-16 && \ - rm -rf /var/lib/apt/lists/* - -# Create aliases for the different PostgreSQL client versions -RUN echo 'alias pg_dump_13="/usr/lib/postgresql/13/bin/pg_dump"' >> /etc/bash.bashrc && \ - echo 'alias pg_dump_14="/usr/lib/postgresql/14/bin/pg_dump"' >> /etc/bash.bashrc && \ - echo 'alias pg_dump_15="/usr/lib/postgresql/15/bin/pg_dump"' >> /etc/bash.bashrc && \ - echo 'alias pg_dump_16="/usr/lib/postgresql/16/bin/pg_dump"' >> /etc/bash.bashrc - -# Move to temporary directory to install additional tools -WORKDIR /app/temp - -# Install task -RUN wget --no-verbose https://github.com/go-task/task/releases/download/v3.37.2/task_linux_amd64.tar.gz && \ - tar -xzf task_linux_amd64.tar.gz && \ - mv ./task /usr/local/bin/task && \ - chmod 777 /usr/local/bin/task - -# Install GitHub CLI -RUN wget --no-verbose https://github.com/cli/cli/releases/download/v2.52.0/gh_2.52.0_linux_amd64.tar.gz && \ - tar -xzf gh_2.52.0_linux_amd64.tar.gz && \ - mv gh_2.52.0_linux_amd64/bin/gh /usr/local/bin/gh && \ - chmod 777 /usr/local/bin/gh - -# Install air -RUN wget --no-verbose https://github.com/cosmtrek/air/releases/download/v1.52.3/air_1.52.3_linux_amd64.tar.gz && \ - tar -xzf air_1.52.3_linux_amd64.tar.gz && \ - mv ./air /usr/local/bin/air && \ - chmod 777 /usr/local/bin/air - -# Install goose -RUN wget --no-verbose https://github.com/pressly/goose/releases/download/v3.21.1/goose_linux_x86_64 && \ - mv ./goose_linux_x86_64 /usr/local/bin/goose && \ - chmod 777 /usr/local/bin/goose - -# Install sqlc -RUN wget --no-verbose https://github.com/sqlc-dev/sqlc/releases/download/v1.26.0/sqlc_1.26.0_linux_amd64.tar.gz && \ - tar -xzf sqlc_1.26.0_linux_amd64.tar.gz && \ - mv ./sqlc /usr/local/bin/sqlc && \ - chmod 777 /usr/local/bin/sqlc - -# Install golangci-lint -RUN wget --no-verbose https://github.com/golangci/golangci-lint/releases/download/v1.59.1/golangci-lint-1.59.1-linux-amd64.tar.gz && \ - tar -xzf golangci-lint-1.59.1-linux-amd64.tar.gz && \ - mv ./golangci-lint-1.59.1-linux-amd64/golangci-lint /usr/local/bin/golangci-lint && \ - chmod 777 /usr/local/bin/golangci-lint - -# Go to the app dir, delete the temporary dir and create backups dir -WORKDIR /app -RUN rm -rf /app/temp && \ - mkdir /backups && \ - chmod 777 /backups - -############## -# START HERE # -############## - -# Copy and install nodejs dependencies -COPY package.json . -RUN npm install - -# Copy and install go dependencies -COPY go.mod . -COPY go.sum . -RUN go mod download - -# Copy the rest of the files -COPY . . +# To make sure we have the node, npm, and golang binaries +FROM node:21.5.0-slim AS node +FROM golang:1.22.5 AS golang + +# This is the actual image we will use +FROM ubuntu:24.04 + +# Copy node and npm binaries from the node image +COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules +COPY --from=node /usr/local/bin/node /usr/local/bin/node +RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm + +# Copy the golang binaries from the golang image +COPY --from=golang /usr/local/go /usr/local/go +ENV PATH="$PATH:/usr/local/go/bin" + +# Set general environment variables +ENV DEBIAN_FRONTEND=noninteractive + +# Install system dependencies +RUN apt-get update && \ + apt-get install -y \ + wget=1.21.4-1ubuntu4.1 unzip=6.0-28ubuntu4 git=1:2.43.0-1ubuntu7.1 \ + gnupg2=2.4.4-2ubuntu17 lsb-release=12.0-2 tzdata=2024a-2ubuntu1 && \ + rm -rf /var/lib/apt/lists/* + +# Add PostgreSQL repository and key +RUN echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ + wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - + +# Install supported PostgreSQL client versions +RUN apt-get update && \ + apt-get install -y postgresql-client-13 postgresql-client-14 \ + postgresql-client-15 postgresql-client-16 && \ + rm -rf /var/lib/apt/lists/* + +# Create aliases for the different PostgreSQL client versions +RUN echo 'alias pg_dump_13="/usr/lib/postgresql/13/bin/pg_dump"' >> /etc/bash.bashrc && \ + echo 'alias pg_dump_14="/usr/lib/postgresql/14/bin/pg_dump"' >> /etc/bash.bashrc && \ + echo 'alias pg_dump_15="/usr/lib/postgresql/15/bin/pg_dump"' >> /etc/bash.bashrc && \ + echo 'alias pg_dump_16="/usr/lib/postgresql/16/bin/pg_dump"' >> /etc/bash.bashrc + +# Move to temporary directory to install additional tools +WORKDIR /app/temp + +# Install task +RUN wget --no-verbose https://github.com/go-task/task/releases/download/v3.37.2/task_linux_amd64.tar.gz && \ + tar -xzf task_linux_amd64.tar.gz && \ + mv ./task /usr/local/bin/task && \ + chmod 777 /usr/local/bin/task + +# Install GitHub CLI +RUN wget --no-verbose https://github.com/cli/cli/releases/download/v2.52.0/gh_2.52.0_linux_amd64.tar.gz && \ + tar -xzf gh_2.52.0_linux_amd64.tar.gz && \ + mv gh_2.52.0_linux_amd64/bin/gh /usr/local/bin/gh && \ + chmod 777 /usr/local/bin/gh + +# Install air +RUN wget --no-verbose https://github.com/cosmtrek/air/releases/download/v1.52.3/air_1.52.3_linux_amd64.tar.gz && \ + tar -xzf air_1.52.3_linux_amd64.tar.gz && \ + mv ./air /usr/local/bin/air && \ + chmod 777 /usr/local/bin/air + +# Install goose +RUN wget --no-verbose https://github.com/pressly/goose/releases/download/v3.21.1/goose_linux_x86_64 && \ + mv ./goose_linux_x86_64 /usr/local/bin/goose && \ + chmod 777 /usr/local/bin/goose + +# Install sqlc +RUN wget --no-verbose https://github.com/sqlc-dev/sqlc/releases/download/v1.26.0/sqlc_1.26.0_linux_amd64.tar.gz && \ + tar -xzf sqlc_1.26.0_linux_amd64.tar.gz && \ + mv ./sqlc /usr/local/bin/sqlc && \ + chmod 777 /usr/local/bin/sqlc + +# Install golangci-lint +RUN wget --no-verbose https://github.com/golangci/golangci-lint/releases/download/v1.59.1/golangci-lint-1.59.1-linux-amd64.tar.gz && \ + tar -xzf golangci-lint-1.59.1-linux-amd64.tar.gz && \ + mv ./golangci-lint-1.59.1-linux-amd64/golangci-lint /usr/local/bin/golangci-lint && \ + chmod 777 /usr/local/bin/golangci-lint + +# Go to the app dir, delete the temporary dir and create backups dir +WORKDIR /app +RUN rm -rf /app/temp && \ + mkdir /backups && \ + chmod 777 /backups + +############## +# START HERE # +############## + +# Copy and install nodejs dependencies +COPY package.json . +RUN npm install + +# Copy and install go dependencies +COPY go.mod . +COPY go.sum . +RUN go mod download + +# Copy the rest of the files +COPY . . diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index ea75f0c7..26b65fcf 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -1,97 +1,97 @@ -# To make sure we have the node, npm, and golang binaries -FROM node:21.5.0-slim AS node -FROM golang:1.22.5 AS golang - -# This is the actual image we will use -FROM ubuntu:24.04 - -# Copy node and npm binaries from the node image -COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules -COPY --from=node /usr/local/bin/node /usr/local/bin/node -RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm - -# Copy the golang binaries from the golang image -COPY --from=golang /usr/local/go /usr/local/go -ENV PATH="$PATH:/usr/local/go/bin" - -# Set general environment variables -ENV DEBIAN_FRONTEND=noninteractive - -# Install system dependencies -RUN apt-get update && \ - apt-get install -y \ - wget=1.21.4-1ubuntu4.1 unzip=6.0-28ubuntu4 git=1:2.43.0-1ubuntu7.1 \ - gnupg2=2.4.4-2ubuntu17 lsb-release=12.0-2 tzdata=2024a-2ubuntu1 && \ - rm -rf /var/lib/apt/lists/* - -# Add PostgreSQL repository and key -RUN echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - - -# Install supported PostgreSQL client versions -RUN apt-get update && \ - apt-get install -y postgresql-client-13 postgresql-client-14 \ - postgresql-client-15 postgresql-client-16 && \ - rm -rf /var/lib/apt/lists/* - -# Create aliases for the different PostgreSQL client versions -RUN echo 'alias pg_dump_13="/usr/lib/postgresql/13/bin/pg_dump"' >> /etc/bash.bashrc && \ - echo 'alias pg_dump_14="/usr/lib/postgresql/14/bin/pg_dump"' >> /etc/bash.bashrc && \ - echo 'alias pg_dump_15="/usr/lib/postgresql/15/bin/pg_dump"' >> /etc/bash.bashrc && \ - echo 'alias pg_dump_16="/usr/lib/postgresql/16/bin/pg_dump"' >> /etc/bash.bashrc - -# Move to temporary directory to install additional tools -WORKDIR /app/temp - -# Install task -RUN wget --no-verbose https://github.com/go-task/task/releases/download/v3.37.2/task_linux_amd64.tar.gz && \ - tar -xzf task_linux_amd64.tar.gz && \ - mv ./task /usr/local/bin/task && \ - chmod 777 /usr/local/bin/task - -# Install GitHub CLI -RUN wget --no-verbose https://github.com/cli/cli/releases/download/v2.52.0/gh_2.52.0_linux_amd64.tar.gz && \ - tar -xzf gh_2.52.0_linux_amd64.tar.gz && \ - mv gh_2.52.0_linux_amd64/bin/gh /usr/local/bin/gh && \ - chmod 777 /usr/local/bin/gh - -# Install air -RUN wget --no-verbose https://github.com/cosmtrek/air/releases/download/v1.52.3/air_1.52.3_linux_amd64.tar.gz && \ - tar -xzf air_1.52.3_linux_amd64.tar.gz && \ - mv ./air /usr/local/bin/air && \ - chmod 777 /usr/local/bin/air - -# Install goose -RUN wget --no-verbose https://github.com/pressly/goose/releases/download/v3.21.1/goose_linux_x86_64 && \ - mv ./goose_linux_x86_64 /usr/local/bin/goose && \ - chmod 777 /usr/local/bin/goose - -# Install sqlc -RUN wget --no-verbose https://github.com/sqlc-dev/sqlc/releases/download/v1.26.0/sqlc_1.26.0_linux_amd64.tar.gz && \ - tar -xzf sqlc_1.26.0_linux_amd64.tar.gz && \ - mv ./sqlc /usr/local/bin/sqlc && \ - chmod 777 /usr/local/bin/sqlc - -# Install golangci-lint -RUN wget --no-verbose https://github.com/golangci/golangci-lint/releases/download/v1.59.1/golangci-lint-1.59.1-linux-amd64.tar.gz && \ - tar -xzf golangci-lint-1.59.1-linux-amd64.tar.gz && \ - mv ./golangci-lint-1.59.1-linux-amd64/golangci-lint /usr/local/bin/golangci-lint && \ - chmod 777 /usr/local/bin/golangci-lint - -# Go to the app dir, delete the temporary dir and create backups dir -WORKDIR /app -RUN rm -rf /app/temp && \ - mkdir /backups && \ - chmod 777 /backups - -############## -# START HERE # -############## - -# Add the startup script on every bash session -COPY scripts/startup.sh /usr/local/bin/startup.sh -RUN echo "\n\n" >> /root/.bashrc && \ - cat /usr/local/bin/startup.sh >> /root/.bashrc - -# Command just to keep the container running -CMD ["tail", "-f", "/dev/null"] +# To make sure we have the node, npm, and golang binaries +FROM node:21.5.0-slim AS node +FROM golang:1.22.5 AS golang + +# This is the actual image we will use +FROM ubuntu:24.04 + +# Copy node and npm binaries from the node image +COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules +COPY --from=node /usr/local/bin/node /usr/local/bin/node +RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm + +# Copy the golang binaries from the golang image +COPY --from=golang /usr/local/go /usr/local/go +ENV PATH="$PATH:/usr/local/go/bin" + +# Set general environment variables +ENV DEBIAN_FRONTEND=noninteractive + +# Install system dependencies +RUN apt-get update && \ + apt-get install -y \ + wget=1.21.4-1ubuntu4.1 unzip=6.0-28ubuntu4 git=1:2.43.0-1ubuntu7.1 \ + gnupg2=2.4.4-2ubuntu17 lsb-release=12.0-2 tzdata=2024a-2ubuntu1 && \ + rm -rf /var/lib/apt/lists/* + +# Add PostgreSQL repository and key +RUN echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ + wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - + +# Install supported PostgreSQL client versions +RUN apt-get update && \ + apt-get install -y postgresql-client-13 postgresql-client-14 \ + postgresql-client-15 postgresql-client-16 && \ + rm -rf /var/lib/apt/lists/* + +# Create aliases for the different PostgreSQL client versions +RUN echo 'alias pg_dump_13="/usr/lib/postgresql/13/bin/pg_dump"' >> /etc/bash.bashrc && \ + echo 'alias pg_dump_14="/usr/lib/postgresql/14/bin/pg_dump"' >> /etc/bash.bashrc && \ + echo 'alias pg_dump_15="/usr/lib/postgresql/15/bin/pg_dump"' >> /etc/bash.bashrc && \ + echo 'alias pg_dump_16="/usr/lib/postgresql/16/bin/pg_dump"' >> /etc/bash.bashrc + +# Move to temporary directory to install additional tools +WORKDIR /app/temp + +# Install task +RUN wget --no-verbose https://github.com/go-task/task/releases/download/v3.37.2/task_linux_amd64.tar.gz && \ + tar -xzf task_linux_amd64.tar.gz && \ + mv ./task /usr/local/bin/task && \ + chmod 777 /usr/local/bin/task + +# Install GitHub CLI +RUN wget --no-verbose https://github.com/cli/cli/releases/download/v2.52.0/gh_2.52.0_linux_amd64.tar.gz && \ + tar -xzf gh_2.52.0_linux_amd64.tar.gz && \ + mv gh_2.52.0_linux_amd64/bin/gh /usr/local/bin/gh && \ + chmod 777 /usr/local/bin/gh + +# Install air +RUN wget --no-verbose https://github.com/cosmtrek/air/releases/download/v1.52.3/air_1.52.3_linux_amd64.tar.gz && \ + tar -xzf air_1.52.3_linux_amd64.tar.gz && \ + mv ./air /usr/local/bin/air && \ + chmod 777 /usr/local/bin/air + +# Install goose +RUN wget --no-verbose https://github.com/pressly/goose/releases/download/v3.21.1/goose_linux_x86_64 && \ + mv ./goose_linux_x86_64 /usr/local/bin/goose && \ + chmod 777 /usr/local/bin/goose + +# Install sqlc +RUN wget --no-verbose https://github.com/sqlc-dev/sqlc/releases/download/v1.26.0/sqlc_1.26.0_linux_amd64.tar.gz && \ + tar -xzf sqlc_1.26.0_linux_amd64.tar.gz && \ + mv ./sqlc /usr/local/bin/sqlc && \ + chmod 777 /usr/local/bin/sqlc + +# Install golangci-lint +RUN wget --no-verbose https://github.com/golangci/golangci-lint/releases/download/v1.59.1/golangci-lint-1.59.1-linux-amd64.tar.gz && \ + tar -xzf golangci-lint-1.59.1-linux-amd64.tar.gz && \ + mv ./golangci-lint-1.59.1-linux-amd64/golangci-lint /usr/local/bin/golangci-lint && \ + chmod 777 /usr/local/bin/golangci-lint + +# Go to the app dir, delete the temporary dir and create backups dir +WORKDIR /app +RUN rm -rf /app/temp && \ + mkdir /backups && \ + chmod 777 /backups + +############## +# START HERE # +############## + +# Add the startup script on every bash session +COPY scripts/startup.sh /usr/local/bin/startup.sh +RUN echo "\n\n" >> /root/.bashrc && \ + cat /usr/local/bin/startup.sh >> /root/.bashrc + +# Command just to keep the container running +CMD ["tail", "-f", "/dev/null"] diff --git a/docker/README.md b/docker/README.md index b8f51b63..c26579c0 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,25 +1,25 @@ -# Docker - -This folder is responsible for storing all Dockerfiles for the different Docker images required for the project. IT IS IMPERATIVE that all images have the same dependencies and versions to ensure consistency and reproducibility across different environments. - -To make sure they are identical, please always ensure that all instructions in the Dockerfiles are exactly the same up to the following comment: - -```Dockerfile -############## -# START HERE # -############## -``` - -After the above comment, you can add the necessary instructions for the specific image. Please replicate all dependencies and environment-related instructions in all Dockerfiles. - -## Dockerfile - -The `Dockerfile` file is the one used for building the production image. - -## Dockerfile.dev - -The `Dockerfile.dev` file is the one used for building the development environment image. - -## Dockerfile.cicd - -The `Dockerfile.cicd` file is the one used for building the CI/CD environment image. +# Docker + +This folder is responsible for storing all Dockerfiles for the different Docker images required for the project. IT IS IMPERATIVE that all images have the same dependencies and versions to ensure consistency and reproducibility across different environments. + +To make sure they are identical, please always ensure that all instructions in the Dockerfiles are exactly the same up to the following comment: + +```Dockerfile +############## +# START HERE # +############## +``` + +After the above comment, you can add the necessary instructions for the specific image. Please replicate all dependencies and environment-related instructions in all Dockerfiles. + +## Dockerfile + +The `Dockerfile` file is the one used for building the production image. + +## Dockerfile.dev + +The `Dockerfile.dev` file is the one used for building the development environment image. + +## Dockerfile.cicd + +The `Dockerfile.cicd` file is the one used for building the CI/CD environment image. diff --git a/go.mod b/go.mod index d0d25a4e..a20916c8 100644 --- a/go.mod +++ b/go.mod @@ -1,43 +1,43 @@ -module github.com/eduardolat/pgbackweb - -go 1.22.5 - -require ( - github.com/adhocore/gronx v1.8.1 - github.com/aws/aws-sdk-go v1.54.20 - github.com/eduardolat/gomponents-lucide v1.2.0 - github.com/go-co-op/gocron/v2 v2.11.0 - github.com/go-playground/validator/v10 v10.22.0 - github.com/google/uuid v1.6.0 - github.com/joho/godotenv v1.5.1 - github.com/labstack/echo/v4 v4.12.0 - github.com/lib/pq v1.10.9 - github.com/maragudk/gomponents v0.20.4 - github.com/orsinium-labs/enum v1.4.0 - github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.25.0 - golang.org/x/sync v0.7.0 -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/jonboulle/clockwork v0.4.0 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/labstack/gommon v0.4.2 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) +module github.com/eduardolat/pgbackweb + +go 1.22.5 + +require ( + github.com/adhocore/gronx v1.8.1 + github.com/aws/aws-sdk-go v1.54.20 + github.com/eduardolat/gomponents-lucide v1.2.0 + github.com/go-co-op/gocron/v2 v2.11.0 + github.com/go-playground/validator/v10 v10.22.0 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 + github.com/labstack/echo/v4 v4.12.0 + github.com/lib/pq v1.10.9 + github.com/maragudk/gomponents v0.20.4 + github.com/orsinium-labs/enum v1.4.0 + github.com/stretchr/testify v1.9.0 + golang.org/x/crypto v0.25.0 + golang.org/x/sync v0.7.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jonboulle/clockwork v0.4.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 9218e571..29c3301f 100644 --- a/go.sum +++ b/go.sum @@ -1,92 +1,92 @@ -github.com/adhocore/gronx v1.8.1 h1:F2mLTG5sB11z7vplwD4iydz3YCEjstSfYmCrdSm3t6A= -github.com/adhocore/gronx v1.8.1/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg= -github.com/aws/aws-sdk-go v1.54.20 h1:FZ2UcXya7bUkvkpf7TaPmiL7EubK0go1nlXGLRwEsoo= -github.com/aws/aws-sdk-go v1.54.20/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/eduardolat/gomponents-lucide v1.2.0 h1:IzvsHfdfMjDF0c8aOxG7P0lawaSjwVLBo7z6VASOBbc= -github.com/eduardolat/gomponents-lucide v1.2.0/go.mod h1:C0Yx64QM2RXIal8pl/HdWU0xac0uek+IQI2eZAaeAwk= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/go-co-op/gocron/v2 v2.11.0 h1:IOowNA6SzwdRFnD4/Ol3Kj6G2xKfsoiiGq2Jhhm9bvE= -github.com/go-co-op/gocron/v2 v2.11.0/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= -github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -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/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= -github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= -github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/maragudk/gomponents v0.20.4 h1:8ayYSzCyz1EUl/+LU5vOBR/K2wI18W7SscNAzzsxsDo= -github.com/maragudk/gomponents v0.20.4/go.mod h1:nHkNnZL6ODgMBeJhrZjkMHVvNdoYsfmpKB2/hjdQ0Hg= -github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= -github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/orsinium-labs/enum v1.4.0 h1:3NInlfV76kuAg0kq2FFUondmg3WO7gMEgrPPrlzLDUM= -github.com/orsinium-labs/enum v1.4.0/go.mod h1:Qj5IK2pnElZtkZbGDxZMjpt7SUsn4tqE5vRelmWaBbc= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= -github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -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/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= -golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/adhocore/gronx v1.8.1 h1:F2mLTG5sB11z7vplwD4iydz3YCEjstSfYmCrdSm3t6A= +github.com/adhocore/gronx v1.8.1/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg= +github.com/aws/aws-sdk-go v1.54.20 h1:FZ2UcXya7bUkvkpf7TaPmiL7EubK0go1nlXGLRwEsoo= +github.com/aws/aws-sdk-go v1.54.20/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/eduardolat/gomponents-lucide v1.2.0 h1:IzvsHfdfMjDF0c8aOxG7P0lawaSjwVLBo7z6VASOBbc= +github.com/eduardolat/gomponents-lucide v1.2.0/go.mod h1:C0Yx64QM2RXIal8pl/HdWU0xac0uek+IQI2eZAaeAwk= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-co-op/gocron/v2 v2.11.0 h1:IOowNA6SzwdRFnD4/Ol3Kj6G2xKfsoiiGq2Jhhm9bvE= +github.com/go-co-op/gocron/v2 v2.11.0/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +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/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= +github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/maragudk/gomponents v0.20.4 h1:8ayYSzCyz1EUl/+LU5vOBR/K2wI18W7SscNAzzsxsDo= +github.com/maragudk/gomponents v0.20.4/go.mod h1:nHkNnZL6ODgMBeJhrZjkMHVvNdoYsfmpKB2/hjdQ0Hg= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/orsinium-labs/enum v1.4.0 h1:3NInlfV76kuAg0kq2FFUondmg3WO7gMEgrPPrlzLDUM= +github.com/orsinium-labs/enum v1.4.0/go.mod h1:Qj5IK2pnElZtkZbGDxZMjpt7SUsn4tqE5vRelmWaBbc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/env.go b/internal/config/env.go index e8a01677..5d1a8ee1 100644 --- a/internal/config/env.go +++ b/internal/config/env.go @@ -1,37 +1,37 @@ -package config - -import ( - "github.com/eduardolat/pgbackweb/internal/logger" - "github.com/joho/godotenv" -) - -type Env struct { - PBW_ENCRYPTION_KEY *string - PBW_POSTGRES_CONN_STRING *string -} - -// GetEnv returns the environment variables. -// -// If there is an error, it will log it and exit the program. -func GetEnv(disableLogs ...bool) *Env { - pickedDisableLogs := len(disableLogs) > 0 && disableLogs[0] - - err := godotenv.Load() - if err == nil && !pickedDisableLogs { - logger.Info("using .env file") - } - - env := &Env{ - PBW_ENCRYPTION_KEY: getEnvAsString(getEnvAsStringParams{ - name: "PBW_ENCRYPTION_KEY", - isRequired: true, - }), - PBW_POSTGRES_CONN_STRING: getEnvAsString(getEnvAsStringParams{ - name: "PBW_POSTGRES_CONN_STRING", - isRequired: true, - }), - } - - validateEnv(env) - return env -} +package config + +import ( + "github.com/eduardolat/pgbackweb/internal/logger" + "github.com/joho/godotenv" +) + +type Env struct { + PBW_ENCRYPTION_KEY *string + PBW_POSTGRES_CONN_STRING *string +} + +// GetEnv returns the environment variables. +// +// If there is an error, it will log it and exit the program. +func GetEnv(disableLogs ...bool) *Env { + pickedDisableLogs := len(disableLogs) > 0 && disableLogs[0] + + err := godotenv.Load() + if err == nil && !pickedDisableLogs { + logger.Info("using .env file") + } + + env := &Env{ + PBW_ENCRYPTION_KEY: getEnvAsString(getEnvAsStringParams{ + name: "PBW_ENCRYPTION_KEY", + isRequired: true, + }), + PBW_POSTGRES_CONN_STRING: getEnvAsString(getEnvAsStringParams{ + name: "PBW_POSTGRES_CONN_STRING", + isRequired: true, + }), + } + + validateEnv(env) + return env +} diff --git a/internal/config/env_validate.go b/internal/config/env_validate.go index 20241cb8..95d0c4d9 100644 --- a/internal/config/env_validate.go +++ b/internal/config/env_validate.go @@ -1,4 +1,4 @@ -package config - -// validateEnv runs additional validations on the environment variables. -func validateEnv(env *Env) {} +package config + +// validateEnv runs additional validations on the environment variables. +func validateEnv(env *Env) {} diff --git a/internal/config/helpers.go b/internal/config/helpers.go index c8bcaa0c..67663255 100644 --- a/internal/config/helpers.go +++ b/internal/config/helpers.go @@ -1,159 +1,159 @@ -package config - -import ( - "errors" - "os" - "strconv" - - "github.com/eduardolat/pgbackweb/internal/logger" -) - -type getEnvAsStringParams struct { - name string - defaultValue *string - isRequired bool -} - -// defaultValue returns a pointer to the given value. -func newDefaultValue[T any](value T) *T { - return &value -} - -// getEnvAsString returns the value of the environment variable with the given name. -func getEnvAsString(params getEnvAsStringParams) *string { //nolint:all - value, err := getEnvAsStringFunc(params) - - if err != nil { - logger.FatalError( - "error getting env variable", logger.KV{ - "name": params.name, - "error": err, - }, - ) - } - - return value -} - -// getEnvAsStringFunc is the outlying function for getEnvAsString. -func getEnvAsStringFunc(params getEnvAsStringParams) (*string, error) { - if params.defaultValue != nil && params.isRequired { - return nil, errors.New("cannot have both a default value and be required") - } - - value, exists := os.LookupEnv(params.name) - - if !exists && params.isRequired { - return nil, errors.New("required env variable does not exist") - } - - if !exists { - if params.defaultValue != nil { - return params.defaultValue, nil - } - return nil, nil - } - - return &value, nil -} - -type getEnvAsIntParams struct { - name string - defaultValue *int - isRequired bool -} - -// getEnvAsInt returns the value of the environment variable with the given name. -func getEnvAsInt(params getEnvAsIntParams) *int { //nolint:all - value, err := getEnvAsIntFunc(params) - - if err != nil { - logger.FatalError( - "error getting env variable", logger.KV{ - "name": params.name, - "error": err, - }, - ) - } - - return value -} - -// getEnvAsIntFunc is the outlying function for getEnvAsInt. -func getEnvAsIntFunc(params getEnvAsIntParams) (*int, error) { - if params.defaultValue != nil && params.isRequired { - return nil, errors.New("cannot have both a default value and be required") - } - - valueStr, exists := os.LookupEnv(params.name) - - if !exists && params.isRequired { - return nil, errors.New("required env variable does not exist") - } - - if !exists { - if params.defaultValue != nil { - return params.defaultValue, nil - } - return nil, nil - } - - value, err := strconv.Atoi(valueStr) - - if err != nil { - return nil, errors.New("env variable is not an integer") - } - - return &value, nil -} - -type getEnvAsBoolParams struct { - name string - defaultValue *bool - isRequired bool -} - -// getEnvAsBool returns the value of the environment variable with the given name. -func getEnvAsBool(params getEnvAsBoolParams) *bool { //nolint:all - value, err := getEnvAsBoolFunc(params) - - if err != nil { - logger.FatalError( - "error getting env variable", logger.KV{ - "name": params.name, - "error": err, - }, - ) - } - - return value -} - -// getEnvAsBoolFunc is the outlying function for getEnvAsBool. -func getEnvAsBoolFunc(params getEnvAsBoolParams) (*bool, error) { - if params.defaultValue != nil && params.isRequired { - return nil, errors.New("cannot have both a default value and be required") - } - - valueStr, exists := os.LookupEnv(params.name) - - if !exists && params.isRequired { - return nil, errors.New("required env variable does not exist") - } - - if !exists { - if params.defaultValue != nil { - return params.defaultValue, nil - } - f := false - return &f, nil - } - - value, err := strconv.ParseBool(valueStr) - - if err != nil { - return nil, errors.New("env variable is not a boolean, must be true or false") - } - - return &value, nil -} +package config + +import ( + "errors" + "os" + "strconv" + + "github.com/eduardolat/pgbackweb/internal/logger" +) + +type getEnvAsStringParams struct { + name string + defaultValue *string + isRequired bool +} + +// defaultValue returns a pointer to the given value. +func newDefaultValue[T any](value T) *T { + return &value +} + +// getEnvAsString returns the value of the environment variable with the given name. +func getEnvAsString(params getEnvAsStringParams) *string { //nolint:all + value, err := getEnvAsStringFunc(params) + + if err != nil { + logger.FatalError( + "error getting env variable", logger.KV{ + "name": params.name, + "error": err, + }, + ) + } + + return value +} + +// getEnvAsStringFunc is the outlying function for getEnvAsString. +func getEnvAsStringFunc(params getEnvAsStringParams) (*string, error) { + if params.defaultValue != nil && params.isRequired { + return nil, errors.New("cannot have both a default value and be required") + } + + value, exists := os.LookupEnv(params.name) + + if !exists && params.isRequired { + return nil, errors.New("required env variable does not exist") + } + + if !exists { + if params.defaultValue != nil { + return params.defaultValue, nil + } + return nil, nil + } + + return &value, nil +} + +type getEnvAsIntParams struct { + name string + defaultValue *int + isRequired bool +} + +// getEnvAsInt returns the value of the environment variable with the given name. +func getEnvAsInt(params getEnvAsIntParams) *int { //nolint:all + value, err := getEnvAsIntFunc(params) + + if err != nil { + logger.FatalError( + "error getting env variable", logger.KV{ + "name": params.name, + "error": err, + }, + ) + } + + return value +} + +// getEnvAsIntFunc is the outlying function for getEnvAsInt. +func getEnvAsIntFunc(params getEnvAsIntParams) (*int, error) { + if params.defaultValue != nil && params.isRequired { + return nil, errors.New("cannot have both a default value and be required") + } + + valueStr, exists := os.LookupEnv(params.name) + + if !exists && params.isRequired { + return nil, errors.New("required env variable does not exist") + } + + if !exists { + if params.defaultValue != nil { + return params.defaultValue, nil + } + return nil, nil + } + + value, err := strconv.Atoi(valueStr) + + if err != nil { + return nil, errors.New("env variable is not an integer") + } + + return &value, nil +} + +type getEnvAsBoolParams struct { + name string + defaultValue *bool + isRequired bool +} + +// getEnvAsBool returns the value of the environment variable with the given name. +func getEnvAsBool(params getEnvAsBoolParams) *bool { //nolint:all + value, err := getEnvAsBoolFunc(params) + + if err != nil { + logger.FatalError( + "error getting env variable", logger.KV{ + "name": params.name, + "error": err, + }, + ) + } + + return value +} + +// getEnvAsBoolFunc is the outlying function for getEnvAsBool. +func getEnvAsBoolFunc(params getEnvAsBoolParams) (*bool, error) { + if params.defaultValue != nil && params.isRequired { + return nil, errors.New("cannot have both a default value and be required") + } + + valueStr, exists := os.LookupEnv(params.name) + + if !exists && params.isRequired { + return nil, errors.New("required env variable does not exist") + } + + if !exists { + if params.defaultValue != nil { + return params.defaultValue, nil + } + f := false + return &f, nil + } + + value, err := strconv.ParseBool(valueStr) + + if err != nil { + return nil, errors.New("env variable is not a boolean, must be true or false") + } + + return &value, nil +} diff --git a/internal/config/helpers_test.go b/internal/config/helpers_test.go index 67b1b091..cac591c3 100644 --- a/internal/config/helpers_test.go +++ b/internal/config/helpers_test.go @@ -1,190 +1,190 @@ -package config - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetEnvAsStringFunc(t *testing.T) { - // Test when environment variable exists - os.Setenv("TEST_ENV", "test_value") - value, err := getEnvAsStringFunc(getEnvAsStringParams{ - name: "TEST_ENV", - isRequired: true, - }) - assert.NoError(t, err) - assert.Equal(t, "test_value", *value) - os.Unsetenv("TEST_ENV") - - // Test when environment variable does not exist, default value is provided, and is not required - value, err = getEnvAsStringFunc(getEnvAsStringParams{ - name: "NON_EXISTENT_ENV", - defaultValue: newDefaultValue("default_value"), - isRequired: false, - }) - assert.NoError(t, err) - assert.Equal(t, "default_value", *value) - - // Test when environment variable does not exist, no default value is provided, and is required - // This should return an error - value, err = getEnvAsStringFunc(getEnvAsStringParams{ - name: "NON_EXISTENT_ENV", - isRequired: true, - }) - assert.Error(t, err) - assert.Nil(t, value) - - // Test when environment variable exists, default value is provided, and is required - os.Setenv("TEST_ENV", "test_value") - value, err = getEnvAsStringFunc(getEnvAsStringParams{ - name: "TEST_ENV", - defaultValue: newDefaultValue("default_value"), - }) - assert.NoError(t, err) - assert.Equal(t, "test_value", *value) - os.Unsetenv("TEST_ENV") - - // Test when environment variable exists, is not required, and no default value is provided - os.Setenv("TEST_ENV", "test_value") - value, err = getEnvAsStringFunc(getEnvAsStringParams{ - name: "TEST_ENV", - isRequired: false, - }) - assert.NoError(t, err) - assert.Equal(t, "test_value", *value) - os.Unsetenv("TEST_ENV") - - // Test when environment variable does not exist, is not required, and no default value is provided - value, err = getEnvAsStringFunc(getEnvAsStringParams{ - name: "NON_EXISTENT_ENV", - isRequired: false, - }) - assert.NoError(t, err) - assert.Nil(t, value) - - // Test when default value and required are both present - // This should return an error - _, err = getEnvAsStringFunc(getEnvAsStringParams{ - name: "NON_EXISTENT_ENV", - defaultValue: newDefaultValue("default_value"), - isRequired: true, - }) - assert.Error(t, err) -} - -func TestGetEnvAsIntFunc(t *testing.T) { - // Test when environment variable exists and is an integer - os.Setenv("TEST_ENV", "123") - value, err := getEnvAsIntFunc(getEnvAsIntParams{ - name: "TEST_ENV", - isRequired: true, - }) - assert.NoError(t, err) - assert.Equal(t, 123, *value) - os.Unsetenv("TEST_ENV") - - // Test when environment variable does not exist, default value is provided, and is not required - value, err = getEnvAsIntFunc(getEnvAsIntParams{ - name: "NON_EXISTENT_ENV", - defaultValue: newDefaultValue(456), - }) - assert.NoError(t, err) - assert.Equal(t, 456, *value) - - // Test when environment variable does not exist, no default value is provided, and is required - // This should return an error - value, err = getEnvAsIntFunc(getEnvAsIntParams{ - name: "NON_EXISTENT_ENV", - isRequired: true, - }) - assert.Error(t, err) - assert.Nil(t, value) - - // Test when environment variable exists, is not an integer, no default value is provided, and is required - // This should return an error - os.Setenv("TEST_ENV", "not_an_integer") - value, err = getEnvAsIntFunc(getEnvAsIntParams{ - name: "TEST_ENV", - isRequired: true, - }) - assert.Error(t, err) - assert.Nil(t, value) - os.Unsetenv("TEST_ENV") - - // Test when environment variable exists, is not required, and no default value is provided - os.Setenv("TEST_ENV", "123") - value, err = getEnvAsIntFunc(getEnvAsIntParams{ - name: "TEST_ENV", - isRequired: false, - }) - assert.NoError(t, err) - assert.Equal(t, 123, *value) - os.Unsetenv("TEST_ENV") - - // Test when environment variable does not exist, is not required, and no default value is provided - value, err = getEnvAsIntFunc(getEnvAsIntParams{ - name: "NON_EXISTENT_ENV", - isRequired: false, - }) - assert.NoError(t, err) - assert.Nil(t, value) - - // Test when default value and required are both present - // This should return an error - _, err = getEnvAsIntFunc(getEnvAsIntParams{ - name: "NON_EXISTENT_ENV", - defaultValue: newDefaultValue(1), - isRequired: true, - }) - assert.Error(t, err) -} - -func TestGetEnvAsBoolFunc(t *testing.T) { - // Test when environment variable exists and is a boolean - os.Setenv("TEST_ENV", "true") - value, err := getEnvAsBoolFunc(getEnvAsBoolParams{ - name: "TEST_ENV", - isRequired: true, - }) - assert.NoError(t, err) - assert.Equal(t, true, *value) - os.Unsetenv("TEST_ENV") - - // Test when environment variable exists, is not a boolean, and is required - os.Setenv("TEST_ENV", "not_a_boolean") - _, err = getEnvAsBoolFunc(getEnvAsBoolParams{ - name: "TEST_ENV", - isRequired: true, - }) - assert.Error(t, err) - os.Unsetenv("TEST_ENV") - - // Test when environment variable exists, is not required, and no default value is provided - os.Setenv("TEST_ENV", "true") - value, err = getEnvAsBoolFunc(getEnvAsBoolParams{ - name: "TEST_ENV", - isRequired: false, - }) - assert.NoError(t, err) - assert.Equal(t, true, *value) - os.Unsetenv("TEST_ENV") - - // Test when environment variable does not exist, is not required, and no default value is provided - value, err = getEnvAsBoolFunc(getEnvAsBoolParams{ - name: "NON_EXISTENT_ENV", - isRequired: false, - }) - assert.NoError(t, err) - assert.Equal(t, false, *value) - - // Test when default value and required are both present - // This should return an error - _, err = getEnvAsBoolFunc(getEnvAsBoolParams{ - name: "NON_EXISTENT_ENV", - defaultValue: newDefaultValue(true), - isRequired: true, - }) - assert.Error(t, err) -} +package config + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetEnvAsStringFunc(t *testing.T) { + // Test when environment variable exists + os.Setenv("TEST_ENV", "test_value") + value, err := getEnvAsStringFunc(getEnvAsStringParams{ + name: "TEST_ENV", + isRequired: true, + }) + assert.NoError(t, err) + assert.Equal(t, "test_value", *value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable does not exist, default value is provided, and is not required + value, err = getEnvAsStringFunc(getEnvAsStringParams{ + name: "NON_EXISTENT_ENV", + defaultValue: newDefaultValue("default_value"), + isRequired: false, + }) + assert.NoError(t, err) + assert.Equal(t, "default_value", *value) + + // Test when environment variable does not exist, no default value is provided, and is required + // This should return an error + value, err = getEnvAsStringFunc(getEnvAsStringParams{ + name: "NON_EXISTENT_ENV", + isRequired: true, + }) + assert.Error(t, err) + assert.Nil(t, value) + + // Test when environment variable exists, default value is provided, and is required + os.Setenv("TEST_ENV", "test_value") + value, err = getEnvAsStringFunc(getEnvAsStringParams{ + name: "TEST_ENV", + defaultValue: newDefaultValue("default_value"), + }) + assert.NoError(t, err) + assert.Equal(t, "test_value", *value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable exists, is not required, and no default value is provided + os.Setenv("TEST_ENV", "test_value") + value, err = getEnvAsStringFunc(getEnvAsStringParams{ + name: "TEST_ENV", + isRequired: false, + }) + assert.NoError(t, err) + assert.Equal(t, "test_value", *value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable does not exist, is not required, and no default value is provided + value, err = getEnvAsStringFunc(getEnvAsStringParams{ + name: "NON_EXISTENT_ENV", + isRequired: false, + }) + assert.NoError(t, err) + assert.Nil(t, value) + + // Test when default value and required are both present + // This should return an error + _, err = getEnvAsStringFunc(getEnvAsStringParams{ + name: "NON_EXISTENT_ENV", + defaultValue: newDefaultValue("default_value"), + isRequired: true, + }) + assert.Error(t, err) +} + +func TestGetEnvAsIntFunc(t *testing.T) { + // Test when environment variable exists and is an integer + os.Setenv("TEST_ENV", "123") + value, err := getEnvAsIntFunc(getEnvAsIntParams{ + name: "TEST_ENV", + isRequired: true, + }) + assert.NoError(t, err) + assert.Equal(t, 123, *value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable does not exist, default value is provided, and is not required + value, err = getEnvAsIntFunc(getEnvAsIntParams{ + name: "NON_EXISTENT_ENV", + defaultValue: newDefaultValue(456), + }) + assert.NoError(t, err) + assert.Equal(t, 456, *value) + + // Test when environment variable does not exist, no default value is provided, and is required + // This should return an error + value, err = getEnvAsIntFunc(getEnvAsIntParams{ + name: "NON_EXISTENT_ENV", + isRequired: true, + }) + assert.Error(t, err) + assert.Nil(t, value) + + // Test when environment variable exists, is not an integer, no default value is provided, and is required + // This should return an error + os.Setenv("TEST_ENV", "not_an_integer") + value, err = getEnvAsIntFunc(getEnvAsIntParams{ + name: "TEST_ENV", + isRequired: true, + }) + assert.Error(t, err) + assert.Nil(t, value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable exists, is not required, and no default value is provided + os.Setenv("TEST_ENV", "123") + value, err = getEnvAsIntFunc(getEnvAsIntParams{ + name: "TEST_ENV", + isRequired: false, + }) + assert.NoError(t, err) + assert.Equal(t, 123, *value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable does not exist, is not required, and no default value is provided + value, err = getEnvAsIntFunc(getEnvAsIntParams{ + name: "NON_EXISTENT_ENV", + isRequired: false, + }) + assert.NoError(t, err) + assert.Nil(t, value) + + // Test when default value and required are both present + // This should return an error + _, err = getEnvAsIntFunc(getEnvAsIntParams{ + name: "NON_EXISTENT_ENV", + defaultValue: newDefaultValue(1), + isRequired: true, + }) + assert.Error(t, err) +} + +func TestGetEnvAsBoolFunc(t *testing.T) { + // Test when environment variable exists and is a boolean + os.Setenv("TEST_ENV", "true") + value, err := getEnvAsBoolFunc(getEnvAsBoolParams{ + name: "TEST_ENV", + isRequired: true, + }) + assert.NoError(t, err) + assert.Equal(t, true, *value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable exists, is not a boolean, and is required + os.Setenv("TEST_ENV", "not_a_boolean") + _, err = getEnvAsBoolFunc(getEnvAsBoolParams{ + name: "TEST_ENV", + isRequired: true, + }) + assert.Error(t, err) + os.Unsetenv("TEST_ENV") + + // Test when environment variable exists, is not required, and no default value is provided + os.Setenv("TEST_ENV", "true") + value, err = getEnvAsBoolFunc(getEnvAsBoolParams{ + name: "TEST_ENV", + isRequired: false, + }) + assert.NoError(t, err) + assert.Equal(t, true, *value) + os.Unsetenv("TEST_ENV") + + // Test when environment variable does not exist, is not required, and no default value is provided + value, err = getEnvAsBoolFunc(getEnvAsBoolParams{ + name: "NON_EXISTENT_ENV", + isRequired: false, + }) + assert.NoError(t, err) + assert.Equal(t, false, *value) + + // Test when default value and required are both present + // This should return an error + _, err = getEnvAsBoolFunc(getEnvAsBoolParams{ + name: "NON_EXISTENT_ENV", + defaultValue: newDefaultValue(true), + isRequired: true, + }) + assert.Error(t, err) +} diff --git a/internal/config/version.go b/internal/config/version.go index 452d73eb..49369931 100644 --- a/internal/config/version.go +++ b/internal/config/version.go @@ -1,3 +1,3 @@ -package config - -const Version = "v0.2.0" +package config + +const Version = "v0.2.0" diff --git a/internal/database/connect.go b/internal/database/connect.go index 7e07d2ac..b764d572 100644 --- a/internal/database/connect.go +++ b/internal/database/connect.go @@ -1,36 +1,36 @@ -package database - -import ( - "database/sql" - - "github.com/eduardolat/pgbackweb/internal/config" - "github.com/eduardolat/pgbackweb/internal/logger" - _ "github.com/lib/pq" -) - -func Connect(env *config.Env) *sql.DB { - db, err := sql.Open("postgres", *env.PBW_POSTGRES_CONN_STRING) - if err != nil { - logger.FatalError( - "could not connect to DB", - logger.KV{ - "error": err, - }, - ) - } - - err = db.Ping() - if err != nil { - logger.FatalError( - "could not ping DB", - logger.KV{ - "error": err, - }, - ) - } - - db.SetMaxOpenConns(10) - logger.Info("connected to DB") - - return db -} +package database + +import ( + "database/sql" + + "github.com/eduardolat/pgbackweb/internal/config" + "github.com/eduardolat/pgbackweb/internal/logger" + _ "github.com/lib/pq" +) + +func Connect(env *config.Env) *sql.DB { + db, err := sql.Open("postgres", *env.PBW_POSTGRES_CONN_STRING) + if err != nil { + logger.FatalError( + "could not connect to DB", + logger.KV{ + "error": err, + }, + ) + } + + err = db.Ping() + if err != nil { + logger.FatalError( + "could not ping DB", + logger.KV{ + "error": err, + }, + ) + } + + db.SetMaxOpenConns(10) + logger.Info("connected to DB") + + return db +} diff --git a/internal/database/migrations/20240818185840_add_webhooks_tables.sql b/internal/database/migrations/20240818185840_add_webhooks_tables.sql index f9ffc96c..4ad73f93 100644 --- a/internal/database/migrations/20240818185840_add_webhooks_tables.sql +++ b/internal/database/migrations/20240818185840_add_webhooks_tables.sql @@ -1,55 +1,55 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS webhooks ( - id UUID NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY, - - name TEXT NOT NULL, - is_active BOOLEAN NOT NULL DEFAULT FALSE, - - event_type TEXT NOT NULL CHECK (event_type IN ( - 'database_healthy', 'database_unhealthy', - 'destination_healthy', 'destination_unhealthy', - 'execution_success', 'execution_failed' - )), - target_ids UUID[] NOT NULL, -- database_id, restoration_id, etc. - - url TEXT NOT NULL, - method TEXT NOT NULL CHECK (method IN ('GET', 'POST')), - headers TEXT, -- user-defined headers in JSON format - body TEXT, -- user-defined body in JSON format - - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ -); - -CREATE TRIGGER webhooks_change_updated_at -BEFORE UPDATE ON webhooks FOR EACH ROW EXECUTE FUNCTION change_updated_at(); - -CREATE INDEX IF NOT EXISTS -idx_webhooks_target_ids ON webhooks USING GIN (target_ids); - -CREATE TABLE IF NOT EXISTS webhook_results ( - id UUID NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY, - webhook_id UUID NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE, - - req_method TEXT CHECK (req_method IN ('GET', 'POST')), - req_headers TEXT, - req_body TEXT, - - res_status SMALLINT, - res_headers TEXT, - res_body TEXT, - res_duration INTEGER, -- in milliseconds - - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS -idx_webhook_results_webhook_id ON webhook_results(webhook_id); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TABLE IF EXISTS webhook_results; -DROP TABLE IF EXISTS webhooks; --- +goose StatementEnd +-- +goose Up +-- +goose StatementBegin +CREATE TABLE IF NOT EXISTS webhooks ( + id UUID NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY, + + name TEXT NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT FALSE, + + event_type TEXT NOT NULL CHECK (event_type IN ( + 'database_healthy', 'database_unhealthy', + 'destination_healthy', 'destination_unhealthy', + 'execution_success', 'execution_failed' + )), + target_ids UUID[] NOT NULL, -- database_id, restoration_id, etc. + + url TEXT NOT NULL, + method TEXT NOT NULL CHECK (method IN ('GET', 'POST')), + headers TEXT, -- user-defined headers in JSON format + body TEXT, -- user-defined body in JSON format + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ +); + +CREATE TRIGGER webhooks_change_updated_at +BEFORE UPDATE ON webhooks FOR EACH ROW EXECUTE FUNCTION change_updated_at(); + +CREATE INDEX IF NOT EXISTS +idx_webhooks_target_ids ON webhooks USING GIN (target_ids); + +CREATE TABLE IF NOT EXISTS webhook_results ( + id UUID NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY, + webhook_id UUID NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE, + + req_method TEXT CHECK (req_method IN ('GET', 'POST')), + req_headers TEXT, + req_body TEXT, + + res_status SMALLINT, + res_headers TEXT, + res_body TEXT, + res_duration INTEGER, -- in milliseconds + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS +idx_webhook_results_webhook_id ON webhook_results(webhook_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS webhook_results; +DROP TABLE IF EXISTS webhooks; +-- +goose StatementEnd diff --git a/internal/integration/postgres/postgres.go b/internal/integration/postgres/postgres.go index 76729117..f578fa67 100644 --- a/internal/integration/postgres/postgres.go +++ b/internal/integration/postgres/postgres.go @@ -1,261 +1,261 @@ -package postgres - -import ( - "archive/zip" - "bytes" - "fmt" - "io" - "os" - "os/exec" - - "github.com/eduardolat/pgbackweb/internal/util/strutil" - "github.com/orsinium-labs/enum" -) - -/* - Important: - Versions supported by PG Back Web must be supported in PostgreSQL Version Policy - https://www.postgresql.org/support/versioning/ - - Backing up a database from an old unsupported version should not be allowed. -*/ - -type version struct { - version string - pgDump string - psql string -} - -type PGVersion enum.Member[version] - -var ( - PG13 = PGVersion{version{ - version: "13", - pgDump: "/usr/lib/postgresql/13/bin/pg_dump", - psql: "/usr/lib/postgresql/13/bin/psql", - }} - PG14 = PGVersion{version{ - version: "14", - pgDump: "/usr/lib/postgresql/14/bin/pg_dump", - psql: "/usr/lib/postgresql/14/bin/psql", - }} - PG15 = PGVersion{version{ - version: "15", - pgDump: "/usr/lib/postgresql/15/bin/pg_dump", - psql: "/usr/lib/postgresql/15/bin/psql", - }} - PG16 = PGVersion{version{ - version: "16", - pgDump: "/usr/lib/postgresql/16/bin/pg_dump", - psql: "/usr/lib/postgresql/16/bin/psql", - }} -) - -type Client struct{} - -func New() *Client { - return &Client{} -} - -// ParseVersion returns the PGVersion enum member for the given PostgreSQL -// version as a string. -func (Client) ParseVersion(version string) (PGVersion, error) { - switch version { - case "13": - return PG13, nil - case "14": - return PG14, nil - case "15": - return PG15, nil - case "16": - return PG16, nil - default: - return PGVersion{}, fmt.Errorf("pg version not allowed: %s", version) - } -} - -// Test tests the connection to the PostgreSQL database -func (Client) Test(version PGVersion, connString string) error { - cmd := exec.Command(version.Value.psql, connString, "-c", "SELECT 1;") - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf( - "error running psql test v%s: %s", - version.Value.version, output, - ) - } - - return nil -} - -// DumpParams contains the parameters for the pg_dump command -type DumpParams struct { - // DataOnly (--data-only): Dump only the data, not the schema (data definitions). - // Table data, large objects, and sequence values are dumped. - DataOnly bool - - // SchemaOnly (--schema-only): Dump only the object definitions (schema), not data. - SchemaOnly bool - - // Clean (--clean): Output commands to DROP all the dumped database objects - // prior to outputting the commands for creating them. This option is useful - // when the restore is to overwrite an existing database. If any of the - // objects do not exist in the destination database, ignorable error messages - // will be reported during restore, unless --if-exists is also specified. - Clean bool - - // IfExists (--if-exists): Use DROP ... IF EXISTS commands to drop objects in - // --clean mode. This suppresses “does not exist” errors that might otherwise - // be reported. This option is not valid unless --clean is also specified. - IfExists bool - - // Create (--create): Begin the output with a command to create the database - // itself and reconnect to the created database. (With a script of this form, - // it doesn't matter which database in the destination installation you - // connect to before running the script.) If --clean is also specified, the - // script drops and recreates the target database before reconnecting to it. - Create bool - - // NoComments (--no-comments): Do not dump comments. - NoComments bool -} - -// Dump runs the pg_dump command with the given parameters. It returns the SQL -// dump as an io.Reader. -func (Client) Dump( - version PGVersion, connString string, params ...DumpParams, -) io.Reader { - pickedParams := DumpParams{} - if len(params) > 0 { - pickedParams = params[0] - } - - args := []string{connString} - if pickedParams.DataOnly { - args = append(args, "--data-only") - } - if pickedParams.SchemaOnly { - args = append(args, "--schema-only") - } - if pickedParams.Clean { - args = append(args, "--clean") - } - if pickedParams.IfExists { - args = append(args, "--if-exists") - } - if pickedParams.Create { - args = append(args, "--create") - } - if pickedParams.NoComments { - args = append(args, "--no-comments") - } - - errorBuffer := &bytes.Buffer{} - reader, writer := io.Pipe() - cmd := exec.Command(version.Value.pgDump, args...) - cmd.Stdout = writer - cmd.Stderr = errorBuffer - - go func() { - defer writer.Close() - if err := cmd.Run(); err != nil { - writer.CloseWithError(fmt.Errorf( - "error running pg_dump v%s: %s", - version.Value.version, errorBuffer.String(), - )) - } - }() - - return reader -} - -// DumpZip runs the pg_dump command with the given parameters and returns the -// ZIP-compressed SQL dump as an io.Reader. -func (c *Client) DumpZip( - version PGVersion, connString string, params ...DumpParams, -) io.Reader { - dumpReader := c.Dump(version, connString, params...) - reader, writer := io.Pipe() - - go func() { - defer writer.Close() - - zipWriter := zip.NewWriter(writer) - defer zipWriter.Close() - - fileWriter, err := zipWriter.Create("dump.sql") - if err != nil { - writer.CloseWithError(fmt.Errorf("error creating zip file: %w", err)) - return - } - - if _, err := io.Copy(fileWriter, dumpReader); err != nil { - writer.CloseWithError(fmt.Errorf("error writing to zip file: %w", err)) - return - } - }() - - return reader -} - -// RestoreZip downloads or copies the ZIP from the given url or path, unzips it, -// and runs the psql command to restore the database. -// -// The ZIP file must contain a dump.sql file with the SQL dump to restore. -// -// - version: PostgreSQL version to use for the restore -// - connString: connection string to the database -// - isLocal: whether the ZIP file is local or a URL -// - zipURLOrPath: URL or path to the ZIP file -func (Client) RestoreZip( - version PGVersion, connString string, isLocal bool, zipURLOrPath string, -) error { - workDir, err := os.MkdirTemp("", "pbw-restore-*") - if err != nil { - return fmt.Errorf("error creating temp dir: %w", err) - } - defer os.RemoveAll(workDir) - zipPath := strutil.CreatePath(true, workDir, "dump.zip") - dumpPath := strutil.CreatePath(true, workDir, "dump.sql") - - if isLocal { - cmd := exec.Command("cp", zipURLOrPath, zipPath) - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("error copying ZIP file to temp dir: %s", output) - } - } - - if !isLocal { - cmd := exec.Command("wget", "--no-verbose", "-O", zipPath, zipURLOrPath) - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("error downloading ZIP file: %s", output) - } - } - - if _, err := os.Stat(zipPath); os.IsNotExist(err) { - return fmt.Errorf("zip file not found: %s", zipPath) - } - - cmd := exec.Command("unzip", "-o", zipPath, "dump.sql", "-d", workDir) - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("error unzipping ZIP file: %s", output) - } - - if _, err := os.Stat(dumpPath); os.IsNotExist(err) { - return fmt.Errorf("dump.sql file not found in ZIP file: %s", zipPath) - } - - cmd = exec.Command(version.Value.psql, connString, "-f", dumpPath) - output, err = cmd.CombinedOutput() - if err != nil { - return fmt.Errorf( - "error running psql v%s command: %s", - version.Value.version, output, - ) - } - - return nil -} +package postgres + +import ( + "archive/zip" + "bytes" + "fmt" + "io" + "os" + "os/exec" + + "github.com/eduardolat/pgbackweb/internal/util/strutil" + "github.com/orsinium-labs/enum" +) + +/* + Important: + Versions supported by PG Back Web must be supported in PostgreSQL Version Policy + https://www.postgresql.org/support/versioning/ + + Backing up a database from an old unsupported version should not be allowed. +*/ + +type version struct { + version string + pgDump string + psql string +} + +type PGVersion enum.Member[version] + +var ( + PG13 = PGVersion{version{ + version: "13", + pgDump: "/usr/lib/postgresql/13/bin/pg_dump", + psql: "/usr/lib/postgresql/13/bin/psql", + }} + PG14 = PGVersion{version{ + version: "14", + pgDump: "/usr/lib/postgresql/14/bin/pg_dump", + psql: "/usr/lib/postgresql/14/bin/psql", + }} + PG15 = PGVersion{version{ + version: "15", + pgDump: "/usr/lib/postgresql/15/bin/pg_dump", + psql: "/usr/lib/postgresql/15/bin/psql", + }} + PG16 = PGVersion{version{ + version: "16", + pgDump: "/usr/lib/postgresql/16/bin/pg_dump", + psql: "/usr/lib/postgresql/16/bin/psql", + }} +) + +type Client struct{} + +func New() *Client { + return &Client{} +} + +// ParseVersion returns the PGVersion enum member for the given PostgreSQL +// version as a string. +func (Client) ParseVersion(version string) (PGVersion, error) { + switch version { + case "13": + return PG13, nil + case "14": + return PG14, nil + case "15": + return PG15, nil + case "16": + return PG16, nil + default: + return PGVersion{}, fmt.Errorf("pg version not allowed: %s", version) + } +} + +// Test tests the connection to the PostgreSQL database +func (Client) Test(version PGVersion, connString string) error { + cmd := exec.Command(version.Value.psql, connString, "-c", "SELECT 1;") + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf( + "error running psql test v%s: %s", + version.Value.version, output, + ) + } + + return nil +} + +// DumpParams contains the parameters for the pg_dump command +type DumpParams struct { + // DataOnly (--data-only): Dump only the data, not the schema (data definitions). + // Table data, large objects, and sequence values are dumped. + DataOnly bool + + // SchemaOnly (--schema-only): Dump only the object definitions (schema), not data. + SchemaOnly bool + + // Clean (--clean): Output commands to DROP all the dumped database objects + // prior to outputting the commands for creating them. This option is useful + // when the restore is to overwrite an existing database. If any of the + // objects do not exist in the destination database, ignorable error messages + // will be reported during restore, unless --if-exists is also specified. + Clean bool + + // IfExists (--if-exists): Use DROP ... IF EXISTS commands to drop objects in + // --clean mode. This suppresses “does not exist” errors that might otherwise + // be reported. This option is not valid unless --clean is also specified. + IfExists bool + + // Create (--create): Begin the output with a command to create the database + // itself and reconnect to the created database. (With a script of this form, + // it doesn't matter which database in the destination installation you + // connect to before running the script.) If --clean is also specified, the + // script drops and recreates the target database before reconnecting to it. + Create bool + + // NoComments (--no-comments): Do not dump comments. + NoComments bool +} + +// Dump runs the pg_dump command with the given parameters. It returns the SQL +// dump as an io.Reader. +func (Client) Dump( + version PGVersion, connString string, params ...DumpParams, +) io.Reader { + pickedParams := DumpParams{} + if len(params) > 0 { + pickedParams = params[0] + } + + args := []string{connString} + if pickedParams.DataOnly { + args = append(args, "--data-only") + } + if pickedParams.SchemaOnly { + args = append(args, "--schema-only") + } + if pickedParams.Clean { + args = append(args, "--clean") + } + if pickedParams.IfExists { + args = append(args, "--if-exists") + } + if pickedParams.Create { + args = append(args, "--create") + } + if pickedParams.NoComments { + args = append(args, "--no-comments") + } + + errorBuffer := &bytes.Buffer{} + reader, writer := io.Pipe() + cmd := exec.Command(version.Value.pgDump, args...) + cmd.Stdout = writer + cmd.Stderr = errorBuffer + + go func() { + defer writer.Close() + if err := cmd.Run(); err != nil { + writer.CloseWithError(fmt.Errorf( + "error running pg_dump v%s: %s", + version.Value.version, errorBuffer.String(), + )) + } + }() + + return reader +} + +// DumpZip runs the pg_dump command with the given parameters and returns the +// ZIP-compressed SQL dump as an io.Reader. +func (c *Client) DumpZip( + version PGVersion, connString string, params ...DumpParams, +) io.Reader { + dumpReader := c.Dump(version, connString, params...) + reader, writer := io.Pipe() + + go func() { + defer writer.Close() + + zipWriter := zip.NewWriter(writer) + defer zipWriter.Close() + + fileWriter, err := zipWriter.Create("dump.sql") + if err != nil { + writer.CloseWithError(fmt.Errorf("error creating zip file: %w", err)) + return + } + + if _, err := io.Copy(fileWriter, dumpReader); err != nil { + writer.CloseWithError(fmt.Errorf("error writing to zip file: %w", err)) + return + } + }() + + return reader +} + +// RestoreZip downloads or copies the ZIP from the given url or path, unzips it, +// and runs the psql command to restore the database. +// +// The ZIP file must contain a dump.sql file with the SQL dump to restore. +// +// - version: PostgreSQL version to use for the restore +// - connString: connection string to the database +// - isLocal: whether the ZIP file is local or a URL +// - zipURLOrPath: URL or path to the ZIP file +func (Client) RestoreZip( + version PGVersion, connString string, isLocal bool, zipURLOrPath string, +) error { + workDir, err := os.MkdirTemp("", "pbw-restore-*") + if err != nil { + return fmt.Errorf("error creating temp dir: %w", err) + } + defer os.RemoveAll(workDir) + zipPath := strutil.CreatePath(true, workDir, "dump.zip") + dumpPath := strutil.CreatePath(true, workDir, "dump.sql") + + if isLocal { + cmd := exec.Command("cp", zipURLOrPath, zipPath) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("error copying ZIP file to temp dir: %s", output) + } + } + + if !isLocal { + cmd := exec.Command("wget", "--no-verbose", "-O", zipPath, zipURLOrPath) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("error downloading ZIP file: %s", output) + } + } + + if _, err := os.Stat(zipPath); os.IsNotExist(err) { + return fmt.Errorf("zip file not found: %s", zipPath) + } + + cmd := exec.Command("unzip", "-o", zipPath, "dump.sql", "-d", workDir) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("error unzipping ZIP file: %s", output) + } + + if _, err := os.Stat(dumpPath); os.IsNotExist(err) { + return fmt.Errorf("dump.sql file not found in ZIP file: %s", zipPath) + } + + cmd = exec.Command(version.Value.psql, connString, "-f", dumpPath) + output, err = cmd.CombinedOutput() + if err != nil { + return fmt.Errorf( + "error running psql v%s command: %s", + version.Value.version, output, + ) + } + + return nil +} diff --git a/internal/integration/storage/s3.go b/internal/integration/storage/s3.go index 91055c2e..87707743 100644 --- a/internal/integration/storage/s3.go +++ b/internal/integration/storage/s3.go @@ -1,142 +1,142 @@ -package storage - -import ( - "fmt" - "io" - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3" - "github.com/aws/aws-sdk-go/service/s3/s3manager" - "github.com/eduardolat/pgbackweb/internal/util/strutil" -) - -// createS3Client creates a new S3 client -func createS3Client( - accessKey, secretKey, region, endpoint string, -) (*s3.S3, error) { - sess, err := session.NewSession(&aws.Config{ - Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""), - Region: aws.String(region), - Endpoint: aws.String(endpoint), - S3ForcePathStyle: aws.Bool(true), - }) - if err != nil { - return nil, fmt.Errorf("failed to create aws session: %w", err) - } - - return s3.New(sess), nil -} - -// S3Test tests the connection to S3 -func (Client) S3Test( - accessKey, secretKey, region, endpoint, bucketName string, -) error { - s3Client, err := createS3Client( - accessKey, secretKey, region, endpoint, - ) - if err != nil { - return err - } - - _, err = s3Client.HeadBucket(&s3.HeadBucketInput{ - Bucket: aws.String(bucketName), - }) - if err != nil { - return fmt.Errorf("failed to test S3 bucket: %w", err) - } - - return nil -} - -// S3Upload uploads a file to S3 from a reader. -// -// Returns the file size, in bytes. -func (Client) S3Upload( - accessKey, secretKey, region, endpoint, bucketName, key string, - fileReader io.Reader, -) (int64, error) { - s3Client, err := createS3Client( - accessKey, secretKey, region, endpoint, - ) - if err != nil { - return 0, err - } - - key = strutil.RemoveLeadingSlash(key) - contentType := strutil.GetContentTypeFromFileName(key) - - uploader := s3manager.NewUploaderWithClient(s3Client) - _, err = uploader.Upload(&s3manager.UploadInput{ - Bucket: aws.String(bucketName), - Key: aws.String(key), - Body: fileReader, - ContentType: aws.String(contentType), - }) - if err != nil { - return 0, fmt.Errorf("failed to upload file to S3: %w", err) - } - - fileHead, err := s3Client.HeadObject(&s3.HeadObjectInput{ - Bucket: aws.String(bucketName), - Key: aws.String(key), - }) - if err != nil { - return 0, fmt.Errorf("failed to get uploaded file info from S3: %w", err) - } - fileSize := *fileHead.ContentLength - - return fileSize, nil -} - -// S3Delete deletes a file from S3 -func (Client) S3Delete( - accessKey, secretKey, region, endpoint, bucketName, key string, -) error { - s3Client, err := createS3Client( - accessKey, secretKey, region, endpoint, - ) - if err != nil { - return err - } - - key = strutil.RemoveLeadingSlash(key) - - _, err = s3Client.DeleteObject(&s3.DeleteObjectInput{ - Bucket: aws.String(bucketName), - Key: aws.String(key), - }) - if err != nil { - return fmt.Errorf("failed to delete file from S3: %w", err) - } - - return nil -} - -// S3GetDownloadLink generates a presigned URL for downloading a file from S3 -func (Client) S3GetDownloadLink( - accessKey, secretKey, region, endpoint, bucketName, key string, - expiration time.Duration, -) (string, error) { - s3Client, err := createS3Client( - accessKey, secretKey, region, endpoint, - ) - if err != nil { - return "", err - } - - key = strutil.RemoveLeadingSlash(key) - req, _ := s3Client.GetObjectRequest(&s3.GetObjectInput{ - Bucket: aws.String(bucketName), - Key: aws.String(key), - }) - - url, err := req.Presign(expiration) - if err != nil { - return "", fmt.Errorf("failed to generate presigned URL: %w", err) - } - - return url, nil -} +package storage + +import ( + "fmt" + "io" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "github.com/eduardolat/pgbackweb/internal/util/strutil" +) + +// createS3Client creates a new S3 client +func createS3Client( + accessKey, secretKey, region, endpoint string, +) (*s3.S3, error) { + sess, err := session.NewSession(&aws.Config{ + Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""), + Region: aws.String(region), + Endpoint: aws.String(endpoint), + S3ForcePathStyle: aws.Bool(true), + }) + if err != nil { + return nil, fmt.Errorf("failed to create aws session: %w", err) + } + + return s3.New(sess), nil +} + +// S3Test tests the connection to S3 +func (Client) S3Test( + accessKey, secretKey, region, endpoint, bucketName string, +) error { + s3Client, err := createS3Client( + accessKey, secretKey, region, endpoint, + ) + if err != nil { + return err + } + + _, err = s3Client.HeadBucket(&s3.HeadBucketInput{ + Bucket: aws.String(bucketName), + }) + if err != nil { + return fmt.Errorf("failed to test S3 bucket: %w", err) + } + + return nil +} + +// S3Upload uploads a file to S3 from a reader. +// +// Returns the file size, in bytes. +func (Client) S3Upload( + accessKey, secretKey, region, endpoint, bucketName, key string, + fileReader io.Reader, +) (int64, error) { + s3Client, err := createS3Client( + accessKey, secretKey, region, endpoint, + ) + if err != nil { + return 0, err + } + + key = strutil.RemoveLeadingSlash(key) + contentType := strutil.GetContentTypeFromFileName(key) + + uploader := s3manager.NewUploaderWithClient(s3Client) + _, err = uploader.Upload(&s3manager.UploadInput{ + Bucket: aws.String(bucketName), + Key: aws.String(key), + Body: fileReader, + ContentType: aws.String(contentType), + }) + if err != nil { + return 0, fmt.Errorf("failed to upload file to S3: %w", err) + } + + fileHead, err := s3Client.HeadObject(&s3.HeadObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(key), + }) + if err != nil { + return 0, fmt.Errorf("failed to get uploaded file info from S3: %w", err) + } + fileSize := *fileHead.ContentLength + + return fileSize, nil +} + +// S3Delete deletes a file from S3 +func (Client) S3Delete( + accessKey, secretKey, region, endpoint, bucketName, key string, +) error { + s3Client, err := createS3Client( + accessKey, secretKey, region, endpoint, + ) + if err != nil { + return err + } + + key = strutil.RemoveLeadingSlash(key) + + _, err = s3Client.DeleteObject(&s3.DeleteObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(key), + }) + if err != nil { + return fmt.Errorf("failed to delete file from S3: %w", err) + } + + return nil +} + +// S3GetDownloadLink generates a presigned URL for downloading a file from S3 +func (Client) S3GetDownloadLink( + accessKey, secretKey, region, endpoint, bucketName, key string, + expiration time.Duration, +) (string, error) { + s3Client, err := createS3Client( + accessKey, secretKey, region, endpoint, + ) + if err != nil { + return "", err + } + + key = strutil.RemoveLeadingSlash(key) + req, _ := s3Client.GetObjectRequest(&s3.GetObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(key), + }) + + url, err := req.Presign(expiration) + if err != nil { + return "", fmt.Errorf("failed to generate presigned URL: %w", err) + } + + return url, nil +} diff --git a/internal/logger/REAME.md b/internal/logger/REAME.md index 00de7bcd..d66409ca 100644 --- a/internal/logger/REAME.md +++ b/internal/logger/REAME.md @@ -1,11 +1,11 @@ -# Logger - -This package includes a custom logger for the project. - -Functions are exposed for logging messages easily; it is imperative that -these functions are used throughout the project to maintain a standard in -log messages. - -If necessary, the `logWriter` can be edited so that in addition to writing to -stdout, it can also write to a file or send the logs to a server like -Better Stack or New Relic. +# Logger + +This package includes a custom logger for the project. + +Functions are exposed for logging messages easily; it is imperative that +these functions are used throughout the project to maintain a standard in +log messages. + +If necessary, the `logWriter` can be edited so that in addition to writing to +stdout, it can also write to a file or send the logs to a server like +Better Stack or New Relic. diff --git a/internal/service/auth/auth.go b/internal/service/auth/auth.go index b5983fdf..7a4fa48f 100644 --- a/internal/service/auth/auth.go +++ b/internal/service/auth/auth.go @@ -1,24 +1,24 @@ -package auth - -import ( - "time" - - "github.com/eduardolat/pgbackweb/internal/config" - "github.com/eduardolat/pgbackweb/internal/database/dbgen" -) - -const ( - maxSessionAge = time.Hour * 12 -) - -type Service struct { - env *config.Env - dbgen *dbgen.Queries -} - -func New(env *config.Env, dbgen *dbgen.Queries) *Service { - return &Service{ - env: env, - dbgen: dbgen, - } -} +package auth + +import ( + "time" + + "github.com/eduardolat/pgbackweb/internal/config" + "github.com/eduardolat/pgbackweb/internal/database/dbgen" +) + +const ( + maxSessionAge = time.Hour * 12 +) + +type Service struct { + env *config.Env + dbgen *dbgen.Queries +} + +func New(env *config.Env, dbgen *dbgen.Queries) *Service { + return &Service{ + env: env, + dbgen: dbgen, + } +} diff --git a/internal/service/auth/cookies.go b/internal/service/auth/cookies.go index b6bfbcf4..1ef5cf3a 100644 --- a/internal/service/auth/cookies.go +++ b/internal/service/auth/cookies.go @@ -1,57 +1,57 @@ -package auth - -import ( - "errors" - "net/http" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/labstack/echo/v4" -) - -const ( - sessionCookieName = "pbw_session" -) - -func (s *Service) SetSessionCookie(c echo.Context, token string) { - cookie := http.Cookie{ - Name: sessionCookieName, - Value: token, - MaxAge: int(maxSessionAge.Seconds()), - HttpOnly: true, - Path: "/", - SameSite: http.SameSiteStrictMode, - } - c.SetCookie(&cookie) -} - -func (s *Service) ClearSessionCookie(c echo.Context) { - cookie := http.Cookie{ - Name: sessionCookieName, - Value: "", - MaxAge: -1, - HttpOnly: true, - Path: "/", - SameSite: http.SameSiteStrictMode, - } - c.SetCookie(&cookie) -} - -func (s *Service) GetUserFromSessionCookie(c echo.Context) ( - bool, dbgen.AuthServiceGetUserByTokenRow, error, -) { - ctx := c.Request().Context() - - cookie, err := c.Cookie(sessionCookieName) - if err != nil && errors.Is(err, http.ErrNoCookie) { - return false, dbgen.AuthServiceGetUserByTokenRow{}, nil - } - if err != nil { - return false, dbgen.AuthServiceGetUserByTokenRow{}, err - } - - if cookie.Value == "" { - return false, dbgen.AuthServiceGetUserByTokenRow{}, nil - } - - return s.GetUserByToken(ctx, cookie.Value) -} +package auth + +import ( + "errors" + "net/http" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/labstack/echo/v4" +) + +const ( + sessionCookieName = "pbw_session" +) + +func (s *Service) SetSessionCookie(c echo.Context, token string) { + cookie := http.Cookie{ + Name: sessionCookieName, + Value: token, + MaxAge: int(maxSessionAge.Seconds()), + HttpOnly: true, + Path: "/", + SameSite: http.SameSiteStrictMode, + } + c.SetCookie(&cookie) +} + +func (s *Service) ClearSessionCookie(c echo.Context) { + cookie := http.Cookie{ + Name: sessionCookieName, + Value: "", + MaxAge: -1, + HttpOnly: true, + Path: "/", + SameSite: http.SameSiteStrictMode, + } + c.SetCookie(&cookie) +} + +func (s *Service) GetUserFromSessionCookie(c echo.Context) ( + bool, dbgen.AuthServiceGetUserByTokenRow, error, +) { + ctx := c.Request().Context() + + cookie, err := c.Cookie(sessionCookieName) + if err != nil && errors.Is(err, http.ErrNoCookie) { + return false, dbgen.AuthServiceGetUserByTokenRow{}, nil + } + if err != nil { + return false, dbgen.AuthServiceGetUserByTokenRow{}, err + } + + if cookie.Value == "" { + return false, dbgen.AuthServiceGetUserByTokenRow{}, nil + } + + return s.GetUserByToken(ctx, cookie.Value) +} diff --git a/internal/service/auth/get_user_by_token.go b/internal/service/auth/get_user_by_token.go index 927f3f6a..2ce5ca48 100644 --- a/internal/service/auth/get_user_by_token.go +++ b/internal/service/auth/get_user_by_token.go @@ -1,28 +1,28 @@ -package auth - -import ( - "context" - "database/sql" - "errors" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" -) - -func (s *Service) GetUserByToken( - ctx context.Context, token string, -) (bool, dbgen.AuthServiceGetUserByTokenRow, error) { - user, err := s.dbgen.AuthServiceGetUserByToken( - ctx, dbgen.AuthServiceGetUserByTokenParams{ - Token: token, - EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, - }, - ) - if err != nil && errors.Is(err, sql.ErrNoRows) { - return false, user, nil - } - if err != nil { - return false, user, err - } - - return true, user, nil -} +package auth + +import ( + "context" + "database/sql" + "errors" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" +) + +func (s *Service) GetUserByToken( + ctx context.Context, token string, +) (bool, dbgen.AuthServiceGetUserByTokenRow, error) { + user, err := s.dbgen.AuthServiceGetUserByToken( + ctx, dbgen.AuthServiceGetUserByTokenParams{ + Token: token, + EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, + }, + ) + if err != nil && errors.Is(err, sql.ErrNoRows) { + return false, user, nil + } + if err != nil { + return false, user, err + } + + return true, user, nil +} diff --git a/internal/service/auth/login.go b/internal/service/auth/login.go index 6ccd9caa..0d8090de 100644 --- a/internal/service/auth/login.go +++ b/internal/service/auth/login.go @@ -1,38 +1,38 @@ -package auth - -import ( - "context" - "fmt" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/eduardolat/pgbackweb/internal/util/cryptoutil" - "github.com/google/uuid" -) - -func (s *Service) Login( - ctx context.Context, email, password, ip, userAgent string, -) (dbgen.AuthServiceLoginCreateSessionRow, error) { - user, err := s.dbgen.AuthServiceLoginGetUserByEmail(ctx, email) - if err != nil { - return dbgen.AuthServiceLoginCreateSessionRow{}, err - } - - if err := cryptoutil.VerifyBcryptHash(password, user.Password); err != nil { - return dbgen.AuthServiceLoginCreateSessionRow{}, fmt.Errorf("invalid password") - } - - session, err := s.dbgen.AuthServiceLoginCreateSession( - ctx, dbgen.AuthServiceLoginCreateSessionParams{ - UserID: user.ID, - Ip: ip, - UserAgent: userAgent, - Token: uuid.NewString(), - EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, - }, - ) - if err != nil { - return dbgen.AuthServiceLoginCreateSessionRow{}, err - } - - return session, nil -} +package auth + +import ( + "context" + "fmt" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/cryptoutil" + "github.com/google/uuid" +) + +func (s *Service) Login( + ctx context.Context, email, password, ip, userAgent string, +) (dbgen.AuthServiceLoginCreateSessionRow, error) { + user, err := s.dbgen.AuthServiceLoginGetUserByEmail(ctx, email) + if err != nil { + return dbgen.AuthServiceLoginCreateSessionRow{}, err + } + + if err := cryptoutil.VerifyBcryptHash(password, user.Password); err != nil { + return dbgen.AuthServiceLoginCreateSessionRow{}, fmt.Errorf("invalid password") + } + + session, err := s.dbgen.AuthServiceLoginCreateSession( + ctx, dbgen.AuthServiceLoginCreateSessionParams{ + UserID: user.ID, + Ip: ip, + UserAgent: userAgent, + Token: uuid.NewString(), + EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, + }, + ) + if err != nil { + return dbgen.AuthServiceLoginCreateSessionRow{}, err + } + + return session, nil +} diff --git a/internal/service/backups/get_backups_qty.sql b/internal/service/backups/get_backups_qty.sql index 5f462aaa..d73fd19e 100644 --- a/internal/service/backups/get_backups_qty.sql +++ b/internal/service/backups/get_backups_qty.sql @@ -1,6 +1,6 @@ --- name: BackupsServiceGetBackupsQty :one -SELECT - COUNT(*) AS all, - SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS active, - SUM(CASE WHEN is_active = false THEN 1 ELSE 0 END) AS inactive -FROM backups; +-- name: BackupsServiceGetBackupsQty :one +SELECT + COUNT(*) AS all, + SUM(CASE WHEN is_active = true THEN 1 ELSE 0 END) AS active, + SUM(CASE WHEN is_active = false THEN 1 ELSE 0 END) AS inactive +FROM backups; diff --git a/internal/service/databases/create_database.go b/internal/service/databases/create_database.go index d17a2b6d..1d8349e9 100644 --- a/internal/service/databases/create_database.go +++ b/internal/service/databases/create_database.go @@ -1,23 +1,23 @@ -package databases - -import ( - "context" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" -) - -func (s *Service) CreateDatabase( - ctx context.Context, params dbgen.DatabasesServiceCreateDatabaseParams, -) (dbgen.Database, error) { - err := s.TestDatabase(ctx, params.PgVersion, params.ConnectionString) - if err != nil { - return dbgen.Database{}, err - } - - params.EncryptionKey = *s.env.PBW_ENCRYPTION_KEY - db, err := s.dbgen.DatabasesServiceCreateDatabase(ctx, params) - - _ = s.TestDatabaseAndStoreResult(ctx, db.ID) - - return db, err -} +package databases + +import ( + "context" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" +) + +func (s *Service) CreateDatabase( + ctx context.Context, params dbgen.DatabasesServiceCreateDatabaseParams, +) (dbgen.Database, error) { + err := s.TestDatabase(ctx, params.PgVersion, params.ConnectionString) + if err != nil { + return dbgen.Database{}, err + } + + params.EncryptionKey = *s.env.PBW_ENCRYPTION_KEY + db, err := s.dbgen.DatabasesServiceCreateDatabase(ctx, params) + + _ = s.TestDatabaseAndStoreResult(ctx, db.ID) + + return db, err +} diff --git a/internal/service/databases/databases.go b/internal/service/databases/databases.go index 920bc326..0be25403 100644 --- a/internal/service/databases/databases.go +++ b/internal/service/databases/databases.go @@ -1,27 +1,27 @@ -package databases - -import ( - "github.com/eduardolat/pgbackweb/internal/config" - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/eduardolat/pgbackweb/internal/integration" - "github.com/eduardolat/pgbackweb/internal/service/webhooks" -) - -type Service struct { - env *config.Env - dbgen *dbgen.Queries - ints *integration.Integration - webhooksService *webhooks.Service -} - -func New( - env *config.Env, dbgen *dbgen.Queries, ints *integration.Integration, - webhooksService *webhooks.Service, -) *Service { - return &Service{ - env: env, - dbgen: dbgen, - ints: ints, - webhooksService: webhooksService, - } -} +package databases + +import ( + "github.com/eduardolat/pgbackweb/internal/config" + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/integration" + "github.com/eduardolat/pgbackweb/internal/service/webhooks" +) + +type Service struct { + env *config.Env + dbgen *dbgen.Queries + ints *integration.Integration + webhooksService *webhooks.Service +} + +func New( + env *config.Env, dbgen *dbgen.Queries, ints *integration.Integration, + webhooksService *webhooks.Service, +) *Service { + return &Service{ + env: env, + dbgen: dbgen, + ints: ints, + webhooksService: webhooksService, + } +} diff --git a/internal/service/databases/get_all_databases.go b/internal/service/databases/get_all_databases.go index 8a55b320..f995a244 100644 --- a/internal/service/databases/get_all_databases.go +++ b/internal/service/databases/get_all_databases.go @@ -1,15 +1,15 @@ -package databases - -import ( - "context" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" -) - -func (s *Service) GetAllDatabases( - ctx context.Context, -) ([]dbgen.DatabasesServiceGetAllDatabasesRow, error) { - return s.dbgen.DatabasesServiceGetAllDatabases( - ctx, *s.env.PBW_ENCRYPTION_KEY, - ) -} +package databases + +import ( + "context" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" +) + +func (s *Service) GetAllDatabases( + ctx context.Context, +) ([]dbgen.DatabasesServiceGetAllDatabasesRow, error) { + return s.dbgen.DatabasesServiceGetAllDatabases( + ctx, *s.env.PBW_ENCRYPTION_KEY, + ) +} diff --git a/internal/service/databases/get_database.go b/internal/service/databases/get_database.go index 401e69b1..38cf9135 100644 --- a/internal/service/databases/get_database.go +++ b/internal/service/databases/get_database.go @@ -1,20 +1,20 @@ -package databases - -import ( - "context" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/google/uuid" -) - -// GetDatabase retrieves a database entry by ID. -func (s *Service) GetDatabase( - ctx context.Context, id uuid.UUID, -) (dbgen.DatabasesServiceGetDatabaseRow, error) { - return s.dbgen.DatabasesServiceGetDatabase( - ctx, dbgen.DatabasesServiceGetDatabaseParams{ - ID: id, - EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, - }, - ) -} +package databases + +import ( + "context" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/google/uuid" +) + +// GetDatabase retrieves a database entry by ID. +func (s *Service) GetDatabase( + ctx context.Context, id uuid.UUID, +) (dbgen.DatabasesServiceGetDatabaseRow, error) { + return s.dbgen.DatabasesServiceGetDatabase( + ctx, dbgen.DatabasesServiceGetDatabaseParams{ + ID: id, + EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, + }, + ) +} diff --git a/internal/service/databases/get_databases_qty.sql b/internal/service/databases/get_databases_qty.sql index 2693c57e..fd4e1ebe 100644 --- a/internal/service/databases/get_databases_qty.sql +++ b/internal/service/databases/get_databases_qty.sql @@ -1,6 +1,6 @@ --- name: DatabasesServiceGetDatabasesQty :one -SELECT - COUNT(*) AS all, - SUM(CASE WHEN test_ok = true THEN 1 ELSE 0 END) AS healthy, - SUM(CASE WHEN test_ok = false THEN 1 ELSE 0 END) AS unhealthy -FROM databases; +-- name: DatabasesServiceGetDatabasesQty :one +SELECT + COUNT(*) AS all, + SUM(CASE WHEN test_ok = true THEN 1 ELSE 0 END) AS healthy, + SUM(CASE WHEN test_ok = false THEN 1 ELSE 0 END) AS unhealthy +FROM databases; diff --git a/internal/service/databases/paginate_databases.go b/internal/service/databases/paginate_databases.go index f19e7eea..5a0f431e 100644 --- a/internal/service/databases/paginate_databases.go +++ b/internal/service/databases/paginate_databases.go @@ -1,45 +1,45 @@ -package databases - -import ( - "context" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/eduardolat/pgbackweb/internal/util/paginateutil" -) - -type PaginateDatabasesParams struct { - Page int - Limit int -} - -func (s *Service) PaginateDatabases( - ctx context.Context, params PaginateDatabasesParams, -) (paginateutil.PaginateResponse, []dbgen.DatabasesServicePaginateDatabasesRow, error) { - page := max(params.Page, 1) - limit := min(max(params.Limit, 1), 100) - - count, err := s.dbgen.DatabasesServicePaginateDatabasesCount(ctx) - if err != nil { - return paginateutil.PaginateResponse{}, nil, err - } - - paginateParams := paginateutil.PaginateParams{ - Page: page, - Limit: limit, - } - offset := paginateutil.CreateOffsetFromParams(paginateParams) - paginateResponse := paginateutil.CreatePaginateResponse(paginateParams, int(count)) - - databases, err := s.dbgen.DatabasesServicePaginateDatabases( - ctx, dbgen.DatabasesServicePaginateDatabasesParams{ - EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, - Limit: int32(params.Limit), - Offset: int32(offset), - }, - ) - if err != nil { - return paginateutil.PaginateResponse{}, nil, err - } - - return paginateResponse, databases, nil -} +package databases + +import ( + "context" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/paginateutil" +) + +type PaginateDatabasesParams struct { + Page int + Limit int +} + +func (s *Service) PaginateDatabases( + ctx context.Context, params PaginateDatabasesParams, +) (paginateutil.PaginateResponse, []dbgen.DatabasesServicePaginateDatabasesRow, error) { + page := max(params.Page, 1) + limit := min(max(params.Limit, 1), 100) + + count, err := s.dbgen.DatabasesServicePaginateDatabasesCount(ctx) + if err != nil { + return paginateutil.PaginateResponse{}, nil, err + } + + paginateParams := paginateutil.PaginateParams{ + Page: page, + Limit: limit, + } + offset := paginateutil.CreateOffsetFromParams(paginateParams) + paginateResponse := paginateutil.CreatePaginateResponse(paginateParams, int(count)) + + databases, err := s.dbgen.DatabasesServicePaginateDatabases( + ctx, dbgen.DatabasesServicePaginateDatabasesParams{ + EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, + Limit: int32(params.Limit), + Offset: int32(offset), + }, + ) + if err != nil { + return paginateutil.PaginateResponse{}, nil, err + } + + return paginateResponse, databases, nil +} diff --git a/internal/service/databases/update_database.go b/internal/service/databases/update_database.go index b0c53674..8fa637f2 100644 --- a/internal/service/databases/update_database.go +++ b/internal/service/databases/update_database.go @@ -1,26 +1,26 @@ -package databases - -import ( - "context" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" -) - -// UpdateDatabase updates an existing database entry. -func (s *Service) UpdateDatabase( - ctx context.Context, params dbgen.DatabasesServiceUpdateDatabaseParams, -) (dbgen.Database, error) { - err := s.TestDatabase( - ctx, params.PgVersion.String, params.ConnectionString.String, - ) - if err != nil { - return dbgen.Database{}, err - } - - params.EncryptionKey = *s.env.PBW_ENCRYPTION_KEY - db, err := s.dbgen.DatabasesServiceUpdateDatabase(ctx, params) - - _ = s.TestDatabaseAndStoreResult(ctx, db.ID) - - return db, err -} +package databases + +import ( + "context" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" +) + +// UpdateDatabase updates an existing database entry. +func (s *Service) UpdateDatabase( + ctx context.Context, params dbgen.DatabasesServiceUpdateDatabaseParams, +) (dbgen.Database, error) { + err := s.TestDatabase( + ctx, params.PgVersion.String, params.ConnectionString.String, + ) + if err != nil { + return dbgen.Database{}, err + } + + params.EncryptionKey = *s.env.PBW_ENCRYPTION_KEY + db, err := s.dbgen.DatabasesServiceUpdateDatabase(ctx, params) + + _ = s.TestDatabaseAndStoreResult(ctx, db.ID) + + return db, err +} diff --git a/internal/service/destinations/create_destination.go b/internal/service/destinations/create_destination.go index 9ade7803..759227b8 100644 --- a/internal/service/destinations/create_destination.go +++ b/internal/service/destinations/create_destination.go @@ -1,26 +1,26 @@ -package destinations - -import ( - "context" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" -) - -func (s *Service) CreateDestination( - ctx context.Context, params dbgen.DestinationsServiceCreateDestinationParams, -) (dbgen.Destination, error) { - err := s.TestDestination( - params.AccessKey, params.SecretKey, params.Region, params.Endpoint, - params.BucketName, - ) - if err != nil { - return dbgen.Destination{}, err - } - - params.EncryptionKey = *s.env.PBW_ENCRYPTION_KEY - dest, err := s.dbgen.DestinationsServiceCreateDestination(ctx, params) - - _ = s.TestDestinationAndStoreResult(ctx, dest.ID) - - return dest, err -} +package destinations + +import ( + "context" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" +) + +func (s *Service) CreateDestination( + ctx context.Context, params dbgen.DestinationsServiceCreateDestinationParams, +) (dbgen.Destination, error) { + err := s.TestDestination( + params.AccessKey, params.SecretKey, params.Region, params.Endpoint, + params.BucketName, + ) + if err != nil { + return dbgen.Destination{}, err + } + + params.EncryptionKey = *s.env.PBW_ENCRYPTION_KEY + dest, err := s.dbgen.DestinationsServiceCreateDestination(ctx, params) + + _ = s.TestDestinationAndStoreResult(ctx, dest.ID) + + return dest, err +} diff --git a/internal/service/destinations/destinations.go b/internal/service/destinations/destinations.go index 48399763..a0d6316e 100644 --- a/internal/service/destinations/destinations.go +++ b/internal/service/destinations/destinations.go @@ -1,27 +1,27 @@ -package destinations - -import ( - "github.com/eduardolat/pgbackweb/internal/config" - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/eduardolat/pgbackweb/internal/integration" - "github.com/eduardolat/pgbackweb/internal/service/webhooks" -) - -type Service struct { - env *config.Env - dbgen *dbgen.Queries - ints *integration.Integration - webhooksService *webhooks.Service -} - -func New( - env *config.Env, dbgen *dbgen.Queries, ints *integration.Integration, - webhooksService *webhooks.Service, -) *Service { - return &Service{ - env: env, - dbgen: dbgen, - ints: ints, - webhooksService: webhooksService, - } -} +package destinations + +import ( + "github.com/eduardolat/pgbackweb/internal/config" + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/integration" + "github.com/eduardolat/pgbackweb/internal/service/webhooks" +) + +type Service struct { + env *config.Env + dbgen *dbgen.Queries + ints *integration.Integration + webhooksService *webhooks.Service +} + +func New( + env *config.Env, dbgen *dbgen.Queries, ints *integration.Integration, + webhooksService *webhooks.Service, +) *Service { + return &Service{ + env: env, + dbgen: dbgen, + ints: ints, + webhooksService: webhooksService, + } +} diff --git a/internal/service/destinations/get_all_destinations.go b/internal/service/destinations/get_all_destinations.go index 42805455..7e6d7bbf 100644 --- a/internal/service/destinations/get_all_destinations.go +++ b/internal/service/destinations/get_all_destinations.go @@ -1,15 +1,15 @@ -package destinations - -import ( - "context" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" -) - -func (s *Service) GetAllDestinations( - ctx context.Context, -) ([]dbgen.DestinationsServiceGetAllDestinationsRow, error) { - return s.dbgen.DestinationsServiceGetAllDestinations( - ctx, *s.env.PBW_ENCRYPTION_KEY, - ) -} +package destinations + +import ( + "context" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" +) + +func (s *Service) GetAllDestinations( + ctx context.Context, +) ([]dbgen.DestinationsServiceGetAllDestinationsRow, error) { + return s.dbgen.DestinationsServiceGetAllDestinations( + ctx, *s.env.PBW_ENCRYPTION_KEY, + ) +} diff --git a/internal/service/destinations/get_destination.go b/internal/service/destinations/get_destination.go index e253bbf3..ae74ffd2 100644 --- a/internal/service/destinations/get_destination.go +++ b/internal/service/destinations/get_destination.go @@ -1,19 +1,19 @@ -package destinations - -import ( - "context" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/google/uuid" -) - -func (s *Service) GetDestination( - ctx context.Context, id uuid.UUID, -) (dbgen.DestinationsServiceGetDestinationRow, error) { - return s.dbgen.DestinationsServiceGetDestination( - ctx, dbgen.DestinationsServiceGetDestinationParams{ - ID: id, - EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, - }, - ) -} +package destinations + +import ( + "context" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/google/uuid" +) + +func (s *Service) GetDestination( + ctx context.Context, id uuid.UUID, +) (dbgen.DestinationsServiceGetDestinationRow, error) { + return s.dbgen.DestinationsServiceGetDestination( + ctx, dbgen.DestinationsServiceGetDestinationParams{ + ID: id, + EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, + }, + ) +} diff --git a/internal/service/destinations/get_destinations_qty.sql b/internal/service/destinations/get_destinations_qty.sql index 698bb95e..271dc89c 100644 --- a/internal/service/destinations/get_destinations_qty.sql +++ b/internal/service/destinations/get_destinations_qty.sql @@ -1,6 +1,6 @@ --- name: DestinationsServiceGetDestinationsQty :one -SELECT - COUNT(*) AS all, - SUM(CASE WHEN test_ok = true THEN 1 ELSE 0 END) AS healthy, - SUM(CASE WHEN test_ok = false THEN 1 ELSE 0 END) AS unhealthy -FROM destinations; +-- name: DestinationsServiceGetDestinationsQty :one +SELECT + COUNT(*) AS all, + SUM(CASE WHEN test_ok = true THEN 1 ELSE 0 END) AS healthy, + SUM(CASE WHEN test_ok = false THEN 1 ELSE 0 END) AS unhealthy +FROM destinations; diff --git a/internal/service/destinations/paginate_destinations.go b/internal/service/destinations/paginate_destinations.go index 6450aa24..8e959b94 100644 --- a/internal/service/destinations/paginate_destinations.go +++ b/internal/service/destinations/paginate_destinations.go @@ -1,45 +1,45 @@ -package destinations - -import ( - "context" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/eduardolat/pgbackweb/internal/util/paginateutil" -) - -type PaginateDestinationsParams struct { - Page int - Limit int -} - -func (s *Service) PaginateDestinations( - ctx context.Context, params PaginateDestinationsParams, -) (paginateutil.PaginateResponse, []dbgen.DestinationsServicePaginateDestinationsRow, error) { - page := max(params.Page, 1) - limit := min(max(params.Limit, 1), 100) - - count, err := s.dbgen.DestinationsServicePaginateDestinationsCount(ctx) - if err != nil { - return paginateutil.PaginateResponse{}, nil, err - } - - paginateParams := paginateutil.PaginateParams{ - Page: page, - Limit: limit, - } - offset := paginateutil.CreateOffsetFromParams(paginateParams) - paginateResponse := paginateutil.CreatePaginateResponse(paginateParams, int(count)) - - destinations, err := s.dbgen.DestinationsServicePaginateDestinations( - ctx, dbgen.DestinationsServicePaginateDestinationsParams{ - EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, - Limit: int32(params.Limit), - Offset: int32(offset), - }, - ) - if err != nil { - return paginateutil.PaginateResponse{}, nil, err - } - - return paginateResponse, destinations, nil -} +package destinations + +import ( + "context" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/paginateutil" +) + +type PaginateDestinationsParams struct { + Page int + Limit int +} + +func (s *Service) PaginateDestinations( + ctx context.Context, params PaginateDestinationsParams, +) (paginateutil.PaginateResponse, []dbgen.DestinationsServicePaginateDestinationsRow, error) { + page := max(params.Page, 1) + limit := min(max(params.Limit, 1), 100) + + count, err := s.dbgen.DestinationsServicePaginateDestinationsCount(ctx) + if err != nil { + return paginateutil.PaginateResponse{}, nil, err + } + + paginateParams := paginateutil.PaginateParams{ + Page: page, + Limit: limit, + } + offset := paginateutil.CreateOffsetFromParams(paginateParams) + paginateResponse := paginateutil.CreatePaginateResponse(paginateParams, int(count)) + + destinations, err := s.dbgen.DestinationsServicePaginateDestinations( + ctx, dbgen.DestinationsServicePaginateDestinationsParams{ + EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, + Limit: int32(params.Limit), + Offset: int32(offset), + }, + ) + if err != nil { + return paginateutil.PaginateResponse{}, nil, err + } + + return paginateResponse, destinations, nil +} diff --git a/internal/service/destinations/update_destination.go b/internal/service/destinations/update_destination.go index d3ee093a..f5f94644 100644 --- a/internal/service/destinations/update_destination.go +++ b/internal/service/destinations/update_destination.go @@ -1,26 +1,26 @@ -package destinations - -import ( - "context" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" -) - -func (s *Service) UpdateDestination( - ctx context.Context, params dbgen.DestinationsServiceUpdateDestinationParams, -) (dbgen.Destination, error) { - err := s.TestDestination( - params.AccessKey.String, params.SecretKey.String, params.Region.String, - params.Endpoint.String, params.BucketName.String, - ) - if err != nil { - return dbgen.Destination{}, err - } - - params.EncryptionKey = *s.env.PBW_ENCRYPTION_KEY - dest, err := s.dbgen.DestinationsServiceUpdateDestination(ctx, params) - - _ = s.TestDestinationAndStoreResult(ctx, dest.ID) - - return dest, err -} +package destinations + +import ( + "context" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" +) + +func (s *Service) UpdateDestination( + ctx context.Context, params dbgen.DestinationsServiceUpdateDestinationParams, +) (dbgen.Destination, error) { + err := s.TestDestination( + params.AccessKey.String, params.SecretKey.String, params.Region.String, + params.Endpoint.String, params.BucketName.String, + ) + if err != nil { + return dbgen.Destination{}, err + } + + params.EncryptionKey = *s.env.PBW_ENCRYPTION_KEY + dest, err := s.dbgen.DestinationsServiceUpdateDestination(ctx, params) + + _ = s.TestDestinationAndStoreResult(ctx, dest.ID) + + return dest, err +} diff --git a/internal/service/executions/executions.go b/internal/service/executions/executions.go index cee5346e..5c2fecdb 100644 --- a/internal/service/executions/executions.go +++ b/internal/service/executions/executions.go @@ -1,27 +1,27 @@ -package executions - -import ( - "github.com/eduardolat/pgbackweb/internal/config" - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/eduardolat/pgbackweb/internal/integration" - "github.com/eduardolat/pgbackweb/internal/service/webhooks" -) - -type Service struct { - env *config.Env - dbgen *dbgen.Queries - ints *integration.Integration - webhooksService *webhooks.Service -} - -func New( - env *config.Env, dbgen *dbgen.Queries, ints *integration.Integration, - webhooksService *webhooks.Service, -) *Service { - return &Service{ - env: env, - dbgen: dbgen, - ints: ints, - webhooksService: webhooksService, - } -} +package executions + +import ( + "github.com/eduardolat/pgbackweb/internal/config" + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/integration" + "github.com/eduardolat/pgbackweb/internal/service/webhooks" +) + +type Service struct { + env *config.Env + dbgen *dbgen.Queries + ints *integration.Integration + webhooksService *webhooks.Service +} + +func New( + env *config.Env, dbgen *dbgen.Queries, ints *integration.Integration, + webhooksService *webhooks.Service, +) *Service { + return &Service{ + env: env, + dbgen: dbgen, + ints: ints, + webhooksService: webhooksService, + } +} diff --git a/internal/service/executions/get_execution_download_link_or_path.go b/internal/service/executions/get_execution_download_link_or_path.go index 70e4553f..586c3bb0 100644 --- a/internal/service/executions/get_execution_download_link_or_path.go +++ b/internal/service/executions/get_execution_download_link_or_path.go @@ -1,47 +1,47 @@ -package executions - -import ( - "context" - "fmt" - "time" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/google/uuid" -) - -// GetExecutionDownloadLinkOrPath returns a download link for the file associated -// with the given execution. If the execution is stored locally, the link will -// be a file path. -// -// Returns a boolean indicating if the file is locally stored and the download -// link/path. -func (s *Service) GetExecutionDownloadLinkOrPath( - ctx context.Context, executionID uuid.UUID, -) (bool, string, error) { - data, err := s.dbgen.ExecutionsServiceGetDownloadLinkOrPathData( - ctx, dbgen.ExecutionsServiceGetDownloadLinkOrPathDataParams{ - ExecutionID: executionID, - DecryptionKey: *s.env.PBW_ENCRYPTION_KEY, - }, - ) - if err != nil { - return false, "", err - } - - if !data.Path.Valid { - return false, "", fmt.Errorf("execution has no file associated") - } - - if data.IsLocal { - return true, s.ints.StorageClient.LocalGetFullPath(data.Path.String), nil - } - - link, err := s.ints.StorageClient.S3GetDownloadLink( - data.DecryptedAccessKey, data.DecryptedSecretKey, data.Region.String, - data.Endpoint.String, data.BucketName.String, data.Path.String, time.Hour*12, - ) - if err != nil { - return false, "", err - } - return false, link, nil -} +package executions + +import ( + "context" + "fmt" + "time" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/google/uuid" +) + +// GetExecutionDownloadLinkOrPath returns a download link for the file associated +// with the given execution. If the execution is stored locally, the link will +// be a file path. +// +// Returns a boolean indicating if the file is locally stored and the download +// link/path. +func (s *Service) GetExecutionDownloadLinkOrPath( + ctx context.Context, executionID uuid.UUID, +) (bool, string, error) { + data, err := s.dbgen.ExecutionsServiceGetDownloadLinkOrPathData( + ctx, dbgen.ExecutionsServiceGetDownloadLinkOrPathDataParams{ + ExecutionID: executionID, + DecryptionKey: *s.env.PBW_ENCRYPTION_KEY, + }, + ) + if err != nil { + return false, "", err + } + + if !data.Path.Valid { + return false, "", fmt.Errorf("execution has no file associated") + } + + if data.IsLocal { + return true, s.ints.StorageClient.LocalGetFullPath(data.Path.String), nil + } + + link, err := s.ints.StorageClient.S3GetDownloadLink( + data.DecryptedAccessKey, data.DecryptedSecretKey, data.Region.String, + data.Endpoint.String, data.BucketName.String, data.Path.String, time.Hour*12, + ) + if err != nil { + return false, "", err + } + return false, link, nil +} diff --git a/internal/service/executions/get_executions_qty.sql b/internal/service/executions/get_executions_qty.sql index 8bf72ca7..cc5948f6 100644 --- a/internal/service/executions/get_executions_qty.sql +++ b/internal/service/executions/get_executions_qty.sql @@ -1,8 +1,8 @@ --- name: ExecutionsServiceGetExecutionsQty :one -SELECT - COUNT(*) AS all, - SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running, - SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS success, - SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed, - SUM(CASE WHEN status = 'deleted' THEN 1 ELSE 0 END) AS deleted -FROM executions; +-- name: ExecutionsServiceGetExecutionsQty :one +SELECT + COUNT(*) AS all, + SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running, + SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS success, + SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed, + SUM(CASE WHEN status = 'deleted' THEN 1 ELSE 0 END) AS deleted +FROM executions; diff --git a/internal/service/executions/run_execution.go b/internal/service/executions/run_execution.go index a85d9359..512ffd0c 100644 --- a/internal/service/executions/run_execution.go +++ b/internal/service/executions/run_execution.go @@ -1,164 +1,164 @@ -package executions - -import ( - "context" - "database/sql" - "fmt" - "time" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/eduardolat/pgbackweb/internal/integration/postgres" - "github.com/eduardolat/pgbackweb/internal/logger" - "github.com/eduardolat/pgbackweb/internal/util/strutil" - "github.com/eduardolat/pgbackweb/internal/util/timeutil" - "github.com/google/uuid" -) - -// RunExecution runs a backup execution -func (s *Service) RunExecution(ctx context.Context, backupID uuid.UUID) error { - updateExec := func(params dbgen.ExecutionsServiceUpdateExecutionParams) error { - if params.Status.String == "success" { - s.webhooksService.RunExecutionSuccess(backupID) - } - - if params.Status.String == "failed" { - s.webhooksService.RunExecutionFailed(backupID) - } - - _, err := s.dbgen.ExecutionsServiceUpdateExecution( - ctx, params, - ) - return err - } - - logError := func(err error) { - logger.Error("error running backup", logger.KV{ - "backup_id": backupID.String(), - "error": err.Error(), - }) - } - - back, err := s.dbgen.ExecutionsServiceGetBackupData( - ctx, dbgen.ExecutionsServiceGetBackupDataParams{ - BackupID: backupID, - EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, - }, - ) - if err != nil { - logError(err) - return err - } - - ex, err := s.CreateExecution(ctx, dbgen.ExecutionsServiceCreateExecutionParams{ - BackupID: backupID, - Status: "running", - }) - if err != nil { - logError(err) - return err - } - - if !back.BackupIsLocal { - err = s.ints.StorageClient.S3Test( - back.DecryptedDestinationAccessKey, back.DecryptedDestinationSecretKey, - back.DestinationRegion.String, back.DestinationEndpoint.String, - back.DestinationBucketName.String, - ) - if err != nil { - logError(err) - return updateExec(dbgen.ExecutionsServiceUpdateExecutionParams{ - ID: ex.ID, - Status: sql.NullString{Valid: true, String: "failed"}, - Message: sql.NullString{Valid: true, String: err.Error()}, - FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, - }) - } - } - - pgVersion, err := s.ints.PGClient.ParseVersion(back.DatabasePgVersion) - if err != nil { - logError(err) - return updateExec(dbgen.ExecutionsServiceUpdateExecutionParams{ - ID: ex.ID, - Status: sql.NullString{Valid: true, String: "failed"}, - Message: sql.NullString{Valid: true, String: err.Error()}, - FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, - }) - } - - err = s.ints.PGClient.Test(pgVersion, back.DecryptedDatabaseConnectionString) - if err != nil { - logError(err) - return updateExec(dbgen.ExecutionsServiceUpdateExecutionParams{ - ID: ex.ID, - Status: sql.NullString{Valid: true, String: "failed"}, - Message: sql.NullString{Valid: true, String: err.Error()}, - FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, - }) - } - - dumpReader := s.ints.PGClient.DumpZip( - pgVersion, back.DecryptedDatabaseConnectionString, postgres.DumpParams{ - DataOnly: back.BackupOptDataOnly, - SchemaOnly: back.BackupOptSchemaOnly, - Clean: back.BackupOptClean, - IfExists: back.BackupOptIfExists, - Create: back.BackupOptCreate, - NoComments: back.BackupOptNoComments, - }, - ) - - date := time.Now().Format(timeutil.LayoutSlashYYYYMMDD) - file := fmt.Sprintf( - "dump-%s-%s.zip", - time.Now().Format(timeutil.LayoutYYYYMMDDHHMMSS), - uuid.NewString(), - ) - path := strutil.CreatePath(false, back.BackupDestDir, date, file) - fileSize := int64(0) - - if back.BackupIsLocal { - fileSize, err = s.ints.StorageClient.LocalUpload(path, dumpReader) - if err != nil { - logError(err) - return updateExec(dbgen.ExecutionsServiceUpdateExecutionParams{ - ID: ex.ID, - Status: sql.NullString{Valid: true, String: "failed"}, - Message: sql.NullString{Valid: true, String: err.Error()}, - Path: sql.NullString{Valid: true, String: path}, - FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, - }) - } - } - - if !back.BackupIsLocal { - fileSize, err = s.ints.StorageClient.S3Upload( - back.DecryptedDestinationAccessKey, back.DecryptedDestinationSecretKey, - back.DestinationRegion.String, back.DestinationEndpoint.String, - back.DestinationBucketName.String, path, dumpReader, - ) - if err != nil { - logError(err) - return updateExec(dbgen.ExecutionsServiceUpdateExecutionParams{ - ID: ex.ID, - Status: sql.NullString{Valid: true, String: "failed"}, - Message: sql.NullString{Valid: true, String: err.Error()}, - Path: sql.NullString{Valid: true, String: path}, - FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, - }) - } - } - - logger.Info("backup created successfully", logger.KV{ - "backup_id": backupID.String(), - "execution_id": ex.ID.String(), - }) - return updateExec(dbgen.ExecutionsServiceUpdateExecutionParams{ - ID: ex.ID, - Status: sql.NullString{Valid: true, String: "success"}, - Message: sql.NullString{Valid: true, String: "Backup created successfully"}, - Path: sql.NullString{Valid: true, String: path}, - FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, - FileSize: sql.NullInt64{Valid: true, Int64: fileSize}, - }) -} +package executions + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/integration/postgres" + "github.com/eduardolat/pgbackweb/internal/logger" + "github.com/eduardolat/pgbackweb/internal/util/strutil" + "github.com/eduardolat/pgbackweb/internal/util/timeutil" + "github.com/google/uuid" +) + +// RunExecution runs a backup execution +func (s *Service) RunExecution(ctx context.Context, backupID uuid.UUID) error { + updateExec := func(params dbgen.ExecutionsServiceUpdateExecutionParams) error { + if params.Status.String == "success" { + s.webhooksService.RunExecutionSuccess(backupID) + } + + if params.Status.String == "failed" { + s.webhooksService.RunExecutionFailed(backupID) + } + + _, err := s.dbgen.ExecutionsServiceUpdateExecution( + ctx, params, + ) + return err + } + + logError := func(err error) { + logger.Error("error running backup", logger.KV{ + "backup_id": backupID.String(), + "error": err.Error(), + }) + } + + back, err := s.dbgen.ExecutionsServiceGetBackupData( + ctx, dbgen.ExecutionsServiceGetBackupDataParams{ + BackupID: backupID, + EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, + }, + ) + if err != nil { + logError(err) + return err + } + + ex, err := s.CreateExecution(ctx, dbgen.ExecutionsServiceCreateExecutionParams{ + BackupID: backupID, + Status: "running", + }) + if err != nil { + logError(err) + return err + } + + if !back.BackupIsLocal { + err = s.ints.StorageClient.S3Test( + back.DecryptedDestinationAccessKey, back.DecryptedDestinationSecretKey, + back.DestinationRegion.String, back.DestinationEndpoint.String, + back.DestinationBucketName.String, + ) + if err != nil { + logError(err) + return updateExec(dbgen.ExecutionsServiceUpdateExecutionParams{ + ID: ex.ID, + Status: sql.NullString{Valid: true, String: "failed"}, + Message: sql.NullString{Valid: true, String: err.Error()}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + }) + } + } + + pgVersion, err := s.ints.PGClient.ParseVersion(back.DatabasePgVersion) + if err != nil { + logError(err) + return updateExec(dbgen.ExecutionsServiceUpdateExecutionParams{ + ID: ex.ID, + Status: sql.NullString{Valid: true, String: "failed"}, + Message: sql.NullString{Valid: true, String: err.Error()}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + }) + } + + err = s.ints.PGClient.Test(pgVersion, back.DecryptedDatabaseConnectionString) + if err != nil { + logError(err) + return updateExec(dbgen.ExecutionsServiceUpdateExecutionParams{ + ID: ex.ID, + Status: sql.NullString{Valid: true, String: "failed"}, + Message: sql.NullString{Valid: true, String: err.Error()}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + }) + } + + dumpReader := s.ints.PGClient.DumpZip( + pgVersion, back.DecryptedDatabaseConnectionString, postgres.DumpParams{ + DataOnly: back.BackupOptDataOnly, + SchemaOnly: back.BackupOptSchemaOnly, + Clean: back.BackupOptClean, + IfExists: back.BackupOptIfExists, + Create: back.BackupOptCreate, + NoComments: back.BackupOptNoComments, + }, + ) + + date := time.Now().Format(timeutil.LayoutSlashYYYYMMDD) + file := fmt.Sprintf( + "dump-%s-%s.zip", + time.Now().Format(timeutil.LayoutYYYYMMDDHHMMSS), + uuid.NewString(), + ) + path := strutil.CreatePath(false, back.BackupDestDir, date, file) + fileSize := int64(0) + + if back.BackupIsLocal { + fileSize, err = s.ints.StorageClient.LocalUpload(path, dumpReader) + if err != nil { + logError(err) + return updateExec(dbgen.ExecutionsServiceUpdateExecutionParams{ + ID: ex.ID, + Status: sql.NullString{Valid: true, String: "failed"}, + Message: sql.NullString{Valid: true, String: err.Error()}, + Path: sql.NullString{Valid: true, String: path}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + }) + } + } + + if !back.BackupIsLocal { + fileSize, err = s.ints.StorageClient.S3Upload( + back.DecryptedDestinationAccessKey, back.DecryptedDestinationSecretKey, + back.DestinationRegion.String, back.DestinationEndpoint.String, + back.DestinationBucketName.String, path, dumpReader, + ) + if err != nil { + logError(err) + return updateExec(dbgen.ExecutionsServiceUpdateExecutionParams{ + ID: ex.ID, + Status: sql.NullString{Valid: true, String: "failed"}, + Message: sql.NullString{Valid: true, String: err.Error()}, + Path: sql.NullString{Valid: true, String: path}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + }) + } + } + + logger.Info("backup created successfully", logger.KV{ + "backup_id": backupID.String(), + "execution_id": ex.ID.String(), + }) + return updateExec(dbgen.ExecutionsServiceUpdateExecutionParams{ + ID: ex.ID, + Status: sql.NullString{Valid: true, String: "success"}, + Message: sql.NullString{Valid: true, String: "Backup created successfully"}, + Path: sql.NullString{Valid: true, String: path}, + FinishedAt: sql.NullTime{Valid: true, Time: time.Now()}, + FileSize: sql.NullInt64{Valid: true, Int64: fileSize}, + }) +} diff --git a/internal/service/executions/soft_delete_execution.go b/internal/service/executions/soft_delete_execution.go index 742d85cd..90e69b99 100644 --- a/internal/service/executions/soft_delete_execution.go +++ b/internal/service/executions/soft_delete_execution.go @@ -1,47 +1,47 @@ -package executions - -import ( - "context" - "database/sql" - "errors" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/google/uuid" -) - -func (s *Service) SoftDeleteExecution( - ctx context.Context, executionID uuid.UUID, -) error { - execution, err := s.dbgen.ExecutionsServiceGetExecutionForSoftDelete( - ctx, dbgen.ExecutionsServiceGetExecutionForSoftDeleteParams{ - ExecutionID: executionID, - EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, - }, - ) - if err != nil && errors.Is(err, sql.ErrNoRows) { - return nil - } - if err != nil { - return err - } - - if execution.ExecutionPath.Valid && !execution.BackupIsLocal { - err := s.ints.StorageClient.S3Delete( - execution.DecryptedDestinationAccessKey, execution.DecryptedDestinationSecretKey, - execution.DestinationRegion.String, execution.DestinationEndpoint.String, - execution.DestinationBucketName.String, execution.ExecutionPath.String, - ) - if err != nil { - return err - } - } - - if execution.ExecutionPath.Valid && execution.BackupIsLocal { - err := s.ints.StorageClient.LocalDelete(execution.ExecutionPath.String) - if err != nil { - return err - } - } - - return s.dbgen.ExecutionsServiceSoftDeleteExecution(ctx, executionID) -} +package executions + +import ( + "context" + "database/sql" + "errors" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/google/uuid" +) + +func (s *Service) SoftDeleteExecution( + ctx context.Context, executionID uuid.UUID, +) error { + execution, err := s.dbgen.ExecutionsServiceGetExecutionForSoftDelete( + ctx, dbgen.ExecutionsServiceGetExecutionForSoftDeleteParams{ + ExecutionID: executionID, + EncryptionKey: *s.env.PBW_ENCRYPTION_KEY, + }, + ) + if err != nil && errors.Is(err, sql.ErrNoRows) { + return nil + } + if err != nil { + return err + } + + if execution.ExecutionPath.Valid && !execution.BackupIsLocal { + err := s.ints.StorageClient.S3Delete( + execution.DecryptedDestinationAccessKey, execution.DecryptedDestinationSecretKey, + execution.DestinationRegion.String, execution.DestinationEndpoint.String, + execution.DestinationBucketName.String, execution.ExecutionPath.String, + ) + if err != nil { + return err + } + } + + if execution.ExecutionPath.Valid && execution.BackupIsLocal { + err := s.ints.StorageClient.LocalDelete(execution.ExecutionPath.String) + if err != nil { + return err + } + } + + return s.dbgen.ExecutionsServiceSoftDeleteExecution(ctx, executionID) +} diff --git a/internal/service/restorations/get_restorations_qty.sql b/internal/service/restorations/get_restorations_qty.sql index 82573955..295f32af 100644 --- a/internal/service/restorations/get_restorations_qty.sql +++ b/internal/service/restorations/get_restorations_qty.sql @@ -1,7 +1,7 @@ --- name: RestorationsServiceGetRestorationsQty :one -SELECT - COUNT(*) AS all, - SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running, - SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS success, - SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed -FROM restorations; +-- name: RestorationsServiceGetRestorationsQty :one +SELECT + COUNT(*) AS all, + SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running, + SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS success, + SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed +FROM restorations; diff --git a/internal/service/service.go b/internal/service/service.go index a96aa38d..277bd6da 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -1,54 +1,54 @@ -package service - -import ( - "github.com/eduardolat/pgbackweb/internal/config" - "github.com/eduardolat/pgbackweb/internal/cron" - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/eduardolat/pgbackweb/internal/integration" - "github.com/eduardolat/pgbackweb/internal/service/auth" - "github.com/eduardolat/pgbackweb/internal/service/backups" - "github.com/eduardolat/pgbackweb/internal/service/databases" - "github.com/eduardolat/pgbackweb/internal/service/destinations" - "github.com/eduardolat/pgbackweb/internal/service/executions" - "github.com/eduardolat/pgbackweb/internal/service/restorations" - "github.com/eduardolat/pgbackweb/internal/service/users" - "github.com/eduardolat/pgbackweb/internal/service/webhooks" -) - -type Service struct { - AuthService *auth.Service - BackupsService *backups.Service - DatabasesService *databases.Service - DestinationsService *destinations.Service - ExecutionsService *executions.Service - UsersService *users.Service - RestorationsService *restorations.Service - WebhooksService *webhooks.Service -} - -func New( - env *config.Env, dbgen *dbgen.Queries, - cr *cron.Cron, ints *integration.Integration, -) *Service { - webhooksService := webhooks.New(dbgen) - authService := auth.New(env, dbgen) - databasesService := databases.New(env, dbgen, ints, webhooksService) - destinationsService := destinations.New(env, dbgen, ints, webhooksService) - executionsService := executions.New(env, dbgen, ints, webhooksService) - usersService := users.New(dbgen) - backupsService := backups.New(dbgen, cr, executionsService) - restorationsService := restorations.New( - dbgen, ints, executionsService, databasesService, destinationsService, - ) - - return &Service{ - AuthService: authService, - BackupsService: backupsService, - DatabasesService: databasesService, - DestinationsService: destinationsService, - ExecutionsService: executionsService, - UsersService: usersService, - RestorationsService: restorationsService, - WebhooksService: webhooksService, - } -} +package service + +import ( + "github.com/eduardolat/pgbackweb/internal/config" + "github.com/eduardolat/pgbackweb/internal/cron" + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/integration" + "github.com/eduardolat/pgbackweb/internal/service/auth" + "github.com/eduardolat/pgbackweb/internal/service/backups" + "github.com/eduardolat/pgbackweb/internal/service/databases" + "github.com/eduardolat/pgbackweb/internal/service/destinations" + "github.com/eduardolat/pgbackweb/internal/service/executions" + "github.com/eduardolat/pgbackweb/internal/service/restorations" + "github.com/eduardolat/pgbackweb/internal/service/users" + "github.com/eduardolat/pgbackweb/internal/service/webhooks" +) + +type Service struct { + AuthService *auth.Service + BackupsService *backups.Service + DatabasesService *databases.Service + DestinationsService *destinations.Service + ExecutionsService *executions.Service + UsersService *users.Service + RestorationsService *restorations.Service + WebhooksService *webhooks.Service +} + +func New( + env *config.Env, dbgen *dbgen.Queries, + cr *cron.Cron, ints *integration.Integration, +) *Service { + webhooksService := webhooks.New(dbgen) + authService := auth.New(env, dbgen) + databasesService := databases.New(env, dbgen, ints, webhooksService) + destinationsService := destinations.New(env, dbgen, ints, webhooksService) + executionsService := executions.New(env, dbgen, ints, webhooksService) + usersService := users.New(dbgen) + backupsService := backups.New(dbgen, cr, executionsService) + restorationsService := restorations.New( + dbgen, ints, executionsService, databasesService, destinationsService, + ) + + return &Service{ + AuthService: authService, + BackupsService: backupsService, + DatabasesService: databasesService, + DestinationsService: destinationsService, + ExecutionsService: executionsService, + UsersService: usersService, + RestorationsService: restorationsService, + WebhooksService: webhooksService, + } +} diff --git a/internal/service/webhooks/run_webhook.go b/internal/service/webhooks/run_webhook.go index 38f3dacd..42566de2 100644 --- a/internal/service/webhooks/run_webhook.go +++ b/internal/service/webhooks/run_webhook.go @@ -1,180 +1,180 @@ -package webhooks - -import ( - "context" - "database/sql" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - "time" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/eduardolat/pgbackweb/internal/logger" - "github.com/google/uuid" - "golang.org/x/sync/errgroup" -) - -// RunDatabaseHealthy runs the healthy webhooks for the given database ID. -func (s *Service) RunDatabaseHealthy(databaseID uuid.UUID) { - go func() { - ctx := context.Background() - runWebhook(s, ctx, EventTypeDatabaseHealthy, databaseID) - }() -} - -// RunDatabaseUnhealthy runs the unhealthy webhooks for the given database ID. -func (s *Service) RunDatabaseUnhealthy(databaseID uuid.UUID) { - go func() { - ctx := context.Background() - runWebhook(s, ctx, EventTypeDatabaseUnhealthy, databaseID) - }() -} - -// RunDestinationHealthy runs the healthy webhooks for the given destination ID. -func (s *Service) RunDestinationHealthy(destinationID uuid.UUID) { - go func() { - ctx := context.Background() - runWebhook(s, ctx, EventTypeDestinationHealthy, destinationID) - }() -} - -// RunDestinationUnhealthy runs the unhealthy webhooks for the given -// destination ID. -func (s *Service) RunDestinationUnhealthy(destinationID uuid.UUID) { - go func() { - ctx := context.Background() - runWebhook(s, ctx, EventTypeDestinationUnhealthy, destinationID) - }() -} - -// RunExecutionSuccess runs the success webhooks for the given execution ID. -func (s *Service) RunExecutionSuccess(backupID uuid.UUID) { - go func() { - ctx := context.Background() - runWebhook(s, ctx, EventTypeExecutionSuccess, backupID) - }() -} - -// RunExecutionFailed runs the failed webhooks for the given execution ID. -func (s *Service) RunExecutionFailed(backupID uuid.UUID) { - go func() { - ctx := context.Background() - runWebhook(s, ctx, EventTypeExecutionFailed, backupID) - }() -} - -// runWebhook runs the webhooks for the given event type and target ID. -func runWebhook( - s *Service, ctx context.Context, eventType eventType, targetID uuid.UUID, -) { - webhooks, err := s.dbgen.WebhooksServiceGetWebhooksToRun( - ctx, dbgen.WebhooksServiceGetWebhooksToRunParams{ - EventType: eventType.Value.Key, - TargetID: targetID, - }, - ) - if err != nil { - logger.Error("error getting webhooks to run", logger.KV{"error": err}) - return - } - if len(webhooks) == 0 { - return - } - - eg, ctx := errgroup.WithContext(ctx) - eg.SetLimit(5) - - for _, webhook := range webhooks { - eg.Go(func() error { - err := sendWebhookRequest(s, ctx, webhook) - if err != nil { - logger.Error("error sending webhook request", logger.KV{ - "webhook_id": webhook.ID, - "error": err.Error(), - }) - } - return nil - }) - } - - _ = eg.Wait() -} - -// sendWebhookRequest sends a webhook request to the given webhook and -// stores the result in the database. -func sendWebhookRequest( - s *Service, ctx context.Context, webhook dbgen.Webhook, -) error { - timeStart := time.Now() - - if !webhook.Body.Valid || webhook.Body.String == "" { - webhook.Body = sql.NullString{String: "{}", Valid: true} - } - bodyReader := strings.NewReader(webhook.Body.String) - - if !webhook.Headers.Valid || webhook.Headers.String == "" { - webhook.Headers = sql.NullString{String: "{}", Valid: true} - } - headers := map[string]string{} - err := json.Unmarshal([]byte(webhook.Headers.String), &headers) - if err != nil { - return fmt.Errorf("error parsing headers: %w", err) - } - - client := http.Client{Timeout: time.Second * 30} - req, err := http.NewRequestWithContext( - ctx, webhook.Method, webhook.Url, bodyReader, - ) - if err != nil { - return fmt.Errorf("error creating request: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - for k, v := range headers { - req.Header.Set(k, v) - } - - res, err := client.Do(req) - if err != nil { - return fmt.Errorf("error sending request: %w", err) - } - defer res.Body.Close() - - resHeaders, err := json.Marshal(res.Header) - if err != nil { - return fmt.Errorf("error marshalling response headers: %w", err) - } - - resBody, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("error reading response body: %w", err) - } - - _, err = s.dbgen.WebhooksServiceCreateWebhookResult( - ctx, dbgen.WebhooksServiceCreateWebhookResultParams{ - WebhookID: webhook.ID, - ReqMethod: sql.NullString{String: req.Method, Valid: true}, - ReqHeaders: webhook.Headers, - ReqBody: webhook.Body, - ResStatus: sql.NullInt16{Int16: int16(res.StatusCode), Valid: true}, - ResHeaders: sql.NullString{String: string(resHeaders), Valid: true}, - ResBody: sql.NullString{String: string(resBody), Valid: true}, - ResDuration: sql.NullInt32{ - Int32: int32(time.Since(timeStart).Milliseconds()), - Valid: true, - }, - }, - ) - if err != nil { - return fmt.Errorf("error updating webhook result: %w", err) - } - - logger.Info("webhook sent successfully", logger.KV{ - "webhook_id": webhook.ID, - "status": res.Status, - }) - - return nil -} +package webhooks + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/logger" + "github.com/google/uuid" + "golang.org/x/sync/errgroup" +) + +// RunDatabaseHealthy runs the healthy webhooks for the given database ID. +func (s *Service) RunDatabaseHealthy(databaseID uuid.UUID) { + go func() { + ctx := context.Background() + runWebhook(s, ctx, EventTypeDatabaseHealthy, databaseID) + }() +} + +// RunDatabaseUnhealthy runs the unhealthy webhooks for the given database ID. +func (s *Service) RunDatabaseUnhealthy(databaseID uuid.UUID) { + go func() { + ctx := context.Background() + runWebhook(s, ctx, EventTypeDatabaseUnhealthy, databaseID) + }() +} + +// RunDestinationHealthy runs the healthy webhooks for the given destination ID. +func (s *Service) RunDestinationHealthy(destinationID uuid.UUID) { + go func() { + ctx := context.Background() + runWebhook(s, ctx, EventTypeDestinationHealthy, destinationID) + }() +} + +// RunDestinationUnhealthy runs the unhealthy webhooks for the given +// destination ID. +func (s *Service) RunDestinationUnhealthy(destinationID uuid.UUID) { + go func() { + ctx := context.Background() + runWebhook(s, ctx, EventTypeDestinationUnhealthy, destinationID) + }() +} + +// RunExecutionSuccess runs the success webhooks for the given execution ID. +func (s *Service) RunExecutionSuccess(backupID uuid.UUID) { + go func() { + ctx := context.Background() + runWebhook(s, ctx, EventTypeExecutionSuccess, backupID) + }() +} + +// RunExecutionFailed runs the failed webhooks for the given execution ID. +func (s *Service) RunExecutionFailed(backupID uuid.UUID) { + go func() { + ctx := context.Background() + runWebhook(s, ctx, EventTypeExecutionFailed, backupID) + }() +} + +// runWebhook runs the webhooks for the given event type and target ID. +func runWebhook( + s *Service, ctx context.Context, eventType eventType, targetID uuid.UUID, +) { + webhooks, err := s.dbgen.WebhooksServiceGetWebhooksToRun( + ctx, dbgen.WebhooksServiceGetWebhooksToRunParams{ + EventType: eventType.Value.Key, + TargetID: targetID, + }, + ) + if err != nil { + logger.Error("error getting webhooks to run", logger.KV{"error": err}) + return + } + if len(webhooks) == 0 { + return + } + + eg, ctx := errgroup.WithContext(ctx) + eg.SetLimit(5) + + for _, webhook := range webhooks { + eg.Go(func() error { + err := sendWebhookRequest(s, ctx, webhook) + if err != nil { + logger.Error("error sending webhook request", logger.KV{ + "webhook_id": webhook.ID, + "error": err.Error(), + }) + } + return nil + }) + } + + _ = eg.Wait() +} + +// sendWebhookRequest sends a webhook request to the given webhook and +// stores the result in the database. +func sendWebhookRequest( + s *Service, ctx context.Context, webhook dbgen.Webhook, +) error { + timeStart := time.Now() + + if !webhook.Body.Valid || webhook.Body.String == "" { + webhook.Body = sql.NullString{String: "{}", Valid: true} + } + bodyReader := strings.NewReader(webhook.Body.String) + + if !webhook.Headers.Valid || webhook.Headers.String == "" { + webhook.Headers = sql.NullString{String: "{}", Valid: true} + } + headers := map[string]string{} + err := json.Unmarshal([]byte(webhook.Headers.String), &headers) + if err != nil { + return fmt.Errorf("error parsing headers: %w", err) + } + + client := http.Client{Timeout: time.Second * 30} + req, err := http.NewRequestWithContext( + ctx, webhook.Method, webhook.Url, bodyReader, + ) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + for k, v := range headers { + req.Header.Set(k, v) + } + + res, err := client.Do(req) + if err != nil { + return fmt.Errorf("error sending request: %w", err) + } + defer res.Body.Close() + + resHeaders, err := json.Marshal(res.Header) + if err != nil { + return fmt.Errorf("error marshalling response headers: %w", err) + } + + resBody, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("error reading response body: %w", err) + } + + _, err = s.dbgen.WebhooksServiceCreateWebhookResult( + ctx, dbgen.WebhooksServiceCreateWebhookResultParams{ + WebhookID: webhook.ID, + ReqMethod: sql.NullString{String: req.Method, Valid: true}, + ReqHeaders: webhook.Headers, + ReqBody: webhook.Body, + ResStatus: sql.NullInt16{Int16: int16(res.StatusCode), Valid: true}, + ResHeaders: sql.NullString{String: string(resHeaders), Valid: true}, + ResBody: sql.NullString{String: string(resBody), Valid: true}, + ResDuration: sql.NullInt32{ + Int32: int32(time.Since(timeStart).Milliseconds()), + Valid: true, + }, + }, + ) + if err != nil { + return fmt.Errorf("error updating webhook result: %w", err) + } + + logger.Info("webhook sent successfully", logger.KV{ + "webhook_id": webhook.ID, + "status": res.Status, + }) + + return nil +} diff --git a/internal/service/webhooks/run_webhook.sql b/internal/service/webhooks/run_webhook.sql index 147fcf8a..797c68e0 100644 --- a/internal/service/webhooks/run_webhook.sql +++ b/internal/service/webhooks/run_webhook.sql @@ -1,16 +1,16 @@ --- name: WebhooksServiceGetWebhooksToRun :many -SELECT * FROM webhooks -WHERE is_active = true -AND event_type = @event_type -AND @target_id::UUID = ANY(target_ids); - --- name: WebhooksServiceCreateWebhookResult :one -INSERT INTO webhook_results ( - webhook_id, req_method, req_headers, req_body, - res_status, res_headers, res_body, res_duration -) -VALUES ( - @webhook_id, @req_method, @req_headers, @req_body, - @res_status, @res_headers, @res_body, @res_duration -) -RETURNING *; +-- name: WebhooksServiceGetWebhooksToRun :many +SELECT * FROM webhooks +WHERE is_active = true +AND event_type = @event_type +AND @target_id::UUID = ANY(target_ids); + +-- name: WebhooksServiceCreateWebhookResult :one +INSERT INTO webhook_results ( + webhook_id, req_method, req_headers, req_body, + res_status, res_headers, res_body, res_duration +) +VALUES ( + @webhook_id, @req_method, @req_headers, @req_body, + @res_status, @res_headers, @res_body, @res_duration +) +RETURNING *; diff --git a/internal/service/webhooks/update_webhook.sql b/internal/service/webhooks/update_webhook.sql index 8e14da16..0c619ff9 100644 --- a/internal/service/webhooks/update_webhook.sql +++ b/internal/service/webhooks/update_webhook.sql @@ -1,12 +1,12 @@ --- name: WebhooksServiceUpdateWebhook :one -UPDATE webhooks -SET - name = COALESCE(sqlc.narg('name'), name), - is_active = COALESCE(sqlc.narg('is_active'), is_active), - target_ids = COALESCE(sqlc.narg('target_ids'), target_ids), - url = COALESCE(sqlc.narg('url'), url), - method = COALESCE(sqlc.narg('method'), method), - headers = COALESCE(sqlc.narg('headers'), headers), - body = COALESCE(sqlc.narg('body'), body) -WHERE id = @webhook_id +-- name: WebhooksServiceUpdateWebhook :one +UPDATE webhooks +SET + name = COALESCE(sqlc.narg('name'), name), + is_active = COALESCE(sqlc.narg('is_active'), is_active), + target_ids = COALESCE(sqlc.narg('target_ids'), target_ids), + url = COALESCE(sqlc.narg('url'), url), + method = COALESCE(sqlc.narg('method'), method), + headers = COALESCE(sqlc.narg('headers'), headers), + body = COALESCE(sqlc.narg('body'), body) +WHERE id = @webhook_id RETURNING *; \ No newline at end of file diff --git a/internal/util/echoutil/render_gomponent.go b/internal/util/echoutil/render_gomponent.go index cb024e05..3c2ebe15 100644 --- a/internal/util/echoutil/render_gomponent.go +++ b/internal/util/echoutil/render_gomponent.go @@ -1,28 +1,28 @@ -package echoutil - -import ( - "bytes" - "io" - "net/http" - - "github.com/labstack/echo/v4" -) - -type renderer interface { - Render(w io.Writer) error -} - -// RenderGomponent renders a gomponents component to the response of -// the echo context. -func RenderGomponent(c echo.Context, code int, component renderer) error { - if component == nil { - return c.NoContent(code) - } - - buf := bytes.Buffer{} - err := component.Render(&buf) - if err != nil { - return c.String(http.StatusInternalServerError, err.Error()) - } - return c.HTML(code, buf.String()) -} +package echoutil + +import ( + "bytes" + "io" + "net/http" + + "github.com/labstack/echo/v4" +) + +type renderer interface { + Render(w io.Writer) error +} + +// RenderGomponent renders a gomponents component to the response of +// the echo context. +func RenderGomponent(c echo.Context, code int, component renderer) error { + if component == nil { + return c.NoContent(code) + } + + buf := bytes.Buffer{} + err := component.Render(&buf) + if err != nil { + return c.String(http.StatusInternalServerError, err.Error()) + } + return c.HTML(code, buf.String()) +} diff --git a/internal/util/echoutil/render_gomponent_test.go b/internal/util/echoutil/render_gomponent_test.go index 362c6f4c..c4cb1248 100644 --- a/internal/util/echoutil/render_gomponent_test.go +++ b/internal/util/echoutil/render_gomponent_test.go @@ -1,67 +1,67 @@ -package echoutil - -import ( - "errors" - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" -) - -type okRenderer struct{} - -func (m *okRenderer) Render(w io.Writer) error { - _, err := w.Write([]byte("render ok")) - return err -} - -type nokRenderer struct{} - -func (m *nokRenderer) Render(w io.Writer) error { - return errors.New("render nok") -} - -func TestRenderGomponent(t *testing.T) { - // Setup renderer mocks - okRendererIns := new(okRenderer) - nokRendererIns := new(nokRenderer) - - t.Run("Successful Render", func(t *testing.T) { - e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - - err := RenderGomponent(c, http.StatusOK, okRendererIns) - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, c.Response().Status) - assert.Equal(t, "render ok", rec.Body.String()) - }) - - t.Run("Unsuccessful Render", func(t *testing.T) { - e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - - err := RenderGomponent(c, http.StatusOK, nokRendererIns) - assert.NoError(t, err) - assert.Equal(t, http.StatusInternalServerError, rec.Code) - assert.Equal(t, "render nok", rec.Body.String()) - }) -} - -func TestRenderNilGomponent(t *testing.T) { - e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - - err := RenderGomponent(c, http.StatusOK, nil) - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, rec.Code) - assert.Equal(t, "", rec.Body.String()) -} +package echoutil + +import ( + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +type okRenderer struct{} + +func (m *okRenderer) Render(w io.Writer) error { + _, err := w.Write([]byte("render ok")) + return err +} + +type nokRenderer struct{} + +func (m *nokRenderer) Render(w io.Writer) error { + return errors.New("render nok") +} + +func TestRenderGomponent(t *testing.T) { + // Setup renderer mocks + okRendererIns := new(okRenderer) + nokRendererIns := new(nokRenderer) + + t.Run("Successful Render", func(t *testing.T) { + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err := RenderGomponent(c, http.StatusOK, okRendererIns) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, c.Response().Status) + assert.Equal(t, "render ok", rec.Body.String()) + }) + + t.Run("Unsuccessful Render", func(t *testing.T) { + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err := RenderGomponent(c, http.StatusOK, nokRendererIns) + assert.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, rec.Code) + assert.Equal(t, "render nok", rec.Body.String()) + }) +} + +func TestRenderNilGomponent(t *testing.T) { + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err := RenderGomponent(c, http.StatusOK, nil) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "", rec.Body.String()) +} diff --git a/internal/util/paginateutil/README.md b/internal/util/paginateutil/README.md index a1effc72..aa1d25bc 100644 --- a/internal/util/paginateutil/README.md +++ b/internal/util/paginateutil/README.md @@ -1,46 +1,46 @@ -# paginateutil - -This package provides a utility for paginating data. It should be used in conjunction with database queries to paginate results. - -Use these utilities to paginate results and, above all, to return a common structure in the different places where pagination is performed, maintaining consistency throughout the project. - -## Usage - -### 1. Define your queries: - -- **PaginateCount:** This function should return the total number of records that match the query. -- **Paginate:** This function should return the paginated records. - -### 2. Create the offset from the request parameters: - -Use the `CreateOffsetFromParams` function to create the offset from the request parameters needed to paginate the results. - -### 3. Define the signature of your pagination function: - -The signature of your wrapper function should be: - -```go -type PaginateXYZParams struct { - Page int - Limit int - ABCFilter sql.NullString -} - -func PaginateXYZ( - ctx context.Context, - params PaginateXYZParams, -) ( - paginateutil.PaginateResponse, - []XYZ, - error -) -``` - -### Example - -Refer to `internal/service/backups/paginate_backups.go` for an example of how to use the `paginateutil` package. - -## Notes - -- **Default Values:** Ensure to set reasonable default values for pagination parameters (`page` and `limit`) if they are not provided. -- **Common Response Structure:** Use `CreatePaginateResponse` to generate a common response structure that includes information about pagination, such as the total number of items, the current page, and the number of items per page. +# paginateutil + +This package provides a utility for paginating data. It should be used in conjunction with database queries to paginate results. + +Use these utilities to paginate results and, above all, to return a common structure in the different places where pagination is performed, maintaining consistency throughout the project. + +## Usage + +### 1. Define your queries: + +- **PaginateCount:** This function should return the total number of records that match the query. +- **Paginate:** This function should return the paginated records. + +### 2. Create the offset from the request parameters: + +Use the `CreateOffsetFromParams` function to create the offset from the request parameters needed to paginate the results. + +### 3. Define the signature of your pagination function: + +The signature of your wrapper function should be: + +```go +type PaginateXYZParams struct { + Page int + Limit int + ABCFilter sql.NullString +} + +func PaginateXYZ( + ctx context.Context, + params PaginateXYZParams, +) ( + paginateutil.PaginateResponse, + []XYZ, + error +) +``` + +### Example + +Refer to `internal/service/backups/paginate_backups.go` for an example of how to use the `paginateutil` package. + +## Notes + +- **Default Values:** Ensure to set reasonable default values for pagination parameters (`page` and `limit`) if they are not provided. +- **Common Response Structure:** Use `CreatePaginateResponse` to generate a common response structure that includes information about pagination, such as the total number of items, the current page, and the number of items per page. diff --git a/internal/validate/README.md b/internal/validate/README.md index 0244bc3a..7af1bbbc 100644 --- a/internal/validate/README.md +++ b/internal/validate/README.md @@ -1,5 +1,5 @@ -# Validate functions - -All the functions in this directory are used to validate data, all of them _MUST BE PURE FUNCTIONS_. - -https://en.wikipedia.org/wiki/Pure_function +# Validate functions + +All the functions in this directory are used to validate data, all of them _MUST BE PURE FUNCTIONS_. + +https://en.wikipedia.org/wiki/Pure_function diff --git a/internal/view/middleware/inject_reqctx.go b/internal/view/middleware/inject_reqctx.go index 37ffab79..64bf6456 100644 --- a/internal/view/middleware/inject_reqctx.go +++ b/internal/view/middleware/inject_reqctx.go @@ -1,41 +1,41 @@ -package middleware - -import ( - "net/http" - - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/eduardolat/pgbackweb/internal/logger" - "github.com/eduardolat/pgbackweb/internal/view/reqctx" - "github.com/labstack/echo/v4" -) - -func (m *Middleware) InjectReqctx(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - found, user, err := m.servs.AuthService.GetUserFromSessionCookie(c) - if err != nil { - logger.Error("failed to get user from session cookie", logger.KV{ - "ip": c.RealIP(), - "ua": c.Request().UserAgent(), - "error": err, - }) - return c.String(http.StatusInternalServerError, "Internal server error") - } - - if !found { - return next(c) - } - - reqctx.SetCtx(c, reqctx.Ctx{ - IsAuthed: true, - SessionID: user.SessionID, - User: dbgen.User{ - ID: user.ID, - Name: user.Name, - Email: user.Email, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - }, - }) - return next(c) - } -} +package middleware + +import ( + "net/http" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/logger" + "github.com/eduardolat/pgbackweb/internal/view/reqctx" + "github.com/labstack/echo/v4" +) + +func (m *Middleware) InjectReqctx(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + found, user, err := m.servs.AuthService.GetUserFromSessionCookie(c) + if err != nil { + logger.Error("failed to get user from session cookie", logger.KV{ + "ip": c.RealIP(), + "ua": c.Request().UserAgent(), + "error": err, + }) + return c.String(http.StatusInternalServerError, "Internal server error") + } + + if !found { + return next(c) + } + + reqctx.SetCtx(c, reqctx.Ctx{ + IsAuthed: true, + SessionID: user.SessionID, + User: dbgen.User{ + ID: user.ID, + Name: user.Name, + Email: user.Email, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + }, + }) + return next(c) + } +} diff --git a/internal/view/middleware/require_auth.go b/internal/view/middleware/require_auth.go index 63e010c7..a5c5b737 100644 --- a/internal/view/middleware/require_auth.go +++ b/internal/view/middleware/require_auth.go @@ -1,36 +1,36 @@ -package middleware - -import ( - "net/http" - - "github.com/eduardolat/pgbackweb/internal/logger" - "github.com/eduardolat/pgbackweb/internal/view/reqctx" - "github.com/labstack/echo/v4" -) - -func (m *Middleware) RequireAuth(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - ctx := c.Request().Context() - reqCtx := reqctx.GetCtx(c) - - if reqCtx.IsAuthed { - return next(c) - } - - usersQty, err := m.servs.UsersService.GetUsersQty(ctx) - if err != nil { - logger.Error("failed to get users qty", logger.KV{ - "ip": c.RealIP(), - "ua": c.Request().UserAgent(), - "error": err, - }) - return c.String(http.StatusInternalServerError, "Internal server error") - } - - if usersQty == 0 { - return c.Redirect(http.StatusFound, "/auth/create-first-user") - } - - return c.Redirect(http.StatusFound, "/auth/login") - } -} +package middleware + +import ( + "net/http" + + "github.com/eduardolat/pgbackweb/internal/logger" + "github.com/eduardolat/pgbackweb/internal/view/reqctx" + "github.com/labstack/echo/v4" +) + +func (m *Middleware) RequireAuth(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + ctx := c.Request().Context() + reqCtx := reqctx.GetCtx(c) + + if reqCtx.IsAuthed { + return next(c) + } + + usersQty, err := m.servs.UsersService.GetUsersQty(ctx) + if err != nil { + logger.Error("failed to get users qty", logger.KV{ + "ip": c.RealIP(), + "ua": c.Request().UserAgent(), + "error": err, + }) + return c.String(http.StatusInternalServerError, "Internal server error") + } + + if usersQty == 0 { + return c.Redirect(http.StatusFound, "/auth/create-first-user") + } + + return c.Redirect(http.StatusFound, "/auth/login") + } +} diff --git a/internal/view/middleware/require_no_auth.go b/internal/view/middleware/require_no_auth.go index 62cfa0b6..fa31f4d5 100644 --- a/internal/view/middleware/require_no_auth.go +++ b/internal/view/middleware/require_no_auth.go @@ -1,20 +1,20 @@ -package middleware - -import ( - "net/http" - - "github.com/eduardolat/pgbackweb/internal/view/reqctx" - "github.com/labstack/echo/v4" -) - -func (m *Middleware) RequireNoAuth(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - reqCtx := reqctx.GetCtx(c) - - if reqCtx.IsAuthed { - return c.Redirect(http.StatusFound, "/dashboard") - } - - return next(c) - } -} +package middleware + +import ( + "net/http" + + "github.com/eduardolat/pgbackweb/internal/view/reqctx" + "github.com/labstack/echo/v4" +) + +func (m *Middleware) RequireNoAuth(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + reqCtx := reqctx.GetCtx(c) + + if reqCtx.IsAuthed { + return c.Redirect(http.StatusFound, "/dashboard") + } + + return next(c) + } +} diff --git a/internal/view/reqctx/README.md b/internal/view/reqctx/README.md index 4c03176d..c76c4ea7 100644 --- a/internal/view/reqctx/README.md +++ b/internal/view/reqctx/README.md @@ -1,9 +1,9 @@ -# reqctx - -`reqctx` is a utility package designed to manage request-specific context values in this project. It helps to encapsulate authentication status and user information within the Echo request context in a type-safe manner. - -## Purpose - -When developing web applications, it is common to pass values such as authentication status and user information through the request lifecycle. Using Echo's built-in context (`echo.Context`) can lead to potential issues such as typographical errors, lack of type safety, and reduced code readability. - -`reqctx` addresses these issues by providing a structured way to manage these context values. +# reqctx + +`reqctx` is a utility package designed to manage request-specific context values in this project. It helps to encapsulate authentication status and user information within the Echo request context in a type-safe manner. + +## Purpose + +When developing web applications, it is common to pass values such as authentication status and user information through the request lifecycle. Using Echo's built-in context (`echo.Context`) can lead to potential issues such as typographical errors, lack of type safety, and reduced code readability. + +`reqctx` addresses these issues by providing a structured way to manage these context values. diff --git a/internal/view/reqctx/ctx.go b/internal/view/reqctx/ctx.go index 175e9f2b..284511e2 100644 --- a/internal/view/reqctx/ctx.go +++ b/internal/view/reqctx/ctx.go @@ -1,51 +1,51 @@ -package reqctx - -import ( - "github.com/eduardolat/pgbackweb/internal/database/dbgen" - "github.com/google/uuid" - "github.com/labstack/echo/v4" -) - -// Context keys to avoid typos -const ( - isAuthedKey = "isAuthed" - sessionIDKey = "sessionId" - userKey = "user" -) - -// Ctx represents the values passed through a single request context. -type Ctx struct { - IsAuthed bool - SessionID uuid.UUID - User dbgen.User -} - -// SetCtx inserts values into the Echo request context. -func SetCtx(c echo.Context, ctx Ctx) { - c.Set(isAuthedKey, ctx.IsAuthed) - c.Set(sessionIDKey, ctx.SessionID) - c.Set(userKey, ctx.User) -} - -// GetCtx retrieves values from the Echo request context. -func GetCtx(c echo.Context) Ctx { - var isAuthed bool - var sessionID uuid.UUID - var user dbgen.User - - if ia, ok := c.Get(isAuthedKey).(bool); ok { - isAuthed = ia - } - if sid, ok := c.Get(sessionIDKey).(uuid.UUID); ok { - sessionID = sid - } - if au, ok := c.Get(userKey).(dbgen.User); ok { - user = au - } - - return Ctx{ - IsAuthed: isAuthed, - SessionID: sessionID, - User: user, - } -} +package reqctx + +import ( + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/google/uuid" + "github.com/labstack/echo/v4" +) + +// Context keys to avoid typos +const ( + isAuthedKey = "isAuthed" + sessionIDKey = "sessionId" + userKey = "user" +) + +// Ctx represents the values passed through a single request context. +type Ctx struct { + IsAuthed bool + SessionID uuid.UUID + User dbgen.User +} + +// SetCtx inserts values into the Echo request context. +func SetCtx(c echo.Context, ctx Ctx) { + c.Set(isAuthedKey, ctx.IsAuthed) + c.Set(sessionIDKey, ctx.SessionID) + c.Set(userKey, ctx.User) +} + +// GetCtx retrieves values from the Echo request context. +func GetCtx(c echo.Context) Ctx { + var isAuthed bool + var sessionID uuid.UUID + var user dbgen.User + + if ia, ok := c.Get(isAuthedKey).(bool); ok { + isAuthed = ia + } + if sid, ok := c.Get(sessionIDKey).(uuid.UUID); ok { + sessionID = sid + } + if au, ok := c.Get(userKey).(dbgen.User); ok { + user = au + } + + return Ctx{ + IsAuthed: isAuthed, + SessionID: sessionID, + User: user, + } +} diff --git a/internal/view/router.go b/internal/view/router.go index 56a22a8d..d131f642 100644 --- a/internal/view/router.go +++ b/internal/view/router.go @@ -1,22 +1,22 @@ -package view - -import ( - "github.com/eduardolat/pgbackweb/internal/service" - "github.com/eduardolat/pgbackweb/internal/view/api" - "github.com/eduardolat/pgbackweb/internal/view/middleware" - "github.com/eduardolat/pgbackweb/internal/view/static" - "github.com/eduardolat/pgbackweb/internal/view/web" - "github.com/labstack/echo/v4" -) - -func MountRouter(app *echo.Echo, servs *service.Service) { - mids := middleware.New(servs) - - app.StaticFS("", static.StaticFs) - - apiGroup := app.Group("/api") - api.MountRouter(apiGroup, mids, servs) - - webGroup := app.Group("", mids.InjectReqctx) - web.MountRouter(webGroup, mids, servs) -} +package view + +import ( + "github.com/eduardolat/pgbackweb/internal/service" + "github.com/eduardolat/pgbackweb/internal/view/api" + "github.com/eduardolat/pgbackweb/internal/view/middleware" + "github.com/eduardolat/pgbackweb/internal/view/static" + "github.com/eduardolat/pgbackweb/internal/view/web" + "github.com/labstack/echo/v4" +) + +func MountRouter(app *echo.Echo, servs *service.Service) { + mids := middleware.New(servs) + + app.StaticFS("", static.StaticFs) + + apiGroup := app.Group("/api") + api.MountRouter(apiGroup, mids, servs) + + webGroup := app.Group("", mids.InjectReqctx) + web.MountRouter(webGroup, mids, servs) +} diff --git a/internal/view/static/css/slim-select.css b/internal/view/static/css/slim-select.css index 8a3b5faa..c03fb6e7 100644 --- a/internal/view/static/css/slim-select.css +++ b/internal/view/static/css/slim-select.css @@ -1,62 +1,62 @@ -/* - For Slim Select, this takes some styles from: - https://github.com/saadeghi/daisyui/blob/master/src/components/styled/input.css -*/ - -.ss-main { - all: unset; - @apply !input !input-bordered !text-base-content; -} - -.ss-content { - all: unset; - @apply !bg-base-100 !text-base-content !rounded-btn; - @apply !px-2 !py-2 !space-y-1; - @apply !border !border-base-content/20; -} - -.ss-list { - all: unset; - scrollbar-width: thin; - @apply !space-y-1; -} - -.ss-search input { - all: unset; - @apply !input !input-bordered !input-sm; -} - -.ss-option { - all: unset; - @apply !bg-base-100 !text-base-content !rounded-btn; -} - -.ss-option:hover { - all: unset; - @apply !bg-base-200; -} - -.ss-option.ss-selected { - all: unset; - @apply !bg-base-200; -} - -.ss-option.ss-disabled { - @apply !text-opacity-80; -} - -.ss-value { - @apply !bg-primary !rounded-badge; -} - -.ss-value-text { - @apply !text-primary-content; -} - -.ss-value-delete { - @apply !border-0 !border-l !border-primary-content; -} - -.ss-value-delete svg path { - @apply !stroke-primary-content; +/* + For Slim Select, this takes some styles from: + https://github.com/saadeghi/daisyui/blob/master/src/components/styled/input.css +*/ + +.ss-main { + all: unset; + @apply !input !input-bordered !text-base-content; +} + +.ss-content { + all: unset; + @apply !bg-base-100 !text-base-content !rounded-btn; + @apply !px-2 !py-2 !space-y-1; + @apply !border !border-base-content/20; +} + +.ss-list { + all: unset; + scrollbar-width: thin; + @apply !space-y-1; +} + +.ss-search input { + all: unset; + @apply !input !input-bordered !input-sm; +} + +.ss-option { + all: unset; + @apply !bg-base-100 !text-base-content !rounded-btn; +} + +.ss-option:hover { + all: unset; + @apply !bg-base-200; +} + +.ss-option.ss-selected { + all: unset; + @apply !bg-base-200; +} + +.ss-option.ss-disabled { + @apply !text-opacity-80; +} + +.ss-value { + @apply !bg-primary !rounded-badge; +} + +.ss-value-text { + @apply !text-primary-content; +} + +.ss-value-delete { + @apply !border-0 !border-l !border-primary-content; +} + +.ss-value-delete svg path { + @apply !stroke-primary-content; } \ No newline at end of file diff --git a/internal/view/static/css/tailwind.css b/internal/view/static/css/tailwind.css index ec644050..30950a68 100644 --- a/internal/view/static/css/tailwind.css +++ b/internal/view/static/css/tailwind.css @@ -1,45 +1,45 @@ -@import "./slim-select.css"; - -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer components { - .normal-width { - @apply container mx-auto px-4 max-w-[1920px]; - } -} - -/* General */ -html { - overflow-x: hidden; - overflow-y: auto; -} - -/* Scrollbar */ -* { - scrollbar-width: thin; -} - -/* For AlpineJS */ -[x-cloak] { - display: none !important; -} - -/* For HTMX */ -.htmx-indicator { - display: none; -} - -.htmx-request .htmx-indicator { - display: block; -} - -.htmx-request.htmx-indicator { - display: block; -} - -/* For Gomponents Lucide */ -svg[data-glucide="icon"]:not([class*="size-"]) { - @apply size-4; +@import "./slim-select.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer components { + .normal-width { + @apply container mx-auto px-4 max-w-[1920px]; + } +} + +/* General */ +html { + overflow-x: hidden; + overflow-y: auto; +} + +/* Scrollbar */ +* { + scrollbar-width: thin; +} + +/* For AlpineJS */ +[x-cloak] { + display: none !important; +} + +/* For HTMX */ +.htmx-indicator { + display: none; +} + +.htmx-request .htmx-indicator { + display: block; +} + +.htmx-request.htmx-indicator { + display: block; +} + +/* For Gomponents Lucide */ +svg[data-glucide="icon"]:not([class*="size-"]) { + @apply size-4; } \ No newline at end of file diff --git a/internal/view/static/js/alpine-components/change-theme-button.js b/internal/view/static/js/alpine-components/change-theme-button.js index 052fa762..ad2ab15f 100644 --- a/internal/view/static/js/alpine-components/change-theme-button.js +++ b/internal/view/static/js/alpine-components/change-theme-button.js @@ -1,22 +1,22 @@ -export const changeThemeButton = { - name: 'changeThemeButton', - fn: () => ({ - theme: '', - - getCurrentTheme () { - const el = document.querySelector('html') - const theme = el.getAttribute('data-theme') - if (theme) { - this.theme = theme - return - } - this.theme = 'system' - }, - - init () { - setTimeout(() => { - this.getCurrentTheme() - }, 200) - } - }) -} +export const changeThemeButton = { + name: 'changeThemeButton', + fn: () => ({ + theme: '', + + getCurrentTheme () { + const el = document.querySelector('html') + const theme = el.getAttribute('data-theme') + if (theme) { + this.theme = theme + return + } + this.theme = 'system' + }, + + init () { + setTimeout(() => { + this.getCurrentTheme() + }, 200) + } + }) +} diff --git a/internal/view/static/js/alpine-components/dashboard-aside-item.js b/internal/view/static/js/alpine-components/dashboard-aside-item.js index 577aa1e9..93aeecf6 100644 --- a/internal/view/static/js/alpine-components/dashboard-aside-item.js +++ b/internal/view/static/js/alpine-components/dashboard-aside-item.js @@ -1,16 +1,16 @@ -export const dashboardAsideItem = { - name: 'dashboardAsideItem', - fn: (link = '', strict = false) => ({ - link, - strict, - is_active: false, - init () { - if (this.strict) { - this.is_active = window.location.pathname === this.link - return - } - - this.is_active = window.location.pathname.startsWith(this.link) - } - }) -} +export const dashboardAsideItem = { + name: 'dashboardAsideItem', + fn: (link = '', strict = false) => ({ + link, + strict, + is_active: false, + init () { + if (this.strict) { + this.is_active = window.location.pathname === this.link + return + } + + this.is_active = window.location.pathname.startsWith(this.link) + } + }) +} diff --git a/internal/view/static/js/alpine-components/generic-slider.js b/internal/view/static/js/alpine-components/generic-slider.js index c223a6fd..474c1000 100644 --- a/internal/view/static/js/alpine-components/generic-slider.js +++ b/internal/view/static/js/alpine-components/generic-slider.js @@ -1,18 +1,18 @@ -export const genericSlider = { - name: 'genericSlider', - fn: (slidesQty = 0) => ({ - currentSlide: slidesQty > 0 ? 1 : 0, - get hasNextSlide () { - return this.currentSlide < slidesQty - }, - get hasPrevSlide () { - return this.currentSlide > 1 - }, - nextSlide () { - if (this.hasNextSlide) this.currentSlide++ - }, - prevSlide () { - if (this.hasPrevSlide) this.currentSlide-- - } - }) -} +export const genericSlider = { + name: 'genericSlider', + fn: (slidesQty = 0) => ({ + currentSlide: slidesQty > 0 ? 1 : 0, + get hasNextSlide () { + return this.currentSlide < slidesQty + }, + get hasPrevSlide () { + return this.currentSlide > 1 + }, + nextSlide () { + if (this.hasNextSlide) this.currentSlide++ + }, + prevSlide () { + if (this.hasPrevSlide) this.currentSlide-- + } + }) +} diff --git a/internal/view/static/js/alpine-components/github-repo-info.js b/internal/view/static/js/alpine-components/github-repo-info.js index 869f8ca7..b2bc8b38 100644 --- a/internal/view/static/js/alpine-components/github-repo-info.js +++ b/internal/view/static/js/alpine-components/github-repo-info.js @@ -1,77 +1,77 @@ -export const githubRepoInfo = { - name: 'githubRepoInfo', - fn: () => ({ - stars: '', - latestRelease: '', - - async init () { - const stars = await this.getStars() - if (stars !== null) { - this.stars = stars - } - - const latestRelease = await this.getLatestRelease() - if (latestRelease !== null) { - this.latestRelease = latestRelease - } - }, - async getStars () { - const cacheKey = 'pbw_gh_stars' - const cachedData = this.getCachedData(cacheKey) - if (cachedData !== null) { - return cachedData - } - - const url = 'https://api.github.com/repos/eduardolat/pgbackweb' - try { - const response = await fetch(url) - if (!response.ok) { - return null - } - const data = await response.json() - this.cacheData(cacheKey, data.stargazers_count) - return data.stargazers_count - } catch { - return null - } - }, - async getLatestRelease () { - const cacheKey = 'pbw_gh_last_release' - const cachedData = this.getCachedData(cacheKey) - if (cachedData !== null) { - return cachedData - } - - const url = 'https://api.github.com/repos/eduardolat/pgbackweb/releases/latest' - try { - const response = await fetch(url) - if (!response.ok) { - return null - } - const data = await response.json() - this.cacheData(cacheKey, data.name) - return data.name - } catch { - return null - } - }, - getCachedData (key) { - const cachedJSON = localStorage.getItem(key) - if (!cachedJSON) { - return null - } - const cached = JSON.parse(cachedJSON) - if (Date.now() - cached.timestamp < 2 * 60 * 1000) { - return cached.value - } - return null - }, - cacheData (key, value) { - const data = JSON.stringify({ - value, - timestamp: Date.now() - }) - localStorage.setItem(key, data) - } - }) -} +export const githubRepoInfo = { + name: 'githubRepoInfo', + fn: () => ({ + stars: '', + latestRelease: '', + + async init () { + const stars = await this.getStars() + if (stars !== null) { + this.stars = stars + } + + const latestRelease = await this.getLatestRelease() + if (latestRelease !== null) { + this.latestRelease = latestRelease + } + }, + async getStars () { + const cacheKey = 'pbw_gh_stars' + const cachedData = this.getCachedData(cacheKey) + if (cachedData !== null) { + return cachedData + } + + const url = 'https://api.github.com/repos/eduardolat/pgbackweb' + try { + const response = await fetch(url) + if (!response.ok) { + return null + } + const data = await response.json() + this.cacheData(cacheKey, data.stargazers_count) + return data.stargazers_count + } catch { + return null + } + }, + async getLatestRelease () { + const cacheKey = 'pbw_gh_last_release' + const cachedData = this.getCachedData(cacheKey) + if (cachedData !== null) { + return cachedData + } + + const url = 'https://api.github.com/repos/eduardolat/pgbackweb/releases/latest' + try { + const response = await fetch(url) + if (!response.ok) { + return null + } + const data = await response.json() + this.cacheData(cacheKey, data.name) + return data.name + } catch { + return null + } + }, + getCachedData (key) { + const cachedJSON = localStorage.getItem(key) + if (!cachedJSON) { + return null + } + const cached = JSON.parse(cachedJSON) + if (Date.now() - cached.timestamp < 2 * 60 * 1000) { + return cached.value + } + return null + }, + cacheData (key, value) { + const data = JSON.stringify({ + value, + timestamp: Date.now() + }) + localStorage.setItem(key, data) + } + }) +} diff --git a/internal/view/static/js/app.js b/internal/view/static/js/app.js index 0a28d135..321ea536 100644 --- a/internal/view/static/js/app.js +++ b/internal/view/static/js/app.js @@ -1,11 +1,11 @@ -import { initNotyf } from './init-notyf.js' -import { initHTMX } from './init-htmx.js' -import { initAlpineComponents } from './init-alpine-components.js' -import { initCopyFunction } from './init-copy-function.js' -import { initDashboardAsideScroll } from './dashboard-aside-scroll.js' - -initNotyf() -initHTMX() -initAlpineComponents() -initCopyFunction() -initDashboardAsideScroll() +import { initNotyf } from './init-notyf.js' +import { initHTMX } from './init-htmx.js' +import { initAlpineComponents } from './init-alpine-components.js' +import { initCopyFunction } from './init-copy-function.js' +import { initDashboardAsideScroll } from './dashboard-aside-scroll.js' + +initNotyf() +initHTMX() +initAlpineComponents() +initCopyFunction() +initDashboardAsideScroll() diff --git a/internal/view/static/js/dashboard-aside-scroll.js b/internal/view/static/js/dashboard-aside-scroll.js index 8192515f..be6709d2 100644 --- a/internal/view/static/js/dashboard-aside-scroll.js +++ b/internal/view/static/js/dashboard-aside-scroll.js @@ -1,16 +1,16 @@ -export function initDashboardAsideScroll () { - const el = document.getElementById('dashboard-aside') - const key = 'dashboard-aside-scroll-position' - - window.addEventListener('beforeunload', function () { - const scrollPosition = el.scrollTop - localStorage.setItem(key, scrollPosition) - }) - - document.addEventListener('DOMContentLoaded', function () { - const scrollPosition = localStorage.getItem(key) - if (scrollPosition) { - el.scrollTop = parseInt(scrollPosition, 10) - } - }) -} +export function initDashboardAsideScroll () { + const el = document.getElementById('dashboard-aside') + const key = 'dashboard-aside-scroll-position' + + window.addEventListener('beforeunload', function () { + const scrollPosition = el.scrollTop + localStorage.setItem(key, scrollPosition) + }) + + document.addEventListener('DOMContentLoaded', function () { + const scrollPosition = localStorage.getItem(key) + if (scrollPosition) { + el.scrollTop = parseInt(scrollPosition, 10) + } + }) +} diff --git a/internal/view/static/js/init-alpine-components.js b/internal/view/static/js/init-alpine-components.js index 653a498a..70454bf6 100644 --- a/internal/view/static/js/init-alpine-components.js +++ b/internal/view/static/js/init-alpine-components.js @@ -1,13 +1,13 @@ -import { changeThemeButton } from './alpine-components/change-theme-button.js' -import { githubRepoInfo } from './alpine-components/github-repo-info.js' -import { dashboardAsideItem } from './alpine-components/dashboard-aside-item.js' -import { genericSlider } from './alpine-components/generic-slider.js' - -export function initAlpineComponents () { - document.addEventListener('alpine:init', () => { - Alpine.data(changeThemeButton.name, changeThemeButton.fn) - Alpine.data(githubRepoInfo.name, githubRepoInfo.fn) - Alpine.data(dashboardAsideItem.name, dashboardAsideItem.fn) - Alpine.data(genericSlider.name, genericSlider.fn) - }) -} +import { changeThemeButton } from './alpine-components/change-theme-button.js' +import { githubRepoInfo } from './alpine-components/github-repo-info.js' +import { dashboardAsideItem } from './alpine-components/dashboard-aside-item.js' +import { genericSlider } from './alpine-components/generic-slider.js' + +export function initAlpineComponents () { + document.addEventListener('alpine:init', () => { + Alpine.data(changeThemeButton.name, changeThemeButton.fn) + Alpine.data(githubRepoInfo.name, githubRepoInfo.fn) + Alpine.data(dashboardAsideItem.name, dashboardAsideItem.fn) + Alpine.data(genericSlider.name, genericSlider.fn) + }) +} diff --git a/internal/view/static/js/init-copy-function.js b/internal/view/static/js/init-copy-function.js index 8d1c039f..d8b663d2 100644 --- a/internal/view/static/js/init-copy-function.js +++ b/internal/view/static/js/init-copy-function.js @@ -1,47 +1,47 @@ -export function initCopyFunction () { - function copyToClipboard (textToCopy) { - const successMessage = 'Text copied' - const errorMessage = 'Error copying text' - - /* First, try the modern approach */ - if (navigator.clipboard && window.isSecureContext) { - return navigator.clipboard - .writeText(textToCopy) - .then(() => { - toaster.success(successMessage) - }) - .catch((err) => { - console.error(errorMessage, err) - toaster.error(errorMessage) - }) - } - - /* Fallback: use execCommand("copy") method */ - const textArea = document.createElement('textarea') - textArea.value = textToCopy - - textArea.style.position = 'fixed' - textArea.style.left = '-9999px' - textArea.style.top = '-9999px' - document.body.appendChild(textArea) - textArea.focus() - textArea.select() - - try { - const successful = document.execCommand('copy') - if (successful) { - toaster.success(successMessage) - } else { - console.error('Fallback', errorMessage) - toaster.error(errorMessage) - } - } catch (err) { - console.error('Fallback', errorMessage, err) - toaster.error(errorMessage) - } - - document.body.removeChild(textArea) - } - - window.copyToClipboard = copyToClipboard -} +export function initCopyFunction () { + function copyToClipboard (textToCopy) { + const successMessage = 'Text copied' + const errorMessage = 'Error copying text' + + /* First, try the modern approach */ + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard + .writeText(textToCopy) + .then(() => { + toaster.success(successMessage) + }) + .catch((err) => { + console.error(errorMessage, err) + toaster.error(errorMessage) + }) + } + + /* Fallback: use execCommand("copy") method */ + const textArea = document.createElement('textarea') + textArea.value = textToCopy + + textArea.style.position = 'fixed' + textArea.style.left = '-9999px' + textArea.style.top = '-9999px' + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + + try { + const successful = document.execCommand('copy') + if (successful) { + toaster.success(successMessage) + } else { + console.error('Fallback', errorMessage) + toaster.error(errorMessage) + } + } catch (err) { + console.error('Fallback', errorMessage, err) + toaster.error(errorMessage) + } + + document.body.removeChild(textArea) + } + + window.copyToClipboard = copyToClipboard +} diff --git a/internal/view/static/js/init-htmx.js b/internal/view/static/js/init-htmx.js index 04a748b7..d7460c0b 100644 --- a/internal/view/static/js/init-htmx.js +++ b/internal/view/static/js/init-htmx.js @@ -1,60 +1,60 @@ -export function initHTMX () { - const triggers = { - ctm_alert: function (evt) { - const message = decodeURIComponent(evt.detail.value) - alert(message) - }, - ctm_alert_with_refresh: function (evt) { - const message = decodeURIComponent(evt.detail.value) - alert(message) - location.reload() - }, - ctm_alert_with_redirect: function (evt) { - const payload = decodeURIComponent(evt.detail.value) - const parts = payload.split('-::-::-') - if (parts.length !== 2) { - return - } - const message = parts[0] - const url = parts[1] - - alert(message) - location.href = url - }, - ctm_toast_success: function (evt) { - const message = decodeURIComponent(evt.detail.value) - toaster.success(message) - }, - ctm_toast_error: function (evt) { - const message = decodeURIComponent(evt.detail.value) - toaster.error(message) - }, - ctm_toast_success_infinite: function (evt) { - const message = decodeURIComponent(evt.detail.value) - toaster.successInfinite(message) - }, - ctm_toast_error_infinite: function (evt) { - const message = decodeURIComponent(evt.detail.value) - toaster.errorInfinite(message) - } - } - - for (const key in triggers) { - document.addEventListener(key, triggers[key]) - } - - /* - This fixes this issue: - https://stackoverflow.com/questions/73658449/htmx-request-not-firing-when-hx-attributes-are-added-dynamically-from-javascrip - */ - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - mutation.addedNodes.forEach((node) => { - if (node.nodeType === 1 && !node['htmx-internal-data']) { - htmx.process(node) - } - }) - }) - }) - observer.observe(document, { childList: true, subtree: true }) -} +export function initHTMX () { + const triggers = { + ctm_alert: function (evt) { + const message = decodeURIComponent(evt.detail.value) + alert(message) + }, + ctm_alert_with_refresh: function (evt) { + const message = decodeURIComponent(evt.detail.value) + alert(message) + location.reload() + }, + ctm_alert_with_redirect: function (evt) { + const payload = decodeURIComponent(evt.detail.value) + const parts = payload.split('-::-::-') + if (parts.length !== 2) { + return + } + const message = parts[0] + const url = parts[1] + + alert(message) + location.href = url + }, + ctm_toast_success: function (evt) { + const message = decodeURIComponent(evt.detail.value) + toaster.success(message) + }, + ctm_toast_error: function (evt) { + const message = decodeURIComponent(evt.detail.value) + toaster.error(message) + }, + ctm_toast_success_infinite: function (evt) { + const message = decodeURIComponent(evt.detail.value) + toaster.successInfinite(message) + }, + ctm_toast_error_infinite: function (evt) { + const message = decodeURIComponent(evt.detail.value) + toaster.errorInfinite(message) + } + } + + for (const key in triggers) { + document.addEventListener(key, triggers[key]) + } + + /* + This fixes this issue: + https://stackoverflow.com/questions/73658449/htmx-request-not-firing-when-hx-attributes-are-added-dynamically-from-javascrip + */ + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === 1 && !node['htmx-internal-data']) { + htmx.process(node) + } + }) + }) + }) + observer.observe(document, { childList: true, subtree: true }) +} diff --git a/internal/view/static/js/init-notyf.js b/internal/view/static/js/init-notyf.js index cf48093b..f46181ea 100644 --- a/internal/view/static/js/init-notyf.js +++ b/internal/view/static/js/init-notyf.js @@ -1,53 +1,53 @@ -export function initNotyf () { - let toastQueue = [] - - const toastCfg = { - duration: 5000, - ripple: false, - position: { x: 'right', y: 'bottom' }, - dismissible: true - } - - const infiniteToastCfg = { - ...toastCfg, - duration: 0 - } - - window.toaster = { - success: (message) => { - toastQueue.push({ type: 'success', message, config: toastCfg }) - }, - error: (message) => { - toastQueue.push({ type: 'error', message, config: toastCfg }) - }, - successInfinite: (message) => { - toastQueue.push({ type: 'success', message, config: infiniteToastCfg }) - }, - errorInfinite: (message) => { - toastQueue.push({ type: 'error', message, config: infiniteToastCfg }) - } - } - - document.addEventListener('DOMContentLoaded', function () { - const notyf = new Notyf() - - toastQueue.forEach(item => { - notyf.open({ type: item.type, message: item.message, ...item.config }) - }) - - toastQueue = [] - - window.toaster.success = (message) => { - notyf.open({ type: 'success', message, ...toastCfg }) - } - window.toaster.error = (message) => { - notyf.open({ type: 'error', message, ...toastCfg }) - } - window.toaster.successInfinite = (message) => { - notyf.open({ type: 'success', message, ...infiniteToastCfg }) - } - window.toaster.errorInfinite = (message) => { - notyf.open({ type: 'error', message, ...infiniteToastCfg }) - } - }) -} +export function initNotyf () { + let toastQueue = [] + + const toastCfg = { + duration: 5000, + ripple: false, + position: { x: 'right', y: 'bottom' }, + dismissible: true + } + + const infiniteToastCfg = { + ...toastCfg, + duration: 0 + } + + window.toaster = { + success: (message) => { + toastQueue.push({ type: 'success', message, config: toastCfg }) + }, + error: (message) => { + toastQueue.push({ type: 'error', message, config: toastCfg }) + }, + successInfinite: (message) => { + toastQueue.push({ type: 'success', message, config: infiniteToastCfg }) + }, + errorInfinite: (message) => { + toastQueue.push({ type: 'error', message, config: infiniteToastCfg }) + } + } + + document.addEventListener('DOMContentLoaded', function () { + const notyf = new Notyf() + + toastQueue.forEach(item => { + notyf.open({ type: item.type, message: item.message, ...item.config }) + }) + + toastQueue = [] + + window.toaster.success = (message) => { + notyf.open({ type: 'success', message, ...toastCfg }) + } + window.toaster.error = (message) => { + notyf.open({ type: 'error', message, ...toastCfg }) + } + window.toaster.successInfinite = (message) => { + notyf.open({ type: 'success', message, ...infiniteToastCfg }) + } + window.toaster.errorInfinite = (message) => { + notyf.open({ type: 'error', message, ...infiniteToastCfg }) + } + }) +} diff --git a/internal/view/static/libs/chartjs/chartjs-4.4.3.umd.min.js b/internal/view/static/libs/chartjs/chartjs-4.4.3.umd.min.js index c0f9743e..239587ae 100644 --- a/internal/view/static/libs/chartjs/chartjs-4.4.3.umd.min.js +++ b/internal/view/static/libs/chartjs/chartjs-4.4.3.umd.min.js @@ -1,20 +1,20 @@ -/** - * Skipped minification because the original files appears to be already minified. - * Original file: /npm/chart.js@4.4.3/dist/chart.umd.js - * - * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files - */ -/*! - * Chart.js v4.4.3 - * https://www.chartjs.org - * (c) 2024 Chart.js Contributors - * Released under the MIT License - */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Go},get Decimation(){return Qo},get Filler(){return ma},get Legend(){return ya},get SubTitle(){return ka},get Title(){return Ma},get Tooltip(){return Ba}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;at,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)t-e)).pop(),e}function N(t){return!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;sl&&h=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return nt[s][e]et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;ss&&t[n-1]>i;)n--;return s>0||n{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){const i=t._chartjs;if(!i)return;const s=i.listeners,n=s.indexOf(e);-1!==n&&s.splice(n,1),s.length>0||(ot.forEach((e=>{delete t[e]})),delete t._chartjs)}function lt(t){const e=new Set(t);return e.size===t.length?t:Array.from(e)}const ht="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function ct(t,e){let i=[],s=!1;return function(...n){i=n,s||(s=!0,ht.call(window,(()=>{s=!1,t.apply(e,i)})))}}function dt(t,e){let i;return function(...s){return e?(clearTimeout(i),i=setTimeout(t,e,s)):t.apply(this,s),e}}const ut=t=>"start"===t?"left":"end"===t?"right":"center",ft=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,gt=(t,e,i,s)=>t===(s?"left":"right")?i:"center"===t?(e+i)/2:e;function pt(t,e,i){const s=e.length;let n=0,o=s;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:h,max:c,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(n=J(Math.min(it(r,l,h).lo,i?s:it(e,l,a.getPixelForValue(h)).lo),0,s-1)),o=u?J(Math.max(it(r,a.axis,c,!0).hi+1,i?0:it(e,l,a.getPixelForValue(c),!0).hi+1),n,s)-n:s-n}return{start:n,count:o}}function mt(t){const{xScale:e,yScale:i,_scaleRanges:s}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!s)return t._scaleRanges=n,!0;const o=s.xmin!==e.min||s.xmax!==e.max||s.ymin!==i.min||s.ymax!==i.max;return Object.assign(s,n),o}class xt{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,s){const n=e.listeners[s],o=e.duration;n.forEach((s=>s({chart:t,initial:e.initial,numSteps:o,currentStep:Math.min(i-e.start,o)})))}_refresh(){this._request||(this._running=!0,this._request=ht.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,s)=>{if(!i.running||!i.items.length)return;const n=i.items;let o,a=n.length-1,r=!1;for(;a>=0;--a)o=n[a],o._active?(o._total>i.duration&&(i.duration=o._total),o.tick(t),r=!0):(n[a]=n[n.length-1],n.pop());r&&(s.draw(),this._notify(s,i,t,"progress")),n.length||(i.running=!1,this._notify(s,i,t,"complete"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let s=i.length-1;for(;s>=0;--s)i[s].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}var bt=new xt; -/*! - * @kurkle/color v0.3.2 - * https://github.com/kurkle/color#readme - * (c) 2023 Jukka Kurkela - * Released under the MIT License - */function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),s=Ft(s),t.r=s[0],t.g=s[1],t.b=s[2]}}function qt(t,e){return t?Object.assign(e||{},t):t}function Kt(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Mt(t[3]))):(e=qt(t,{r:0,g:0,b:0,a:1})).a=Mt(e.a),e}function Gt(t){return"r"===t.charAt(0)?function(t){const e=$t.exec(t);let i,s,n,o=255;if(e){if(e[7]!==i){const t=+e[7];o=e[8]?vt(t):yt(255*t,0,255)}return i=+e[1],s=+e[3],n=+e[5],i=255&(e[2]?vt(i):yt(i,0,255)),s=255&(e[4]?vt(s):yt(s,0,255)),n=255&(e[6]?vt(n):yt(n,0,255)),{r:i,g:s,b:n,a:o}}}(t):Bt(t)}class Zt{constructor(t){if(t instanceof Zt)return t;const e=typeof t;let i;var s,n,o;"object"===e?i=Kt(t):"string"===e&&(o=(s=t).length,"#"===s[0]&&(4===o||5===o?n={r:255&17*St[s[1]],g:255&17*St[s[2]],b:255&17*St[s[3]],a:5===o?17*St[s[4]]:255}:7!==o&&9!==o||(n={r:St[s[1]]<<4|St[s[2]],g:St[s[3]]<<4|St[s[4]],b:St[s[5]]<<4|St[s[6]],a:9===o?St[s[7]]<<4|St[s[8]]:255})),i=n||jt(t)||Gt(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=qt(this._rgb);return t&&(t.a=wt(t.a)),t}set rgb(t){this._rgb=Kt(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${wt(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):void 0;var t}hexString(){return this._valid?At(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=It(t),i=e[0],s=kt(e[1]),n=kt(e[2]);return t.a<255?`hsla(${i}, ${s}%, ${n}%, ${wt(t.a)})`:`hsl(${i}, ${s}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,s=t.rgb;let n;const o=e===n?.5:e,a=2*o-1,r=i.a-s.a,l=((a*r==-1?a:(a+r)/(1+a*r))+1)/2;n=1-l,i.r=255&l*i.r+n*s.r+.5,i.g=255&l*i.g+n*s.g+.5,i.b=255&l*i.b+n*s.b+.5,i.a=o*i.a+(1-o)*s.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const s=Ut(wt(t.r)),n=Ut(wt(t.g)),o=Ut(wt(t.b));return{r:Mt(Yt(s+i*(Ut(wt(e.r))-s))),g:Mt(Yt(n+i*(Ut(wt(e.g))-n))),b:Mt(Yt(o+i*(Ut(wt(e.b))-o))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new Zt(this.rgb)}alpha(t){return this._rgb.a=Mt(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=_t(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Xt(this._rgb,2,t),this}darken(t){return Xt(this._rgb,2,-t),this}saturate(t){return Xt(this._rgb,1,t),this}desaturate(t){return Xt(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=It(t);i[0]=Vt(i[0]+e),i=Ft(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function Jt(t){if(t&&"object"==typeof t){const e=t.toString();return"[object CanvasPattern]"===e||"[object CanvasGradient]"===e}return!1}function Qt(t){return Jt(t)?t:new Zt(t)}function te(t){return Jt(t)?t:new Zt(t).saturate(.5).darken(.1).hexString()}const ee=["x","y","borderWidth","radius","tension"],ie=["color","borderColor","backgroundColor"];const se=new Map;function ne(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let s=se.get(i);return s||(s=new Intl.NumberFormat(t,e),se.set(i,s)),s}(e,i).format(t)}const oe={values:t=>n(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const s=this.chart.options.locale;let n,o=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n="scientific"),o=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=z(Math.abs(o)),r=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),ne(t,s,l)},logarithmic(t,e,i){if(0===t)return"0";const s=i[e].significand||t/Math.pow(10,Math.floor(z(t)));return[1,2,3,5,10,15].includes(s)||e>.8*i.length?oe.numeric.call(this,t,e,i):""}};var ae={formatters:oe};const re=Object.create(null),le=Object.create(null);function he(t,e){if(!e)return t;const i=e.split(".");for(let e=0,s=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>te(e.backgroundColor),this.hoverBorderColor=(t,e)=>te(e.borderColor),this.hoverColor=(t,e)=>te(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return ce(this,t,e)}get(t){return he(this,t)}describe(t,e){return ce(le,t,e)}override(t,e){return ce(re,t,e)}route(t,e,i,s){const n=he(this,t),a=he(this,i),r="_"+e;Object.defineProperties(n,{[r]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[r],e=a[s];return o(t)?Object.assign({},e,t):l(t,e)},set(t){this[r]=t}}})}apply(t){t.forEach((t=>t(this)))}}var ue=new de({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),t.set("animations",{colors:{type:"color",properties:ie},numbers:{type:"number",properties:ee}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}})},function(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ae.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t&&"dash"!==t}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t})}]);function fe(){return"undefined"!=typeof window&&"undefined"!=typeof document}function ge(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function pe(t,e,i){let s;return"string"==typeof t?(s=parseInt(t,10),-1!==t.indexOf("%")&&(s=s/100*e.parentNode[i])):s=t,s}const me=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function xe(t,e){return me(t).getPropertyValue(e)}const be=["top","right","bottom","left"];function _e(t,e,i){const s={};i=i?"-"+i:"";for(let n=0;n<4;n++){const o=be[n];s[o]=parseFloat(t[e+"-"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}const ye=(t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot);function ve(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=me(i),o="border-box"===n.boxSizing,a=_e(n,"padding"),r=_e(n,"border","width"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(ye(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const Me=t=>Math.round(10*t)/10;function we(t,e,i,s){const n=me(t),o=_e(n,"margin"),a=pe(n.maxWidth,t,"clientWidth")||T,r=pe(n.maxHeight,t,"clientHeight")||T,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=t&&ge(t);if(o){const t=o.getBoundingClientRect(),a=me(o),r=_e(a,"border","width"),l=_e(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=pe(a.maxWidth,o,"clientWidth"),n=pe(a.maxHeight,o,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||T,maxHeight:n||T}}(t,e,i);let{width:h,height:c}=l;if("content-box"===n.boxSizing){const t=_e(n,"border","width"),e=_e(n,"padding");h-=e.width+t.width,c-=e.height+t.height}h=Math.max(0,h-o.width),c=Math.max(0,s?h/s:c-o.height),h=Me(Math.min(h,a,l.maxWidth)),c=Me(Math.min(c,r,l.maxHeight)),h&&!c&&(c=Me(h/2));return(void 0!==e||void 0!==i)&&s&&l.height&&c>l.height&&(c=l.height,h=Me(Math.floor(c*s))),{width:h,height:c}}function ke(t,e,i){const s=e||1,n=Math.floor(t.height*s),o=Math.floor(t.width*s);t.height=Math.floor(t.height),t.width=Math.floor(t.width);const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const Se=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};fe()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch(t){}return t}();function Pe(t,e){const i=xe(t,e),s=i&&i.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function De(t){return!t||s(t.size)||s(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function Ce(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function Oe(t,e,i,s){let o=(s=s||{}).data=s.data||{},a=s.garbageCollect=s.garbageCollect||[];s.font!==e&&(o=s.data={},a=s.garbageCollect=[],s.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;hi.length){for(h=0;h0&&t.stroke()}}function Re(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==r.strokeColor;let c,d;for(t.save(),t.font=a.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]),s(e.rotation)||t.rotate(e.rotation),e.color&&(t.fillStyle=e.color),e.textAlign&&(t.textAlign=e.textAlign),e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,r),c=0;ct[0])){const o=i||t;void 0===s&&(s=ti("_fallback",t));const a={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:o,_fallback:s,_getTarget:n,override:i=>je([i,...t],e,o,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,s)=>qe(i,s,(()=>function(t,e,i,s){let n;for(const o of e)if(n=ti(Ue(o,t),i),void 0!==n)return Xe(t,n)?Je(i,s,t,n):n}(s,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ei(t).includes(e),ownKeys:t=>ei(t),set(t,e,i){const s=t._storage||(t._storage=n());return t[e]=s[e]=i,delete t._keys,!0}})}function $e(t,e,i,s){const a={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Ye(t,s),setContext:e=>$e(t,e,i,s),override:n=>$e(t.override(n),e,i,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>qe(t,e,(()=>function(t,e,i){const{_proxy:s,_context:a,_subProxy:r,_descriptors:l}=t;let h=s[e];S(h)&&l.isScriptable(e)&&(h=function(t,e,i,s){const{_proxy:n,_context:o,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t);let l=e(o,a||s);r.delete(t),Xe(t,l)&&(l=Je(n._scopes,n,t,l));return l}(e,h,t,i));n(h)&&h.length&&(h=function(t,e,i,s){const{_proxy:n,_context:a,_subProxy:r,_descriptors:l}=i;if(void 0!==a.index&&s(t))return e[a.index%e.length];if(o(e[0])){const i=e,s=n._scopes.filter((t=>t!==i));e=[];for(const o of i){const i=Je(s,n,t,o);e.push($e(i,a,r&&r[t],l))}}return e}(e,h,t,l.isIndexable));Xe(e,h)&&(h=$e(h,a,r&&r[e],l));return h}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,s)=>(t[i]=s,delete e[i],!0)})}function Ye(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:s=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:s,isScriptable:S(i)?i:()=>i,isIndexable:S(s)?s:()=>s}}const Ue=(t,e)=>t?t+w(e):e,Xe=(t,e)=>o(e)&&"adapters"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function qe(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e)||"constructor"===e)return t[e];const s=i();return t[e]=s,s}function Ke(t,e,i){return S(t)?t(e,i):t}const Ge=(t,e)=>!0===t?e:"string"==typeof t?M(e,t):void 0;function Ze(t,e,i,s,n){for(const o of e){const e=Ge(i,o);if(e){t.add(e);const o=Ke(e._fallback,i,n);if(void 0!==o&&o!==i&&o!==s)return o}else if(!1===e&&void 0!==s&&i!==s)return null}return!1}function Je(t,e,i,s){const a=e._rootScopes,r=Ke(e._fallback,i,s),l=[...t,...a],h=new Set;h.add(s);let c=Qe(h,l,i,r||i,s);return null!==c&&((void 0===r||r===i||(c=Qe(h,l,r,c,s),null!==c))&&je(Array.from(h),[""],a,r,(()=>function(t,e,i){const s=t._getTarget();e in s||(s[e]={});const a=s[e];if(n(a)&&o(i))return i;return a||{}}(e,i,s))))}function Qe(t,e,i,s,n){for(;i;)i=Ze(t,e,i,s,n);return i}function ti(t,e){for(const i of e){if(!i)continue;const e=i[t];if(void 0!==e)return e}}function ei(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}function ii(t,e,i,s){const{iScale:n}=t,{key:o="r"}=this._parsing,a=new Array(s);let r,l,h,c;for(r=0,l=s;re"x"===t?"y":"x";function ai(t,e,i,s){const n=t.skip?e:t,o=e,a=i.skip?e:i,r=q(o,n),l=q(a,o);let h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;const d=s*h,u=s*c;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function ri(t,e="x"){const i=oi(e),s=t.length,n=Array(s).fill(0),o=Array(s);let a,r,l,h=ni(t,0);for(a=0;a!t.skip))),"monotone"===e.cubicInterpolationMode)ri(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o0===t||1===t,di=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),ui=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,fi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*E),easeOutSine:t=>Math.sin(t*E),easeInOutSine:t=>-.5*(Math.cos(C*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>ci(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ci(t)?t:di(t,.075,.3),easeOutElastic:t=>ci(t)?t:ui(t,.075,.3),easeInOutElastic(t){const e=.1125;return ci(t)?t:t<.5?.5*di(2*t,e,.45):.5+.5*ui(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-fi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*fi.easeInBounce(2*t):.5*fi.easeOutBounce(2*t-1)+.5};function gi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function pi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:"middle"===s?i<.5?t.y:e.y:"after"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function mi(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=gi(t,n,i),r=gi(n,o,i),l=gi(o,e,i),h=gi(a,r,i),c=gi(r,l,i);return gi(h,c,i)}const xi=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,bi=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function _i(t,e){const i=(""+t).match(xi);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}const yi=t=>+t||0;function vi(t,e){const i={},s=o(e),n=s?Object.keys(e):e,a=o(t)?s?i=>l(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=yi(a(t));return i}function Mi(t){return vi(t,{top:"y",right:"x",bottom:"y",left:"x"})}function wi(t){return vi(t,["topLeft","topRight","bottomLeft","bottomRight"])}function ki(t){const e=Mi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Si(t,e){t=t||{},e=e||ue.font;let i=l(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let s=l(t.style,e.style);s&&!(""+s).match(bi)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);const n={family:l(t.family,e.family),lineHeight:_i(l(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:l(t.weight,e.weight),string:""};return n.string=De(n),n}function Pi(t,e,i,s){let o,a,r,l=!0;for(o=0,a=t.length;oi&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function Ci(t,e){return Object.assign(Object.create(t),e)}function Oi(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Ai(t,e){let i,s;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,s=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=s)}function Ti(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Li(t){return"angle"===t?{between:Z,compare:K,normalize:G}:{between:tt,compare:(t,e)=>t-e,normalize:t=>t}}function Ei({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Ri(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Li(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Li(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;hb||l(n,x,p)&&0!==r(n,x),v=()=>!b||0===r(o,p)||l(o,x,p);for(let t=c,i=c;t<=d;++t)m=e[t%a],m.skip||(p=h(m[s]),p!==x&&(b=l(p,n,o),null===_&&y()&&(_=0===r(p,n)?t:i),null!==_&&v()&&(g.push(Ei({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,x=p));return null!==_&&g.push(Ei({start:_,end:d,loop:u,count:a,style:f})),g}function Ii(t,e){const i=[],s=t.segments;for(let n=0;nn&&t[o%e].skip;)o--;return o%=e,{start:n,end:o}}(i,n,o,s);if(!0===s)return Fi(t,[{start:a,end:r,loop:o}],i,e);return Fi(t,function(t,e,i,s){const n=t.length,o=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%n];i.skip||i.stop?l.skip||(s=!1,o.push({start:e%n,end:(a-1)%n,loop:s}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&o.push({start:e%n,end:r%n,loop:s}),o}(i,a,r{t[a](e[i],n)&&(o.push({element:t,datasetIndex:s,index:l}),r=r||t.inRange(e.x,e.y,n))})),s&&!r?[]:o}var Xi={evaluateInteractionItems:Hi,modes:{index(t,e,i,s){const n=ve(e,t),o=i.axis||"x",a=i.includeInvisible||!1,r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;let r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;tji(t,ve(e,t),i.axis||"xy",s,i.includeInvisible||!1),nearest(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;return Yi(t,n,o,i.intersect,s,a)},x:(t,e,i,s)=>Ui(t,ve(e,t),"x",i.intersect,s),y:(t,e,i,s)=>Ui(t,ve(e,t),"y",i.intersect,s)}};const qi=["left","top","right","bottom"];function Ki(t,e){return t.filter((t=>t.pos===e))}function Gi(t,e){return t.filter((t=>-1===qi.indexOf(t.pos)&&t.box.axis===e))}function Zi(t,e){return t.sort(((t,i)=>{const s=e?i:t,n=e?t:i;return s.weight===n.weight?s.index-n.index:s.weight-n.weight}))}function Ji(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:s,stackWeight:n}=i;if(!t||!qi.includes(s))continue;const o=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});o.count++,o.weight+=n}return e}(t),{vBoxMaxWidth:s,hBoxMaxHeight:n}=e;let o,a,r;for(o=0,a=t.length;o{s[t]=Math.max(e[t],i[t])})),s}return s(t?["left","right"]:["top","bottom"])}function ss(t,e,i,s){const n=[];let o,a,r,l,h,c;for(o=0,a=t.length,h=0;ot.box.fullSize)),!0),s=Zi(Ki(e,"left"),!0),n=Zi(Ki(e,"right")),o=Zi(Ki(e,"top"),!0),a=Zi(Ki(e,"bottom")),r=Gi(e,"x"),l=Gi(e,"y");return{fullSize:i,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Ki(e,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}(t.boxes),l=r.vertical,h=r.horizontal;u(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const c=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/c,hBoxMaxHeight:a/2}),f=Object.assign({},n);ts(f,ki(s));const g=Object.assign({maxPadding:f,w:o,h:a,x:n.left,y:n.top},n),p=Ji(l.concat(h),d);ss(r.fullSize,g,d,p),ss(l,g,d,p),ss(h,g,d,p)&&ss(l,g,d,p),function(t){const e=t.maxPadding;function i(i){const s=Math.max(e[i]-t[i],0);return t[i]+=s,s}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(g),os(r.leftAndTop,g,d,p),g.x+=g.w,g.y+=g.h,os(r.rightAndBottom,g,d,p),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},u(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})}))}};class rs{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,s){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,s?Math.floor(e/s):i)}}isAttached(t){return!0}updateConfig(t){}}class ls extends rs{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const hs="$chartjs",cs={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},ds=t=>null===t||""===t;const us=!!Se&&{passive:!0};function fs(t,e,i){t&&t.canvas&&t.canvas.removeEventListener(e,i,us)}function gs(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function ps(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.addedNodes,s),e=e&&!gs(i.removedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function ms(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.removedNodes,s),e=e&&!gs(i.addedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const xs=new Map;let bs=0;function _s(){const t=window.devicePixelRatio;t!==bs&&(bs=t,xs.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function ys(t,e,i){const s=t.canvas,n=s&&ge(s);if(!n)return;const o=ct(((t,e)=>{const s=n.clientWidth;i(t,e),s{const e=t[0],i=e.contentRect.width,s=e.contentRect.height;0===i&&0===s||o(i,s)}));return a.observe(n),function(t,e){xs.size||window.addEventListener("resize",_s),xs.set(t,e)}(t,o),a}function vs(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){xs.delete(t),xs.size||window.removeEventListener("resize",_s)}(t)}function Ms(t,e,i){const s=t.canvas,n=ct((e=>{null!==t.ctx&&i(function(t,e){const i=cs[t.type]||t.type,{x:s,y:n}=ve(t,e);return{type:i,chart:e,native:t,x:void 0!==s?s:null,y:void 0!==n?n:null}}(e,t))}),t);return function(t,e,i){t&&t.addEventListener(e,i,us)}(s,e,n),n}class ws extends rs{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,s=t.getAttribute("height"),n=t.getAttribute("width");if(t[hs]={initial:{height:s,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",ds(n)){const e=Pe(t,"width");void 0!==e&&(t.width=e)}if(ds(s))if(""===t.style.height)t.height=t.width/(e||2);else{const e=Pe(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e[hs])return!1;const i=e[hs].initial;["height","width"].forEach((t=>{const n=i[t];s(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e[hs],!0}addEventListener(t,e,i){this.removeEventListener(t,e);const s=t.$proxies||(t.$proxies={}),n={attach:ps,detach:ms,resize:ys}[e]||Ms;s[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),s=i[e];if(!s)return;({attach:vs,detach:vs,resize:vs}[e]||fs)(t,e,s),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,s){return we(t,e,i,s)}isAttached(t){const e=t&&ge(t);return!(!e||!e.isConnected)}}function ks(t){return!fe()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?ls:ws}var Ss=Object.freeze({__proto__:null,BasePlatform:rs,BasicPlatform:ls,DomPlatform:ws,_detectPlatform:ks});const Ps="transparent",Ds={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const s=Qt(t||Ps),n=s.valid&&Qt(e||Ps);return n&&n.valid?n.mix(s,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class Cs{constructor(t,e,i,s){const n=e[i];s=Pi([t.to,s,n,t.from]);const o=Pi([t.from,n,s]);this._active=!0,this._fn=t.fn||Ds[t.type||typeof o],this._easing=fi[t.easing]||fi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=o,this._to=s,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const s=this._target[this._prop],n=i-this._start,o=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(o,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=Pi([t.to,e,s,t.from]),this._from=Pi([t.from,s,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,s=this._prop,n=this._from,o=this._loop,a=this._to;let r;if(this._active=n!==a&&(o||e1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[s]=this._fn(n,a,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t{const a=t[s];if(!o(a))return;const r={};for(const t of e)r[t]=a[t];(n(a.properties)&&a.properties||[s]).forEach((t=>{t!==s&&i.has(t)||i.set(t,r)}))}))}_animateOptions(t,e){const i=e.options,s=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!s)return[];const n=this._createAnimations(s,i);return i.$shared&&function(t,e){const i=[],s=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,s=[],n=t.$animations||(t.$animations={}),o=Object.keys(e),a=Date.now();let r;for(r=o.length-1;r>=0;--r){const l=o[r];if("$"===l.charAt(0))continue;if("options"===l){s.push(...this._animateOptions(t,e));continue}const h=e[l];let c=n[l];const d=i.get(l);if(c){if(d&&c.active()){c.update(d,h,a);continue}c.cancel()}d&&d.duration?(n[l]=c=new Cs(d,t,l,h),s.push(c)):t[l]=h}return s}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(bt.add(this._chart,i),!0):void 0}}function As(t,e){const i=t&&t.options||{},s=i.reverse,n=void 0===i.min?e:0,o=void 0===i.max?e:0;return{start:s?o:n,end:s?n:o}}function Ts(t,e){const i=[],s=t._getSortedDatasetMetas(e);let n,o;for(n=0,o=s.length;n0||!i&&e<0)return n.index}return null}function zs(t,e){const{chart:i,_cachedMeta:s}=t,n=i._stacks||(i._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,h=a.axis,c=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(o,a,s),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Vs(t,e){const i=t.controller.index,s=t.vScale&&t.vScale.axis;if(s){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[s]||void 0===e[s][i])return;delete e[s][i],void 0!==e[s]._visualValues&&void 0!==e[s]._visualValues[i]&&delete e[s]._visualValues[i]}}}const Bs=t=>"reset"===t||"none"===t,Ws=(t,e)=>e?t:Object.assign({},t);class Ns{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Es(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&Vs(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),s=(t,e,i,s)=>"x"===t?e:"r"===t?s:i,n=e.xAxisID=l(i.xAxisID,Fs(t,"x")),o=e.yAxisID=l(i.yAxisID,Fs(t,"y")),a=e.rAxisID=l(i.rAxisID,Fs(t,"r")),r=e.indexAxis,h=e.iAxisID=s(r,n,o,a),c=e.vAxisID=s(r,o,n,a);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(o),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(h),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&rt(this._data,this),t._stacked&&Vs(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(o(e)){const t=this._cachedMeta;this._data=function(t,e){const{iScale:i,vScale:s}=e,n="x"===i.axis?"x":"y",o="x"===s.axis?"x":"y",a=Object.keys(t),r=new Array(a.length);let l,h,c;for(l=0,h=a.length;l0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=s,i._sorted=!0,d=s;else{d=n(s[t])?this.parseArrayData(i,s,t,e):o(s[t])?this.parseObjectData(i,s,t,e):this.parsePrimitiveData(i,s,t,e);const a=()=>null===c[l]||f&&c[l]t&&!e.hidden&&e._stacked&&{keys:Ts(i,!0),values:null})(e,i,this.chart),h={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:s,maxDefined:n}=t.getUserBounds();return{min:s?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(r);let u,f;function g(){f=s[u];const e=f[r.axis];return!a(f[t.axis])||c>e||d=0;--u)if(!g()){this.updateRangeFromParsed(h,t,f,l);break}return h}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let s,n,o;for(s=0,n=e.length;s=0&&tthis.getContext(i,s,e)),c);return f.$shared&&(f.$shared=r,n[o]=Object.freeze(Ws(f,r))),f}_resolveAnimations(t,e,i){const s=this.chart,n=this._cachedDataOpts,o=`animation-${e}`,a=n[o];if(a)return a;let r;if(!1!==s.options.animation){const s=this.chart.config,n=s.datasetAnimationScopeKeys(this._type,e),o=s.getOptionScopes(this.getDataset(),n);r=s.createResolver(o,this.getContext(t,i,e))}const l=new Os(s,r&&r.animations);return r&&r._cacheable&&(n[o]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Bs(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),s=this._sharedOptions,n=this.getSharedOptions(i),o=this.includeOptions(e,n)||n!==s;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:o}}updateElement(t,e,i,s){Bs(s)?Object.assign(t,i):this._resolveAnimations(e,s).update(t,i)}updateSharedOptions(t,e,i){t&&!Bs(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,s){t.active=s;const n=this.getStyle(e,s);this._resolveAnimations(e,i,s).update(t,{options:!s&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const s=i.length,n=e.length,o=Math.min(n,s);o&&this.parse(0,o),n>s?this._insertElements(s,n-s,t):n{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;a{s[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),s}}function js(t,e){const i=t.options.ticks,n=function(t){const e=t.options.offset,i=t._tickSize(),s=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(s,n))}(t),o=Math.min(i.maxTicksLimit||n,n),a=i.major.enabled?function(t){const e=[];let i,s;for(i=0,s=t.length;io)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;nn)return e}return Math.max(n,1)}(a,e,o);if(r>0){let t,i;const n=r>1?Math.round((h-l)/(r-1)):null;for($s(e,c,d,s(n)?0:l-n,l),t=0,i=r-1;t"top"===e||"left"===e?t[e]+i:t[e]-i,Us=(t,e)=>Math.min(e||t,t);function Xs(t,e){const i=[],s=t.length/e,n=t.length;let o=0;for(;oa+r)))return h}function Ks(t){return t.drawTicks?t.tickLength:0}function Gs(t,e){if(!t.display)return 0;const i=Si(t.font,e),s=ki(t.padding);return(n(t.text)?t.text.length:1)*i.lineHeight+s.height}function Zs(t,e,i){let s=ut(t);return(i&&"right"!==e||!i&&"right"===e)&&(s=(t=>"left"===t?"right":"right"===t?"left":t)(s)),s}class Js extends Hs{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:s}=this;return t=r(t,Number.POSITIVE_INFINITY),e=r(e,Number.NEGATIVE_INFINITY),i=r(i,Number.POSITIVE_INFINITY),s=r(s,Number.NEGATIVE_INFINITY),{min:r(t,i),max:r(e,s),minDefined:a(t),maxDefined:a(e)}}getMinMax(t){let e,{min:i,max:s,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:i,max:s};const a=this.getMatchingVisibleMetas();for(let r=0,l=a.length;rs?s:i,s=n&&i>s?i:s,{min:r(i,r(s,i)),max:r(s,r(i,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){d(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:s,grace:n,ticks:o}=this.options,a=o.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=Di(this,n,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=a=n||i<=1||!this.isHorizontal())return void(this.labelRotation=s);const h=this._getLabelSizes(),c=h.widest.width,d=h.highest.height,u=J(this.chart.width-c,0,this.maxWidth);o=t.offset?this.maxWidth/i:u/(i-1),c+6>o&&(o=u/(i-(t.offset?.5:1)),a=this.maxHeight-Ks(t.grid)-e.padding-Gs(t.title,this.chart.options.font),r=Math.sqrt(c*c+d*d),l=Y(Math.min(Math.asin(J((h.highest.height+6)/o,-1,1)),Math.asin(J(a/r,-1,1))-Math.asin(J(d/r,-1,1)))),l=Math.max(s,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){d(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){d(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:s,grid:n}}=this,o=this._isVisible(),a=this.isHorizontal();if(o){const o=Gs(s,e.options.font);if(a?(t.width=this.maxWidth,t.height=Ks(n)+o):(t.height=this.maxHeight,t.width=Ks(n)+o),i.display&&this.ticks.length){const{first:e,last:s,widest:n,highest:o}=this._getLabelSizes(),r=2*i.padding,l=$(this.labelRotation),h=Math.cos(l),c=Math.sin(l);if(a){const e=i.mirror?0:c*n.width+h*o.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:h*n.width+c*o.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,s,c,h)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,s){const{ticks:{align:n,padding:o},position:a}=this.options,r=0!==this.labelRotation,l="top"!==a&&"x"===this.axis;if(this.isHorizontal()){const a=this.getPixelForTick(0)-this.left,h=this.right-this.getPixelForTick(this.ticks.length-1);let c=0,d=0;r?l?(c=s*t.width,d=i*e.height):(c=i*t.height,d=s*e.width):"start"===n?d=e.width:"end"===n?c=t.width:"inner"!==n&&(c=t.width/2,d=e.width/2),this.paddingLeft=Math.max((c-a+o)*this.width/(this.width-a),0),this.paddingRight=Math.max((d-h+o)*this.width/(this.width-h),0)}else{let i=e.height/2,s=t.height/2;"start"===n?(i=0,s=t.height):"end"===n&&(i=e.height,s=0),this.paddingTop=i+o,this.paddingBottom=s+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){d(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,i;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,i=t.length;e{const i=t.gc,s=i.length/2;let n;if(s>e){for(n=0;n({width:r[t]||0,height:l[t]||0});return{first:P(0),last:P(e-1),widest:P(k),highest:P(S),widths:r,heights:l}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Q(this._alignToPixels?Ae(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&ta*s?a/i:r/s:r*s0}_computeGridLineItems(t){const e=this.axis,i=this.chart,s=this.options,{grid:n,position:a,border:r}=s,h=n.offset,c=this.isHorizontal(),d=this.ticks.length+(h?1:0),u=Ks(n),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,x=function(t){return Ae(i,t,p)};let b,_,y,v,M,w,k,S,P,D,C,O;if("top"===a)b=x(this.bottom),w=this.bottom-u,S=b-m,D=x(t.top)+m,O=t.bottom;else if("bottom"===a)b=x(this.top),D=t.top,O=x(t.bottom)-m,w=b+m,S=this.top+u;else if("left"===a)b=x(this.right),M=this.right-u,k=b-m,P=x(t.left)+m,C=t.right;else if("right"===a)b=x(this.left),P=t.left,C=x(t.right)-m,M=b+m,k=this.left+u;else if("x"===e){if("center"===a)b=x((t.top+t.bottom)/2+.5);else if(o(a)){const t=Object.keys(a)[0],e=a[t];b=x(this.chart.scales[t].getPixelForValue(e))}D=t.top,O=t.bottom,w=b+m,S=w+u}else if("y"===e){if("center"===a)b=x((t.left+t.right)/2);else if(o(a)){const t=Object.keys(a)[0],e=a[t];b=x(this.chart.scales[t].getPixelForValue(e))}M=b-m,k=M-u,P=t.left,C=t.right}const A=l(s.ticks.maxTicksLimit,d),T=Math.max(1,Math.ceil(d/A));for(_=0;_0&&(o-=s/2)}d={left:o,top:n,width:s+e.width,height:i+e.height,color:t.backdropColor}}x.push({label:v,font:P,textOffset:O,options:{rotation:m,color:i,strokeColor:o,strokeWidth:h,textAlign:f,textBaseline:A,translation:[M,w],backdrop:d}})}return x}_getXAxisLabelAlignment(){const{position:t,ticks:e}=this.options;if(-$(this.labelRotation))return"top"===t?"left":"right";let i="center";return"start"===e.align?i="left":"end"===e.align?i="right":"inner"===e.align&&(i="inner"),i}_getYAxisLabelAlignment(t){const{position:e,ticks:{crossAlign:i,mirror:s,padding:n}}=this.options,o=t+n,a=this._getLabelSizes().widest.width;let r,l;return"left"===e?s?(l=this.right+n,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l+=a)):(l=this.right-o,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l=this.left)):"right"===e?s?(l=this.left+n,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l-=a)):(l=this.left+o,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l=this.right)):r="right",{textAlign:r,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;const t=this.chart,e=this.options.position;return"left"===e||"right"===e?{top:0,left:this.left,bottom:t.height,right:this.right}:"top"===e||"bottom"===e?{top:this.top,left:0,bottom:this.bottom,right:t.width}:void 0}drawBackground(){const{ctx:t,options:{backgroundColor:e},left:i,top:s,width:n,height:o}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,s,n,o),t.restore())}getLineWidthForValue(t){const e=this.options.grid;if(!this._isVisible()||!e.display)return 0;const i=this.ticks.findIndex((e=>e.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,o;const a=(t,e,s)=>{s.width&&s.color&&(i.save(),i.lineWidth=s.width,i.strokeStyle=s.color,i.setLineDash(s.borderDash||[]),i.lineDashOffset=s.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,o=s.length;n{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",s=[];let n,o;for(n=0,o=e.length;n{const s=i.split("."),n=s.pop(),o=[t].concat(s).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");ue.route(o,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&ue.describe(e,t.descriptors)}(t,o,i),this.override&&ue.override(t.id,t.overrides)),o}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,s=this.scope;i in e&&delete e[i],s&&i in ue[s]&&(delete ue[s][i],this.override&&delete re[i])}}class tn{constructor(){this.controllers=new Qs(Ns,"datasets",!0),this.elements=new Qs(Hs,"elements"),this.plugins=new Qs(Object,"plugins"),this.scales=new Qs(Js,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const s=i||this._getRegistryForType(e);i||s.isForType(e)||s===this.plugins&&e.id?this._exec(t,s,e):u(e,(e=>{const s=i||this._getRegistryForType(e);this._exec(t,s,e)}))}))}_exec(t,e,i){const s=w(t);d(i["before"+s],[],i),e[t](i),d(i["after"+s],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(s(e,i),t,"stop"),this._notify(s(i,e),t,"start")}}function nn(t,e){return e||!1!==t?!0===t?{}:t:null}function on(t,{plugin:e,local:i},s,n){const o=t.pluginScopeKeys(e),a=t.getOptionScopes(s,o);return i&&e.defaults&&a.push(e.defaults),t.createResolver(a,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function an(t,e){const i=ue.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function rn(t){if("x"===t||"y"===t||"r"===t)return t}function ln(t,...e){if(rn(t))return t;for(const s of e){const e=s.axis||("top"===(i=s.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.length>1&&rn(t[0].toLowerCase());if(e)return e}var i;throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function hn(t,e,i){if(i[e+"AxisID"]===t)return{axis:e}}function cn(t,e){const i=re[t.type]||{scales:{}},s=e.scales||{},n=an(t.type,e),a=Object.create(null);return Object.keys(s).forEach((e=>{const r=s[e];if(!o(r))return console.error(`Invalid scale configuration for scale: ${e}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${e}`);const l=ln(e,r,function(t,e){if(e.data&&e.data.datasets){const i=e.data.datasets.filter((e=>e.xAxisID===t||e.yAxisID===t));if(i.length)return hn(t,"x",i[0])||hn(t,"y",i[0])}return{}}(e,t),ue.scales[r.type]),h=function(t,e){return t===e?"_index_":"_value_"}(l,n),c=i.scales||{};a[e]=b(Object.create(null),[{axis:l},r,c[l],c[h]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,o=i.indexAxis||an(n,e),r=(re[n]||{}).scales||{};Object.keys(r).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,o),n=i[e+"AxisID"]||e;a[n]=a[n]||Object.create(null),b(a[n],[{axis:e},s[n],r[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];b(e,[ue.scales[e.type],ue.scale])})),a}function dn(t){const e=t.options||(t.options={});e.plugins=l(e.plugins,{}),e.scales=cn(t,e)}function un(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const fn=new Map,gn=new Set;function pn(t,e){let i=fn.get(t);return i||(i=e(),fn.set(t,i),gn.add(i)),i}const mn=(t,e,i)=>{const s=M(e,i);void 0!==s&&t.add(s)};class xn{constructor(t){this._config=function(t){return(t=t||{}).data=un(t.data),dn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=un(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),dn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return pn(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return pn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return pn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return pn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let s=i.get(t);return s&&!e||(s=new Map,i.set(t,s)),s}getOptionScopes(t,e,i){const{options:s,type:n}=this,o=this._cachedScopes(t,i),a=o.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>mn(r,t,e)))),e.forEach((t=>mn(r,s,t))),e.forEach((t=>mn(r,re[n]||{},t))),e.forEach((t=>mn(r,ue,t))),e.forEach((t=>mn(r,le,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),gn.has(e)&&o.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,re[e]||{},ue.datasets[e]||{},{type:e},ue,le]}resolveNamedOptions(t,e,i,s=[""]){const o={$shared:!0},{resolver:a,subPrefixes:r}=bn(this._resolverCache,t,s);let l=a;if(function(t,e){const{isScriptable:i,isIndexable:s}=Ye(t);for(const o of e){const e=i(o),a=s(o),r=(a||e)&&t[o];if(e&&(S(r)||_n(r))||a&&n(r))return!0}return!1}(a,e)){o.$shared=!1;l=$e(a,i=S(i)?i():i,this.createResolver(t,i,r))}for(const t of e)o[t]=l[t];return o}createResolver(t,e,i=[""],s){const{resolver:n}=bn(this._resolverCache,t,i);return o(e)?$e(n,e,void 0,s):n}}function bn(t,e,i){let s=t.get(e);s||(s=new Map,t.set(e,s));const n=i.join();let o=s.get(n);if(!o){o={resolver:je(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},s.set(n,o)}return o}const _n=t=>o(t)&&Object.getOwnPropertyNames(t).some((e=>S(t[e])));const yn=["top","bottom","left","right","chartArea"];function vn(t,e){return"top"===t||"bottom"===t||-1===yn.indexOf(t)&&"x"===e}function Mn(t,e){return function(i,s){return i[t]===s[t]?i[e]-s[e]:i[t]-s[t]}}function wn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),d(i&&i.onComplete,[t],e)}function kn(t){const e=t.chart,i=e.options.animation;d(i&&i.onProgress,[t],e)}function Sn(t){return fe()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Pn={},Dn=t=>{const e=Sn(t);return Object.values(Pn).filter((t=>t.canvas===e)).pop()};function Cn(t,e,i){const s=Object.keys(t);for(const n of s){const s=+n;if(s>=e){const o=t[n];delete t[n],(i>0||s>e)&&(t[s+i]=o)}}}function On(t,e,i){return t.options.clip?t[i]:e[i]}class An{static defaults=ue;static instances=Pn;static overrides=re;static registry=en;static version="4.4.3";static getChart=Dn;static register(...t){en.add(...t),Tn()}static unregister(...t){en.remove(...t),Tn()}constructor(t,e){const s=this.config=new xn(e),n=Sn(t),o=Dn(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||ks(n)),this.platform.updateConfig(s);const r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,h=l&&l.height,c=l&&l.width;this.id=i(),this.ctx=r,this.canvas=l,this.width=c,this.height=h,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new sn,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=dt((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],Pn[this.id]=this,r&&l?(bt.listen(this,"complete",wn),bt.listen(this,"progress",kn),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return s(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return en}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():ke(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Te(this.canvas,this.ctx),this}stop(){return bt.stop(this),this}resize(t,e){bt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,s=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(s,t,e,n),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,ke(this,a,!0)&&(this.notifyPlugins("resize",{size:o}),d(i.onResize,[this,o],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){u(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,s=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],s=ln(t,i),n="r"===s,o="x"===s;return{options:i,dposition:n?"chartArea":o?"bottom":"left",dtype:n?"radialLinear":o?"category":"linear"}})))),u(n,(e=>{const n=e.options,o=n.id,a=ln(o,n),r=l(n.type,e.dtype);void 0!==n.position&&vn(n.position,a)===vn(e.dposition)||(n.position=e.dposition),s[o]=!0;let h=null;if(o in i&&i[o].type===r)h=i[o];else{h=new(en.getScale(r))({id:o,type:r,ctx:this.ctx,chart:this}),i[h.id]=h}h.init(n,t)})),u(s,((t,e)=>{t||delete i[e]})),u(i,(t=>{as.configure(this,t,t.options),as.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;te.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,s;for(this._removeUnreferencedMetasets(),i=0,s=e.length;i{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let t=0,e=this.data.datasets.length;t{t.reset()})),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(Mn("z","_idx"));const{_active:a,_lastEvent:r}=this;r?this._eventHandler(r,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){u(this.scales,(t=>{as.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);P(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:s,count:n}of e){Cn(t,s,"_removeElements"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+","+t.splice(1).join(",")))),s=i(0);for(let t=1;tt.split(","))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;as.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],u(this.boxes,(t=>{i&&"chartArea"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,i=t._clip,s=!i.disabled,n=function(t,e){const{xScale:i,yScale:s}=t;return i&&s?{left:On(i,e,"left"),right:On(i,e,"right"),top:On(s,e,"top"),bottom:On(s,e,"bottom")}:e}(t,this.chartArea),o={meta:t,index:t.index,cancelable:!0};!1!==this.notifyPlugins("beforeDatasetDraw",o)&&(s&&Ie(e,{left:!1===i.left?0:n.left-i.left,right:!1===i.right?this.width:n.right+i.right,top:!1===i.top?0:n.top-i.top,bottom:!1===i.bottom?this.height:n.bottom+i.bottom}),t.controller.draw(),s&&ze(e),o.cancelable=!1,this.notifyPlugins("afterDatasetDraw",o))}isPointInArea(t){return Re(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,s){const n=Xi.modes[e];return"function"==typeof n?n(this,t,i,s):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let s=i.filter((t=>t&&t._dataset===e)).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(s)),s}getContext(){return this.$context||(this.$context=Ci(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const s=i?"show":"hide",n=this.getDatasetMeta(t),o=n.controller._resolveAnimations(void 0,s);k(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),o.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?s:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),bt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,i,s),t[i]=s},s=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};u(this.options.events,(t=>i(t,s)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(i,s)=>{t[i]&&(e.removeEventListener(this,i,s),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let o;const a=()=>{s("attach",a),this.attached=!0,this.resize(),i("resize",n),i("detach",o)};o=()=>{this.attached=!1,s("resize",n),this._stop(),this._resize(0,0),i("attach",a)},e.isAttached(this.canvas)?a():o()}unbindEvents(){u(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},u(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const s=i?"set":"remove";let n,o,a,r;for("dataset"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller["_"+s+"DatasetHoverStyle"]()),a=0,r=t.length;a{const i=this.getDatasetMeta(t);if(!i)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:i.data[e],index:e}}));!f(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return 1===this._plugins._cache.filter((e=>e.plugin.id===t)).length}_updateHoverStyles(t,e,i){const s=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),o=n(e,t),a=i?t:n(t,e);o.length&&this.updateHoverStyle(o,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},s=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",i,s))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,s),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:s=[],options:n}=this,o=e,a=this._getActiveElements(t,s,i,o),r=D(t),l=function(t,e,i,s){return i&&"mouseout"!==t.type?s?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,d(n.onHover,[t,a,this],this),r&&d(n.onClick,[t,a,this],this));const h=!f(a,s);return(h||e)&&(this._active=a,this._updateHoverStyles(a,s,e)),this._lastEvent=l,h}_getActiveElements(t,e,i,s){if("mouseout"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,s)}}function Tn(){return u(An.instances,(t=>t._plugins.invalidate()))}function Ln(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class En{static override(t){Object.assign(En.prototype,t)}options;constructor(t){this.options=t||{}}init(){}formats(){return Ln()}parse(){return Ln()}format(){return Ln()}add(){return Ln()}diff(){return Ln()}startOf(){return Ln()}endOf(){return Ln()}}var Rn={_date:En};function In(t){const e=t.iScale,i=function(t,e){if(!t._cache.$bar){const i=t.getMatchingVisibleMetas(e);let s=[];for(let e=0,n=i.length;et-e)))}return t._cache.$bar}(e,t.type);let s,n,o,a,r=e._length;const l=()=>{32767!==o&&-32768!==o&&(k(a)&&(r=Math.min(r,Math.abs(o-a)||r)),a=o)};for(s=0,n=i.length;sMath.abs(r)&&(l=r,h=a),e[i.axis]=h,e._custom={barStart:l,barEnd:h,start:n,end:o,min:a,max:r}}(t,e,i,s):e[i.axis]=i.parse(t,s),e}function Fn(t,e,i,s){const n=t.iScale,o=t.vScale,a=n.getLabels(),r=n===o,l=[];let h,c,d,u;for(h=i,c=i+s;ht.x,i="left",s="right"):(e=t.base"spacing"!==t,_indexable:t=>"spacing"!==t&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i,color:s}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,s=this._cachedMeta;if(!1===this._parsing)s._parsed=i;else{let n,a,r=t=>+i[t];if(o(i[t])){const{key:t="value"}=this._parsing;r=e=>+M(i[e],t)}for(n=t,a=t+e;nZ(t,r,l,!0)?1:Math.max(e,e*i,s,s*i),g=(t,e,s)=>Z(t,r,l,!0)?-1:Math.min(e,e*i,s,s*i),p=f(0,h,d),m=f(E,c,u),x=g(C,h,d),b=g(C+E,c,u);s=(p-x)/2,n=(m-b)/2,o=-(p+x)/2,a=-(m+b)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}(u,d,r),x=(i.width-o)/f,b=(i.height-o)/g,_=Math.max(Math.min(x,b)/2,0),y=c(this.options.radius,_),v=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=p*y,this.offsetY=m*y,s.total=this.calculateTotal(),this.outerRadius=y-v*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-v*l,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,s=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===s._parsed[t]||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*n/O)}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.chartArea,r=o.options.animation,l=(a.left+a.right)/2,h=(a.top+a.bottom)/2,c=n&&r.animateScale,d=c?0:this.innerRadius,u=c?0:this.outerRadius,{sharedOptions:f,includeOptions:g}=this._getSharedOptions(e,s);let p,m=this._getRotation();for(p=0;p0&&!isNaN(t)?O*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t],i.options.locale);return{label:s[t]||"",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let s,n,o,a,r;if(!t)for(s=0,n=i.data.datasets.length;s{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t].r,i.options.locale);return{label:s[t]||"",value:n}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const s=this.getParsed(i).r;!isNaN(s)&&this.chart.getDataVisibility(i)&&(se.max&&(e.max=s))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,s=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(s/2,0),o=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-o*this.index,this.innerRadius=this.outerRadius-o}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,h=r.yCenter,c=r.getIndexAngle(0)-.5*C;let d,u=c;const f=360/this.countVisibleElements();for(d=0;d{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?$(this.resolveDataElementOptions(t,e).angle||i):0}}var Yn=Object.freeze({__proto__:null,BarController:class extends Ns{static id="bar";static defaults={datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}};static overrides={scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}};parsePrimitiveData(t,e,i,s){return Fn(t,e,i,s)}parseArrayData(t,e,i,s){return Fn(t,e,i,s)}parseObjectData(t,e,i,s){const{iScale:n,vScale:o}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l="x"===n.axis?a:r,h="x"===o.axis?a:r,c=[];let d,u,f,g;for(d=i,u=i+s;dt.controller.options.grouped)),o=i.options.stacked,a=[],r=t=>{const i=t.controller.getParsed(e),n=i&&i[t.vScale.axis];if(s(n)||isNaN(n))return!0};for(const i of n)if((void 0===e||!r(i))&&((!1===o||-1===a.indexOf(i.stack)||void 0===o&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getStackIndex(t,e,i){const s=this._getStacks(t,i),n=void 0!==e?s.indexOf(e):-1;return-1===n?s.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,s=[];let n,o;for(n=0,o=e.data.length;n=i?1:-1)}(u,e,r)*a,f===r&&(x-=u/2);const t=e.getPixelForDecimal(0),s=e.getPixelForDecimal(1),o=Math.min(t,s),h=Math.max(t,s);x=Math.max(Math.min(x,h),o),d=x+u,i&&!c&&(l._stacks[e.axis]._visualValues[n]=e.getValueForPixel(d)-e.getValueForPixel(x))}if(x===e.getPixelForValue(r)){const t=F(u)*e.getLineWidthForValue(r)/2;x+=t,u-=t}return{size:u,base:x,head:d,center:d+u/2}}_calculateBarIndexPixels(t,e){const i=e.scale,n=this.options,o=n.skipNull,a=l(n.maxBarThickness,1/0);let r,h;if(e.grouped){const i=o?this._getStackCount(t):e.stackCount,l="flex"===n.barThickness?function(t,e,i,s){const n=e.pixels,o=n[t];let a=t>0?n[t-1]:null,r=t=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:s,yScale:n}=e,o=this.getParsed(t),a=s.getLabelForValue(o.x),r=n.getLabelForValue(o.y),l=o._custom;return{label:i[t]||"",value:"("+a+", "+r+(l?", "+l:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,s){const n="reset"===s,{iScale:o,vScale:a}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,s),h=o.axis,c=a.axis;for(let d=e;d0&&this.getParsed(e-1);for(let i=0;i<_;++i){const g=t[i],_=x?g:{};if(i=b){_.skip=!0;continue}const v=this.getParsed(i),M=s(v[f]),w=_[u]=a.getPixelForValue(v[u],i),k=_[f]=o||M?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,v,l):v[f],i);_.skip=isNaN(w)||isNaN(k)||M,_.stop=i>0&&Math.abs(v[u]-y[u])>m,p&&(_.parsed=v,_.raw=h.data[i]),d&&(_.options=c||this.resolveDataElementOptions(i,g.active?"active":n)),x||this.updateElement(g,i,_,n),y=v}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,s=t.data||[];if(!s.length)return i;const n=s[0].size(this.resolveDataElementOptions(0)),o=s[s.length-1].size(this.resolveDataElementOptions(s.length-1));return Math.max(i,n,o)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}},PieController:class extends jn{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}},PolarAreaController:$n,RadarController:class extends Ns{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta,i=e.dataset,s=e.data||[],n=e.iScale.getLabels();if(i.points=s,"resize"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const o={_loop:!0,_fullLoop:n.length===s.length,options:e};this.updateElement(i,void 0,o,t)}this.updateElements(s,0,s.length,t)}updateElements(t,e,i,s){const n=this._cachedMeta.rScale,o="reset"===s;for(let a=e;a0&&this.getParsed(e-1);for(let c=e;c0&&Math.abs(i[f]-_[f])>x,m&&(p.parsed=i,p.raw=h.data[c]),u&&(p.options=d||this.resolveDataElementOptions(c,e.active?"active":n)),b||this.updateElement(e,c,p,n),_=i}this.updateSharedOptions(d,n,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,s=i.options&&i.options.borderWidth||0;if(!e.length)return s;const n=e[0].size(this.resolveDataElementOptions(0)),o=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(s,n,o)/2}}});function Un(t,e,i,s){const n=vi(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const o=(i-e)/2,a=Math.min(o,s*e/2),r=t=>{const e=(i-Math.min(o,t))*s/2;return J(t,0,Math.min(o,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:J(n.innerStart,0,a),innerEnd:J(n.innerEnd,0,a)}}function Xn(t,e,i,s){return{x:i+t*Math.cos(e),y:s+t*Math.sin(e)}}function qn(t,e,i,s,n,o){const{x:a,y:r,startAngle:l,pixelMargin:h,innerRadius:c}=e,d=Math.max(e.outerRadius+s+i-h,0),u=c>0?c+s+i+h:0;let f=0;const g=n-l;if(s){const t=((c>0?c-s:0)+(d>0?d-s:0))/2;f=(g-(0!==t?g*t/(t+s):g))/2}const p=(g-Math.max(.001,g*d-i/C)/d)/2,m=l+p+f,x=n-p-f,{outerStart:b,outerEnd:_,innerStart:y,innerEnd:v}=Un(e,u,d,x-m),M=d-b,w=d-_,k=m+b/M,S=x-_/w,P=u+y,D=u+v,O=m+y/P,A=x-v/D;if(t.beginPath(),o){const e=(k+S)/2;if(t.arc(a,r,d,k,e),t.arc(a,r,d,e,S),_>0){const e=Xn(w,S,a,r);t.arc(e.x,e.y,_,S,x+E)}const i=Xn(D,x,a,r);if(t.lineTo(i.x,i.y),v>0){const e=Xn(D,A,a,r);t.arc(e.x,e.y,v,x+E,A+Math.PI)}const s=(x-v/u+(m+y/u))/2;if(t.arc(a,r,u,x-v/u,s,!0),t.arc(a,r,u,s,m+y/u,!0),y>0){const e=Xn(P,O,a,r);t.arc(e.x,e.y,y,O+Math.PI,m-E)}const n=Xn(M,m,a,r);if(t.lineTo(n.x,n.y),b>0){const e=Xn(M,k,a,r);t.arc(e.x,e.y,b,m-E,k)}}else{t.moveTo(a,r);const e=Math.cos(k)*d+a,i=Math.sin(k)*d+r;t.lineTo(e,i);const s=Math.cos(S)*d+a,n=Math.sin(S)*d+r;t.lineTo(s,n)}t.closePath()}function Kn(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r,options:l}=e,{borderWidth:h,borderJoinStyle:c,borderDash:d,borderDashOffset:u}=l,f="inner"===l.borderAlign;if(!h)return;t.setLineDash(d||[]),t.lineDashOffset=u,f?(t.lineWidth=2*h,t.lineJoin=c||"round"):(t.lineWidth=h,t.lineJoin=c||"bevel");let g=e.endAngle;if(o){qn(t,e,i,s,g,n);for(let e=0;en?(h=n/l,t.arc(o,a,l,i+h,s-h,!0)):t.arc(o,a,n,i+E,s-E),t.closePath(),t.clip()}(t,e,g),o||(qn(t,e,i,s,g,n),t.stroke())}function Gn(t,e,i=e){t.lineCap=l(i.borderCapStyle,e.borderCapStyle),t.setLineDash(l(i.borderDash,e.borderDash)),t.lineDashOffset=l(i.borderDashOffset,e.borderDashOffset),t.lineJoin=l(i.borderJoinStyle,e.borderJoinStyle),t.lineWidth=l(i.borderWidth,e.borderWidth),t.strokeStyle=l(i.borderColor,e.borderColor)}function Zn(t,e,i){t.lineTo(i.x,i.y)}function Jn(t,e,i={}){const s=t.length,{start:n=0,end:o=s-1}=i,{start:a,end:r}=e,l=Math.max(n,a),h=Math.min(o,r),c=nr&&o>r;return{count:s,start:l,loop:e.loop,ilen:h(a+(h?r-t:t))%o,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=n[b(0)],t.moveTo(d.x,d.y)),c=0;c<=r;++c){if(d=n[b(c)],d.skip)continue;const e=d.x,i=d.y,s=0|e;s===u?(ig&&(g=i),m=(x*m+e)/++x):(_(),t.lineTo(e,i),u=s,x=0,f=g=i),p=i}_()}function eo(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?to:Qn}const io="function"==typeof Path2D;function so(t,e,i,s){io&&!e.options.segment?function(t,e,i,s){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,s)&&n.closePath()),Gn(t,e.options),t.stroke(n)}(t,e,i,s):function(t,e,i,s){const{segments:n,options:o}=e,a=eo(e);for(const r of n)Gn(t,o,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+s-1})&&t.closePath(),t.stroke()}(t,e,i,s)}class no extends Hs{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t&&"fill"!==t};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const s=i.spanGaps?this._loop:this._fullLoop;hi(this._points,i,t,s,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=zi(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this.options,s=t[e],n=this.points,o=Ii(this,{property:e,start:s,end:s});if(!o.length)return;const a=[],r=function(t){return t.stepped?pi:t.tension||"monotone"===t.cubicInterpolationMode?mi:gi}(i);let l,h;for(l=0,h=o.length;l"borderDash"!==t};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,i){const s=this.getProps(["x","y"],i),{angle:n,distance:o}=X(s,{x:t,y:e}),{startAngle:a,endAngle:r,innerRadius:h,outerRadius:c,circumference:d}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],i),u=(this.options.spacing+this.options.borderWidth)/2,f=l(d,r-a)>=O||Z(n,a,r),g=tt(o,h+u,c+u);return f&&g}getCenterPoint(t){const{x:e,y:i,startAngle:s,endAngle:n,innerRadius:o,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),{offset:r,spacing:l}=this.options,h=(s+n)/2,c=(o+a+l+r)/2;return{x:e+Math.cos(h)*c,y:i+Math.sin(h)*c}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,s=(e.offset||0)/4,n=(e.spacing||0)/2,o=e.circular;if(this.pixelMargin="inner"===e.borderAlign?.33:0,this.fullCircles=i>O?Math.floor(i/O):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();const a=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(a)*s,Math.sin(a)*s);const r=s*(1-Math.sin(Math.min(C,i||0)));t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,function(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r}=e;let l=e.endAngle;if(o){qn(t,e,i,s,l,n);for(let e=0;e("string"==typeof e?(i=t.push(e)-1,s.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,s);return n!==t.lastIndexOf(e)?i:n}function po(t){const e=this.getLabels();return t>=0&&ts=e?s:t,a=t=>n=i?n:t;if(t){const t=F(s),e=F(n);t<0&&e<0?a(0):t>0&&e>0&&o(0)}if(s===n){let e=0===n?1:Math.abs(.05*n);a(n+e),t||o(s-e)}this.min=s,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:s}=t;return s?(e=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let i=this.getTickLimit();i=Math.max(2,i);const n=function(t,e){const i=[],{bounds:n,step:o,min:a,max:r,precision:l,count:h,maxTicks:c,maxDigits:d,includeBounds:u}=t,f=o||1,g=c-1,{min:p,max:m}=e,x=!s(a),b=!s(r),_=!s(h),y=(m-p)/(d+1);let v,M,w,k,S=B((m-p)/g/f)*f;if(S<1e-14&&!x&&!b)return[{value:p},{value:m}];k=Math.ceil(m/S)-Math.floor(p/S),k>g&&(S=B(k*S/g/f)*f),s(l)||(v=Math.pow(10,l),S=Math.ceil(S*v)/v),"ticks"===n?(M=Math.floor(p/S)*S,w=Math.ceil(m/S)*S):(M=p,w=m),x&&b&&o&&H((r-a)/o,S/1e3)?(k=Math.round(Math.min((r-a)/S,c)),S=(r-a)/k,M=a,w=r):_?(M=x?a:M,w=b?r:w,k=h-1,S=(w-M)/k):(k=(w-M)/S,k=V(k,Math.round(k),S/1e3)?Math.round(k):Math.ceil(k));const P=Math.max(U(S),U(M));v=Math.pow(10,s(l)?P:l),M=Math.round(M*v)/v,w=Math.round(w*v)/v;let D=0;for(x&&(u&&M!==a?(i.push({value:a}),Mr)break;i.push({value:t})}return b&&u&&w!==r?i.length&&V(i[i.length-1].value,r,mo(r,y,t))?i[i.length-1].value=r:i.push({value:r}):b&&w!==r||i.push({value:w}),i}({maxTicks:i,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:!1!==e.includeBounds},this._range||this);return"ticks"===t.bounds&&j(n,this,"value"),t.reverse?(n.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),n}configure(){const t=this.ticks;let e=this.min,i=this.max;if(super.configure(),this.options.offset&&t.length){const s=(i-e)/Math.max(t.length-1,1)/2;e-=s,i+=s}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(t){return ne(t,this.chart.options.locale,this.options.ticks.format)}}class bo extends xo{static id="linear";static defaults={ticks:{callback:ae.formatters.numeric}};determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?t:0,this.max=a(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){const t=this.isHorizontal(),e=t?this.width:this.height,i=$(this.options.ticks.minRotation),s=(t?Math.sin(i):Math.cos(i))||.001,n=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,n.lineHeight/s))}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}const _o=t=>Math.floor(z(t)),yo=(t,e)=>Math.pow(10,_o(t)+e);function vo(t){return 1===t/Math.pow(10,_o(t))}function Mo(t,e,i){const s=Math.pow(10,i),n=Math.floor(t/s);return Math.ceil(e/s)-n}function wo(t,{min:e,max:i}){e=r(t.min,e);const s=[],n=_o(e);let o=function(t,e){let i=_o(e-t);for(;Mo(t,e,i)>10;)i++;for(;Mo(t,e,i)<10;)i--;return Math.min(i,_o(t))}(e,i),a=o<0?Math.pow(10,Math.abs(o)):1;const l=Math.pow(10,o),h=n>o?Math.pow(10,n):0,c=Math.round((e-h)*a)/a,d=Math.floor((e-h)/l/10)*l*10;let u=Math.floor((c-d)/Math.pow(10,o)),f=r(t.min,Math.round((h+d+u*Math.pow(10,o))*a)/a);for(;f=10?u=u<15?15:20:u++,u>=20&&(o++,u=2,a=o>=0?1:a),f=Math.round((h+d+u*Math.pow(10,o))*a)/a;const g=r(t.max,f);return s.push({value:g,major:vo(g),significand:u}),s}class ko extends Js{static id="logarithmic";static defaults={ticks:{callback:ae.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=xo.prototype.parse.apply(this,[t,e]);if(0!==i)return a(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?Math.max(0,t):null,this.max=a(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!a(this._userMin)&&(this.min=t===yo(this.min,0)?yo(this.min,-1):yo(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,s=this.max;const n=e=>i=t?i:e,o=t=>s=e?s:t;i===s&&(i<=0?(n(1),o(10)):(n(yo(i,-1)),o(yo(s,1)))),i<=0&&n(yo(s,-1)),s<=0&&o(yo(i,1)),this.min=i,this.max=s}buildTicks(){const t=this.options,e=wo({min:this._userMin,max:this._userMax},this);return"ticks"===t.bounds&&j(e,this,"value"),t.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(t){return void 0===t?"0":ne(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure(),this._startValue=z(t),this._valueRange=z(this.max)-z(t)}getPixelForValue(t){return void 0!==t&&0!==t||(t=this.min),null===t||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(z(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function So(t){const e=t.ticks;if(e.display&&t.display){const t=ki(e.backdropPadding);return l(e.font&&e.font.size,ue.font.size)+t.height}return 0}function Po(t,e,i,s,n){return t===s||t===n?{start:e-i/2,end:e+i/2}:tn?{start:e-i,end:e}:{start:e,end:e+i}}function Do(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),s=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?C/a:0;for(let u=0;ue.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.starte.b&&(l=(n.end-e.b)/a,t.b=Math.max(t.b,e.b+l))}function Oo(t,e,i){const s=t.drawingArea,{extra:n,additionalAngle:o,padding:a,size:r}=i,l=t.getPointPosition(e,s+n+a,o),h=Math.round(Y(G(l.angle+E))),c=function(t,e,i){90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e);return t}(l.y,r.h,h),d=function(t){if(0===t||180===t)return"center";if(t<180)return"left";return"right"}(h),u=function(t,e,i){"right"===i?t-=e:"center"===i&&(t-=e/2);return t}(l.x,r.w,d);return{visible:!0,x:l.x,y:c,textAlign:d,left:u,top:c,right:u+r.w,bottom:c+r.h}}function Ao(t,e){if(!e)return!0;const{left:i,top:s,right:n,bottom:o}=t;return!(Re({x:i,y:s},e)||Re({x:i,y:o},e)||Re({x:n,y:s},e)||Re({x:n,y:o},e))}function To(t,e,i){const{left:n,top:o,right:a,bottom:r}=i,{backdropColor:l}=e;if(!s(l)){const i=wi(e.borderRadius),s=ki(e.backdropPadding);t.fillStyle=l;const h=n-s.left,c=o-s.top,d=a-n+s.width,u=r-o+s.height;Object.values(i).some((t=>0!==t))?(t.beginPath(),He(t,{x:h,y:c,w:d,h:u,radius:i}),t.fill()):t.fillRect(h,c,d,u)}}function Lo(t,e,i,s){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,O);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let o=1;ot,padding:5,centerPointLabels:!1}};static defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};static descriptors={angleLines:{_fallback:"grid"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=ki(So(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=a(t)&&!isNaN(t)?t:0,this.max=a(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/So(this.options))}generateTickLabels(t){xo.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=d(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?Do(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return G(t*(O/(this._pointLabels.length||1))+$(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(s(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(s(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t=0;n--){const e=t._pointLabelItems[n];if(!e.visible)continue;const o=s.setContext(t.getPointLabelContext(n));To(i,o,e);const a=Si(o.font),{x:r,y:l,textAlign:h}=e;Ne(i,t._pointLabels[n],r,l+a.lineHeight/2,a,{color:o.color,textAlign:h,textBaseline:"middle"})}}(this,o),s.display&&this.ticks.forEach(((t,e)=>{if(0!==e||0===e&&this.min<0){r=this.getDistanceFromCenterForValue(t.value);const i=this.getContext(e),a=s.setContext(i),l=n.setContext(i);!function(t,e,i,s,n){const o=t.ctx,a=e.circular,{color:r,lineWidth:l}=e;!a&&!s||!r||!l||i<0||(o.save(),o.strokeStyle=r,o.lineWidth=l,o.setLineDash(n.dash),o.lineDashOffset=n.dashOffset,o.beginPath(),Lo(t,i,a,s),o.closePath(),o.stroke(),o.restore())}(this,a,r,o,l)}})),i.display){for(t.save(),a=o-1;a>=0;a--){const s=i.setContext(this.getPointLabelContext(a)),{color:n,lineWidth:o}=s;o&&n&&(t.lineWidth=o,t.strokeStyle=n,t.setLineDash(s.borderDash),t.lineDashOffset=s.borderDashOffset,r=this.getDistanceFromCenterForValue(e.ticks.reverse?this.min:this.max),l=this.getPointPosition(a,r),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(l.x,l.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const s=this.getIndexAngle(0);let n,o;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(s),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach(((s,a)=>{if(0===a&&this.min>=0&&!e.reverse)return;const r=i.setContext(this.getContext(a)),l=Si(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[a].value),r.showLabelBackdrop){t.font=l.string,o=t.measureText(s.label).width,t.fillStyle=r.backdropColor;const e=ki(r.backdropPadding);t.fillRect(-o/2-e.left,-n-l.size/2-e.top,o+e.width,l.size+e.height)}Ne(t,s.label,0,-n,l,{color:r.color,strokeColor:r.textStrokeColor,strokeWidth:r.textStrokeWidth})})),t.restore()}drawTitle(){}}const Ro={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Io=Object.keys(Ro);function zo(t,e){return t-e}function Fo(t,e){if(s(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:r}=t._parseOpts;let l=e;return"function"==typeof n&&(l=n(l)),a(l)||(l="string"==typeof n?i.parse(l,n):i.parse(l)),null===l?null:(o&&(l="week"!==o||!N(r)&&!0!==r?i.startOf(l,o):i.startOf(l,"isoWeek",r)),+l)}function Vo(t,e,i,s){const n=Io.length;for(let o=Io.indexOf(t);o=e?i[s]:i[n]]=!0}}else t[e]=!0}function Wo(t,e,i){const s=[],n={},o=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,s,n,i):s}class No extends Js{static id="time";static defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}};constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const i=t.time||(t.time={}),s=this._adapter=new Rn._date(t.adapters.date);s.init(e),b(i.displayFormats,s.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Fo(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||"day";let{min:s,max:n,minDefined:o,maxDefined:r}=this.getUserBounds();function l(t){o||isNaN(t.min)||(s=Math.min(s,t.min)),r||isNaN(t.max)||(n=Math.max(n,t.max))}o&&r||(l(this._getLabelBounds()),"ticks"===t.bounds&&"labels"===t.ticks.source||l(this.getMinMax(!1))),s=a(s)&&!isNaN(s)?s:+e.startOf(Date.now(),i),n=a(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(s,n-1),this.max=Math.max(s+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,s="labels"===i.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const n=this.min,o=nt(s,n,this.max);return this._unit=e.unit||(i.autoSkip?Vo(e.minUnit,this.min,this.max,this._getLabelCapacity(n)):function(t,e,i,s,n){for(let o=Io.length-1;o>=Io.indexOf(i);o--){const i=Io[o];if(Ro[i].common&&t._adapter.diff(n,s,i)>=e-1)return i}return Io[i?Io.indexOf(i):0]}(this,o.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&"year"!==this._unit?function(t){for(let e=Io.indexOf(t)+1,i=Io.length;e+t.value)))}initOffsets(t=[]){let e,i,s=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),s=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const o=t.length<3?.5:.25;s=J(s,0,o),n=J(n,0,o),this._offsets={start:s,end:n,factor:1/(s+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,s=this.options,n=s.time,o=n.unit||Vo(n.minUnit,e,i,this._getLabelCapacity(e)),a=l(s.ticks.stepSize,1),r="week"===o&&n.isoWeekday,h=N(r)||!0===r,c={};let d,u,f=e;if(h&&(f=+t.startOf(f,"isoWeek",r)),f=+t.startOf(f,h?"day":o),t.diff(i,e,o)>1e5*a)throw new Error(e+" and "+i+" are too far apart with stepSize of "+a+" "+o);const g="data"===s.ticks.source&&this.getDataTimestamps();for(d=f,u=0;d+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}format(t,e){const i=this.options.time.displayFormats,s=this._unit,n=e||i[s];return this._adapter.format(t,n)}_tickFormatFunction(t,e,i,s){const n=this.options,o=n.ticks.callback;if(o)return d(o,[t,e,i],this);const a=n.time.displayFormats,r=this._unit,l=this._majorUnit,h=r&&a[r],c=l&&a[l],u=i[e],f=l&&c&&u&&u.major;return this._adapter.format(t,s||(f?c:h))}generateTickLabels(t){let e,i,s;for(e=0,i=t.length;e0?a:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(t=0,e=s.length;t=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=it(t,"pos",e)),({pos:s,time:o}=t[r]),({pos:n,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=it(t,"time",e)),({time:s,pos:o}=t[r]),({time:n,pos:a}=t[l]));const h=n-s;return h?o+(a-o)*(e-s)/h:o}var jo=Object.freeze({__proto__:null,CategoryScale:class extends Js{static id="category";static defaults={ticks:{callback:po}};constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const t=this.getLabels();for(const{index:i,label:s}of e)t[i]===s&&t.splice(i,1);this._addedLabels=[]}super.init(t)}parse(t,e){if(s(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:J(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:go(i,t,l(e,t),this._addedLabels),i.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:s}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(i=0),e||(s=this.getLabels().length-1)),this.min=i,this.max=s}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,s=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)s.push({value:i});return s}getLabelForValue(t){return po.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return"number"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}},LinearScale:bo,LogarithmicScale:ko,RadialLinearScale:Eo,TimeScale:No,TimeSeriesScale:class extends No{static id="timeseries";static defaults=No.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=Ho(e,this.min),this._tableRange=Ho(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,s=[],n=[];let o,a,r,l,h;for(o=0,a=t.length;o=e&&l<=i&&s.push(l);if(s.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(o=0,a=s.length;ot-e))}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const e=this.getDataTimestamps(),i=this.getLabelTimestamps();return t=e.length&&i.length?this.normalize(e.concat(i)):e.length?e:i,t=this._cache.all=t,t}getDecimalForValue(t){return(Ho(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return Ho(this._table,i*this._tableRange+this._minPos,!0)}}});const $o=["rgb(54, 162, 235)","rgb(255, 99, 132)","rgb(255, 159, 64)","rgb(255, 205, 86)","rgb(75, 192, 192)","rgb(153, 102, 255)","rgb(201, 203, 207)"],Yo=$o.map((t=>t.replace("rgb(","rgba(").replace(")",", 0.5)")));function Uo(t){return $o[t%$o.length]}function Xo(t){return Yo[t%Yo.length]}function qo(t){let e=0;return(i,s)=>{const n=t.getDatasetMeta(s).controller;n instanceof jn?e=function(t,e){return t.backgroundColor=t.data.map((()=>Uo(e++))),e}(i,e):n instanceof $n?e=function(t,e){return t.backgroundColor=t.data.map((()=>Xo(e++))),e}(i,e):n&&(e=function(t,e){return t.borderColor=Uo(e),t.backgroundColor=Xo(e),++e}(i,e))}}function Ko(t){let e;for(e in t)if(t[e].borderColor||t[e].backgroundColor)return!0;return!1}var Go={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,e,i){if(!i.enabled)return;const{data:{datasets:s},options:n}=t.config,{elements:o}=n;if(!i.forceOverride&&(Ko(s)||(a=n)&&(a.borderColor||a.backgroundColor)||o&&Ko(o)))return;var a;const r=qo(t);s.forEach(r)}};function Zo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{configurable:!0,enumerable:!0,writable:!0,value:e})}}function Jo(t){t.data.datasets.forEach((t=>{Zo(t)}))}var Qo={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Jo(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:a,indexAxis:r}=e,l=t.getDatasetMeta(o),h=a||e.data;if("y"===Pi([r,t.options.indexAxis]))return;if(!l.controller.supportsDecimation)return;const c=t.scales[l.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let{start:d,count:u}=function(t,e){const i=e.length;let s,n=0;const{iScale:o}=t,{min:a,max:r,minDefined:l,maxDefined:h}=o.getUserBounds();return l&&(n=J(it(e,o.axis,a).lo,0,i-1)),s=h?J(it(e,o.axis,r).hi+1,n,i)-n:i-n,{start:n,count:s}}(l,h);if(u<=(i.threshold||4*n))return void Zo(e);let f;switch(s(a)&&(e._data=h,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":f=function(t,e,i,s,n){const o=n.samples||s;if(o>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(o-2);let l=0;const h=e+i-1;let c,d,u,f,g,p=e;for(a[l++]=t[p],c=0;cu&&(u=f,d=t[s],g=s);a[l++]=d,p=g}return a[l++]=t[h],a}(h,d,u,n,i);break;case"min-max":f=function(t,e,i,n){let o,a,r,l,h,c,d,u,f,g,p=0,m=0;const x=[],b=e+i-1,_=t[e].x,y=t[b].x-_;for(o=e;og&&(g=l,d=o),p=(m*p+a.x)/++m;else{const i=o-1;if(!s(c)&&!s(d)){const e=Math.min(c,d),s=Math.max(c,d);e!==u&&e!==i&&x.push({...t[e],x:p}),s!==u&&s!==i&&x.push({...t[s],x:p})}o>0&&i!==u&&x.push(t[i]),x.push(a),h=e,m=0,f=g=l,c=d=u=o}}return x}(h,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=f}))},destroy(t){Jo(t)}};function ta(t,e,i,s){if(s)return;let n=e[t],o=i[t];return"angle"===t&&(n=G(n),o=G(o)),{property:t,start:n,end:o}}function ea(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function ia(t,e,i,s){return t&&e?s(t[i],e[i]):t?t[i]:e?e[i]:0}function sa(t,e){let i=[],s=!1;return n(t)?(s=!0,i=t):i=function(t,e){const{x:i=null,y:s=null}=t||{},n=e.points,o=[];return e.segments.forEach((({start:t,end:e})=>{e=ea(t,e,n);const a=n[t],r=n[e];null!==s?(o.push({x:a.x,y:s}),o.push({x:r.x,y:s})):null!==i&&(o.push({x:i,y:a.y}),o.push({x:i,y:r.y}))})),o}(t,e),i.length?new no({points:i,options:{tension:0},_loop:s,_fullLoop:s}):null}function na(t){return t&&!1!==t.fill}function oa(t,e,i){let s=t[e].fill;const n=[e];let o;if(!i)return s;for(;!1!==s&&-1===n.indexOf(s);){if(!a(s))return s;if(o=t[s],!o)return!1;if(o.visible)return s;n.push(s),s=o.fill}return!1}function aa(t,e,i){const s=function(t){const e=t.options,i=e.fill;let s=l(i&&i.target,i);void 0===s&&(s=!!e.backgroundColor);if(!1===s||null===s)return!1;if(!0===s)return"origin";return s}(t);if(o(s))return!isNaN(s.value)&&s;let n=parseFloat(s);return a(n)&&Math.floor(n)===n?function(t,e,i,s){"-"!==t&&"+"!==t||(i=e+i);if(i===e||i<0||i>=s)return!1;return i}(s[0],e,n,i):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function ra(t,e,i){const s=[];for(let n=0;n=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(o,i.axis),s&&i.fill&&da(t.ctx,i,o))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const s=t.getSortedVisibleDatasetMetas();for(let e=s.length-1;e>=0;--e){const i=s[e].$filler;na(i)&&da(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const s=e.meta.$filler;na(s)&&"beforeDatasetDraw"===i.drawTime&&da(t.ctx,s,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const xa=(t,e)=>{let{boxHeight:i=e,boxWidth:s=e}=t;return t.usePointStyle&&(i=Math.min(i,e),s=t.pointStyleWidth||Math.min(s,e)),{boxWidth:s,boxHeight:i,itemHeight:Math.max(e,i)}};class ba extends Hs{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=d(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,s=Si(i.font),n=s.size,o=this._computeTitleHeight(),{boxWidth:a,itemHeight:r}=xa(i,n);let l,h;e.font=s.string,this.isHorizontal()?(l=this.maxWidth,h=this._fitRows(o,n,a,r)+10):(h=this.maxHeight,l=this._fitCols(o,s,a,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,s){const{ctx:n,maxWidth:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],h=s+a;let c=t;n.textAlign="left",n.textBaseline="middle";let d=-1,u=-h;return this.legendItems.forEach(((t,f)=>{const g=i+e/2+n.measureText(t.text).width;(0===f||l[l.length-1]+g+2*a>o)&&(c+=h,l[l.length-(f>0?0:1)]=0,u+=h,d++),r[f]={left:0,top:u,row:d,width:g,height:s},l[l.length-1]+=g+a})),c}_fitCols(t,e,i,s){const{ctx:n,maxHeight:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],h=o-t;let c=a,d=0,u=0,f=0,g=0;return this.legendItems.forEach(((t,o)=>{const{itemWidth:p,itemHeight:m}=function(t,e,i,s,n){const o=function(t,e,i,s){let n=t.text;n&&"string"!=typeof n&&(n=n.reduce(((t,e)=>t.length>e.length?t:e)));return e+i.size/2+s.measureText(n).width}(s,t,e,i),a=function(t,e,i){let s=t;"string"!=typeof e.text&&(s=_a(e,i));return s}(n,s,e.lineHeight);return{itemWidth:o,itemHeight:a}}(i,e,n,t,s);o>0&&u+m+2*a>h&&(c+=d+a,l.push({width:d,height:u}),f+=d+a,g++,d=u=0),r[o]={left:f,top:u,col:g,width:p,height:m},d=Math.max(d,p),u+=m+a})),c+=d,l.push({width:d,height:u}),c}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:s},rtl:n}}=this,o=Oi(n,this.left,this.width);if(this.isHorizontal()){let n=0,a=ft(i,this.left+s,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,a=ft(i,this.left+s,this.right-this.lineWidths[n])),r.top+=this.top+t+s,r.left=o.leftForLtr(o.x(a),r.width),a+=r.width+s}else{let n=0,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height)),r.top=a,r.left+=this.left+s,r.left=o.leftForLtr(o.x(r.left),r.width),a+=r.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Ie(t,this),this._draw(),ze(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:s}=this,{align:n,labels:o}=t,a=ue.color,r=Oi(t.rtl,this.left,this.width),h=Si(o.font),{padding:c}=o,d=h.size,u=d/2;let f;this.drawTitle(),s.textAlign=r.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=h.string;const{boxWidth:g,boxHeight:p,itemHeight:m}=xa(o,d),x=this.isHorizontal(),b=this._computeTitleHeight();f=x?{x:ft(n,this.left+c,this.right-i[0]),y:this.top+c+b,line:0}:{x:this.left+c,y:ft(n,this.top+b+c,this.bottom-e[0].height),line:0},Ai(this.ctx,t.textDirection);const _=m+c;this.legendItems.forEach(((y,v)=>{s.strokeStyle=y.fontColor,s.fillStyle=y.fontColor;const M=s.measureText(y.text).width,w=r.textAlign(y.textAlign||(y.textAlign=o.textAlign)),k=g+u+M;let S=f.x,P=f.y;r.setWidth(this.width),x?v>0&&S+k+c>this.right&&(P=f.y+=_,f.line++,S=f.x=ft(n,this.left+c,this.right-i[f.line])):v>0&&P+_>this.bottom&&(S=f.x=S+e[f.line].width+c,f.line++,P=f.y=ft(n,this.top+b+c,this.bottom-e[f.line].height));if(function(t,e,i){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;s.save();const n=l(i.lineWidth,1);if(s.fillStyle=l(i.fillStyle,a),s.lineCap=l(i.lineCap,"butt"),s.lineDashOffset=l(i.lineDashOffset,0),s.lineJoin=l(i.lineJoin,"miter"),s.lineWidth=n,s.strokeStyle=l(i.strokeStyle,a),s.setLineDash(l(i.lineDash,[])),o.usePointStyle){const a={radius:p*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},l=r.xPlus(t,g/2);Ee(s,a,l,e+u,o.pointStyleWidth&&g)}else{const o=e+Math.max((d-p)/2,0),a=r.leftForLtr(t,g),l=wi(i.borderRadius);s.beginPath(),Object.values(l).some((t=>0!==t))?He(s,{x:a,y:o,w:g,h:p,radius:l}):s.rect(a,o,g,p),s.fill(),0!==n&&s.stroke()}s.restore()}(r.x(S),P,y),S=gt(w,S+g+u,x?S+k:this.right,t.rtl),function(t,e,i){Ne(s,i.text,t,e+m/2,h,{strikethrough:i.hidden,textAlign:r.textAlign(i.textAlign)})}(r.x(S),P,y),x)f.x+=k+c;else if("string"!=typeof y.text){const t=h.lineHeight;f.y+=_a(y,t)+c}else f.y+=_})),Ti(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=Si(e.font),s=ki(e.padding);if(!e.display)return;const n=Oi(t.rtl,this.left,this.width),o=this.ctx,a=e.position,r=i.size/2,l=s.top+r;let h,c=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+l,c=ft(t.align,c,this.right-d);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);h=l+ft(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=ft(a,c,c+d);o.textAlign=n.textAlign(ut(a)),o.textBaseline="middle",o.strokeStyle=e.color,o.fillStyle=e.color,o.font=i.string,Ne(o,e.text,u,h,i)}_computeTitleHeight(){const t=this.options.title,e=Si(t.font),i=ki(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,s,n;if(tt(t,this.left,this.right)&&tt(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;it.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const l=t.controller.getStyle(i?0:void 0),h=ki(l.borderWidth);return{text:e[t.index].label,fillStyle:l.backgroundColor,fontColor:o,hidden:!t.visible,lineCap:l.borderCapStyle,lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:l.borderColor,pointStyle:s||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:a&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class va extends Hs{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const s=n(i.text)?i.text.length:1;this._padding=ki(i.padding);const o=s*Si(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ft(a,i,n),h=e+t,r=n-i):("left"===o.position?(l=i+t,h=ft(a,s,e),c=-.5*C):(l=n-t,h=ft(a,e,s),c=.5*C),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=Si(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ne(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:ut(e.align),textBaseline:"middle",translation:[n,o]})}}var Ma={id:"title",_element:va,start(t,e,i){!function(t,e){const i=new va({ctx:t.ctx,options:e,chart:t});as.configure(t,i,e),as.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;as.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;as.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const wa=new WeakMap;var ka={id:"subtitle",start(t,e,i){const s=new va({ctx:t.ctx,options:i,chart:t});as.configure(t,s,i),as.addBox(t,s),wa.set(t,s)},stop(t){as.removeBox(t,wa.get(t)),wa.delete(t)},beforeUpdate(t,e,i){const s=wa.get(t);as.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Sa={average(t){if(!t.length)return!1;let e,i,s=new Set,n=0,o=0;for(e=0,i=t.length;et+e))/s.size,y:n/o}},nearest(t,e){if(!t.length)return!1;let i,s,n,o=e.x,a=e.y,r=Number.POSITIVE_INFINITY;for(i=0,s=t.length;i-1?t.split("\n"):t}function Ca(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Oa(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=Si(e.bodyFont),h=Si(e.titleFont),c=Si(e.footerFont),d=o.length,f=n.length,g=s.length,p=ki(e.padding);let m=p.height,x=0,b=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(b+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),b){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(b-g)*l.lineHeight+(b-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){x=Math.max(x,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),x+=p.width,{width:x,height:m}}function Aa(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function Ta(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return it.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||Aa(t,e,i,s),yAlign:s}}function La(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:J(g,0,s.width-e.width),y:J(p,0,s.height-e.height)}}function Ea(t,e,i){const s=ki(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function Ra(t){return Pa([],Da(t))}function Ia(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const za={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex{const e={before:[],lines:[],after:[]},n=Ia(i,t);Pa(e.before,Da(Fa(n,"beforeLabel",this,t))),Pa(e.lines,Fa(n,"label",this,t)),Pa(e.after,Da(Fa(n,"afterLabel",this,t))),s.push(e)})),s}getAfterBody(t,e){return Ra(Fa(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Fa(i,"beforeFooter",this,t),n=Fa(i,"footer",this,t),o=Fa(i,"afterFooter",this,t);let a=[];return a=Pa(a,Da(s)),a=Pa(a,Da(n)),a=Pa(a,Da(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;at.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),u(l,(e=>{const i=Ia(t.callbacks,e);s.push(Fa(i,"labelColor",this,e)),n.push(Fa(i,"labelPointStyle",this,e)),o.push(Fa(i,"labelTextColor",this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=Sa[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=Oa(this,i),a=Object.assign({},t,e),r=Ta(this.chart,i,a),l=La(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=wi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,x,b,_,y;return"center"===n?(_=u+g/2,"left"===s?(p=d,m=p-o,b=_+o,y=_-o):(p=d+f,m=p+o,b=_-o,y=_+o),x=p):(m="left"===s?d+Math.max(r,h)+o:"right"===s?d+f-Math.max(l,c)-o:this.caretX,"top"===n?(b=u,_=b-o,p=m-o,x=m+o):(b=u+g,_=b+o,p=m+o,x=m-o),y=b),{x1:p,x2:m,x3:x,y1:b,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=Oi(i.rtl,this.x,this.width);for(t.x=Ea(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",o=Si(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,He(t,{x:e,y:g,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),He(t,{x:i,y:g+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,g,h,l),t.strokeRect(e,g,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=Si(i.bodyFont);let d=c.lineHeight,f=0;const g=Oi(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+d/2),t.y+=d+n},m=g.textAlign(o);let x,b,_,y,v,M,w;for(e.textAlign=o,e.textBaseline="middle",e.font=c.string,t.x=Ea(this,m,i),e.fillStyle=i.bodyColor,u(this.beforeBody,p),f=a&&"right"!==m?"center"===o?l/2+h:l+2+h:0,y=0,M=s.length;y0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=Sa[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=Oa(this,t),a=Object.assign({},i,this._size),r=Ta(e,t,a),l=La(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=ki(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),Ai(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Ti(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!f(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!f(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if("mouseout"===t.type)return[];if(!s)return e.filter((t=>this.chart.data.datasets[t.datasetIndex]&&void 0!==this.chart.getDatasetMeta(t.datasetIndex).controller.getParsed(t.index)));const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=Sa[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}var Ba={id:"tooltip",_element:Va,positioners:Sa,afterInit(t,e,i){i&&(t.tooltip=new Va({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:za},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};return An.register(Yn,jo,fo,t),An.helpers={...Wi},An._adapters=Rn,An.Animation=Cs,An.Animations=Os,An.animator=bt,An.controllers=en.controllers.items,An.DatasetController=Ns,An.Element=Hs,An.elements=fo,An.Interaction=Xi,An.layouts=as,An.platforms=Ss,An.Scale=Js,An.Ticks=ae,Object.assign(An,Yn,jo,fo,t,Ss),An.Chart=An,"undefined"!=typeof window&&(window.Chart=An),An})); -//# sourceMappingURL=chart.umd.js.map +/** + * Skipped minification because the original files appears to be already minified. + * Original file: /npm/chart.js@4.4.3/dist/chart.umd.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +/*! + * Chart.js v4.4.3 + * https://www.chartjs.org + * (c) 2024 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Go},get Decimation(){return Qo},get Filler(){return ma},get Legend(){return ya},get SubTitle(){return ka},get Title(){return Ma},get Tooltip(){return Ba}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;at,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)t-e)).pop(),e}function N(t){return!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;sl&&h=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return nt[s][e]et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;ss&&t[n-1]>i;)n--;return s>0||n{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){const i=t._chartjs;if(!i)return;const s=i.listeners,n=s.indexOf(e);-1!==n&&s.splice(n,1),s.length>0||(ot.forEach((e=>{delete t[e]})),delete t._chartjs)}function lt(t){const e=new Set(t);return e.size===t.length?t:Array.from(e)}const ht="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function ct(t,e){let i=[],s=!1;return function(...n){i=n,s||(s=!0,ht.call(window,(()=>{s=!1,t.apply(e,i)})))}}function dt(t,e){let i;return function(...s){return e?(clearTimeout(i),i=setTimeout(t,e,s)):t.apply(this,s),e}}const ut=t=>"start"===t?"left":"end"===t?"right":"center",ft=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,gt=(t,e,i,s)=>t===(s?"left":"right")?i:"center"===t?(e+i)/2:e;function pt(t,e,i){const s=e.length;let n=0,o=s;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:h,max:c,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(n=J(Math.min(it(r,l,h).lo,i?s:it(e,l,a.getPixelForValue(h)).lo),0,s-1)),o=u?J(Math.max(it(r,a.axis,c,!0).hi+1,i?0:it(e,l,a.getPixelForValue(c),!0).hi+1),n,s)-n:s-n}return{start:n,count:o}}function mt(t){const{xScale:e,yScale:i,_scaleRanges:s}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!s)return t._scaleRanges=n,!0;const o=s.xmin!==e.min||s.xmax!==e.max||s.ymin!==i.min||s.ymax!==i.max;return Object.assign(s,n),o}class xt{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,s){const n=e.listeners[s],o=e.duration;n.forEach((s=>s({chart:t,initial:e.initial,numSteps:o,currentStep:Math.min(i-e.start,o)})))}_refresh(){this._request||(this._running=!0,this._request=ht.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,s)=>{if(!i.running||!i.items.length)return;const n=i.items;let o,a=n.length-1,r=!1;for(;a>=0;--a)o=n[a],o._active?(o._total>i.duration&&(i.duration=o._total),o.tick(t),r=!0):(n[a]=n[n.length-1],n.pop());r&&(s.draw(),this._notify(s,i,t,"progress")),n.length||(i.running=!1,this._notify(s,i,t,"complete"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let s=i.length-1;for(;s>=0;--s)i[s].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}var bt=new xt; +/*! + * @kurkle/color v0.3.2 + * https://github.com/kurkle/color#readme + * (c) 2023 Jukka Kurkela + * Released under the MIT License + */function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),s=Ft(s),t.r=s[0],t.g=s[1],t.b=s[2]}}function qt(t,e){return t?Object.assign(e||{},t):t}function Kt(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Mt(t[3]))):(e=qt(t,{r:0,g:0,b:0,a:1})).a=Mt(e.a),e}function Gt(t){return"r"===t.charAt(0)?function(t){const e=$t.exec(t);let i,s,n,o=255;if(e){if(e[7]!==i){const t=+e[7];o=e[8]?vt(t):yt(255*t,0,255)}return i=+e[1],s=+e[3],n=+e[5],i=255&(e[2]?vt(i):yt(i,0,255)),s=255&(e[4]?vt(s):yt(s,0,255)),n=255&(e[6]?vt(n):yt(n,0,255)),{r:i,g:s,b:n,a:o}}}(t):Bt(t)}class Zt{constructor(t){if(t instanceof Zt)return t;const e=typeof t;let i;var s,n,o;"object"===e?i=Kt(t):"string"===e&&(o=(s=t).length,"#"===s[0]&&(4===o||5===o?n={r:255&17*St[s[1]],g:255&17*St[s[2]],b:255&17*St[s[3]],a:5===o?17*St[s[4]]:255}:7!==o&&9!==o||(n={r:St[s[1]]<<4|St[s[2]],g:St[s[3]]<<4|St[s[4]],b:St[s[5]]<<4|St[s[6]],a:9===o?St[s[7]]<<4|St[s[8]]:255})),i=n||jt(t)||Gt(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=qt(this._rgb);return t&&(t.a=wt(t.a)),t}set rgb(t){this._rgb=Kt(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${wt(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):void 0;var t}hexString(){return this._valid?At(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=It(t),i=e[0],s=kt(e[1]),n=kt(e[2]);return t.a<255?`hsla(${i}, ${s}%, ${n}%, ${wt(t.a)})`:`hsl(${i}, ${s}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,s=t.rgb;let n;const o=e===n?.5:e,a=2*o-1,r=i.a-s.a,l=((a*r==-1?a:(a+r)/(1+a*r))+1)/2;n=1-l,i.r=255&l*i.r+n*s.r+.5,i.g=255&l*i.g+n*s.g+.5,i.b=255&l*i.b+n*s.b+.5,i.a=o*i.a+(1-o)*s.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const s=Ut(wt(t.r)),n=Ut(wt(t.g)),o=Ut(wt(t.b));return{r:Mt(Yt(s+i*(Ut(wt(e.r))-s))),g:Mt(Yt(n+i*(Ut(wt(e.g))-n))),b:Mt(Yt(o+i*(Ut(wt(e.b))-o))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new Zt(this.rgb)}alpha(t){return this._rgb.a=Mt(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=_t(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Xt(this._rgb,2,t),this}darken(t){return Xt(this._rgb,2,-t),this}saturate(t){return Xt(this._rgb,1,t),this}desaturate(t){return Xt(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=It(t);i[0]=Vt(i[0]+e),i=Ft(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function Jt(t){if(t&&"object"==typeof t){const e=t.toString();return"[object CanvasPattern]"===e||"[object CanvasGradient]"===e}return!1}function Qt(t){return Jt(t)?t:new Zt(t)}function te(t){return Jt(t)?t:new Zt(t).saturate(.5).darken(.1).hexString()}const ee=["x","y","borderWidth","radius","tension"],ie=["color","borderColor","backgroundColor"];const se=new Map;function ne(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let s=se.get(i);return s||(s=new Intl.NumberFormat(t,e),se.set(i,s)),s}(e,i).format(t)}const oe={values:t=>n(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const s=this.chart.options.locale;let n,o=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n="scientific"),o=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=z(Math.abs(o)),r=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),ne(t,s,l)},logarithmic(t,e,i){if(0===t)return"0";const s=i[e].significand||t/Math.pow(10,Math.floor(z(t)));return[1,2,3,5,10,15].includes(s)||e>.8*i.length?oe.numeric.call(this,t,e,i):""}};var ae={formatters:oe};const re=Object.create(null),le=Object.create(null);function he(t,e){if(!e)return t;const i=e.split(".");for(let e=0,s=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>te(e.backgroundColor),this.hoverBorderColor=(t,e)=>te(e.borderColor),this.hoverColor=(t,e)=>te(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return ce(this,t,e)}get(t){return he(this,t)}describe(t,e){return ce(le,t,e)}override(t,e){return ce(re,t,e)}route(t,e,i,s){const n=he(this,t),a=he(this,i),r="_"+e;Object.defineProperties(n,{[r]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[r],e=a[s];return o(t)?Object.assign({},e,t):l(t,e)},set(t){this[r]=t}}})}apply(t){t.forEach((t=>t(this)))}}var ue=new de({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),t.set("animations",{colors:{type:"color",properties:ie},numbers:{type:"number",properties:ee}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}})},function(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ae.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t&&"dash"!==t}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t})}]);function fe(){return"undefined"!=typeof window&&"undefined"!=typeof document}function ge(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function pe(t,e,i){let s;return"string"==typeof t?(s=parseInt(t,10),-1!==t.indexOf("%")&&(s=s/100*e.parentNode[i])):s=t,s}const me=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function xe(t,e){return me(t).getPropertyValue(e)}const be=["top","right","bottom","left"];function _e(t,e,i){const s={};i=i?"-"+i:"";for(let n=0;n<4;n++){const o=be[n];s[o]=parseFloat(t[e+"-"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}const ye=(t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot);function ve(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=me(i),o="border-box"===n.boxSizing,a=_e(n,"padding"),r=_e(n,"border","width"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(ye(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const Me=t=>Math.round(10*t)/10;function we(t,e,i,s){const n=me(t),o=_e(n,"margin"),a=pe(n.maxWidth,t,"clientWidth")||T,r=pe(n.maxHeight,t,"clientHeight")||T,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=t&&ge(t);if(o){const t=o.getBoundingClientRect(),a=me(o),r=_e(a,"border","width"),l=_e(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=pe(a.maxWidth,o,"clientWidth"),n=pe(a.maxHeight,o,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||T,maxHeight:n||T}}(t,e,i);let{width:h,height:c}=l;if("content-box"===n.boxSizing){const t=_e(n,"border","width"),e=_e(n,"padding");h-=e.width+t.width,c-=e.height+t.height}h=Math.max(0,h-o.width),c=Math.max(0,s?h/s:c-o.height),h=Me(Math.min(h,a,l.maxWidth)),c=Me(Math.min(c,r,l.maxHeight)),h&&!c&&(c=Me(h/2));return(void 0!==e||void 0!==i)&&s&&l.height&&c>l.height&&(c=l.height,h=Me(Math.floor(c*s))),{width:h,height:c}}function ke(t,e,i){const s=e||1,n=Math.floor(t.height*s),o=Math.floor(t.width*s);t.height=Math.floor(t.height),t.width=Math.floor(t.width);const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const Se=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};fe()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch(t){}return t}();function Pe(t,e){const i=xe(t,e),s=i&&i.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function De(t){return!t||s(t.size)||s(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function Ce(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function Oe(t,e,i,s){let o=(s=s||{}).data=s.data||{},a=s.garbageCollect=s.garbageCollect||[];s.font!==e&&(o=s.data={},a=s.garbageCollect=[],s.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;hi.length){for(h=0;h0&&t.stroke()}}function Re(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==r.strokeColor;let c,d;for(t.save(),t.font=a.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]),s(e.rotation)||t.rotate(e.rotation),e.color&&(t.fillStyle=e.color),e.textAlign&&(t.textAlign=e.textAlign),e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,r),c=0;ct[0])){const o=i||t;void 0===s&&(s=ti("_fallback",t));const a={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:o,_fallback:s,_getTarget:n,override:i=>je([i,...t],e,o,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,s)=>qe(i,s,(()=>function(t,e,i,s){let n;for(const o of e)if(n=ti(Ue(o,t),i),void 0!==n)return Xe(t,n)?Je(i,s,t,n):n}(s,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ei(t).includes(e),ownKeys:t=>ei(t),set(t,e,i){const s=t._storage||(t._storage=n());return t[e]=s[e]=i,delete t._keys,!0}})}function $e(t,e,i,s){const a={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Ye(t,s),setContext:e=>$e(t,e,i,s),override:n=>$e(t.override(n),e,i,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>qe(t,e,(()=>function(t,e,i){const{_proxy:s,_context:a,_subProxy:r,_descriptors:l}=t;let h=s[e];S(h)&&l.isScriptable(e)&&(h=function(t,e,i,s){const{_proxy:n,_context:o,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t);let l=e(o,a||s);r.delete(t),Xe(t,l)&&(l=Je(n._scopes,n,t,l));return l}(e,h,t,i));n(h)&&h.length&&(h=function(t,e,i,s){const{_proxy:n,_context:a,_subProxy:r,_descriptors:l}=i;if(void 0!==a.index&&s(t))return e[a.index%e.length];if(o(e[0])){const i=e,s=n._scopes.filter((t=>t!==i));e=[];for(const o of i){const i=Je(s,n,t,o);e.push($e(i,a,r&&r[t],l))}}return e}(e,h,t,l.isIndexable));Xe(e,h)&&(h=$e(h,a,r&&r[e],l));return h}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,s)=>(t[i]=s,delete e[i],!0)})}function Ye(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:s=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:s,isScriptable:S(i)?i:()=>i,isIndexable:S(s)?s:()=>s}}const Ue=(t,e)=>t?t+w(e):e,Xe=(t,e)=>o(e)&&"adapters"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function qe(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e)||"constructor"===e)return t[e];const s=i();return t[e]=s,s}function Ke(t,e,i){return S(t)?t(e,i):t}const Ge=(t,e)=>!0===t?e:"string"==typeof t?M(e,t):void 0;function Ze(t,e,i,s,n){for(const o of e){const e=Ge(i,o);if(e){t.add(e);const o=Ke(e._fallback,i,n);if(void 0!==o&&o!==i&&o!==s)return o}else if(!1===e&&void 0!==s&&i!==s)return null}return!1}function Je(t,e,i,s){const a=e._rootScopes,r=Ke(e._fallback,i,s),l=[...t,...a],h=new Set;h.add(s);let c=Qe(h,l,i,r||i,s);return null!==c&&((void 0===r||r===i||(c=Qe(h,l,r,c,s),null!==c))&&je(Array.from(h),[""],a,r,(()=>function(t,e,i){const s=t._getTarget();e in s||(s[e]={});const a=s[e];if(n(a)&&o(i))return i;return a||{}}(e,i,s))))}function Qe(t,e,i,s,n){for(;i;)i=Ze(t,e,i,s,n);return i}function ti(t,e){for(const i of e){if(!i)continue;const e=i[t];if(void 0!==e)return e}}function ei(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}function ii(t,e,i,s){const{iScale:n}=t,{key:o="r"}=this._parsing,a=new Array(s);let r,l,h,c;for(r=0,l=s;re"x"===t?"y":"x";function ai(t,e,i,s){const n=t.skip?e:t,o=e,a=i.skip?e:i,r=q(o,n),l=q(a,o);let h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;const d=s*h,u=s*c;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function ri(t,e="x"){const i=oi(e),s=t.length,n=Array(s).fill(0),o=Array(s);let a,r,l,h=ni(t,0);for(a=0;a!t.skip))),"monotone"===e.cubicInterpolationMode)ri(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o0===t||1===t,di=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),ui=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,fi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*E),easeOutSine:t=>Math.sin(t*E),easeInOutSine:t=>-.5*(Math.cos(C*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>ci(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ci(t)?t:di(t,.075,.3),easeOutElastic:t=>ci(t)?t:ui(t,.075,.3),easeInOutElastic(t){const e=.1125;return ci(t)?t:t<.5?.5*di(2*t,e,.45):.5+.5*ui(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-fi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*fi.easeInBounce(2*t):.5*fi.easeOutBounce(2*t-1)+.5};function gi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function pi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:"middle"===s?i<.5?t.y:e.y:"after"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function mi(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=gi(t,n,i),r=gi(n,o,i),l=gi(o,e,i),h=gi(a,r,i),c=gi(r,l,i);return gi(h,c,i)}const xi=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,bi=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function _i(t,e){const i=(""+t).match(xi);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}const yi=t=>+t||0;function vi(t,e){const i={},s=o(e),n=s?Object.keys(e):e,a=o(t)?s?i=>l(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=yi(a(t));return i}function Mi(t){return vi(t,{top:"y",right:"x",bottom:"y",left:"x"})}function wi(t){return vi(t,["topLeft","topRight","bottomLeft","bottomRight"])}function ki(t){const e=Mi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Si(t,e){t=t||{},e=e||ue.font;let i=l(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let s=l(t.style,e.style);s&&!(""+s).match(bi)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);const n={family:l(t.family,e.family),lineHeight:_i(l(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:l(t.weight,e.weight),string:""};return n.string=De(n),n}function Pi(t,e,i,s){let o,a,r,l=!0;for(o=0,a=t.length;oi&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function Ci(t,e){return Object.assign(Object.create(t),e)}function Oi(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Ai(t,e){let i,s;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,s=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=s)}function Ti(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Li(t){return"angle"===t?{between:Z,compare:K,normalize:G}:{between:tt,compare:(t,e)=>t-e,normalize:t=>t}}function Ei({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Ri(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Li(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Li(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;hb||l(n,x,p)&&0!==r(n,x),v=()=>!b||0===r(o,p)||l(o,x,p);for(let t=c,i=c;t<=d;++t)m=e[t%a],m.skip||(p=h(m[s]),p!==x&&(b=l(p,n,o),null===_&&y()&&(_=0===r(p,n)?t:i),null!==_&&v()&&(g.push(Ei({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,x=p));return null!==_&&g.push(Ei({start:_,end:d,loop:u,count:a,style:f})),g}function Ii(t,e){const i=[],s=t.segments;for(let n=0;nn&&t[o%e].skip;)o--;return o%=e,{start:n,end:o}}(i,n,o,s);if(!0===s)return Fi(t,[{start:a,end:r,loop:o}],i,e);return Fi(t,function(t,e,i,s){const n=t.length,o=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%n];i.skip||i.stop?l.skip||(s=!1,o.push({start:e%n,end:(a-1)%n,loop:s}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&o.push({start:e%n,end:r%n,loop:s}),o}(i,a,r{t[a](e[i],n)&&(o.push({element:t,datasetIndex:s,index:l}),r=r||t.inRange(e.x,e.y,n))})),s&&!r?[]:o}var Xi={evaluateInteractionItems:Hi,modes:{index(t,e,i,s){const n=ve(e,t),o=i.axis||"x",a=i.includeInvisible||!1,r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;let r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;tji(t,ve(e,t),i.axis||"xy",s,i.includeInvisible||!1),nearest(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;return Yi(t,n,o,i.intersect,s,a)},x:(t,e,i,s)=>Ui(t,ve(e,t),"x",i.intersect,s),y:(t,e,i,s)=>Ui(t,ve(e,t),"y",i.intersect,s)}};const qi=["left","top","right","bottom"];function Ki(t,e){return t.filter((t=>t.pos===e))}function Gi(t,e){return t.filter((t=>-1===qi.indexOf(t.pos)&&t.box.axis===e))}function Zi(t,e){return t.sort(((t,i)=>{const s=e?i:t,n=e?t:i;return s.weight===n.weight?s.index-n.index:s.weight-n.weight}))}function Ji(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:s,stackWeight:n}=i;if(!t||!qi.includes(s))continue;const o=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});o.count++,o.weight+=n}return e}(t),{vBoxMaxWidth:s,hBoxMaxHeight:n}=e;let o,a,r;for(o=0,a=t.length;o{s[t]=Math.max(e[t],i[t])})),s}return s(t?["left","right"]:["top","bottom"])}function ss(t,e,i,s){const n=[];let o,a,r,l,h,c;for(o=0,a=t.length,h=0;ot.box.fullSize)),!0),s=Zi(Ki(e,"left"),!0),n=Zi(Ki(e,"right")),o=Zi(Ki(e,"top"),!0),a=Zi(Ki(e,"bottom")),r=Gi(e,"x"),l=Gi(e,"y");return{fullSize:i,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Ki(e,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}(t.boxes),l=r.vertical,h=r.horizontal;u(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const c=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/c,hBoxMaxHeight:a/2}),f=Object.assign({},n);ts(f,ki(s));const g=Object.assign({maxPadding:f,w:o,h:a,x:n.left,y:n.top},n),p=Ji(l.concat(h),d);ss(r.fullSize,g,d,p),ss(l,g,d,p),ss(h,g,d,p)&&ss(l,g,d,p),function(t){const e=t.maxPadding;function i(i){const s=Math.max(e[i]-t[i],0);return t[i]+=s,s}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(g),os(r.leftAndTop,g,d,p),g.x+=g.w,g.y+=g.h,os(r.rightAndBottom,g,d,p),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},u(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})}))}};class rs{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,s){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,s?Math.floor(e/s):i)}}isAttached(t){return!0}updateConfig(t){}}class ls extends rs{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const hs="$chartjs",cs={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},ds=t=>null===t||""===t;const us=!!Se&&{passive:!0};function fs(t,e,i){t&&t.canvas&&t.canvas.removeEventListener(e,i,us)}function gs(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function ps(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.addedNodes,s),e=e&&!gs(i.removedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function ms(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.removedNodes,s),e=e&&!gs(i.addedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const xs=new Map;let bs=0;function _s(){const t=window.devicePixelRatio;t!==bs&&(bs=t,xs.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function ys(t,e,i){const s=t.canvas,n=s&&ge(s);if(!n)return;const o=ct(((t,e)=>{const s=n.clientWidth;i(t,e),s{const e=t[0],i=e.contentRect.width,s=e.contentRect.height;0===i&&0===s||o(i,s)}));return a.observe(n),function(t,e){xs.size||window.addEventListener("resize",_s),xs.set(t,e)}(t,o),a}function vs(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){xs.delete(t),xs.size||window.removeEventListener("resize",_s)}(t)}function Ms(t,e,i){const s=t.canvas,n=ct((e=>{null!==t.ctx&&i(function(t,e){const i=cs[t.type]||t.type,{x:s,y:n}=ve(t,e);return{type:i,chart:e,native:t,x:void 0!==s?s:null,y:void 0!==n?n:null}}(e,t))}),t);return function(t,e,i){t&&t.addEventListener(e,i,us)}(s,e,n),n}class ws extends rs{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,s=t.getAttribute("height"),n=t.getAttribute("width");if(t[hs]={initial:{height:s,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",ds(n)){const e=Pe(t,"width");void 0!==e&&(t.width=e)}if(ds(s))if(""===t.style.height)t.height=t.width/(e||2);else{const e=Pe(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e[hs])return!1;const i=e[hs].initial;["height","width"].forEach((t=>{const n=i[t];s(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e[hs],!0}addEventListener(t,e,i){this.removeEventListener(t,e);const s=t.$proxies||(t.$proxies={}),n={attach:ps,detach:ms,resize:ys}[e]||Ms;s[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),s=i[e];if(!s)return;({attach:vs,detach:vs,resize:vs}[e]||fs)(t,e,s),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,s){return we(t,e,i,s)}isAttached(t){const e=t&&ge(t);return!(!e||!e.isConnected)}}function ks(t){return!fe()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?ls:ws}var Ss=Object.freeze({__proto__:null,BasePlatform:rs,BasicPlatform:ls,DomPlatform:ws,_detectPlatform:ks});const Ps="transparent",Ds={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const s=Qt(t||Ps),n=s.valid&&Qt(e||Ps);return n&&n.valid?n.mix(s,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class Cs{constructor(t,e,i,s){const n=e[i];s=Pi([t.to,s,n,t.from]);const o=Pi([t.from,n,s]);this._active=!0,this._fn=t.fn||Ds[t.type||typeof o],this._easing=fi[t.easing]||fi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=o,this._to=s,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const s=this._target[this._prop],n=i-this._start,o=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(o,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=Pi([t.to,e,s,t.from]),this._from=Pi([t.from,s,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,s=this._prop,n=this._from,o=this._loop,a=this._to;let r;if(this._active=n!==a&&(o||e1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[s]=this._fn(n,a,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t{const a=t[s];if(!o(a))return;const r={};for(const t of e)r[t]=a[t];(n(a.properties)&&a.properties||[s]).forEach((t=>{t!==s&&i.has(t)||i.set(t,r)}))}))}_animateOptions(t,e){const i=e.options,s=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!s)return[];const n=this._createAnimations(s,i);return i.$shared&&function(t,e){const i=[],s=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,s=[],n=t.$animations||(t.$animations={}),o=Object.keys(e),a=Date.now();let r;for(r=o.length-1;r>=0;--r){const l=o[r];if("$"===l.charAt(0))continue;if("options"===l){s.push(...this._animateOptions(t,e));continue}const h=e[l];let c=n[l];const d=i.get(l);if(c){if(d&&c.active()){c.update(d,h,a);continue}c.cancel()}d&&d.duration?(n[l]=c=new Cs(d,t,l,h),s.push(c)):t[l]=h}return s}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(bt.add(this._chart,i),!0):void 0}}function As(t,e){const i=t&&t.options||{},s=i.reverse,n=void 0===i.min?e:0,o=void 0===i.max?e:0;return{start:s?o:n,end:s?n:o}}function Ts(t,e){const i=[],s=t._getSortedDatasetMetas(e);let n,o;for(n=0,o=s.length;n0||!i&&e<0)return n.index}return null}function zs(t,e){const{chart:i,_cachedMeta:s}=t,n=i._stacks||(i._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,h=a.axis,c=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(o,a,s),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Vs(t,e){const i=t.controller.index,s=t.vScale&&t.vScale.axis;if(s){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[s]||void 0===e[s][i])return;delete e[s][i],void 0!==e[s]._visualValues&&void 0!==e[s]._visualValues[i]&&delete e[s]._visualValues[i]}}}const Bs=t=>"reset"===t||"none"===t,Ws=(t,e)=>e?t:Object.assign({},t);class Ns{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Es(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&Vs(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),s=(t,e,i,s)=>"x"===t?e:"r"===t?s:i,n=e.xAxisID=l(i.xAxisID,Fs(t,"x")),o=e.yAxisID=l(i.yAxisID,Fs(t,"y")),a=e.rAxisID=l(i.rAxisID,Fs(t,"r")),r=e.indexAxis,h=e.iAxisID=s(r,n,o,a),c=e.vAxisID=s(r,o,n,a);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(o),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(h),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&rt(this._data,this),t._stacked&&Vs(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(o(e)){const t=this._cachedMeta;this._data=function(t,e){const{iScale:i,vScale:s}=e,n="x"===i.axis?"x":"y",o="x"===s.axis?"x":"y",a=Object.keys(t),r=new Array(a.length);let l,h,c;for(l=0,h=a.length;l0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=s,i._sorted=!0,d=s;else{d=n(s[t])?this.parseArrayData(i,s,t,e):o(s[t])?this.parseObjectData(i,s,t,e):this.parsePrimitiveData(i,s,t,e);const a=()=>null===c[l]||f&&c[l]t&&!e.hidden&&e._stacked&&{keys:Ts(i,!0),values:null})(e,i,this.chart),h={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:s,maxDefined:n}=t.getUserBounds();return{min:s?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(r);let u,f;function g(){f=s[u];const e=f[r.axis];return!a(f[t.axis])||c>e||d=0;--u)if(!g()){this.updateRangeFromParsed(h,t,f,l);break}return h}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let s,n,o;for(s=0,n=e.length;s=0&&tthis.getContext(i,s,e)),c);return f.$shared&&(f.$shared=r,n[o]=Object.freeze(Ws(f,r))),f}_resolveAnimations(t,e,i){const s=this.chart,n=this._cachedDataOpts,o=`animation-${e}`,a=n[o];if(a)return a;let r;if(!1!==s.options.animation){const s=this.chart.config,n=s.datasetAnimationScopeKeys(this._type,e),o=s.getOptionScopes(this.getDataset(),n);r=s.createResolver(o,this.getContext(t,i,e))}const l=new Os(s,r&&r.animations);return r&&r._cacheable&&(n[o]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Bs(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),s=this._sharedOptions,n=this.getSharedOptions(i),o=this.includeOptions(e,n)||n!==s;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:o}}updateElement(t,e,i,s){Bs(s)?Object.assign(t,i):this._resolveAnimations(e,s).update(t,i)}updateSharedOptions(t,e,i){t&&!Bs(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,s){t.active=s;const n=this.getStyle(e,s);this._resolveAnimations(e,i,s).update(t,{options:!s&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const s=i.length,n=e.length,o=Math.min(n,s);o&&this.parse(0,o),n>s?this._insertElements(s,n-s,t):n{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;a{s[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),s}}function js(t,e){const i=t.options.ticks,n=function(t){const e=t.options.offset,i=t._tickSize(),s=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(s,n))}(t),o=Math.min(i.maxTicksLimit||n,n),a=i.major.enabled?function(t){const e=[];let i,s;for(i=0,s=t.length;io)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;nn)return e}return Math.max(n,1)}(a,e,o);if(r>0){let t,i;const n=r>1?Math.round((h-l)/(r-1)):null;for($s(e,c,d,s(n)?0:l-n,l),t=0,i=r-1;t"top"===e||"left"===e?t[e]+i:t[e]-i,Us=(t,e)=>Math.min(e||t,t);function Xs(t,e){const i=[],s=t.length/e,n=t.length;let o=0;for(;oa+r)))return h}function Ks(t){return t.drawTicks?t.tickLength:0}function Gs(t,e){if(!t.display)return 0;const i=Si(t.font,e),s=ki(t.padding);return(n(t.text)?t.text.length:1)*i.lineHeight+s.height}function Zs(t,e,i){let s=ut(t);return(i&&"right"!==e||!i&&"right"===e)&&(s=(t=>"left"===t?"right":"right"===t?"left":t)(s)),s}class Js extends Hs{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:s}=this;return t=r(t,Number.POSITIVE_INFINITY),e=r(e,Number.NEGATIVE_INFINITY),i=r(i,Number.POSITIVE_INFINITY),s=r(s,Number.NEGATIVE_INFINITY),{min:r(t,i),max:r(e,s),minDefined:a(t),maxDefined:a(e)}}getMinMax(t){let e,{min:i,max:s,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:i,max:s};const a=this.getMatchingVisibleMetas();for(let r=0,l=a.length;rs?s:i,s=n&&i>s?i:s,{min:r(i,r(s,i)),max:r(s,r(i,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){d(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:s,grace:n,ticks:o}=this.options,a=o.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=Di(this,n,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=a=n||i<=1||!this.isHorizontal())return void(this.labelRotation=s);const h=this._getLabelSizes(),c=h.widest.width,d=h.highest.height,u=J(this.chart.width-c,0,this.maxWidth);o=t.offset?this.maxWidth/i:u/(i-1),c+6>o&&(o=u/(i-(t.offset?.5:1)),a=this.maxHeight-Ks(t.grid)-e.padding-Gs(t.title,this.chart.options.font),r=Math.sqrt(c*c+d*d),l=Y(Math.min(Math.asin(J((h.highest.height+6)/o,-1,1)),Math.asin(J(a/r,-1,1))-Math.asin(J(d/r,-1,1)))),l=Math.max(s,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){d(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){d(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:s,grid:n}}=this,o=this._isVisible(),a=this.isHorizontal();if(o){const o=Gs(s,e.options.font);if(a?(t.width=this.maxWidth,t.height=Ks(n)+o):(t.height=this.maxHeight,t.width=Ks(n)+o),i.display&&this.ticks.length){const{first:e,last:s,widest:n,highest:o}=this._getLabelSizes(),r=2*i.padding,l=$(this.labelRotation),h=Math.cos(l),c=Math.sin(l);if(a){const e=i.mirror?0:c*n.width+h*o.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:h*n.width+c*o.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,s,c,h)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,s){const{ticks:{align:n,padding:o},position:a}=this.options,r=0!==this.labelRotation,l="top"!==a&&"x"===this.axis;if(this.isHorizontal()){const a=this.getPixelForTick(0)-this.left,h=this.right-this.getPixelForTick(this.ticks.length-1);let c=0,d=0;r?l?(c=s*t.width,d=i*e.height):(c=i*t.height,d=s*e.width):"start"===n?d=e.width:"end"===n?c=t.width:"inner"!==n&&(c=t.width/2,d=e.width/2),this.paddingLeft=Math.max((c-a+o)*this.width/(this.width-a),0),this.paddingRight=Math.max((d-h+o)*this.width/(this.width-h),0)}else{let i=e.height/2,s=t.height/2;"start"===n?(i=0,s=t.height):"end"===n&&(i=e.height,s=0),this.paddingTop=i+o,this.paddingBottom=s+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){d(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,i;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,i=t.length;e{const i=t.gc,s=i.length/2;let n;if(s>e){for(n=0;n({width:r[t]||0,height:l[t]||0});return{first:P(0),last:P(e-1),widest:P(k),highest:P(S),widths:r,heights:l}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Q(this._alignToPixels?Ae(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&ta*s?a/i:r/s:r*s0}_computeGridLineItems(t){const e=this.axis,i=this.chart,s=this.options,{grid:n,position:a,border:r}=s,h=n.offset,c=this.isHorizontal(),d=this.ticks.length+(h?1:0),u=Ks(n),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,x=function(t){return Ae(i,t,p)};let b,_,y,v,M,w,k,S,P,D,C,O;if("top"===a)b=x(this.bottom),w=this.bottom-u,S=b-m,D=x(t.top)+m,O=t.bottom;else if("bottom"===a)b=x(this.top),D=t.top,O=x(t.bottom)-m,w=b+m,S=this.top+u;else if("left"===a)b=x(this.right),M=this.right-u,k=b-m,P=x(t.left)+m,C=t.right;else if("right"===a)b=x(this.left),P=t.left,C=x(t.right)-m,M=b+m,k=this.left+u;else if("x"===e){if("center"===a)b=x((t.top+t.bottom)/2+.5);else if(o(a)){const t=Object.keys(a)[0],e=a[t];b=x(this.chart.scales[t].getPixelForValue(e))}D=t.top,O=t.bottom,w=b+m,S=w+u}else if("y"===e){if("center"===a)b=x((t.left+t.right)/2);else if(o(a)){const t=Object.keys(a)[0],e=a[t];b=x(this.chart.scales[t].getPixelForValue(e))}M=b-m,k=M-u,P=t.left,C=t.right}const A=l(s.ticks.maxTicksLimit,d),T=Math.max(1,Math.ceil(d/A));for(_=0;_0&&(o-=s/2)}d={left:o,top:n,width:s+e.width,height:i+e.height,color:t.backdropColor}}x.push({label:v,font:P,textOffset:O,options:{rotation:m,color:i,strokeColor:o,strokeWidth:h,textAlign:f,textBaseline:A,translation:[M,w],backdrop:d}})}return x}_getXAxisLabelAlignment(){const{position:t,ticks:e}=this.options;if(-$(this.labelRotation))return"top"===t?"left":"right";let i="center";return"start"===e.align?i="left":"end"===e.align?i="right":"inner"===e.align&&(i="inner"),i}_getYAxisLabelAlignment(t){const{position:e,ticks:{crossAlign:i,mirror:s,padding:n}}=this.options,o=t+n,a=this._getLabelSizes().widest.width;let r,l;return"left"===e?s?(l=this.right+n,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l+=a)):(l=this.right-o,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l=this.left)):"right"===e?s?(l=this.left+n,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l-=a)):(l=this.left+o,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l=this.right)):r="right",{textAlign:r,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;const t=this.chart,e=this.options.position;return"left"===e||"right"===e?{top:0,left:this.left,bottom:t.height,right:this.right}:"top"===e||"bottom"===e?{top:this.top,left:0,bottom:this.bottom,right:t.width}:void 0}drawBackground(){const{ctx:t,options:{backgroundColor:e},left:i,top:s,width:n,height:o}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,s,n,o),t.restore())}getLineWidthForValue(t){const e=this.options.grid;if(!this._isVisible()||!e.display)return 0;const i=this.ticks.findIndex((e=>e.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,o;const a=(t,e,s)=>{s.width&&s.color&&(i.save(),i.lineWidth=s.width,i.strokeStyle=s.color,i.setLineDash(s.borderDash||[]),i.lineDashOffset=s.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,o=s.length;n{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",s=[];let n,o;for(n=0,o=e.length;n{const s=i.split("."),n=s.pop(),o=[t].concat(s).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");ue.route(o,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&ue.describe(e,t.descriptors)}(t,o,i),this.override&&ue.override(t.id,t.overrides)),o}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,s=this.scope;i in e&&delete e[i],s&&i in ue[s]&&(delete ue[s][i],this.override&&delete re[i])}}class tn{constructor(){this.controllers=new Qs(Ns,"datasets",!0),this.elements=new Qs(Hs,"elements"),this.plugins=new Qs(Object,"plugins"),this.scales=new Qs(Js,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const s=i||this._getRegistryForType(e);i||s.isForType(e)||s===this.plugins&&e.id?this._exec(t,s,e):u(e,(e=>{const s=i||this._getRegistryForType(e);this._exec(t,s,e)}))}))}_exec(t,e,i){const s=w(t);d(i["before"+s],[],i),e[t](i),d(i["after"+s],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(s(e,i),t,"stop"),this._notify(s(i,e),t,"start")}}function nn(t,e){return e||!1!==t?!0===t?{}:t:null}function on(t,{plugin:e,local:i},s,n){const o=t.pluginScopeKeys(e),a=t.getOptionScopes(s,o);return i&&e.defaults&&a.push(e.defaults),t.createResolver(a,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function an(t,e){const i=ue.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function rn(t){if("x"===t||"y"===t||"r"===t)return t}function ln(t,...e){if(rn(t))return t;for(const s of e){const e=s.axis||("top"===(i=s.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.length>1&&rn(t[0].toLowerCase());if(e)return e}var i;throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function hn(t,e,i){if(i[e+"AxisID"]===t)return{axis:e}}function cn(t,e){const i=re[t.type]||{scales:{}},s=e.scales||{},n=an(t.type,e),a=Object.create(null);return Object.keys(s).forEach((e=>{const r=s[e];if(!o(r))return console.error(`Invalid scale configuration for scale: ${e}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${e}`);const l=ln(e,r,function(t,e){if(e.data&&e.data.datasets){const i=e.data.datasets.filter((e=>e.xAxisID===t||e.yAxisID===t));if(i.length)return hn(t,"x",i[0])||hn(t,"y",i[0])}return{}}(e,t),ue.scales[r.type]),h=function(t,e){return t===e?"_index_":"_value_"}(l,n),c=i.scales||{};a[e]=b(Object.create(null),[{axis:l},r,c[l],c[h]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,o=i.indexAxis||an(n,e),r=(re[n]||{}).scales||{};Object.keys(r).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,o),n=i[e+"AxisID"]||e;a[n]=a[n]||Object.create(null),b(a[n],[{axis:e},s[n],r[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];b(e,[ue.scales[e.type],ue.scale])})),a}function dn(t){const e=t.options||(t.options={});e.plugins=l(e.plugins,{}),e.scales=cn(t,e)}function un(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const fn=new Map,gn=new Set;function pn(t,e){let i=fn.get(t);return i||(i=e(),fn.set(t,i),gn.add(i)),i}const mn=(t,e,i)=>{const s=M(e,i);void 0!==s&&t.add(s)};class xn{constructor(t){this._config=function(t){return(t=t||{}).data=un(t.data),dn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=un(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),dn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return pn(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return pn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return pn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return pn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let s=i.get(t);return s&&!e||(s=new Map,i.set(t,s)),s}getOptionScopes(t,e,i){const{options:s,type:n}=this,o=this._cachedScopes(t,i),a=o.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>mn(r,t,e)))),e.forEach((t=>mn(r,s,t))),e.forEach((t=>mn(r,re[n]||{},t))),e.forEach((t=>mn(r,ue,t))),e.forEach((t=>mn(r,le,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),gn.has(e)&&o.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,re[e]||{},ue.datasets[e]||{},{type:e},ue,le]}resolveNamedOptions(t,e,i,s=[""]){const o={$shared:!0},{resolver:a,subPrefixes:r}=bn(this._resolverCache,t,s);let l=a;if(function(t,e){const{isScriptable:i,isIndexable:s}=Ye(t);for(const o of e){const e=i(o),a=s(o),r=(a||e)&&t[o];if(e&&(S(r)||_n(r))||a&&n(r))return!0}return!1}(a,e)){o.$shared=!1;l=$e(a,i=S(i)?i():i,this.createResolver(t,i,r))}for(const t of e)o[t]=l[t];return o}createResolver(t,e,i=[""],s){const{resolver:n}=bn(this._resolverCache,t,i);return o(e)?$e(n,e,void 0,s):n}}function bn(t,e,i){let s=t.get(e);s||(s=new Map,t.set(e,s));const n=i.join();let o=s.get(n);if(!o){o={resolver:je(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},s.set(n,o)}return o}const _n=t=>o(t)&&Object.getOwnPropertyNames(t).some((e=>S(t[e])));const yn=["top","bottom","left","right","chartArea"];function vn(t,e){return"top"===t||"bottom"===t||-1===yn.indexOf(t)&&"x"===e}function Mn(t,e){return function(i,s){return i[t]===s[t]?i[e]-s[e]:i[t]-s[t]}}function wn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),d(i&&i.onComplete,[t],e)}function kn(t){const e=t.chart,i=e.options.animation;d(i&&i.onProgress,[t],e)}function Sn(t){return fe()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Pn={},Dn=t=>{const e=Sn(t);return Object.values(Pn).filter((t=>t.canvas===e)).pop()};function Cn(t,e,i){const s=Object.keys(t);for(const n of s){const s=+n;if(s>=e){const o=t[n];delete t[n],(i>0||s>e)&&(t[s+i]=o)}}}function On(t,e,i){return t.options.clip?t[i]:e[i]}class An{static defaults=ue;static instances=Pn;static overrides=re;static registry=en;static version="4.4.3";static getChart=Dn;static register(...t){en.add(...t),Tn()}static unregister(...t){en.remove(...t),Tn()}constructor(t,e){const s=this.config=new xn(e),n=Sn(t),o=Dn(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||ks(n)),this.platform.updateConfig(s);const r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,h=l&&l.height,c=l&&l.width;this.id=i(),this.ctx=r,this.canvas=l,this.width=c,this.height=h,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new sn,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=dt((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],Pn[this.id]=this,r&&l?(bt.listen(this,"complete",wn),bt.listen(this,"progress",kn),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return s(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return en}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():ke(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Te(this.canvas,this.ctx),this}stop(){return bt.stop(this),this}resize(t,e){bt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,s=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(s,t,e,n),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,ke(this,a,!0)&&(this.notifyPlugins("resize",{size:o}),d(i.onResize,[this,o],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){u(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,s=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],s=ln(t,i),n="r"===s,o="x"===s;return{options:i,dposition:n?"chartArea":o?"bottom":"left",dtype:n?"radialLinear":o?"category":"linear"}})))),u(n,(e=>{const n=e.options,o=n.id,a=ln(o,n),r=l(n.type,e.dtype);void 0!==n.position&&vn(n.position,a)===vn(e.dposition)||(n.position=e.dposition),s[o]=!0;let h=null;if(o in i&&i[o].type===r)h=i[o];else{h=new(en.getScale(r))({id:o,type:r,ctx:this.ctx,chart:this}),i[h.id]=h}h.init(n,t)})),u(s,((t,e)=>{t||delete i[e]})),u(i,(t=>{as.configure(this,t,t.options),as.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;te.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,s;for(this._removeUnreferencedMetasets(),i=0,s=e.length;i{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let t=0,e=this.data.datasets.length;t{t.reset()})),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(Mn("z","_idx"));const{_active:a,_lastEvent:r}=this;r?this._eventHandler(r,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){u(this.scales,(t=>{as.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);P(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:s,count:n}of e){Cn(t,s,"_removeElements"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+","+t.splice(1).join(",")))),s=i(0);for(let t=1;tt.split(","))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;as.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],u(this.boxes,(t=>{i&&"chartArea"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,i=t._clip,s=!i.disabled,n=function(t,e){const{xScale:i,yScale:s}=t;return i&&s?{left:On(i,e,"left"),right:On(i,e,"right"),top:On(s,e,"top"),bottom:On(s,e,"bottom")}:e}(t,this.chartArea),o={meta:t,index:t.index,cancelable:!0};!1!==this.notifyPlugins("beforeDatasetDraw",o)&&(s&&Ie(e,{left:!1===i.left?0:n.left-i.left,right:!1===i.right?this.width:n.right+i.right,top:!1===i.top?0:n.top-i.top,bottom:!1===i.bottom?this.height:n.bottom+i.bottom}),t.controller.draw(),s&&ze(e),o.cancelable=!1,this.notifyPlugins("afterDatasetDraw",o))}isPointInArea(t){return Re(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,s){const n=Xi.modes[e];return"function"==typeof n?n(this,t,i,s):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let s=i.filter((t=>t&&t._dataset===e)).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(s)),s}getContext(){return this.$context||(this.$context=Ci(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const s=i?"show":"hide",n=this.getDatasetMeta(t),o=n.controller._resolveAnimations(void 0,s);k(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),o.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?s:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),bt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,i,s),t[i]=s},s=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};u(this.options.events,(t=>i(t,s)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(i,s)=>{t[i]&&(e.removeEventListener(this,i,s),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let o;const a=()=>{s("attach",a),this.attached=!0,this.resize(),i("resize",n),i("detach",o)};o=()=>{this.attached=!1,s("resize",n),this._stop(),this._resize(0,0),i("attach",a)},e.isAttached(this.canvas)?a():o()}unbindEvents(){u(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},u(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const s=i?"set":"remove";let n,o,a,r;for("dataset"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller["_"+s+"DatasetHoverStyle"]()),a=0,r=t.length;a{const i=this.getDatasetMeta(t);if(!i)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:i.data[e],index:e}}));!f(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return 1===this._plugins._cache.filter((e=>e.plugin.id===t)).length}_updateHoverStyles(t,e,i){const s=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),o=n(e,t),a=i?t:n(t,e);o.length&&this.updateHoverStyle(o,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},s=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",i,s))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,s),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:s=[],options:n}=this,o=e,a=this._getActiveElements(t,s,i,o),r=D(t),l=function(t,e,i,s){return i&&"mouseout"!==t.type?s?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,d(n.onHover,[t,a,this],this),r&&d(n.onClick,[t,a,this],this));const h=!f(a,s);return(h||e)&&(this._active=a,this._updateHoverStyles(a,s,e)),this._lastEvent=l,h}_getActiveElements(t,e,i,s){if("mouseout"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,s)}}function Tn(){return u(An.instances,(t=>t._plugins.invalidate()))}function Ln(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class En{static override(t){Object.assign(En.prototype,t)}options;constructor(t){this.options=t||{}}init(){}formats(){return Ln()}parse(){return Ln()}format(){return Ln()}add(){return Ln()}diff(){return Ln()}startOf(){return Ln()}endOf(){return Ln()}}var Rn={_date:En};function In(t){const e=t.iScale,i=function(t,e){if(!t._cache.$bar){const i=t.getMatchingVisibleMetas(e);let s=[];for(let e=0,n=i.length;et-e)))}return t._cache.$bar}(e,t.type);let s,n,o,a,r=e._length;const l=()=>{32767!==o&&-32768!==o&&(k(a)&&(r=Math.min(r,Math.abs(o-a)||r)),a=o)};for(s=0,n=i.length;sMath.abs(r)&&(l=r,h=a),e[i.axis]=h,e._custom={barStart:l,barEnd:h,start:n,end:o,min:a,max:r}}(t,e,i,s):e[i.axis]=i.parse(t,s),e}function Fn(t,e,i,s){const n=t.iScale,o=t.vScale,a=n.getLabels(),r=n===o,l=[];let h,c,d,u;for(h=i,c=i+s;ht.x,i="left",s="right"):(e=t.base"spacing"!==t,_indexable:t=>"spacing"!==t&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i,color:s}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,s=this._cachedMeta;if(!1===this._parsing)s._parsed=i;else{let n,a,r=t=>+i[t];if(o(i[t])){const{key:t="value"}=this._parsing;r=e=>+M(i[e],t)}for(n=t,a=t+e;nZ(t,r,l,!0)?1:Math.max(e,e*i,s,s*i),g=(t,e,s)=>Z(t,r,l,!0)?-1:Math.min(e,e*i,s,s*i),p=f(0,h,d),m=f(E,c,u),x=g(C,h,d),b=g(C+E,c,u);s=(p-x)/2,n=(m-b)/2,o=-(p+x)/2,a=-(m+b)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}(u,d,r),x=(i.width-o)/f,b=(i.height-o)/g,_=Math.max(Math.min(x,b)/2,0),y=c(this.options.radius,_),v=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=p*y,this.offsetY=m*y,s.total=this.calculateTotal(),this.outerRadius=y-v*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-v*l,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,s=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===s._parsed[t]||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*n/O)}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.chartArea,r=o.options.animation,l=(a.left+a.right)/2,h=(a.top+a.bottom)/2,c=n&&r.animateScale,d=c?0:this.innerRadius,u=c?0:this.outerRadius,{sharedOptions:f,includeOptions:g}=this._getSharedOptions(e,s);let p,m=this._getRotation();for(p=0;p0&&!isNaN(t)?O*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t],i.options.locale);return{label:s[t]||"",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let s,n,o,a,r;if(!t)for(s=0,n=i.data.datasets.length;s{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t].r,i.options.locale);return{label:s[t]||"",value:n}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const s=this.getParsed(i).r;!isNaN(s)&&this.chart.getDataVisibility(i)&&(se.max&&(e.max=s))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,s=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(s/2,0),o=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-o*this.index,this.innerRadius=this.outerRadius-o}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,h=r.yCenter,c=r.getIndexAngle(0)-.5*C;let d,u=c;const f=360/this.countVisibleElements();for(d=0;d{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?$(this.resolveDataElementOptions(t,e).angle||i):0}}var Yn=Object.freeze({__proto__:null,BarController:class extends Ns{static id="bar";static defaults={datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}};static overrides={scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}};parsePrimitiveData(t,e,i,s){return Fn(t,e,i,s)}parseArrayData(t,e,i,s){return Fn(t,e,i,s)}parseObjectData(t,e,i,s){const{iScale:n,vScale:o}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l="x"===n.axis?a:r,h="x"===o.axis?a:r,c=[];let d,u,f,g;for(d=i,u=i+s;dt.controller.options.grouped)),o=i.options.stacked,a=[],r=t=>{const i=t.controller.getParsed(e),n=i&&i[t.vScale.axis];if(s(n)||isNaN(n))return!0};for(const i of n)if((void 0===e||!r(i))&&((!1===o||-1===a.indexOf(i.stack)||void 0===o&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getStackIndex(t,e,i){const s=this._getStacks(t,i),n=void 0!==e?s.indexOf(e):-1;return-1===n?s.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,s=[];let n,o;for(n=0,o=e.data.length;n=i?1:-1)}(u,e,r)*a,f===r&&(x-=u/2);const t=e.getPixelForDecimal(0),s=e.getPixelForDecimal(1),o=Math.min(t,s),h=Math.max(t,s);x=Math.max(Math.min(x,h),o),d=x+u,i&&!c&&(l._stacks[e.axis]._visualValues[n]=e.getValueForPixel(d)-e.getValueForPixel(x))}if(x===e.getPixelForValue(r)){const t=F(u)*e.getLineWidthForValue(r)/2;x+=t,u-=t}return{size:u,base:x,head:d,center:d+u/2}}_calculateBarIndexPixels(t,e){const i=e.scale,n=this.options,o=n.skipNull,a=l(n.maxBarThickness,1/0);let r,h;if(e.grouped){const i=o?this._getStackCount(t):e.stackCount,l="flex"===n.barThickness?function(t,e,i,s){const n=e.pixels,o=n[t];let a=t>0?n[t-1]:null,r=t=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:s,yScale:n}=e,o=this.getParsed(t),a=s.getLabelForValue(o.x),r=n.getLabelForValue(o.y),l=o._custom;return{label:i[t]||"",value:"("+a+", "+r+(l?", "+l:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,s){const n="reset"===s,{iScale:o,vScale:a}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,s),h=o.axis,c=a.axis;for(let d=e;d0&&this.getParsed(e-1);for(let i=0;i<_;++i){const g=t[i],_=x?g:{};if(i=b){_.skip=!0;continue}const v=this.getParsed(i),M=s(v[f]),w=_[u]=a.getPixelForValue(v[u],i),k=_[f]=o||M?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,v,l):v[f],i);_.skip=isNaN(w)||isNaN(k)||M,_.stop=i>0&&Math.abs(v[u]-y[u])>m,p&&(_.parsed=v,_.raw=h.data[i]),d&&(_.options=c||this.resolveDataElementOptions(i,g.active?"active":n)),x||this.updateElement(g,i,_,n),y=v}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,s=t.data||[];if(!s.length)return i;const n=s[0].size(this.resolveDataElementOptions(0)),o=s[s.length-1].size(this.resolveDataElementOptions(s.length-1));return Math.max(i,n,o)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}},PieController:class extends jn{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}},PolarAreaController:$n,RadarController:class extends Ns{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta,i=e.dataset,s=e.data||[],n=e.iScale.getLabels();if(i.points=s,"resize"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const o={_loop:!0,_fullLoop:n.length===s.length,options:e};this.updateElement(i,void 0,o,t)}this.updateElements(s,0,s.length,t)}updateElements(t,e,i,s){const n=this._cachedMeta.rScale,o="reset"===s;for(let a=e;a0&&this.getParsed(e-1);for(let c=e;c0&&Math.abs(i[f]-_[f])>x,m&&(p.parsed=i,p.raw=h.data[c]),u&&(p.options=d||this.resolveDataElementOptions(c,e.active?"active":n)),b||this.updateElement(e,c,p,n),_=i}this.updateSharedOptions(d,n,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,s=i.options&&i.options.borderWidth||0;if(!e.length)return s;const n=e[0].size(this.resolveDataElementOptions(0)),o=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(s,n,o)/2}}});function Un(t,e,i,s){const n=vi(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const o=(i-e)/2,a=Math.min(o,s*e/2),r=t=>{const e=(i-Math.min(o,t))*s/2;return J(t,0,Math.min(o,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:J(n.innerStart,0,a),innerEnd:J(n.innerEnd,0,a)}}function Xn(t,e,i,s){return{x:i+t*Math.cos(e),y:s+t*Math.sin(e)}}function qn(t,e,i,s,n,o){const{x:a,y:r,startAngle:l,pixelMargin:h,innerRadius:c}=e,d=Math.max(e.outerRadius+s+i-h,0),u=c>0?c+s+i+h:0;let f=0;const g=n-l;if(s){const t=((c>0?c-s:0)+(d>0?d-s:0))/2;f=(g-(0!==t?g*t/(t+s):g))/2}const p=(g-Math.max(.001,g*d-i/C)/d)/2,m=l+p+f,x=n-p-f,{outerStart:b,outerEnd:_,innerStart:y,innerEnd:v}=Un(e,u,d,x-m),M=d-b,w=d-_,k=m+b/M,S=x-_/w,P=u+y,D=u+v,O=m+y/P,A=x-v/D;if(t.beginPath(),o){const e=(k+S)/2;if(t.arc(a,r,d,k,e),t.arc(a,r,d,e,S),_>0){const e=Xn(w,S,a,r);t.arc(e.x,e.y,_,S,x+E)}const i=Xn(D,x,a,r);if(t.lineTo(i.x,i.y),v>0){const e=Xn(D,A,a,r);t.arc(e.x,e.y,v,x+E,A+Math.PI)}const s=(x-v/u+(m+y/u))/2;if(t.arc(a,r,u,x-v/u,s,!0),t.arc(a,r,u,s,m+y/u,!0),y>0){const e=Xn(P,O,a,r);t.arc(e.x,e.y,y,O+Math.PI,m-E)}const n=Xn(M,m,a,r);if(t.lineTo(n.x,n.y),b>0){const e=Xn(M,k,a,r);t.arc(e.x,e.y,b,m-E,k)}}else{t.moveTo(a,r);const e=Math.cos(k)*d+a,i=Math.sin(k)*d+r;t.lineTo(e,i);const s=Math.cos(S)*d+a,n=Math.sin(S)*d+r;t.lineTo(s,n)}t.closePath()}function Kn(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r,options:l}=e,{borderWidth:h,borderJoinStyle:c,borderDash:d,borderDashOffset:u}=l,f="inner"===l.borderAlign;if(!h)return;t.setLineDash(d||[]),t.lineDashOffset=u,f?(t.lineWidth=2*h,t.lineJoin=c||"round"):(t.lineWidth=h,t.lineJoin=c||"bevel");let g=e.endAngle;if(o){qn(t,e,i,s,g,n);for(let e=0;en?(h=n/l,t.arc(o,a,l,i+h,s-h,!0)):t.arc(o,a,n,i+E,s-E),t.closePath(),t.clip()}(t,e,g),o||(qn(t,e,i,s,g,n),t.stroke())}function Gn(t,e,i=e){t.lineCap=l(i.borderCapStyle,e.borderCapStyle),t.setLineDash(l(i.borderDash,e.borderDash)),t.lineDashOffset=l(i.borderDashOffset,e.borderDashOffset),t.lineJoin=l(i.borderJoinStyle,e.borderJoinStyle),t.lineWidth=l(i.borderWidth,e.borderWidth),t.strokeStyle=l(i.borderColor,e.borderColor)}function Zn(t,e,i){t.lineTo(i.x,i.y)}function Jn(t,e,i={}){const s=t.length,{start:n=0,end:o=s-1}=i,{start:a,end:r}=e,l=Math.max(n,a),h=Math.min(o,r),c=nr&&o>r;return{count:s,start:l,loop:e.loop,ilen:h(a+(h?r-t:t))%o,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=n[b(0)],t.moveTo(d.x,d.y)),c=0;c<=r;++c){if(d=n[b(c)],d.skip)continue;const e=d.x,i=d.y,s=0|e;s===u?(ig&&(g=i),m=(x*m+e)/++x):(_(),t.lineTo(e,i),u=s,x=0,f=g=i),p=i}_()}function eo(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?to:Qn}const io="function"==typeof Path2D;function so(t,e,i,s){io&&!e.options.segment?function(t,e,i,s){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,s)&&n.closePath()),Gn(t,e.options),t.stroke(n)}(t,e,i,s):function(t,e,i,s){const{segments:n,options:o}=e,a=eo(e);for(const r of n)Gn(t,o,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+s-1})&&t.closePath(),t.stroke()}(t,e,i,s)}class no extends Hs{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t&&"fill"!==t};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const s=i.spanGaps?this._loop:this._fullLoop;hi(this._points,i,t,s,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=zi(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this.options,s=t[e],n=this.points,o=Ii(this,{property:e,start:s,end:s});if(!o.length)return;const a=[],r=function(t){return t.stepped?pi:t.tension||"monotone"===t.cubicInterpolationMode?mi:gi}(i);let l,h;for(l=0,h=o.length;l"borderDash"!==t};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,i){const s=this.getProps(["x","y"],i),{angle:n,distance:o}=X(s,{x:t,y:e}),{startAngle:a,endAngle:r,innerRadius:h,outerRadius:c,circumference:d}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],i),u=(this.options.spacing+this.options.borderWidth)/2,f=l(d,r-a)>=O||Z(n,a,r),g=tt(o,h+u,c+u);return f&&g}getCenterPoint(t){const{x:e,y:i,startAngle:s,endAngle:n,innerRadius:o,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),{offset:r,spacing:l}=this.options,h=(s+n)/2,c=(o+a+l+r)/2;return{x:e+Math.cos(h)*c,y:i+Math.sin(h)*c}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,s=(e.offset||0)/4,n=(e.spacing||0)/2,o=e.circular;if(this.pixelMargin="inner"===e.borderAlign?.33:0,this.fullCircles=i>O?Math.floor(i/O):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();const a=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(a)*s,Math.sin(a)*s);const r=s*(1-Math.sin(Math.min(C,i||0)));t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,function(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r}=e;let l=e.endAngle;if(o){qn(t,e,i,s,l,n);for(let e=0;e("string"==typeof e?(i=t.push(e)-1,s.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,s);return n!==t.lastIndexOf(e)?i:n}function po(t){const e=this.getLabels();return t>=0&&ts=e?s:t,a=t=>n=i?n:t;if(t){const t=F(s),e=F(n);t<0&&e<0?a(0):t>0&&e>0&&o(0)}if(s===n){let e=0===n?1:Math.abs(.05*n);a(n+e),t||o(s-e)}this.min=s,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:s}=t;return s?(e=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let i=this.getTickLimit();i=Math.max(2,i);const n=function(t,e){const i=[],{bounds:n,step:o,min:a,max:r,precision:l,count:h,maxTicks:c,maxDigits:d,includeBounds:u}=t,f=o||1,g=c-1,{min:p,max:m}=e,x=!s(a),b=!s(r),_=!s(h),y=(m-p)/(d+1);let v,M,w,k,S=B((m-p)/g/f)*f;if(S<1e-14&&!x&&!b)return[{value:p},{value:m}];k=Math.ceil(m/S)-Math.floor(p/S),k>g&&(S=B(k*S/g/f)*f),s(l)||(v=Math.pow(10,l),S=Math.ceil(S*v)/v),"ticks"===n?(M=Math.floor(p/S)*S,w=Math.ceil(m/S)*S):(M=p,w=m),x&&b&&o&&H((r-a)/o,S/1e3)?(k=Math.round(Math.min((r-a)/S,c)),S=(r-a)/k,M=a,w=r):_?(M=x?a:M,w=b?r:w,k=h-1,S=(w-M)/k):(k=(w-M)/S,k=V(k,Math.round(k),S/1e3)?Math.round(k):Math.ceil(k));const P=Math.max(U(S),U(M));v=Math.pow(10,s(l)?P:l),M=Math.round(M*v)/v,w=Math.round(w*v)/v;let D=0;for(x&&(u&&M!==a?(i.push({value:a}),Mr)break;i.push({value:t})}return b&&u&&w!==r?i.length&&V(i[i.length-1].value,r,mo(r,y,t))?i[i.length-1].value=r:i.push({value:r}):b&&w!==r||i.push({value:w}),i}({maxTicks:i,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:!1!==e.includeBounds},this._range||this);return"ticks"===t.bounds&&j(n,this,"value"),t.reverse?(n.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),n}configure(){const t=this.ticks;let e=this.min,i=this.max;if(super.configure(),this.options.offset&&t.length){const s=(i-e)/Math.max(t.length-1,1)/2;e-=s,i+=s}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(t){return ne(t,this.chart.options.locale,this.options.ticks.format)}}class bo extends xo{static id="linear";static defaults={ticks:{callback:ae.formatters.numeric}};determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?t:0,this.max=a(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){const t=this.isHorizontal(),e=t?this.width:this.height,i=$(this.options.ticks.minRotation),s=(t?Math.sin(i):Math.cos(i))||.001,n=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,n.lineHeight/s))}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}const _o=t=>Math.floor(z(t)),yo=(t,e)=>Math.pow(10,_o(t)+e);function vo(t){return 1===t/Math.pow(10,_o(t))}function Mo(t,e,i){const s=Math.pow(10,i),n=Math.floor(t/s);return Math.ceil(e/s)-n}function wo(t,{min:e,max:i}){e=r(t.min,e);const s=[],n=_o(e);let o=function(t,e){let i=_o(e-t);for(;Mo(t,e,i)>10;)i++;for(;Mo(t,e,i)<10;)i--;return Math.min(i,_o(t))}(e,i),a=o<0?Math.pow(10,Math.abs(o)):1;const l=Math.pow(10,o),h=n>o?Math.pow(10,n):0,c=Math.round((e-h)*a)/a,d=Math.floor((e-h)/l/10)*l*10;let u=Math.floor((c-d)/Math.pow(10,o)),f=r(t.min,Math.round((h+d+u*Math.pow(10,o))*a)/a);for(;f=10?u=u<15?15:20:u++,u>=20&&(o++,u=2,a=o>=0?1:a),f=Math.round((h+d+u*Math.pow(10,o))*a)/a;const g=r(t.max,f);return s.push({value:g,major:vo(g),significand:u}),s}class ko extends Js{static id="logarithmic";static defaults={ticks:{callback:ae.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=xo.prototype.parse.apply(this,[t,e]);if(0!==i)return a(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?Math.max(0,t):null,this.max=a(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!a(this._userMin)&&(this.min=t===yo(this.min,0)?yo(this.min,-1):yo(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,s=this.max;const n=e=>i=t?i:e,o=t=>s=e?s:t;i===s&&(i<=0?(n(1),o(10)):(n(yo(i,-1)),o(yo(s,1)))),i<=0&&n(yo(s,-1)),s<=0&&o(yo(i,1)),this.min=i,this.max=s}buildTicks(){const t=this.options,e=wo({min:this._userMin,max:this._userMax},this);return"ticks"===t.bounds&&j(e,this,"value"),t.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(t){return void 0===t?"0":ne(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure(),this._startValue=z(t),this._valueRange=z(this.max)-z(t)}getPixelForValue(t){return void 0!==t&&0!==t||(t=this.min),null===t||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(z(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function So(t){const e=t.ticks;if(e.display&&t.display){const t=ki(e.backdropPadding);return l(e.font&&e.font.size,ue.font.size)+t.height}return 0}function Po(t,e,i,s,n){return t===s||t===n?{start:e-i/2,end:e+i/2}:tn?{start:e-i,end:e}:{start:e,end:e+i}}function Do(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),s=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?C/a:0;for(let u=0;ue.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.starte.b&&(l=(n.end-e.b)/a,t.b=Math.max(t.b,e.b+l))}function Oo(t,e,i){const s=t.drawingArea,{extra:n,additionalAngle:o,padding:a,size:r}=i,l=t.getPointPosition(e,s+n+a,o),h=Math.round(Y(G(l.angle+E))),c=function(t,e,i){90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e);return t}(l.y,r.h,h),d=function(t){if(0===t||180===t)return"center";if(t<180)return"left";return"right"}(h),u=function(t,e,i){"right"===i?t-=e:"center"===i&&(t-=e/2);return t}(l.x,r.w,d);return{visible:!0,x:l.x,y:c,textAlign:d,left:u,top:c,right:u+r.w,bottom:c+r.h}}function Ao(t,e){if(!e)return!0;const{left:i,top:s,right:n,bottom:o}=t;return!(Re({x:i,y:s},e)||Re({x:i,y:o},e)||Re({x:n,y:s},e)||Re({x:n,y:o},e))}function To(t,e,i){const{left:n,top:o,right:a,bottom:r}=i,{backdropColor:l}=e;if(!s(l)){const i=wi(e.borderRadius),s=ki(e.backdropPadding);t.fillStyle=l;const h=n-s.left,c=o-s.top,d=a-n+s.width,u=r-o+s.height;Object.values(i).some((t=>0!==t))?(t.beginPath(),He(t,{x:h,y:c,w:d,h:u,radius:i}),t.fill()):t.fillRect(h,c,d,u)}}function Lo(t,e,i,s){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,O);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let o=1;ot,padding:5,centerPointLabels:!1}};static defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};static descriptors={angleLines:{_fallback:"grid"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=ki(So(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=a(t)&&!isNaN(t)?t:0,this.max=a(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/So(this.options))}generateTickLabels(t){xo.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=d(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?Do(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return G(t*(O/(this._pointLabels.length||1))+$(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(s(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(s(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t=0;n--){const e=t._pointLabelItems[n];if(!e.visible)continue;const o=s.setContext(t.getPointLabelContext(n));To(i,o,e);const a=Si(o.font),{x:r,y:l,textAlign:h}=e;Ne(i,t._pointLabels[n],r,l+a.lineHeight/2,a,{color:o.color,textAlign:h,textBaseline:"middle"})}}(this,o),s.display&&this.ticks.forEach(((t,e)=>{if(0!==e||0===e&&this.min<0){r=this.getDistanceFromCenterForValue(t.value);const i=this.getContext(e),a=s.setContext(i),l=n.setContext(i);!function(t,e,i,s,n){const o=t.ctx,a=e.circular,{color:r,lineWidth:l}=e;!a&&!s||!r||!l||i<0||(o.save(),o.strokeStyle=r,o.lineWidth=l,o.setLineDash(n.dash),o.lineDashOffset=n.dashOffset,o.beginPath(),Lo(t,i,a,s),o.closePath(),o.stroke(),o.restore())}(this,a,r,o,l)}})),i.display){for(t.save(),a=o-1;a>=0;a--){const s=i.setContext(this.getPointLabelContext(a)),{color:n,lineWidth:o}=s;o&&n&&(t.lineWidth=o,t.strokeStyle=n,t.setLineDash(s.borderDash),t.lineDashOffset=s.borderDashOffset,r=this.getDistanceFromCenterForValue(e.ticks.reverse?this.min:this.max),l=this.getPointPosition(a,r),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(l.x,l.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const s=this.getIndexAngle(0);let n,o;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(s),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach(((s,a)=>{if(0===a&&this.min>=0&&!e.reverse)return;const r=i.setContext(this.getContext(a)),l=Si(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[a].value),r.showLabelBackdrop){t.font=l.string,o=t.measureText(s.label).width,t.fillStyle=r.backdropColor;const e=ki(r.backdropPadding);t.fillRect(-o/2-e.left,-n-l.size/2-e.top,o+e.width,l.size+e.height)}Ne(t,s.label,0,-n,l,{color:r.color,strokeColor:r.textStrokeColor,strokeWidth:r.textStrokeWidth})})),t.restore()}drawTitle(){}}const Ro={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Io=Object.keys(Ro);function zo(t,e){return t-e}function Fo(t,e){if(s(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:r}=t._parseOpts;let l=e;return"function"==typeof n&&(l=n(l)),a(l)||(l="string"==typeof n?i.parse(l,n):i.parse(l)),null===l?null:(o&&(l="week"!==o||!N(r)&&!0!==r?i.startOf(l,o):i.startOf(l,"isoWeek",r)),+l)}function Vo(t,e,i,s){const n=Io.length;for(let o=Io.indexOf(t);o=e?i[s]:i[n]]=!0}}else t[e]=!0}function Wo(t,e,i){const s=[],n={},o=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,s,n,i):s}class No extends Js{static id="time";static defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}};constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const i=t.time||(t.time={}),s=this._adapter=new Rn._date(t.adapters.date);s.init(e),b(i.displayFormats,s.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Fo(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||"day";let{min:s,max:n,minDefined:o,maxDefined:r}=this.getUserBounds();function l(t){o||isNaN(t.min)||(s=Math.min(s,t.min)),r||isNaN(t.max)||(n=Math.max(n,t.max))}o&&r||(l(this._getLabelBounds()),"ticks"===t.bounds&&"labels"===t.ticks.source||l(this.getMinMax(!1))),s=a(s)&&!isNaN(s)?s:+e.startOf(Date.now(),i),n=a(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(s,n-1),this.max=Math.max(s+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,s="labels"===i.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const n=this.min,o=nt(s,n,this.max);return this._unit=e.unit||(i.autoSkip?Vo(e.minUnit,this.min,this.max,this._getLabelCapacity(n)):function(t,e,i,s,n){for(let o=Io.length-1;o>=Io.indexOf(i);o--){const i=Io[o];if(Ro[i].common&&t._adapter.diff(n,s,i)>=e-1)return i}return Io[i?Io.indexOf(i):0]}(this,o.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&"year"!==this._unit?function(t){for(let e=Io.indexOf(t)+1,i=Io.length;e+t.value)))}initOffsets(t=[]){let e,i,s=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),s=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const o=t.length<3?.5:.25;s=J(s,0,o),n=J(n,0,o),this._offsets={start:s,end:n,factor:1/(s+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,s=this.options,n=s.time,o=n.unit||Vo(n.minUnit,e,i,this._getLabelCapacity(e)),a=l(s.ticks.stepSize,1),r="week"===o&&n.isoWeekday,h=N(r)||!0===r,c={};let d,u,f=e;if(h&&(f=+t.startOf(f,"isoWeek",r)),f=+t.startOf(f,h?"day":o),t.diff(i,e,o)>1e5*a)throw new Error(e+" and "+i+" are too far apart with stepSize of "+a+" "+o);const g="data"===s.ticks.source&&this.getDataTimestamps();for(d=f,u=0;d+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}format(t,e){const i=this.options.time.displayFormats,s=this._unit,n=e||i[s];return this._adapter.format(t,n)}_tickFormatFunction(t,e,i,s){const n=this.options,o=n.ticks.callback;if(o)return d(o,[t,e,i],this);const a=n.time.displayFormats,r=this._unit,l=this._majorUnit,h=r&&a[r],c=l&&a[l],u=i[e],f=l&&c&&u&&u.major;return this._adapter.format(t,s||(f?c:h))}generateTickLabels(t){let e,i,s;for(e=0,i=t.length;e0?a:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(t=0,e=s.length;t=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=it(t,"pos",e)),({pos:s,time:o}=t[r]),({pos:n,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=it(t,"time",e)),({time:s,pos:o}=t[r]),({time:n,pos:a}=t[l]));const h=n-s;return h?o+(a-o)*(e-s)/h:o}var jo=Object.freeze({__proto__:null,CategoryScale:class extends Js{static id="category";static defaults={ticks:{callback:po}};constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const t=this.getLabels();for(const{index:i,label:s}of e)t[i]===s&&t.splice(i,1);this._addedLabels=[]}super.init(t)}parse(t,e){if(s(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:J(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:go(i,t,l(e,t),this._addedLabels),i.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:s}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(i=0),e||(s=this.getLabels().length-1)),this.min=i,this.max=s}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,s=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)s.push({value:i});return s}getLabelForValue(t){return po.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return"number"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}},LinearScale:bo,LogarithmicScale:ko,RadialLinearScale:Eo,TimeScale:No,TimeSeriesScale:class extends No{static id="timeseries";static defaults=No.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=Ho(e,this.min),this._tableRange=Ho(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,s=[],n=[];let o,a,r,l,h;for(o=0,a=t.length;o=e&&l<=i&&s.push(l);if(s.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(o=0,a=s.length;ot-e))}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const e=this.getDataTimestamps(),i=this.getLabelTimestamps();return t=e.length&&i.length?this.normalize(e.concat(i)):e.length?e:i,t=this._cache.all=t,t}getDecimalForValue(t){return(Ho(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return Ho(this._table,i*this._tableRange+this._minPos,!0)}}});const $o=["rgb(54, 162, 235)","rgb(255, 99, 132)","rgb(255, 159, 64)","rgb(255, 205, 86)","rgb(75, 192, 192)","rgb(153, 102, 255)","rgb(201, 203, 207)"],Yo=$o.map((t=>t.replace("rgb(","rgba(").replace(")",", 0.5)")));function Uo(t){return $o[t%$o.length]}function Xo(t){return Yo[t%Yo.length]}function qo(t){let e=0;return(i,s)=>{const n=t.getDatasetMeta(s).controller;n instanceof jn?e=function(t,e){return t.backgroundColor=t.data.map((()=>Uo(e++))),e}(i,e):n instanceof $n?e=function(t,e){return t.backgroundColor=t.data.map((()=>Xo(e++))),e}(i,e):n&&(e=function(t,e){return t.borderColor=Uo(e),t.backgroundColor=Xo(e),++e}(i,e))}}function Ko(t){let e;for(e in t)if(t[e].borderColor||t[e].backgroundColor)return!0;return!1}var Go={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,e,i){if(!i.enabled)return;const{data:{datasets:s},options:n}=t.config,{elements:o}=n;if(!i.forceOverride&&(Ko(s)||(a=n)&&(a.borderColor||a.backgroundColor)||o&&Ko(o)))return;var a;const r=qo(t);s.forEach(r)}};function Zo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{configurable:!0,enumerable:!0,writable:!0,value:e})}}function Jo(t){t.data.datasets.forEach((t=>{Zo(t)}))}var Qo={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Jo(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:a,indexAxis:r}=e,l=t.getDatasetMeta(o),h=a||e.data;if("y"===Pi([r,t.options.indexAxis]))return;if(!l.controller.supportsDecimation)return;const c=t.scales[l.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let{start:d,count:u}=function(t,e){const i=e.length;let s,n=0;const{iScale:o}=t,{min:a,max:r,minDefined:l,maxDefined:h}=o.getUserBounds();return l&&(n=J(it(e,o.axis,a).lo,0,i-1)),s=h?J(it(e,o.axis,r).hi+1,n,i)-n:i-n,{start:n,count:s}}(l,h);if(u<=(i.threshold||4*n))return void Zo(e);let f;switch(s(a)&&(e._data=h,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":f=function(t,e,i,s,n){const o=n.samples||s;if(o>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(o-2);let l=0;const h=e+i-1;let c,d,u,f,g,p=e;for(a[l++]=t[p],c=0;cu&&(u=f,d=t[s],g=s);a[l++]=d,p=g}return a[l++]=t[h],a}(h,d,u,n,i);break;case"min-max":f=function(t,e,i,n){let o,a,r,l,h,c,d,u,f,g,p=0,m=0;const x=[],b=e+i-1,_=t[e].x,y=t[b].x-_;for(o=e;og&&(g=l,d=o),p=(m*p+a.x)/++m;else{const i=o-1;if(!s(c)&&!s(d)){const e=Math.min(c,d),s=Math.max(c,d);e!==u&&e!==i&&x.push({...t[e],x:p}),s!==u&&s!==i&&x.push({...t[s],x:p})}o>0&&i!==u&&x.push(t[i]),x.push(a),h=e,m=0,f=g=l,c=d=u=o}}return x}(h,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=f}))},destroy(t){Jo(t)}};function ta(t,e,i,s){if(s)return;let n=e[t],o=i[t];return"angle"===t&&(n=G(n),o=G(o)),{property:t,start:n,end:o}}function ea(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function ia(t,e,i,s){return t&&e?s(t[i],e[i]):t?t[i]:e?e[i]:0}function sa(t,e){let i=[],s=!1;return n(t)?(s=!0,i=t):i=function(t,e){const{x:i=null,y:s=null}=t||{},n=e.points,o=[];return e.segments.forEach((({start:t,end:e})=>{e=ea(t,e,n);const a=n[t],r=n[e];null!==s?(o.push({x:a.x,y:s}),o.push({x:r.x,y:s})):null!==i&&(o.push({x:i,y:a.y}),o.push({x:i,y:r.y}))})),o}(t,e),i.length?new no({points:i,options:{tension:0},_loop:s,_fullLoop:s}):null}function na(t){return t&&!1!==t.fill}function oa(t,e,i){let s=t[e].fill;const n=[e];let o;if(!i)return s;for(;!1!==s&&-1===n.indexOf(s);){if(!a(s))return s;if(o=t[s],!o)return!1;if(o.visible)return s;n.push(s),s=o.fill}return!1}function aa(t,e,i){const s=function(t){const e=t.options,i=e.fill;let s=l(i&&i.target,i);void 0===s&&(s=!!e.backgroundColor);if(!1===s||null===s)return!1;if(!0===s)return"origin";return s}(t);if(o(s))return!isNaN(s.value)&&s;let n=parseFloat(s);return a(n)&&Math.floor(n)===n?function(t,e,i,s){"-"!==t&&"+"!==t||(i=e+i);if(i===e||i<0||i>=s)return!1;return i}(s[0],e,n,i):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function ra(t,e,i){const s=[];for(let n=0;n=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(o,i.axis),s&&i.fill&&da(t.ctx,i,o))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const s=t.getSortedVisibleDatasetMetas();for(let e=s.length-1;e>=0;--e){const i=s[e].$filler;na(i)&&da(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const s=e.meta.$filler;na(s)&&"beforeDatasetDraw"===i.drawTime&&da(t.ctx,s,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const xa=(t,e)=>{let{boxHeight:i=e,boxWidth:s=e}=t;return t.usePointStyle&&(i=Math.min(i,e),s=t.pointStyleWidth||Math.min(s,e)),{boxWidth:s,boxHeight:i,itemHeight:Math.max(e,i)}};class ba extends Hs{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=d(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,s=Si(i.font),n=s.size,o=this._computeTitleHeight(),{boxWidth:a,itemHeight:r}=xa(i,n);let l,h;e.font=s.string,this.isHorizontal()?(l=this.maxWidth,h=this._fitRows(o,n,a,r)+10):(h=this.maxHeight,l=this._fitCols(o,s,a,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,s){const{ctx:n,maxWidth:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],h=s+a;let c=t;n.textAlign="left",n.textBaseline="middle";let d=-1,u=-h;return this.legendItems.forEach(((t,f)=>{const g=i+e/2+n.measureText(t.text).width;(0===f||l[l.length-1]+g+2*a>o)&&(c+=h,l[l.length-(f>0?0:1)]=0,u+=h,d++),r[f]={left:0,top:u,row:d,width:g,height:s},l[l.length-1]+=g+a})),c}_fitCols(t,e,i,s){const{ctx:n,maxHeight:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],h=o-t;let c=a,d=0,u=0,f=0,g=0;return this.legendItems.forEach(((t,o)=>{const{itemWidth:p,itemHeight:m}=function(t,e,i,s,n){const o=function(t,e,i,s){let n=t.text;n&&"string"!=typeof n&&(n=n.reduce(((t,e)=>t.length>e.length?t:e)));return e+i.size/2+s.measureText(n).width}(s,t,e,i),a=function(t,e,i){let s=t;"string"!=typeof e.text&&(s=_a(e,i));return s}(n,s,e.lineHeight);return{itemWidth:o,itemHeight:a}}(i,e,n,t,s);o>0&&u+m+2*a>h&&(c+=d+a,l.push({width:d,height:u}),f+=d+a,g++,d=u=0),r[o]={left:f,top:u,col:g,width:p,height:m},d=Math.max(d,p),u+=m+a})),c+=d,l.push({width:d,height:u}),c}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:s},rtl:n}}=this,o=Oi(n,this.left,this.width);if(this.isHorizontal()){let n=0,a=ft(i,this.left+s,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,a=ft(i,this.left+s,this.right-this.lineWidths[n])),r.top+=this.top+t+s,r.left=o.leftForLtr(o.x(a),r.width),a+=r.width+s}else{let n=0,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height)),r.top=a,r.left+=this.left+s,r.left=o.leftForLtr(o.x(r.left),r.width),a+=r.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Ie(t,this),this._draw(),ze(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:s}=this,{align:n,labels:o}=t,a=ue.color,r=Oi(t.rtl,this.left,this.width),h=Si(o.font),{padding:c}=o,d=h.size,u=d/2;let f;this.drawTitle(),s.textAlign=r.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=h.string;const{boxWidth:g,boxHeight:p,itemHeight:m}=xa(o,d),x=this.isHorizontal(),b=this._computeTitleHeight();f=x?{x:ft(n,this.left+c,this.right-i[0]),y:this.top+c+b,line:0}:{x:this.left+c,y:ft(n,this.top+b+c,this.bottom-e[0].height),line:0},Ai(this.ctx,t.textDirection);const _=m+c;this.legendItems.forEach(((y,v)=>{s.strokeStyle=y.fontColor,s.fillStyle=y.fontColor;const M=s.measureText(y.text).width,w=r.textAlign(y.textAlign||(y.textAlign=o.textAlign)),k=g+u+M;let S=f.x,P=f.y;r.setWidth(this.width),x?v>0&&S+k+c>this.right&&(P=f.y+=_,f.line++,S=f.x=ft(n,this.left+c,this.right-i[f.line])):v>0&&P+_>this.bottom&&(S=f.x=S+e[f.line].width+c,f.line++,P=f.y=ft(n,this.top+b+c,this.bottom-e[f.line].height));if(function(t,e,i){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;s.save();const n=l(i.lineWidth,1);if(s.fillStyle=l(i.fillStyle,a),s.lineCap=l(i.lineCap,"butt"),s.lineDashOffset=l(i.lineDashOffset,0),s.lineJoin=l(i.lineJoin,"miter"),s.lineWidth=n,s.strokeStyle=l(i.strokeStyle,a),s.setLineDash(l(i.lineDash,[])),o.usePointStyle){const a={radius:p*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},l=r.xPlus(t,g/2);Ee(s,a,l,e+u,o.pointStyleWidth&&g)}else{const o=e+Math.max((d-p)/2,0),a=r.leftForLtr(t,g),l=wi(i.borderRadius);s.beginPath(),Object.values(l).some((t=>0!==t))?He(s,{x:a,y:o,w:g,h:p,radius:l}):s.rect(a,o,g,p),s.fill(),0!==n&&s.stroke()}s.restore()}(r.x(S),P,y),S=gt(w,S+g+u,x?S+k:this.right,t.rtl),function(t,e,i){Ne(s,i.text,t,e+m/2,h,{strikethrough:i.hidden,textAlign:r.textAlign(i.textAlign)})}(r.x(S),P,y),x)f.x+=k+c;else if("string"!=typeof y.text){const t=h.lineHeight;f.y+=_a(y,t)+c}else f.y+=_})),Ti(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=Si(e.font),s=ki(e.padding);if(!e.display)return;const n=Oi(t.rtl,this.left,this.width),o=this.ctx,a=e.position,r=i.size/2,l=s.top+r;let h,c=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+l,c=ft(t.align,c,this.right-d);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);h=l+ft(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=ft(a,c,c+d);o.textAlign=n.textAlign(ut(a)),o.textBaseline="middle",o.strokeStyle=e.color,o.fillStyle=e.color,o.font=i.string,Ne(o,e.text,u,h,i)}_computeTitleHeight(){const t=this.options.title,e=Si(t.font),i=ki(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,s,n;if(tt(t,this.left,this.right)&&tt(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;it.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const l=t.controller.getStyle(i?0:void 0),h=ki(l.borderWidth);return{text:e[t.index].label,fillStyle:l.backgroundColor,fontColor:o,hidden:!t.visible,lineCap:l.borderCapStyle,lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:l.borderColor,pointStyle:s||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:a&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class va extends Hs{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const s=n(i.text)?i.text.length:1;this._padding=ki(i.padding);const o=s*Si(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ft(a,i,n),h=e+t,r=n-i):("left"===o.position?(l=i+t,h=ft(a,s,e),c=-.5*C):(l=n-t,h=ft(a,e,s),c=.5*C),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=Si(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ne(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:ut(e.align),textBaseline:"middle",translation:[n,o]})}}var Ma={id:"title",_element:va,start(t,e,i){!function(t,e){const i=new va({ctx:t.ctx,options:e,chart:t});as.configure(t,i,e),as.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;as.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;as.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const wa=new WeakMap;var ka={id:"subtitle",start(t,e,i){const s=new va({ctx:t.ctx,options:i,chart:t});as.configure(t,s,i),as.addBox(t,s),wa.set(t,s)},stop(t){as.removeBox(t,wa.get(t)),wa.delete(t)},beforeUpdate(t,e,i){const s=wa.get(t);as.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Sa={average(t){if(!t.length)return!1;let e,i,s=new Set,n=0,o=0;for(e=0,i=t.length;et+e))/s.size,y:n/o}},nearest(t,e){if(!t.length)return!1;let i,s,n,o=e.x,a=e.y,r=Number.POSITIVE_INFINITY;for(i=0,s=t.length;i-1?t.split("\n"):t}function Ca(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Oa(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=Si(e.bodyFont),h=Si(e.titleFont),c=Si(e.footerFont),d=o.length,f=n.length,g=s.length,p=ki(e.padding);let m=p.height,x=0,b=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(b+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),b){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(b-g)*l.lineHeight+(b-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){x=Math.max(x,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),x+=p.width,{width:x,height:m}}function Aa(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function Ta(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return it.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||Aa(t,e,i,s),yAlign:s}}function La(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:J(g,0,s.width-e.width),y:J(p,0,s.height-e.height)}}function Ea(t,e,i){const s=ki(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function Ra(t){return Pa([],Da(t))}function Ia(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const za={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex{const e={before:[],lines:[],after:[]},n=Ia(i,t);Pa(e.before,Da(Fa(n,"beforeLabel",this,t))),Pa(e.lines,Fa(n,"label",this,t)),Pa(e.after,Da(Fa(n,"afterLabel",this,t))),s.push(e)})),s}getAfterBody(t,e){return Ra(Fa(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Fa(i,"beforeFooter",this,t),n=Fa(i,"footer",this,t),o=Fa(i,"afterFooter",this,t);let a=[];return a=Pa(a,Da(s)),a=Pa(a,Da(n)),a=Pa(a,Da(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;at.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),u(l,(e=>{const i=Ia(t.callbacks,e);s.push(Fa(i,"labelColor",this,e)),n.push(Fa(i,"labelPointStyle",this,e)),o.push(Fa(i,"labelTextColor",this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=Sa[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=Oa(this,i),a=Object.assign({},t,e),r=Ta(this.chart,i,a),l=La(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=wi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,x,b,_,y;return"center"===n?(_=u+g/2,"left"===s?(p=d,m=p-o,b=_+o,y=_-o):(p=d+f,m=p+o,b=_-o,y=_+o),x=p):(m="left"===s?d+Math.max(r,h)+o:"right"===s?d+f-Math.max(l,c)-o:this.caretX,"top"===n?(b=u,_=b-o,p=m-o,x=m+o):(b=u+g,_=b+o,p=m+o,x=m-o),y=b),{x1:p,x2:m,x3:x,y1:b,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=Oi(i.rtl,this.x,this.width);for(t.x=Ea(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",o=Si(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,He(t,{x:e,y:g,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),He(t,{x:i,y:g+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,g,h,l),t.strokeRect(e,g,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=Si(i.bodyFont);let d=c.lineHeight,f=0;const g=Oi(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+d/2),t.y+=d+n},m=g.textAlign(o);let x,b,_,y,v,M,w;for(e.textAlign=o,e.textBaseline="middle",e.font=c.string,t.x=Ea(this,m,i),e.fillStyle=i.bodyColor,u(this.beforeBody,p),f=a&&"right"!==m?"center"===o?l/2+h:l+2+h:0,y=0,M=s.length;y0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=Sa[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=Oa(this,t),a=Object.assign({},i,this._size),r=Ta(e,t,a),l=La(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=ki(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),Ai(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Ti(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!f(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!f(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if("mouseout"===t.type)return[];if(!s)return e.filter((t=>this.chart.data.datasets[t.datasetIndex]&&void 0!==this.chart.getDatasetMeta(t.datasetIndex).controller.getParsed(t.index)));const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=Sa[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}var Ba={id:"tooltip",_element:Va,positioners:Sa,afterInit(t,e,i){i&&(t.tooltip=new Va({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:za},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};return An.register(Yn,jo,fo,t),An.helpers={...Wi},An._adapters=Rn,An.Animation=Cs,An.Animations=Os,An.animator=bt,An.controllers=en.controllers.items,An.DatasetController=Ns,An.Element=Hs,An.elements=fo,An.Interaction=Xi,An.layouts=as,An.platforms=Ss,An.Scale=Js,An.Ticks=ae,Object.assign(An,Yn,jo,fo,t,Ss),An.Chart=An,"undefined"!=typeof window&&(window.Chart=An),An})); +//# sourceMappingURL=chart.umd.js.map diff --git a/internal/view/static/static_fs.go b/internal/view/static/static_fs.go index f67df6cc..fe6e4c52 100644 --- a/internal/view/static/static_fs.go +++ b/internal/view/static/static_fs.go @@ -1,6 +1,6 @@ -package static - -import "embed" - -//go:embed * -var StaticFs embed.FS +package static + +import "embed" + +//go:embed * +var StaticFs embed.FS diff --git a/internal/view/web/alpine/alpine.go b/internal/view/web/alpine/alpine.go index 58070401..714a1471 100644 --- a/internal/view/web/alpine/alpine.go +++ b/internal/view/web/alpine/alpine.go @@ -1,120 +1,120 @@ -package alpine - -import "github.com/maragudk/gomponents" - -// Template is a generic function for rendering an