diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..7f36c22204 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.envrc +/.bundle +/coverage +/node_modules +/.idea +spec/examples.txt +package.json +yarn.lock diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..2de79620b9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.rb] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.env.default b/.env.default index 2c0cfce563..c88abab0fe 100644 --- a/.env.default +++ b/.env.default @@ -1,13 +1,11 @@ ERRBIT_HOST=errbit.example.com -ERRBIT_PROTOCOL=http -ERRBIT_ENFORCE_SSL=false CONFIRM_RESOLVE_ERR=true ERRBIT_CONFIRM_ERR_ACTIONS=true ERRBIT_USER_HAS_USERNAME=false ERRBIT_USE_GRAVATAR=true ERRBIT_GRAVATAR_DEFAULT=identicon +ERRBIT_DISABLE_FORCE_SSL=false ALLOW_COMMENTS_WITH_ISSUE_TRACKER=true -SERVE_STATIC_ASSETS=true SECRET_KEY_BASE=f258ed69266dc8ad0ca79363c3d2f945c388a9c5920fc9a1ae99a98fbb619f135001c6434849b625884a9405a60cd3d50fc3e3b07ecd38cbed7406a4fccdb59c ERRBIT_EMAIL_FROM='errbit@example.com' ERRBIT_EMAIL_AT_NOTICES='[1,10,100]' @@ -15,7 +13,6 @@ ERRBIT_PER_APP_EMAIL_AT_NOTICES=false ERRBIT_NOTIFY_AT_NOTICES='[0]' ERRBIT_PER_APP_NOTIFY_AT_NOTICES=false MONGO_URL='mongodb://localhost' -ERRBIT_LOG_LEVEL=info ERRBIT_LOG_LOCATION=STDOUT GITHUB_URL=https://github.com GITHUB_AUTHENTICATION=true diff --git a/.envrc b/.envrc new file mode 100644 index 0000000000..1854e34efd --- /dev/null +++ b/.envrc @@ -0,0 +1,4 @@ +PATH_add bin + +export SE_AVOID_STATS="true" +export CI="true" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..a2e814f675 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @biow0lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..b1d5f38e36 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,54 @@ +version: 2 +updates: + - package-ecosystem: "bundler" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 7 + open-pull-requests-limit: 99 + allow: + - dependency-type: "direct" + - dependency-type: "indirect" + assignees: + - "biow0lf" + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 7 + open-pull-requests-limit: 99 + assignees: + - "biow0lf" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 7 + open-pull-requests-limit: 99 + assignees: + - "biow0lf" + + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 7 + open-pull-requests-limit: 99 + assignees: + - "biow0lf" + + - package-ecosystem: "docker-compose" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 7 + open-pull-requests-limit: 99 + assignees: + - "biow0lf" diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml new file mode 100644 index 0000000000..79abab4a1e --- /dev/null +++ b/.github/workflows/actionlint.yml @@ -0,0 +1,61 @@ +name: Action Lint + +on: + push: + branches: + - main + - 0-11-stable + pull_request: + branches: + - main + - 0-11-stable + workflow_dispatch: {} + schedule: + - cron: "0 21 * * 6" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + actionlint: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + disable-sudo-and-containers: true + egress-policy: block + allowed-endpoints: > + api.github.com:443 + github.com:443 + registry.npmjs.org:443 + release-assets.githubusercontent.com:443 + raw.githubusercontent.com:443 + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 + id: actionlint + with: + pyflakes: false + + - name: actionlint Summary + if: ${{ steps.actionlint.outputs.exit-code != 0 }} + run: | + echo "Used actionlint version ${{ steps.actionlint.outputs.version-semver }}" + echo "Used actionlint release ${{ steps.actionlint.outputs.version-tag }}" + echo "actionlint ended with ${{ steps.actionlint.outputs.exit-code }} exit code" + echo "actionlint ended because '${{ steps.actionlint.outputs.exit-message }}'" + echo "actionlint found ${{ steps.actionlint.outputs.total-errors }} errors" + echo "actionlint checked ${{ steps.actionlint.outputs.total-files }} files" + echo "actionlint cache used: ${{ steps.actionlint.outputs.cache-hit }}" + # shellcheck disable=SC2242 + exit ${{ steps.actionlint.outputs.exit-code }} diff --git a/.github/workflows/brakeman.yml b/.github/workflows/brakeman.yml new file mode 100644 index 0000000000..96c8537a68 --- /dev/null +++ b/.github/workflows/brakeman.yml @@ -0,0 +1,54 @@ +name: Brakeman + +on: + push: + branches: + - main + - 0-10-stable + - 0-11-stable + pull_request: + branches: + - main + - 0-10-stable + - 0-11-stable + workflow_dispatch: {} + schedule: + - cron: "0 21 * * 6" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + brakeman: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - name: Harden Runner + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + disable-sudo-and-containers: true + egress-policy: block + allowed-endpoints: > + github.com:443 + hosted-compute-watchdog-prod-iad-02.githubapp.com:443 + index.rubygems.org:443 + release-assets.githubusercontent.com:443 + rubygems.org:443 + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Ruby + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 + with: + rubygems: "4.0.22" + bundler: "4.0.22" + bundler-cache: true + + - run: bin/brakeman --format github --quiet --run-all-checks || true diff --git a/.github/workflows/bundler-audit.yml b/.github/workflows/bundler-audit.yml new file mode 100644 index 0000000000..365b5e3cff --- /dev/null +++ b/.github/workflows/bundler-audit.yml @@ -0,0 +1,59 @@ +name: Bundler audit + +on: + push: + branches: + - main + - 0-10-stable + - 0-11-stable + pull_request: + branches: + - main + - 0-10-stable + - 0-11-stable + workflow_dispatch: {} + schedule: + - cron: "0 21 * * 6" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + bundler-audit: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - name: Harden Runner + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + disable-sudo-and-containers: true + egress-policy: block + allowed-endpoints: > + github.com:443 + hosted-compute-watchdog-prod-eus-01.githubapp.com:443 + hosted-compute-watchdog-prod-eus-02.githubapp.com:443 + hosted-compute-watchdog-prod-iad-01.githubapp.com:443 + hosted-compute-watchdog-prod-iad-02.githubapp.com:443 + index.rubygems.org:443 + objects.githubusercontent.com:443 + release-assets.githubusercontent.com:443 + rubygems.org:443 + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Ruby + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 + with: + rubygems: "4.0.22" + bundler: "4.0.22" + bundler-cache: true + + - run: bin/bundle-audit update + - run: bin/bundle-audit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000000..91e1d9bdf2 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,42 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, +# PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +name: "Dependency Review" +on: [pull_request] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + dependency-review: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + disable-sudo-and-containers: true + egress-policy: block + allowed-endpoints: > + api.deps.dev:443 + api.github.com:443 + api.securityscorecards.dev:443 + github.com:443 + + - name: "Checkout Repository" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: "Dependency Review" + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 diff --git a/.github/workflows/herb.yml b/.github/workflows/herb.yml new file mode 100644 index 0000000000..fd55f022a5 --- /dev/null +++ b/.github/workflows/herb.yml @@ -0,0 +1,52 @@ +name: Herb + +on: + push: + branches: + - main + - 0-11-stable + pull_request: + branches: + - main + - 0-11-stable + workflow_dispatch: {} + schedule: + - cron: "0 21 * * 6" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + herb: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - name: Harden Runner + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + disable-sudo-and-containers: true + egress-policy: block + allowed-endpoints: > + github.com:443 + index.rubygems.org:443 + objects.githubusercontent.com:443 + release-assets.githubusercontent.com:443 + rubygems.org:443 + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Ruby + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 + with: + rubygems: "4.0.22" + bundler: "4.0.22" + bundler-cache: true + + - run: bin/herb analyze diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml new file mode 100644 index 0000000000..ce22c5f9ec --- /dev/null +++ b/.github/workflows/prettier.yml @@ -0,0 +1,58 @@ +name: Prettier + +on: + push: + branches: + - main + - 0-11-stable + pull_request: + branches: + - main + - 0-11-stable + workflow_dispatch: {} + schedule: + - cron: "0 21 * * 6" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + prettier: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - name: Harden Runner + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + disable-sudo-and-containers: true + egress-policy: block + allowed-endpoints: > + api.github.com:443 + github.com:443 + hosted-compute-watchdog-prod-iad-01.githubapp.com:443 + hosted-compute-watchdog-prod-iad-02.githubapp.com:443 + nodejs.org:443 + objects.githubusercontent.com:443 + registry.yarnpkg.com:443 + release-assets.githubusercontent.com:443 + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + check-latest: true + + - name: yarn install + run: yarn install --frozen-lockfile + + - name: run prettier + run: yarn prettier --check "*.{yml,yaml}" ".github/*.{yml,yaml}" ".github/**/*.{yml,yaml}" "config/**/*.{yml,yaml}" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000..92f28588ca --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,93 @@ +name: build-and-publish + +on: + push: + branches: + - main + tags: + - "*" + workflow_dispatch: {} + schedule: + - cron: "0 21 * * 6" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build-and-publish: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + disable-sudo: true + egress-policy: block + allowed-endpoints: > + api.github.com:443 + auth.docker.io:443 + check.trivy.dev:443 + deb.debian.org:80 + dl-cdn.alpinelinux.org:443 + docker-images-prod.6aa30f8b08e16409b46e0173d6de2f56.r2.cloudflarestorage.com:443 + github.com:443 + hosted-compute-watchdog-prod-eus-02.githubapp.com:443 + hosted-compute-watchdog-prod-iad-01.githubapp.com:443 + hosted-compute-watchdog-prod-iad-02.githubapp.com:443 + index.docker.io:443 + index.rubygems.org:443 + mirror.gcr.io:443 + production.cloudflare.docker.com:443 + registry-1.docker.io:443 + registry.docker.com:443 + release-assets.githubusercontent.com:443 + rubygems.org:443 + production.cloudfront.docker.com:443 + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Login to DockerHub + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ${{ github.event.repository.owner.name }}/${{ github.event.repository.name }} + tags: | + type=raw,value=latest + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + - name: Build and push + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: . + pull: true + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + continue-on-error: true + with: + image-ref: "docker.io/errbit/errbit:latest" + format: "table" + exit-code: "1" + ignore-unfixed: true + vuln-type: "os,library" + severity: "MEDIUM,HIGH,CRITICAL" + scanners: "vuln" diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml new file mode 100644 index 0000000000..2b08e4e7f0 --- /dev/null +++ b/.github/workflows/rspec.yml @@ -0,0 +1,66 @@ +name: RSpec + +on: + push: + branches: + - main + - 0-11-stable + pull_request: + branches: + - main + - 0-11-stable + workflow_dispatch: {} + schedule: + - cron: "0 21 * * 6" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + CI: "true" + SE_AVOID_STATS: "true" + +jobs: + rspec: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + ruby: ["4.0.7"] + mongodb-version: ["7.0", "8.0", "8.2", "8.3", "9.0"] + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + disable-sudo: true + egress-policy: audit + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Ruby + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: "4.0.22" + bundler: "4.0.22" + bundler-cache: true + + - name: Start MongoDB + uses: supercharge/mongodb-github-action@315db7fe45ac2880b7758f1933e6e5d59afd5e94 # 1.12.1 + with: + mongodb-version: ${{ matrix.mongodb-version }} + + - run: bundle exec bootsnap precompile --gemfile + - run: bundle exec bootsnap precompile app/ lib/ config/ Rakefile + - run: bundle exec rails zeitwerk:check + - run: bundle exec rails assets:precompile + - run: bundle exec rails errbit:bootstrap + - run: bundle exec rspec diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml new file mode 100644 index 0000000000..c6bf2638dd --- /dev/null +++ b/.github/workflows/rubocop.yml @@ -0,0 +1,62 @@ +name: Rubocop + +on: + push: + branches: + - main + - 0-11-stable + pull_request: + branches: + - main + - 0-11-stable + workflow_dispatch: {} + schedule: + - cron: "0 21 * * 6" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + rubocop: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - name: Harden Runner + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + disable-sudo-and-containers: true + egress-policy: block + allowed-endpoints: > + github.com:443 + index.rubygems.org:443 + objects.githubusercontent.com:443 + release-assets.githubusercontent.com:443 + rubygems.org:443 + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Ruby + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 + with: + rubygems: "4.0.22" + bundler: "4.0.22" + bundler-cache: true + + - run: | + bin/rubocop \ + app/controllers/users_controller.rb \ + app/controllers/unlink_githubs_controller.rb \ + app/controllers/unlink_googles_controller.rb \ + app/interactors/user_destroy.rb \ + spec/factories/app_factory.rb \ + spec/factories/user_factory.rb \ + spec/requests/users_controller_spec.rb \ + spec/requests/unlink_githubs_controller_spec.rb \ + spec/requests/unlink_googles_controller_spec.rb diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml new file mode 100644 index 0000000000..8b27d48930 --- /dev/null +++ b/.github/workflows/scorecards.yml @@ -0,0 +1,102 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: "20 7 * * 2" + push: + branches: ["main"] + +# Declare default permissions as read only. +permissions: read-all + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-24.04 + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + contents: read + actions: read + # To allow GraphQL ListCommits to work + issues: read + pull-requests: read + # To detect SAST tools + checks: read + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + disable-sudo: true + egress-policy: block + allowed-endpoints: > + api.deps.dev:443 + api.github.com:443 + api.osv.dev:443 + api.scorecard.dev:443 + auth.docker.io:443 + fulcio.sigstore.dev:443 + github.com:443 + hosted-compute-watchdog-prod-eus-01.githubapp.com:443 + hosted-compute-watchdog-prod-iad-02.githubapp.com:443 + index.docker.io:443 + oauth2.sigstore.dev:443 + oss-fuzz-build-logs.storage.googleapis.com:443 + rekor.sigstore.dev:443 + tuf-repo-cdn.sigstore.dev:443 + www.bestpractices.dev:443 + + - name: "Checkout code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecards on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + with: + sarif_file: results.sarif diff --git a/.github/workflows/standard.yml b/.github/workflows/standard.yml new file mode 100644 index 0000000000..6f44b6e4e1 --- /dev/null +++ b/.github/workflows/standard.yml @@ -0,0 +1,59 @@ +name: Standard + +on: + push: + branches: + - main + - 0-11-stable + pull_request: + branches: + - main + - 0-11-stable + workflow_dispatch: {} + schedule: + - cron: "0 21 * * 6" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + standard: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - name: Harden Runner + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + disable-sudo-and-containers: true + egress-policy: block + allowed-endpoints: > + github.com:443 + index.rubygems.org:443 + objects.githubusercontent.com:443 + release-assets.githubusercontent.com:443 + rubygems.org:443 + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Ruby + uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0 + with: + rubygems: "4.0.22" + bundler: "4.0.22" + bundler-cache: true + + - run: | + bin/standardrb \ + app/controllers/users_controller.rb \ + app/controllers/unlink_githubs_controller.rb \ + app/controllers/unlink_googles_controller.rb \ + app/interactors \ + app/helpers \ + spec diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000000..ce2f4292c6 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,40 @@ +name: GitHub Actions Security Analysis with zizmor 🌈 + +on: + push: + branches: + - "main" + pull_request: + branches: + - "**" + workflow_dispatch: {} + schedule: + - cron: "0 21 * * 6" + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + zizmor: + runs-on: ubuntu-24.04 + permissions: + security-events: write + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run zizmor 🌈 + uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 + with: + persona: "auditor" diff --git a/.gitignore b/.gitignore index 1327d0ce25..60f41b34cb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,16 +8,15 @@ config/config.yml config/deploy.rb config/deploy config/mongoid.yml -config/newrelic.yml .rvmrc .idea *~ *.rbc .DS_Store *.rbx -bundle coverage +spec/examples.txt *# -.ruby-version -.tool-versions NOTES +*.swp +/node_modules diff --git a/.rspec b/.rspec index 2f0d715186..c99d2e7396 100644 --- a/.rspec +++ b/.rspec @@ -1,3 +1 @@ ---color --require spec_helper ---order random diff --git a/.rubocop.yml b/.rubocop.yml index 2a15d35c1f..2b37dc2206 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,50 +1,471 @@ -inherit_from: .rubocop_todo.yml +plugins: + - rubocop-capybara + - rubocop-rails + - rubocop-rake + - rubocop-rspec + - rubocop-rspec_rails + - rubocop-performance + - rubocop-thread_safety + - rubocop-factory_bot + - rubocop-erb + - rubocop-disable_syntax -# Caveat: .find_by raises exception in mongoid, unlike ActiveRecord which -# returns nil -Rails/FindBy: +AllCops: + TargetRubyVersion: 4.0 + NewCops: enable + UseProjectIndex: true + ProjectIndexIncludesGems: true + +# We use standard as a linter and formatter instead of Rubocop. +# Also, we are explicitly disable all rubocop rules what +# already enabled in `standard`. And standard-performance. + +# Standard rules. Style: + +# Enforced by `standard`. Disable. +Style/StringLiterals: Enabled: false -Layout/EmptyLineBetweenDefs: - AllowAdjacentOneLineDefs: true +# Enforced by `standard`. Disable. +Style/HashSyntax: + Enabled: false + +# Enforced by `standard`. Disable. +Style/NestedParenthesizedCalls: + Enabled: false + +# Enforced by `standard`. Disable. +Style/RedundantRegexpArgument: + Enabled: false + +# Enforced by `standard`. Disable. +Style/PercentLiteralDelimiters: + Enabled: false + +# Enforced by `standard`. Disable. +Style/RedundantBegin: + Enabled: false + +# Enforced by `standard`. Disable. +Style/SuperWithArgsParentheses: + Enabled: false + +# Enforced by `standard`. Disable. +Style/Encoding: + Enabled: false + +# Enforced by `standard`. Disable. +Style/NumericLiteralPrefix: + Enabled: false + +# Enforced by `standard`. Disable. +Style/RedundantParentheses: + Enabled: false + +# Enforced by `standard`. Disable. +Style/EmptyMethod: + Enabled: false + +# Enforced by `standard`. Disable. +Style/SingleLineMethods: + Enabled: false +# Enforced by `standard`. Disable. +Style/SafeNavigation: + Enabled: false + +# Enforced by `standard`. Disable. +Style/RescueStandardError: + Enabled: false + +# Enforced by `standard`. Disable. +Style/RedundantSelf: + Enabled: false + +# Enforced by `standard`. Disable. +Style/TernaryParentheses: + Enabled: false + +# Enforced by `standard`. Disable. +Style/RedundantLineContinuation: + Enabled: false + +# Enforced by `standard`. Disable. +Style/SlicingWithRange: + Enabled: false + +# Enforced by `standard`. Disable. +Style/MultilineIfModifier: + Enabled: false + +# Enforced by `standard`. Disable. +Style/RedundantCondition: + Enabled: false + +# Enforced by `standard`. Disable. +Style/RedundantInterpolation: + Enabled: false + +# Enforced by `standard`. Disable. +Style/OrAssignment: + Enabled: false + +# Enforced by `standard`. Disable. +Style/ConditionalAssignment: + Enabled: false + +# Enforced by `standard`. Disable. +Style/ItAssignment: + Enabled: false + +# Enforced by `standard`. Disable. +Style/EachWithObject: + Enabled: false + +# Enforced by `standard`. Disable. +Style/GlobalStdStream: + Enabled: false + +# Enforced by `standard`. Disable. +Style/StringLiteralsInInterpolation: + Enabled: false + +# Disabled as in `standard`. +Style/HashAsLastArrayItem: + Enabled: false + +# Enforced by `standard`. Disable. +Style/Alias: + Enabled: false + +# Disabled as in `standard`. +Style/IfUnlessModifier: + Enabled: false + +# Disabled as in `standard`. +Style/NegatedIf: + Enabled: false + +# Standard rules. Layout: + +# Enforced by `standard`. Disable. +Layout/HashAlignment: + Enabled: false + +# Enforced by `standard`. Disable. +Layout/FirstArrayElementIndentation: + Enabled: false + +# Enforced by `standard`. Disable. +Layout/SpaceInsideHashLiteralBraces: + Enabled: false + +# Enforced by `standard`. Disable. +Layout/SpaceInsideStringInterpolation: + Enabled: false + +# Enforced by `standard`. Disable. +Layout/DotPosition: + Enabled: false + +# Enforced by `standard`. Disable. Layout/ExtraSpacing: - AllowForAlignment: true + Enabled: false -Layout/IndentationConsistency: - # `rails` style: - # prescribes that in classes and modules the `protected` and `private` - # modifier keywords shall be indented the same as public methods and that - # protected and private members shall be indented one step more than the - # modifiers. - EnforcedStyle: rails +# Enforced by `standard`. Disable. +Layout/ArgumentAlignment: + Enabled: false -Style/GuardClause: - MinBodyLength: 4 +# Enforced by `standard`. Disable. +Layout/MultilineMethodCallBraceLayout: + Enabled: false +# Enforced by `standard`. Disable. Layout/AccessModifierIndentation: - EnforcedStyle: outdent + Enabled: false -Layout/AlignParameters: - EnforcedStyle: with_fixed_indentation +# Enforced by `standard`. Disable. +Layout/FirstHashElementIndentation: + Enabled: false -Layout/DotPosition: - # Support code copy/paste into console - EnforcedStyle: trailing +# Enforced by `standard`. Disable. +Layout/IndentationWidth: + Enabled: false + +# Enforced by `standard`. Disable. +Layout/ElseAlignment: + Enabled: false + +# Enforced by `standard`. Disable. +Layout/EndAlignment: + Enabled: false + +# Enforced by `standard`. Disable. +Layout/MultilineHashBraceLayout: + Enabled: false -Layout/MultilineOperationIndentation: - EnforcedStyle: indented +# Enforced by `standard`. Disable. +Layout/EmptyLineBetweenDefs: + Enabled: false -Layout/IndentHash: - EnforcedStyle: consistent +# Enforced by `standard`. Disable. +Layout/MultilineArrayBraceLayout: + Enabled: false -Layout/AlignHash: - EnforcedHashRocketStyle: table - EnforcedColonStyle: table +# Enforced by `standard`. Disable. +Layout/EmptyLineAfterMagicComment: + Enabled: false +# Enforced by `standard`. Disable. Layout/SpaceAroundOperators: - AllowForAlignment: true + Enabled: false + +# Enforced by `standard`. Disable. +Layout/ArrayAlignment: + Enabled: false + +# Enforced by `standard`. Disable. +Layout/AssignmentIndentation: + Enabled: false + +# Enforced by `standard`. Disable. +Layout/ClosingParenthesisIndentation: + Enabled: false +# Enforced by `standard`. Disable. +Layout/LineLength: + Enabled: false + +# Enforced by `standard`. Disable. +Layout/MultilineMethodCallIndentation: + Enabled: false + +# Enforced by `standard`. Disable. +Layout/CaseIndentation: + Enabled: false + +# Standard rules. Lint: + +# Enforced by `standard`. Disable. +Lint/ImplicitStringConcatenation: + Enabled: false + +# Enforced by `standard`. Disable. +Lint/TripleQuotes: + Enabled: false + +# Enforced by `standard`. Disable. +Lint/IneffectiveAccessModifier: + Enabled: false + +# Enforced by `standard`. Disable. +Lint/SymbolConversion: + Enabled: false + +# Enforced by `rubocop` and `standard`. +Lint/CopDirectiveSyntax: + Enabled: true + +# Enforced by `standard`. Disable. +Lint/DuplicateMethods: + Enabled: false + +# Enforced by `standard`. Disable. +Lint/ConstantDefinitionInBlock: + Enabled: false + +# Enforced by `standard`. Disable. +Lint/UselessTimes: + Enabled: false + +# We are using empty blocks in specs as placeholders for future implementation. +Lint/EmptyBlock: + Enabled: false + +# Standard-performance rules. + +# Enforced by `standard-performance`. Disable. +Performance/Detect: + Enabled: false + +# Enforced by `standard-performance`. Disable. +Performance/StringIdentifierArgument: + Enabled: false + +# Enforced by `standard-performance`. Disable. +Performance/RegexpMatch: + Enabled: false + +# Always enable `rubocop` Security: + +# Enforced by `rubocop` and `standard`. +Security/JSONLoad: + Enabled: true + +# Our `rubocop` rules + +# Bundler rules. + +Bundler/OrderedGems: + Enabled: false + +# Gemspec rules + +Gemspec/OrderedDependencies: + Enabled: false + +# Style rules + +# Don't allow %i[foo bar baz] +Style/SymbolArray: + Enabled: true + EnforcedStyle: brackets + +# Don't allow %w[foo bar baz] +Style/WordArray: + Enabled: true + EnforcedStyle: brackets + +# Disable warnings like "Missing top-level documentation comment for" +Style/Documentation: + Enabled: false + +# Disable as in `standard`. +Style/ArgumentsForwarding: + Enabled: false + +# RSpec rules + +# Prefer eq over be. +RSpec/BeEq: + Enabled: false + +# Prefer eq over eql. +RSpec/BeEql: + Enabled: false + +# We prefer to use `expect` over `allow`. +RSpec/StubbedMock: + Enabled: false + +# We prefer multiple before blocks in tests. +RSpec/ScatteredSetup: + Enabled: false + +# We use `expect` in before hooks. +RSpec/ExpectInHook: + Enabled: false + +# We use item_1, item_2, etc. Disable. +RSpec/IndexedLet: + Enabled: false + +# We don't use named subject's +RSpec/NamedSubject: + Enabled: false + +# Ignore `system` and `requests` specs. +RSpec/ExampleLength: + Enabled: true + Exclude: + - "spec/system/**/*.rb" + - "spec/requests/**/*.rb" + +# Ignore multiple expectations in all specs. +RSpec/MultipleExpectations: + Enabled: false + +# Extend maximum example nesting from 3 to 5. +RSpec/NestedGroups: + Enabled: true + Max: 5 + +# Just ignore +RSpec/ScatteredLet: + Enabled: false + +# Enforce receive style +RSpec/MessageSpies: + Enabled: true + EnforcedStyle: receive + +# We prefer `type: :system, retry: 3` instead of sorted `retry: 3, type: :system` +RSpec/SortMetadata: + Enabled: false + +# We prefer `let() { } and then subject {}` +RSpec/LeadingSubject: + Enabled: false + +RSpec/LeakyConstantDeclaration: + Enabled: true + +RSpec/LetSetup: + Enabled: false + +RSpec/LeakyLocalVariable: + Enabled: true + +RSpec/ReceiveMessages: + Enabled: false + +RSpec/EmptyExampleGroup: + Enabled: false + +# Naming rules: + +# Disable anonymous block forwarding. +Naming/BlockForwarding: + Enabled: true + EnforcedStyle: explicit + +# Enable and exclude specific files. Naming/FileName: + Enabled: true + Exclude: + - "spec/support/mongoid-rspec.rb" + - "spec/support/rspec-retry.rb" + - "spec/support/selenium-webdriver.rb" + - "spec/support/rspec-rebound.rb" + +# name_1, name_2, etc instead of name1, name2 +Naming/VariableNumber: + Enabled: true + EnforcedStyle: snake_case + +# Disabled syntax: + +# Disable shorthand hash syntax like: ({ x:, y: }) +# Disable % style literals +Style/DisableSyntax: + DisableSyntax: + - shorthand_hash_syntax + - percent_literals + +# RSpec Rails rules: + +# This is a new RSpec default. Disable. +RSpecRails/InferredSpecType: + Enabled: false + +RSpecRails/HttpStatusNameConsistency: + Enabled: true + +# Rails rules: + +# We don't use ActiveRecord. +Rails/ActiveRecordAliases: + Enabled: false + +# We don't use ActiveRecord. +Rails/SkipsModelValidations: + Enabled: false + +Capybara/AmbiguousClick: + Enabled: true + +Metrics/BlockLength: + Enabled: true Exclude: - - 'config/initializers/cve-*.rb' + - "config/routes.rb" + - "config/initializers/devise.rb" diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml deleted file mode 100644 index 19fcb53392..0000000000 --- a/.rubocop_todo.yml +++ /dev/null @@ -1,531 +0,0 @@ -# This configuration was generated by -# `rubocop --auto-gen-config` -# on 2018-07-19 13:40:39 +0200 using RuboCop version 0.58.1. -# The point is for the user to remove these configuration records -# one by one as the offenses are removed from the code base. -# Note that changes in the inspected code, or installation of new -# versions of RuboCop, may require this file to be generated again. - -# Offense count: 13 -# Cop supports --auto-correct. -# Configuration parameters: Include, TreatCommentsAsGroupSeparators. -# Include: **/*.gemfile, **/Gemfile, **/gems.rb -Bundler/OrderedGems: - Exclude: - - 'Gemfile' - -# Offense count: 1 -# Cop supports --auto-correct. -Layout/ClosingParenthesisIndentation: - Exclude: - - 'app/models/user.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Layout/EmptyLineAfterMagicComment: - Exclude: - - 'app/helpers/notices_helper.rb' - - 'app/helpers/sort_helper.rb' - -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: IndentationWidth. -# SupportedStyles: special_inside_parentheses, consistent, align_brackets -Layout/IndentArray: - EnforcedStyle: consistent - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: IndentationWidth. -Layout/IndentAssignment: - Exclude: - - 'app/models/problem.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent -Layout/IndentHeredoc: - Exclude: - - 'app/models/notification_services/hipchat_service.rb' - -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: symmetrical, new_line, same_line -Layout/MultilineArrayBraceLayout: - Exclude: - - 'app/models/notification_service.rb' - - 'spec/controllers/api/v1/problems_controller_spec.rb' - - 'spec/models/issue_spec.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: symmetrical, new_line, same_line -Layout/MultilineHashBraceLayout: - Exclude: - - 'app/models/notification_service.rb' - - 'spec/models/problem_spec.rb' - -# Offense count: 22 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: symmetrical, new_line, same_line -Layout/MultilineMethodCallBraceLayout: - Exclude: - - 'app/controllers/site_config_controller.rb' - - 'app/models/backtrace.rb' - - 'app/models/site_config.rb' - - 'app/models/user.rb' - - 'lib/hoptoad/v2.rb' - - 'spec/acceptance/watch_unwatch_app_spec.rb' - - 'spec/controllers/problems_controller_spec.rb' - - 'spec/decorators/backtrace_line_decorator_spec.rb' - - 'spec/initializers/devise_spec.rb' - - 'spec/interactors/user_destroy_spec.rb' - - 'spec/lib/airbrake_api/v3/notice_parser_spec.rb' - - 'spec/views/issue_trackers/issue.txt.erb_spec.rb' - - 'spec/views/problems/index.atom.builder_spec.rb' - - 'spec/views/problems/show.html.haml_spec.rb' - -# Offense count: 12 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: aligned, indented, indented_relative_to_receiver -Layout/MultilineMethodCallIndentation: - Exclude: - - 'app/controllers/apps_controller.rb' - - 'app/controllers/problems_controller.rb' - - 'app/helpers/application_helper.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: AllowForAlignment. -Layout/SpaceAroundOperators: - Exclude: - - 'app/helpers/hash_helper.rb' - -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, no_space -Layout/SpaceInsideStringInterpolation: - Exclude: - - 'app/views/problems/_list.atom.builder' - -# Offense count: 4 -Lint/AmbiguousBlockAssociation: - Exclude: - - 'spec/acceptance/app_regenerate_api_key_spec.rb' - - 'spec/controllers/apps_controller_spec.rb' - - 'spec/interactors/outdated_problem_clearer_spec.rb' - - 'spec/interactors/resolved_problem_clearer_spec.rb' - -# Offense count: 2 -Lint/DuplicateMethods: - Exclude: - - 'app/models/watcher.rb' - - 'lib/airbrake_api/v3/notice_parser.rb' - -# Offense count: 2 -Lint/ImplicitStringConcatenation: - Exclude: - - 'app/models/notification_services/gtalk_service.rb' - -# Offense count: 4 -Lint/IneffectiveAccessModifier: - Exclude: - - 'lib/hoptoad/v2.rb' - -# Offense count: 1 -# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. -Lint/UselessAccessModifier: - Exclude: - - 'lib/hoptoad/v2.rb' - -# Offense count: 35 -Metrics/AbcSize: - Max: 52 - -# Offense count: 118 -# Configuration parameters: CountComments, ExcludedMethods. -# ExcludedMethods: refine -Metrics/BlockLength: - Max: 418 - Exclude: - - 'spec/**/*.rb' - -# Offense count: 4 -# Configuration parameters: CountComments. -Metrics/ClassLength: - Max: 243 - -# Offense count: 6 -Metrics/CyclomaticComplexity: - Max: 11 - -# Offense count: 33 -# Configuration parameters: CountComments. -Metrics/MethodLength: - Max: 33 - -# Offense count: 5 -Metrics/PerceivedComplexity: - Max: 12 - -# Offense count: 3 -# Configuration parameters: EnforcedStyleForLeadingUnderscores. -# SupportedStylesForLeadingUnderscores: disallowed, required, optional -Naming/MemoizedInstanceVariableName: - Exclude: - - 'app/interactors/outdated_problem_clearer.rb' - - 'app/interactors/resolved_problem_clearer.rb' - - 'app/models/error_report.rb' - -# Offense count: 2 -# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. -# AllowedNames: io, id, to, by, on, in, at -Naming/UncommunicativeMethodParamName: - Exclude: - - 'app/models/notice.rb' - - 'app/models/notification_service.rb' - -# Offense count: 6 -# Configuration parameters: EnforcedStyle. -# SupportedStyles: snake_case, normalcase, non_integer -Naming/VariableNumber: - Exclude: - - 'spec/models/app_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Performance/RegexpMatch: - Exclude: - - 'app/decorators/backtrace_line_decorator.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Performance/Size: - Exclude: - - 'app/interactors/problem_destroy.rb' - -# Offense count: 4 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect. -Performance/TimesMap: - Exclude: - - 'spec/interactors/notice_refingerprinter_spec.rb' - - 'spec/interactors/problem_recacher_spec.rb' - - 'spec/mailers/mailer_spec.rb' - -# Offense count: 10 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect. -Security/JSONLoad: - Exclude: - - 'spec/controllers/api/v1/notices_controller_spec.rb' - - 'spec/controllers/api/v1/problems_controller_spec.rb' - -# Offense count: 15 -# Configuration parameters: EnforcedStyle. -# SupportedStyles: inline, group -Style/AccessModifierDeclarations: - Exclude: - - 'app/controllers/api/v1/stats_controller.rb' - - 'app/controllers/apps_controller.rb' - - 'app/controllers/comments_controller.rb' - - 'app/controllers/problems_controller.rb' - - 'app/controllers/site_config_controller.rb' - - 'app/controllers/users/omniauth_callbacks_controller.rb' - - 'app/decorators/issue_tracker_field_decorator.rb' - - 'app/helpers/apps_helper.rb' - - 'app/mailers/mailer.rb' - - 'app/models/backtrace.rb' - - 'app/models/notification_services/hipchat_service.rb' - - 'app/models/user.rb' - - 'lib/hoptoad.rb' - - 'lib/overrides/hoptoad_notifier/hoptoad_notifier.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: prefer_alias, prefer_alias_method -Style/Alias: - Exclude: - - 'app/decorators/issue_tracker_field_decorator.rb' - - 'app/models/app.rb' - -# Offense count: 15 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle. -# SupportedStyles: nested, compact -Style/ClassAndModuleChildren: - Exclude: - - 'app/controllers/api/v1/notices_controller.rb' - - 'app/controllers/api/v1/problems_controller.rb' - - 'app/controllers/api/v1/stats_controller.rb' - - 'app/controllers/api/v3/notices_controller.rb' - - 'app/controllers/users/omniauth_callbacks_controller.rb' - - 'app/models/notification_services/campfire_service.rb' - - 'app/models/notification_services/flowdock_service.rb' - - 'app/models/notification_services/gtalk_service.rb' - - 'app/models/notification_services/hipchat_service.rb' - - 'app/models/notification_services/hoiio_service.rb' - - 'app/models/notification_services/hubot_service.rb' - - 'app/models/notification_services/pushover_service.rb' - - 'app/models/notification_services/slack_service.rb' - - 'app/models/notification_services/webhook_service.rb' - - 'config/initializers/overrides.rb' - -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. -# SupportedStyles: assign_to_condition, assign_inside_condition -Style/ConditionalAssignment: - Exclude: - - 'app/models/notice_fingerprinter.rb' - - 'app/models/notification_service.rb' - - 'lib/recurse.rb' - -# Offense count: 72 -Style/Documentation: - Enabled: false - -# Offense count: 4 -# Cop supports --auto-correct. -Style/EachWithObject: - Exclude: - - 'app/decorators/issue_tracker_type_decorator.rb' - - 'app/helpers/application_helper.rb' - - 'app/models/notice.rb' - - 'lib/recurse.rb' - -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: compact, expanded -Style/EmptyMethod: - Exclude: - - 'db/migrate/20150527202629_add_v1_to_hipchat_notification_services.rb' - - 'db/migrate/20150926035420_cache_app_on_notice.rb' - - 'db/migrate/201510290041_extract_issue_tracker.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Style/Encoding: - Exclude: - - 'app/helpers/notices_helper.rb' - - 'app/helpers/sort_helper.rb' - -# Offense count: 7 -# Cop supports --auto-correct. -Style/ExpandPathArguments: - Exclude: - - 'Rakefile' - - 'bin/rails' - - 'config/application.rb' - - 'config/boot.rb' - - 'config/environment.rb' - - 'spec/spec_helper.rb' - -# Offense count: 11 -# Configuration parameters: EnforcedStyle. -# SupportedStyles: annotated, template, unannotated -Style/FormatStringToken: - Exclude: - - 'app/decorators/backtrace_line_decorator.rb' - - 'app/helpers/problems_helper.rb' - - 'spec/views/apps/edit.html.haml_spec.rb' - - 'spec/views/problems/show.html.haml_spec.rb' - - 'spec/views/users/show.html.haml_spec.rb' - -# Offense count: 218 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: when_needed, always, never -Style/FrozenStringLiteralComment: - Enabled: false - -# Offense count: 4 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. -# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys -Style/HashSyntax: - Exclude: - - 'app/views/apps/show.atom.builder' - - 'app/views/problems/_list.atom.builder' - - 'app/views/problems/index.atom.builder' - -# Offense count: 19 -# Cop supports --auto-correct. -Style/IfUnlessModifier: - Enabled: false - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: InverseMethods, InverseBlocks. -Style/InverseMethods: - Exclude: - - 'app/models/app.rb' - - 'spec/errbit_plugin/mock_issue_tracker.rb' - -# Offense count: 1 -Style/MixinUsage: - Exclude: - - 'config/initializers/notification_services.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Style/MultilineIfModifier: - Exclude: - - 'app/models/app.rb' - -# Offense count: 24 -# Cop supports --auto-correct. -Style/MutableConstant: - Enabled: false - -# Offense count: 9 -# Cop supports --auto-correct. -# Configuration parameters: Whitelist. -# Whitelist: be, be_a, be_an, be_between, be_falsey, be_kind_of, be_instance_of, be_truthy, be_within, eq, eql, end_with, include, match, raise_error, respond_to, start_with -Style/NestedParenthesizedCalls: - Exclude: - - 'spec/controllers/devise_sessions_controller_spec.rb' - - 'spec/controllers/problems_controller_spec.rb' - - 'spec/views/issue_trackers/issue.md.erb_spec.rb' - - 'spec/views/issue_trackers/issue.txt.erb_spec.rb' - - 'spec/views/problems/index.html.haml_spec.rb' - - 'spec/views/problems/show.html.haml_spec.rb' - -# Offense count: 3 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedOctalStyle. -# SupportedOctalStyles: zero_with_o, zero_only -Style/NumericLiteralPrefix: - Exclude: - - 'spec/controllers/api/v1/problems_controller_spec.rb' - -# Offense count: 9 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle. -# SupportedStyles: predicate, comparison -Style/NumericPredicate: - Exclude: - - 'spec/**/*' - - 'app/controllers/apps_controller.rb' - - 'app/interactors/notice_refingerprinter.rb' - - 'app/interactors/outdated_problem_clearer.rb' - - 'app/interactors/problem_recacher.rb' - - 'app/interactors/resolved_problem_clearer.rb' - - 'app/models/notice_fingerprinter.rb' - - 'lib/tasks/errbit/database.rake' - -# Offense count: 1 -# Cop supports --auto-correct. -Style/OrAssignment: - Exclude: - - 'app/models/user.rb' - -# Offense count: 27 -# Cop supports --auto-correct. -# Configuration parameters: PreferredDelimiters. -Style/PercentLiteralDelimiters: - Enabled: false - -# Offense count: 2 -# Cop supports --auto-correct. -Style/RedundantParentheses: - Exclude: - - 'app/models/problem.rb' - - 'spec/controllers/problems_controller_spec.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Style/RedundantSelf: - Exclude: - - 'app/models/problem.rb' - -# Offense count: 4 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: implicit, explicit -Style/RescueStandardError: - Exclude: - - 'app/models/error_report.rb' - - 'app/models/issue.rb' - -# Offense count: 8 -# Cop supports --auto-correct. -# Configuration parameters: ConvertCodeThatCanStartToReturnNil, Whitelist. -# Whitelist: present?, blank?, presence, try, try! -Style/SafeNavigation: - Exclude: - - 'app/decorators/backtrace_line_decorator.rb' - - 'app/models/app.rb' - - 'app/models/comment.rb' - - 'app/models/issue.rb' - - 'app/models/notice.rb' - - 'app/models/notification_services/gtalk_service.rb' - - 'config/load.rb' - -# Offense count: 7 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: only_raise, only_fail, semantic -Style/SignalException: - Exclude: - - 'app/controllers/notices_controller.rb' - - 'app/interactors/problem_merge.rb' - - 'app/models/problem.rb' - - 'config/boot.rb' - - 'lib/airbrake_api/v3/notice_parser.rb' - - 'lib/hoptoad.rb' - -# Offense count: 938 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. -# SupportedStyles: single_quotes, double_quotes -Style/StringLiterals: - Enabled: false - -# Offense count: 19 -# Cop supports --auto-correct. -# Configuration parameters: MinSize. -# SupportedStyles: percent, brackets -Style/SymbolArray: - EnforcedStyle: brackets - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, AllowSafeAssignment. -# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex -Style/TernaryParentheses: - Exclude: - - 'app/helpers/sort_helper.rb' - - 'app/models/notification_services/hipchat_service.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Style/UnneededCondition: - Exclude: - - 'app/models/issue_tracker.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Style/UnneededInterpolation: - Exclude: - - 'app/models/notification_services/pushover_service.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Style/ZeroLengthPredicate: - Exclude: - - 'app/interactors/notice_refingerprinter.rb' - -# Offense count: 452 -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. -# URISchemes: http, https -Metrics/LineLength: - Max: 227 diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000000..43beb4001b --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +4.0.7 diff --git a/.standard.yml b/.standard.yml new file mode 100644 index 0000000000..323fdc27c4 --- /dev/null +++ b/.standard.yml @@ -0,0 +1 @@ +ruby_version: 4.0 diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000000..f6db94fe04 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 24.21.0 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 25cf7f26c9..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,48 +0,0 @@ -language: ruby - -rvm: - # Let's build with the most relevant versions first: - - 2.5 - - 2.4 - - 2.3 - - ruby-head - -env: - global: - - COVERAGE=true JRUBY_OPTS=--debug - matrix: - - MONGODB=4.0.0 - - MONGODB=3.6.6 - - MONGODB=3.4.15 - -sudo: false - -cache: - bundler: true - directories: - - /home/travis/.cache/rubocop_cache - - /home/travis/mongodb-install - -before_install: - - gem update --system - -before_script: - - test -f mongodb-install/mongodb-linux-x86_64-${MONGODB}.tgz || wget --directory-prefix=mongodb-install/ https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-${MONGODB}.tgz - - tar xzf mongodb-install/mongodb-linux-x86_64-${MONGODB}.tgz - - ${PWD}/mongodb-linux-x86_64-${MONGODB}/bin/mongod --version - - mkdir /tmp/mongodb-data - - > - ${PWD}/mongodb-linux-x86_64-${MONGODB}/bin/mongod - --dbpath /tmp/mongodb-data - --logpath /tmp/mongodb.log - --fork - - until nc -z localhost 27017; do echo "waiting for mongodb ${MONGODB}"; sleep 1; done - - bundle exec rake errbit:bootstrap - -script: - - bundle exec rspec - - bundle exec rubocop - -matrix: - allow_failures: - - rvm: ruby-head diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index d906143dc6..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,255 +0,0 @@ -## 0.7.0 -https://github.com/errbit/errbit/issues?q=milestone%3A0.7.0 - -## 0.6.1 -https://github.com/errbit/errbit/issues?utf8=%E2%9C%93&q=milestone%3A0.6.1 - -## 0.6.0 -https://github.com/errbit/errbit/issues?utf8=%E2%9C%93&q=milestone%3A0.6.0 - -## 0.5.0 -https://github.com/errbit/errbit/issues?utf8=%E2%9C%93&q=milestone%3A0.5.0 - -## 0.4.0 - -- #791 Fix double escaping of problem title in list view.(@felixbuenemann) -- #790 Fix favicon location.(@felixbuenemann) -- #716 Fix inline comments styles.(@oelmekki) -- #778 Update to rails 4.1.(@arthurnn) -- #682 Namespace fingerprint classes better.(@csaunders) -- Drop ruby support for versions 1.9.x, and 2.0.x.(@arthurnn) -- #773 Only leave GitHub issue tracker on vanila errbit, as the others could be - added just using the plugin.(@arthurnn) -- #768 Update to Mongoid 4.0 and rails to 4.0 (@arthurnn) -- #820 Environment-only configuration - -## 0.3.0 - -### Improvements - -- [#515][] Update to Mongoid 3.1 ([@arthurnn][], [@shingara][]) -- Drop the 1.8 support -- [#289][] Add unwatch button on App view ([@numbata][], [@shingara][]) -- Add autofocus on email login page ([@shingara][]) -- Update Gem ([@arthurnn][], [@shingara][]) -- [#547][] Add capabilities to admin to regenerate the API key of his - app ([@shingara][]) -- Add environement variable configuration to change the ruby version to - use ([@shingara][]) -- [#588][] Change documentation to see Airbrake gem instead of - hoptoad_nofier ([@ugisozols][]) -- [#592][] Avoid taskmapper dependencies ([@arthurnn][]) -- [#597][] Improve fingerprinting if only change by object memory adress - ([@1st8][]) -- [#603][] Improve the page rendering of App list ([@arthurnn][]) -- [#606][] Add ability to deploy on Cloud 66 ([@pkallberg][]) -- [#613][] Improve text about delete thing ([@mildavw][]) -- [#616][] Improvement about our test suite ([@durrantm][]) -- [#620][] Add App version information in email notification - ([@soberstadt][]) -- [#624][] Allow configuration of port if errbit not on 80 - ([@rsutphin][]) -- [#617][] Allow GET request on notice API ([@soberstadt][]) -- [#618][] Improve the stack information in JS notifier ([@soberstadt][]) -- [#619][] Handle the App version information in the API ([@soberstadt][]) - -### Bug Fixes - -- [#565][] Treat request URL as CDATA so query string params don't - result in invalid XML ([@mildavw][]) -- [#594][] Dont use ^ and $ on email_regexp([@arthurnn][]) -- [#598][] Fix issue about gitlab integration ([@jozefvaclavik][]) -- [#609][] Fix issue about page system on search error ([@zhekanax][]) -- [#611][] Fix issue about select all error in search page ([@zhekanax][]) -- [#615][] Fix some LDAP information in README ([@felixbuenemann][]) - - -[@arthurnn]: https://github.com/arthurnn -[@mildavw]: https://github.com/mildavw -[@numbata]: https://github.com/numbata -[@shingara]: https://github.com/shingara -[@ugisozols]: https://github.com/ugisozols -[@1st8]: https://github.com/1st8 -[@jozefvaclavik]: https://github.com/jozefvaclavik -[@zhekanax]: https://github.com/zhekanax -[@pkallberg]: https://github.com/pkallberg -[@durrantm]: https://github.com/durrantm -[@felixbuenemann]: https://github.com/felixbuenemann -[@soberstadt]: https://github.com/soberstadt -[@rsutphin]: https://github.com/rsutphin - -[#289]: https://github.com/errbit/errbit/issues/289 -[#515]: https://github.com/errbit/errbit/issues/515 -[#547]: https://github.com/errbit/errbit/issues/547 -[#565]: https://github.com/errbit/errbit/issues/565 -[#588]: https://github.com/errbit/errbit/issues/588 -[#592]: https://github.com/errbit/errbit/pull/592 -[#594]: https://github.com/errbit/errbit/pull/594 -[#597]: https://github.com/errbit/errbit/pull/597 -[#598]: https://github.com/errbit/errbit/pull/598 -[#603]: https://github.com/errbit/errbit/pull/603 -[#606]: https://github.com/errbit/errbit/pull/606 -[#609]: https://github.com/errbit/errbit/pull/609 -[#611]: https://github.com/errbit/errbit/pull/611 -[#613]: https://github.com/errbit/errbit/pull/613 -[#616]: https://github.com/errbit/errbit/pull/616 -[#615]: https://github.com/errbit/errbit/pull/615 -[#617]: https://github.com/errbit/errbit/pull/617 -[#618]: https://github.com/errbit/errbit/pull/618 -[#619]: https://github.com/errbit/errbit/pull/619 -[#620]: https://github.com/errbit/errbit/pull/620 -[#624]: https://github.com/errbit/errbit/pull/624 - -## 0.2.1 - Not released Yet - -### Improvements - -- [#552][] Limite size of asset on errbit ([@tscolari][]) -- [#562][] See the version number of errbit on footer ([@nashby][]) -- [#555][] Avoid same group by same error if other line of backtrace - instead of first change ([@shingara][]) - -### Bug Fixes - -- [#558][] Avoid failure if you remote bitbucket_rest_api gem - ([@shingara][]) -- [#559][] Fix some issue on the migration with old database - ([@shingara][]) - -[@nashby]: https://github.com/nashby -[@shingara]: https://github.com/shingara -[@tscolari]: https://github.com/tscolari - -[#552]: https://github.com/errbit/errbit/issues/552 -[#558]: https://github.com/errbit/errbit/issues/558 -[#562]: https://github.com/errbit/errbit/issues/562 - -## 0.2.0 - 2013-09-11 - -### Improvements - -- Update some gems ([@shingara][]) -- [#492][] Improve some Pjax call ([@nfedyashev][]) -- [#428][] Add the support of Unfuddle Issue Tracker ([@parallel588][]) -- Avoid to delete his own user ([@shingara][]) -- [#456][] Avoid to delete admin access of current user logged ([@shingara][]) -- [#253][] Refactor the Fingerprint generation ([@boblail][]) -- [#508][] Merge comments to when you merge problems ([@shingara][]) -- Update the Devise Gem to the last one ([@shingara][]) -- [#524][] Add current user information on the notifer.js ([@roryf][]) -- [#523][] Update javascript-stacktrace ([@aliscott][]) -- [#516][] Add Jira Issue tracker ([@xenji][]) -- [#512][] Add capabilities to configure the use of sendmail to send - email from Errbit ([@shingara][]) -- [#532][] Use https link in Gravatar if you use errbit on https - ([@jeroenj][]) -- [#536][] Order app by name by default ([@2called-chaos][]) -- [#542][] Allow the MONGODB_URL env configuration about Mongodb ([@bacongobbler][]) -- [#530][] Improve the flowdock notification ([@nfedyashev][]) -- [#531][] Improve the HipChat notification message ([@brendonrapp][]) - - -### Bug Fixes - -- [#343][] Fix the ical generation. ([@shingara][]) -- [#503][] Fix issue on where the service_url choose never use. ([@nfedyashev][]) -- [#506][] Fix issue on bitbucket issue tracker creation failed. ([@Gonzih][]) -- [#514][] Add CDATA in xml return by Javascript. ([@mildavw][]) -- [#517][] Javascript escape path from javascript Notifier. ([@roryf][]) -- [#518][] Fix issue when you try launch task errbit:db:update_update_problem_attrs. ([@shingara][]) -- [#526][] Fix issue of pagination after search. ([@shingara][]) -- [#528][] Fix issue of action after search. ([@shingara][]) - -## 0.1.0 - 2013-05-29 - -### Improvements - -- [#474][] Improve message when access denied. ([@chadcf][]) -- [#468][] Launch a repairDatabase in MongoDB database after launching - the clear_resolved task. ([@shingara][]) -- Update gem of Mongoid to 2.7.1 -- Update gem of Mongo to 1.8.5 -- [#457][] Add task information about db:seed in Readme. ([@mildavw][]) -- Add support of Ruby 2.0.0 -- [#475][] Return a HTTP 422 status code when you try push notice with - bad api key. ([@shingara][]) -- Return a 400 http status when you try put a notice without args. - ([@shingara][]) -- [#486][] Add confirms box when you do massive action. ([@manuelmeurer][]) -- [#487][] Add specific template to redmine notification with less useless data. ([@tvdeyen][]) - -### Bug fixes - -- [#469][] Fix issue about the documentation of new heroku addons usage. - ([@adamjt][]) -- [#455][] Avoid raising exception if you comment an exception and no - other user are define to received this comment. ([@alvarobp][]) -- [#453][] Fix ruby 2.0.0 incompatibilities with gem ([@SamSaffron][]) -- [#476][] Fix javascript notifier issue with IE8 ([@sdepold][]) -- [#466][] Fix not see problem if octokit gem not define ([@tamaloa][]) -- [#460][] Fix issue when you try see user with gravatar activate but no - email define to this user ([@ivanyv][]) -- [#478][] Fix issue about calculation of statisque of problem after - merge ([@shingara][]) - - - -[#253]: https://github.com/errbit/errbit/issues/253 -[#343]: https://github.com/errbit/errbit/issues/343 -[#428]: https://github.com/errbit/errbit/issues/428 -[#453]: https://github.com/errbit/errbit/issues/453 -[#455]: https://github.com/errbit/errbit/issues/455 -[#456]: https://github.com/errbit/errbit/issues/456 -[#457]: https://github.com/errbit/errbit/issues/457 -[#460]: https://github.com/errbit/errbit/issues/460 -[#466]: https://github.com/errbit/errbit/issues/466 -[#468]: https://github.com/errbit/errbit/issues/468 -[#469]: https://github.com/errbit/errbit/issues/469 -[#474]: https://github.com/errbit/errbit/issues/474 -[#475]: https://github.com/errbit/errbit/issues/475 -[#476]: https://github.com/errbit/errbit/issues/476 -[#478]: https://github.com/errbit/errbit/issues/478 -[#487]: https://github.com/errbit/errbit/issues/487 -[#486]: https://github.com/errbit/errbit/issues/486 -[#492]: https://github.com/errbit/errbit/issues/492 -[#503]: https://github.com/errbit/errbit/issues/503 -[#506]: https://github.com/errbit/errbit/issues/506 -[#508]: https://github.com/errbit/errbit/issues/508 -[#514]: https://github.com/errbit/errbit/issues/514 -[#516]: https://github.com/errbit/errbit/issues/516 -[#517]: https://github.com/errbit/errbit/issues/517 -[#524]: https://github.com/errbit/errbit/issues/524 -[#526]: https://github.com/errbit/errbit/issues/526 -[#528]: https://github.com/errbit/errbit/issues/528 -[#530]: https://github.com/errbit/errbit/issues/530 -[#531]: https://github.com/errbit/errbit/issues/531 -[#532]: https://github.com/errbit/errbit/issues/532 -[#542]: https://github.com/errbit/errbit/issues/542 - - - -[@2called-chaos]: https://github.com/2called-chaos -[@Gonzih]: https://github.com/Gonzih -[@SamSaffron]: https://github.com/SamSaffron -[@adamjt]: https://github.com/adamjt -[@aliscott]: http://github.com/aliscott -[@alvarobp]: https://github.com/alvarobp -[@arthurnn]: https://github.com/arthurnn -[@bacongobbler]: https://github.com/bacongobbler -[@boblail]: https://github.com/boblail -[@brendonrapp]: https://github.com/brendonrapp -[@chadcf]: https://github.com/chadcf -[@ivanyv]: https://github.com/ivanyv -[@jeroenj]: https://github.com/jeroenj -[@manuelmeurer]: https://github.com/manuelmeurer -[@mildavw]: https://github.com/mildavw -[@mildavw]: https://github.com/mildavw -[@nfedyashev]: https://github.com/nfedyashev -[@parallel588]: https://github.com/parallel588 -[@roryf]: https://github.com/roryf -[@sdepold]: https://github.com/sdepold -[@shingara]: https://github.com/shingara -[@tamaloa]: https://github.com/tamaloa -[@tvdeyen]: https://github.com/tvdeyen -[@williamn]: https://github.com/williamn -[@xenji]: https://github.com/xenji diff --git a/CHECKS b/CHECKS deleted file mode 100644 index 02a571d1ec..0000000000 --- a/CHECKS +++ /dev/null @@ -1 +0,0 @@ -/users/sign_in Errbit diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bce0af31b1..ee8f92a03c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ follow the guidelines under 'All Contributions' below. All code change contributions should be made through the following steps: -1. Fork errbit/errbit on GitHub +1. Fork `errbit/errbit` on GitHub 2. Create a branch with a descriptive name 3. Make your changes 4. Submit a pull request with a clear explanation of what you have diff --git a/Dockerfile b/Dockerfile index 5c37bad995..afe7287a88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,42 +1,76 @@ -FROM ruby:2.5.1-alpine -LABEL maintainer="David Papp " - -WORKDIR /app -# throw errors if Gemfile has been modified since Gemfile.lock -RUN echo "gem: --no-document" >> /etc/gemrc \ - && bundle config --global frozen 1 \ - && bundle config --global clean true \ - && bundle config --global disable_shared_gems false \ - && gem update --system 2.7.4 \ - && gem install bundler --version 1.16.1 \ - && apk add --no-cache \ - curl \ - less \ - libxml2-dev \ - libxslt-dev \ - nodejs \ - tzdata - - -COPY ["Gemfile", "Gemfile.lock", "/app/"] - -RUN apk add --no-cache --virtual build-dependencies \ - build-base \ - && bundle config build.nokogiri --use-system-libraries \ - && bundle install \ - -j "$(getconf _NPROCESSORS_ONLN)" \ - --retry 5 \ - --without test development no_docker \ - && apk del build-dependencies - -COPY . /app - -RUN RAILS_ENV=production bundle exec rake assets:precompile \ - && rm -rf /app/tmp/* \ - && chmod 777 /app/tmp - -EXPOSE 8080 - -HEALTHCHECK CMD curl --fail "http://$(/bin/hostname -i | /usr/bin/awk '{ print $1 }'):${PORT:-8080}/users/sign_in" || exit 1 - -CMD ["bundle","exec","puma","-C","config/puma.default.rb"] +# syntax = docker/dockerfile:1 + +FROM registry.docker.com/library/ruby:4.0.7-slim@sha256:db9ddd17cc6ac603f2497d98ac5c88e4118908d6f9a45f2422ebee141f91e485 AS base + +# Rails app lives here +WORKDIR /rails + +# Install base packages +RUN set -eux ; \ + apt-get update -qq ; \ + apt-get dist-upgrade -qq ; \ + apt-get install --no-install-recommends -y curl libjemalloc2 shared-mime-info ; \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment +# https://github.com/rails/rails/pull/46981 +# https://github.com/rails/rails/commit/1a7e88323e6e92bf2d3ddf397b3023529b505e86#commitcomment-96003108 +# We don't use this image for testing on CI, so: add "test" to BUNDLE_WITHOUT +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development:test" \ + BOOTSNAP_LOG="true" \ + BOOTSNAP_READONLY="true" + +RUN set -eux ; \ + gem update --system "4.0.22" ; \ + gem install bundler --version "4.0.22" --force + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build gems +RUN set -eux ; \ + apt-get update -qq ; \ + apt-get dist-upgrade -qq ; \ + apt-get install --no-install-recommends -y build-essential git pkg-config libyaml-dev + +# Install application gems +COPY .ruby-version Gemfile Gemfile.lock UserGemfile ./ +RUN set -eux ; \ + bundle install ; \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git ; \ + bundle exec bootsnap precompile --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ config/ Rakefile + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + +# Final stage for app image +FROM base + +# Copy built artifacts: gems, application +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN set -eux ; \ + groupadd --system --gid 1000 rails ; \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash ; \ + chown -R rails:rails db log storage tmp + +USER 1000:1000 + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start server via Thruster by default, this can be overwritten at runtime +EXPOSE 80/tcp + +CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/Gemfile b/Gemfile index b168100c2b..1589c52a08 100644 --- a/Gemfile +++ b/Gemfile @@ -1,109 +1,114 @@ -source 'https://rubygems.org' - -RAILS_VERSION = '~> 4.2.10' - -send :ruby, ENV['GEMFILE_RUBY_VERSION'] if ENV['GEMFILE_RUBY_VERSION'] - -gem 'actionmailer', RAILS_VERSION -gem 'actionpack', RAILS_VERSION -gem 'railties', RAILS_VERSION - -gem 'actionmailer_inline_css' -gem 'decent_exposure' -gem 'devise', '~> 4.4.0' -gem 'dotenv-rails' -gem 'draper' -gem 'errbit_plugin' -gem 'errbit_github_plugin' -gem 'font-awesome-rails' -gem 'haml', '~> 5.1' -gem 'htmlentities' -gem 'kaminari' -gem 'kaminari-mongoid' -gem 'mongoid', '~> 5.4' -gem 'mongoid_rails_migrations' -gem 'rack-ssl', require: 'rack/ssl' # force SSL -gem 'rack-ssl-enforcer', require: false -gem 'rails_autolink' -gem 'useragent' - -# Please don't update hoptoad_notifier to airbrake. -# It's for internal use only, and we monkeypatch certain methods -gem 'hoptoad_notifier', "~> 2.4" +# frozen_string_literal: true + +source "https://rubygems.org" + +ruby file: ".ruby-version" + +gem "rails", "8.1.4" + +gem "sprockets-rails" +gem "stimulus-rails" +gem "importmap-rails" +gem "turbo-rails" +gem "activemodel-serializers-xml" +gem "actionmailer_inline_css" +gem "decent_exposure" +gem "devise" +gem "devise-i18n" +gem "pundit" +gem "dotenv" +gem "draper" +gem "errbit_plugin" +gem "errbit_github_plugin" +gem "font-awesome-rails" +gem "htmlentities" +gem "kaminari" +gem "kaminari-mongoid" +gem "kaminari-i18n" +gem "mongoid" +gem "faraday-retry" +gem "octokit" +gem "omniauth" +gem "omniauth-rails_csrf_protection" +gem "omniauth-github" +gem "omniauth-google-oauth2" +gem "rails_autolink" +gem "rails-i18n" +gem "inline_svg" +gem "useragent" +gem "nokogiri" +gem "uri" +gem "rack-timeout" +gem "puma" +gem "ostruct" +gem "securerandom" +gem "airbrake", require: false # Notification services # --------------------------------------- -gem 'campy' -# Hipchat -gem 'hipchat' -# Google Talk -gem 'xmpp4r', require: ["xmpp4r", "xmpp4r/muc"] +gem "campy" # Hoiio (SMS) -gem 'hoi' -# Pushover (iOS Push notifications) -gem 'rushover' +gem "hoi" +# Pushover.net (iOS/Android Push notifications) +gem "pushover2" # Hubot -gem 'httparty' -# Flowdock -gem 'flowdock' +gem "httparty" -# Authentication -# --------------------------------------- -# GitHub OAuth -gem 'omniauth-github' -# Google OAuth -gem 'omniauth-google-oauth2' +gem "icalendar" +gem "json" + +gem "pry-rails" -gem 'ri_cal' -gem 'yajl-ruby', platform: 'ruby' -gem 'json', platform: 'jruby' +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", ">= 1.4.4", require: false + +# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] +gem "thruster", require: false group :development, :test do - gem 'airbrake', '~> 4.3.5', require: false - gem 'pry-rails' - gem 'pry-byebug', platforms: [:mri] - gem 'quiet_assets' - gem 'rubocop', require: false + gem "rubydex", require: false + gem "rubocop", require: false + gem "rubocop-rails", require: false + gem "rubocop-rake", require: false + gem "rubocop-capybara", require: false + gem "rubocop-rspec", require: false + gem "rubocop-rspec_rails", require: false + gem "rubocop-performance", require: false + gem "rubocop-disable_syntax", require: false + gem "rubocop-thread_safety", require: false + gem "rubocop-factory_bot", require: false + gem "rubocop-erb", require: false + gem "standard", "1.56.0", require: false + gem "faker" + gem "factory_bot_rails" + gem "brakeman", require: false + gem "herb", require: false end group :development do - gem 'better_errors' - gem 'binding_of_caller', platform: 'ruby' - gem 'meta_request' + gem "listen", "~> 3.10" + gem "bundler-audit", require: false end group :test do - gem 'rake' - gem 'rspec' - gem 'rspec-rails', require: false - gem 'rspec-activemodel-mocks' - gem 'mongoid-rspec', require: false - gem 'fabrication' - gem 'capybara' - gem 'poltergeist' - gem 'phantomjs' - gem 'launchy' - gem 'email_spec' - gem 'timecop' - gem 'coveralls', require: false -end - -group :heroku, :production do - gem 'rails_12factor', require: ENV.key?("HEROKU") + gem "rails-controller-testing" + gem "rspec-rails", require: false + gem "rspec-rebound", require: false + gem "rspec-activemodel-mocks" + gem "mongoid-rspec" + gem "pundit-matchers" + gem "capybara" + gem "selenium-webdriver" + gem "launchy" + gem "email_spec" + gem "simplecov", require: false + gem "super_diff" + gem "webmock" + gem "vcr" end -group :no_docker, :test, :development do - gem 'therubyracer', platform: :ruby # C Ruby (MRI) or Rubinius, but NOT Windows -end - -gem 'puma' -gem 'sass-rails' -gem 'uglifier' -gem 'jquery-rails' -gem 'pjax_rails' -gem 'underscore-rails' - -gem 'sucker_punch' +gem "jquery-rails" +gem "pjax_rails" +gem "underscore-rails" -ENV['USER_GEMFILE'] ||= './UserGemfile' -eval_gemfile ENV['USER_GEMFILE'] if File.exist?(ENV['USER_GEMFILE']) +eval_gemfile "./UserGemfile" diff --git a/Gemfile.lock b/Gemfile.lock index 6793634357..f31a6864c6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,502 +1,1012 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.10) - actionpack (= 4.2.10) - actionview (= 4.2.10) - activejob (= 4.2.10) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 1.0, >= 1.0.5) + action_text-trix (2.1.19) + railties + actioncable (8.1.4) + actionpack (= 8.1.4) + activesupport (= 8.1.4) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.4) + actionpack (= 8.1.4) + activejob (= 8.1.4) + activerecord (= 8.1.4) + activestorage (= 8.1.4) + activesupport (= 8.1.4) + mail (>= 2.8.0) + actionmailer (8.1.4) + actionpack (= 8.1.4) + actionview (= 8.1.4) + activejob (= 8.1.4) + activesupport (= 8.1.4) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) actionmailer_inline_css (1.6.0) actionmailer (>= 3.0.0) nokogiri (>= 1.7.0) premailer (>= 1.11.0) - actionpack (4.2.10) - actionview (= 4.2.10) - activesupport (= 4.2.10) - rack (~> 1.6) - rack-test (~> 0.6.2) - rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.10) - activesupport (= 4.2.10) + actionpack (8.1.4) + actionview (= 8.1.4) + activesupport (= 8.1.4) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.4) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.4) + activerecord (= 8.1.4) + activestorage (= 8.1.4) + activesupport (= 8.1.4) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.4) + activesupport (= 8.1.4) builder (~> 3.1) - erubis (~> 2.7.0) - rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (4.2.10) - activesupport (= 4.2.10) - globalid (>= 0.3.0) - activemodel (4.2.10) - activesupport (= 4.2.10) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.4) + activesupport (= 8.1.4) + globalid (>= 0.3.6) + activemodel (8.1.4) + activesupport (= 8.1.4) + activemodel-serializers-xml (1.0.3) + activemodel (>= 5.0.0.a) + activesupport (>= 5.0.0.a) builder (~> 3.1) - activerecord (4.2.10) - activemodel (= 4.2.10) - activesupport (= 4.2.10) - arel (~> 6.0) - activesupport (4.2.10) - i18n (~> 0.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - addressable (2.4.0) - airbrake (4.3.8) - builder - multi_json - arel (6.0.4) - ast (2.4.0) - bcrypt (3.1.12) - bcrypt (3.1.12-java) - better_errors (2.5.0) - coderay (>= 1.0.0) - erubi (>= 1.0.0) - rack (>= 0.9.0) - binding_of_caller (0.8.0) - debug_inspector (>= 0.0.1) - bson (4.3.0) - bson (4.3.0-java) - builder (3.2.3) - byebug (10.0.2) - callsite (0.0.11) + activerecord (8.1.4) + activemodel (= 8.1.4) + activesupport (= 8.1.4) + timeout (>= 0.4.0) + activestorage (8.1.4) + actionpack (= 8.1.4) + activejob (= 8.1.4) + activerecord (= 8.1.4) + activesupport (= 8.1.4) + marcel (~> 1.0) + activesupport (8.1.4) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + airbrake (13.0.5) + airbrake-ruby (~> 6.0) + airbrake-ruby (6.2.2) + rbtree3 (~> 0.6) + anonymous_loader (0.1.3) + version_gem (~> 1.1, >= 1.1.14) + ast (2.4.3) + attr_extras (7.1.0) + auth-sanitizer (0.2.3) + version_gem (~> 1.1, >= 1.1.14) + base64 (0.3.0) + bcrypt (3.1.22) + bigdecimal (4.1.3) + bootsnap (1.26.0) + msgpack (~> 1.5) + brakeman (8.0.6) + racc + bson (5.2.0) + builder (3.3.0) + bundler-audit (0.9.3) + bundler (>= 1.2.0) + thor (~> 1.0) campy (1.0.0) - capybara (3.12.0) + capybara (3.40.0) addressable + matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) - regexp_parser (~> 1.2) + regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - cliver (0.3.2) - coderay (1.1.2) - concurrent-ruby (1.0.5) - concurrent-ruby (1.0.5-java) - coveralls (0.8.22) - json (>= 1.8, < 3) - simplecov (~> 0.16.1) - term-ansicolor (~> 1.3) - thor (~> 0.19.4) - tins (~> 1.6) - crass (1.0.4) - css_parser (1.6.0) + childprocess (5.1.0) + logger (~> 1.5) + coderay (1.1.3) + concurrent-ruby (1.3.8) + connection_pool (3.0.2) + crack (1.0.1) + bigdecimal + rexml + crass (1.0.7) + css_parser (3.2.0) addressable - debug_inspector (0.0.3) - decent_exposure (3.0.2) + base64 + ssrf_filter (~> 1.5) + csv (3.3.6) + date (3.5.1) + decent_exposure (3.0.4) activesupport (>= 4.0) - devise (4.4.3) + devise (5.0.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0, < 6.0) + railties (>= 7.0) responders warden (~> 1.2.3) - diff-lcs (1.3) - docile (1.3.1) - domain_name (0.5.20180417) - unf (>= 0.0.5, < 1.0.0) - dotenv (2.5.0) - dotenv-rails (2.5.0) - dotenv (= 2.5.0) - railties (>= 3.2, < 6.0) - draper (2.1.0) - actionpack (>= 3.0) - activemodel (>= 3.0) - activesupport (>= 3.0) - request_store (~> 1.0) - email_spec (1.6.0) - launchy (~> 2.1) - mail (~> 2.2) - errbit_github_plugin (0.3.0) + devise-i18n (1.16.1) + devise (>= 5.0.0) + rails-i18n + diff-lcs (1.6.2) + dotenv (3.2.0) + draper (4.0.6) + actionpack (>= 5.0) + activemodel (>= 5.0) + activemodel-serializers-xml (>= 1.0) + activesupport (>= 5.0) + request_store (>= 1.0) + ruby2_keywords + drb (2.2.3) + email_spec (2.3.1) + htmlentities (~> 4.3) + launchy (>= 2.1, < 4.0) + mail (~> 2.7) + erb (6.0.7) + errbit_github_plugin (0.5.0) + activesupport errbit_plugin + faraday-retry octokit - errbit_plugin (0.6.0) - erubi (1.7.1) - erubis (2.7.0) - execjs (2.7.0) - fabrication (2.20.1) - faraday (0.9.2) - multipart-post (>= 1.2, < 3) - ffi (1.9.25) - ffi (1.9.25-java) - flowdock (0.7.1) - httparty (~> 0.7) - multi_json - font-awesome-rails (4.2.0.0) - railties (>= 3.2, < 5.0) - globalid (0.4.1) - activesupport (>= 4.2.0) - haml (5.1.2) - temple (>= 0.8.0) - tilt - hashie (3.5.7) - hipchat (1.6.0) - httparty - mimemagic + errbit_plugin (0.8.0) + erubi (1.13.1) + factory_bot (6.6.0) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + faker (3.8.0) + i18n (>= 1.8.11, < 2) + faraday (2.14.4) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.4) + net-http (~> 0.5) + faraday-retry (2.4.0) + faraday (~> 2.0) + ffi (1.17.4) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86-linux-gnu) + ffi (1.17.4-x86-linux-musl) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) + font-awesome-rails (4.7.0.9) + railties (>= 3.2, < 9.0) + globalid (1.4.0) + activesupport (>= 6.1) + hashdiff (1.2.1) + hashie (5.1.0) + logger + herb (0.11.0) + herb (0.11.0-aarch64-linux-gnu) + herb (0.11.0-aarch64-linux-musl) + herb (0.11.0-arm-linux-gnu) + herb (0.11.0-arm-linux-musl) + herb (0.11.0-arm64-darwin) + herb (0.11.0-x86-linux-gnu) + herb (0.11.0-x86-linux-musl) + herb (0.11.0-x86_64-darwin) + herb (0.11.0-x86_64-linux-gnu) + herb (0.11.0-x86_64-linux-musl) hoi (0.0.7) httparty json - hoptoad_notifier (2.4.11) - activesupport - builder - htmlentities (4.3.4) - http-cookie (1.0.3) - domain_name (~> 0.5) - httparty (0.16.3) - mime-types (~> 3.0) + htmlentities (4.4.2) + httparty (0.24.2) + csv + mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (0.9.5) + i18n (1.15.2) concurrent-ruby (~> 1.0) - jaro_winkler (1.5.1) - jaro_winkler (1.5.1-java) - jquery-rails (4.3.3) + icalendar (2.12.5) + base64 + ice_cube (~> 0.16) + logger + ostruct + ice_cube (0.17.0) + importmap-rails (2.2.3) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + inline_svg (1.10.0) + activesupport (>= 3.0) + nokogiri (>= 1.6) + io-console (0.9.4) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jquery-rails (4.6.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.1.0) - json (2.1.0-java) - jwt (2.1.0) - kaminari (1.1.1) + json (2.21.2) + jwt (3.2.0) + base64 + kaminari (1.2.2) activesupport (>= 4.1.0) - kaminari-actionview (= 1.1.1) - kaminari-activerecord (= 1.1.1) - kaminari-core (= 1.1.1) - kaminari-actionview (1.1.1) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) actionview - kaminari-core (= 1.1.1) - kaminari-activerecord (1.1.1) + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) activerecord - kaminari-core (= 1.1.1) - kaminari-core (1.1.1) - kaminari-mongoid (1.0.1) + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) + kaminari-i18n (0.5.0) + kaminari + rails + kaminari-mongoid (1.0.2) kaminari-core (~> 1.0) mongoid - launchy (2.4.3) - addressable (~> 2.3) - launchy (2.4.3-java) - addressable (~> 2.3) - spoon (~> 0.0.1) - libv8 (3.16.14.19) - loofah (2.2.3) + language_server-protocol (3.17.0.6) + launchy (3.1.1) + addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) + lint_roller (1.1.0) + listen (3.10.0) + logger + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + logger (1.7.0) + loofah (2.25.2) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + mail (2.9.1) + logger mini_mime (>= 0.1.1) - meta_request (0.6.0) - callsite (~> 0.0, >= 0.0.11) - rack-contrib (>= 1.1, < 3) - railties (>= 3.0.0, < 6) - method_source (0.9.2) - mime-types (3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) - mimemagic (0.3.2) - mini_mime (1.0.1) - mini_portile2 (2.3.0) - minitest (5.11.3) - mongo (2.6.2) - bson (>= 4.3.0, < 5.0.0) - mongoid (5.4.0) - activemodel (~> 4.0) - mongo (>= 2.5.1, < 3.0.0) - origin (~> 2.3) - tzinfo (>= 0.3.37) - mongoid-rspec (3.0.0) - mongoid (~> 5.0) - rake - rspec (~> 3.3) - mongoid_rails_migrations (1.2.0) - activesupport (>= 4.2.0) - bundler (>= 1.0.0) - mongoid (>= 4.0.0) - rails (>= 4.2.0) - railties (>= 4.2.0) - multi_json (1.13.1) - multi_xml (0.6.0) - multipart-post (2.0.0) - netrc (0.11.0) - nokogiri (1.8.5) - mini_portile2 (~> 2.3.0) - nokogiri (1.8.5-java) - oauth2 (1.4.1) - faraday (>= 0.8, < 0.16.0) - jwt (>= 1.0, < 3.0) - multi_json (~> 1.3) + net-imap + net-pop + net-smtp + marcel (1.2.1) + matrix (0.4.3) + method_source (1.1.0) + mini_mime (1.1.5) + mini_portile2 (2.8.9) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) + mongo (2.26.0) + base64 + bson (>= 4.14.1, < 6.0.0) + mongoid (9.1.1) + activemodel (>= 5.1, < 8.2, != 7.0.0) + concurrent-ruby (>= 1.0.5, < 2.0) + mongo (>= 2.18.0, < 3.0.0) + ostruct + mongoid-compatibility (1.0.0) + activesupport + mongoid (>= 2.0) + mongoid-rspec (4.2.0) + mongoid (>= 3.0, < 10.0) + mongoid-compatibility (>= 0.5.1) + msgpack (1.8.5) + multi_xml (0.9.1) + bigdecimal (>= 3.1, < 5) + net-http (0.9.1) + uri (>= 0.11.1) + net-imap (0.6.7) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.4.0) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.4) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.19.4-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.19.4-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.19.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-musl) + racc (~> 1.4) + oauth2 (2.0.25) + anonymous_loader (~> 0.1, >= 0.1.3) + auth-sanitizer (~> 0.2, >= 0.2.3) + faraday (>= 0.17.3, < 4.0) + jwt (>= 1.0, < 4.0) + logger (~> 1.2) multi_xml (~> 0.5) - rack (>= 1.2, < 3) - octokit (4.3.0) - sawyer (~> 0.7.0, >= 0.5.3) - omniauth (1.8.1) - hashie (>= 3.4.6, < 3.6.0) - rack (>= 1.6.2, < 3) - omniauth-github (1.3.0) - omniauth (~> 1.5) - omniauth-oauth2 (>= 1.4.0, < 2.0) - omniauth-google-oauth2 (0.5.4) - jwt (>= 1.5) - omniauth (>= 1.1.1) - omniauth-oauth2 (>= 1.5) - omniauth-oauth2 (1.5.0) - oauth2 (~> 1.1) - omniauth (~> 1.2) - origin (2.3.1) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0, >= 2.0.7) + version_gem (~> 1.1, >= 1.1.14) + octokit (10.0.0) + faraday (>= 1, < 3) + sawyer (~> 0.9) + omniauth (2.1.4) + hashie (>= 3.4.6) + logger + rack (>= 2.2.3) + rack-protection + omniauth-github (2.0.1) + omniauth (~> 2.0) + omniauth-oauth2 (~> 1.8) + omniauth-google-oauth2 (1.2.3) + jwt (>= 2.9.2, < 4) + oauth2 (~> 2.0) + omniauth (~> 2.0) + omniauth-oauth2 (~> 1.8) + omniauth-oauth2 (1.9.0) + oauth2 (>= 2.0.2, < 3) + omniauth (~> 2.0) + omniauth-rails_csrf_protection (2.0.1) + actionpack (>= 4.2) + omniauth (~> 2.0) + optimist (3.2.1) orm_adapter (0.5.0) - parallel (1.12.1) - parser (2.5.1.2) - ast (~> 2.4.0) - phantomjs (2.1.1.0) - pjax_rails (0.4.0) + ostruct (0.6.3) + parallel (2.2.0) + parser (3.3.12.0) + ast (~> 2.4.1) + racc + patience_diff (1.2.0) + optimist (~> 3.0) + pjax_rails (0.5.1) jquery-rails - railties (>= 3.2, < 5.0) - poltergeist (1.18.1) - capybara (>= 2.1, < 4) - cliver (~> 0.3.1) - websocket-driver (>= 0.2.0) - powerpack (0.1.2) - premailer (1.11.1) + railties (>= 4.0) + pp (0.6.4) + prettyprint + premailer (1.31.0) addressable - css_parser (>= 1.6.0) + css_parser (>= 1.19.0) htmlentities (>= 4.0.0) - pry (0.12.2) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - pry (0.12.2-java) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - spoon (~> 0.0) - pry-byebug (3.6.0) - byebug (~> 10.0) - pry (~> 0.10) - pry-rails (0.3.9) - pry (>= 0.10.4) - puma (3.12.0) - puma (3.12.0-java) - quiet_assets (1.1.0) - railties (>= 3.1, < 5.0) - rack (1.6.11) - rack-contrib (1.8.0) - rack (~> 1.4) - rack-ssl (1.4.1) - rack - rack-ssl-enforcer (0.2.9) - rack-test (0.6.3) - rack (>= 1.0) - rails (4.2.10) - actionmailer (= 4.2.10) - actionpack (= 4.2.10) - actionview (= 4.2.10) - activejob (= 4.2.10) - activemodel (= 4.2.10) - activerecord (= 4.2.10) - activesupport (= 4.2.10) - bundler (>= 1.3.0, < 2.0) - railties (= 4.2.10) - sprockets-rails - rails-deprecated_sanitizer (1.0.3) - activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.9) - activesupport (>= 4.2.0, < 5.0) - nokogiri (~> 1.6) - rails-deprecated_sanitizer (>= 1.0.1) - rails-html-sanitizer (1.0.4) - loofah (~> 2.2, >= 2.2.2) - rails_12factor (0.0.3) - rails_serve_static_assets - rails_stdout_logging - rails_autolink (1.1.6) - rails (> 3.1) - rails_serve_static_assets (0.0.4) - rails_stdout_logging (0.0.3) - railties (4.2.10) - actionpack (= 4.2.10) - activesupport (= 4.2.10) - rake (>= 0.8.7) - thor (>= 0.18.1, < 2.0) - rainbow (3.0.0) - rake (12.3.2) - rb-fsevent (0.10.3) - rb-inotify (0.9.10) - ffi (>= 0.5.0, < 2) - ref (2.0.0) - regexp_parser (1.3.0) - request_store (1.1.0) - responders (2.4.0) - actionpack (>= 4.2.0, < 5.3) - railties (>= 4.2.0, < 5.3) - rest-client (2.0.2) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 4.0) - netrc (~> 0.8) - ri_cal (0.8.8) - rspec (3.8.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-activemodel-mocks (1.1.0) + prettyprint (0.2.0) + prism (1.9.0) + pry (0.16.0) + coderay (~> 1.1) + method_source (~> 1.0) + reline (>= 0.6.0) + pry-rails (0.3.11) + pry (>= 0.13.0) + public_suffix (7.0.5) + puma (8.0.2) + nio4r (~> 2.0) + pundit (2.5.2) + activesupport (>= 3.0.0) + pundit-matchers (4.0.0) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) + pushover2 (0.1.0) + json + net-http + uri + racc (1.8.1) + rack (3.2.7) + rack-protection (4.2.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rack-timeout (0.7.0) + rackup (2.3.1) + rack (>= 3) + rails (8.1.4) + actioncable (= 8.1.4) + actionmailbox (= 8.1.4) + actionmailer (= 8.1.4) + actionpack (= 8.1.4) + actiontext (= 8.1.4) + actionview (= 8.1.4) + activejob (= 8.1.4) + activemodel (= 8.1.4) + activerecord (= 8.1.4) + activestorage (= 8.1.4) + activesupport (= 8.1.4) + bundler (>= 1.15.0) + railties (= 8.1.4) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + rails-i18n (8.1.0) + i18n (>= 0.7, < 2) + railties (>= 8.0.0, < 9) + rails_autolink (1.1.8) + actionview (> 3.1) + activesupport (> 3.1) + railties (> 3.1) + railties (8.1.4) + actionpack (= 8.1.4) + activesupport (= 8.1.4) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.4.2) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rbs (4.2.0) + logger + prism (>= 1.6.0) + tsort + rbtree3 (0.7.1) + rdoc (8.1.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort + regexp_parser (2.13.1) + reline (0.7.0) + io-console (~> 0.5) + request_store (1.7.0) + rack (>= 1.4) + responders (3.2.1) + actionpack (>= 7.0) + railties (>= 7.0) + rexml (3.4.4) + rspec-activemodel-mocks (1.3.0) activemodel (>= 3.0) activesupport (>= 3.0) rspec-mocks (>= 2.99, < 4.0) - rspec-core (3.8.0) - rspec-support (~> 3.8.0) - rspec-expectations (3.8.2) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-mocks (3.8.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-rails (3.8.1) - actionpack (>= 3.0) - activesupport (>= 3.0) - railties (>= 3.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-support (~> 3.8.0) - rspec-support (3.8.0) - rubocop (0.58.2) - jaro_winkler (~> 1.5.1) - parallel (~> 1.10) - parser (>= 2.5, != 2.5.1.1) - powerpack (~> 0.1) + rspec-support (~> 3.13.0) + rspec-rails (8.0.4) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (>= 3.13.0, < 5.0.0) + rspec-expectations (>= 3.13.0, < 5.0.0) + rspec-mocks (>= 3.13.0, < 5.0.0) + rspec-support (>= 3.13.0, < 5.0.0) + rspec-rebound (0.2.1) + rspec-core (~> 3.3) + rspec-support (3.13.7) + rubocop (1.88.2) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (>= 1.10) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) - ruby-progressbar (1.9.0) - rushover (0.3.0) - json - rest-client - sass (3.5.6) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.0.7) - railties (>= 4.0.0, < 6) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) - sawyer (0.7.0) - addressable (>= 2.3.5, < 2.5) - faraday (~> 0.8, < 0.10) - simplecov (0.16.1) - docile (~> 1.1) - json (>= 1.8, < 3) - simplecov-html (~> 0.10.0) - simplecov-html (0.10.2) - spoon (0.0.6) - ffi - sprockets (3.7.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.50.0) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-capybara (3.0.0) + lint_roller (~> 1.1) + rubocop (~> 1.81) + rubocop-disable_syntax (0.2.0) + lint_roller + rubocop (>= 1.72.0) + rubocop-erb (0.7.1) + herb (~> 0.7) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-factory_bot (2.28.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.36.0) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rake (0.7.1) + lint_roller (~> 1.1) + rubocop (>= 1.72.1) + rubocop-rspec (3.10.2) + lint_roller (~> 1.1) + regexp_parser (>= 2.0) + rubocop (~> 1.86, >= 1.86.2) + rubocop-rspec_rails (2.32.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-rspec (~> 3.5) + rubocop-thread_safety (0.7.3) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-ast (>= 1.44.0, < 2.0) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + rubydex (0.4.1) + rubydex (0.4.1-aarch64-linux) + rubydex (0.4.1-arm64-darwin) + rubydex (0.4.1-x86_64-darwin) + rubydex (0.4.1-x86_64-linux) + rubyzip (3.7.0) + sawyer (0.9.3) + addressable (>= 2.3.5) + faraday (>= 0.17.3, < 3) + securerandom (0.4.1) + selenium-webdriver (4.49.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) + simplecov (1.3.1) + snaky_hash (2.0.7) + hashie (>= 0.1.0, < 6) + version_gem (~> 1.1, >= 1.1.14) + sprockets (4.4.1) + concurrent-ruby (~> 1.1) + logger + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) - sucker_punch (2.1.1) + ssrf_filter (1.6.0) + standard (1.56.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.88.0) + standard-custom (~> 1.0.0) + standard-performance (~> 1.8) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.9.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.26.0) + stimulus-rails (1.3.4) + railties (>= 6.0.0) + super_diff (0.19.0) + attr_extras (>= 6.2.4, < 8) + diff-lcs (~> 1.5) + patience_diff (~> 1.2) + thor (1.5.0) + thruster (0.1.26) + thruster (0.1.26-aarch64-linux) + thruster (0.1.26-arm64-darwin) + thruster (0.1.26-x86_64-darwin) + thruster (0.1.26-x86_64-linux) + timeout (0.6.1) + tsort (0.2.0) + turbo-rails (2.0.23) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) - temple (0.8.2) - term-ansicolor (1.6.0) - tins (~> 1.0) - therubyracer (0.12.3) - libv8 (~> 3.16.14.15) - ref - thor (0.19.4) - thread_safe (0.3.6) - thread_safe (0.3.6-java) - tilt (2.0.10) - timecop (0.9.1) - tins (1.16.3) - tzinfo (1.2.5) - thread_safe (~> 0.1) - uglifier (4.1.20) - execjs (>= 0.3.0, < 3) underscore-rails (1.8.3) - unf (0.1.4) - unf_ext - unf (0.1.4-java) - unf_ext (0.0.7.5) - unicode-display_width (1.4.0) - useragent (0.16.10) - warden (1.2.7) - rack (>= 1.0) - websocket-driver (0.7.0) - websocket-extensions (>= 0.1.0) - websocket-driver (0.7.0-java) + unicode-display_width (3.3.0) + unicode-emoji (~> 4.3) + unicode-emoji (4.3.0) + uri (1.1.1) + useragent (0.16.11) + vcr (6.4.0) + version_gem (1.1.15) + warden (1.2.9) + rack (>= 2.0.9) + webmock (3.26.4) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + websocket (1.2.11) + websocket-driver (0.8.2) + base64 websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.3) - xmpp4r (0.5.6) + websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - yajl-ruby (1.4.1) + zeitwerk (2.8.3) PLATFORMS - java + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin ruby + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES - actionmailer (~> 4.2.10) actionmailer_inline_css - actionpack (~> 4.2.10) - airbrake (~> 4.3.5) - better_errors - binding_of_caller + activemodel-serializers-xml + airbrake + bootsnap (>= 1.4.4) + brakeman + bundler-audit campy capybara - coveralls decent_exposure - devise (~> 4.4.0) - dotenv-rails + devise + devise-i18n + dotenv draper email_spec errbit_github_plugin errbit_plugin - fabrication - flowdock + factory_bot_rails + faker + faraday-retry font-awesome-rails - haml (~> 5.1) - hipchat + herb hoi - hoptoad_notifier (~> 2.4) htmlentities httparty + icalendar + importmap-rails + inline_svg jquery-rails json kaminari + kaminari-i18n kaminari-mongoid launchy - meta_request - mongoid (~> 5.4) + listen (~> 3.10) + mongoid mongoid-rspec - mongoid_rails_migrations + nokogiri + octokit + omniauth omniauth-github omniauth-google-oauth2 - phantomjs + omniauth-rails_csrf_protection + ostruct pjax_rails - poltergeist - pry-byebug pry-rails puma - quiet_assets - rack-ssl - rack-ssl-enforcer - rails_12factor + pundit + pundit-matchers + pushover2 + rack-timeout + rails (= 8.1.4) + rails-controller-testing + rails-i18n rails_autolink - railties (~> 4.2.10) - rake - ri_cal - rspec rspec-activemodel-mocks rspec-rails + rspec-rebound rubocop - rushover - sass-rails - sucker_punch - therubyracer - timecop - uglifier + rubocop-capybara + rubocop-disable_syntax + rubocop-erb + rubocop-factory_bot + rubocop-performance + rubocop-rails + rubocop-rake + rubocop-rspec + rubocop-rspec_rails + rubocop-thread_safety + rubydex + securerandom + selenium-webdriver + simplecov + sprockets-rails + standard (= 1.56.0) + stimulus-rails + super_diff + thruster + turbo-rails underscore-rails + uri useragent - xmpp4r - yajl-ruby + vcr + webmock + +CHECKSUMS + action_text-trix (2.1.19) sha256=7012f59421009cf284aa651294896414d653a61a2417c9b8714c8476d2f74009 + actioncable (8.1.4) sha256=cfcd72f8c861e01ccdc96d772ad8c5ad6ed0ea7c571aef81a794b6027eaa7f11 + actionmailbox (8.1.4) sha256=9c4525e577ebb671ce558f3ea78b8a634f64e56a0e6e8d105b28b873793a06eb + actionmailer (8.1.4) sha256=f82d95d6762ae46ea38f221d3363639266af4f8e56d50e2bd367558674baf093 + actionmailer_inline_css (1.6.0) sha256=b763306c37bf79f9e46b67b5e3cca261b42a0457f0fb05ca1b850f9aef0f5450 + actionpack (8.1.4) sha256=245493cc51be195a7645e6fc184880fa7e5b32ff1a08e053ee31f58c76e30ea4 + actiontext (8.1.4) sha256=33ee99bea7da6af911fc7d98b17013a8e1ec62aa01b20b20e9a5366f40dbf644 + actionview (8.1.4) sha256=eff1efd07c4d969abad288322b423bcb67fdbab5e324c91ca37352b80f226076 + activejob (8.1.4) sha256=4ed5b2ed8d4cc5c8f5f09becfef8346f202a4884eee0c8c73cae5ac378824099 + activemodel (8.1.4) sha256=9a0906c127bd118aa5f06157db12b8f750da83dccee72db2f658547a0ae7a0e8 + activemodel-serializers-xml (1.0.3) sha256=fa1b16305e7254cc58a59c68833e3c0a593a59c8ab95d3be5aaea7cd9416c397 + activerecord (8.1.4) sha256=3b4957ea7f246c9231bd7faa909312eac03a9101aaf87977b6c52733ff00b04f + activestorage (8.1.4) sha256=f4956f3ec9ee63e5e0b332b57c877cb627f82b56f4cc902279310d2aa285c7a8 + activesupport (8.1.4) sha256=891dd3fe43c4fc8391934b237b1189bb1f661c932a84a840043db9d46a4e80df + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + airbrake (13.0.5) sha256=901f5074c25d5ef77ed87f5bde7a28400a7324f5d7013a8a12d07e0099cc31b6 + airbrake-ruby (6.2.2) sha256=293e34fb36e763e1b6d67ab584cce7c5b6fe9eea1a70c26d8c13c0f5d7de2fbc + anonymous_loader (0.1.3) sha256=084a18e2439144d955447dc11dfc982f41fcd1583ad32d4d55151325dc44cb55 + ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 + attr_extras (7.1.0) sha256=d96fc9a9dd5d85ba2d37762440a816f840093959ae26bb90da994c2d9f1fc827 + auth-sanitizer (0.2.3) sha256=db10aac92cfbe4c64ab637eebcbe1d67395d1694798041362173370f59933e3c + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032 + bigdecimal (4.1.3) sha256=61ebe1e5e559bdc3cc6f2c0ee7f427321fc838f59611c294356eb04d6e21cf66 + bootsnap (1.26.0) sha256=ca96237015e6cd74a02963d5821cf00ac5ea134653b323e8cd6d702a7718bf1b + brakeman (8.0.6) sha256=759cc69341115e6c2dcd47b6fd8649a0b9bd540e3585ac8a0a94e31c66fee386 + bson (5.2.0) sha256=c468c1e8a3cfa1e80531cc519a890f85586986721d8e305f83465cc36bb82608 + builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f + bundler (4.0.22) sha256=d8d5ec84c8555e0af71db63ed7aee4d1a8fb839ec46d84212d61979242a5d75a + bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9 + campy (1.0.0) sha256=fc353b535c448afad5acfc972264b609a9a461335b09cda151d3e7bbf9504ef6 + capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef + childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec + coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b + concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1 + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e + crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295 + css_parser (3.2.0) sha256=e2d9d76164246d4d5d0b96e22fcbf581e1c65368ec3e52e056d4154e42eaac65 + csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456 + date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 + decent_exposure (3.0.4) sha256=5ce9e3df24f8d77f094b2e612d1c43a78a2de879847ccbb57c0e596e6c303cf1 + devise (5.0.4) sha256=d605f2b85854e74e56ee789e2d398702bc2d06e6bcd894717a670a3199c74cc1 + devise-i18n (1.16.1) sha256=66321fa28111a78de25cbd7d4b5e7edcf67d3b73a419a8df563365849f68b2ba + diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 + dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d + draper (4.0.6) sha256=e96990bffd4d98806913972f5ae2e6a7e8a5fb5433f17b80fbed7ba99d3c9236 + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + email_spec (2.3.1) sha256=5e8891319427fd6de0f3883dffe3c2ac4f5434359a33ed9c48e6d68e9f712f1b + erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92 + errbit_github_plugin (0.5.0) sha256=58db10fbc1012e2b71209e2d5928b3405560b8b6733b9562870c6a9079e0a42a + errbit_plugin (0.8.0) sha256=dfd883a4f0e55b5742cb839b0f623ac6188e52e7424f4a5f19aa11a136b09846 + erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 + factory_bot (6.6.0) sha256=1fc1b3b5620ec980a6a27aec1b6ec8c250ca82962e970e8a40f93e8d388d4b89 + factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68 + faker (3.8.0) sha256=c147b308df73a90f27a4fc84f18d4c22ef0ad9c2a64b2b61c86fd0ca71753efc + faraday (2.14.4) sha256=9bb4408c44621b0dbaaaa39e5dbfc4dbe58cc6751b6eb94d16ae9e1f2f2fab6e + faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588 + faraday-retry (2.4.0) sha256=7b79c48fb7e56526faf247b12d94a680071ff40c9fda7cf1ec1549439ad11ebe + ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf + ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df + ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 + ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 + ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 + ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b + ffi (1.17.4-x86-linux-gnu) sha256=38e150df5f4ca555e25beca4090823ae09657bceded154e3c52f8631c1ed72cf + ffi (1.17.4-x86-linux-musl) sha256=fbeec0fc7c795bcf86f623bb18d31ea1820f7bd580e1703a3d3740d527437809 + ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + font-awesome-rails (4.7.0.9) sha256=b45044908fbf3178b8d84fbae415dae4b9bf847612bcb1170d88ed13842c8732 + globalid (1.4.0) sha256=037f12fbf1d9d7a014d501c2d5c77356fd4ddd96d7a7991d6700bba96706f427 + hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1 + hashie (5.1.0) sha256=c266471896f323c446ea8207f8ffac985d2718df0a0ba98651a3057096ca3870 + herb (0.11.0) sha256=cba4bfc57f7c1f4b3ee2f4ed02d966d121ce9d8649b087c721d4f9c61f7c63e2 + herb (0.11.0-aarch64-linux-gnu) sha256=7d8244655bb99a50b2d52ea13408c2a76b3fbd70bcca8ccee4229d3e1902bb0b + herb (0.11.0-aarch64-linux-musl) sha256=8b19e8ce70cae1a016468cd6c3a88a051e47fd68f3f8b1bd3ffb751a62109982 + herb (0.11.0-arm-linux-gnu) sha256=8fd8e3c4d45c3b6bc8a2b7f056ac4bf961cfb03407fef26f7e1d5ef69406bf5d + herb (0.11.0-arm-linux-musl) sha256=0ac4e441389d8e67ef1afcabdb1bff94f5b7cd1c2811159647f61fc7bc241f8d + herb (0.11.0-arm64-darwin) sha256=8972ac3e5a7ee7296a89d78fceb40d8fb85c3ea8d40c9781a9c8db3e58194501 + herb (0.11.0-x86-linux-gnu) sha256=3059da43892860326523d40fdf22b10f5c1c676241b1bfa849dbaebe33dede01 + herb (0.11.0-x86-linux-musl) sha256=50d0768f16e998375fd6fa3133156a4c47f2b7cb7b196564ac836478c6cfb14b + herb (0.11.0-x86_64-darwin) sha256=2eb9db026a1949cb2ad8cfa1944ef8402feab784c7d37a35c48575a9e0d79912 + herb (0.11.0-x86_64-linux-gnu) sha256=aba19094bc1618e1bc0fb59db52ba251c93a24598f5228815616b282f10bb8a0 + herb (0.11.0-x86_64-linux-musl) sha256=e27a08fe72304c8043e851b568ed30eaa45f06b5333d151939a7834e936c01aa + hoi (0.0.7) sha256=bbea294ad76f08fcc6931f17e689ab3342cb09d73472fd5f7f1a1744af5f836d + htmlentities (4.4.2) sha256=bbafbdf69f2eca9262be4efef7e43e6a1de54c95eb600f26984f71d2fe96c5c3 + httparty (0.24.2) sha256=8fca6a54aa0c4aa4303a0fd33e5e2156175d6a5334f714263b458abd7fda9c38 + i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5 + icalendar (2.12.5) sha256=bda963d46bb08d9de1366eff05308c1e60a178532422fcc11d60360f84f58156 + ice_cube (0.17.0) sha256=32deb45dda4b4acc53505c2f581f6d32b5afc04d29b9004769944a0df5a5fcbe + importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a + inline_svg (1.10.0) sha256=5b652934236fd9f8adc61f3fd6e208b7ca3282698b19f28659971da84bf9a10f + io-console (0.9.4) sha256=a403304f32928ee85e56d457b4612ca43f2703b561a3258844992bd24085fbb1 + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 + jquery-rails (4.6.1) sha256=619f3496cdcdeaae1fd6dafa52dbac3fc45b745d4e09712da4184a16b3a8d9c0 + json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a + jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7 + kaminari (1.2.2) sha256=c4076ff9adccc6109408333f87b5c4abbda5e39dc464bd4c66d06d9f73442a3e + kaminari-actionview (1.2.2) sha256=1330f6fc8b59a4a4ef6a549ff8a224797289ebf7a3a503e8c1652535287cc909 + kaminari-activerecord (1.2.2) sha256=0dd3a67bab356a356f36b3b7236bcb81cef313095365befe8e98057dd2472430 + kaminari-core (1.2.2) sha256=3bd26fec7370645af40ca73b9426a448d09b8a8ba7afa9ba3c3e0d39cdbb83ff + kaminari-i18n (0.5.0) sha256=f20efcd31370f07eb423defdb296b8e05e48c8bc897655c636bc1b69b0fbe252 + kaminari-mongoid (1.0.2) sha256=7d28be9a8322d82bca36e6b4983f2f91e1149eee84755b52ce26c0cd0f040ebc + language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0 + launchy (3.1.1) sha256=72b847b5cc961589dde2c395af0108c86ff0119f42d4648d25b5440ebb10059e + lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + listen (3.10.0) sha256=c6e182db62143aeccc2e1960033bebe7445309c7272061979bb098d03760c9d2 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + loofah (2.25.2) sha256=2007f746959ac65552456e04b433e83deb22759ab38c838b4445c70e43425918 + mail (2.9.1) sha256=06574eca475253d6c18145dd70af80d0eb970182d55053497c5f4d797ea160e8 + marcel (1.2.1) sha256=1678e9360e32f9eafa917c80029e2f6d10b2715c66a4b87b6d0da9b9cd1f859f + matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b + method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5 + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289 + minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1 + mongo (2.26.0) sha256=e93190d0c184b5c14858e4239a61fe255b2eae86e04bf9590afb962b85a5fa5d + mongoid (9.1.1) sha256=eaf0e14f34374fd7b8cb041e913d4c60daa66e1b841188f4635296bcdb2e3337 + mongoid-compatibility (1.0.0) sha256=08e5770d102258a3b8f39443057de42cde61d465980f182954b1b70d965465e8 + mongoid-rspec (4.2.0) sha256=0811fccdc6dcca21987486208a0f614cc395620ccb7ce884b9bce0b5a677ecc8 + msgpack (1.8.5) sha256=97419eaa7b35dfe3e462c0e6c1a3dfdcbfc8756605c09e46e4c6daef127517ba + multi_xml (0.9.1) sha256=7ce766b59c17241ed62976caeae1fae9b2431b263398c35396239a68c4a64e57 + net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996 + net-imap (0.6.7) sha256=b5c9573be975d856de252ee851871724da66aa2d449b2482d5690bd12bc23660 + net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 + net-protocol (0.4.0) sha256=f9f90c8c6f64794a45160bf422177a336db12809de3beeeaa26b88fcd2e8c853 + net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 + nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 + nokogiri (1.19.4) sha256=50c951611c92bca05c51411aef45f1cbc50f2821c4802758c5c6d34696533ab5 + nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f + nokogiri (1.19.4-aarch64-linux-musl) sha256=35c65b9ce72b3bb03207bdbe7067915019dc18c1b9b59139684bd6690fdd01af + nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca + nokogiri (1.19.4-arm-linux-musl) sha256=588923c101bcfa78869734d247d25b598674323e7f22474fc468f6e5647311eb + nokogiri (1.19.4-arm64-darwin) sha256=a46db9853286e6597b36ebc6953817d15acf3a299583eb3f89fdc6f91dd63527 + nokogiri (1.19.4-x86_64-darwin) sha256=7fd17057d3e1f00e9954a74b3cd76595d3d4a5ef233b7ed9599047c204f70551 + nokogiri (1.19.4-x86_64-linux-gnu) sha256=379fae440b28915e3f19d752ce2dcf8465ed2b2fbefd2a7ca0dd497bc981a06a + nokogiri (1.19.4-x86_64-linux-musl) sha256=17dfb7c1fa194ae02fbf7c51a7afc8d278045ab3fdacfd86f91d02d7b274470b + oauth2 (2.0.25) sha256=2f736a2f93c2caa67c1b08dc3c9889bb907d62643f3183fefaa1723cba6a82ac + octokit (10.0.0) sha256=82e99a539b7637b7e905e6d277bb0c1a4bed56735935cc33db6da7eae49a24e8 + omniauth (2.1.4) sha256=42a05b0496f0d22e1dd85d42aaf602f064e36bb47a6826a27ab55e5ba608763c + omniauth-github (2.0.1) sha256=8ff8e70ac6d6db9d52485eef52cfa894938c941496e66b52b5e2773ade3ccad4 + omniauth-google-oauth2 (1.2.3) sha256=bdbab67e64e50b7902eee2dcfc2225575cefba15ab89a3b6e92c8947987c1da9 + omniauth-oauth2 (1.9.0) sha256=ed15f6d9d20991807ce114cc5b9c1453bce3645b64e51c68c90cff5ff153fee8 + omniauth-rails_csrf_protection (2.0.1) sha256=c6e3204d7e3925bb537cb52d50fdfc9f05293f1a9d87c5d4ab4ca3a39ba8c32d + optimist (3.2.1) sha256=8cf8a0fd69f3aa24ab48885d3a666717c27bc3d9edd6e976e18b9d771e72e34e + orm_adapter (0.5.0) sha256=aa5d0be5d540cbb46d3a93e88061f4ece6a25f6e97d6a47122beb84fe595e9b9 + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + parallel (2.2.0) sha256=e1059c5fd7b649558a0aec38a769f06a42942bdb40503d005a59c352fe011cd8 + parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828 + patience_diff (1.2.0) sha256=f492094486af02fff4a80070fa6b4d0ebbcf4d42fb38bba29d095eef43f6822c + pjax_rails (0.5.1) sha256=3cb4b53d874de1349014aaae2cb879c8924ec86d68f3054a0d958c995b0c8ddb + pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570 + premailer (1.31.0) sha256=79653fb7920081a5d8e55f25b82838e88f5a385dadee643e2b50a87d88fee273 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e + pry-rails (0.3.11) sha256=a69e28e24a34d75d1f60bcf241192a54253f8f7ef8a62cba1e75750a9653593d + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb + pundit (2.5.2) sha256=e374152baa24f90b630428293faf4b4c5468fc3cc010165f7d8fcb44ce108bbd + pundit-matchers (4.0.0) sha256=59d6077a1d575ea7cceca3ed73df5257488ee1a111ec707b2a797e76908cffd5 + pushover2 (0.1.0) sha256=8c3d9e3953eb89f0e96b52394a7d8d07401f8b8cfb1fce8a29f751ff3d2afe8f + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7 + rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac + rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 + rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 + rack-timeout (0.7.0) sha256=757337e9793cca999bb73a61fe2a7d4280aa9eefbaf787ce3b98d860749c87d9 + rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 + rails (8.1.4) sha256=5f3ecdf9a2a3b79b2e31b8fee3b64a00f7284d76e5631f87d0e58e665911a33b + rails-controller-testing (1.0.5) sha256=741448db59366073e86fc965ba403f881c636b79a2c39a48d0486f2607182e94 + rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d + rails-html-sanitizer (1.7.1) sha256=e797a7c9b01e567307e317c576b49ab4168017e63eea4dba9ce3cb587e2f22c2 + rails-i18n (8.1.0) sha256=52d5fd6c0abef28d84223cc05647f6ae0fd552637a1ede92deee9545755b6cf3 + rails_autolink (1.1.8) sha256=fbcb9d2e5f2cea9435a32e5f12556c7a9b7ed6867889fb9cf5eac405049bfc3a + railties (8.1.4) sha256=b5e7a8e8984e33114d68af62227971960aae1e6c02bd7c9d826ffad97fff583c + rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe + rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e + rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674 + rbtree3 (0.7.1) sha256=ab60ead728a5491b70df4f4065e180b18dbab5319f817ce1dbf5dd906f26d8ba + rdoc (8.1.0) sha256=befc37880a5cfd36dcb176c748b5fd5ee39dd3536a75e81824e4972ce230f60a + regexp_parser (2.13.1) sha256=5aedb6b7c35688f51e86eef17a15374dfd287c83f4ca644b2c5f3ec50a33b44b + reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d + request_store (1.7.0) sha256=e1b75d5346a315f452242a68c937ef8e48b215b9453a77a6c0acdca2934c88cb + responders (3.2.1) sha256=c2772970cbda654c0e772d852e5a851e82cd8e96f5fa958650861ba75b8ffa84 + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rspec-activemodel-mocks (1.3.0) sha256=b51b111b726370a20a2103a8a6851296783a70d2b0bd0287bfa24d293f58d76e + rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d + rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 + rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 + rspec-rails (8.0.4) sha256=06235692fc0892683d3d34977e081db867434b3a24ae0dd0c6f3516bad4e22df + rspec-rebound (0.2.1) sha256=cda576775096339a4a5aec14da064f9ee8572deeb8849b548157c3e050b6cdd1 + rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c + rubocop (1.88.2) sha256=8def251c90cd955feb4daa3edc0ab56893250c4ce90ef81e6c80c03f9a939bbf + rubocop-ast (1.50.0) sha256=b9ca88300da0803ee222ad20cdb30494c0a784eed06fdc35d254b06d662788db + rubocop-capybara (3.0.0) sha256=7a64655238acda7f8f3c87e37ac825a64c615a79c17c253f1a28270dc3768c4b + rubocop-disable_syntax (0.2.0) sha256=1e61645773b3fc2f74e995ec65f605f7db59437c274fdfd4f385f60bf86af73e + rubocop-erb (0.7.1) sha256=17d376b5b9ebb08b75f6bce6bcdc61603b506ef4523473483885d2be88e9099d + rubocop-factory_bot (2.28.0) sha256=4b17fc02124444173317e131759d195b0d762844a71a29fe8139c1105d92f0cb + rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 + rubocop-rails (2.36.0) sha256=105a5f5b0001ff2ef28a3af90da50862464e0775e8d0016d599079c0ca408bdb + rubocop-rake (0.7.1) sha256=3797f2b6810c3e9df7376c26d5f44f3475eda59eb1adc38e6f62ecf027cbae4d + rubocop-rspec (3.10.2) sha256=0b3e2ecc592cd10ecbf0095bb58d1e357905276e069643523cc19eb7495f65e2 + rubocop-rspec_rails (2.32.0) sha256=4a0d641c72f6ebb957534f539d9d0a62c47abd8ce0d0aeee1ef4701e892a9100 + rubocop-thread_safety (0.7.3) sha256=067cdd52fbf5deffc18995437e45b5194236eaff4f71de3375a1f6052e48f431 + ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 + ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef + rubydex (0.4.1) sha256=9856af8bfbe656eed95449dd9e2c8239a74662b4bb8563e4b99221dbc065a34a + rubydex (0.4.1-aarch64-linux) sha256=abc1c9eb22479f60dd015fa81373163c5a71f2700b76bced1282631686bd6271 + rubydex (0.4.1-arm64-darwin) sha256=d23720253876d88fb98954e5d3a1ef9696a269697acf50e3ab53cdfbb2b66b96 + rubydex (0.4.1-x86_64-darwin) sha256=a9681db5bc9be6a62f226d96d6889b6b600d0b09e7576391702aafd29912145b + rubydex (0.4.1-x86_64-linux) sha256=4e2c8398c9fc585485b54edd28f5ecf76b5a02255e177b6f8402e9d2afffd51b + rubyzip (3.7.0) sha256=65c19294da75297a939006f3516deacc33185fbd721ff1954f7a231db6d3e121 + sawyer (0.9.3) sha256=0d0f19298408047037638639fe62f4794483fb04320269169bd41af2bdcf5e41 + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + selenium-webdriver (4.49.0) sha256=b430f091a3cababb356b6f6132e0e4f385b017819355f563557e1537819edc89 + simplecov (1.3.1) sha256=b326d38e819053499aa2ebe35d544d8bd780eba143a50f48a624b450ad87feec + snaky_hash (2.0.7) sha256=7d02c70012a3f932e48860cd024577908300c9aa615e0cb9b450aaa749cbcb4d + sprockets (4.4.1) sha256=fbd665edad0916de5c7abefef0fc616866a60c5e48ce2846f39ac3fc5df9e184 + sprockets-rails (3.5.2) sha256=a9e88e6ce9f8c912d349aa5401509165ec42326baf9e942a85de4b76dbc4119e + ssrf_filter (1.6.0) sha256=78d15bc26d0385c931f46f12a9c1cbb90e6556bd96b95f25e5a5f786c8dcf999 + standard (1.56.0) sha256=ae2af4d9669589162ac69ed5ef59dcf9f346d4afc81f7e62b84339310dfcb787 + standard-custom (1.0.2) sha256=424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b + standard-performance (1.9.0) sha256=49483d31be448292951d80e5e67cdcb576c2502103c7b40aec6f1b6e9c88e3f2 + stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 + super_diff (0.19.0) sha256=c35fc1c0daa223d67b203fe3fb49a6cfd67850a53920319565c3c654e03ec902 + thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 + thruster (0.1.26) sha256=6e45e807086b29d51404841bd1ad493b67cd95892fd65dc5afcdd32e82e94ce8 + thruster (0.1.26-aarch64-linux) sha256=2171cb34928c0250830008f535c4ab2ee57846cc3f5d3e96c3475f7b3de7a541 + thruster (0.1.26-arm64-darwin) sha256=40676164c433abf31313422305d9e9e9210cf941b1befad88e2428b3b8dcc36c + thruster (0.1.26-x86_64-darwin) sha256=c7f7f0cbefd8030ea03bf88f6a988f595a0618a35a1a0d358e729c6898eb5b6a + thruster (0.1.26-x86_64-linux) sha256=3117a6ee430663f845a0457699fe9a05232dcc6c396e2cc83504de5a223c60e8 + timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + underscore-rails (1.8.3) sha256=d37d0a297df9078a27e00df6bfa3f3b110eb27dd8f84c1588a9727a2cfbada47 + unicode-display_width (3.3.0) sha256=4b7aa66a4b11db50f6f7e98411215cd0dd10eaecb665d36eec5bcacb3fa0b613 + unicode-emoji (4.3.0) sha256=11c02fa73290378c066bb0562cd4c87d8e1b706fbbe0059ca12746a4244de8ce + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 + vcr (6.4.0) sha256=077ac92cc16efc5904eb90492a18153b5e6ca5398046d8a249a7c96a9ea24ae6 + version_gem (1.1.15) sha256=a73241587b29252e3567e0c818ded80730eb754d43b508f6ce4db22ca9ba27d0 + warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0 + webmock (3.26.4) sha256=8d8da206d217ebe6968cfb09c77f4533c23074e1432bad865f3994eacbaad50d + websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737 + websocket-driver (0.8.2) sha256=97c556b019bf3410b4961002ac501621e9322d3f8a7bc02161a09301cc4c4146 + websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 + xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e + zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5 + +RUBY VERSION + ruby 4.0.7 BUNDLED WITH - 2.1.2 + 4.0.22 diff --git a/LICENSE b/LICENSE index a033bf5b75..356b875794 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013 errbit team +Copyright (c) 2010-2026 Errbit Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/Procfile b/Procfile index e91374b975..0ed27b44b1 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: bundle exec puma -C config/puma.default.rb +web: bin/rails server diff --git a/README.md b/README.md index ae45ce70c8..d393309e6d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,16 @@ -# Errbit [![TravisCI][travis-img-url]][travis-ci-url] [![Code Climate][codeclimate-img-url]][codeclimate-url] [![Coveralls][coveralls-img-url]][coveralls-url] [![Deploy](https://www.herokucdn.com/deploy/button.svg)][heroku-deploy-url] +# Errbit -[travis-img-url]: https://travis-ci.org/errbit/errbit.svg?branch=master -[travis-ci-url]: http://travis-ci.org/errbit/errbit -[codeclimate-img-url]: https://codeclimate.com/github/errbit/errbit.png -[codeclimate-url]: https://codeclimate.com/github/errbit/errbit -[coveralls-img-url]: https://coveralls.io/repos/github/errbit/errbit/badge.png?branch=master -[coveralls-url]: https://coveralls.io/github/errbit/errbit?branch=master -[heroku-deploy-url]:https://heroku.com/deploy?template=https://github.com/errbit/errbit/tree/master +[![RSpec](https://github.com/errbit/errbit/actions/workflows/rspec.yml/badge.svg)](https://github.com/errbit/errbit/actions/workflows/rspec.yml) +[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/standardrb/standard) -### The open source, self-hosted error catcher +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/errbit/errbit/tree/main) + +## The open source, self-hosted error catcher Errbit is a tool for collecting and managing errors from other applications. -It is [Airbrake](http://airbrake.io) API compliant, so if you are already using -Airbrake, you can just point the `airbrake` gem to your Errbit server. +It is [Airbrake](https://www.airbrake.io) API compliant, so you can just +point the `airbrake` gem to your Errbit server (see +[howto](app/views/apps/_configuration_instructions.html.erb)). @@ -20,48 +18,46 @@ Airbrake, you can just point the `airbrake` gem to your Errbit server. Apps -
+
Apps
Errors -
+
Errors
Error Summary -
+
Error Summary
Error Backtraces -
+
Error Backtraces
-Mailing List ------------- +## Mailing List -Join the Google Group at https://groups.google.com/group/errbit to receive +Join the [Google Group](https://groups.google.com/group/errbit) to receive updates and notifications. -# Requirements +## Requirements The list of requirements to install Errbit are: -* Ruby >= 2.3.x -* MongoDB 3.4.x-4.0.x +* Ruby 4.0 +* MongoDB >= 7.0 -Installation ------------- +## Installation *Note*: This app is intended for people with experience deploying and maintaining Rails applications. @@ -69,54 +65,56 @@ Rails applications. * [Install MongoDB](https://www.mongodb.org/downloads) * `git clone https://github.com/errbit/errbit.git` * `bundle install` -* `bundle exec rake errbit:bootstrap` +* `bundle exec rails errbit:bootstrap` * `bundle exec rails server` -Configuration -------------- +## Configuration + Errbit configuration is done entirely through environment variables. See [configuration](docs/configuration.md) -Deployment ----------- +## Deployment + See [notes on deployment](docs/deployment.md) -Notice Grouping ---------------- +## Notice Grouping + The way Errbit arranges notices into error groups is configurable. By default, Errbit uses the notice's error class, error message, complete backtrace, component (or controller), action and environment name to generate a unique -fingerprint for every notice. Notices with identical fingerprints appear in the -UI as different occurences of the same error and notices with differing +fingerprint for every notice. Notices with identical fingerprints appear in +the UI as different occurrences of the same error and notices with differing fingerprints are displayed as separate errors. Changing the fingerprinter (under the "Config" menu) applies to all apps and the change affects only notices that arrive after the change. If you want to -refingerprint old notices, you can run `rake errbit:notice_refingerprint`. +refingerprint old notices, you can run +`bundle exec rails errbit:notice_refingerprint`. Since version 0.7.0, the notice grouping can be separately configured for each app (under the "edit" menu). -Managing apps ---------------------- +## Managing apps + An Errbit app is a place to collect error notifications from your external application deployments. See [apps](docs/apps.md) -Authentication --------------- -### Configuring GitHub authentication: +## Authentication + +### Configuring GitHub authentication + * Set `GITHUB_AUTHENTICATION=true` * Register your instance of Errbit at https://github.com/settings/applications/new -If you host Errbit at errbit.example.com, you would fill in: +If you host Errbit at `errbit.example.com`, you would fill in:
-
URL -
http://errbit.example.com -
Callback URL -
http://errbit.example.com/users/auth/github/callback +
URL
+
https://errbit.example.com
+
Callback URL
+
https://errbit.example.com/users/auth/github/callback
* After you have registered your app, set `GITHUB_CLIENT_ID` and `GITHUB_SECRET` @@ -132,35 +130,34 @@ create issues on GitHub. If you use another issue tracker, see [Issue Trackers](#issue-trackers). You can change the OAuth scope Errbit requests from GitHub by setting -`GITHUB_ACCESS_SCOPE`. The default ['repo'] is very permissive, but there are a +`GITHUB_ACCESS_SCOPE`. The default `['repo']` is very permissive, but there are a few others that could make sense for your needs:
-
GITHUB_ACCESS_SCOPE="['repo']"
-
Allow creating issues for public and private repos
-
GITHUB_ACCESS_SCOPE="['public_repo']"
-
Allow creating issues for public repos only
-
GITHUB_ACCESS_SCOPE="[]"
-
No permissions at all, but allows errbit login through github
+
GITHUB_ACCESS_SCOPE="['repo']"
+
Allow creating issues for public and private repos
+
GITHUB_ACCESS_SCOPE="['public_repo']"
+
Allow creating issues for public repos only
+
GITHUB_ACCESS_SCOPE="[]"
+
No permissions at all, but allows Errbit login through GitHub
* `GITHUB_ORG_ID` is an optional environment variable you can set to your own - github organization id. If set, only users of the specified GitHub + GitHub organization id. If set, only users of the specified GitHub organization can log in to Errbit through GitHub. Errbit will provision accounts for new users. -### Configuring Google authentication: -* Set GOOGLE_AUTHENTICATION=true +### Configuring Google authentication + +* Set `GOOGLE_AUTHENTICATION=true` * Register your instance of Errbit at https://console.developers.google.com/apis/api/plus/overview -If you host Errbit at errbit.example.com, you would fill in: +If you host Errbit at `errbit.example.com`, you would fill in: -
-
URL -
http://errbit.example.com -
Callback URL -
http://errbit.example.com/users/auth/google_oauth2/callback -
+| Parameter | Value | +|--------------|----------------------------------------------------------------| +| URL | `https://errbit.example.com` | +| Callback URL | `https://errbit.example.com/users/auth/google_oauth2/callback` | * After you have registered your app, set `GOOGLE_CLIENT_ID` and `GOOGLE_SECRET` with your app's Client ID and Secret key. @@ -169,33 +166,34 @@ When you start your application, you should see the option to **Sign in with Google** on the Login page. You will also be able to link your Google profile to your user account on your **Edit profile** page. -### Configuring LDAP authentication: +### Configuring LDAP authentication * Set `ERRBIT_USER_HAS_USERNAME=true` * Follow the [devise_ldap_authenticatable setup instructions](https://github.com/cschiewek/devise_ldap_authenticatable). -* Set ```config.ldap_create_user = true``` in ```config/initializers/devise.rb```, this enables creating the users from LDAP, otherwhise login will not work. -* Create a new initializer (e.g. ```config/initializers/devise_ldap.rb```) and add the following code to enable ldap authentication in the User-model: +* Set `config.ldap_create_user = true` in `config/initializers/devise.rb`, this enables creating the users from LDAP, otherwise login will not work. +* Create a new initializer (e.g. `config/initializers/devise_ldap.rb`) and add the following code to enable ldap authentication in the User-model: ```ruby Errbit::Config.devise_modules << :ldap_authenticatable ``` -* If you are authenticating by `username`, you will need to set the user's email manually -before authentication. You must add the following lines to `app/models/user.rb`: +* If you are authenticating by `username`, you will need to set the user's + email manually before authentication. You must add the following lines to + `app/models/user.rb`: ```ruby - def ldap_before_save - name = Devise::LDAP::Adapter.get_ldap_param(self.username, "givenName") - surname = Devise::LDAP::Adapter.get_ldap_param(self.username, "sn") - mail = Devise::LDAP::Adapter.get_ldap_param(self.username, "mail") - - self.name = (name + surname).join ' ' - self.email = mail.first - end +def ldap_before_save + name = Devise::LDAP::Adapter.get_ldap_param(self.username, "givenName") + surname = Devise::LDAP::Adapter.get_ldap_param(self.username, "sn") + mail = Devise::LDAP::Adapter.get_ldap_param(self.username, "mail") + + self.name = (name + surname).join(" ") + self.email = mail.first +end ``` * Now login with your user from LDAP, this will create a user in the database -* Open a rails console and set the admin flag for your user: +* Open a `bundle exec rails console` and set the admin flag for your user: ```ruby user = User.first @@ -207,13 +205,13 @@ user.save! When upgrading Errbit, please run: -```bash -git pull origin master # assuming origin is the github.com/errbit/errbit repo +```shell +git pull origin main # assuming origin is the github.com/errbit/errbit repo bundle install -rake db:migrate -rake db:mongoid:remove_undefined_indexes -rake db:mongoid:create_indexes -rake assets:precompile +bundle exec rails db:migrate +bundle exec rails db:mongoid:remove_undefined_indexes +bundle exec rails db:mongoid:create_indexes +bundle exec rails assets:precompile ``` This will ensure that your application stays up to date with any schema changes. @@ -226,7 +224,9 @@ Errbit can now display information about the user who experienced an error. This gives you the ability to ask the user for more information, and let them know when you've fixed the bug. -The Airbrake gem will look for ```current_user``` or ```current_member```. By default it will only send the ```id``` of the user, to specify other attributes you can set ```config.user_attributes```. See [the Airbrake wiki for more information](https://github.com/airbrake/airbrake/wiki/Sending-current-user-information). +The Airbrake gem will look for `current_user` or `current_member`. By default, +it will only send the `id` of the user, to specify other attributes you can +set `config.user_attributes`. See [the Airbrake wiki for more information](https://github.com/airbrake/airbrake/wiki/Sending-current-user-information). If user information is received with an error report, it will be displayed under the *User Details* tab: @@ -235,8 +235,12 @@ it will be displayed under the *User Details* tab: This tab will be hidden if no user information is available. -Javascript error notifications --------------------------------------- +## Configuration + +https://github.com/airbrake/airbrake + +## Javascript error notifications + You can log javascript errors that occur in your application by including the [airbrake-js](https://github.com/airbrake/airbrake-js) javascript library. @@ -246,30 +250,30 @@ documentation at https://github.com/airbrake/airbrake-js/blob/master/README.md ```javascript var airbrake = new airbrakeJs.Client({ - projectId: 'ERRBIT API KEY', - projectKey: 'ERRBIT API KEY (again)', - reporter: 'xhr', - host: 'https://myerrbit.com' + projectId: "ERRBIT API KEY", + projectKey: "ERRBIT API KEY (again)", + reporter: "xhr", + remoteConfig: false, + host: "https://errbit.example.com" }); ``` -Plugins and Integrations ------------------------- +## Plugins and Integrations + You can extend Errbit by adding Ruby gems and plugins which are typically gems. It's nice to keep track of which gems are core Errbit dependencies and which gems are your own dependencies. If you want to add gems to your own Errbit, place them in a new file called `UserGemfile` and Errbit will treat that file -as an additional Gemfile. If you want to use a file with a different name, you -can pass the name of that file in an environment variable named `USER_GEMFILE`. -If you want to use errbit_jira_plugin, just add it to UserGemfile: +as an additional Gemfile. If you want to use `errbit_jira_plugin`, just add it +to `UserGemfile`: -```bash +```shell echo "gem 'errbit_jira_plugin'" > UserGemfile bundle install ``` -Issue Trackers --------------- +## Issue Trackers + Each issue tracker integration is implemented as a gem that depends on [errbit_plugin](https://github.com/errbit/errbit_plugin). The only officially supported issue tracker plugin is @@ -278,10 +282,9 @@ supported issue tracker plugin is If you want to implement your own issue tracker plugin, read the README.md file at [errbit_plugin](https://github.com/errbit/errbit_plugin). -What if Errbit has an error? ----------------------------- +## If Errbit has an error -Errbit will log it's own errors to an internal app named **Self.Errbit**. The +Errbit will log its own errors to an internal app named **Self.Errbit**. The **Self.Errbit** app is automatically created when the first error happens. If your Errbit instance has logged an error, we would appreciate a bug report @@ -289,40 +292,38 @@ on GitHub Issues. You can post this manually at [https://github.com/errbit/errbit/issues](https://github.com/errbit/errbit/issues), or you can set up the GitHub Issues tracker for your **Self.Errbit** app: - * Go to the **Self.Errbit** app's edit page. If that app does not exist yet, - go to the apps page and click **Add a new App** to create it. (You can also - create it by running `rake airbrake:test`.) - * In the **Issue Tracker** section, click **GitHub Issues**. - * Fill in the **Account/Repository** field with **errbit/errbit**. - * Fill in the **Username** field with your github username. - * If you are logged in on [GitHub](https://github.com), you can find your - **API Token** on this page: - [https://github.com/account/admin](https://github.com/account/admin). - * Save the settings by clicking **Update App** (or **Add App**) - * You can now easily post bug reports to GitHub Issues by clicking the - **Create Issue** button on a **Self.Errbit** error. - -Getting Help ------------- +* Go to the **Self.Errbit** app's edit page. If that app does not exist yet, + go to the apps page and click **Add a new App** to create it. (You can also + create it by running `bundle exec rails airbrake:test`.) +* In the **Issue Tracker** section, click **GitHub Issues**. +* Fill in the **Account/Repository** field with **errbit/errbit**. +* Fill in the **Username** field with your GitHub username. +* If you are logged in on [GitHub](https://github.com), you can find your + **API Token** on this page: + [https://github.com/account/admin](https://github.com/account/admin). +* Save the settings by clicking **Update App** (or **Add App**) +* You can now easily post bug reports to GitHub Issues by clicking the + **Create Issue** button on a **Self.Errbit** error. + +## Getting Help If you need help, try asking your question on StackOverflow using the tag errbit: https://stackoverflow.com/questions/tagged/errbit -Use Errbit with applications written in other languages -------------------------------------------------------- +## Use Errbit with applications written in other languages In theory, any Airbrake-compatible error catcher for other languages should work with Errbit. Solutions known to work are listed below: -| Language | Project | -|----------|---------| -| PHP (>= 5.3) | [wasilak/errbit-php](https://github.com/wasilak/errbit-php) | -| OOP PHP (>= 5.3) | [emgiezet/errbitPHP](https://github.com/emgiezet/errbitPHP) | -| Python | [mkorenkov/errbit.py](https://github.com/mkorenkov/errbit.py) , [pulseenergy/airbrakepy](https://github.com/pulseenergy/airbrakepy) | +| Language | Project | +|---------------------|---------------------------------------------------------------------| +| PHP (>= 5.3) | [wasilak/errbit-php](https://github.com/wasilak/errbit-php) | +| OOP PHP (>= 5.3) | [emgiezet/errbitPHP](https://github.com/emgiezet/errbitPHP) | +| Python | [mkorenkov/errbit.py](https://github.com/mkorenkov/errbit.py) | +| Python | [pulseenergy/airbrakepy](https://github.com/pulseenergy/airbrakepy) | -People using Errbit -------------------- +## People using Errbit See our wiki page for a [list of people and companies around the world who use Errbit](https://github.com/errbit/errbit/wiki/People-using-Errbit). You may @@ -330,9 +331,7 @@ Errbit](https://github.com/errbit/errbit/wiki/People-using-Errbit). You may page](https://github.com/errbit/errbit/wiki/People-using-Errbit/_edit), and add your name and country to the list if you are using Errbit. - -Special Thanks --------------- +## Special Thanks * [Michael Parenteau](http://michaelparenteau.com) - For rocking the Errbit design and providing a great user experience. * [Nick Recobra (@oruen)](https://github.com/oruen) - Nick is Errbit's first core contributor. He's been working hard at making Errbit more awesome. @@ -341,21 +340,22 @@ Special Thanks * [Marcin Ciunelis (@martinciu)](https://github.com/martinciu) - Helping to improve Errbit's architecture * [Cyril Mougel (@shingara)](https://github.com/shingara) - Maintaining Errbit and contributing many features * [Relevance](http://thinkrelevance.com) - For giving me Open-source Fridays to work on Errbit and all my awesome co-workers for giving feedback and inspiration. -* [Thoughtbot](http://thoughtbot.com) - For being great open-source advocates and setting the bar with [Airbrake](http://airbrake.io). +* [Thoughtbot](https://thoughtbot.com) - For being great open-source advocates and setting the bar with [Airbrake](https://www.airbrake.io). See the [contributors graph](https://github.com/errbit/errbit/graphs/contributors) for more details. -Contributing to Errbit ------------- +## Contributing to Errbit See the [contribution guidelines](CONTRIBUTING.md) -Running tests -------------- +## Running tests + +Check the [.github/workflows/rspec.yml](.github/workflows/rspec.yml) file to see how tests are run + +## License -Check the [.travis.yml](.travis.yml) file to see how tests are run +Errbit is available as open source under the terms of the [MIT License](https://opensource.org/license/MIT). -Copyright ---------- +## Copyright -Copyright (c) 2010-2015 Errbit Team +Copyright © 2010-2026 Errbit Team diff --git a/RELEASE.md b/RELEASE.md index e4144f88bf..62446a001a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,7 +1,7 @@ # Releasing Errbit -1. Ensure a GitHub mileston exists and that it is populated with all the substantive changes since the previous milestone. -2. Ensure the current milestone is complete and create a next mileston in GitHub. -3. Update the Errbit::VERSION constant with a new git commit. +1. Ensure a GitHub milestone exists and that it is populated with all the substantive changes since the previous milestone. +2. Ensure the current milestone is complete and create a next milestone in GitHub. +3. Update the `Errbit::VERSION` constant with a new git commit. 4. Create and sign a git tag named v pointing to the release commit and push it to GitHub. 5. Announce the new version on the mailing list. diff --git a/Rakefile b/Rakefile index fa6762643c..d2a78aa258 100644 --- a/Rakefile +++ b/Rakefile @@ -1,21 +1,8 @@ +# frozen_string_literal: true + # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -# These rake tasks should always be run in the 'test' environment and the -# environment name must be set before loading the Rails application -test_env_tasks = %w(default spec) -if Rake.application.top_level_tasks.any? { |t| test_env_tasks.include?(t) } - ENV['RAILS_ENV'] = 'test' -end - -require File.expand_path('../config/application', __FILE__) +require_relative "config/application" Rails.application.load_tasks - -begin - require 'rspec/core/rake_task' - RSpec::Core::RakeTask.new(:spec) - task default: :spec -rescue LoadError - warn "Notice: no rspec tasks available in this environment" -end diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..83246effd5 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,18 @@ +# Errbit Security Policy + +## Supported Versions + +We release patches for security vulnerabilities against the following versions: + +| Version | Supported | +|---------|--------------------| +| main | :white_check_mark: | +| v0.11.x | :white_check_mark: | +| v0.10.x | :x: | + +## Reporting a Vulnerability + +Please report security vulnerabilities via +[GitHub Security Advisories](https://github.com/errbit/errbit/security/advisories/new). + +If you cannot use GitHub, email igor.zubkov@gmail.com. Please do not open a public issue. You should receive a response within 7 days. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days. diff --git a/UserGemfile b/UserGemfile new file mode 100644 index 0000000000..8e9b8f90fa --- /dev/null +++ b/UserGemfile @@ -0,0 +1 @@ +# frozen_string_literal: true diff --git a/app.json b/app.json index 25f54b1a8d..dce7403a53 100644 --- a/app.json +++ b/app.json @@ -2,11 +2,14 @@ "name": "errbit", "description": "The open source error catcher that's Airbrake API compliant", "keywords": ["productivity", "tools", "error tracking"], + "website": "https://errbit.com", "repository": "https://github.com/errbit/errbit", "env": { - "BUNDLE_WITHOUT": "development", - "ERRBIT_ENFORCE_SSL": "true", - "GEMFILE_RUBY_VERSION": "2.5.1", + "BUNDLE_WITHOUT": "development:test", + "MONGODB_URI": { + "description": "The connection URI to your MongoDB Cluster. If you don't have one, you can create a free plan at https://cloud.mongodb.com . Follow the Heroku deployment docs on the Errbit repo for more information on obtaining this string.", + "required": true + }, "SECRET_KEY_BASE": { "description": "A secret key for verifying the integrity of signed cookies.", "generator": "secret" @@ -19,11 +22,7 @@ "SMTP_SERVER": "smtp.sendgrid.net" }, "scripts": { - "postdeploy": "bundle exec rake errbit:bootstrap errbit:demo" + "postdeploy": "bundle exec rails errbit:bootstrap" }, - "addons": [ - "mongolab:sandbox", - "scheduler:standard", - "sendgrid:starter" - ] + "addons": ["scheduler:standard", "sendgrid:starter"] } diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 0000000000..c2bab35ad8 --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,6 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css +//= link mailers/mailer.css +//= link_tree ../../javascript .js +//= link_tree ../../../vendor/javascript .js diff --git a/app/assets/images/flowdock_create.png b/app/assets/images/flowdock_create.png deleted file mode 100644 index a18c89ca72..0000000000 Binary files a/app/assets/images/flowdock_create.png and /dev/null differ diff --git a/app/assets/images/flowdock_goto.png b/app/assets/images/flowdock_goto.png deleted file mode 100644 index a18c89ca72..0000000000 Binary files a/app/assets/images/flowdock_goto.png and /dev/null differ diff --git a/app/assets/images/flowdock_inactive.png b/app/assets/images/flowdock_inactive.png deleted file mode 100644 index e670234e7f..0000000000 Binary files a/app/assets/images/flowdock_inactive.png and /dev/null differ diff --git a/app/assets/images/gtalk_create.png b/app/assets/images/gtalk_create.png deleted file mode 100644 index 6228e3db15..0000000000 Binary files a/app/assets/images/gtalk_create.png and /dev/null differ diff --git a/app/assets/images/gtalk_goto.png b/app/assets/images/gtalk_goto.png deleted file mode 100644 index 6228e3db15..0000000000 Binary files a/app/assets/images/gtalk_goto.png and /dev/null differ diff --git a/app/assets/images/gtalk_inactive.png b/app/assets/images/gtalk_inactive.png deleted file mode 100644 index 30faee1cb2..0000000000 Binary files a/app/assets/images/gtalk_inactive.png and /dev/null differ diff --git a/app/assets/images/hipchat_create.png b/app/assets/images/hipchat_create.png deleted file mode 100644 index 962d22547b..0000000000 Binary files a/app/assets/images/hipchat_create.png and /dev/null differ diff --git a/app/assets/images/hipchat_goto.png b/app/assets/images/hipchat_goto.png deleted file mode 100644 index 962d22547b..0000000000 Binary files a/app/assets/images/hipchat_goto.png and /dev/null differ diff --git a/app/assets/images/hipchat_inactive.png b/app/assets/images/hipchat_inactive.png deleted file mode 100644 index 887d2bb6c4..0000000000 Binary files a/app/assets/images/hipchat_inactive.png and /dev/null differ diff --git a/app/assets/images/icons/arrow-repeat.svg b/app/assets/images/icons/arrow-repeat.svg new file mode 100644 index 0000000000..b17dba47be --- /dev/null +++ b/app/assets/images/icons/arrow-repeat.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/images/icons/check-circle.svg b/app/assets/images/icons/check-circle.svg new file mode 100644 index 0000000000..016f6072a0 --- /dev/null +++ b/app/assets/images/icons/check-circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/images/icons/floppy.svg b/app/assets/images/icons/floppy.svg new file mode 100644 index 0000000000..65ae562d06 --- /dev/null +++ b/app/assets/images/icons/floppy.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/images/icons/github.svg b/app/assets/images/icons/github.svg new file mode 100644 index 0000000000..013e025322 --- /dev/null +++ b/app/assets/images/icons/github.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/assets/images/icons/google.svg b/app/assets/images/icons/google.svg new file mode 100644 index 0000000000..9f603de0b0 --- /dev/null +++ b/app/assets/images/icons/google.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/assets/images/icons/hand-thumbs-up-fill.svg b/app/assets/images/icons/hand-thumbs-up-fill.svg new file mode 100644 index 0000000000..c68bc07ffd --- /dev/null +++ b/app/assets/images/icons/hand-thumbs-up-fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/assets/images/icons/pencil-square.svg b/app/assets/images/icons/pencil-square.svg new file mode 100644 index 0000000000..95c0529759 --- /dev/null +++ b/app/assets/images/icons/pencil-square.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/images/icons/person-fill-add.svg b/app/assets/images/icons/person-fill-add.svg new file mode 100644 index 0000000000..d6d15f97eb --- /dev/null +++ b/app/assets/images/icons/person-fill-add.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/images/icons/plus-circle.svg b/app/assets/images/icons/plus-circle.svg new file mode 100644 index 0000000000..2832373563 --- /dev/null +++ b/app/assets/images/icons/plus-circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/images/icons/trash.svg b/app/assets/images/icons/trash.svg new file mode 100644 index 0000000000..3020264c97 --- /dev/null +++ b/app/assets/images/icons/trash.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/images/icons/x-circle.svg b/app/assets/images/icons/x-circle.svg new file mode 100644 index 0000000000..0e8c641ace --- /dev/null +++ b/app/assets/images/icons/x-circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/app.js.erb similarity index 93% rename from app/assets/javascripts/application.js.erb rename to app/assets/javascripts/app.js.erb index a54dab3f2a..fe9cc34016 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/app.js.erb @@ -4,7 +4,6 @@ //= require form //= require jquery.pjax //= require errbit -//= require apps.show //= require_self // Allow any gems named 'errbit_*' to require their own assets diff --git a/app/assets/javascripts/apps.show.js b/app/assets/javascripts/apps.show.js deleted file mode 100644 index ee5e277766..0000000000 --- a/app/assets/javascripts/apps.show.js +++ /dev/null @@ -1,11 +0,0 @@ -$(function() { - - $("#watchers_toggle").click(function() { - $("#watchers_div").slideToggle("slow"); - }); - - $("#repository_toggle").click(function() { - $("#repository_div").slideToggle("slow"); - }); - -}); diff --git a/app/assets/javascripts/errbit.js b/app/assets/javascripts/errbit.js index ad47178ff9..4e9494712a 100644 --- a/app/assets/javascripts/errbit.js +++ b/app/assets/javascripts/errbit.js @@ -16,7 +16,7 @@ $(function() { // On page apps/:app_id/edit $('a.copy_config').on("click", function() { - $('select.choose_other_app').show().focus(); + $('select.choose_other_app').removeClass('hidden').focus(); }); $('select.choose_other_app').on("change", function() { @@ -31,20 +31,33 @@ $(function() { $.pjax.defaults = {timeout: 2000}; $('#content').pjax('.notice-pagination a').on('pjax:start', function() { - $('.notice-pagination-loader').css("visibility", "visible"); + $('.notice-pagination-loader').addClass('visible'); currentTab = $('.tab-bar ul li a.button.active').attr('rel'); }).on('pjax:end', function() { activateTabbedPanels(); + loadSparklines(); }); }); } + $(document).on('ajax:success', 'form.search-form', function(_event, data) { + var form = $(this); + var target = $(form.data('search-target')); + + target.html(data); + $('#flash-messages').empty(); + if (target.attr('id') == 'problem_table') { + toggleProblemsCheckboxes(); + bindProblemButtonsActions(); + } + }); + function activateTabbedPanels() { $('.tab-bar a').each(function(){ var tab = $(this); var panel = $('#'+tab.attr('rel')); panel.addClass('panel'); - panel.find('h3').hide(); + panel.find('h3').addClass('hidden'); }); $('.tab-bar a').click(function(){ @@ -64,8 +77,8 @@ $(function() { // If clicking into 'backtrace' tab, hide external backtrace if (tab.attr('rel') == "backtrace") { hide_external_backtrace(); } - $('.panel').hide(); - panel.show(); + $('.panel').addClass('hidden'); + panel.removeClass('hidden'); } window.toggleProblemsCheckboxes = function() { @@ -112,22 +125,39 @@ $(function() { toggleRequiredPasswordMarks(); function hide_external_backtrace() { - $('tr.toggle_external_backtrace').hide(); - $('td.backtrace_separator').show(); + $('tr.toggle_external_backtrace').addClass('hidden_external_backtrace'); + $('td.backtrace_separator').removeClass('hidden'); } function show_external_backtrace() { - $('tr.toggle_external_backtrace').show(); - $('td.backtrace_separator').hide(); + $('tr.toggle_external_backtrace').removeClass('hidden_external_backtrace'); + $('td.backtrace_separator').addClass('hidden'); } // Show external backtrace lines when clicking separator - $('td.backtrace_separator span').on('click', show_external_backtrace); + $(document).on('click', 'td.backtrace_separator span', show_external_backtrace); // Hide external backtrace on page load hide_external_backtrace(); $('.head a.show_tail').click(function(e) { - $(this).hide().closest('.head_and_tail').find('.tail').show(); + $(this).addClass('hidden').closest('.head_and_tail').find('.tail').removeClass('hidden'); e.preventDefault(); }); + function loadSparklines() { + $('#sparkline-placeholder[data-sparkline-url]').each(function() { + var placeholder = $(this); + $.ajax({url: placeholder.data('sparkline-url')}).then(function(response) { + placeholder.replaceWith(response); + }); + }); + } + + $('a#forgot_password').click(function(e) { + // Set email field on password reset page to email that user entered on this page. + e.preventDefault(); + window.location.href = $(this).attr('href') + '?email=' + encodeURIComponent($('#user_email').val()); + }); + + loadSparklines(); + init(); }); diff --git a/app/assets/javascripts/form.js b/app/assets/javascripts/form.js index 3646e69c5f..255b59758c 100644 --- a/app/assets/javascripts/form.js +++ b/app/assets/javascripts/form.js @@ -36,14 +36,14 @@ function makeNestedItemsDestroyable(wrapper) { wrapper.find('.nested').each(function(){ var nestedItem = $(this); var destroyLink = $('').text('remove').addClass('remove-nested'); - destroyLink.css('float','right'); + destroyLink.addClass('float-right'); nestedItem.find('label').first().before(destroyLink); }) } function appendNestedItem() { var addLink = $(this); - var nestedItem = addLink.parent().find('.nested').first().clone().show(); + var nestedItem = addLink.parent().find('.nested').first().clone().removeClass('hidden'); var timestamp = new Date(); timestamp = timestamp.valueOf(); @@ -71,7 +71,7 @@ function removeNestedItem() { var destroyFlag = $('').attr('name',destroyFlagName).attr('type','hidden').val('true'); $("input[name='"+idFieldName+"']").after(destroyFlag); } - nestedItem.hide(); + nestedItem.addClass('hidden'); } @@ -102,13 +102,13 @@ function activateCheckboxHooks() { $('input[type="checkbox"][data-hide-when-checked]').each(function(){ $(this).change(function(){ var el = $($(this).data('hide-when-checked')); - el.toggle(!$(this).is(':checked')); + el.toggleClass('hidden', $(this).is(':checked')); }); }); $('input[type="checkbox"][data-show-when-checked]').each(function(){ $(this).change(function(){ var el = $($(this).data('show-when-checked')); - el.toggle($(this).is(':checked')); + el.toggleClass('hidden', !$(this).is(':checked')); }); }); } @@ -124,4 +124,3 @@ function activateLabelIcons() { }); }; }; - diff --git a/app/assets/stylesheets/application.css.erb b/app/assets/stylesheets/application.css similarity index 85% rename from app/assets/stylesheets/application.css.erb rename to app/assets/stylesheets/application.css index 5c0b52eeea..773c280f4d 100644 --- a/app/assets/stylesheets/application.css.erb +++ b/app/assets/stylesheets/application.css @@ -3,5 +3,6 @@ *= require errbit *= require notification_service_icons *= require font-awesome + *= require tailwind *= require_self */ diff --git a/app/assets/stylesheets/bootstrap.css b/app/assets/stylesheets/bootstrap.css new file mode 100644 index 0000000000..b83f5079f2 --- /dev/null +++ b/app/assets/stylesheets/bootstrap.css @@ -0,0 +1,12048 @@ +@charset "UTF-8"; +/*! + * Bootstrap v5.3.8 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +:root, +[data-bs-theme=light] { + --bs-blue: #0d6efd; + --bs-indigo: #6610f2; + --bs-purple: #6f42c1; + --bs-pink: #d63384; + --bs-red: #dc3545; + --bs-orange: #fd7e14; + --bs-yellow: #ffc107; + --bs-green: #198754; + --bs-teal: #20c997; + --bs-cyan: #0dcaf0; + --bs-black: #000; + --bs-white: #fff; + --bs-gray: #6c757d; + --bs-gray-dark: #343a40; + --bs-gray-100: #f8f9fa; + --bs-gray-200: #e9ecef; + --bs-gray-300: #dee2e6; + --bs-gray-400: #ced4da; + --bs-gray-500: #adb5bd; + --bs-gray-600: #6c757d; + --bs-gray-700: #495057; + --bs-gray-800: #343a40; + --bs-gray-900: #212529; + --bs-primary: #0d6efd; + --bs-secondary: #6c757d; + --bs-success: #198754; + --bs-info: #0dcaf0; + --bs-warning: #ffc107; + --bs-danger: #dc3545; + --bs-light: #f8f9fa; + --bs-dark: #212529; + --bs-primary-rgb: 13, 110, 253; + --bs-secondary-rgb: 108, 117, 125; + --bs-success-rgb: 25, 135, 84; + --bs-info-rgb: 13, 202, 240; + --bs-warning-rgb: 255, 193, 7; + --bs-danger-rgb: 220, 53, 69; + --bs-light-rgb: 248, 249, 250; + --bs-dark-rgb: 33, 37, 41; + --bs-primary-text-emphasis: #052c65; + --bs-secondary-text-emphasis: #2b2f32; + --bs-success-text-emphasis: #0a3622; + --bs-info-text-emphasis: #055160; + --bs-warning-text-emphasis: #664d03; + --bs-danger-text-emphasis: #58151c; + --bs-light-text-emphasis: #495057; + --bs-dark-text-emphasis: #495057; + --bs-primary-bg-subtle: #cfe2ff; + --bs-secondary-bg-subtle: #e2e3e5; + --bs-success-bg-subtle: #d1e7dd; + --bs-info-bg-subtle: #cff4fc; + --bs-warning-bg-subtle: #fff3cd; + --bs-danger-bg-subtle: #f8d7da; + --bs-light-bg-subtle: #fcfcfd; + --bs-dark-bg-subtle: #ced4da; + --bs-primary-border-subtle: #9ec5fe; + --bs-secondary-border-subtle: #c4c8cb; + --bs-success-border-subtle: #a3cfbb; + --bs-info-border-subtle: #9eeaf9; + --bs-warning-border-subtle: #ffe69c; + --bs-danger-border-subtle: #f1aeb5; + --bs-light-border-subtle: #e9ecef; + --bs-dark-border-subtle: #adb5bd; + --bs-white-rgb: 255, 255, 255; + --bs-black-rgb: 0, 0, 0; + --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); + --bs-body-font-family: var(--bs-font-sans-serif); + --bs-body-font-size: 1rem; + --bs-body-font-weight: 400; + --bs-body-line-height: 1.5; + --bs-body-color: #212529; + --bs-body-color-rgb: 33, 37, 41; + --bs-body-bg: #fff; + --bs-body-bg-rgb: 255, 255, 255; + --bs-emphasis-color: #000; + --bs-emphasis-color-rgb: 0, 0, 0; + --bs-secondary-color: rgba(33, 37, 41, 0.75); + --bs-secondary-color-rgb: 33, 37, 41; + --bs-secondary-bg: #e9ecef; + --bs-secondary-bg-rgb: 233, 236, 239; + --bs-tertiary-color: rgba(33, 37, 41, 0.5); + --bs-tertiary-color-rgb: 33, 37, 41; + --bs-tertiary-bg: #f8f9fa; + --bs-tertiary-bg-rgb: 248, 249, 250; + --bs-heading-color: inherit; + --bs-link-color: #0d6efd; + --bs-link-color-rgb: 13, 110, 253; + --bs-link-decoration: underline; + --bs-link-hover-color: #0a58ca; + --bs-link-hover-color-rgb: 10, 88, 202; + --bs-code-color: #d63384; + --bs-highlight-color: #212529; + --bs-highlight-bg: #fff3cd; + --bs-border-width: 1px; + --bs-border-style: solid; + --bs-border-color: #dee2e6; + --bs-border-color-translucent: rgba(0, 0, 0, 0.175); + --bs-border-radius: 0.375rem; + --bs-border-radius-sm: 0.25rem; + --bs-border-radius-lg: 0.5rem; + --bs-border-radius-xl: 1rem; + --bs-border-radius-xxl: 2rem; + --bs-border-radius-2xl: var(--bs-border-radius-xxl); + --bs-border-radius-pill: 50rem; + --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); + --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); + --bs-focus-ring-width: 0.25rem; + --bs-focus-ring-opacity: 0.25; + --bs-focus-ring-color: rgba(13, 110, 253, 0.25); + --bs-form-valid-color: #198754; + --bs-form-valid-border-color: #198754; + --bs-form-invalid-color: #dc3545; + --bs-form-invalid-border-color: #dc3545; +} + +[data-bs-theme=dark] { + color-scheme: dark; + --bs-body-color: #dee2e6; + --bs-body-color-rgb: 222, 226, 230; + --bs-body-bg: #212529; + --bs-body-bg-rgb: 33, 37, 41; + --bs-emphasis-color: #fff; + --bs-emphasis-color-rgb: 255, 255, 255; + --bs-secondary-color: rgba(222, 226, 230, 0.75); + --bs-secondary-color-rgb: 222, 226, 230; + --bs-secondary-bg: #343a40; + --bs-secondary-bg-rgb: 52, 58, 64; + --bs-tertiary-color: rgba(222, 226, 230, 0.5); + --bs-tertiary-color-rgb: 222, 226, 230; + --bs-tertiary-bg: #2b3035; + --bs-tertiary-bg-rgb: 43, 48, 53; + --bs-primary-text-emphasis: #6ea8fe; + --bs-secondary-text-emphasis: #a7acb1; + --bs-success-text-emphasis: #75b798; + --bs-info-text-emphasis: #6edff6; + --bs-warning-text-emphasis: #ffda6a; + --bs-danger-text-emphasis: #ea868f; + --bs-light-text-emphasis: #f8f9fa; + --bs-dark-text-emphasis: #dee2e6; + --bs-primary-bg-subtle: #031633; + --bs-secondary-bg-subtle: #161719; + --bs-success-bg-subtle: #051b11; + --bs-info-bg-subtle: #032830; + --bs-warning-bg-subtle: #332701; + --bs-danger-bg-subtle: #2c0b0e; + --bs-light-bg-subtle: #343a40; + --bs-dark-bg-subtle: #1a1d20; + --bs-primary-border-subtle: #084298; + --bs-secondary-border-subtle: #41464b; + --bs-success-border-subtle: #0f5132; + --bs-info-border-subtle: #087990; + --bs-warning-border-subtle: #997404; + --bs-danger-border-subtle: #842029; + --bs-light-border-subtle: #495057; + --bs-dark-border-subtle: #343a40; + --bs-heading-color: inherit; + --bs-link-color: #6ea8fe; + --bs-link-hover-color: #8bb9fe; + --bs-link-color-rgb: 110, 168, 254; + --bs-link-hover-color-rgb: 139, 185, 254; + --bs-code-color: #e685b5; + --bs-highlight-color: #dee2e6; + --bs-highlight-bg: #664d03; + --bs-border-color: #495057; + --bs-border-color-translucent: rgba(255, 255, 255, 0.15); + --bs-form-valid-color: #75b798; + --bs-form-valid-border-color: #75b798; + --bs-form-invalid-color: #ea868f; + --bs-form-invalid-border-color: #ea868f; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +@media (prefers-reduced-motion: no-preference) { + :root { + scroll-behavior: smooth; + } +} + +body { + margin: 0; + font-family: var(--bs-body-font-family); + font-size: var(--bs-body-font-size); + font-weight: var(--bs-body-font-weight); + line-height: var(--bs-body-line-height); + color: var(--bs-body-color); + text-align: var(--bs-body-text-align); + background-color: var(--bs-body-bg); + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +hr { + margin: 1rem 0; + color: inherit; + border: 0; + border-top: var(--bs-border-width) solid; + opacity: 0.25; +} + +h6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 { + margin-top: 0; + margin-bottom: 0.5rem; + font-weight: 500; + line-height: 1.2; + color: var(--bs-heading-color); +} + +h1, .h1 { + font-size: calc(1.375rem + 1.5vw); +} +@media (min-width: 1200px) { + h1, .h1 { + font-size: 2.5rem; + } +} + +h2, .h2 { + font-size: calc(1.325rem + 0.9vw); +} +@media (min-width: 1200px) { + h2, .h2 { + font-size: 2rem; + } +} + +h3, .h3 { + font-size: calc(1.3rem + 0.6vw); +} +@media (min-width: 1200px) { + h3, .h3 { + font-size: 1.75rem; + } +} + +h4, .h4 { + font-size: calc(1.275rem + 0.3vw); +} +@media (min-width: 1200px) { + h4, .h4 { + font-size: 1.5rem; + } +} + +h5, .h5 { + font-size: 1.25rem; +} + +h6, .h6 { + font-size: 1rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title] { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul { + padding-left: 2rem; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: 0.5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +b, +strong { + font-weight: bolder; +} + +small, .small { + font-size: 0.875em; +} + +mark, .mark { + padding: 0.1875em; + color: var(--bs-highlight-color); + background-color: var(--bs-highlight-bg); +} + +sub, +sup { + position: relative; + font-size: 0.75em; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +a { + color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); + text-decoration: underline; +} +a:hover { + --bs-link-color-rgb: var(--bs-link-hover-color-rgb); +} + +a:not([href]):not([class]), a:not([href]):not([class]):hover { + color: inherit; + text-decoration: none; +} + +pre, +code, +kbd, +samp { + font-family: var(--bs-font-monospace); + font-size: 1em; +} + +pre { + display: block; + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; + font-size: 0.875em; +} +pre code { + font-size: inherit; + color: inherit; + word-break: normal; +} + +code { + font-size: 0.875em; + color: var(--bs-code-color); + word-wrap: break-word; +} +a > code { + color: inherit; +} + +kbd { + padding: 0.1875rem 0.375rem; + font-size: 0.875em; + color: var(--bs-body-bg); + background-color: var(--bs-body-color); + border-radius: 0.25rem; +} +kbd kbd { + padding: 0; + font-size: 1em; +} + +figure { + margin: 0 0 1rem; +} + +img, +svg { + vertical-align: middle; +} + +table { + caption-side: bottom; + border-collapse: collapse; +} + +caption { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + color: var(--bs-secondary-color); + text-align: left; +} + +th { + text-align: inherit; + text-align: -webkit-match-parent; +} + +thead, +tbody, +tfoot, +tr, +td, +th { + border-color: inherit; + border-style: solid; + border-width: 0; +} + +label { + display: inline-block; +} + +button { + border-radius: 0; +} + +button:focus:not(:focus-visible) { + outline: 0; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +select { + text-transform: none; +} + +[role=button] { + cursor: pointer; +} + +select { + word-wrap: normal; +} +select:disabled { + opacity: 1; +} + +[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { + display: none !important; +} + +button, +[type=button], +[type=reset], +[type=submit] { + -webkit-appearance: button; +} +button:not(:disabled), +[type=button]:not(:disabled), +[type=reset]:not(:disabled), +[type=submit]:not(:disabled) { + cursor: pointer; +} + +::-moz-focus-inner { + padding: 0; + border-style: none; +} + +textarea { + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + float: left; + width: 100%; + padding: 0; + margin-bottom: 0.5rem; + line-height: inherit; + font-size: calc(1.275rem + 0.3vw); +} +@media (min-width: 1200px) { + legend { + font-size: 1.5rem; + } +} +legend + * { + clear: left; +} + +::-webkit-datetime-edit-fields-wrapper, +::-webkit-datetime-edit-text, +::-webkit-datetime-edit-minute, +::-webkit-datetime-edit-hour-field, +::-webkit-datetime-edit-day-field, +::-webkit-datetime-edit-month-field, +::-webkit-datetime-edit-year-field { + padding: 0; +} + +::-webkit-inner-spin-button { + height: auto; +} + +[type=search] { + -webkit-appearance: textfield; + outline-offset: -2px; +} +[type=search]::-webkit-search-cancel-button { + cursor: pointer; + filter: grayscale(1); +} + +/* rtl:raw: +[type="tel"], +[type="url"], +[type="email"], +[type="number"] { + direction: ltr; +} +*/ +::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-color-swatch-wrapper { + padding: 0; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +::file-selector-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +iframe { + border: 0; +} + +summary { + display: list-item; + cursor: pointer; +} + +progress { + vertical-align: baseline; +} + +[hidden] { + display: none !important; +} + +.lead { + font-size: 1.25rem; + font-weight: 300; +} + +.display-1 { + font-weight: 300; + line-height: 1.2; + font-size: calc(1.625rem + 4.5vw); +} +@media (min-width: 1200px) { + .display-1 { + font-size: 5rem; + } +} + +.display-2 { + font-weight: 300; + line-height: 1.2; + font-size: calc(1.575rem + 3.9vw); +} +@media (min-width: 1200px) { + .display-2 { + font-size: 4.5rem; + } +} + +.display-3 { + font-weight: 300; + line-height: 1.2; + font-size: calc(1.525rem + 3.3vw); +} +@media (min-width: 1200px) { + .display-3 { + font-size: 4rem; + } +} + +.display-4 { + font-weight: 300; + line-height: 1.2; + font-size: calc(1.475rem + 2.7vw); +} +@media (min-width: 1200px) { + .display-4 { + font-size: 3.5rem; + } +} + +.display-5 { + font-weight: 300; + line-height: 1.2; + font-size: calc(1.425rem + 2.1vw); +} +@media (min-width: 1200px) { + .display-5 { + font-size: 3rem; + } +} + +.display-6 { + font-weight: 300; + line-height: 1.2; + font-size: calc(1.375rem + 1.5vw); +} +@media (min-width: 1200px) { + .display-6 { + font-size: 2.5rem; + } +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + padding-left: 0; + list-style: none; +} + +.list-inline-item { + display: inline-block; +} +.list-inline-item:not(:last-child) { + margin-right: 0.5rem; +} + +.initialism { + font-size: 0.875em; + text-transform: uppercase; +} + +.blockquote { + margin-bottom: 1rem; + font-size: 1.25rem; +} +.blockquote > :last-child { + margin-bottom: 0; +} + +.blockquote-footer { + margin-top: -1rem; + margin-bottom: 1rem; + font-size: 0.875em; + color: #6c757d; +} +.blockquote-footer::before { + content: "— "; +} + +.img-fluid { + max-width: 100%; + height: auto; +} + +.img-thumbnail { + padding: 0.25rem; + background-color: var(--bs-body-bg); + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + max-width: 100%; + height: auto; +} + +.figure { + display: inline-block; +} + +.figure-img { + margin-bottom: 0.5rem; + line-height: 1; +} + +.figure-caption { + font-size: 0.875em; + color: var(--bs-secondary-color); +} + +.container, +.container-fluid, +.container-xxl, +.container-xl, +.container-lg, +.container-md, +.container-sm { + --bs-gutter-x: 1.5rem; + --bs-gutter-y: 0; + width: 100%; + padding-right: calc(var(--bs-gutter-x) * 0.5); + padding-left: calc(var(--bs-gutter-x) * 0.5); + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container-sm, .container { + max-width: 540px; + } +} +@media (min-width: 768px) { + .container-md, .container-sm, .container { + max-width: 720px; + } +} +@media (min-width: 992px) { + .container-lg, .container-md, .container-sm, .container { + max-width: 960px; + } +} +@media (min-width: 1200px) { + .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1140px; + } +} +@media (min-width: 1400px) { + .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1320px; + } +} +:root { + --bs-breakpoint-xs: 0; + --bs-breakpoint-sm: 576px; + --bs-breakpoint-md: 768px; + --bs-breakpoint-lg: 992px; + --bs-breakpoint-xl: 1200px; + --bs-breakpoint-xxl: 1400px; +} + +.row { + --bs-gutter-x: 1.5rem; + --bs-gutter-y: 0; + display: flex; + flex-wrap: wrap; + margin-top: calc(-1 * var(--bs-gutter-y)); + margin-right: calc(-0.5 * var(--bs-gutter-x)); + margin-left: calc(-0.5 * var(--bs-gutter-x)); +} +.row > * { + flex-shrink: 0; + width: 100%; + max-width: 100%; + padding-right: calc(var(--bs-gutter-x) * 0.5); + padding-left: calc(var(--bs-gutter-x) * 0.5); + margin-top: var(--bs-gutter-y); +} + +.col { + flex: 1 0 0; +} + +.row-cols-auto > * { + flex: 0 0 auto; + width: auto; +} + +.row-cols-1 > * { + flex: 0 0 auto; + width: 100%; +} + +.row-cols-2 > * { + flex: 0 0 auto; + width: 50%; +} + +.row-cols-3 > * { + flex: 0 0 auto; + width: 33.33333333%; +} + +.row-cols-4 > * { + flex: 0 0 auto; + width: 25%; +} + +.row-cols-5 > * { + flex: 0 0 auto; + width: 20%; +} + +.row-cols-6 > * { + flex: 0 0 auto; + width: 16.66666667%; +} + +.col-auto { + flex: 0 0 auto; + width: auto; +} + +.col-1 { + flex: 0 0 auto; + width: 8.33333333%; +} + +.col-2 { + flex: 0 0 auto; + width: 16.66666667%; +} + +.col-3 { + flex: 0 0 auto; + width: 25%; +} + +.col-4 { + flex: 0 0 auto; + width: 33.33333333%; +} + +.col-5 { + flex: 0 0 auto; + width: 41.66666667%; +} + +.col-6 { + flex: 0 0 auto; + width: 50%; +} + +.col-7 { + flex: 0 0 auto; + width: 58.33333333%; +} + +.col-8 { + flex: 0 0 auto; + width: 66.66666667%; +} + +.col-9 { + flex: 0 0 auto; + width: 75%; +} + +.col-10 { + flex: 0 0 auto; + width: 83.33333333%; +} + +.col-11 { + flex: 0 0 auto; + width: 91.66666667%; +} + +.col-12 { + flex: 0 0 auto; + width: 100%; +} + +.offset-1 { + margin-left: 8.33333333%; +} + +.offset-2 { + margin-left: 16.66666667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.33333333%; +} + +.offset-5 { + margin-left: 41.66666667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.33333333%; +} + +.offset-8 { + margin-left: 66.66666667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.33333333%; +} + +.offset-11 { + margin-left: 91.66666667%; +} + +.g-0, +.gx-0 { + --bs-gutter-x: 0; +} + +.g-0, +.gy-0 { + --bs-gutter-y: 0; +} + +.g-1, +.gx-1 { + --bs-gutter-x: 0.25rem; +} + +.g-1, +.gy-1 { + --bs-gutter-y: 0.25rem; +} + +.g-2, +.gx-2 { + --bs-gutter-x: 0.5rem; +} + +.g-2, +.gy-2 { + --bs-gutter-y: 0.5rem; +} + +.g-3, +.gx-3 { + --bs-gutter-x: 1rem; +} + +.g-3, +.gy-3 { + --bs-gutter-y: 1rem; +} + +.g-4, +.gx-4 { + --bs-gutter-x: 1.5rem; +} + +.g-4, +.gy-4 { + --bs-gutter-y: 1.5rem; +} + +.g-5, +.gx-5 { + --bs-gutter-x: 3rem; +} + +.g-5, +.gy-5 { + --bs-gutter-y: 3rem; +} + +@media (min-width: 576px) { + .col-sm { + flex: 1 0 0; + } + .row-cols-sm-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-sm-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-sm-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-sm-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-sm-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-sm-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-sm-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-sm-auto { + flex: 0 0 auto; + width: auto; + } + .col-sm-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-sm-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-sm-3 { + flex: 0 0 auto; + width: 25%; + } + .col-sm-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-sm-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-sm-6 { + flex: 0 0 auto; + width: 50%; + } + .col-sm-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-sm-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-sm-9 { + flex: 0 0 auto; + width: 75%; + } + .col-sm-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-sm-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-sm-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-sm-0 { + margin-left: 0; + } + .offset-sm-1 { + margin-left: 8.33333333%; + } + .offset-sm-2 { + margin-left: 16.66666667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.33333333%; + } + .offset-sm-5 { + margin-left: 41.66666667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.33333333%; + } + .offset-sm-8 { + margin-left: 66.66666667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.33333333%; + } + .offset-sm-11 { + margin-left: 91.66666667%; + } + .g-sm-0, + .gx-sm-0 { + --bs-gutter-x: 0; + } + .g-sm-0, + .gy-sm-0 { + --bs-gutter-y: 0; + } + .g-sm-1, + .gx-sm-1 { + --bs-gutter-x: 0.25rem; + } + .g-sm-1, + .gy-sm-1 { + --bs-gutter-y: 0.25rem; + } + .g-sm-2, + .gx-sm-2 { + --bs-gutter-x: 0.5rem; + } + .g-sm-2, + .gy-sm-2 { + --bs-gutter-y: 0.5rem; + } + .g-sm-3, + .gx-sm-3 { + --bs-gutter-x: 1rem; + } + .g-sm-3, + .gy-sm-3 { + --bs-gutter-y: 1rem; + } + .g-sm-4, + .gx-sm-4 { + --bs-gutter-x: 1.5rem; + } + .g-sm-4, + .gy-sm-4 { + --bs-gutter-y: 1.5rem; + } + .g-sm-5, + .gx-sm-5 { + --bs-gutter-x: 3rem; + } + .g-sm-5, + .gy-sm-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 768px) { + .col-md { + flex: 1 0 0; + } + .row-cols-md-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-md-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-md-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-md-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-md-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-md-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-md-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-md-auto { + flex: 0 0 auto; + width: auto; + } + .col-md-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-md-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-md-3 { + flex: 0 0 auto; + width: 25%; + } + .col-md-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-md-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-md-6 { + flex: 0 0 auto; + width: 50%; + } + .col-md-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-md-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-md-9 { + flex: 0 0 auto; + width: 75%; + } + .col-md-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-md-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-md-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-md-0 { + margin-left: 0; + } + .offset-md-1 { + margin-left: 8.33333333%; + } + .offset-md-2 { + margin-left: 16.66666667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.33333333%; + } + .offset-md-5 { + margin-left: 41.66666667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.33333333%; + } + .offset-md-8 { + margin-left: 66.66666667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.33333333%; + } + .offset-md-11 { + margin-left: 91.66666667%; + } + .g-md-0, + .gx-md-0 { + --bs-gutter-x: 0; + } + .g-md-0, + .gy-md-0 { + --bs-gutter-y: 0; + } + .g-md-1, + .gx-md-1 { + --bs-gutter-x: 0.25rem; + } + .g-md-1, + .gy-md-1 { + --bs-gutter-y: 0.25rem; + } + .g-md-2, + .gx-md-2 { + --bs-gutter-x: 0.5rem; + } + .g-md-2, + .gy-md-2 { + --bs-gutter-y: 0.5rem; + } + .g-md-3, + .gx-md-3 { + --bs-gutter-x: 1rem; + } + .g-md-3, + .gy-md-3 { + --bs-gutter-y: 1rem; + } + .g-md-4, + .gx-md-4 { + --bs-gutter-x: 1.5rem; + } + .g-md-4, + .gy-md-4 { + --bs-gutter-y: 1.5rem; + } + .g-md-5, + .gx-md-5 { + --bs-gutter-x: 3rem; + } + .g-md-5, + .gy-md-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 992px) { + .col-lg { + flex: 1 0 0; + } + .row-cols-lg-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-lg-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-lg-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-lg-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-lg-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-lg-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-lg-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-lg-auto { + flex: 0 0 auto; + width: auto; + } + .col-lg-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-lg-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-lg-3 { + flex: 0 0 auto; + width: 25%; + } + .col-lg-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-lg-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-lg-6 { + flex: 0 0 auto; + width: 50%; + } + .col-lg-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-lg-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-lg-9 { + flex: 0 0 auto; + width: 75%; + } + .col-lg-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-lg-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-lg-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-lg-0 { + margin-left: 0; + } + .offset-lg-1 { + margin-left: 8.33333333%; + } + .offset-lg-2 { + margin-left: 16.66666667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.33333333%; + } + .offset-lg-5 { + margin-left: 41.66666667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.33333333%; + } + .offset-lg-8 { + margin-left: 66.66666667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.33333333%; + } + .offset-lg-11 { + margin-left: 91.66666667%; + } + .g-lg-0, + .gx-lg-0 { + --bs-gutter-x: 0; + } + .g-lg-0, + .gy-lg-0 { + --bs-gutter-y: 0; + } + .g-lg-1, + .gx-lg-1 { + --bs-gutter-x: 0.25rem; + } + .g-lg-1, + .gy-lg-1 { + --bs-gutter-y: 0.25rem; + } + .g-lg-2, + .gx-lg-2 { + --bs-gutter-x: 0.5rem; + } + .g-lg-2, + .gy-lg-2 { + --bs-gutter-y: 0.5rem; + } + .g-lg-3, + .gx-lg-3 { + --bs-gutter-x: 1rem; + } + .g-lg-3, + .gy-lg-3 { + --bs-gutter-y: 1rem; + } + .g-lg-4, + .gx-lg-4 { + --bs-gutter-x: 1.5rem; + } + .g-lg-4, + .gy-lg-4 { + --bs-gutter-y: 1.5rem; + } + .g-lg-5, + .gx-lg-5 { + --bs-gutter-x: 3rem; + } + .g-lg-5, + .gy-lg-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1200px) { + .col-xl { + flex: 1 0 0; + } + .row-cols-xl-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-xl-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-xl-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-xl-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-xl-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-xl-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-xl-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xl-auto { + flex: 0 0 auto; + width: auto; + } + .col-xl-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-xl-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xl-3 { + flex: 0 0 auto; + width: 25%; + } + .col-xl-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-xl-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-xl-6 { + flex: 0 0 auto; + width: 50%; + } + .col-xl-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-xl-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-xl-9 { + flex: 0 0 auto; + width: 75%; + } + .col-xl-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-xl-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-xl-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-xl-0 { + margin-left: 0; + } + .offset-xl-1 { + margin-left: 8.33333333%; + } + .offset-xl-2 { + margin-left: 16.66666667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.33333333%; + } + .offset-xl-5 { + margin-left: 41.66666667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.33333333%; + } + .offset-xl-8 { + margin-left: 66.66666667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.33333333%; + } + .offset-xl-11 { + margin-left: 91.66666667%; + } + .g-xl-0, + .gx-xl-0 { + --bs-gutter-x: 0; + } + .g-xl-0, + .gy-xl-0 { + --bs-gutter-y: 0; + } + .g-xl-1, + .gx-xl-1 { + --bs-gutter-x: 0.25rem; + } + .g-xl-1, + .gy-xl-1 { + --bs-gutter-y: 0.25rem; + } + .g-xl-2, + .gx-xl-2 { + --bs-gutter-x: 0.5rem; + } + .g-xl-2, + .gy-xl-2 { + --bs-gutter-y: 0.5rem; + } + .g-xl-3, + .gx-xl-3 { + --bs-gutter-x: 1rem; + } + .g-xl-3, + .gy-xl-3 { + --bs-gutter-y: 1rem; + } + .g-xl-4, + .gx-xl-4 { + --bs-gutter-x: 1.5rem; + } + .g-xl-4, + .gy-xl-4 { + --bs-gutter-y: 1.5rem; + } + .g-xl-5, + .gx-xl-5 { + --bs-gutter-x: 3rem; + } + .g-xl-5, + .gy-xl-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1400px) { + .col-xxl { + flex: 1 0 0; + } + .row-cols-xxl-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-xxl-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-xxl-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-xxl-3 > * { + flex: 0 0 auto; + width: 33.33333333%; + } + .row-cols-xxl-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-xxl-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-xxl-6 > * { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xxl-auto { + flex: 0 0 auto; + width: auto; + } + .col-xxl-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-xxl-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xxl-3 { + flex: 0 0 auto; + width: 25%; + } + .col-xxl-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-xxl-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-xxl-6 { + flex: 0 0 auto; + width: 50%; + } + .col-xxl-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-xxl-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-xxl-9 { + flex: 0 0 auto; + width: 75%; + } + .col-xxl-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-xxl-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-xxl-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-xxl-0 { + margin-left: 0; + } + .offset-xxl-1 { + margin-left: 8.33333333%; + } + .offset-xxl-2 { + margin-left: 16.66666667%; + } + .offset-xxl-3 { + margin-left: 25%; + } + .offset-xxl-4 { + margin-left: 33.33333333%; + } + .offset-xxl-5 { + margin-left: 41.66666667%; + } + .offset-xxl-6 { + margin-left: 50%; + } + .offset-xxl-7 { + margin-left: 58.33333333%; + } + .offset-xxl-8 { + margin-left: 66.66666667%; + } + .offset-xxl-9 { + margin-left: 75%; + } + .offset-xxl-10 { + margin-left: 83.33333333%; + } + .offset-xxl-11 { + margin-left: 91.66666667%; + } + .g-xxl-0, + .gx-xxl-0 { + --bs-gutter-x: 0; + } + .g-xxl-0, + .gy-xxl-0 { + --bs-gutter-y: 0; + } + .g-xxl-1, + .gx-xxl-1 { + --bs-gutter-x: 0.25rem; + } + .g-xxl-1, + .gy-xxl-1 { + --bs-gutter-y: 0.25rem; + } + .g-xxl-2, + .gx-xxl-2 { + --bs-gutter-x: 0.5rem; + } + .g-xxl-2, + .gy-xxl-2 { + --bs-gutter-y: 0.5rem; + } + .g-xxl-3, + .gx-xxl-3 { + --bs-gutter-x: 1rem; + } + .g-xxl-3, + .gy-xxl-3 { + --bs-gutter-y: 1rem; + } + .g-xxl-4, + .gx-xxl-4 { + --bs-gutter-x: 1.5rem; + } + .g-xxl-4, + .gy-xxl-4 { + --bs-gutter-y: 1.5rem; + } + .g-xxl-5, + .gx-xxl-5 { + --bs-gutter-x: 3rem; + } + .g-xxl-5, + .gy-xxl-5 { + --bs-gutter-y: 3rem; + } +} +.table { + --bs-table-color-type: initial; + --bs-table-bg-type: initial; + --bs-table-color-state: initial; + --bs-table-bg-state: initial; + --bs-table-color: var(--bs-emphasis-color); + --bs-table-bg: var(--bs-body-bg); + --bs-table-border-color: var(--bs-border-color); + --bs-table-accent-bg: transparent; + --bs-table-striped-color: var(--bs-emphasis-color); + --bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05); + --bs-table-active-color: var(--bs-emphasis-color); + --bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1); + --bs-table-hover-color: var(--bs-emphasis-color); + --bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075); + width: 100%; + margin-bottom: 1rem; + vertical-align: top; + border-color: var(--bs-table-border-color); +} +.table > :not(caption) > * > * { + padding: 0.5rem 0.5rem; + color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color))); + background-color: var(--bs-table-bg); + border-bottom-width: var(--bs-border-width); + box-shadow: inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg))); +} +.table > tbody { + vertical-align: inherit; +} +.table > thead { + vertical-align: bottom; +} + +.table-group-divider { + border-top: calc(var(--bs-border-width) * 2) solid currentcolor; +} + +.caption-top { + caption-side: top; +} + +.table-sm > :not(caption) > * > * { + padding: 0.25rem 0.25rem; +} + +.table-bordered > :not(caption) > * { + border-width: var(--bs-border-width) 0; +} +.table-bordered > :not(caption) > * > * { + border-width: 0 var(--bs-border-width); +} + +.table-borderless > :not(caption) > * > * { + border-bottom-width: 0; +} +.table-borderless > :not(:first-child) { + border-top-width: 0; +} + +.table-striped > tbody > tr:nth-of-type(odd) > * { + --bs-table-color-type: var(--bs-table-striped-color); + --bs-table-bg-type: var(--bs-table-striped-bg); +} + +.table-striped-columns > :not(caption) > tr > :nth-child(even) { + --bs-table-color-type: var(--bs-table-striped-color); + --bs-table-bg-type: var(--bs-table-striped-bg); +} + +.table-active { + --bs-table-color-state: var(--bs-table-active-color); + --bs-table-bg-state: var(--bs-table-active-bg); +} + +.table-hover > tbody > tr:hover > * { + --bs-table-color-state: var(--bs-table-hover-color); + --bs-table-bg-state: var(--bs-table-hover-bg); +} + +.table-primary { + --bs-table-color: #000; + --bs-table-bg: #cfe2ff; + --bs-table-border-color: #a6b5cc; + --bs-table-striped-bg: #c5d7f2; + --bs-table-striped-color: #000; + --bs-table-active-bg: #bacbe6; + --bs-table-active-color: #000; + --bs-table-hover-bg: #bfd1ec; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-secondary { + --bs-table-color: #000; + --bs-table-bg: #e2e3e5; + --bs-table-border-color: #b5b6b7; + --bs-table-striped-bg: #d7d8da; + --bs-table-striped-color: #000; + --bs-table-active-bg: #cbccce; + --bs-table-active-color: #000; + --bs-table-hover-bg: #d1d2d4; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-success { + --bs-table-color: #000; + --bs-table-bg: #d1e7dd; + --bs-table-border-color: #a7b9b1; + --bs-table-striped-bg: #c7dbd2; + --bs-table-striped-color: #000; + --bs-table-active-bg: #bcd0c7; + --bs-table-active-color: #000; + --bs-table-hover-bg: #c1d6cc; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-info { + --bs-table-color: #000; + --bs-table-bg: #cff4fc; + --bs-table-border-color: #a6c3ca; + --bs-table-striped-bg: #c5e8ef; + --bs-table-striped-color: #000; + --bs-table-active-bg: #badce3; + --bs-table-active-color: #000; + --bs-table-hover-bg: #bfe2e9; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-warning { + --bs-table-color: #000; + --bs-table-bg: #fff3cd; + --bs-table-border-color: #ccc2a4; + --bs-table-striped-bg: #f2e7c3; + --bs-table-striped-color: #000; + --bs-table-active-bg: #e6dbb9; + --bs-table-active-color: #000; + --bs-table-hover-bg: #ece1be; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-danger { + --bs-table-color: #000; + --bs-table-bg: #f8d7da; + --bs-table-border-color: #c6acae; + --bs-table-striped-bg: #eccccf; + --bs-table-striped-color: #000; + --bs-table-active-bg: #dfc2c4; + --bs-table-active-color: #000; + --bs-table-hover-bg: #e5c7ca; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-light { + --bs-table-color: #000; + --bs-table-bg: #f8f9fa; + --bs-table-border-color: #c6c7c8; + --bs-table-striped-bg: #ecedee; + --bs-table-striped-color: #000; + --bs-table-active-bg: #dfe0e1; + --bs-table-active-color: #000; + --bs-table-hover-bg: #e5e6e7; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-dark { + --bs-table-color: #fff; + --bs-table-bg: #212529; + --bs-table-border-color: #4d5154; + --bs-table-striped-bg: #2c3034; + --bs-table-striped-color: #fff; + --bs-table-active-bg: #373b3e; + --bs-table-active-color: #fff; + --bs-table-hover-bg: #323539; + --bs-table-hover-color: #fff; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +@media (max-width: 575.98px) { + .table-responsive-sm { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 767.98px) { + .table-responsive-md { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 991.98px) { + .table-responsive-lg { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 1199.98px) { + .table-responsive-xl { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 1399.98px) { + .table-responsive-xxl { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +.form-label { + margin-bottom: 0.5rem; +} + +.col-form-label { + padding-top: calc(0.375rem + var(--bs-border-width)); + padding-bottom: calc(0.375rem + var(--bs-border-width)); + margin-bottom: 0; + font-size: inherit; + line-height: 1.5; +} + +.col-form-label-lg { + padding-top: calc(0.5rem + var(--bs-border-width)); + padding-bottom: calc(0.5rem + var(--bs-border-width)); + font-size: 1.25rem; +} + +.col-form-label-sm { + padding-top: calc(0.25rem + var(--bs-border-width)); + padding-bottom: calc(0.25rem + var(--bs-border-width)); + font-size: 0.875rem; +} + +.form-text { + margin-top: 0.25rem; + font-size: 0.875em; + color: var(--bs-secondary-color); +} + +.form-control { + display: block; + width: 100%; + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: var(--bs-body-color); + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: var(--bs-body-bg); + background-clip: padding-box; + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-control { + transition: none; + } +} +.form-control[type=file] { + overflow: hidden; +} +.form-control[type=file]:not(:disabled):not([readonly]) { + cursor: pointer; +} +.form-control:focus { + color: var(--bs-body-color); + background-color: var(--bs-body-bg); + border-color: #86b7fe; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-control::-webkit-date-and-time-value { + min-width: 85px; + height: 1.5em; + margin: 0; +} +.form-control::-webkit-datetime-edit { + display: block; + padding: 0; +} +.form-control::placeholder { + color: var(--bs-secondary-color); + opacity: 1; +} +.form-control:disabled { + background-color: var(--bs-secondary-bg); + opacity: 1; +} +.form-control::-webkit-file-upload-button { + padding: 0.375rem 0.75rem; + margin: -0.375rem -0.75rem; + -webkit-margin-end: 0.75rem; + margin-inline-end: 0.75rem; + color: var(--bs-body-color); + background-color: var(--bs-tertiary-bg); + pointer-events: none; + border-color: inherit; + border-style: solid; + border-width: 0; + border-inline-end-width: var(--bs-border-width); + border-radius: 0; + -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +.form-control::file-selector-button { + padding: 0.375rem 0.75rem; + margin: -0.375rem -0.75rem; + -webkit-margin-end: 0.75rem; + margin-inline-end: 0.75rem; + color: var(--bs-body-color); + background-color: var(--bs-tertiary-bg); + pointer-events: none; + border-color: inherit; + border-style: solid; + border-width: 0; + border-inline-end-width: var(--bs-border-width); + border-radius: 0; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-control::-webkit-file-upload-button { + -webkit-transition: none; + transition: none; + } + .form-control::file-selector-button { + transition: none; + } +} +.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button { + background-color: var(--bs-secondary-bg); +} +.form-control:hover:not(:disabled):not([readonly])::file-selector-button { + background-color: var(--bs-secondary-bg); +} + +.form-control-plaintext { + display: block; + width: 100%; + padding: 0.375rem 0; + margin-bottom: 0; + line-height: 1.5; + color: var(--bs-body-color); + background-color: transparent; + border: solid transparent; + border-width: var(--bs-border-width) 0; +} +.form-control-plaintext:focus { + outline: 0; +} +.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { + padding-right: 0; + padding-left: 0; +} + +.form-control-sm { + min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: var(--bs-border-radius-sm); +} +.form-control-sm::-webkit-file-upload-button { + padding: 0.25rem 0.5rem; + margin: -0.25rem -0.5rem; + -webkit-margin-end: 0.5rem; + margin-inline-end: 0.5rem; +} +.form-control-sm::file-selector-button { + padding: 0.25rem 0.5rem; + margin: -0.25rem -0.5rem; + -webkit-margin-end: 0.5rem; + margin-inline-end: 0.5rem; +} + +.form-control-lg { + min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); + padding: 0.5rem 1rem; + font-size: 1.25rem; + border-radius: var(--bs-border-radius-lg); +} +.form-control-lg::-webkit-file-upload-button { + padding: 0.5rem 1rem; + margin: -0.5rem -1rem; + -webkit-margin-end: 1rem; + margin-inline-end: 1rem; +} +.form-control-lg::file-selector-button { + padding: 0.5rem 1rem; + margin: -0.5rem -1rem; + -webkit-margin-end: 1rem; + margin-inline-end: 1rem; +} + +textarea.form-control { + min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2)); +} +textarea.form-control-sm { + min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); +} +textarea.form-control-lg { + min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); +} + +.form-control-color { + width: 3rem; + height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2)); + padding: 0.375rem; +} +.form-control-color:not(:disabled):not([readonly]) { + cursor: pointer; +} +.form-control-color::-moz-color-swatch { + border: 0 !important; + border-radius: var(--bs-border-radius); +} +.form-control-color::-webkit-color-swatch { + border: 0 !important; + border-radius: var(--bs-border-radius); +} +.form-control-color.form-control-sm { + height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); +} +.form-control-color.form-control-lg { + height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); +} + +.form-select { + --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); + display: block; + width: 100%; + padding: 0.375rem 2.25rem 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: var(--bs-body-color); + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: var(--bs-body-bg); + background-image: var(--bs-form-select-bg-img), var(--bs-form-select-bg-icon, none); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 16px 12px; + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-select { + transition: none; + } +} +.form-select:focus { + border-color: #86b7fe; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-select[multiple], .form-select[size]:not([size="1"]) { + padding-right: 0.75rem; + background-image: none; +} +.form-select:disabled { + background-color: var(--bs-secondary-bg); +} +.form-select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 var(--bs-body-color); +} + +.form-select-sm { + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + font-size: 0.875rem; + border-radius: var(--bs-border-radius-sm); +} + +.form-select-lg { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + font-size: 1.25rem; + border-radius: var(--bs-border-radius-lg); +} + +[data-bs-theme=dark] .form-select { + --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); +} + +.form-check { + display: block; + min-height: 1.5rem; + padding-left: 1.5em; + margin-bottom: 0.125rem; +} +.form-check .form-check-input { + float: left; + margin-left: -1.5em; +} + +.form-check-reverse { + padding-right: 1.5em; + padding-left: 0; + text-align: right; +} +.form-check-reverse .form-check-input { + float: right; + margin-right: -1.5em; + margin-left: 0; +} + +.form-check-input { + --bs-form-check-bg: var(--bs-body-bg); + flex-shrink: 0; + width: 1em; + height: 1em; + margin-top: 0.25em; + vertical-align: top; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: var(--bs-form-check-bg); + background-image: var(--bs-form-check-bg-image); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + border: var(--bs-border-width) solid var(--bs-border-color); + -webkit-print-color-adjust: exact; + color-adjust: exact; + print-color-adjust: exact; +} +.form-check-input[type=checkbox] { + border-radius: 0.25em; +} +.form-check-input[type=radio] { + border-radius: 50%; +} +.form-check-input:active { + filter: brightness(90%); +} +.form-check-input:focus { + border-color: #86b7fe; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-check-input:checked { + background-color: #0d6efd; + border-color: #0d6efd; +} +.form-check-input:checked[type=checkbox] { + --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e"); +} +.form-check-input:checked[type=radio] { + --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e"); +} +.form-check-input[type=checkbox]:indeterminate { + background-color: #0d6efd; + border-color: #0d6efd; + --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e"); +} +.form-check-input:disabled { + pointer-events: none; + filter: none; + opacity: 0.5; +} +.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label { + cursor: default; + opacity: 0.5; +} + +.form-switch { + padding-left: 2.5em; +} +.form-switch .form-check-input { + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e"); + width: 2em; + margin-left: -2.5em; + background-image: var(--bs-form-switch-bg); + background-position: left center; + border-radius: 2em; + transition: background-position 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-switch .form-check-input { + transition: none; + } +} +.form-switch .form-check-input:focus { + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e"); +} +.form-switch .form-check-input:checked { + background-position: right center; + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); +} +.form-switch.form-check-reverse { + padding-right: 2.5em; + padding-left: 0; +} +.form-switch.form-check-reverse .form-check-input { + margin-right: -2.5em; + margin-left: 0; +} + +.form-check-inline { + display: inline-block; + margin-right: 1rem; +} + +.btn-check { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} +.btn-check[disabled] + .btn, .btn-check:disabled + .btn { + pointer-events: none; + filter: none; + opacity: 0.65; +} + +[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus) { + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e"); +} + +.form-range { + width: 100%; + height: 1.5rem; + padding: 0; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: transparent; +} +.form-range:focus { + outline: 0; +} +.form-range:focus::-webkit-slider-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-range:focus::-moz-range-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-range::-moz-focus-outer { + border: 0; +} +.form-range::-webkit-slider-thumb { + width: 1rem; + height: 1rem; + margin-top: -0.25rem; + -webkit-appearance: none; + appearance: none; + background-color: #0d6efd; + border: 0; + border-radius: 1rem; + -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-range::-webkit-slider-thumb { + -webkit-transition: none; + transition: none; + } +} +.form-range::-webkit-slider-thumb:active { + background-color: #b6d4fe; +} +.form-range::-webkit-slider-runnable-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: var(--bs-secondary-bg); + border-color: transparent; + border-radius: 1rem; +} +.form-range::-moz-range-thumb { + width: 1rem; + height: 1rem; + -moz-appearance: none; + appearance: none; + background-color: #0d6efd; + border: 0; + border-radius: 1rem; + -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-range::-moz-range-thumb { + -moz-transition: none; + transition: none; + } +} +.form-range::-moz-range-thumb:active { + background-color: #b6d4fe; +} +.form-range::-moz-range-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: var(--bs-secondary-bg); + border-color: transparent; + border-radius: 1rem; +} +.form-range:disabled { + pointer-events: none; +} +.form-range:disabled::-webkit-slider-thumb { + background-color: var(--bs-secondary-color); +} +.form-range:disabled::-moz-range-thumb { + background-color: var(--bs-secondary-color); +} + +.form-floating { + position: relative; +} +.form-floating > .form-control, +.form-floating > .form-control-plaintext, +.form-floating > .form-select { + height: calc(3.5rem + calc(var(--bs-border-width) * 2)); + min-height: calc(3.5rem + calc(var(--bs-border-width) * 2)); + line-height: 1.25; +} +.form-floating > label { + position: absolute; + top: 0; + left: 0; + z-index: 2; + max-width: 100%; + height: 100%; + padding: 1rem 0.75rem; + overflow: hidden; + color: rgba(var(--bs-body-color-rgb), 0.65); + text-align: start; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + border: var(--bs-border-width) solid transparent; + transform-origin: 0 0; + transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-floating > label { + transition: none; + } +} +.form-floating > .form-control, +.form-floating > .form-control-plaintext { + padding: 1rem 0.75rem; +} +.form-floating > .form-control::placeholder, +.form-floating > .form-control-plaintext::placeholder { + color: transparent; +} +.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown), +.form-floating > .form-control-plaintext:focus, +.form-floating > .form-control-plaintext:not(:placeholder-shown) { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-control:-webkit-autofill, +.form-floating > .form-control-plaintext:-webkit-autofill { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-select { + padding-top: 1.625rem; + padding-bottom: 0.625rem; + padding-left: 0.75rem; +} +.form-floating > .form-control:focus ~ label, +.form-floating > .form-control:not(:placeholder-shown) ~ label, +.form-floating > .form-control-plaintext ~ label, +.form-floating > .form-select ~ label { + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); +} +.form-floating > .form-control:-webkit-autofill ~ label { + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); +} +.form-floating > textarea:focus ~ label::after, +.form-floating > textarea:not(:placeholder-shown) ~ label::after { + position: absolute; + inset: 1rem 0.375rem; + z-index: -1; + height: 1.5em; + content: ""; + background-color: var(--bs-body-bg); + border-radius: var(--bs-border-radius); +} +.form-floating > textarea:disabled ~ label::after { + background-color: var(--bs-secondary-bg); +} +.form-floating > .form-control-plaintext ~ label { + border-width: var(--bs-border-width) 0; +} +.form-floating > :disabled ~ label, +.form-floating > .form-control:disabled ~ label { + color: #6c757d; +} + +.input-group { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: stretch; + width: 100%; +} +.input-group > .form-control, +.input-group > .form-select, +.input-group > .form-floating { + position: relative; + flex: 1 1 auto; + width: 1%; + min-width: 0; +} +.input-group > .form-control:focus, +.input-group > .form-select:focus, +.input-group > .form-floating:focus-within { + z-index: 5; +} +.input-group .btn { + position: relative; + z-index: 2; +} +.input-group .btn:focus { + z-index: 5; +} + +.input-group-text { + display: flex; + align-items: center; + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: var(--bs-body-color); + text-align: center; + white-space: nowrap; + background-color: var(--bs-tertiary-bg); + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); +} + +.input-group-lg > .form-control, +.input-group-lg > .form-select, +.input-group-lg > .input-group-text, +.input-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + border-radius: var(--bs-border-radius-lg); +} + +.input-group-sm > .form-control, +.input-group-sm > .form-select, +.input-group-sm > .input-group-text, +.input-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: var(--bs-border-radius-sm); +} + +.input-group-lg > .form-select, +.input-group-sm > .form-select { + padding-right: 3rem; +} + +.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), +.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3), +.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control, +.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), +.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4), +.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-control, +.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-select { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) { + margin-left: calc(-1 * var(--bs-border-width)); + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.input-group > .form-floating:not(:first-child) > .form-control, +.input-group > .form-floating:not(:first-child) > .form-select { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 0.875em; + color: var(--bs-form-valid-color); +} + +.valid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: 0.1rem; + font-size: 0.875rem; + color: #fff; + background-color: var(--bs-success); + border-radius: var(--bs-border-radius); +} + +.was-validated :valid ~ .valid-feedback, +.was-validated :valid ~ .valid-tooltip, +.is-valid ~ .valid-feedback, +.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-control:valid, .form-control.is-valid { + border-color: var(--bs-form-valid-border-color); + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-control:valid:focus, .form-control.is-valid:focus { + border-color: var(--bs-form-valid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); +} + +.was-validated textarea.form-control:valid, textarea.form-control.is-valid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .form-select:valid, .form-select.is-valid { + border-color: var(--bs-form-valid-border-color); +} +.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] { + --bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e"); + padding-right: 4.125rem; + background-position: right 0.75rem center, center right 2.25rem; + background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-select:valid:focus, .form-select.is-valid:focus { + border-color: var(--bs-form-valid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); +} + +.was-validated .form-control-color:valid, .form-control-color.is-valid { + width: calc(3rem + calc(1.5em + 0.75rem)); +} + +.was-validated .form-check-input:valid, .form-check-input.is-valid { + border-color: var(--bs-form-valid-border-color); +} +.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked { + background-color: var(--bs-form-valid-color); +} +.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus { + box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); +} +.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: var(--bs-form-valid-color); +} + +.form-check-inline .form-check-input ~ .valid-feedback { + margin-left: 0.5em; +} + +.was-validated .input-group > .form-control:not(:focus):valid, .input-group > .form-control:not(:focus).is-valid, +.was-validated .input-group > .form-select:not(:focus):valid, +.input-group > .form-select:not(:focus).is-valid, +.was-validated .input-group > .form-floating:not(:focus-within):valid, +.input-group > .form-floating:not(:focus-within).is-valid { + z-index: 3; +} + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 0.875em; + color: var(--bs-form-invalid-color); +} + +.invalid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: 0.1rem; + font-size: 0.875rem; + color: #fff; + background-color: var(--bs-danger); + border-radius: var(--bs-border-radius); +} + +.was-validated :invalid ~ .invalid-feedback, +.was-validated :invalid ~ .invalid-tooltip, +.is-invalid ~ .invalid-feedback, +.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-control:invalid, .form-control.is-invalid { + border-color: var(--bs-form-invalid-border-color); + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { + border-color: var(--bs-form-invalid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); +} + +.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .form-select:invalid, .form-select.is-invalid { + border-color: var(--bs-form-invalid-border-color); +} +.was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size="1"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size="1"] { + --bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); + padding-right: 4.125rem; + background-position: right 0.75rem center, center right 2.25rem; + background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus { + border-color: var(--bs-form-invalid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); +} + +.was-validated .form-control-color:invalid, .form-control-color.is-invalid { + width: calc(3rem + calc(1.5em + 0.75rem)); +} + +.was-validated .form-check-input:invalid, .form-check-input.is-invalid { + border-color: var(--bs-form-invalid-border-color); +} +.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked { + background-color: var(--bs-form-invalid-color); +} +.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus { + box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); +} +.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: var(--bs-form-invalid-color); +} + +.form-check-inline .form-check-input ~ .invalid-feedback { + margin-left: 0.5em; +} + +.was-validated .input-group > .form-control:not(:focus):invalid, .input-group > .form-control:not(:focus).is-invalid, +.was-validated .input-group > .form-select:not(:focus):invalid, +.input-group > .form-select:not(:focus).is-invalid, +.was-validated .input-group > .form-floating:not(:focus-within):invalid, +.input-group > .form-floating:not(:focus-within).is-invalid { + z-index: 4; +} + +.btn { + --bs-btn-padding-x: 0.75rem; + --bs-btn-padding-y: 0.375rem; + --bs-btn-font-family: ; + --bs-btn-font-size: 1rem; + --bs-btn-font-weight: 400; + --bs-btn-line-height: 1.5; + --bs-btn-color: var(--bs-body-color); + --bs-btn-bg: transparent; + --bs-btn-border-width: var(--bs-border-width); + --bs-btn-border-color: transparent; + --bs-btn-border-radius: var(--bs-border-radius); + --bs-btn-hover-border-color: transparent; + --bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); + --bs-btn-disabled-opacity: 0.65; + --bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5); + display: inline-block; + padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x); + font-family: var(--bs-btn-font-family); + font-size: var(--bs-btn-font-size); + font-weight: var(--bs-btn-font-weight); + line-height: var(--bs-btn-line-height); + color: var(--bs-btn-color); + text-align: center; + text-decoration: none; + vertical-align: middle; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + border: var(--bs-btn-border-width) solid var(--bs-btn-border-color); + border-radius: var(--bs-btn-border-radius); + background-color: var(--bs-btn-bg); + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .btn { + transition: none; + } +} +.btn:hover { + color: var(--bs-btn-hover-color); + background-color: var(--bs-btn-hover-bg); + border-color: var(--bs-btn-hover-border-color); +} +.btn-check + .btn:hover { + color: var(--bs-btn-color); + background-color: var(--bs-btn-bg); + border-color: var(--bs-btn-border-color); +} +.btn:focus-visible { + color: var(--bs-btn-hover-color); + background-color: var(--bs-btn-hover-bg); + border-color: var(--bs-btn-hover-border-color); + outline: 0; + box-shadow: var(--bs-btn-focus-box-shadow); +} +.btn-check:focus-visible + .btn { + border-color: var(--bs-btn-hover-border-color); + outline: 0; + box-shadow: var(--bs-btn-focus-box-shadow); +} +.btn-check:checked + .btn, :not(.btn-check) + .btn:active, .btn:first-child:active, .btn.active, .btn.show { + color: var(--bs-btn-active-color); + background-color: var(--bs-btn-active-bg); + border-color: var(--bs-btn-active-border-color); +} +.btn-check:checked + .btn:focus-visible, :not(.btn-check) + .btn:active:focus-visible, .btn:first-child:active:focus-visible, .btn.active:focus-visible, .btn.show:focus-visible { + box-shadow: var(--bs-btn-focus-box-shadow); +} +.btn-check:checked:focus-visible + .btn { + box-shadow: var(--bs-btn-focus-box-shadow); +} +.btn:disabled, .btn.disabled, fieldset:disabled .btn { + color: var(--bs-btn-disabled-color); + pointer-events: none; + background-color: var(--bs-btn-disabled-bg); + border-color: var(--bs-btn-disabled-border-color); + opacity: var(--bs-btn-disabled-opacity); +} + +.btn-primary { + --bs-btn-color: #fff; + --bs-btn-bg: #0d6efd; + --bs-btn-border-color: #0d6efd; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #0b5ed7; + --bs-btn-hover-border-color: #0a58ca; + --bs-btn-focus-shadow-rgb: 49, 132, 253; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #0a58ca; + --bs-btn-active-border-color: #0a53be; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #0d6efd; + --bs-btn-disabled-border-color: #0d6efd; +} + +.btn-secondary { + --bs-btn-color: #fff; + --bs-btn-bg: #6c757d; + --bs-btn-border-color: #6c757d; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #5c636a; + --bs-btn-hover-border-color: #565e64; + --bs-btn-focus-shadow-rgb: 130, 138, 145; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #565e64; + --bs-btn-active-border-color: #51585e; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #6c757d; + --bs-btn-disabled-border-color: #6c757d; +} + +.btn-success { + --bs-btn-color: #fff; + --bs-btn-bg: #198754; + --bs-btn-border-color: #198754; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #157347; + --bs-btn-hover-border-color: #146c43; + --bs-btn-focus-shadow-rgb: 60, 153, 110; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #146c43; + --bs-btn-active-border-color: #13653f; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #198754; + --bs-btn-disabled-border-color: #198754; +} + +.btn-info { + --bs-btn-color: #000; + --bs-btn-bg: #0dcaf0; + --bs-btn-border-color: #0dcaf0; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #31d2f2; + --bs-btn-hover-border-color: #25cff2; + --bs-btn-focus-shadow-rgb: 11, 172, 204; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #3dd5f3; + --bs-btn-active-border-color: #25cff2; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #000; + --bs-btn-disabled-bg: #0dcaf0; + --bs-btn-disabled-border-color: #0dcaf0; +} + +.btn-warning { + --bs-btn-color: #000; + --bs-btn-bg: #ffc107; + --bs-btn-border-color: #ffc107; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #ffca2c; + --bs-btn-hover-border-color: #ffc720; + --bs-btn-focus-shadow-rgb: 217, 164, 6; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #ffcd39; + --bs-btn-active-border-color: #ffc720; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #000; + --bs-btn-disabled-bg: #ffc107; + --bs-btn-disabled-border-color: #ffc107; +} + +.btn-danger { + --bs-btn-color: #fff; + --bs-btn-bg: #dc3545; + --bs-btn-border-color: #dc3545; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #bb2d3b; + --bs-btn-hover-border-color: #b02a37; + --bs-btn-focus-shadow-rgb: 225, 83, 97; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #b02a37; + --bs-btn-active-border-color: #a52834; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #dc3545; + --bs-btn-disabled-border-color: #dc3545; +} + +.btn-light { + --bs-btn-color: #000; + --bs-btn-bg: #f8f9fa; + --bs-btn-border-color: #f8f9fa; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #d3d4d5; + --bs-btn-hover-border-color: #c6c7c8; + --bs-btn-focus-shadow-rgb: 211, 212, 213; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #c6c7c8; + --bs-btn-active-border-color: #babbbc; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #000; + --bs-btn-disabled-bg: #f8f9fa; + --bs-btn-disabled-border-color: #f8f9fa; +} + +.btn-dark { + --bs-btn-color: #fff; + --bs-btn-bg: #212529; + --bs-btn-border-color: #212529; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #424649; + --bs-btn-hover-border-color: #373b3e; + --bs-btn-focus-shadow-rgb: 66, 70, 73; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #4d5154; + --bs-btn-active-border-color: #373b3e; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #212529; + --bs-btn-disabled-border-color: #212529; +} + +.btn-outline-primary { + --bs-btn-color: #0d6efd; + --bs-btn-border-color: #0d6efd; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #0d6efd; + --bs-btn-hover-border-color: #0d6efd; + --bs-btn-focus-shadow-rgb: 13, 110, 253; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #0d6efd; + --bs-btn-active-border-color: #0d6efd; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #0d6efd; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #0d6efd; + --bs-gradient: none; +} + +.btn-outline-secondary { + --bs-btn-color: #6c757d; + --bs-btn-border-color: #6c757d; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #6c757d; + --bs-btn-hover-border-color: #6c757d; + --bs-btn-focus-shadow-rgb: 108, 117, 125; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #6c757d; + --bs-btn-active-border-color: #6c757d; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #6c757d; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #6c757d; + --bs-gradient: none; +} + +.btn-outline-success { + --bs-btn-color: #198754; + --bs-btn-border-color: #198754; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #198754; + --bs-btn-hover-border-color: #198754; + --bs-btn-focus-shadow-rgb: 25, 135, 84; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #198754; + --bs-btn-active-border-color: #198754; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #198754; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #198754; + --bs-gradient: none; +} + +.btn-outline-info { + --bs-btn-color: #0dcaf0; + --bs-btn-border-color: #0dcaf0; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #0dcaf0; + --bs-btn-hover-border-color: #0dcaf0; + --bs-btn-focus-shadow-rgb: 13, 202, 240; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #0dcaf0; + --bs-btn-active-border-color: #0dcaf0; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #0dcaf0; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #0dcaf0; + --bs-gradient: none; +} + +.btn-outline-warning { + --bs-btn-color: #ffc107; + --bs-btn-border-color: #ffc107; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #ffc107; + --bs-btn-hover-border-color: #ffc107; + --bs-btn-focus-shadow-rgb: 255, 193, 7; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #ffc107; + --bs-btn-active-border-color: #ffc107; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #ffc107; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #ffc107; + --bs-gradient: none; +} + +.btn-outline-danger { + --bs-btn-color: #dc3545; + --bs-btn-border-color: #dc3545; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #dc3545; + --bs-btn-hover-border-color: #dc3545; + --bs-btn-focus-shadow-rgb: 220, 53, 69; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #dc3545; + --bs-btn-active-border-color: #dc3545; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #dc3545; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #dc3545; + --bs-gradient: none; +} + +.btn-outline-light { + --bs-btn-color: #f8f9fa; + --bs-btn-border-color: #f8f9fa; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #f8f9fa; + --bs-btn-hover-border-color: #f8f9fa; + --bs-btn-focus-shadow-rgb: 248, 249, 250; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #f8f9fa; + --bs-btn-active-border-color: #f8f9fa; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #f8f9fa; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #f8f9fa; + --bs-gradient: none; +} + +.btn-outline-dark { + --bs-btn-color: #212529; + --bs-btn-border-color: #212529; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #212529; + --bs-btn-hover-border-color: #212529; + --bs-btn-focus-shadow-rgb: 33, 37, 41; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #212529; + --bs-btn-active-border-color: #212529; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #212529; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #212529; + --bs-gradient: none; +} + +.btn-link { + --bs-btn-font-weight: 400; + --bs-btn-color: var(--bs-link-color); + --bs-btn-bg: transparent; + --bs-btn-border-color: transparent; + --bs-btn-hover-color: var(--bs-link-hover-color); + --bs-btn-hover-border-color: transparent; + --bs-btn-active-color: var(--bs-link-hover-color); + --bs-btn-active-border-color: transparent; + --bs-btn-disabled-color: #6c757d; + --bs-btn-disabled-border-color: transparent; + --bs-btn-box-shadow: 0 0 0 #000; + --bs-btn-focus-shadow-rgb: 49, 132, 253; + text-decoration: underline; +} +.btn-link:focus-visible { + color: var(--bs-btn-color); +} +.btn-link:hover { + color: var(--bs-btn-hover-color); +} + +.btn-lg, .btn-group-lg > .btn { + --bs-btn-padding-y: 0.5rem; + --bs-btn-padding-x: 1rem; + --bs-btn-font-size: 1.25rem; + --bs-btn-border-radius: var(--bs-border-radius-lg); +} + +.btn-sm, .btn-group-sm > .btn { + --bs-btn-padding-y: 0.25rem; + --bs-btn-padding-x: 0.5rem; + --bs-btn-font-size: 0.875rem; + --bs-btn-border-radius: var(--bs-border-radius-sm); +} + +.fade { + transition: opacity 0.15s linear; +} +@media (prefers-reduced-motion: reduce) { + .fade { + transition: none; + } +} +.fade:not(.show) { + opacity: 0; +} + +.collapse:not(.show) { + display: none; +} + +.collapsing { + height: 0; + overflow: hidden; + transition: height 0.35s ease; +} +@media (prefers-reduced-motion: reduce) { + .collapsing { + transition: none; + } +} +.collapsing.collapse-horizontal { + width: 0; + height: auto; + transition: width 0.35s ease; +} +@media (prefers-reduced-motion: reduce) { + .collapsing.collapse-horizontal { + transition: none; + } +} + +.dropup, +.dropend, +.dropdown, +.dropstart, +.dropup-center, +.dropdown-center { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; +} +.dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.3em solid transparent; +} +.dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropdown-menu { + --bs-dropdown-zindex: 1000; + --bs-dropdown-min-width: 10rem; + --bs-dropdown-padding-x: 0; + --bs-dropdown-padding-y: 0.5rem; + --bs-dropdown-spacer: 0.125rem; + --bs-dropdown-font-size: 1rem; + --bs-dropdown-color: var(--bs-body-color); + --bs-dropdown-bg: var(--bs-body-bg); + --bs-dropdown-border-color: var(--bs-border-color-translucent); + --bs-dropdown-border-radius: var(--bs-border-radius); + --bs-dropdown-border-width: var(--bs-border-width); + --bs-dropdown-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width)); + --bs-dropdown-divider-bg: var(--bs-border-color-translucent); + --bs-dropdown-divider-margin-y: 0.5rem; + --bs-dropdown-box-shadow: var(--bs-box-shadow); + --bs-dropdown-link-color: var(--bs-body-color); + --bs-dropdown-link-hover-color: var(--bs-body-color); + --bs-dropdown-link-hover-bg: var(--bs-tertiary-bg); + --bs-dropdown-link-active-color: #fff; + --bs-dropdown-link-active-bg: #0d6efd; + --bs-dropdown-link-disabled-color: var(--bs-tertiary-color); + --bs-dropdown-item-padding-x: 1rem; + --bs-dropdown-item-padding-y: 0.25rem; + --bs-dropdown-header-color: #6c757d; + --bs-dropdown-header-padding-x: 1rem; + --bs-dropdown-header-padding-y: 0.5rem; + position: absolute; + z-index: var(--bs-dropdown-zindex); + display: none; + min-width: var(--bs-dropdown-min-width); + padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x); + margin: 0; + font-size: var(--bs-dropdown-font-size); + color: var(--bs-dropdown-color); + text-align: left; + list-style: none; + background-color: var(--bs-dropdown-bg); + background-clip: padding-box; + border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color); + border-radius: var(--bs-dropdown-border-radius); +} +.dropdown-menu[data-bs-popper] { + top: 100%; + left: 0; + margin-top: var(--bs-dropdown-spacer); +} + +.dropdown-menu-start { + --bs-position: start; +} +.dropdown-menu-start[data-bs-popper] { + right: auto; + left: 0; +} + +.dropdown-menu-end { + --bs-position: end; +} +.dropdown-menu-end[data-bs-popper] { + right: 0; + left: auto; +} + +@media (min-width: 576px) { + .dropdown-menu-sm-start { + --bs-position: start; + } + .dropdown-menu-sm-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-sm-end { + --bs-position: end; + } + .dropdown-menu-sm-end[data-bs-popper] { + right: 0; + left: auto; + } +} +@media (min-width: 768px) { + .dropdown-menu-md-start { + --bs-position: start; + } + .dropdown-menu-md-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-md-end { + --bs-position: end; + } + .dropdown-menu-md-end[data-bs-popper] { + right: 0; + left: auto; + } +} +@media (min-width: 992px) { + .dropdown-menu-lg-start { + --bs-position: start; + } + .dropdown-menu-lg-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-lg-end { + --bs-position: end; + } + .dropdown-menu-lg-end[data-bs-popper] { + right: 0; + left: auto; + } +} +@media (min-width: 1200px) { + .dropdown-menu-xl-start { + --bs-position: start; + } + .dropdown-menu-xl-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-xl-end { + --bs-position: end; + } + .dropdown-menu-xl-end[data-bs-popper] { + right: 0; + left: auto; + } +} +@media (min-width: 1400px) { + .dropdown-menu-xxl-start { + --bs-position: start; + } + .dropdown-menu-xxl-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-xxl-end { + --bs-position: end; + } + .dropdown-menu-xxl-end[data-bs-popper] { + right: 0; + left: auto; + } +} +.dropup .dropdown-menu[data-bs-popper] { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: var(--bs-dropdown-spacer); +} +.dropup .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0; + border-right: 0.3em solid transparent; + border-bottom: 0.3em solid; + border-left: 0.3em solid transparent; +} +.dropup .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropend .dropdown-menu[data-bs-popper] { + top: 0; + right: auto; + left: 100%; + margin-top: 0; + margin-left: var(--bs-dropdown-spacer); +} +.dropend .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0; + border-bottom: 0.3em solid transparent; + border-left: 0.3em solid; +} +.dropend .dropdown-toggle:empty::after { + margin-left: 0; +} +.dropend .dropdown-toggle::after { + vertical-align: 0; +} + +.dropstart .dropdown-menu[data-bs-popper] { + top: 0; + right: 100%; + left: auto; + margin-top: 0; + margin-right: var(--bs-dropdown-spacer); +} +.dropstart .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; +} +.dropstart .dropdown-toggle::after { + display: none; +} +.dropstart .dropdown-toggle::before { + display: inline-block; + margin-right: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0.3em solid; + border-bottom: 0.3em solid transparent; +} +.dropstart .dropdown-toggle:empty::after { + margin-left: 0; +} +.dropstart .dropdown-toggle::before { + vertical-align: 0; +} + +.dropdown-divider { + height: 0; + margin: var(--bs-dropdown-divider-margin-y) 0; + overflow: hidden; + border-top: 1px solid var(--bs-dropdown-divider-bg); + opacity: 1; +} + +.dropdown-item { + display: block; + width: 100%; + padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x); + clear: both; + font-weight: 400; + color: var(--bs-dropdown-link-color); + text-align: inherit; + text-decoration: none; + white-space: nowrap; + background-color: transparent; + border: 0; + border-radius: var(--bs-dropdown-item-border-radius, 0); +} +.dropdown-item:hover, .dropdown-item:focus { + color: var(--bs-dropdown-link-hover-color); + background-color: var(--bs-dropdown-link-hover-bg); +} +.dropdown-item.active, .dropdown-item:active { + color: var(--bs-dropdown-link-active-color); + text-decoration: none; + background-color: var(--bs-dropdown-link-active-bg); +} +.dropdown-item.disabled, .dropdown-item:disabled { + color: var(--bs-dropdown-link-disabled-color); + pointer-events: none; + background-color: transparent; +} + +.dropdown-menu.show { + display: block; +} + +.dropdown-header { + display: block; + padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x); + margin-bottom: 0; + font-size: 0.875rem; + color: var(--bs-dropdown-header-color); + white-space: nowrap; +} + +.dropdown-item-text { + display: block; + padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x); + color: var(--bs-dropdown-link-color); +} + +.dropdown-menu-dark { + --bs-dropdown-color: #dee2e6; + --bs-dropdown-bg: #343a40; + --bs-dropdown-border-color: var(--bs-border-color-translucent); + --bs-dropdown-box-shadow: ; + --bs-dropdown-link-color: #dee2e6; + --bs-dropdown-link-hover-color: #fff; + --bs-dropdown-divider-bg: var(--bs-border-color-translucent); + --bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15); + --bs-dropdown-link-active-color: #fff; + --bs-dropdown-link-active-bg: #0d6efd; + --bs-dropdown-link-disabled-color: #adb5bd; + --bs-dropdown-header-color: #adb5bd; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-flex; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + flex: 1 1 auto; +} +.btn-group > .btn-check:checked + .btn, +.btn-group > .btn-check:focus + .btn, +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn-check:checked + .btn, +.btn-group-vertical > .btn-check:focus + .btn, +.btn-group-vertical > .btn:hover, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + z-index: 1; +} + +.btn-toolbar { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; +} +.btn-toolbar .input-group { + width: auto; +} + +.btn-group { + border-radius: var(--bs-border-radius); +} +.btn-group > :not(.btn-check:first-child) + .btn, +.btn-group > .btn-group:not(:first-child) { + margin-left: calc(-1 * var(--bs-border-width)); +} +.btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn.dropdown-toggle-split:first-child, +.btn-group > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn:nth-child(n+3), +.btn-group > :not(.btn-check) + .btn, +.btn-group > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.dropdown-toggle-split { + padding-right: 0.5625rem; + padding-left: 0.5625rem; +} +.dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropend .dropdown-toggle-split::after { + margin-left: 0; +} +.dropstart .dropdown-toggle-split::before { + margin-right: 0; +} + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; +} + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.75rem; +} + +.btn-group-vertical { + flex-direction: column; + align-items: flex-start; + justify-content: center; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group { + width: 100%; +} +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) { + margin-top: calc(-1 * var(--bs-border-width)); +} +.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group-vertical > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn:nth-child(n+3), +.btn-group-vertical > :not(.btn-check) + .btn, +.btn-group-vertical > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.nav { + --bs-nav-link-padding-x: 1rem; + --bs-nav-link-padding-y: 0.5rem; + --bs-nav-link-font-weight: ; + --bs-nav-link-color: var(--bs-link-color); + --bs-nav-link-hover-color: var(--bs-link-hover-color); + --bs-nav-link-disabled-color: var(--bs-secondary-color); + display: flex; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav-link { + display: block; + padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x); + font-size: var(--bs-nav-link-font-size); + font-weight: var(--bs-nav-link-font-weight); + color: var(--bs-nav-link-color); + text-decoration: none; + background: none; + border: 0; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .nav-link { + transition: none; + } +} +.nav-link:hover, .nav-link:focus { + color: var(--bs-nav-link-hover-color); +} +.nav-link:focus-visible { + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.nav-link.disabled, .nav-link:disabled { + color: var(--bs-nav-link-disabled-color); + pointer-events: none; + cursor: default; +} + +.nav-tabs { + --bs-nav-tabs-border-width: var(--bs-border-width); + --bs-nav-tabs-border-color: var(--bs-border-color); + --bs-nav-tabs-border-radius: var(--bs-border-radius); + --bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color); + --bs-nav-tabs-link-active-color: var(--bs-emphasis-color); + --bs-nav-tabs-link-active-bg: var(--bs-body-bg); + --bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg); + border-bottom: var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color); +} +.nav-tabs .nav-link { + margin-bottom: calc(-1 * var(--bs-nav-tabs-border-width)); + border: var(--bs-nav-tabs-border-width) solid transparent; + border-top-left-radius: var(--bs-nav-tabs-border-radius); + border-top-right-radius: var(--bs-nav-tabs-border-radius); +} +.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { + isolation: isolate; + border-color: var(--bs-nav-tabs-link-hover-border-color); +} +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: var(--bs-nav-tabs-link-active-color); + background-color: var(--bs-nav-tabs-link-active-bg); + border-color: var(--bs-nav-tabs-link-active-border-color); +} +.nav-tabs .dropdown-menu { + margin-top: calc(-1 * var(--bs-nav-tabs-border-width)); + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.nav-pills { + --bs-nav-pills-border-radius: var(--bs-border-radius); + --bs-nav-pills-link-active-color: #fff; + --bs-nav-pills-link-active-bg: #0d6efd; +} +.nav-pills .nav-link { + border-radius: var(--bs-nav-pills-border-radius); +} +.nav-pills .nav-link.active, +.nav-pills .show > .nav-link { + color: var(--bs-nav-pills-link-active-color); + background-color: var(--bs-nav-pills-link-active-bg); +} + +.nav-underline { + --bs-nav-underline-gap: 1rem; + --bs-nav-underline-border-width: 0.125rem; + --bs-nav-underline-link-active-color: var(--bs-emphasis-color); + gap: var(--bs-nav-underline-gap); +} +.nav-underline .nav-link { + padding-right: 0; + padding-left: 0; + border-bottom: var(--bs-nav-underline-border-width) solid transparent; +} +.nav-underline .nav-link:hover, .nav-underline .nav-link:focus { + border-bottom-color: currentcolor; +} +.nav-underline .nav-link.active, +.nav-underline .show > .nav-link { + font-weight: 700; + color: var(--bs-nav-underline-link-active-color); + border-bottom-color: currentcolor; +} + +.nav-fill > .nav-link, +.nav-fill .nav-item { + flex: 1 1 auto; + text-align: center; +} + +.nav-justified > .nav-link, +.nav-justified .nav-item { + flex-grow: 1; + flex-basis: 0; + text-align: center; +} + +.nav-fill .nav-item .nav-link, +.nav-justified .nav-item .nav-link { + width: 100%; +} + +.tab-content > .tab-pane { + display: none; +} +.tab-content > .active { + display: block; +} + +.navbar { + --bs-navbar-padding-x: 0; + --bs-navbar-padding-y: 0.5rem; + --bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65); + --bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), 0.8); + --bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3); + --bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1); + --bs-navbar-brand-padding-y: 0.3125rem; + --bs-navbar-brand-margin-end: 1rem; + --bs-navbar-brand-font-size: 1.25rem; + --bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1); + --bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1); + --bs-navbar-nav-link-padding-x: 0.5rem; + --bs-navbar-toggler-padding-y: 0.25rem; + --bs-navbar-toggler-padding-x: 0.75rem; + --bs-navbar-toggler-font-size: 1.25rem; + --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); + --bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15); + --bs-navbar-toggler-border-radius: var(--bs-border-radius); + --bs-navbar-toggler-focus-width: 0.25rem; + --bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out; + position: relative; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x); +} +.navbar > .container, +.navbar > .container-fluid, +.navbar > .container-sm, +.navbar > .container-md, +.navbar > .container-lg, +.navbar > .container-xl, +.navbar > .container-xxl { + display: flex; + flex-wrap: inherit; + align-items: center; + justify-content: space-between; +} +.navbar-brand { + padding-top: var(--bs-navbar-brand-padding-y); + padding-bottom: var(--bs-navbar-brand-padding-y); + margin-right: var(--bs-navbar-brand-margin-end); + font-size: var(--bs-navbar-brand-font-size); + color: var(--bs-navbar-brand-color); + text-decoration: none; + white-space: nowrap; +} +.navbar-brand:hover, .navbar-brand:focus { + color: var(--bs-navbar-brand-hover-color); +} + +.navbar-nav { + --bs-nav-link-padding-x: 0; + --bs-nav-link-padding-y: 0.5rem; + --bs-nav-link-font-weight: ; + --bs-nav-link-color: var(--bs-navbar-color); + --bs-nav-link-hover-color: var(--bs-navbar-hover-color); + --bs-nav-link-disabled-color: var(--bs-navbar-disabled-color); + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.navbar-nav .nav-link.active, .navbar-nav .nav-link.show { + color: var(--bs-navbar-active-color); +} +.navbar-nav .dropdown-menu { + position: static; +} + +.navbar-text { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + color: var(--bs-navbar-color); +} +.navbar-text a, +.navbar-text a:hover, +.navbar-text a:focus { + color: var(--bs-navbar-active-color); +} + +.navbar-collapse { + flex-grow: 1; + flex-basis: 100%; + align-items: center; +} + +.navbar-toggler { + padding: var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x); + font-size: var(--bs-navbar-toggler-font-size); + line-height: 1; + color: var(--bs-navbar-color); + background-color: transparent; + border: var(--bs-border-width) solid var(--bs-navbar-toggler-border-color); + border-radius: var(--bs-navbar-toggler-border-radius); + transition: var(--bs-navbar-toggler-transition); +} +@media (prefers-reduced-motion: reduce) { + .navbar-toggler { + transition: none; + } +} +.navbar-toggler:hover { + text-decoration: none; +} +.navbar-toggler:focus { + text-decoration: none; + outline: 0; + box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width); +} + +.navbar-toggler-icon { + display: inline-block; + width: 1.5em; + height: 1.5em; + vertical-align: middle; + background-image: var(--bs-navbar-toggler-icon-bg); + background-repeat: no-repeat; + background-position: center; + background-size: 100%; +} + +.navbar-nav-scroll { + max-height: var(--bs-scroll-height, 75vh); + overflow-y: auto; +} + +@media (min-width: 576px) { + .navbar-expand-sm { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-sm .navbar-nav { + flex-direction: row; + } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-sm .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-sm .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-sm .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-sm .navbar-toggler { + display: none; + } + .navbar-expand-sm .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-sm .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-sm .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 768px) { + .navbar-expand-md { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-md .navbar-nav { + flex-direction: row; + } + .navbar-expand-md .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-md .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-md .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-md .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-md .navbar-toggler { + display: none; + } + .navbar-expand-md .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-md .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-md .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 992px) { + .navbar-expand-lg { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-lg .navbar-nav { + flex-direction: row; + } + .navbar-expand-lg .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-lg .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-lg .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-lg .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-lg .navbar-toggler { + display: none; + } + .navbar-expand-lg .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-lg .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-lg .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 1200px) { + .navbar-expand-xl { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-xl .navbar-nav { + flex-direction: row; + } + .navbar-expand-xl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xl .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-xl .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-xl .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-xl .navbar-toggler { + display: none; + } + .navbar-expand-xl .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-xl .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-xl .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 1400px) { + .navbar-expand-xxl { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-xxl .navbar-nav { + flex-direction: row; + } + .navbar-expand-xxl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xxl .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-xxl .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-xxl .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-xxl .navbar-toggler { + display: none; + } + .navbar-expand-xxl .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-xxl .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-xxl .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +.navbar-expand { + flex-wrap: nowrap; + justify-content: flex-start; +} +.navbar-expand .navbar-nav { + flex-direction: row; +} +.navbar-expand .navbar-nav .dropdown-menu { + position: absolute; +} +.navbar-expand .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); +} +.navbar-expand .navbar-nav-scroll { + overflow: visible; +} +.navbar-expand .navbar-collapse { + display: flex !important; + flex-basis: auto; +} +.navbar-expand .navbar-toggler { + display: none; +} +.navbar-expand .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; +} +.navbar-expand .offcanvas .offcanvas-header { + display: none; +} +.navbar-expand .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; +} + +.navbar-dark, +.navbar[data-bs-theme=dark] { + --bs-navbar-color: rgba(255, 255, 255, 0.55); + --bs-navbar-hover-color: rgba(255, 255, 255, 0.75); + --bs-navbar-disabled-color: rgba(255, 255, 255, 0.25); + --bs-navbar-active-color: #fff; + --bs-navbar-brand-color: #fff; + --bs-navbar-brand-hover-color: #fff; + --bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1); + --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} + +[data-bs-theme=dark] .navbar-toggler-icon { + --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} + +.card { + --bs-card-spacer-y: 1rem; + --bs-card-spacer-x: 1rem; + --bs-card-title-spacer-y: 0.5rem; + --bs-card-title-color: ; + --bs-card-subtitle-color: ; + --bs-card-border-width: var(--bs-border-width); + --bs-card-border-color: var(--bs-border-color-translucent); + --bs-card-border-radius: var(--bs-border-radius); + --bs-card-box-shadow: ; + --bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width))); + --bs-card-cap-padding-y: 0.5rem; + --bs-card-cap-padding-x: 1rem; + --bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03); + --bs-card-cap-color: ; + --bs-card-height: ; + --bs-card-color: ; + --bs-card-bg: var(--bs-body-bg); + --bs-card-img-overlay-padding: 1rem; + --bs-card-group-margin: 0.75rem; + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + height: var(--bs-card-height); + color: var(--bs-body-color); + word-wrap: break-word; + background-color: var(--bs-card-bg); + background-clip: border-box; + border: var(--bs-card-border-width) solid var(--bs-card-border-color); + border-radius: var(--bs-card-border-radius); +} +.card > hr { + margin-right: 0; + margin-left: 0; +} +.card > .list-group { + border-top: inherit; + border-bottom: inherit; +} +.card > .list-group:first-child { + border-top-width: 0; + border-top-left-radius: var(--bs-card-inner-border-radius); + border-top-right-radius: var(--bs-card-inner-border-radius); +} +.card > .list-group:last-child { + border-bottom-width: 0; + border-bottom-right-radius: var(--bs-card-inner-border-radius); + border-bottom-left-radius: var(--bs-card-inner-border-radius); +} +.card > .card-header + .list-group, +.card > .list-group + .card-footer { + border-top: 0; +} + +.card-body { + flex: 1 1 auto; + padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x); + color: var(--bs-card-color); +} + +.card-title { + margin-bottom: var(--bs-card-title-spacer-y); + color: var(--bs-card-title-color); +} + +.card-subtitle { + margin-top: calc(-0.5 * var(--bs-card-title-spacer-y)); + margin-bottom: 0; + color: var(--bs-card-subtitle-color); +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link + .card-link { + margin-left: var(--bs-card-spacer-x); +} + +.card-header { + padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x); + margin-bottom: 0; + color: var(--bs-card-cap-color); + background-color: var(--bs-card-cap-bg); + border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color); +} +.card-header:first-child { + border-radius: var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0; +} + +.card-footer { + padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x); + color: var(--bs-card-cap-color); + background-color: var(--bs-card-cap-bg); + border-top: var(--bs-card-border-width) solid var(--bs-card-border-color); +} +.card-footer:last-child { + border-radius: 0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius); +} + +.card-header-tabs { + margin-right: calc(-0.5 * var(--bs-card-cap-padding-x)); + margin-bottom: calc(-1 * var(--bs-card-cap-padding-y)); + margin-left: calc(-0.5 * var(--bs-card-cap-padding-x)); + border-bottom: 0; +} +.card-header-tabs .nav-link.active { + background-color: var(--bs-card-bg); + border-bottom-color: var(--bs-card-bg); +} + +.card-header-pills { + margin-right: calc(-0.5 * var(--bs-card-cap-padding-x)); + margin-left: calc(-0.5 * var(--bs-card-cap-padding-x)); +} + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: var(--bs-card-img-overlay-padding); + border-radius: var(--bs-card-inner-border-radius); +} + +.card-img, +.card-img-top, +.card-img-bottom { + width: 100%; +} + +.card-img, +.card-img-top { + border-top-left-radius: var(--bs-card-inner-border-radius); + border-top-right-radius: var(--bs-card-inner-border-radius); +} + +.card-img, +.card-img-bottom { + border-bottom-right-radius: var(--bs-card-inner-border-radius); + border-bottom-left-radius: var(--bs-card-inner-border-radius); +} + +.card-group > .card { + margin-bottom: var(--bs-card-group-margin); +} +@media (min-width: 576px) { + .card-group { + display: flex; + flex-flow: row wrap; + } + .card-group > .card { + flex: 1 0 0; + margin-bottom: 0; + } + .card-group > .card + .card { + margin-left: 0; + border-left: 0; + } + .card-group > .card:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .card-group > .card:not(:last-child) > .card-img-top, + .card-group > .card:not(:last-child) > .card-header { + border-top-right-radius: 0; + } + .card-group > .card:not(:last-child) > .card-img-bottom, + .card-group > .card:not(:last-child) > .card-footer { + border-bottom-right-radius: 0; + } + .card-group > .card:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + .card-group > .card:not(:first-child) > .card-img-top, + .card-group > .card:not(:first-child) > .card-header { + border-top-left-radius: 0; + } + .card-group > .card:not(:first-child) > .card-img-bottom, + .card-group > .card:not(:first-child) > .card-footer { + border-bottom-left-radius: 0; + } +} + +.accordion { + --bs-accordion-color: var(--bs-body-color); + --bs-accordion-bg: var(--bs-body-bg); + --bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease; + --bs-accordion-border-color: var(--bs-border-color); + --bs-accordion-border-width: var(--bs-border-width); + --bs-accordion-border-radius: var(--bs-border-radius); + --bs-accordion-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width))); + --bs-accordion-btn-padding-x: 1.25rem; + --bs-accordion-btn-padding-y: 1rem; + --bs-accordion-btn-color: var(--bs-body-color); + --bs-accordion-btn-bg: var(--bs-accordion-bg); + --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); + --bs-accordion-btn-icon-width: 1.25rem; + --bs-accordion-btn-icon-transform: rotate(-180deg); + --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out; + --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); + --bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + --bs-accordion-body-padding-x: 1.25rem; + --bs-accordion-body-padding-y: 1rem; + --bs-accordion-active-color: var(--bs-primary-text-emphasis); + --bs-accordion-active-bg: var(--bs-primary-bg-subtle); +} + +.accordion-button { + position: relative; + display: flex; + align-items: center; + width: 100%; + padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x); + font-size: 1rem; + color: var(--bs-accordion-btn-color); + text-align: left; + background-color: var(--bs-accordion-btn-bg); + border: 0; + border-radius: 0; + overflow-anchor: none; + transition: var(--bs-accordion-transition); +} +@media (prefers-reduced-motion: reduce) { + .accordion-button { + transition: none; + } +} +.accordion-button:not(.collapsed) { + color: var(--bs-accordion-active-color); + background-color: var(--bs-accordion-active-bg); + box-shadow: inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color); +} +.accordion-button:not(.collapsed)::after { + background-image: var(--bs-accordion-btn-active-icon); + transform: var(--bs-accordion-btn-icon-transform); +} +.accordion-button::after { + flex-shrink: 0; + width: var(--bs-accordion-btn-icon-width); + height: var(--bs-accordion-btn-icon-width); + margin-left: auto; + content: ""; + background-image: var(--bs-accordion-btn-icon); + background-repeat: no-repeat; + background-size: var(--bs-accordion-btn-icon-width); + transition: var(--bs-accordion-btn-icon-transition); +} +@media (prefers-reduced-motion: reduce) { + .accordion-button::after { + transition: none; + } +} +.accordion-button:hover { + z-index: 2; +} +.accordion-button:focus { + z-index: 3; + outline: 0; + box-shadow: var(--bs-accordion-btn-focus-box-shadow); +} + +.accordion-header { + margin-bottom: 0; +} + +.accordion-item { + color: var(--bs-accordion-color); + background-color: var(--bs-accordion-bg); + border: var(--bs-accordion-border-width) solid var(--bs-accordion-border-color); +} +.accordion-item:first-of-type { + border-top-left-radius: var(--bs-accordion-border-radius); + border-top-right-radius: var(--bs-accordion-border-radius); +} +.accordion-item:first-of-type > .accordion-header .accordion-button { + border-top-left-radius: var(--bs-accordion-inner-border-radius); + border-top-right-radius: var(--bs-accordion-inner-border-radius); +} +.accordion-item:not(:first-of-type) { + border-top: 0; +} +.accordion-item:last-of-type { + border-bottom-right-radius: var(--bs-accordion-border-radius); + border-bottom-left-radius: var(--bs-accordion-border-radius); +} +.accordion-item:last-of-type > .accordion-header .accordion-button.collapsed { + border-bottom-right-radius: var(--bs-accordion-inner-border-radius); + border-bottom-left-radius: var(--bs-accordion-inner-border-radius); +} +.accordion-item:last-of-type > .accordion-collapse { + border-bottom-right-radius: var(--bs-accordion-border-radius); + border-bottom-left-radius: var(--bs-accordion-border-radius); +} + +.accordion-body { + padding: var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x); +} + +.accordion-flush > .accordion-item { + border-right: 0; + border-left: 0; + border-radius: 0; +} +.accordion-flush > .accordion-item:first-child { + border-top: 0; +} +.accordion-flush > .accordion-item:last-child { + border-bottom: 0; +} +.accordion-flush > .accordion-item > .accordion-collapse, +.accordion-flush > .accordion-item > .accordion-header .accordion-button, +.accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed { + border-radius: 0; +} + +[data-bs-theme=dark] .accordion-button::after { + --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e"); + --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e"); +} + +.breadcrumb { + --bs-breadcrumb-padding-x: 0; + --bs-breadcrumb-padding-y: 0; + --bs-breadcrumb-margin-bottom: 1rem; + --bs-breadcrumb-bg: ; + --bs-breadcrumb-border-radius: ; + --bs-breadcrumb-divider-color: var(--bs-secondary-color); + --bs-breadcrumb-item-padding-x: 0.5rem; + --bs-breadcrumb-item-active-color: var(--bs-secondary-color); + display: flex; + flex-wrap: wrap; + padding: var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x); + margin-bottom: var(--bs-breadcrumb-margin-bottom); + font-size: var(--bs-breadcrumb-font-size); + list-style: none; + background-color: var(--bs-breadcrumb-bg); + border-radius: var(--bs-breadcrumb-border-radius); +} + +.breadcrumb-item + .breadcrumb-item { + padding-left: var(--bs-breadcrumb-item-padding-x); +} +.breadcrumb-item + .breadcrumb-item::before { + float: left; + padding-right: var(--bs-breadcrumb-item-padding-x); + color: var(--bs-breadcrumb-divider-color); + content: var(--bs-breadcrumb-divider, "/") /* rtl: var(--bs-breadcrumb-divider, "/") */; +} +.breadcrumb-item.active { + color: var(--bs-breadcrumb-item-active-color); +} + +.pagination { + --bs-pagination-padding-x: 0.75rem; + --bs-pagination-padding-y: 0.375rem; + --bs-pagination-font-size: 1rem; + --bs-pagination-color: var(--bs-link-color); + --bs-pagination-bg: var(--bs-body-bg); + --bs-pagination-border-width: var(--bs-border-width); + --bs-pagination-border-color: var(--bs-border-color); + --bs-pagination-border-radius: var(--bs-border-radius); + --bs-pagination-hover-color: var(--bs-link-hover-color); + --bs-pagination-hover-bg: var(--bs-tertiary-bg); + --bs-pagination-hover-border-color: var(--bs-border-color); + --bs-pagination-focus-color: var(--bs-link-hover-color); + --bs-pagination-focus-bg: var(--bs-secondary-bg); + --bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + --bs-pagination-active-color: #fff; + --bs-pagination-active-bg: #0d6efd; + --bs-pagination-active-border-color: #0d6efd; + --bs-pagination-disabled-color: var(--bs-secondary-color); + --bs-pagination-disabled-bg: var(--bs-secondary-bg); + --bs-pagination-disabled-border-color: var(--bs-border-color); + display: flex; + padding-left: 0; + list-style: none; +} + +.page-link { + position: relative; + display: block; + padding: var(--bs-pagination-padding-y) var(--bs-pagination-padding-x); + font-size: var(--bs-pagination-font-size); + color: var(--bs-pagination-color); + text-decoration: none; + background-color: var(--bs-pagination-bg); + border: var(--bs-pagination-border-width) solid var(--bs-pagination-border-color); + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .page-link { + transition: none; + } +} +.page-link:hover { + z-index: 2; + color: var(--bs-pagination-hover-color); + background-color: var(--bs-pagination-hover-bg); + border-color: var(--bs-pagination-hover-border-color); +} +.page-link:focus { + z-index: 3; + color: var(--bs-pagination-focus-color); + background-color: var(--bs-pagination-focus-bg); + outline: 0; + box-shadow: var(--bs-pagination-focus-box-shadow); +} +.page-link.active, .active > .page-link { + z-index: 3; + color: var(--bs-pagination-active-color); + background-color: var(--bs-pagination-active-bg); + border-color: var(--bs-pagination-active-border-color); +} +.page-link.disabled, .disabled > .page-link { + color: var(--bs-pagination-disabled-color); + pointer-events: none; + background-color: var(--bs-pagination-disabled-bg); + border-color: var(--bs-pagination-disabled-border-color); +} + +.page-item:not(:first-child) .page-link { + margin-left: calc(-1 * var(--bs-border-width)); +} +.page-item:first-child .page-link { + border-top-left-radius: var(--bs-pagination-border-radius); + border-bottom-left-radius: var(--bs-pagination-border-radius); +} +.page-item:last-child .page-link { + border-top-right-radius: var(--bs-pagination-border-radius); + border-bottom-right-radius: var(--bs-pagination-border-radius); +} + +.pagination-lg { + --bs-pagination-padding-x: 1.5rem; + --bs-pagination-padding-y: 0.75rem; + --bs-pagination-font-size: 1.25rem; + --bs-pagination-border-radius: var(--bs-border-radius-lg); +} + +.pagination-sm { + --bs-pagination-padding-x: 0.5rem; + --bs-pagination-padding-y: 0.25rem; + --bs-pagination-font-size: 0.875rem; + --bs-pagination-border-radius: var(--bs-border-radius-sm); +} + +.badge { + --bs-badge-padding-x: 0.65em; + --bs-badge-padding-y: 0.35em; + --bs-badge-font-size: 0.75em; + --bs-badge-font-weight: 700; + --bs-badge-color: #fff; + --bs-badge-border-radius: var(--bs-border-radius); + display: inline-block; + padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x); + font-size: var(--bs-badge-font-size); + font-weight: var(--bs-badge-font-weight); + line-height: 1; + color: var(--bs-badge-color); + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: var(--bs-badge-border-radius); +} +.badge:empty { + display: none; +} + +.btn .badge { + position: relative; + top: -1px; +} + +.alert { + --bs-alert-bg: transparent; + --bs-alert-padding-x: 1rem; + --bs-alert-padding-y: 1rem; + --bs-alert-margin-bottom: 1rem; + --bs-alert-color: inherit; + --bs-alert-border-color: transparent; + --bs-alert-border: var(--bs-border-width) solid var(--bs-alert-border-color); + --bs-alert-border-radius: var(--bs-border-radius); + --bs-alert-link-color: inherit; + position: relative; + padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x); + margin-bottom: var(--bs-alert-margin-bottom); + color: var(--bs-alert-color); + background-color: var(--bs-alert-bg); + border: var(--bs-alert-border); + border-radius: var(--bs-alert-border-radius); +} + +.alert-heading { + color: inherit; +} + +.alert-link { + font-weight: 700; + color: var(--bs-alert-link-color); +} + +.alert-dismissible { + padding-right: 3rem; +} +.alert-dismissible .btn-close { + position: absolute; + top: 0; + right: 0; + z-index: 2; + padding: 1.25rem 1rem; +} + +.alert-primary { + --bs-alert-color: var(--bs-primary-text-emphasis); + --bs-alert-bg: var(--bs-primary-bg-subtle); + --bs-alert-border-color: var(--bs-primary-border-subtle); + --bs-alert-link-color: var(--bs-primary-text-emphasis); +} + +.alert-secondary { + --bs-alert-color: var(--bs-secondary-text-emphasis); + --bs-alert-bg: var(--bs-secondary-bg-subtle); + --bs-alert-border-color: var(--bs-secondary-border-subtle); + --bs-alert-link-color: var(--bs-secondary-text-emphasis); +} + +.alert-success { + --bs-alert-color: var(--bs-success-text-emphasis); + --bs-alert-bg: var(--bs-success-bg-subtle); + --bs-alert-border-color: var(--bs-success-border-subtle); + --bs-alert-link-color: var(--bs-success-text-emphasis); +} + +.alert-info { + --bs-alert-color: var(--bs-info-text-emphasis); + --bs-alert-bg: var(--bs-info-bg-subtle); + --bs-alert-border-color: var(--bs-info-border-subtle); + --bs-alert-link-color: var(--bs-info-text-emphasis); +} + +.alert-warning { + --bs-alert-color: var(--bs-warning-text-emphasis); + --bs-alert-bg: var(--bs-warning-bg-subtle); + --bs-alert-border-color: var(--bs-warning-border-subtle); + --bs-alert-link-color: var(--bs-warning-text-emphasis); +} + +.alert-danger { + --bs-alert-color: var(--bs-danger-text-emphasis); + --bs-alert-bg: var(--bs-danger-bg-subtle); + --bs-alert-border-color: var(--bs-danger-border-subtle); + --bs-alert-link-color: var(--bs-danger-text-emphasis); +} + +.alert-light { + --bs-alert-color: var(--bs-light-text-emphasis); + --bs-alert-bg: var(--bs-light-bg-subtle); + --bs-alert-border-color: var(--bs-light-border-subtle); + --bs-alert-link-color: var(--bs-light-text-emphasis); +} + +.alert-dark { + --bs-alert-color: var(--bs-dark-text-emphasis); + --bs-alert-bg: var(--bs-dark-bg-subtle); + --bs-alert-border-color: var(--bs-dark-border-subtle); + --bs-alert-link-color: var(--bs-dark-text-emphasis); +} + +@keyframes progress-bar-stripes { + 0% { + background-position-x: var(--bs-progress-height); + } +} +.progress, +.progress-stacked { + --bs-progress-height: 1rem; + --bs-progress-font-size: 0.75rem; + --bs-progress-bg: var(--bs-secondary-bg); + --bs-progress-border-radius: var(--bs-border-radius); + --bs-progress-box-shadow: var(--bs-box-shadow-inset); + --bs-progress-bar-color: #fff; + --bs-progress-bar-bg: #0d6efd; + --bs-progress-bar-transition: width 0.6s ease; + display: flex; + height: var(--bs-progress-height); + overflow: hidden; + font-size: var(--bs-progress-font-size); + background-color: var(--bs-progress-bg); + border-radius: var(--bs-progress-border-radius); +} + +.progress-bar { + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; + color: var(--bs-progress-bar-color); + text-align: center; + white-space: nowrap; + background-color: var(--bs-progress-bar-bg); + transition: var(--bs-progress-bar-transition); +} +@media (prefers-reduced-motion: reduce) { + .progress-bar { + transition: none; + } +} + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: var(--bs-progress-height) var(--bs-progress-height); +} + +.progress-stacked > .progress { + overflow: visible; +} + +.progress-stacked > .progress > .progress-bar { + width: 100%; +} + +.progress-bar-animated { + animation: 1s linear infinite progress-bar-stripes; +} +@media (prefers-reduced-motion: reduce) { + .progress-bar-animated { + animation: none; + } +} + +.list-group { + --bs-list-group-color: var(--bs-body-color); + --bs-list-group-bg: var(--bs-body-bg); + --bs-list-group-border-color: var(--bs-border-color); + --bs-list-group-border-width: var(--bs-border-width); + --bs-list-group-border-radius: var(--bs-border-radius); + --bs-list-group-item-padding-x: 1rem; + --bs-list-group-item-padding-y: 0.5rem; + --bs-list-group-action-color: var(--bs-secondary-color); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-tertiary-bg); + --bs-list-group-action-active-color: var(--bs-body-color); + --bs-list-group-action-active-bg: var(--bs-secondary-bg); + --bs-list-group-disabled-color: var(--bs-secondary-color); + --bs-list-group-disabled-bg: var(--bs-body-bg); + --bs-list-group-active-color: #fff; + --bs-list-group-active-bg: #0d6efd; + --bs-list-group-active-border-color: #0d6efd; + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + border-radius: var(--bs-list-group-border-radius); +} + +.list-group-numbered { + list-style-type: none; + counter-reset: section; +} +.list-group-numbered > .list-group-item::before { + content: counters(section, ".") ". "; + counter-increment: section; +} + +.list-group-item { + position: relative; + display: block; + padding: var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x); + color: var(--bs-list-group-color); + text-decoration: none; + background-color: var(--bs-list-group-bg); + border: var(--bs-list-group-border-width) solid var(--bs-list-group-border-color); +} +.list-group-item:first-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} +.list-group-item:last-child { + border-bottom-right-radius: inherit; + border-bottom-left-radius: inherit; +} +.list-group-item.disabled, .list-group-item:disabled { + color: var(--bs-list-group-disabled-color); + pointer-events: none; + background-color: var(--bs-list-group-disabled-bg); +} +.list-group-item.active { + z-index: 2; + color: var(--bs-list-group-active-color); + background-color: var(--bs-list-group-active-bg); + border-color: var(--bs-list-group-active-border-color); +} +.list-group-item + .list-group-item { + border-top-width: 0; +} +.list-group-item + .list-group-item.active { + margin-top: calc(-1 * var(--bs-list-group-border-width)); + border-top-width: var(--bs-list-group-border-width); +} + +.list-group-item-action { + width: 100%; + color: var(--bs-list-group-action-color); + text-align: inherit; +} +.list-group-item-action:not(.active):hover, .list-group-item-action:not(.active):focus { + z-index: 1; + color: var(--bs-list-group-action-hover-color); + text-decoration: none; + background-color: var(--bs-list-group-action-hover-bg); +} +.list-group-item-action:not(.active):active { + color: var(--bs-list-group-action-active-color); + background-color: var(--bs-list-group-action-active-bg); +} + +.list-group-horizontal { + flex-direction: row; +} +.list-group-horizontal > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; +} +.list-group-horizontal > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; +} +.list-group-horizontal > .list-group-item.active { + margin-top: 0; +} +.list-group-horizontal > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; +} +.list-group-horizontal > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); +} + +@media (min-width: 576px) { + .list-group-horizontal-sm { + flex-direction: row; + } + .list-group-horizontal-sm > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-sm > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-sm > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-sm > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-sm > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +@media (min-width: 768px) { + .list-group-horizontal-md { + flex-direction: row; + } + .list-group-horizontal-md > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-md > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-md > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-md > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-md > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +@media (min-width: 992px) { + .list-group-horizontal-lg { + flex-direction: row; + } + .list-group-horizontal-lg > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-lg > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-lg > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-lg > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-lg > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +@media (min-width: 1200px) { + .list-group-horizontal-xl { + flex-direction: row; + } + .list-group-horizontal-xl > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-xl > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-xl > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-xl > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-xl > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +@media (min-width: 1400px) { + .list-group-horizontal-xxl { + flex-direction: row; + } + .list-group-horizontal-xxl > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-xxl > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-xxl > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-xxl > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-xxl > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +.list-group-flush { + border-radius: 0; +} +.list-group-flush > .list-group-item { + border-width: 0 0 var(--bs-list-group-border-width); +} +.list-group-flush > .list-group-item:last-child { + border-bottom-width: 0; +} + +.list-group-item-primary { + --bs-list-group-color: var(--bs-primary-text-emphasis); + --bs-list-group-bg: var(--bs-primary-bg-subtle); + --bs-list-group-border-color: var(--bs-primary-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-primary-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-primary-border-subtle); + --bs-list-group-active-color: var(--bs-primary-bg-subtle); + --bs-list-group-active-bg: var(--bs-primary-text-emphasis); + --bs-list-group-active-border-color: var(--bs-primary-text-emphasis); +} + +.list-group-item-secondary { + --bs-list-group-color: var(--bs-secondary-text-emphasis); + --bs-list-group-bg: var(--bs-secondary-bg-subtle); + --bs-list-group-border-color: var(--bs-secondary-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-secondary-border-subtle); + --bs-list-group-active-color: var(--bs-secondary-bg-subtle); + --bs-list-group-active-bg: var(--bs-secondary-text-emphasis); + --bs-list-group-active-border-color: var(--bs-secondary-text-emphasis); +} + +.list-group-item-success { + --bs-list-group-color: var(--bs-success-text-emphasis); + --bs-list-group-bg: var(--bs-success-bg-subtle); + --bs-list-group-border-color: var(--bs-success-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-success-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-success-border-subtle); + --bs-list-group-active-color: var(--bs-success-bg-subtle); + --bs-list-group-active-bg: var(--bs-success-text-emphasis); + --bs-list-group-active-border-color: var(--bs-success-text-emphasis); +} + +.list-group-item-info { + --bs-list-group-color: var(--bs-info-text-emphasis); + --bs-list-group-bg: var(--bs-info-bg-subtle); + --bs-list-group-border-color: var(--bs-info-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-info-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-info-border-subtle); + --bs-list-group-active-color: var(--bs-info-bg-subtle); + --bs-list-group-active-bg: var(--bs-info-text-emphasis); + --bs-list-group-active-border-color: var(--bs-info-text-emphasis); +} + +.list-group-item-warning { + --bs-list-group-color: var(--bs-warning-text-emphasis); + --bs-list-group-bg: var(--bs-warning-bg-subtle); + --bs-list-group-border-color: var(--bs-warning-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-warning-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-warning-border-subtle); + --bs-list-group-active-color: var(--bs-warning-bg-subtle); + --bs-list-group-active-bg: var(--bs-warning-text-emphasis); + --bs-list-group-active-border-color: var(--bs-warning-text-emphasis); +} + +.list-group-item-danger { + --bs-list-group-color: var(--bs-danger-text-emphasis); + --bs-list-group-bg: var(--bs-danger-bg-subtle); + --bs-list-group-border-color: var(--bs-danger-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-danger-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-danger-border-subtle); + --bs-list-group-active-color: var(--bs-danger-bg-subtle); + --bs-list-group-active-bg: var(--bs-danger-text-emphasis); + --bs-list-group-active-border-color: var(--bs-danger-text-emphasis); +} + +.list-group-item-light { + --bs-list-group-color: var(--bs-light-text-emphasis); + --bs-list-group-bg: var(--bs-light-bg-subtle); + --bs-list-group-border-color: var(--bs-light-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-light-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-light-border-subtle); + --bs-list-group-active-color: var(--bs-light-bg-subtle); + --bs-list-group-active-bg: var(--bs-light-text-emphasis); + --bs-list-group-active-border-color: var(--bs-light-text-emphasis); +} + +.list-group-item-dark { + --bs-list-group-color: var(--bs-dark-text-emphasis); + --bs-list-group-bg: var(--bs-dark-bg-subtle); + --bs-list-group-border-color: var(--bs-dark-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-dark-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-dark-border-subtle); + --bs-list-group-active-color: var(--bs-dark-bg-subtle); + --bs-list-group-active-bg: var(--bs-dark-text-emphasis); + --bs-list-group-active-border-color: var(--bs-dark-text-emphasis); +} + +.btn-close { + --bs-btn-close-color: #000; + --bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e"); + --bs-btn-close-opacity: 0.5; + --bs-btn-close-hover-opacity: 0.75; + --bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + --bs-btn-close-focus-opacity: 1; + --bs-btn-close-disabled-opacity: 0.25; + box-sizing: content-box; + width: 1em; + height: 1em; + padding: 0.25em 0.25em; + color: var(--bs-btn-close-color); + background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat; + filter: var(--bs-btn-close-filter); + border: 0; + border-radius: 0.375rem; + opacity: var(--bs-btn-close-opacity); +} +.btn-close:hover { + color: var(--bs-btn-close-color); + text-decoration: none; + opacity: var(--bs-btn-close-hover-opacity); +} +.btn-close:focus { + outline: 0; + box-shadow: var(--bs-btn-close-focus-shadow); + opacity: var(--bs-btn-close-focus-opacity); +} +.btn-close:disabled, .btn-close.disabled { + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + opacity: var(--bs-btn-close-disabled-opacity); +} + +.btn-close-white { + --bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%); +} + +:root, +[data-bs-theme=light] { + --bs-btn-close-filter: ; +} + +[data-bs-theme=dark] { + --bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%); +} + +.toast { + --bs-toast-zindex: 1090; + --bs-toast-padding-x: 0.75rem; + --bs-toast-padding-y: 0.5rem; + --bs-toast-spacing: 1.5rem; + --bs-toast-max-width: 350px; + --bs-toast-font-size: 0.875rem; + --bs-toast-color: ; + --bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85); + --bs-toast-border-width: var(--bs-border-width); + --bs-toast-border-color: var(--bs-border-color-translucent); + --bs-toast-border-radius: var(--bs-border-radius); + --bs-toast-box-shadow: var(--bs-box-shadow); + --bs-toast-header-color: var(--bs-secondary-color); + --bs-toast-header-bg: rgba(var(--bs-body-bg-rgb), 0.85); + --bs-toast-header-border-color: var(--bs-border-color-translucent); + width: var(--bs-toast-max-width); + max-width: 100%; + font-size: var(--bs-toast-font-size); + color: var(--bs-toast-color); + pointer-events: auto; + background-color: var(--bs-toast-bg); + background-clip: padding-box; + border: var(--bs-toast-border-width) solid var(--bs-toast-border-color); + box-shadow: var(--bs-toast-box-shadow); + border-radius: var(--bs-toast-border-radius); +} +.toast.showing { + opacity: 0; +} +.toast:not(.show) { + display: none; +} + +.toast-container { + --bs-toast-zindex: 1090; + position: absolute; + z-index: var(--bs-toast-zindex); + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + max-width: 100%; + pointer-events: none; +} +.toast-container > :not(:last-child) { + margin-bottom: var(--bs-toast-spacing); +} + +.toast-header { + display: flex; + align-items: center; + padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x); + color: var(--bs-toast-header-color); + background-color: var(--bs-toast-header-bg); + background-clip: padding-box; + border-bottom: var(--bs-toast-border-width) solid var(--bs-toast-header-border-color); + border-top-left-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width)); + border-top-right-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width)); +} +.toast-header .btn-close { + margin-right: calc(-0.5 * var(--bs-toast-padding-x)); + margin-left: var(--bs-toast-padding-x); +} + +.toast-body { + padding: var(--bs-toast-padding-x); + word-wrap: break-word; +} + +.modal { + --bs-modal-zindex: 1055; + --bs-modal-width: 500px; + --bs-modal-padding: 1rem; + --bs-modal-margin: 0.5rem; + --bs-modal-color: var(--bs-body-color); + --bs-modal-bg: var(--bs-body-bg); + --bs-modal-border-color: var(--bs-border-color-translucent); + --bs-modal-border-width: var(--bs-border-width); + --bs-modal-border-radius: var(--bs-border-radius-lg); + --bs-modal-box-shadow: var(--bs-box-shadow-sm); + --bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width))); + --bs-modal-header-padding-x: 1rem; + --bs-modal-header-padding-y: 1rem; + --bs-modal-header-padding: 1rem 1rem; + --bs-modal-header-border-color: var(--bs-border-color); + --bs-modal-header-border-width: var(--bs-border-width); + --bs-modal-title-line-height: 1.5; + --bs-modal-footer-gap: 0.5rem; + --bs-modal-footer-bg: ; + --bs-modal-footer-border-color: var(--bs-border-color); + --bs-modal-footer-border-width: var(--bs-border-width); + position: fixed; + top: 0; + left: 0; + z-index: var(--bs-modal-zindex); + display: none; + width: 100%; + height: 100%; + overflow-x: hidden; + overflow-y: auto; + outline: 0; +} + +.modal-dialog { + position: relative; + width: auto; + margin: var(--bs-modal-margin); + pointer-events: none; +} +.modal.fade .modal-dialog { + transform: translate(0, -50px); + transition: transform 0.3s ease-out; +} +@media (prefers-reduced-motion: reduce) { + .modal.fade .modal-dialog { + transition: none; + } +} +.modal.show .modal-dialog { + transform: none; +} +.modal.modal-static .modal-dialog { + transform: scale(1.02); +} + +.modal-dialog-scrollable { + height: calc(100% - var(--bs-modal-margin) * 2); +} +.modal-dialog-scrollable .modal-content { + max-height: 100%; + overflow: hidden; +} +.modal-dialog-scrollable .modal-body { + overflow-y: auto; +} + +.modal-dialog-centered { + display: flex; + align-items: center; + min-height: calc(100% - var(--bs-modal-margin) * 2); +} + +.modal-content { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + color: var(--bs-modal-color); + pointer-events: auto; + background-color: var(--bs-modal-bg); + background-clip: padding-box; + border: var(--bs-modal-border-width) solid var(--bs-modal-border-color); + border-radius: var(--bs-modal-border-radius); + outline: 0; +} + +.modal-backdrop { + --bs-backdrop-zindex: 1050; + --bs-backdrop-bg: #000; + --bs-backdrop-opacity: 0.5; + position: fixed; + top: 0; + left: 0; + z-index: var(--bs-backdrop-zindex); + width: 100vw; + height: 100vh; + background-color: var(--bs-backdrop-bg); +} +.modal-backdrop.fade { + opacity: 0; +} +.modal-backdrop.show { + opacity: var(--bs-backdrop-opacity); +} + +.modal-header { + display: flex; + flex-shrink: 0; + align-items: center; + padding: var(--bs-modal-header-padding); + border-bottom: var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color); + border-top-left-radius: var(--bs-modal-inner-border-radius); + border-top-right-radius: var(--bs-modal-inner-border-radius); +} +.modal-header .btn-close { + padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5); + margin-top: calc(-0.5 * var(--bs-modal-header-padding-y)); + margin-right: calc(-0.5 * var(--bs-modal-header-padding-x)); + margin-bottom: calc(-0.5 * var(--bs-modal-header-padding-y)); + margin-left: auto; +} + +.modal-title { + margin-bottom: 0; + line-height: var(--bs-modal-title-line-height); +} + +.modal-body { + position: relative; + flex: 1 1 auto; + padding: var(--bs-modal-padding); +} + +.modal-footer { + display: flex; + flex-shrink: 0; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + padding: calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * 0.5); + background-color: var(--bs-modal-footer-bg); + border-top: var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color); + border-bottom-right-radius: var(--bs-modal-inner-border-radius); + border-bottom-left-radius: var(--bs-modal-inner-border-radius); +} +.modal-footer > * { + margin: calc(var(--bs-modal-footer-gap) * 0.5); +} + +@media (min-width: 576px) { + .modal { + --bs-modal-margin: 1.75rem; + --bs-modal-box-shadow: var(--bs-box-shadow); + } + .modal-dialog { + max-width: var(--bs-modal-width); + margin-right: auto; + margin-left: auto; + } + .modal-sm { + --bs-modal-width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg, + .modal-xl { + --bs-modal-width: 800px; + } +} +@media (min-width: 1200px) { + .modal-xl { + --bs-modal-width: 1140px; + } +} +.modal-fullscreen { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; +} +.modal-fullscreen .modal-content { + height: 100%; + border: 0; + border-radius: 0; +} +.modal-fullscreen .modal-header, +.modal-fullscreen .modal-footer { + border-radius: 0; +} +.modal-fullscreen .modal-body { + overflow-y: auto; +} + +@media (max-width: 575.98px) { + .modal-fullscreen-sm-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-sm-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-sm-down .modal-header, + .modal-fullscreen-sm-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-sm-down .modal-body { + overflow-y: auto; + } +} +@media (max-width: 767.98px) { + .modal-fullscreen-md-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-md-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-md-down .modal-header, + .modal-fullscreen-md-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-md-down .modal-body { + overflow-y: auto; + } +} +@media (max-width: 991.98px) { + .modal-fullscreen-lg-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-lg-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-lg-down .modal-header, + .modal-fullscreen-lg-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-lg-down .modal-body { + overflow-y: auto; + } +} +@media (max-width: 1199.98px) { + .modal-fullscreen-xl-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-xl-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-xl-down .modal-header, + .modal-fullscreen-xl-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-xl-down .modal-body { + overflow-y: auto; + } +} +@media (max-width: 1399.98px) { + .modal-fullscreen-xxl-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-xxl-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-xxl-down .modal-header, + .modal-fullscreen-xxl-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-xxl-down .modal-body { + overflow-y: auto; + } +} +.tooltip { + --bs-tooltip-zindex: 1080; + --bs-tooltip-max-width: 200px; + --bs-tooltip-padding-x: 0.5rem; + --bs-tooltip-padding-y: 0.25rem; + --bs-tooltip-margin: ; + --bs-tooltip-font-size: 0.875rem; + --bs-tooltip-color: var(--bs-body-bg); + --bs-tooltip-bg: var(--bs-emphasis-color); + --bs-tooltip-border-radius: var(--bs-border-radius); + --bs-tooltip-opacity: 0.9; + --bs-tooltip-arrow-width: 0.8rem; + --bs-tooltip-arrow-height: 0.4rem; + z-index: var(--bs-tooltip-zindex); + display: block; + margin: var(--bs-tooltip-margin); + font-family: var(--bs-font-sans-serif); + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + white-space: normal; + word-spacing: normal; + line-break: auto; + font-size: var(--bs-tooltip-font-size); + word-wrap: break-word; + opacity: 0; +} +.tooltip.show { + opacity: var(--bs-tooltip-opacity); +} +.tooltip .tooltip-arrow { + display: block; + width: var(--bs-tooltip-arrow-width); + height: var(--bs-tooltip-arrow-height); +} +.tooltip .tooltip-arrow::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow { + bottom: calc(-1 * var(--bs-tooltip-arrow-height)); +} +.bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before { + top: -1px; + border-width: var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0; + border-top-color: var(--bs-tooltip-bg); +} + +/* rtl:begin:ignore */ +.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow { + left: calc(-1 * var(--bs-tooltip-arrow-height)); + width: var(--bs-tooltip-arrow-height); + height: var(--bs-tooltip-arrow-width); +} +.bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before { + right: -1px; + border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0; + border-right-color: var(--bs-tooltip-bg); +} + +/* rtl:end:ignore */ +.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow { + top: calc(-1 * var(--bs-tooltip-arrow-height)); +} +.bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before { + bottom: -1px; + border-width: 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height); + border-bottom-color: var(--bs-tooltip-bg); +} + +/* rtl:begin:ignore */ +.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow { + right: calc(-1 * var(--bs-tooltip-arrow-height)); + width: var(--bs-tooltip-arrow-height); + height: var(--bs-tooltip-arrow-width); +} +.bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before { + left: -1px; + border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height); + border-left-color: var(--bs-tooltip-bg); +} + +/* rtl:end:ignore */ +.tooltip-inner { + max-width: var(--bs-tooltip-max-width); + padding: var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x); + color: var(--bs-tooltip-color); + text-align: center; + background-color: var(--bs-tooltip-bg); + border-radius: var(--bs-tooltip-border-radius); +} + +.popover { + --bs-popover-zindex: 1070; + --bs-popover-max-width: 276px; + --bs-popover-font-size: 0.875rem; + --bs-popover-bg: var(--bs-body-bg); + --bs-popover-border-width: var(--bs-border-width); + --bs-popover-border-color: var(--bs-border-color-translucent); + --bs-popover-border-radius: var(--bs-border-radius-lg); + --bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width)); + --bs-popover-box-shadow: var(--bs-box-shadow); + --bs-popover-header-padding-x: 1rem; + --bs-popover-header-padding-y: 0.5rem; + --bs-popover-header-font-size: 1rem; + --bs-popover-header-color: inherit; + --bs-popover-header-bg: var(--bs-secondary-bg); + --bs-popover-body-padding-x: 1rem; + --bs-popover-body-padding-y: 1rem; + --bs-popover-body-color: var(--bs-body-color); + --bs-popover-arrow-width: 1rem; + --bs-popover-arrow-height: 0.5rem; + --bs-popover-arrow-border: var(--bs-popover-border-color); + z-index: var(--bs-popover-zindex); + display: block; + max-width: var(--bs-popover-max-width); + font-family: var(--bs-font-sans-serif); + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + white-space: normal; + word-spacing: normal; + line-break: auto; + font-size: var(--bs-popover-font-size); + word-wrap: break-word; + background-color: var(--bs-popover-bg); + background-clip: padding-box; + border: var(--bs-popover-border-width) solid var(--bs-popover-border-color); + border-radius: var(--bs-popover-border-radius); +} +.popover .popover-arrow { + display: block; + width: var(--bs-popover-arrow-width); + height: var(--bs-popover-arrow-height); +} +.popover .popover-arrow::before, .popover .popover-arrow::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; + border-width: 0; +} + +.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow { + bottom: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); +} +.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before, .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after { + border-width: var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0; +} +.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before { + bottom: 0; + border-top-color: var(--bs-popover-arrow-border); +} +.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after { + bottom: var(--bs-popover-border-width); + border-top-color: var(--bs-popover-bg); +} + +/* rtl:begin:ignore */ +.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow { + left: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); + width: var(--bs-popover-arrow-height); + height: var(--bs-popover-arrow-width); +} +.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before, .bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after { + border-width: calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0; +} +.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before { + left: 0; + border-right-color: var(--bs-popover-arrow-border); +} +.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after { + left: var(--bs-popover-border-width); + border-right-color: var(--bs-popover-bg); +} + +/* rtl:end:ignore */ +.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow { + top: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); +} +.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before, .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after { + border-width: 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height); +} +.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before { + top: 0; + border-bottom-color: var(--bs-popover-arrow-border); +} +.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after { + top: var(--bs-popover-border-width); + border-bottom-color: var(--bs-popover-bg); +} +.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=bottom] .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: var(--bs-popover-arrow-width); + margin-left: calc(-0.5 * var(--bs-popover-arrow-width)); + content: ""; + border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-header-bg); +} + +/* rtl:begin:ignore */ +.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow { + right: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); + width: var(--bs-popover-arrow-height); + height: var(--bs-popover-arrow-width); +} +.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before, .bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after { + border-width: calc(var(--bs-popover-arrow-width) * 0.5) 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height); +} +.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before { + right: 0; + border-left-color: var(--bs-popover-arrow-border); +} +.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after { + right: var(--bs-popover-border-width); + border-left-color: var(--bs-popover-bg); +} + +/* rtl:end:ignore */ +.popover-header { + padding: var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x); + margin-bottom: 0; + font-size: var(--bs-popover-header-font-size); + color: var(--bs-popover-header-color); + background-color: var(--bs-popover-header-bg); + border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-border-color); + border-top-left-radius: var(--bs-popover-inner-border-radius); + border-top-right-radius: var(--bs-popover-inner-border-radius); +} +.popover-header:empty { + display: none; +} + +.popover-body { + padding: var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x); + color: var(--bs-popover-body-color); +} + +.carousel { + position: relative; +} + +.carousel.pointer-event { + touch-action: pan-y; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} +.carousel-inner::after { + display: block; + clear: both; + content: ""; +} + +.carousel-item { + position: relative; + display: none; + float: left; + width: 100%; + margin-right: -100%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + transition: transform 0.6s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .carousel-item { + transition: none; + } +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +.carousel-item-next:not(.carousel-item-start), +.active.carousel-item-end { + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-end), +.active.carousel-item-start { + transform: translateX(-100%); +} + +.carousel-fade .carousel-item { + opacity: 0; + transition-property: opacity; + transform: none; +} +.carousel-fade .carousel-item.active, +.carousel-fade .carousel-item-next.carousel-item-start, +.carousel-fade .carousel-item-prev.carousel-item-end { + z-index: 1; + opacity: 1; +} +.carousel-fade .active.carousel-item-start, +.carousel-fade .active.carousel-item-end { + z-index: 0; + opacity: 0; + transition: opacity 0s 0.6s; +} +@media (prefers-reduced-motion: reduce) { + .carousel-fade .active.carousel-item-start, + .carousel-fade .active.carousel-item-end { + transition: none; + } +} + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + width: 15%; + padding: 0; + color: #fff; + text-align: center; + background: none; + filter: var(--bs-carousel-control-icon-filter); + border: 0; + opacity: 0.5; + transition: opacity 0.15s ease; +} +@media (prefers-reduced-motion: reduce) { + .carousel-control-prev, + .carousel-control-next { + transition: none; + } +} +.carousel-control-prev:hover, .carousel-control-prev:focus, +.carousel-control-next:hover, +.carousel-control-next:focus { + color: #fff; + text-decoration: none; + outline: 0; + opacity: 0.9; +} + +.carousel-control-prev { + left: 0; +} + +.carousel-control-next { + right: 0; +} + +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: 2rem; + height: 2rem; + background-repeat: no-repeat; + background-position: 50%; + background-size: 100% 100%; +} + +.carousel-control-prev-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")*/; +} + +.carousel-control-next-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e")*/; +} + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + display: flex; + justify-content: center; + padding: 0; + margin-right: 15%; + margin-bottom: 1rem; + margin-left: 15%; +} +.carousel-indicators [data-bs-target] { + box-sizing: content-box; + flex: 0 1 auto; + width: 30px; + height: 3px; + padding: 0; + margin-right: 3px; + margin-left: 3px; + text-indent: -999px; + cursor: pointer; + background-color: var(--bs-carousel-indicator-active-bg); + background-clip: padding-box; + border: 0; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + opacity: 0.5; + transition: opacity 0.6s ease; +} +@media (prefers-reduced-motion: reduce) { + .carousel-indicators [data-bs-target] { + transition: none; + } +} +.carousel-indicators .active { + opacity: 1; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 1.25rem; + left: 15%; + padding-top: 1.25rem; + padding-bottom: 1.25rem; + color: var(--bs-carousel-caption-color); + text-align: center; +} + +.carousel-dark { + --bs-carousel-indicator-active-bg: #000; + --bs-carousel-caption-color: #000; + --bs-carousel-control-icon-filter: invert(1) grayscale(100); +} + +:root, +[data-bs-theme=light] { + --bs-carousel-indicator-active-bg: #fff; + --bs-carousel-caption-color: #fff; + --bs-carousel-control-icon-filter: ; +} + +[data-bs-theme=dark] { + --bs-carousel-indicator-active-bg: #000; + --bs-carousel-caption-color: #000; + --bs-carousel-control-icon-filter: invert(1) grayscale(100); +} + +.spinner-grow, +.spinner-border { + display: inline-block; + flex-shrink: 0; + width: var(--bs-spinner-width); + height: var(--bs-spinner-height); + vertical-align: var(--bs-spinner-vertical-align); + border-radius: 50%; + animation: var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name); +} + +@keyframes spinner-border { + to { + transform: rotate(360deg) /* rtl:ignore */; + } +} +.spinner-border { + --bs-spinner-width: 2rem; + --bs-spinner-height: 2rem; + --bs-spinner-vertical-align: -0.125em; + --bs-spinner-border-width: 0.25em; + --bs-spinner-animation-speed: 0.75s; + --bs-spinner-animation-name: spinner-border; + border: var(--bs-spinner-border-width) solid currentcolor; + border-right-color: transparent; +} + +.spinner-border-sm { + --bs-spinner-width: 1rem; + --bs-spinner-height: 1rem; + --bs-spinner-border-width: 0.2em; +} + +@keyframes spinner-grow { + 0% { + transform: scale(0); + } + 50% { + opacity: 1; + transform: none; + } +} +.spinner-grow { + --bs-spinner-width: 2rem; + --bs-spinner-height: 2rem; + --bs-spinner-vertical-align: -0.125em; + --bs-spinner-animation-speed: 0.75s; + --bs-spinner-animation-name: spinner-grow; + background-color: currentcolor; + opacity: 0; +} + +.spinner-grow-sm { + --bs-spinner-width: 1rem; + --bs-spinner-height: 1rem; +} + +@media (prefers-reduced-motion: reduce) { + .spinner-border, + .spinner-grow { + --bs-spinner-animation-speed: 1.5s; + } +} +.offcanvas, .offcanvas-xxl, .offcanvas-xl, .offcanvas-lg, .offcanvas-md, .offcanvas-sm { + --bs-offcanvas-zindex: 1045; + --bs-offcanvas-width: 400px; + --bs-offcanvas-height: 30vh; + --bs-offcanvas-padding-x: 1rem; + --bs-offcanvas-padding-y: 1rem; + --bs-offcanvas-color: var(--bs-body-color); + --bs-offcanvas-bg: var(--bs-body-bg); + --bs-offcanvas-border-width: var(--bs-border-width); + --bs-offcanvas-border-color: var(--bs-border-color-translucent); + --bs-offcanvas-box-shadow: var(--bs-box-shadow-sm); + --bs-offcanvas-transition: transform 0.3s ease-in-out; + --bs-offcanvas-title-line-height: 1.5; +} + +@media (max-width: 575.98px) { + .offcanvas-sm { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 575.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-sm { + transition: none; + } +} +@media (max-width: 575.98px) { + .offcanvas-sm.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-sm.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-sm.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-sm.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-sm.showing, .offcanvas-sm.show:not(.hiding) { + transform: none; + } + .offcanvas-sm.showing, .offcanvas-sm.hiding, .offcanvas-sm.show { + visibility: visible; + } +} +@media (min-width: 576px) { + .offcanvas-sm { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-sm .offcanvas-header { + display: none; + } + .offcanvas-sm .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 767.98px) { + .offcanvas-md { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 767.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-md { + transition: none; + } +} +@media (max-width: 767.98px) { + .offcanvas-md.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-md.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-md.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-md.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-md.showing, .offcanvas-md.show:not(.hiding) { + transform: none; + } + .offcanvas-md.showing, .offcanvas-md.hiding, .offcanvas-md.show { + visibility: visible; + } +} +@media (min-width: 768px) { + .offcanvas-md { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-md .offcanvas-header { + display: none; + } + .offcanvas-md .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 991.98px) { + .offcanvas-lg { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 991.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-lg { + transition: none; + } +} +@media (max-width: 991.98px) { + .offcanvas-lg.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-lg.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-lg.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-lg.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-lg.showing, .offcanvas-lg.show:not(.hiding) { + transform: none; + } + .offcanvas-lg.showing, .offcanvas-lg.hiding, .offcanvas-lg.show { + visibility: visible; + } +} +@media (min-width: 992px) { + .offcanvas-lg { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-lg .offcanvas-header { + display: none; + } + .offcanvas-lg .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 1199.98px) { + .offcanvas-xl { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 1199.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-xl { + transition: none; + } +} +@media (max-width: 1199.98px) { + .offcanvas-xl.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-xl.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-xl.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-xl.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-xl.showing, .offcanvas-xl.show:not(.hiding) { + transform: none; + } + .offcanvas-xl.showing, .offcanvas-xl.hiding, .offcanvas-xl.show { + visibility: visible; + } +} +@media (min-width: 1200px) { + .offcanvas-xl { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-xl .offcanvas-header { + display: none; + } + .offcanvas-xl .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 1399.98px) { + .offcanvas-xxl { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 1399.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-xxl { + transition: none; + } +} +@media (max-width: 1399.98px) { + .offcanvas-xxl.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-xxl.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-xxl.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-xxl.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-xxl.showing, .offcanvas-xxl.show:not(.hiding) { + transform: none; + } + .offcanvas-xxl.showing, .offcanvas-xxl.hiding, .offcanvas-xxl.show { + visibility: visible; + } +} +@media (min-width: 1400px) { + .offcanvas-xxl { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-xxl .offcanvas-header { + display: none; + } + .offcanvas-xxl .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +.offcanvas { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); +} +@media (prefers-reduced-motion: reduce) { + .offcanvas { + transition: none; + } +} +.offcanvas.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); +} +.offcanvas.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); +} +.offcanvas.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); +} +.offcanvas.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); +} +.offcanvas.showing, .offcanvas.show:not(.hiding) { + transform: none; +} +.offcanvas.showing, .offcanvas.hiding, .offcanvas.show { + visibility: visible; +} + +.offcanvas-backdrop { + position: fixed; + top: 0; + left: 0; + z-index: 1040; + width: 100vw; + height: 100vh; + background-color: #000; +} +.offcanvas-backdrop.fade { + opacity: 0; +} +.offcanvas-backdrop.show { + opacity: 0.5; +} + +.offcanvas-header { + display: flex; + align-items: center; + padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x); +} +.offcanvas-header .btn-close { + padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5); + margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y)); + margin-right: calc(-0.5 * var(--bs-offcanvas-padding-x)); + margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y)); + margin-left: auto; +} + +.offcanvas-title { + margin-bottom: 0; + line-height: var(--bs-offcanvas-title-line-height); +} + +.offcanvas-body { + flex-grow: 1; + padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x); + overflow-y: auto; +} + +.placeholder { + display: inline-block; + min-height: 1em; + vertical-align: middle; + cursor: wait; + background-color: currentcolor; + opacity: 0.5; +} +.placeholder.btn::before { + display: inline-block; + content: ""; +} + +.placeholder-xs { + min-height: 0.6em; +} + +.placeholder-sm { + min-height: 0.8em; +} + +.placeholder-lg { + min-height: 1.2em; +} + +.placeholder-glow .placeholder { + animation: placeholder-glow 2s ease-in-out infinite; +} + +@keyframes placeholder-glow { + 50% { + opacity: 0.2; + } +} +.placeholder-wave { + -webkit-mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); + mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); + -webkit-mask-size: 200% 100%; + mask-size: 200% 100%; + animation: placeholder-wave 2s linear infinite; +} + +@keyframes placeholder-wave { + 100% { + -webkit-mask-position: -200% 0%; + mask-position: -200% 0%; + } +} +.clearfix::after { + display: block; + clear: both; + content: ""; +} + +.text-bg-primary { + color: #fff !important; + background-color: RGBA(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-secondary { + color: #fff !important; + background-color: RGBA(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-success { + color: #fff !important; + background-color: RGBA(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-info { + color: #000 !important; + background-color: RGBA(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-warning { + color: #000 !important; + background-color: RGBA(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-danger { + color: #fff !important; + background-color: RGBA(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-light { + color: #000 !important; + background-color: RGBA(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-dark { + color: #fff !important; + background-color: RGBA(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.link-primary { + color: RGBA(var(--bs-primary-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-primary:hover, .link-primary:focus { + color: RGBA(10, 88, 202, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(10, 88, 202, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(10, 88, 202, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-secondary { + color: RGBA(var(--bs-secondary-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-secondary:hover, .link-secondary:focus { + color: RGBA(86, 94, 100, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(86, 94, 100, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(86, 94, 100, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-success { + color: RGBA(var(--bs-success-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-success:hover, .link-success:focus { + color: RGBA(20, 108, 67, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(20, 108, 67, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(20, 108, 67, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-info { + color: RGBA(var(--bs-info-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-info:hover, .link-info:focus { + color: RGBA(61, 213, 243, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(61, 213, 243, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(61, 213, 243, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-warning { + color: RGBA(var(--bs-warning-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-warning:hover, .link-warning:focus { + color: RGBA(255, 205, 57, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(255, 205, 57, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(255, 205, 57, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-danger { + color: RGBA(var(--bs-danger-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-danger:hover, .link-danger:focus { + color: RGBA(176, 42, 55, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(176, 42, 55, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(176, 42, 55, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-light { + color: RGBA(var(--bs-light-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-light:hover, .link-light:focus { + color: RGBA(249, 250, 251, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-dark { + color: RGBA(var(--bs-dark-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-dark:hover, .link-dark:focus { + color: RGBA(26, 30, 33, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(26, 30, 33, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(26, 30, 33, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-body-emphasis { + color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-body-emphasis:hover, .link-body-emphasis:focus { + color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 0.75)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important; + text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important; +} + +.focus-ring:focus { + outline: 0; + box-shadow: var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color); +} + +.icon-link { + display: inline-flex; + gap: 0.375rem; + align-items: center; + -webkit-text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5)); + text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5)); + text-underline-offset: 0.25em; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; +} +.icon-link > .bi { + flex-shrink: 0; + width: 1em; + height: 1em; + fill: currentcolor; + transition: 0.2s ease-in-out transform; +} +@media (prefers-reduced-motion: reduce) { + .icon-link > .bi { + transition: none; + } +} + +.icon-link-hover:hover > .bi, .icon-link-hover:focus-visible > .bi { + transform: var(--bs-icon-link-transform, translate3d(0.25em, 0, 0)); +} + +.ratio { + position: relative; + width: 100%; +} +.ratio::before { + display: block; + padding-top: var(--bs-aspect-ratio); + content: ""; +} +.ratio > * { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.ratio-1x1 { + --bs-aspect-ratio: 100%; +} + +.ratio-4x3 { + --bs-aspect-ratio: 75%; +} + +.ratio-16x9 { + --bs-aspect-ratio: 56.25%; +} + +.ratio-21x9 { + --bs-aspect-ratio: 42.8571428571%; +} + +.fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} + +.fixed-bottom { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; +} + +.sticky-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; +} + +.sticky-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; +} + +@media (min-width: 576px) { + .sticky-sm-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-sm-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } +} +@media (min-width: 768px) { + .sticky-md-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-md-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } +} +@media (min-width: 992px) { + .sticky-lg-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-lg-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } +} +@media (min-width: 1200px) { + .sticky-xl-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-xl-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } +} +@media (min-width: 1400px) { + .sticky-xxl-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-xxl-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } +} +.hstack { + display: flex; + flex-direction: row; + align-items: center; + align-self: stretch; +} + +.vstack { + display: flex; + flex: 1 1 auto; + flex-direction: column; + align-self: stretch; +} + +.visually-hidden, +.visually-hidden-focusable:not(:focus):not(:focus-within) { + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} +.visually-hidden:not(caption), +.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) { + position: absolute !important; +} +.visually-hidden *, +.visually-hidden-focusable:not(:focus):not(:focus-within) * { + overflow: hidden !important; +} + +.stretched-link::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + content: ""; +} + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.vr { + display: inline-block; + align-self: stretch; + width: var(--bs-border-width); + min-height: 1em; + background-color: currentcolor; + opacity: 0.25; +} + +.align-baseline { + vertical-align: baseline !important; +} + +.align-top { + vertical-align: top !important; +} + +.align-middle { + vertical-align: middle !important; +} + +.align-bottom { + vertical-align: bottom !important; +} + +.align-text-bottom { + vertical-align: text-bottom !important; +} + +.align-text-top { + vertical-align: text-top !important; +} + +.float-start { + float: left !important; +} + +.float-end { + float: right !important; +} + +.float-none { + float: none !important; +} + +.object-fit-contain { + -o-object-fit: contain !important; + object-fit: contain !important; +} + +.object-fit-cover { + -o-object-fit: cover !important; + object-fit: cover !important; +} + +.object-fit-fill { + -o-object-fit: fill !important; + object-fit: fill !important; +} + +.object-fit-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; +} + +.object-fit-none { + -o-object-fit: none !important; + object-fit: none !important; +} + +.opacity-0 { + opacity: 0 !important; +} + +.opacity-25 { + opacity: 0.25 !important; +} + +.opacity-50 { + opacity: 0.5 !important; +} + +.opacity-75 { + opacity: 0.75 !important; +} + +.opacity-100 { + opacity: 1 !important; +} + +.overflow-auto { + overflow: auto !important; +} + +.overflow-hidden { + overflow: hidden !important; +} + +.overflow-visible { + overflow: visible !important; +} + +.overflow-scroll { + overflow: scroll !important; +} + +.overflow-x-auto { + overflow-x: auto !important; +} + +.overflow-x-hidden { + overflow-x: hidden !important; +} + +.overflow-x-visible { + overflow-x: visible !important; +} + +.overflow-x-scroll { + overflow-x: scroll !important; +} + +.overflow-y-auto { + overflow-y: auto !important; +} + +.overflow-y-hidden { + overflow-y: hidden !important; +} + +.overflow-y-visible { + overflow-y: visible !important; +} + +.overflow-y-scroll { + overflow-y: scroll !important; +} + +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-grid { + display: grid !important; +} + +.d-inline-grid { + display: inline-grid !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: flex !important; +} + +.d-inline-flex { + display: inline-flex !important; +} + +.d-none { + display: none !important; +} + +.shadow { + box-shadow: var(--bs-box-shadow) !important; +} + +.shadow-sm { + box-shadow: var(--bs-box-shadow-sm) !important; +} + +.shadow-lg { + box-shadow: var(--bs-box-shadow-lg) !important; +} + +.shadow-none { + box-shadow: none !important; +} + +.focus-ring-primary { + --bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-secondary { + --bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-success { + --bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-info { + --bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-warning { + --bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-danger { + --bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-light { + --bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-dark { + --bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity)); +} + +.position-static { + position: static !important; +} + +.position-relative { + position: relative !important; +} + +.position-absolute { + position: absolute !important; +} + +.position-fixed { + position: fixed !important; +} + +.position-sticky { + position: -webkit-sticky !important; + position: sticky !important; +} + +.top-0 { + top: 0 !important; +} + +.top-50 { + top: 50% !important; +} + +.top-100 { + top: 100% !important; +} + +.bottom-0 { + bottom: 0 !important; +} + +.bottom-50 { + bottom: 50% !important; +} + +.bottom-100 { + bottom: 100% !important; +} + +.start-0 { + left: 0 !important; +} + +.start-50 { + left: 50% !important; +} + +.start-100 { + left: 100% !important; +} + +.end-0 { + right: 0 !important; +} + +.end-50 { + right: 50% !important; +} + +.end-100 { + right: 100% !important; +} + +.translate-middle { + transform: translate(-50%, -50%) !important; +} + +.translate-middle-x { + transform: translateX(-50%) !important; +} + +.translate-middle-y { + transform: translateY(-50%) !important; +} + +.border { + border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-0 { + border: 0 !important; +} + +.border-top { + border-top: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-top-0 { + border-top: 0 !important; +} + +.border-end { + border-right: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-end-0 { + border-right: 0 !important; +} + +.border-bottom { + border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-bottom-0 { + border-bottom: 0 !important; +} + +.border-start { + border-left: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-start-0 { + border-left: 0 !important; +} + +.border-primary { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important; +} + +.border-secondary { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important; +} + +.border-success { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important; +} + +.border-info { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important; +} + +.border-warning { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important; +} + +.border-danger { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important; +} + +.border-light { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important; +} + +.border-dark { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important; +} + +.border-black { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-black-rgb), var(--bs-border-opacity)) !important; +} + +.border-white { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important; +} + +.border-primary-subtle { + border-color: var(--bs-primary-border-subtle) !important; +} + +.border-secondary-subtle { + border-color: var(--bs-secondary-border-subtle) !important; +} + +.border-success-subtle { + border-color: var(--bs-success-border-subtle) !important; +} + +.border-info-subtle { + border-color: var(--bs-info-border-subtle) !important; +} + +.border-warning-subtle { + border-color: var(--bs-warning-border-subtle) !important; +} + +.border-danger-subtle { + border-color: var(--bs-danger-border-subtle) !important; +} + +.border-light-subtle { + border-color: var(--bs-light-border-subtle) !important; +} + +.border-dark-subtle { + border-color: var(--bs-dark-border-subtle) !important; +} + +.border-1 { + border-width: 1px !important; +} + +.border-2 { + border-width: 2px !important; +} + +.border-3 { + border-width: 3px !important; +} + +.border-4 { + border-width: 4px !important; +} + +.border-5 { + border-width: 5px !important; +} + +.border-opacity-10 { + --bs-border-opacity: 0.1; +} + +.border-opacity-25 { + --bs-border-opacity: 0.25; +} + +.border-opacity-50 { + --bs-border-opacity: 0.5; +} + +.border-opacity-75 { + --bs-border-opacity: 0.75; +} + +.border-opacity-100 { + --bs-border-opacity: 1; +} + +.w-25 { + width: 25% !important; +} + +.w-50 { + width: 50% !important; +} + +.w-75 { + width: 75% !important; +} + +.w-100 { + width: 100% !important; +} + +.w-auto { + width: auto !important; +} + +.mw-100 { + max-width: 100% !important; +} + +.vw-100 { + width: 100vw !important; +} + +.min-vw-100 { + min-width: 100vw !important; +} + +.h-25 { + height: 25% !important; +} + +.h-50 { + height: 50% !important; +} + +.h-75 { + height: 75% !important; +} + +.h-100 { + height: 100% !important; +} + +.h-auto { + height: auto !important; +} + +.mh-100 { + max-height: 100% !important; +} + +.vh-100 { + height: 100vh !important; +} + +.min-vh-100 { + min-height: 100vh !important; +} + +.flex-fill { + flex: 1 1 auto !important; +} + +.flex-row { + flex-direction: row !important; +} + +.flex-column { + flex-direction: column !important; +} + +.flex-row-reverse { + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + flex-direction: column-reverse !important; +} + +.flex-grow-0 { + flex-grow: 0 !important; +} + +.flex-grow-1 { + flex-grow: 1 !important; +} + +.flex-shrink-0 { + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + flex-shrink: 1 !important; +} + +.flex-wrap { + flex-wrap: wrap !important; +} + +.flex-nowrap { + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + flex-wrap: wrap-reverse !important; +} + +.justify-content-start { + justify-content: flex-start !important; +} + +.justify-content-end { + justify-content: flex-end !important; +} + +.justify-content-center { + justify-content: center !important; +} + +.justify-content-between { + justify-content: space-between !important; +} + +.justify-content-around { + justify-content: space-around !important; +} + +.justify-content-evenly { + justify-content: space-evenly !important; +} + +.align-items-start { + align-items: flex-start !important; +} + +.align-items-end { + align-items: flex-end !important; +} + +.align-items-center { + align-items: center !important; +} + +.align-items-baseline { + align-items: baseline !important; +} + +.align-items-stretch { + align-items: stretch !important; +} + +.align-content-start { + align-content: flex-start !important; +} + +.align-content-end { + align-content: flex-end !important; +} + +.align-content-center { + align-content: center !important; +} + +.align-content-between { + align-content: space-between !important; +} + +.align-content-around { + align-content: space-around !important; +} + +.align-content-stretch { + align-content: stretch !important; +} + +.align-self-auto { + align-self: auto !important; +} + +.align-self-start { + align-self: flex-start !important; +} + +.align-self-end { + align-self: flex-end !important; +} + +.align-self-center { + align-self: center !important; +} + +.align-self-baseline { + align-self: baseline !important; +} + +.align-self-stretch { + align-self: stretch !important; +} + +.order-first { + order: -1 !important; +} + +.order-0 { + order: 0 !important; +} + +.order-1 { + order: 1 !important; +} + +.order-2 { + order: 2 !important; +} + +.order-3 { + order: 3 !important; +} + +.order-4 { + order: 4 !important; +} + +.order-5 { + order: 5 !important; +} + +.order-last { + order: 6 !important; +} + +.m-0 { + margin: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mx-0 { + margin-right: 0 !important; + margin-left: 0 !important; +} + +.mx-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; +} + +.mx-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; +} + +.mx-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; +} + +.mx-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; +} + +.mx-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; +} + +.mx-auto { + margin-right: auto !important; + margin-left: auto !important; +} + +.my-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +.my-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; +} + +.my-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; +} + +.my-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.my-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; +} + +.my-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; +} + +.my-auto { + margin-top: auto !important; + margin-bottom: auto !important; +} + +.mt-0 { + margin-top: 0 !important; +} + +.mt-1 { + margin-top: 0.25rem !important; +} + +.mt-2 { + margin-top: 0.5rem !important; +} + +.mt-3 { + margin-top: 1rem !important; +} + +.mt-4 { + margin-top: 1.5rem !important; +} + +.mt-5 { + margin-top: 3rem !important; +} + +.mt-auto { + margin-top: auto !important; +} + +.me-0 { + margin-right: 0 !important; +} + +.me-1 { + margin-right: 0.25rem !important; +} + +.me-2 { + margin-right: 0.5rem !important; +} + +.me-3 { + margin-right: 1rem !important; +} + +.me-4 { + margin-right: 1.5rem !important; +} + +.me-5 { + margin-right: 3rem !important; +} + +.me-auto { + margin-right: auto !important; +} + +.mb-0 { + margin-bottom: 0 !important; +} + +.mb-1 { + margin-bottom: 0.25rem !important; +} + +.mb-2 { + margin-bottom: 0.5rem !important; +} + +.mb-3 { + margin-bottom: 1rem !important; +} + +.mb-4 { + margin-bottom: 1.5rem !important; +} + +.mb-5 { + margin-bottom: 3rem !important; +} + +.mb-auto { + margin-bottom: auto !important; +} + +.ms-0 { + margin-left: 0 !important; +} + +.ms-1 { + margin-left: 0.25rem !important; +} + +.ms-2 { + margin-left: 0.5rem !important; +} + +.ms-3 { + margin-left: 1rem !important; +} + +.ms-4 { + margin-left: 1.5rem !important; +} + +.ms-5 { + margin-left: 3rem !important; +} + +.ms-auto { + margin-left: auto !important; +} + +.p-0 { + padding: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.px-0 { + padding-right: 0 !important; + padding-left: 0 !important; +} + +.px-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; +} + +.px-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; +} + +.px-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; +} + +.px-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; +} + +.px-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; +} + +.py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.py-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; +} + +.py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; +} + +.py-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; +} + +.py-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; +} + +.py-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; +} + +.pt-0 { + padding-top: 0 !important; +} + +.pt-1 { + padding-top: 0.25rem !important; +} + +.pt-2 { + padding-top: 0.5rem !important; +} + +.pt-3 { + padding-top: 1rem !important; +} + +.pt-4 { + padding-top: 1.5rem !important; +} + +.pt-5 { + padding-top: 3rem !important; +} + +.pe-0 { + padding-right: 0 !important; +} + +.pe-1 { + padding-right: 0.25rem !important; +} + +.pe-2 { + padding-right: 0.5rem !important; +} + +.pe-3 { + padding-right: 1rem !important; +} + +.pe-4 { + padding-right: 1.5rem !important; +} + +.pe-5 { + padding-right: 3rem !important; +} + +.pb-0 { + padding-bottom: 0 !important; +} + +.pb-1 { + padding-bottom: 0.25rem !important; +} + +.pb-2 { + padding-bottom: 0.5rem !important; +} + +.pb-3 { + padding-bottom: 1rem !important; +} + +.pb-4 { + padding-bottom: 1.5rem !important; +} + +.pb-5 { + padding-bottom: 3rem !important; +} + +.ps-0 { + padding-left: 0 !important; +} + +.ps-1 { + padding-left: 0.25rem !important; +} + +.ps-2 { + padding-left: 0.5rem !important; +} + +.ps-3 { + padding-left: 1rem !important; +} + +.ps-4 { + padding-left: 1.5rem !important; +} + +.ps-5 { + padding-left: 3rem !important; +} + +.gap-0 { + gap: 0 !important; +} + +.gap-1 { + gap: 0.25rem !important; +} + +.gap-2 { + gap: 0.5rem !important; +} + +.gap-3 { + gap: 1rem !important; +} + +.gap-4 { + gap: 1.5rem !important; +} + +.gap-5 { + gap: 3rem !important; +} + +.row-gap-0 { + row-gap: 0 !important; +} + +.row-gap-1 { + row-gap: 0.25rem !important; +} + +.row-gap-2 { + row-gap: 0.5rem !important; +} + +.row-gap-3 { + row-gap: 1rem !important; +} + +.row-gap-4 { + row-gap: 1.5rem !important; +} + +.row-gap-5 { + row-gap: 3rem !important; +} + +.column-gap-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; +} + +.column-gap-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; +} + +.column-gap-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; +} + +.column-gap-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; +} + +.column-gap-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; +} + +.column-gap-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; +} + +.font-monospace { + font-family: var(--bs-font-monospace) !important; +} + +.fs-1 { + font-size: calc(1.375rem + 1.5vw) !important; +} + +.fs-2 { + font-size: calc(1.325rem + 0.9vw) !important; +} + +.fs-3 { + font-size: calc(1.3rem + 0.6vw) !important; +} + +.fs-4 { + font-size: calc(1.275rem + 0.3vw) !important; +} + +.fs-5 { + font-size: 1.25rem !important; +} + +.fs-6 { + font-size: 1rem !important; +} + +.fst-italic { + font-style: italic !important; +} + +.fst-normal { + font-style: normal !important; +} + +.fw-lighter { + font-weight: lighter !important; +} + +.fw-light { + font-weight: 300 !important; +} + +.fw-normal { + font-weight: 400 !important; +} + +.fw-medium { + font-weight: 500 !important; +} + +.fw-semibold { + font-weight: 600 !important; +} + +.fw-bold { + font-weight: 700 !important; +} + +.fw-bolder { + font-weight: bolder !important; +} + +.lh-1 { + line-height: 1 !important; +} + +.lh-sm { + line-height: 1.25 !important; +} + +.lh-base { + line-height: 1.5 !important; +} + +.lh-lg { + line-height: 2 !important; +} + +.text-start { + text-align: left !important; +} + +.text-end { + text-align: right !important; +} + +.text-center { + text-align: center !important; +} + +.text-decoration-none { + text-decoration: none !important; +} + +.text-decoration-underline { + text-decoration: underline !important; +} + +.text-decoration-line-through { + text-decoration: line-through !important; +} + +.text-lowercase { + text-transform: lowercase !important; +} + +.text-uppercase { + text-transform: uppercase !important; +} + +.text-capitalize { + text-transform: capitalize !important; +} + +.text-wrap { + white-space: normal !important; +} + +.text-nowrap { + white-space: nowrap !important; +} + +/* rtl:begin:remove */ +.text-break { + word-wrap: break-word !important; + word-break: break-word !important; +} + +/* rtl:end:remove */ +.text-primary { + --bs-text-opacity: 1; + color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important; +} + +.text-secondary { + --bs-text-opacity: 1; + color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important; +} + +.text-success { + --bs-text-opacity: 1; + color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important; +} + +.text-info { + --bs-text-opacity: 1; + color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important; +} + +.text-warning { + --bs-text-opacity: 1; + color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important; +} + +.text-danger { + --bs-text-opacity: 1; + color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important; +} + +.text-light { + --bs-text-opacity: 1; + color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important; +} + +.text-dark { + --bs-text-opacity: 1; + color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important; +} + +.text-black { + --bs-text-opacity: 1; + color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important; +} + +.text-white { + --bs-text-opacity: 1; + color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important; +} + +.text-body { + --bs-text-opacity: 1; + color: rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important; +} + +.text-muted { + --bs-text-opacity: 1; + color: var(--bs-secondary-color) !important; +} + +.text-black-50 { + --bs-text-opacity: 1; + color: rgba(0, 0, 0, 0.5) !important; +} + +.text-white-50 { + --bs-text-opacity: 1; + color: rgba(255, 255, 255, 0.5) !important; +} + +.text-body-secondary { + --bs-text-opacity: 1; + color: var(--bs-secondary-color) !important; +} + +.text-body-tertiary { + --bs-text-opacity: 1; + color: var(--bs-tertiary-color) !important; +} + +.text-body-emphasis { + --bs-text-opacity: 1; + color: var(--bs-emphasis-color) !important; +} + +.text-reset { + --bs-text-opacity: 1; + color: inherit !important; +} + +.text-opacity-25 { + --bs-text-opacity: 0.25; +} + +.text-opacity-50 { + --bs-text-opacity: 0.5; +} + +.text-opacity-75 { + --bs-text-opacity: 0.75; +} + +.text-opacity-100 { + --bs-text-opacity: 1; +} + +.text-primary-emphasis { + color: var(--bs-primary-text-emphasis) !important; +} + +.text-secondary-emphasis { + color: var(--bs-secondary-text-emphasis) !important; +} + +.text-success-emphasis { + color: var(--bs-success-text-emphasis) !important; +} + +.text-info-emphasis { + color: var(--bs-info-text-emphasis) !important; +} + +.text-warning-emphasis { + color: var(--bs-warning-text-emphasis) !important; +} + +.text-danger-emphasis { + color: var(--bs-danger-text-emphasis) !important; +} + +.text-light-emphasis { + color: var(--bs-light-text-emphasis) !important; +} + +.text-dark-emphasis { + color: var(--bs-dark-text-emphasis) !important; +} + +.link-opacity-10 { + --bs-link-opacity: 0.1; +} + +.link-opacity-10-hover:hover { + --bs-link-opacity: 0.1; +} + +.link-opacity-25 { + --bs-link-opacity: 0.25; +} + +.link-opacity-25-hover:hover { + --bs-link-opacity: 0.25; +} + +.link-opacity-50 { + --bs-link-opacity: 0.5; +} + +.link-opacity-50-hover:hover { + --bs-link-opacity: 0.5; +} + +.link-opacity-75 { + --bs-link-opacity: 0.75; +} + +.link-opacity-75-hover:hover { + --bs-link-opacity: 0.75; +} + +.link-opacity-100 { + --bs-link-opacity: 1; +} + +.link-opacity-100-hover:hover { + --bs-link-opacity: 1; +} + +.link-offset-1 { + text-underline-offset: 0.125em !important; +} + +.link-offset-1-hover:hover { + text-underline-offset: 0.125em !important; +} + +.link-offset-2 { + text-underline-offset: 0.25em !important; +} + +.link-offset-2-hover:hover { + text-underline-offset: 0.25em !important; +} + +.link-offset-3 { + text-underline-offset: 0.375em !important; +} + +.link-offset-3-hover:hover { + text-underline-offset: 0.375em !important; +} + +.link-underline-primary { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-secondary { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-success { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-info { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-warning { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-danger { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-light { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-dark { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important; +} + +.link-underline-opacity-0 { + --bs-link-underline-opacity: 0; +} + +.link-underline-opacity-0-hover:hover { + --bs-link-underline-opacity: 0; +} + +.link-underline-opacity-10 { + --bs-link-underline-opacity: 0.1; +} + +.link-underline-opacity-10-hover:hover { + --bs-link-underline-opacity: 0.1; +} + +.link-underline-opacity-25 { + --bs-link-underline-opacity: 0.25; +} + +.link-underline-opacity-25-hover:hover { + --bs-link-underline-opacity: 0.25; +} + +.link-underline-opacity-50 { + --bs-link-underline-opacity: 0.5; +} + +.link-underline-opacity-50-hover:hover { + --bs-link-underline-opacity: 0.5; +} + +.link-underline-opacity-75 { + --bs-link-underline-opacity: 0.75; +} + +.link-underline-opacity-75-hover:hover { + --bs-link-underline-opacity: 0.75; +} + +.link-underline-opacity-100 { + --bs-link-underline-opacity: 1; +} + +.link-underline-opacity-100-hover:hover { + --bs-link-underline-opacity: 1; +} + +.bg-primary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-secondary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-success { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-info { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-warning { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-danger { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-light { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-dark { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-black { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-white { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-body { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-transparent { + --bs-bg-opacity: 1; + background-color: transparent !important; +} + +.bg-body-secondary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-body-tertiary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-opacity-10 { + --bs-bg-opacity: 0.1; +} + +.bg-opacity-25 { + --bs-bg-opacity: 0.25; +} + +.bg-opacity-50 { + --bs-bg-opacity: 0.5; +} + +.bg-opacity-75 { + --bs-bg-opacity: 0.75; +} + +.bg-opacity-100 { + --bs-bg-opacity: 1; +} + +.bg-primary-subtle { + background-color: var(--bs-primary-bg-subtle) !important; +} + +.bg-secondary-subtle { + background-color: var(--bs-secondary-bg-subtle) !important; +} + +.bg-success-subtle { + background-color: var(--bs-success-bg-subtle) !important; +} + +.bg-info-subtle { + background-color: var(--bs-info-bg-subtle) !important; +} + +.bg-warning-subtle { + background-color: var(--bs-warning-bg-subtle) !important; +} + +.bg-danger-subtle { + background-color: var(--bs-danger-bg-subtle) !important; +} + +.bg-light-subtle { + background-color: var(--bs-light-bg-subtle) !important; +} + +.bg-dark-subtle { + background-color: var(--bs-dark-bg-subtle) !important; +} + +.bg-gradient { + background-image: var(--bs-gradient) !important; +} + +.user-select-all { + -webkit-user-select: all !important; + -moz-user-select: all !important; + user-select: all !important; +} + +.user-select-auto { + -webkit-user-select: auto !important; + -moz-user-select: auto !important; + user-select: auto !important; +} + +.user-select-none { + -webkit-user-select: none !important; + -moz-user-select: none !important; + user-select: none !important; +} + +.pe-none { + pointer-events: none !important; +} + +.pe-auto { + pointer-events: auto !important; +} + +.rounded { + border-radius: var(--bs-border-radius) !important; +} + +.rounded-0 { + border-radius: 0 !important; +} + +.rounded-1 { + border-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-2 { + border-radius: var(--bs-border-radius) !important; +} + +.rounded-3 { + border-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-4 { + border-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-5 { + border-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-circle { + border-radius: 50% !important; +} + +.rounded-pill { + border-radius: var(--bs-border-radius-pill) !important; +} + +.rounded-top { + border-top-left-radius: var(--bs-border-radius) !important; + border-top-right-radius: var(--bs-border-radius) !important; +} + +.rounded-top-0 { + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; +} + +.rounded-top-1 { + border-top-left-radius: var(--bs-border-radius-sm) !important; + border-top-right-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-top-2 { + border-top-left-radius: var(--bs-border-radius) !important; + border-top-right-radius: var(--bs-border-radius) !important; +} + +.rounded-top-3 { + border-top-left-radius: var(--bs-border-radius-lg) !important; + border-top-right-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-top-4 { + border-top-left-radius: var(--bs-border-radius-xl) !important; + border-top-right-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-top-5 { + border-top-left-radius: var(--bs-border-radius-xxl) !important; + border-top-right-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-top-circle { + border-top-left-radius: 50% !important; + border-top-right-radius: 50% !important; +} + +.rounded-top-pill { + border-top-left-radius: var(--bs-border-radius-pill) !important; + border-top-right-radius: var(--bs-border-radius-pill) !important; +} + +.rounded-end { + border-top-right-radius: var(--bs-border-radius) !important; + border-bottom-right-radius: var(--bs-border-radius) !important; +} + +.rounded-end-0 { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +.rounded-end-1 { + border-top-right-radius: var(--bs-border-radius-sm) !important; + border-bottom-right-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-end-2 { + border-top-right-radius: var(--bs-border-radius) !important; + border-bottom-right-radius: var(--bs-border-radius) !important; +} + +.rounded-end-3 { + border-top-right-radius: var(--bs-border-radius-lg) !important; + border-bottom-right-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-end-4 { + border-top-right-radius: var(--bs-border-radius-xl) !important; + border-bottom-right-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-end-5 { + border-top-right-radius: var(--bs-border-radius-xxl) !important; + border-bottom-right-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-end-circle { + border-top-right-radius: 50% !important; + border-bottom-right-radius: 50% !important; +} + +.rounded-end-pill { + border-top-right-radius: var(--bs-border-radius-pill) !important; + border-bottom-right-radius: var(--bs-border-radius-pill) !important; +} + +.rounded-bottom { + border-bottom-right-radius: var(--bs-border-radius) !important; + border-bottom-left-radius: var(--bs-border-radius) !important; +} + +.rounded-bottom-0 { + border-bottom-right-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + +.rounded-bottom-1 { + border-bottom-right-radius: var(--bs-border-radius-sm) !important; + border-bottom-left-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-bottom-2 { + border-bottom-right-radius: var(--bs-border-radius) !important; + border-bottom-left-radius: var(--bs-border-radius) !important; +} + +.rounded-bottom-3 { + border-bottom-right-radius: var(--bs-border-radius-lg) !important; + border-bottom-left-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-bottom-4 { + border-bottom-right-radius: var(--bs-border-radius-xl) !important; + border-bottom-left-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-bottom-5 { + border-bottom-right-radius: var(--bs-border-radius-xxl) !important; + border-bottom-left-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-bottom-circle { + border-bottom-right-radius: 50% !important; + border-bottom-left-radius: 50% !important; +} + +.rounded-bottom-pill { + border-bottom-right-radius: var(--bs-border-radius-pill) !important; + border-bottom-left-radius: var(--bs-border-radius-pill) !important; +} + +.rounded-start { + border-bottom-left-radius: var(--bs-border-radius) !important; + border-top-left-radius: var(--bs-border-radius) !important; +} + +.rounded-start-0 { + border-bottom-left-radius: 0 !important; + border-top-left-radius: 0 !important; +} + +.rounded-start-1 { + border-bottom-left-radius: var(--bs-border-radius-sm) !important; + border-top-left-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-start-2 { + border-bottom-left-radius: var(--bs-border-radius) !important; + border-top-left-radius: var(--bs-border-radius) !important; +} + +.rounded-start-3 { + border-bottom-left-radius: var(--bs-border-radius-lg) !important; + border-top-left-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-start-4 { + border-bottom-left-radius: var(--bs-border-radius-xl) !important; + border-top-left-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-start-5 { + border-bottom-left-radius: var(--bs-border-radius-xxl) !important; + border-top-left-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-start-circle { + border-bottom-left-radius: 50% !important; + border-top-left-radius: 50% !important; +} + +.rounded-start-pill { + border-bottom-left-radius: var(--bs-border-radius-pill) !important; + border-top-left-radius: var(--bs-border-radius-pill) !important; +} + +.visible { + visibility: visible !important; +} + +.invisible { + visibility: hidden !important; +} + +.z-n1 { + z-index: -1 !important; +} + +.z-0 { + z-index: 0 !important; +} + +.z-1 { + z-index: 1 !important; +} + +.z-2 { + z-index: 2 !important; +} + +.z-3 { + z-index: 3 !important; +} + +@media (min-width: 576px) { + .float-sm-start { + float: left !important; + } + .float-sm-end { + float: right !important; + } + .float-sm-none { + float: none !important; + } + .object-fit-sm-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-sm-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-sm-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-sm-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-sm-none { + -o-object-fit: none !important; + object-fit: none !important; + } + .d-sm-inline { + display: inline !important; + } + .d-sm-inline-block { + display: inline-block !important; + } + .d-sm-block { + display: block !important; + } + .d-sm-grid { + display: grid !important; + } + .d-sm-inline-grid { + display: inline-grid !important; + } + .d-sm-table { + display: table !important; + } + .d-sm-table-row { + display: table-row !important; + } + .d-sm-table-cell { + display: table-cell !important; + } + .d-sm-flex { + display: flex !important; + } + .d-sm-inline-flex { + display: inline-flex !important; + } + .d-sm-none { + display: none !important; + } + .flex-sm-fill { + flex: 1 1 auto !important; + } + .flex-sm-row { + flex-direction: row !important; + } + .flex-sm-column { + flex-direction: column !important; + } + .flex-sm-row-reverse { + flex-direction: row-reverse !important; + } + .flex-sm-column-reverse { + flex-direction: column-reverse !important; + } + .flex-sm-grow-0 { + flex-grow: 0 !important; + } + .flex-sm-grow-1 { + flex-grow: 1 !important; + } + .flex-sm-shrink-0 { + flex-shrink: 0 !important; + } + .flex-sm-shrink-1 { + flex-shrink: 1 !important; + } + .flex-sm-wrap { + flex-wrap: wrap !important; + } + .flex-sm-nowrap { + flex-wrap: nowrap !important; + } + .flex-sm-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-sm-start { + justify-content: flex-start !important; + } + .justify-content-sm-end { + justify-content: flex-end !important; + } + .justify-content-sm-center { + justify-content: center !important; + } + .justify-content-sm-between { + justify-content: space-between !important; + } + .justify-content-sm-around { + justify-content: space-around !important; + } + .justify-content-sm-evenly { + justify-content: space-evenly !important; + } + .align-items-sm-start { + align-items: flex-start !important; + } + .align-items-sm-end { + align-items: flex-end !important; + } + .align-items-sm-center { + align-items: center !important; + } + .align-items-sm-baseline { + align-items: baseline !important; + } + .align-items-sm-stretch { + align-items: stretch !important; + } + .align-content-sm-start { + align-content: flex-start !important; + } + .align-content-sm-end { + align-content: flex-end !important; + } + .align-content-sm-center { + align-content: center !important; + } + .align-content-sm-between { + align-content: space-between !important; + } + .align-content-sm-around { + align-content: space-around !important; + } + .align-content-sm-stretch { + align-content: stretch !important; + } + .align-self-sm-auto { + align-self: auto !important; + } + .align-self-sm-start { + align-self: flex-start !important; + } + .align-self-sm-end { + align-self: flex-end !important; + } + .align-self-sm-center { + align-self: center !important; + } + .align-self-sm-baseline { + align-self: baseline !important; + } + .align-self-sm-stretch { + align-self: stretch !important; + } + .order-sm-first { + order: -1 !important; + } + .order-sm-0 { + order: 0 !important; + } + .order-sm-1 { + order: 1 !important; + } + .order-sm-2 { + order: 2 !important; + } + .order-sm-3 { + order: 3 !important; + } + .order-sm-4 { + order: 4 !important; + } + .order-sm-5 { + order: 5 !important; + } + .order-sm-last { + order: 6 !important; + } + .m-sm-0 { + margin: 0 !important; + } + .m-sm-1 { + margin: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .m-sm-3 { + margin: 1rem !important; + } + .m-sm-4 { + margin: 1.5rem !important; + } + .m-sm-5 { + margin: 3rem !important; + } + .m-sm-auto { + margin: auto !important; + } + .mx-sm-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-sm-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-sm-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-sm-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-sm-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-sm-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-sm-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-sm-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-sm-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-sm-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-sm-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-sm-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-sm-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-sm-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-sm-0 { + margin-top: 0 !important; + } + .mt-sm-1 { + margin-top: 0.25rem !important; + } + .mt-sm-2 { + margin-top: 0.5rem !important; + } + .mt-sm-3 { + margin-top: 1rem !important; + } + .mt-sm-4 { + margin-top: 1.5rem !important; + } + .mt-sm-5 { + margin-top: 3rem !important; + } + .mt-sm-auto { + margin-top: auto !important; + } + .me-sm-0 { + margin-right: 0 !important; + } + .me-sm-1 { + margin-right: 0.25rem !important; + } + .me-sm-2 { + margin-right: 0.5rem !important; + } + .me-sm-3 { + margin-right: 1rem !important; + } + .me-sm-4 { + margin-right: 1.5rem !important; + } + .me-sm-5 { + margin-right: 3rem !important; + } + .me-sm-auto { + margin-right: auto !important; + } + .mb-sm-0 { + margin-bottom: 0 !important; + } + .mb-sm-1 { + margin-bottom: 0.25rem !important; + } + .mb-sm-2 { + margin-bottom: 0.5rem !important; + } + .mb-sm-3 { + margin-bottom: 1rem !important; + } + .mb-sm-4 { + margin-bottom: 1.5rem !important; + } + .mb-sm-5 { + margin-bottom: 3rem !important; + } + .mb-sm-auto { + margin-bottom: auto !important; + } + .ms-sm-0 { + margin-left: 0 !important; + } + .ms-sm-1 { + margin-left: 0.25rem !important; + } + .ms-sm-2 { + margin-left: 0.5rem !important; + } + .ms-sm-3 { + margin-left: 1rem !important; + } + .ms-sm-4 { + margin-left: 1.5rem !important; + } + .ms-sm-5 { + margin-left: 3rem !important; + } + .ms-sm-auto { + margin-left: auto !important; + } + .p-sm-0 { + padding: 0 !important; + } + .p-sm-1 { + padding: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .p-sm-3 { + padding: 1rem !important; + } + .p-sm-4 { + padding: 1.5rem !important; + } + .p-sm-5 { + padding: 3rem !important; + } + .px-sm-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-sm-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-sm-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-sm-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-sm-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-sm-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-sm-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-sm-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-sm-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-sm-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-sm-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-sm-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-sm-0 { + padding-top: 0 !important; + } + .pt-sm-1 { + padding-top: 0.25rem !important; + } + .pt-sm-2 { + padding-top: 0.5rem !important; + } + .pt-sm-3 { + padding-top: 1rem !important; + } + .pt-sm-4 { + padding-top: 1.5rem !important; + } + .pt-sm-5 { + padding-top: 3rem !important; + } + .pe-sm-0 { + padding-right: 0 !important; + } + .pe-sm-1 { + padding-right: 0.25rem !important; + } + .pe-sm-2 { + padding-right: 0.5rem !important; + } + .pe-sm-3 { + padding-right: 1rem !important; + } + .pe-sm-4 { + padding-right: 1.5rem !important; + } + .pe-sm-5 { + padding-right: 3rem !important; + } + .pb-sm-0 { + padding-bottom: 0 !important; + } + .pb-sm-1 { + padding-bottom: 0.25rem !important; + } + .pb-sm-2 { + padding-bottom: 0.5rem !important; + } + .pb-sm-3 { + padding-bottom: 1rem !important; + } + .pb-sm-4 { + padding-bottom: 1.5rem !important; + } + .pb-sm-5 { + padding-bottom: 3rem !important; + } + .ps-sm-0 { + padding-left: 0 !important; + } + .ps-sm-1 { + padding-left: 0.25rem !important; + } + .ps-sm-2 { + padding-left: 0.5rem !important; + } + .ps-sm-3 { + padding-left: 1rem !important; + } + .ps-sm-4 { + padding-left: 1.5rem !important; + } + .ps-sm-5 { + padding-left: 3rem !important; + } + .gap-sm-0 { + gap: 0 !important; + } + .gap-sm-1 { + gap: 0.25rem !important; + } + .gap-sm-2 { + gap: 0.5rem !important; + } + .gap-sm-3 { + gap: 1rem !important; + } + .gap-sm-4 { + gap: 1.5rem !important; + } + .gap-sm-5 { + gap: 3rem !important; + } + .row-gap-sm-0 { + row-gap: 0 !important; + } + .row-gap-sm-1 { + row-gap: 0.25rem !important; + } + .row-gap-sm-2 { + row-gap: 0.5rem !important; + } + .row-gap-sm-3 { + row-gap: 1rem !important; + } + .row-gap-sm-4 { + row-gap: 1.5rem !important; + } + .row-gap-sm-5 { + row-gap: 3rem !important; + } + .column-gap-sm-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-sm-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-sm-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-sm-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-sm-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-sm-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } + .text-sm-start { + text-align: left !important; + } + .text-sm-end { + text-align: right !important; + } + .text-sm-center { + text-align: center !important; + } +} +@media (min-width: 768px) { + .float-md-start { + float: left !important; + } + .float-md-end { + float: right !important; + } + .float-md-none { + float: none !important; + } + .object-fit-md-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-md-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-md-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-md-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-md-none { + -o-object-fit: none !important; + object-fit: none !important; + } + .d-md-inline { + display: inline !important; + } + .d-md-inline-block { + display: inline-block !important; + } + .d-md-block { + display: block !important; + } + .d-md-grid { + display: grid !important; + } + .d-md-inline-grid { + display: inline-grid !important; + } + .d-md-table { + display: table !important; + } + .d-md-table-row { + display: table-row !important; + } + .d-md-table-cell { + display: table-cell !important; + } + .d-md-flex { + display: flex !important; + } + .d-md-inline-flex { + display: inline-flex !important; + } + .d-md-none { + display: none !important; + } + .flex-md-fill { + flex: 1 1 auto !important; + } + .flex-md-row { + flex-direction: row !important; + } + .flex-md-column { + flex-direction: column !important; + } + .flex-md-row-reverse { + flex-direction: row-reverse !important; + } + .flex-md-column-reverse { + flex-direction: column-reverse !important; + } + .flex-md-grow-0 { + flex-grow: 0 !important; + } + .flex-md-grow-1 { + flex-grow: 1 !important; + } + .flex-md-shrink-0 { + flex-shrink: 0 !important; + } + .flex-md-shrink-1 { + flex-shrink: 1 !important; + } + .flex-md-wrap { + flex-wrap: wrap !important; + } + .flex-md-nowrap { + flex-wrap: nowrap !important; + } + .flex-md-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-md-start { + justify-content: flex-start !important; + } + .justify-content-md-end { + justify-content: flex-end !important; + } + .justify-content-md-center { + justify-content: center !important; + } + .justify-content-md-between { + justify-content: space-between !important; + } + .justify-content-md-around { + justify-content: space-around !important; + } + .justify-content-md-evenly { + justify-content: space-evenly !important; + } + .align-items-md-start { + align-items: flex-start !important; + } + .align-items-md-end { + align-items: flex-end !important; + } + .align-items-md-center { + align-items: center !important; + } + .align-items-md-baseline { + align-items: baseline !important; + } + .align-items-md-stretch { + align-items: stretch !important; + } + .align-content-md-start { + align-content: flex-start !important; + } + .align-content-md-end { + align-content: flex-end !important; + } + .align-content-md-center { + align-content: center !important; + } + .align-content-md-between { + align-content: space-between !important; + } + .align-content-md-around { + align-content: space-around !important; + } + .align-content-md-stretch { + align-content: stretch !important; + } + .align-self-md-auto { + align-self: auto !important; + } + .align-self-md-start { + align-self: flex-start !important; + } + .align-self-md-end { + align-self: flex-end !important; + } + .align-self-md-center { + align-self: center !important; + } + .align-self-md-baseline { + align-self: baseline !important; + } + .align-self-md-stretch { + align-self: stretch !important; + } + .order-md-first { + order: -1 !important; + } + .order-md-0 { + order: 0 !important; + } + .order-md-1 { + order: 1 !important; + } + .order-md-2 { + order: 2 !important; + } + .order-md-3 { + order: 3 !important; + } + .order-md-4 { + order: 4 !important; + } + .order-md-5 { + order: 5 !important; + } + .order-md-last { + order: 6 !important; + } + .m-md-0 { + margin: 0 !important; + } + .m-md-1 { + margin: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .m-md-3 { + margin: 1rem !important; + } + .m-md-4 { + margin: 1.5rem !important; + } + .m-md-5 { + margin: 3rem !important; + } + .m-md-auto { + margin: auto !important; + } + .mx-md-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-md-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-md-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-md-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-md-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-md-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-md-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-md-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-md-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-md-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-md-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-md-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-md-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-md-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-md-0 { + margin-top: 0 !important; + } + .mt-md-1 { + margin-top: 0.25rem !important; + } + .mt-md-2 { + margin-top: 0.5rem !important; + } + .mt-md-3 { + margin-top: 1rem !important; + } + .mt-md-4 { + margin-top: 1.5rem !important; + } + .mt-md-5 { + margin-top: 3rem !important; + } + .mt-md-auto { + margin-top: auto !important; + } + .me-md-0 { + margin-right: 0 !important; + } + .me-md-1 { + margin-right: 0.25rem !important; + } + .me-md-2 { + margin-right: 0.5rem !important; + } + .me-md-3 { + margin-right: 1rem !important; + } + .me-md-4 { + margin-right: 1.5rem !important; + } + .me-md-5 { + margin-right: 3rem !important; + } + .me-md-auto { + margin-right: auto !important; + } + .mb-md-0 { + margin-bottom: 0 !important; + } + .mb-md-1 { + margin-bottom: 0.25rem !important; + } + .mb-md-2 { + margin-bottom: 0.5rem !important; + } + .mb-md-3 { + margin-bottom: 1rem !important; + } + .mb-md-4 { + margin-bottom: 1.5rem !important; + } + .mb-md-5 { + margin-bottom: 3rem !important; + } + .mb-md-auto { + margin-bottom: auto !important; + } + .ms-md-0 { + margin-left: 0 !important; + } + .ms-md-1 { + margin-left: 0.25rem !important; + } + .ms-md-2 { + margin-left: 0.5rem !important; + } + .ms-md-3 { + margin-left: 1rem !important; + } + .ms-md-4 { + margin-left: 1.5rem !important; + } + .ms-md-5 { + margin-left: 3rem !important; + } + .ms-md-auto { + margin-left: auto !important; + } + .p-md-0 { + padding: 0 !important; + } + .p-md-1 { + padding: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .p-md-3 { + padding: 1rem !important; + } + .p-md-4 { + padding: 1.5rem !important; + } + .p-md-5 { + padding: 3rem !important; + } + .px-md-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-md-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-md-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-md-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-md-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-md-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-md-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-md-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-md-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-md-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-md-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-md-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-md-0 { + padding-top: 0 !important; + } + .pt-md-1 { + padding-top: 0.25rem !important; + } + .pt-md-2 { + padding-top: 0.5rem !important; + } + .pt-md-3 { + padding-top: 1rem !important; + } + .pt-md-4 { + padding-top: 1.5rem !important; + } + .pt-md-5 { + padding-top: 3rem !important; + } + .pe-md-0 { + padding-right: 0 !important; + } + .pe-md-1 { + padding-right: 0.25rem !important; + } + .pe-md-2 { + padding-right: 0.5rem !important; + } + .pe-md-3 { + padding-right: 1rem !important; + } + .pe-md-4 { + padding-right: 1.5rem !important; + } + .pe-md-5 { + padding-right: 3rem !important; + } + .pb-md-0 { + padding-bottom: 0 !important; + } + .pb-md-1 { + padding-bottom: 0.25rem !important; + } + .pb-md-2 { + padding-bottom: 0.5rem !important; + } + .pb-md-3 { + padding-bottom: 1rem !important; + } + .pb-md-4 { + padding-bottom: 1.5rem !important; + } + .pb-md-5 { + padding-bottom: 3rem !important; + } + .ps-md-0 { + padding-left: 0 !important; + } + .ps-md-1 { + padding-left: 0.25rem !important; + } + .ps-md-2 { + padding-left: 0.5rem !important; + } + .ps-md-3 { + padding-left: 1rem !important; + } + .ps-md-4 { + padding-left: 1.5rem !important; + } + .ps-md-5 { + padding-left: 3rem !important; + } + .gap-md-0 { + gap: 0 !important; + } + .gap-md-1 { + gap: 0.25rem !important; + } + .gap-md-2 { + gap: 0.5rem !important; + } + .gap-md-3 { + gap: 1rem !important; + } + .gap-md-4 { + gap: 1.5rem !important; + } + .gap-md-5 { + gap: 3rem !important; + } + .row-gap-md-0 { + row-gap: 0 !important; + } + .row-gap-md-1 { + row-gap: 0.25rem !important; + } + .row-gap-md-2 { + row-gap: 0.5rem !important; + } + .row-gap-md-3 { + row-gap: 1rem !important; + } + .row-gap-md-4 { + row-gap: 1.5rem !important; + } + .row-gap-md-5 { + row-gap: 3rem !important; + } + .column-gap-md-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-md-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-md-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-md-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-md-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-md-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } + .text-md-start { + text-align: left !important; + } + .text-md-end { + text-align: right !important; + } + .text-md-center { + text-align: center !important; + } +} +@media (min-width: 992px) { + .float-lg-start { + float: left !important; + } + .float-lg-end { + float: right !important; + } + .float-lg-none { + float: none !important; + } + .object-fit-lg-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-lg-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-lg-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-lg-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-lg-none { + -o-object-fit: none !important; + object-fit: none !important; + } + .d-lg-inline { + display: inline !important; + } + .d-lg-inline-block { + display: inline-block !important; + } + .d-lg-block { + display: block !important; + } + .d-lg-grid { + display: grid !important; + } + .d-lg-inline-grid { + display: inline-grid !important; + } + .d-lg-table { + display: table !important; + } + .d-lg-table-row { + display: table-row !important; + } + .d-lg-table-cell { + display: table-cell !important; + } + .d-lg-flex { + display: flex !important; + } + .d-lg-inline-flex { + display: inline-flex !important; + } + .d-lg-none { + display: none !important; + } + .flex-lg-fill { + flex: 1 1 auto !important; + } + .flex-lg-row { + flex-direction: row !important; + } + .flex-lg-column { + flex-direction: column !important; + } + .flex-lg-row-reverse { + flex-direction: row-reverse !important; + } + .flex-lg-column-reverse { + flex-direction: column-reverse !important; + } + .flex-lg-grow-0 { + flex-grow: 0 !important; + } + .flex-lg-grow-1 { + flex-grow: 1 !important; + } + .flex-lg-shrink-0 { + flex-shrink: 0 !important; + } + .flex-lg-shrink-1 { + flex-shrink: 1 !important; + } + .flex-lg-wrap { + flex-wrap: wrap !important; + } + .flex-lg-nowrap { + flex-wrap: nowrap !important; + } + .flex-lg-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-lg-start { + justify-content: flex-start !important; + } + .justify-content-lg-end { + justify-content: flex-end !important; + } + .justify-content-lg-center { + justify-content: center !important; + } + .justify-content-lg-between { + justify-content: space-between !important; + } + .justify-content-lg-around { + justify-content: space-around !important; + } + .justify-content-lg-evenly { + justify-content: space-evenly !important; + } + .align-items-lg-start { + align-items: flex-start !important; + } + .align-items-lg-end { + align-items: flex-end !important; + } + .align-items-lg-center { + align-items: center !important; + } + .align-items-lg-baseline { + align-items: baseline !important; + } + .align-items-lg-stretch { + align-items: stretch !important; + } + .align-content-lg-start { + align-content: flex-start !important; + } + .align-content-lg-end { + align-content: flex-end !important; + } + .align-content-lg-center { + align-content: center !important; + } + .align-content-lg-between { + align-content: space-between !important; + } + .align-content-lg-around { + align-content: space-around !important; + } + .align-content-lg-stretch { + align-content: stretch !important; + } + .align-self-lg-auto { + align-self: auto !important; + } + .align-self-lg-start { + align-self: flex-start !important; + } + .align-self-lg-end { + align-self: flex-end !important; + } + .align-self-lg-center { + align-self: center !important; + } + .align-self-lg-baseline { + align-self: baseline !important; + } + .align-self-lg-stretch { + align-self: stretch !important; + } + .order-lg-first { + order: -1 !important; + } + .order-lg-0 { + order: 0 !important; + } + .order-lg-1 { + order: 1 !important; + } + .order-lg-2 { + order: 2 !important; + } + .order-lg-3 { + order: 3 !important; + } + .order-lg-4 { + order: 4 !important; + } + .order-lg-5 { + order: 5 !important; + } + .order-lg-last { + order: 6 !important; + } + .m-lg-0 { + margin: 0 !important; + } + .m-lg-1 { + margin: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .m-lg-3 { + margin: 1rem !important; + } + .m-lg-4 { + margin: 1.5rem !important; + } + .m-lg-5 { + margin: 3rem !important; + } + .m-lg-auto { + margin: auto !important; + } + .mx-lg-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-lg-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-lg-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-lg-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-lg-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-lg-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-lg-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-lg-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-lg-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-lg-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-lg-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-lg-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-lg-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-lg-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-lg-0 { + margin-top: 0 !important; + } + .mt-lg-1 { + margin-top: 0.25rem !important; + } + .mt-lg-2 { + margin-top: 0.5rem !important; + } + .mt-lg-3 { + margin-top: 1rem !important; + } + .mt-lg-4 { + margin-top: 1.5rem !important; + } + .mt-lg-5 { + margin-top: 3rem !important; + } + .mt-lg-auto { + margin-top: auto !important; + } + .me-lg-0 { + margin-right: 0 !important; + } + .me-lg-1 { + margin-right: 0.25rem !important; + } + .me-lg-2 { + margin-right: 0.5rem !important; + } + .me-lg-3 { + margin-right: 1rem !important; + } + .me-lg-4 { + margin-right: 1.5rem !important; + } + .me-lg-5 { + margin-right: 3rem !important; + } + .me-lg-auto { + margin-right: auto !important; + } + .mb-lg-0 { + margin-bottom: 0 !important; + } + .mb-lg-1 { + margin-bottom: 0.25rem !important; + } + .mb-lg-2 { + margin-bottom: 0.5rem !important; + } + .mb-lg-3 { + margin-bottom: 1rem !important; + } + .mb-lg-4 { + margin-bottom: 1.5rem !important; + } + .mb-lg-5 { + margin-bottom: 3rem !important; + } + .mb-lg-auto { + margin-bottom: auto !important; + } + .ms-lg-0 { + margin-left: 0 !important; + } + .ms-lg-1 { + margin-left: 0.25rem !important; + } + .ms-lg-2 { + margin-left: 0.5rem !important; + } + .ms-lg-3 { + margin-left: 1rem !important; + } + .ms-lg-4 { + margin-left: 1.5rem !important; + } + .ms-lg-5 { + margin-left: 3rem !important; + } + .ms-lg-auto { + margin-left: auto !important; + } + .p-lg-0 { + padding: 0 !important; + } + .p-lg-1 { + padding: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .p-lg-3 { + padding: 1rem !important; + } + .p-lg-4 { + padding: 1.5rem !important; + } + .p-lg-5 { + padding: 3rem !important; + } + .px-lg-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-lg-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-lg-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-lg-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-lg-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-lg-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-lg-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-lg-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-lg-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-lg-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-lg-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-lg-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-lg-0 { + padding-top: 0 !important; + } + .pt-lg-1 { + padding-top: 0.25rem !important; + } + .pt-lg-2 { + padding-top: 0.5rem !important; + } + .pt-lg-3 { + padding-top: 1rem !important; + } + .pt-lg-4 { + padding-top: 1.5rem !important; + } + .pt-lg-5 { + padding-top: 3rem !important; + } + .pe-lg-0 { + padding-right: 0 !important; + } + .pe-lg-1 { + padding-right: 0.25rem !important; + } + .pe-lg-2 { + padding-right: 0.5rem !important; + } + .pe-lg-3 { + padding-right: 1rem !important; + } + .pe-lg-4 { + padding-right: 1.5rem !important; + } + .pe-lg-5 { + padding-right: 3rem !important; + } + .pb-lg-0 { + padding-bottom: 0 !important; + } + .pb-lg-1 { + padding-bottom: 0.25rem !important; + } + .pb-lg-2 { + padding-bottom: 0.5rem !important; + } + .pb-lg-3 { + padding-bottom: 1rem !important; + } + .pb-lg-4 { + padding-bottom: 1.5rem !important; + } + .pb-lg-5 { + padding-bottom: 3rem !important; + } + .ps-lg-0 { + padding-left: 0 !important; + } + .ps-lg-1 { + padding-left: 0.25rem !important; + } + .ps-lg-2 { + padding-left: 0.5rem !important; + } + .ps-lg-3 { + padding-left: 1rem !important; + } + .ps-lg-4 { + padding-left: 1.5rem !important; + } + .ps-lg-5 { + padding-left: 3rem !important; + } + .gap-lg-0 { + gap: 0 !important; + } + .gap-lg-1 { + gap: 0.25rem !important; + } + .gap-lg-2 { + gap: 0.5rem !important; + } + .gap-lg-3 { + gap: 1rem !important; + } + .gap-lg-4 { + gap: 1.5rem !important; + } + .gap-lg-5 { + gap: 3rem !important; + } + .row-gap-lg-0 { + row-gap: 0 !important; + } + .row-gap-lg-1 { + row-gap: 0.25rem !important; + } + .row-gap-lg-2 { + row-gap: 0.5rem !important; + } + .row-gap-lg-3 { + row-gap: 1rem !important; + } + .row-gap-lg-4 { + row-gap: 1.5rem !important; + } + .row-gap-lg-5 { + row-gap: 3rem !important; + } + .column-gap-lg-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-lg-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-lg-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-lg-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-lg-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-lg-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } + .text-lg-start { + text-align: left !important; + } + .text-lg-end { + text-align: right !important; + } + .text-lg-center { + text-align: center !important; + } +} +@media (min-width: 1200px) { + .float-xl-start { + float: left !important; + } + .float-xl-end { + float: right !important; + } + .float-xl-none { + float: none !important; + } + .object-fit-xl-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-xl-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-xl-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-xl-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-xl-none { + -o-object-fit: none !important; + object-fit: none !important; + } + .d-xl-inline { + display: inline !important; + } + .d-xl-inline-block { + display: inline-block !important; + } + .d-xl-block { + display: block !important; + } + .d-xl-grid { + display: grid !important; + } + .d-xl-inline-grid { + display: inline-grid !important; + } + .d-xl-table { + display: table !important; + } + .d-xl-table-row { + display: table-row !important; + } + .d-xl-table-cell { + display: table-cell !important; + } + .d-xl-flex { + display: flex !important; + } + .d-xl-inline-flex { + display: inline-flex !important; + } + .d-xl-none { + display: none !important; + } + .flex-xl-fill { + flex: 1 1 auto !important; + } + .flex-xl-row { + flex-direction: row !important; + } + .flex-xl-column { + flex-direction: column !important; + } + .flex-xl-row-reverse { + flex-direction: row-reverse !important; + } + .flex-xl-column-reverse { + flex-direction: column-reverse !important; + } + .flex-xl-grow-0 { + flex-grow: 0 !important; + } + .flex-xl-grow-1 { + flex-grow: 1 !important; + } + .flex-xl-shrink-0 { + flex-shrink: 0 !important; + } + .flex-xl-shrink-1 { + flex-shrink: 1 !important; + } + .flex-xl-wrap { + flex-wrap: wrap !important; + } + .flex-xl-nowrap { + flex-wrap: nowrap !important; + } + .flex-xl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-xl-start { + justify-content: flex-start !important; + } + .justify-content-xl-end { + justify-content: flex-end !important; + } + .justify-content-xl-center { + justify-content: center !important; + } + .justify-content-xl-between { + justify-content: space-between !important; + } + .justify-content-xl-around { + justify-content: space-around !important; + } + .justify-content-xl-evenly { + justify-content: space-evenly !important; + } + .align-items-xl-start { + align-items: flex-start !important; + } + .align-items-xl-end { + align-items: flex-end !important; + } + .align-items-xl-center { + align-items: center !important; + } + .align-items-xl-baseline { + align-items: baseline !important; + } + .align-items-xl-stretch { + align-items: stretch !important; + } + .align-content-xl-start { + align-content: flex-start !important; + } + .align-content-xl-end { + align-content: flex-end !important; + } + .align-content-xl-center { + align-content: center !important; + } + .align-content-xl-between { + align-content: space-between !important; + } + .align-content-xl-around { + align-content: space-around !important; + } + .align-content-xl-stretch { + align-content: stretch !important; + } + .align-self-xl-auto { + align-self: auto !important; + } + .align-self-xl-start { + align-self: flex-start !important; + } + .align-self-xl-end { + align-self: flex-end !important; + } + .align-self-xl-center { + align-self: center !important; + } + .align-self-xl-baseline { + align-self: baseline !important; + } + .align-self-xl-stretch { + align-self: stretch !important; + } + .order-xl-first { + order: -1 !important; + } + .order-xl-0 { + order: 0 !important; + } + .order-xl-1 { + order: 1 !important; + } + .order-xl-2 { + order: 2 !important; + } + .order-xl-3 { + order: 3 !important; + } + .order-xl-4 { + order: 4 !important; + } + .order-xl-5 { + order: 5 !important; + } + .order-xl-last { + order: 6 !important; + } + .m-xl-0 { + margin: 0 !important; + } + .m-xl-1 { + margin: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .m-xl-3 { + margin: 1rem !important; + } + .m-xl-4 { + margin: 1.5rem !important; + } + .m-xl-5 { + margin: 3rem !important; + } + .m-xl-auto { + margin: auto !important; + } + .mx-xl-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-xl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-xl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-xl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-xl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-xl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-xl-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-xl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-xl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-xl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-xl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-xl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-xl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-xl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-xl-0 { + margin-top: 0 !important; + } + .mt-xl-1 { + margin-top: 0.25rem !important; + } + .mt-xl-2 { + margin-top: 0.5rem !important; + } + .mt-xl-3 { + margin-top: 1rem !important; + } + .mt-xl-4 { + margin-top: 1.5rem !important; + } + .mt-xl-5 { + margin-top: 3rem !important; + } + .mt-xl-auto { + margin-top: auto !important; + } + .me-xl-0 { + margin-right: 0 !important; + } + .me-xl-1 { + margin-right: 0.25rem !important; + } + .me-xl-2 { + margin-right: 0.5rem !important; + } + .me-xl-3 { + margin-right: 1rem !important; + } + .me-xl-4 { + margin-right: 1.5rem !important; + } + .me-xl-5 { + margin-right: 3rem !important; + } + .me-xl-auto { + margin-right: auto !important; + } + .mb-xl-0 { + margin-bottom: 0 !important; + } + .mb-xl-1 { + margin-bottom: 0.25rem !important; + } + .mb-xl-2 { + margin-bottom: 0.5rem !important; + } + .mb-xl-3 { + margin-bottom: 1rem !important; + } + .mb-xl-4 { + margin-bottom: 1.5rem !important; + } + .mb-xl-5 { + margin-bottom: 3rem !important; + } + .mb-xl-auto { + margin-bottom: auto !important; + } + .ms-xl-0 { + margin-left: 0 !important; + } + .ms-xl-1 { + margin-left: 0.25rem !important; + } + .ms-xl-2 { + margin-left: 0.5rem !important; + } + .ms-xl-3 { + margin-left: 1rem !important; + } + .ms-xl-4 { + margin-left: 1.5rem !important; + } + .ms-xl-5 { + margin-left: 3rem !important; + } + .ms-xl-auto { + margin-left: auto !important; + } + .p-xl-0 { + padding: 0 !important; + } + .p-xl-1 { + padding: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .p-xl-3 { + padding: 1rem !important; + } + .p-xl-4 { + padding: 1.5rem !important; + } + .p-xl-5 { + padding: 3rem !important; + } + .px-xl-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-xl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-xl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-xl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-xl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-xl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-xl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-xl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-xl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-xl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-xl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-xl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-xl-0 { + padding-top: 0 !important; + } + .pt-xl-1 { + padding-top: 0.25rem !important; + } + .pt-xl-2 { + padding-top: 0.5rem !important; + } + .pt-xl-3 { + padding-top: 1rem !important; + } + .pt-xl-4 { + padding-top: 1.5rem !important; + } + .pt-xl-5 { + padding-top: 3rem !important; + } + .pe-xl-0 { + padding-right: 0 !important; + } + .pe-xl-1 { + padding-right: 0.25rem !important; + } + .pe-xl-2 { + padding-right: 0.5rem !important; + } + .pe-xl-3 { + padding-right: 1rem !important; + } + .pe-xl-4 { + padding-right: 1.5rem !important; + } + .pe-xl-5 { + padding-right: 3rem !important; + } + .pb-xl-0 { + padding-bottom: 0 !important; + } + .pb-xl-1 { + padding-bottom: 0.25rem !important; + } + .pb-xl-2 { + padding-bottom: 0.5rem !important; + } + .pb-xl-3 { + padding-bottom: 1rem !important; + } + .pb-xl-4 { + padding-bottom: 1.5rem !important; + } + .pb-xl-5 { + padding-bottom: 3rem !important; + } + .ps-xl-0 { + padding-left: 0 !important; + } + .ps-xl-1 { + padding-left: 0.25rem !important; + } + .ps-xl-2 { + padding-left: 0.5rem !important; + } + .ps-xl-3 { + padding-left: 1rem !important; + } + .ps-xl-4 { + padding-left: 1.5rem !important; + } + .ps-xl-5 { + padding-left: 3rem !important; + } + .gap-xl-0 { + gap: 0 !important; + } + .gap-xl-1 { + gap: 0.25rem !important; + } + .gap-xl-2 { + gap: 0.5rem !important; + } + .gap-xl-3 { + gap: 1rem !important; + } + .gap-xl-4 { + gap: 1.5rem !important; + } + .gap-xl-5 { + gap: 3rem !important; + } + .row-gap-xl-0 { + row-gap: 0 !important; + } + .row-gap-xl-1 { + row-gap: 0.25rem !important; + } + .row-gap-xl-2 { + row-gap: 0.5rem !important; + } + .row-gap-xl-3 { + row-gap: 1rem !important; + } + .row-gap-xl-4 { + row-gap: 1.5rem !important; + } + .row-gap-xl-5 { + row-gap: 3rem !important; + } + .column-gap-xl-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-xl-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-xl-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-xl-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-xl-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-xl-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } + .text-xl-start { + text-align: left !important; + } + .text-xl-end { + text-align: right !important; + } + .text-xl-center { + text-align: center !important; + } +} +@media (min-width: 1400px) { + .float-xxl-start { + float: left !important; + } + .float-xxl-end { + float: right !important; + } + .float-xxl-none { + float: none !important; + } + .object-fit-xxl-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-xxl-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-xxl-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-xxl-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-xxl-none { + -o-object-fit: none !important; + object-fit: none !important; + } + .d-xxl-inline { + display: inline !important; + } + .d-xxl-inline-block { + display: inline-block !important; + } + .d-xxl-block { + display: block !important; + } + .d-xxl-grid { + display: grid !important; + } + .d-xxl-inline-grid { + display: inline-grid !important; + } + .d-xxl-table { + display: table !important; + } + .d-xxl-table-row { + display: table-row !important; + } + .d-xxl-table-cell { + display: table-cell !important; + } + .d-xxl-flex { + display: flex !important; + } + .d-xxl-inline-flex { + display: inline-flex !important; + } + .d-xxl-none { + display: none !important; + } + .flex-xxl-fill { + flex: 1 1 auto !important; + } + .flex-xxl-row { + flex-direction: row !important; + } + .flex-xxl-column { + flex-direction: column !important; + } + .flex-xxl-row-reverse { + flex-direction: row-reverse !important; + } + .flex-xxl-column-reverse { + flex-direction: column-reverse !important; + } + .flex-xxl-grow-0 { + flex-grow: 0 !important; + } + .flex-xxl-grow-1 { + flex-grow: 1 !important; + } + .flex-xxl-shrink-0 { + flex-shrink: 0 !important; + } + .flex-xxl-shrink-1 { + flex-shrink: 1 !important; + } + .flex-xxl-wrap { + flex-wrap: wrap !important; + } + .flex-xxl-nowrap { + flex-wrap: nowrap !important; + } + .flex-xxl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-xxl-start { + justify-content: flex-start !important; + } + .justify-content-xxl-end { + justify-content: flex-end !important; + } + .justify-content-xxl-center { + justify-content: center !important; + } + .justify-content-xxl-between { + justify-content: space-between !important; + } + .justify-content-xxl-around { + justify-content: space-around !important; + } + .justify-content-xxl-evenly { + justify-content: space-evenly !important; + } + .align-items-xxl-start { + align-items: flex-start !important; + } + .align-items-xxl-end { + align-items: flex-end !important; + } + .align-items-xxl-center { + align-items: center !important; + } + .align-items-xxl-baseline { + align-items: baseline !important; + } + .align-items-xxl-stretch { + align-items: stretch !important; + } + .align-content-xxl-start { + align-content: flex-start !important; + } + .align-content-xxl-end { + align-content: flex-end !important; + } + .align-content-xxl-center { + align-content: center !important; + } + .align-content-xxl-between { + align-content: space-between !important; + } + .align-content-xxl-around { + align-content: space-around !important; + } + .align-content-xxl-stretch { + align-content: stretch !important; + } + .align-self-xxl-auto { + align-self: auto !important; + } + .align-self-xxl-start { + align-self: flex-start !important; + } + .align-self-xxl-end { + align-self: flex-end !important; + } + .align-self-xxl-center { + align-self: center !important; + } + .align-self-xxl-baseline { + align-self: baseline !important; + } + .align-self-xxl-stretch { + align-self: stretch !important; + } + .order-xxl-first { + order: -1 !important; + } + .order-xxl-0 { + order: 0 !important; + } + .order-xxl-1 { + order: 1 !important; + } + .order-xxl-2 { + order: 2 !important; + } + .order-xxl-3 { + order: 3 !important; + } + .order-xxl-4 { + order: 4 !important; + } + .order-xxl-5 { + order: 5 !important; + } + .order-xxl-last { + order: 6 !important; + } + .m-xxl-0 { + margin: 0 !important; + } + .m-xxl-1 { + margin: 0.25rem !important; + } + .m-xxl-2 { + margin: 0.5rem !important; + } + .m-xxl-3 { + margin: 1rem !important; + } + .m-xxl-4 { + margin: 1.5rem !important; + } + .m-xxl-5 { + margin: 3rem !important; + } + .m-xxl-auto { + margin: auto !important; + } + .mx-xxl-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-xxl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-xxl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-xxl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-xxl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-xxl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-xxl-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-xxl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-xxl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-xxl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-xxl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-xxl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-xxl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-xxl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-xxl-0 { + margin-top: 0 !important; + } + .mt-xxl-1 { + margin-top: 0.25rem !important; + } + .mt-xxl-2 { + margin-top: 0.5rem !important; + } + .mt-xxl-3 { + margin-top: 1rem !important; + } + .mt-xxl-4 { + margin-top: 1.5rem !important; + } + .mt-xxl-5 { + margin-top: 3rem !important; + } + .mt-xxl-auto { + margin-top: auto !important; + } + .me-xxl-0 { + margin-right: 0 !important; + } + .me-xxl-1 { + margin-right: 0.25rem !important; + } + .me-xxl-2 { + margin-right: 0.5rem !important; + } + .me-xxl-3 { + margin-right: 1rem !important; + } + .me-xxl-4 { + margin-right: 1.5rem !important; + } + .me-xxl-5 { + margin-right: 3rem !important; + } + .me-xxl-auto { + margin-right: auto !important; + } + .mb-xxl-0 { + margin-bottom: 0 !important; + } + .mb-xxl-1 { + margin-bottom: 0.25rem !important; + } + .mb-xxl-2 { + margin-bottom: 0.5rem !important; + } + .mb-xxl-3 { + margin-bottom: 1rem !important; + } + .mb-xxl-4 { + margin-bottom: 1.5rem !important; + } + .mb-xxl-5 { + margin-bottom: 3rem !important; + } + .mb-xxl-auto { + margin-bottom: auto !important; + } + .ms-xxl-0 { + margin-left: 0 !important; + } + .ms-xxl-1 { + margin-left: 0.25rem !important; + } + .ms-xxl-2 { + margin-left: 0.5rem !important; + } + .ms-xxl-3 { + margin-left: 1rem !important; + } + .ms-xxl-4 { + margin-left: 1.5rem !important; + } + .ms-xxl-5 { + margin-left: 3rem !important; + } + .ms-xxl-auto { + margin-left: auto !important; + } + .p-xxl-0 { + padding: 0 !important; + } + .p-xxl-1 { + padding: 0.25rem !important; + } + .p-xxl-2 { + padding: 0.5rem !important; + } + .p-xxl-3 { + padding: 1rem !important; + } + .p-xxl-4 { + padding: 1.5rem !important; + } + .p-xxl-5 { + padding: 3rem !important; + } + .px-xxl-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-xxl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-xxl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-xxl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-xxl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-xxl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-xxl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-xxl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-xxl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-xxl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-xxl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-xxl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-xxl-0 { + padding-top: 0 !important; + } + .pt-xxl-1 { + padding-top: 0.25rem !important; + } + .pt-xxl-2 { + padding-top: 0.5rem !important; + } + .pt-xxl-3 { + padding-top: 1rem !important; + } + .pt-xxl-4 { + padding-top: 1.5rem !important; + } + .pt-xxl-5 { + padding-top: 3rem !important; + } + .pe-xxl-0 { + padding-right: 0 !important; + } + .pe-xxl-1 { + padding-right: 0.25rem !important; + } + .pe-xxl-2 { + padding-right: 0.5rem !important; + } + .pe-xxl-3 { + padding-right: 1rem !important; + } + .pe-xxl-4 { + padding-right: 1.5rem !important; + } + .pe-xxl-5 { + padding-right: 3rem !important; + } + .pb-xxl-0 { + padding-bottom: 0 !important; + } + .pb-xxl-1 { + padding-bottom: 0.25rem !important; + } + .pb-xxl-2 { + padding-bottom: 0.5rem !important; + } + .pb-xxl-3 { + padding-bottom: 1rem !important; + } + .pb-xxl-4 { + padding-bottom: 1.5rem !important; + } + .pb-xxl-5 { + padding-bottom: 3rem !important; + } + .ps-xxl-0 { + padding-left: 0 !important; + } + .ps-xxl-1 { + padding-left: 0.25rem !important; + } + .ps-xxl-2 { + padding-left: 0.5rem !important; + } + .ps-xxl-3 { + padding-left: 1rem !important; + } + .ps-xxl-4 { + padding-left: 1.5rem !important; + } + .ps-xxl-5 { + padding-left: 3rem !important; + } + .gap-xxl-0 { + gap: 0 !important; + } + .gap-xxl-1 { + gap: 0.25rem !important; + } + .gap-xxl-2 { + gap: 0.5rem !important; + } + .gap-xxl-3 { + gap: 1rem !important; + } + .gap-xxl-4 { + gap: 1.5rem !important; + } + .gap-xxl-5 { + gap: 3rem !important; + } + .row-gap-xxl-0 { + row-gap: 0 !important; + } + .row-gap-xxl-1 { + row-gap: 0.25rem !important; + } + .row-gap-xxl-2 { + row-gap: 0.5rem !important; + } + .row-gap-xxl-3 { + row-gap: 1rem !important; + } + .row-gap-xxl-4 { + row-gap: 1.5rem !important; + } + .row-gap-xxl-5 { + row-gap: 3rem !important; + } + .column-gap-xxl-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-xxl-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-xxl-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-xxl-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-xxl-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-xxl-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } + .text-xxl-start { + text-align: left !important; + } + .text-xxl-end { + text-align: right !important; + } + .text-xxl-center { + text-align: center !important; + } +} +@media (min-width: 1200px) { + .fs-1 { + font-size: 2.5rem !important; + } + .fs-2 { + font-size: 2rem !important; + } + .fs-3 { + font-size: 1.75rem !important; + } + .fs-4 { + font-size: 1.5rem !important; + } +} +@media print { + .d-print-inline { + display: inline !important; + } + .d-print-inline-block { + display: inline-block !important; + } + .d-print-block { + display: block !important; + } + .d-print-grid { + display: grid !important; + } + .d-print-inline-grid { + display: inline-grid !important; + } + .d-print-table { + display: table !important; + } + .d-print-table-row { + display: table-row !important; + } + .d-print-table-cell { + display: table-cell !important; + } + .d-print-flex { + display: flex !important; + } + .d-print-inline-flex { + display: inline-flex !important; + } + .d-print-none { + display: none !important; + } +} + +/*# sourceMappingURL=bootstrap.css.map */ \ No newline at end of file diff --git a/app/assets/stylesheets/bootstrap.css.map b/app/assets/stylesheets/bootstrap.css.map new file mode 100644 index 0000000000..99936c6866 --- /dev/null +++ b/app/assets/stylesheets/bootstrap.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap.css","../../scss/mixins/_banner.scss","../../scss/_root.scss","../../scss/vendor/_rfs.scss","../../scss/mixins/_color-mode.scss","../../scss/_reboot.scss","../../scss/_variables.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_color-bg.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_focus-ring.scss","../../scss/helpers/_icon-link.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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jBE;EAGE,gBF0nBkB;EEznBlB,gBFymB0B;EH7WtB,iCAAA;AHmUR;AG/dI;EKpGF;ILuQM,iBAAA;EHgUN;AACF;;AQxkBE;EAGE,gBF0nBkB;EEznBlB,gBFymB0B;EH7WtB,iCAAA;AH8UR;AG1eI;EKpGF;ILuQM,eAAA;EH2UN;AACF;;AQnlBE;EAGE,gBF0nBkB;EEznBlB,gBFymB0B;EH7WtB,iCAAA;AHyVR;AGrfI;EKpGF;ILuQM,iBAAA;EHsVN;AACF;;AQ9lBE;EAGE,gBF0nBkB;EEznBlB,gBFymB0B;EH7WtB,iCAAA;AHoWR;AGhgBI;EKpGF;ILuQM,eAAA;EHiWN;AACF;;AQzmBE;EAGE,gBF0nBkB;EEznBlB,gBFymB0B;EH7WtB,iCAAA;AH+WR;AG3gBI;EKpGF;ILuQM,iBAAA;EH4WN;AACF;;AQ5lBA;ECvDE,eAAA;EACA,gBAAA;ATupBF;;AQ5lBA;EC5DE,eAAA;EACA,gBAAA;AT4pBF;;AQ9lBA;EACE,qBAAA;ARimBF;AQ/lBE;EACE,oBFsoB0B;ANrC9B;;AQvlBA;EL8MM,kBALI;EKvMR,yBAAA;AR0lBF;;AQtlBA;EACE,mBFiUO;EH1HH,kBALI;AHwZV;AQvlBE;EACE,gBAAA;ARylBJ;;AQrlBA;EACE,iBAAA;EACA,mBFuTO;EH1HH,kBALI;EKtLR,cFtFS;AN8qBX;AQtlBE;EACE,aAAA;ARwlBJ;;AUxrBA;ECIE,eAAA;EAGA,YAAA;AXsrBF;;AUvrBA;EACE,gBJ+jDkC;EI9jDlC,mCJ+jDkC;EI9jDlC,2DAAA;EHGE,sCAAA;EIRF,eAAA;EAGA,YAAA;AX+rBF;;AUjrBA;EAEE,qBAAA;AVmrBF;;AUhrBA;EACE,qBAAA;EACA,cAAA;AVmrBF;;AUhrBA;EPyPM,kBALI;EOlPR,gCJkjDkC;AN/3BpC;;AYrtBE;;;;;;;ECHA,qBAAA;EACA,gBAAA;EACA,WAAA;EACA,6CAAA;EACA,4CAAA;EACA,kBAAA;EACA,iBAAA;AbkuBF;;Ac5qBI;EF5CE;IACE,gBNkee;EN0PrB;AACF;AclrBI;EF5CE;IACE,gBNkee;EN+PrB;AACF;AcvrBI;EF5CE;IACE,gB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2BforB0B;EeprB1B,0BforB0B;EenrB1B,2BfsyBoC;EiBp4BtC,uCjBqiCgC;Eer8B9B,oBAAA;EACA,qBAAA;EACA,mBAAA;EACA,eAAA;EACA,+CfgsB0B;Ee/rB1B,gBAAA;ECzFE,6ID0FF;EC1FE,qID0FF;ArBohEJ;AqBhiEE;EACE,yBAAA;EACA,0BAAA;EACA,2BforB0B;EeprB1B,0BforB0B;EenrB1B,2BfsyBoC;EiBp4BtC,uCjBqiCgC;Eer8B9B,oBAAA;EACA,qBAAA;EACA,mBAAA;EACA,eAAA;EACA,+CfgsB0B;Ee/rB1B,gBAAA;ECzFE,qID0FF;ArBohEJ;AsB1mEM;ED0EJ;ICzEM,wBAAA;IAAA,gBAAA;EtB6mEN;EqBpiEA;ICzEM,gBAAA;EtB6mEN;AACF;AqBthEE;EACE,wCf47B8B;AN4lClC;AqBzhEE;EACE,wCf47B8B;AN4lClC;;AqB/gEA;EACE,cAAA;EACA,WAAA;EACA,mBAAA;EACA,gBAAA;EACA,gBfwf4B;Eevf5B,2Bf2xBsC;Ee1xBtC,6BAAA;EACA,yBAAA;EACA,sCAAA;ArBkhEF;AqBhhEE;EACE,UAAA;ArBkhEJ;AqB/gEE;EAEE,gBAAA;EACA,eAAA;ArBghEJ;;AqBrgEA;EACE,mEf4wBsC;Ee3wBtC,uBAAA;ElByII,mBALI;EIvQN,yCAAA;AP6oEJ;AqBtgEE;EACE,uBAAA;EACA,wBAAA;EACA,0BfooB0B;EepoB1B,yBfooB0B;ANo4C9B;AqB3gEE;EACE,uBAAA;EACA,wBAAA;EACA,0BfooB0B;EepoB1B,yBfooB0B;ANo4C9B;;AqBpgEA;EACE,iEfgwBsC;Ee/vBtC,oBAAA;ElB4HI,kBALI;EIvQN,yCAAA;APypEJ;AqBrgEE;EACE,oBAAA;EACA,qBAAA;EACA,wBf2nB0B;Ee3nB1B,uBf2nB0B;AN44C9B;AqB1gEE;EACE,oBAAA;EACA,qBAAA;EACA,wBf2nB0B;Ee3nB1B,uBf2nB0B;AN44C9B;;AqB//DE;EACE,oEf6uBoC;ANqxCxC;AqB//DE;EACE,mEf0uBoC;ANuxCxC;AqB9/DE;EACE,iEfuuBoC;ANyxCxC;;AqB3/DA;EACE,WfquBsC;EepuBtC,gEf8tBsC;Ee7tBtC,iBfilB4B;AN66C9B;AqB5/DE;EACE,eAAA;ArB8/DJ;AqB3/DE;EACE,oBAAA;EdvLA,sCAAA;APqrEJ;AqB1/DE;EACE,oBAAA;Ed5LA,sCAAA;APyrEJ;AqBz/DE;EAAoB,+Df8sBkB;AN8yCxC;AqB3/DE;EAAoB,6Df8sBkB;ANgzCxC;;AwB7sEA;EACE,wPAAA;EAEA,cAAA;EACA,WAAA;EACA,0CAAA;ErBqRI,eALI;EqB7QR,gBlB+lB4B;EkB9lB5B,gBlBsmB4B;EkBrmB5B,2BlBy3BsC;EkBx3BtC,wBAAA;EAAA,qBAAA;EAAA,gBAAA;EACA,mClBk3BsC;EkBj3BtC,mFAAA;EACA,4BAAA;EACA,yClB+9BkC;EkB99BlC,0BlB+9BkC;EkB99BlC,2DAAA;EjBHE,sCAAA;EeHE,wEESJ;AxB6sEF;AsBltEM;EEfN;IFgBQ,gBAAA;EtBqtEN;AACF;AwBhtEE;EACE,qBlBs3BoC;EkBr3BpC,UAAA;EAKE,kDlBi+B4B;AN6uClC;AwB1sEE;EAEE,sBlB6uB0B;EkB5uB1B,sBAAA;AxB2sEJ;AwBxsEE;EAEE,wClBu1BoC;ANk3CxC;AwBpsEE;EACE,kBAAA;EACA,uCAAA;AxBssEJ;;AwBlsEA;EACE,oBlBsuB4B;EkBruB5B,uBlBquB4B;EkBpuB5B,oBlBquB4B;EHlgBxB,mBALI;EIvQN,yCAAA;APgvEJ;;AwBlsEA;EACE,mBlBkuB4B;EkBjuB5B,sBlBiuB4B;EkBhuB5B,kBlBiuB4B;EHtgBxB,kBALI;EIvQN,yCAAA;APwvEJ;;AwBhsEI;EACE,wPAAA;AxBmsEN;;AyB3wEA;EACE,cAAA;EACA,kBnBq6BwC;EmBp6BxC,mBnBq6BwC;EmBp6BxC,uBnBq6BwC;ANy2C1C;AyB5wEE;EACE,WAAA;EACA,mBAAA;AzB8wEJ;;AyB1wEA;EACE,oBnB25BwC;EmB15BxC,eAAA;EACA,iBAAA;AzB6wEF;AyB3wEE;EACE,YAAA;EACA,oBAAA;EACA,cAAA;AzB6wEJ;;AyBzwEA;EACE,qCAAA;EAEA,cAAA;EACA,UnB04BwC;EmBz4BxC,WnBy4BwC;EmBx4BxC,kBAAA;EACA,mBAAA;EACA,wBAAA;EAAA,qBAAA;EAAA,gBAAA;EACA,yCAAA;EACA,+CAAA;EACA,4BAAA;EACA,2BAAA;EACA,wBAAA;EACA,2DnB24BwC;EmB14BxC,iCAAA;EAAA,mBAAA;EAAA,yBAAA;AzB2wEF;AyBxwEE;ElB3BE,qBAAA;APsyEJ;AyBvwEE;EAEE,kBnBm4BsC;ANq4C1C;AyBrwEE;EACE,uBnB03BsC;AN64C1C;AyBpwEE;EACE,qBnBs1BoC;EmBr1BpC,UAAA;EACA,kDnB8foB;ANwwDxB;AyBnwEE;EACE,yBnB5BM;EmB6BN,qBnB7BM;ANkyEV;AyBnwEI;EAII,uPAAA;AzBkwER;AyB9vEI;EAII,+JAAA;AzB6vER;AyBxvEE;EACE,yBnBjDM;EmBkDN,qBnBlDM;EmBuDJ,iPAAA;AzBsvEN;AyBlvEE;EACE,oBAAA;EACA,YAAA;EACA,YnBk2BuC;ANk5C3C;AyB7uEI;EACE,eAAA;EACA,YnBy1BqC;ANs5C3C;;AyBjuEA;EACE,mBnBo1BgC;ANg5ClC;AyBluEE;EACE,2KAAA;EAEA,UnB80B8B;EmB70B9B,mBAAA;EACA,0CAAA;EACA,gCAAA;ElBjHA,kBAAA;EeHE,iDGsHF;AzBmuEJ;AsBr1EM;EG0GJ;IHzGM,gBAAA;EtBw1EN;AACF;AyBtuEI;EACE,6JAAA;AzBwuEN;AyBruEI;EACE,iCnB60B4B;EmBx0B1B,0JAAA;AzBmuER;AyB9tEE;EACE,oBnBwzB8B;EmBvzB9B,eAAA;AzBguEJ;AyB9tEI;EACE,oBAAA;EACA,cAAA;AzBguEN;;AyB3tEA;EACE,qBAAA;EACA,kBnBsyBgC;ANw7ClC;;AyB3tEA;EACE,kBAAA;EACA,sBAAA;EACA,oBAAA;AzB8tEF;AyB1tEI;EACE,oBAAA;EACA,YAAA;EACA,anBspBwB;ANskD9B;;AyBrtEI;EACE,iLAAA;AzBwtEN;;A0B34EA;EACE,WAAA;EACA,cAAA;EACA,UAAA;EACA,wBAAA;EAAA,qBAAA;EAAA,gBAAA;EACA,6BAAA;A1B84EF;A0B54EE;EACE,UAAA;A1B84EJ;A0B14EI;EAA0B,kEpB8gCa;AN+3C3C;A0B54EI;EAA0B,kEpB6gCa;ANk4C3C;A0B54EE;EACE,SAAA;A1B84EJ;A0B34EE;EACE,WpB+/BuC;EoB9/BvC,YpB8/BuC;EoB7/BvC,oBAAA;EACA,wBAAA;EAAA,gBAAA;EH1BF,yBjBkCQ;EoBNN,SpB6/BuC;EC1gCvC,mBAAA;EeHE,oHImBF;EJnBE,4GImBF;A1B44EJ;AsB35EM;EIMJ;IJLM,wBAAA;IAAA,gBAAA;EtB85EN;AACF;A0B/4EI;EHjCF,yBjB8hCyC;ANq5C3C;A0B74EE;EACE,WpBw+B8B;EoBv+B9B,cpBw+B8B;EoBv+B9B,kBAAA;EACA,epBu+B8B;EoBt+B9B,wCpBu+B8B;EoBt+B9B,yBAAA;EnB7BA,mBAAA;AP66EJ;A0B34EE;EACE,WpBo+BuC;EoBn+BvC,YpBm+BuC;EoBl+BvC,qBAAA;EAAA,gBAAA;EHpDF,yBjBkCQ;EoBoBN,SpBm+BuC;EC1gCvC,mBAAA;EeHE,iHI6CF;EJ7CE,4GI6CF;A1B44EJ;AsBr7EM;EIiCJ;IJhCM,qBAAA;IAAA,gBAAA;EtBw7EN;AACF;A0B/4EI;EH3DF,yBjB8hCyC;AN+6C3C;A0B74EE;EACE,WpB88B8B;EoB78B9B,cpB88B8B;EoB78B9B,kBAAA;EACA,epB68B8B;EoB58B9B,wCpB68B8B;EoB58B9B,yBAAA;EnBvDA,mBAAA;APu8EJ;A0B34EE;EACE,oBAAA;A1B64EJ;A0B34EI;EACE,2CpBg9BqC;AN67C3C;A0B14EI;EACE,2CpB48BqC;ANg8C3C;;A2Bn+EA;EACE,kBAAA;A3Bs+EF;A2Bp+EE;;;EAGE,uDrBwiCoC;EqBviCpC,2DrBuiCoC;EqBtiCpC,iBrBuiCoC;AN+7CxC;A2Bn+EE;EACE,kBAAA;EACA,MAAA;EACA,OAAA;EACA,UAAA;EACA,eAAA;EACA,YAAA;EACA,qBAAA;EACA,gBAAA;EACA,2CAAA;EACA,iBAAA;EACA,uBAAA;EACA,mBAAA;EACA,oBAAA;EACA,gDAAA;EACA,qBAAA;ELVE,gEKWF;A3Bq+EJ;AsB5+EM;EKTJ;ILUM,gBAAA;EtB++EN;AACF;A2Bv+EE;;EAEE,qBAAA;A3By+EJ;A2Bv+EI;;EACE,kBAAA;A3B0+EN;A2Bv+EI;;;EAEE,qBrB0gCkC;EqBzgClC,wBrB0gCkC;ANg+CxC;A2Bv+EI;;EACE,qBrBqgCkC;EqBpgClC,wBrBqgCkC;ANq+CxC;A2Bt+EE;EACE,qBrB+/BoC;EqB9/BpC,wBrB+/BoC;EqB9/BpC,qBrBguB0B;ANwwD9B;A2Bj+EI;;;;EACE,8DrBy/BkC;AN6+CxC;A2Bj+EI;EACE,8DrBm/BkC;ANg/CxC;A2B99EI;;EACE,kBAAA;EACA,oBAAA;EACA,WAAA;EACA,arBw+BkC;EqBv+BlC,WAAA;EACA,mCrBqzBkC;ECh3BpC,sCAAA;AP6hFJ;A2B99EE;EACE,wCrBkzBoC;AN8qDxC;A2B59EI;EACE,sCAAA;A3B89EN;A2B19EE;;EAEE,crBhFO;AN4iFX;;A4BtjFA;EACE,kBAAA;EACA,aAAA;EACA,eAAA;EACA,oBAAA;EACA,WAAA;A5ByjFF;A4BvjFE;;;EAGE,kBAAA;EACA,cAAA;EACA,SAAA;EACA,YAAA;A5ByjFJ;A4BrjFE;;;EAGE,UAAA;A5BujFJ;A4BjjFE;EACE,kBAAA;EACA,UAAA;A5BmjFJ;A4BjjFI;EACE,UAAA;A5BmjFN;;A4BxiFA;EACE,aAAA;EACA,mBAAA;EACA,yBAAA;EzB8OI,eALI;EyBvOR,gBtByjB4B;EsBxjB5B,gBtBgkB4B;EsB/jB5B,2BtBm1BsC;EsBl1BtC,kBAAA;EACA,mBAAA;EACA,uCtB06BsC;EsBz6BtC,2DAAA;ErBtCE,sCAAA;APklFJ;;A4BliFA;;;;EAIE,oBAAA;EzBwNI,kBALI;EIvQN,yCAAA;AP2lFJ;;A4BliFA;;;;EAIE,uBAAA;EzB+MI,mBALI;EIvQN,yCAAA;APomFJ;;A4BliFA;;EAEE,mBAAA;A5BqiFF;;A4BxhFI;;;;ErBjEA,0BAAA;EACA,6BAAA;APgmFJ;A4BvhFI;;;;ErB1EA,0BAAA;EACA,6BAAA;APumFJ;A4BjhFE;EACE,8CAAA;ErB1EA,yBAAA;EACA,4BAAA;AP8lFJ;A4BjhFE;;ErB9EE,yBAAA;EACA,4BAAA;APmmFJ;;A6B3nFE;EACE,aAAA;EACA,WAAA;EACA,mBvBu0BoC;EHrkBlC,kBALI;E0B1PN,iCvBkjCqB;AN2kDzB;;A6B1nFE;EACE,kBAAA;EACA,SAAA;EACA,UAAA;EACA,aAAA;EACA,eAAA;EACA,uBAAA;EACA,kBAAA;E1BqPE,mBALI;E0B7ON,WvBqiCqB;EuBpiCrB,mCvBoiCqB;EC/jCrB,sCAAA;APwpFJ;;A6BxnFI;;;;EAEE,cAAA;A7B6nFN;;A6B5qFI;EAqDE,+CvBuhCmB;EuBphCjB,oCvB81BgC;EuB71BhC,0PAAA;EACA,4BAAA;EACA,2DAAA;EACA,gEAAA;A7BynFR;A6BtnFM;EACE,+CvB4gCiB;EuBvgCf,2DvBugCe;AN6mDzB;;A6BzrFI;EA+EI,oCvBu0BgC;EuBt0BhC,kFAAA;A7B8mFR;;A6B9rFI;EAuFE,+CvBq/BmB;ANsnDzB;A6BxmFQ;EAEE,kQAAA;EACA,uBvBq5B8B;EuBp5B9B,+DAAA;EACA,2EAAA;A7BymFV;A6BrmFM;EACE,+CvBw+BiB;EuBn+Bf,2DvBm+Be;ANgoDzB;;A6B5sFI;EAkHI,yCAAA;A7B8lFR;;A6BhtFI;EAyHE,+CvBm9BmB;ANwoDzB;A6BzlFM;EACE,4CvBg9BiB;AN2oDzB;A6BxlFM;EACE,2DvB48BiB;AN8oDzB;A6BvlFM;EACE,iCvBw8BiB;ANipDzB;;A6BplFI;EACE,kBAAA;A7BulFN;;A6BjuFI;;;;;EAoJM,UAAA;A7BqlFV;;A6BrtFE;EACE,aAAA;EACA,WAAA;EACA,mBvBu0BoC;EHrkBlC,kBALI;E0B1PN,mCvBkjCqB;ANqqDzB;;A6BptFE;EACE,kBAAA;EACA,SAAA;EACA,UAAA;EACA,aAAA;EACA,eAAA;EACA,uBAAA;EACA,kBAAA;E1BqPE,mBALI;E0B7ON,WvBqiCqB;EuBpiCrB,kCvBoiCqB;EC/jCrB,sCAAA;APkvFJ;;A6BltFI;;;;EAEE,cAAA;A7ButFN;;A6BtwFI;EAqDE,iDvBuhCmB;EuBphCjB,oCvB81BgC;EuB71BhC,4UAAA;EACA,4BAAA;EACA,2DAAA;EACA,gEAAA;A7BmtFR;A6BhtFM;EAC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,uBAAA;AxEq2OZ;;AwE52OQ;EAOI,2BAAA;AxEy2OZ;;AwEh3OQ;EAOI,wBAAA;AxE62OZ;;AwEp3OQ;EAOI,4BAAA;AxEi3OZ;;AwEx3OQ;EAOI,yBAAA;AxEq3OZ;;AwE53OQ;EAOI,8BAAA;AxEy3OZ;;AwEh4OQ;EAOI,iCAAA;AxE63OZ;;AwEp4OQ;EAOI,sCAAA;AxEi4OZ;;AwEx4OQ;EAOI,yCAAA;AxEq4OZ;;AwE54OQ;EAOI,uBAAA;AxEy4OZ;;AwEh5OQ;EAOI,uBAAA;AxE64OZ;;AwEp5OQ;EAOI,yBAAA;AxEi5OZ;;AwEx5OQ;EAOI,yBAAA;AxEq5OZ;;AwE55OQ;EAOI,0BAAA;AxEy5OZ;;AwEh6OQ;EAOI,4BAAA;AxE65OZ;;AwEp6OQ;EAOI,kCAAA;AxEi6OZ;;AwEx6OQ;EAOI,sCAAA;AxEq6OZ;;AwE56OQ;EAOI,oCAAA;AxEy6OZ;;AwEh7OQ;EAOI,kCAAA;AxE66OZ;;AwEp7OQ;EAOI,yCAAA;AxEi7OZ;;AwEx7OQ;EAOI,wCAAA;AxEq7OZ;;AwE57OQ;EAOI,wCAAA;AxEy7OZ;;AwEh8OQ;EAOI,kCAAA;AxE67OZ;;AwEp8OQ;EAOI,gCAAA;AxEi8OZ;;AwEx8OQ;EAOI,8BAAA;AxEq8OZ;;AwE58OQ;EAOI,gCAAA;AxEy8OZ;;AwEh9OQ;EAOI,+BAAA;AxE68OZ;;AwEp9OQ;EAOI,oCAAA;AxEi9OZ;;AwEx9OQ;EAOI,kCAAA;AxEq9OZ;;AwE59OQ;EAOI,gCAAA;AxEy9OZ;;AwEh+OQ;EAOI,uCAAA;AxE69OZ;;AwEp+OQ;EAOI,sCAAA;AxEi+OZ;;AwEx+OQ;EAOI,iCAAA;AxEq+OZ;;AwE5+OQ;EAOI,2BAAA;AxEy+OZ;;AwEh/OQ;EAOI,iCAAA;AxE6+OZ;;AwEp/OQ;EAOI,+BAAA;AxEi/OZ;;AwEx/OQ;EAOI,6BAAA;AxEq/OZ;;AwE5/OQ;EAOI,+BAAA;AxEy/OZ;;AwEhgPQ;EAOI,8BAAA;AxE6/OZ;;AwEpgPQ;EAOI,oBAAA;AxEigPZ;;AwExgPQ;EAOI,mBAAA;AxEqgPZ;;AwE5gPQ;EAOI,mBAAA;AxEygPZ;;AwEhhPQ;EAOI,mBAAA;AxE6gPZ;;AwEphPQ;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;EwE75TM;IAOI,8BAAA;ExEy5TV;EwEh6TM;IAOI,wBAAA;ExE45TV;EwEn6TM;IAOI,+BAAA;ExE+5TV;EwEt6TM;IAOI,wBAAA;ExEk6TV;EwEz6TM;IAOI,yBAAA;ExEq6TV;EwE56TM;IAOI,8BAAA;ExEw6TV;EwE/6TM;IAOI,iCAAA;ExE26TV;EwEl7TM;IAOI,sCAAA;ExE86TV;EwEr7TM;IAOI,yCAAA;ExEi7TV;EwEx7TM;IAOI,uBAAA;ExEo7TV;EwE37TM;IAOI,uBAAA;ExEu7TV;EwE97TM;IAOI,yBAAA;ExE07TV;EwEj8TM;IAOI,yBAAA;ExE67TV;EwEp8TM;IAOI,0BAAA;ExEg8TV;EwEv8TM;IAOI,4BAAA;ExEm8TV;EwE18TM;IAOI,kCAAA;ExEs8TV;EwE78TM;IAOI,sCAAA;ExEy8TV;EwEh9TM;IAOI,oCAAA;ExE48TV;EwEn9TM;IAOI,kCAAA;ExE+8TV;EwEt9TM;IAOI,yCAAA;ExEk9TV;EwEz9TM;IAOI,wCAAA;ExEq9TV;EwE59TM;IAOI,wCAAA;ExEw9TV;EwE/9TM;IAOI,kCAAA;ExE29TV;EwEl+TM;IAOI,gCAAA;ExE89TV;EwEr+TM;IAOI,8BAAA;ExEi+TV;EwEx+TM;IAOI,gCAAA;ExEo+TV;EwE3+TM;IAOI,+BAAA;ExEu+TV;EwE9+TM;IAOI,oCAAA;ExE0+TV;EwEj/TM;IAOI,kCAAA;ExE6+TV;EwEp/TM;IAOI,gCAAA;ExEg/TV;EwEv/TM;IAOI,uCAAA;ExEm/TV;EwE1/TM;IAOI,sCAAA;ExEs/TV;EwE7/TM;IAOI,iCAAA;ExEy/TV;EwEhgUM;IAOI,2BAAA;ExE4/TV;EwEngUM;IAOI,iCAAA;ExE+/TV;EwEtgUM;IAOI,+BAAA;ExEkgUV;EwEzgUM;IAOI,6BAAA;ExEqgUV;EwE5gUM;IAOI,+BAAA;ExEwgUV;EwE/gUM;IAOI,8BAAA;ExE2gUV;EwElhUM;IAOI,oBAAA;ExE8gUV;EwErhUM;IAOI,mBAAA;ExEihUV;EwExhUM;IAOI,mBAAA;ExEohUV;EwE3hUM;IAOI,mBAAA;ExEuhUV;EwE9hUM;IAOI,mBAAA;ExE0hUV;EwEjiUM;IAO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","file":"bootstrap.css","sourcesContent":["@charset \"UTF-8\";\n/*!\n * Bootstrap v5.3.8 (https://getbootstrap.com/)\n * Copyright 2011-2025 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n:root,\n[data-bs-theme=light] {\n --bs-blue: #0d6efd;\n --bs-indigo: #6610f2;\n --bs-purple: #6f42c1;\n --bs-pink: #d63384;\n --bs-red: #dc3545;\n --bs-orange: #fd7e14;\n --bs-yellow: #ffc107;\n --bs-green: #198754;\n --bs-teal: #20c997;\n --bs-cyan: #0dcaf0;\n --bs-black: #000;\n --bs-white: #fff;\n --bs-gray: #6c757d;\n --bs-gray-dark: #343a40;\n --bs-gray-100: #f8f9fa;\n --bs-gray-200: #e9ecef;\n --bs-gray-300: #dee2e6;\n --bs-gray-400: #ced4da;\n --bs-gray-500: #adb5bd;\n --bs-gray-600: #6c757d;\n --bs-gray-700: #495057;\n --bs-gray-800: #343a40;\n --bs-gray-900: #212529;\n --bs-primary: #0d6efd;\n --bs-secondary: #6c757d;\n --bs-success: #198754;\n --bs-info: #0dcaf0;\n --bs-warning: #ffc107;\n --bs-danger: #dc3545;\n --bs-light: #f8f9fa;\n --bs-dark: #212529;\n --bs-primary-rgb: 13, 110, 253;\n --bs-secondary-rgb: 108, 117, 125;\n --bs-success-rgb: 25, 135, 84;\n --bs-info-rgb: 13, 202, 240;\n --bs-warning-rgb: 255, 193, 7;\n --bs-danger-rgb: 220, 53, 69;\n --bs-light-rgb: 248, 249, 250;\n --bs-dark-rgb: 33, 37, 41;\n --bs-primary-text-emphasis: #052c65;\n --bs-secondary-text-emphasis: #2b2f32;\n --bs-success-text-emphasis: #0a3622;\n --bs-info-text-emphasis: #055160;\n --bs-warning-text-emphasis: #664d03;\n --bs-danger-text-emphasis: #58151c;\n --bs-light-text-emphasis: #495057;\n --bs-dark-text-emphasis: #495057;\n --bs-primary-bg-subtle: #cfe2ff;\n --bs-secondary-bg-subtle: #e2e3e5;\n --bs-success-bg-subtle: #d1e7dd;\n --bs-info-bg-subtle: #cff4fc;\n --bs-warning-bg-subtle: #fff3cd;\n --bs-danger-bg-subtle: #f8d7da;\n --bs-light-bg-subtle: #fcfcfd;\n --bs-dark-bg-subtle: #ced4da;\n --bs-primary-border-subtle: #9ec5fe;\n --bs-secondary-border-subtle: #c4c8cb;\n --bs-success-border-subtle: #a3cfbb;\n --bs-info-border-subtle: #9eeaf9;\n --bs-warning-border-subtle: #ffe69c;\n --bs-danger-border-subtle: #f1aeb5;\n --bs-light-border-subtle: #e9ecef;\n --bs-dark-border-subtle: #adb5bd;\n --bs-white-rgb: 255, 255, 255;\n --bs-black-rgb: 0, 0, 0;\n --bs-font-sans-serif: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", \"Noto Sans\", \"Liberation Sans\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));\n --bs-body-font-family: var(--bs-font-sans-serif);\n --bs-body-font-size: 1rem;\n --bs-body-font-weight: 400;\n --bs-body-line-height: 1.5;\n --bs-body-color: #212529;\n --bs-body-color-rgb: 33, 37, 41;\n --bs-body-bg: #fff;\n --bs-body-bg-rgb: 255, 255, 255;\n --bs-emphasis-color: #000;\n --bs-emphasis-color-rgb: 0, 0, 0;\n --bs-secondary-color: rgba(33, 37, 41, 0.75);\n --bs-secondary-color-rgb: 33, 37, 41;\n --bs-secondary-bg: #e9ecef;\n --bs-secondary-bg-rgb: 233, 236, 239;\n --bs-tertiary-color: rgba(33, 37, 41, 0.5);\n --bs-tertiary-color-rgb: 33, 37, 41;\n --bs-tertiary-bg: #f8f9fa;\n --bs-tertiary-bg-rgb: 248, 249, 250;\n --bs-heading-color: inherit;\n --bs-link-color: #0d6efd;\n --bs-link-color-rgb: 13, 110, 253;\n --bs-link-decoration: underline;\n --bs-link-hover-color: #0a58ca;\n --bs-link-hover-color-rgb: 10, 88, 202;\n --bs-code-color: #d63384;\n --bs-highlight-color: #212529;\n --bs-highlight-bg: #fff3cd;\n --bs-border-width: 1px;\n --bs-border-style: solid;\n --bs-border-color: #dee2e6;\n --bs-border-color-translucent: rgba(0, 0, 0, 0.175);\n --bs-border-radius: 0.375rem;\n --bs-border-radius-sm: 0.25rem;\n --bs-border-radius-lg: 0.5rem;\n --bs-border-radius-xl: 1rem;\n --bs-border-radius-xxl: 2rem;\n --bs-border-radius-2xl: var(--bs-border-radius-xxl);\n --bs-border-radius-pill: 50rem;\n --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);\n --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);\n --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);\n --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);\n --bs-focus-ring-width: 0.25rem;\n --bs-focus-ring-opacity: 0.25;\n --bs-focus-ring-color: rgba(13, 110, 253, 0.25);\n --bs-form-valid-color: #198754;\n --bs-form-valid-border-color: #198754;\n --bs-form-invalid-color: #dc3545;\n --bs-form-invalid-border-color: #dc3545;\n}\n\n[data-bs-theme=dark] {\n color-scheme: dark;\n --bs-body-color: #dee2e6;\n --bs-body-color-rgb: 222, 226, 230;\n --bs-body-bg: #212529;\n --bs-body-bg-rgb: 33, 37, 41;\n --bs-emphasis-color: #fff;\n --bs-emphasis-color-rgb: 255, 255, 255;\n --bs-secondary-color: rgba(222, 226, 230, 0.75);\n --bs-secondary-color-rgb: 222, 226, 230;\n --bs-secondary-bg: #343a40;\n --bs-secondary-bg-rgb: 52, 58, 64;\n --bs-tertiary-color: rgba(222, 226, 230, 0.5);\n --bs-tertiary-color-rgb: 222, 226, 230;\n --bs-tertiary-bg: #2b3035;\n --bs-tertiary-bg-rgb: 43, 48, 53;\n --bs-primary-text-emphasis: #6ea8fe;\n --bs-secondary-text-emphasis: #a7acb1;\n --bs-success-text-emphasis: #75b798;\n --bs-info-text-emphasis: #6edff6;\n --bs-warning-text-emphasis: #ffda6a;\n --bs-danger-text-emphasis: #ea868f;\n --bs-light-text-emphasis: #f8f9fa;\n --bs-dark-text-emphasis: #dee2e6;\n --bs-primary-bg-subtle: #031633;\n --bs-secondary-bg-subtle: #161719;\n --bs-success-bg-subtle: #051b11;\n --bs-info-bg-subtle: #032830;\n --bs-warning-bg-subtle: #332701;\n --bs-danger-bg-subtle: #2c0b0e;\n --bs-light-bg-subtle: #343a40;\n --bs-dark-bg-subtle: #1a1d20;\n --bs-primary-border-subtle: #084298;\n --bs-secondary-border-subtle: #41464b;\n --bs-success-border-subtle: #0f5132;\n --bs-info-border-subtle: #087990;\n --bs-warning-border-subtle: #997404;\n --bs-danger-border-subtle: #842029;\n --bs-light-border-subtle: #495057;\n --bs-dark-border-subtle: #343a40;\n --bs-heading-color: inherit;\n --bs-link-color: #6ea8fe;\n --bs-link-hover-color: #8bb9fe;\n --bs-link-color-rgb: 110, 168, 254;\n --bs-link-hover-color-rgb: 139, 185, 254;\n --bs-code-color: #e685b5;\n --bs-highlight-color: #dee2e6;\n --bs-highlight-bg: #664d03;\n --bs-border-color: #495057;\n --bs-border-color-translucent: rgba(255, 255, 255, 0.15);\n --bs-form-valid-color: #75b798;\n --bs-form-valid-border-color: #75b798;\n --bs-form-invalid-color: #ea868f;\n --bs-form-invalid-border-color: #ea868f;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n :root {\n scroll-behavior: smooth;\n }\n}\n\nbody {\n margin: 0;\n font-family: var(--bs-body-font-family);\n font-size: var(--bs-body-font-size);\n font-weight: var(--bs-body-font-weight);\n line-height: var(--bs-body-line-height);\n color: var(--bs-body-color);\n text-align: var(--bs-body-text-align);\n background-color: var(--bs-body-bg);\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\nhr {\n margin: 1rem 0;\n color: inherit;\n border: 0;\n border-top: var(--bs-border-width) solid;\n opacity: 0.25;\n}\n\nh6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n color: var(--bs-heading-color);\n}\n\nh1, .h1 {\n font-size: calc(1.375rem + 1.5vw);\n}\n@media (min-width: 1200px) {\n h1, .h1 {\n font-size: 2.5rem;\n }\n}\n\nh2, .h2 {\n font-size: calc(1.325rem + 0.9vw);\n}\n@media (min-width: 1200px) {\n h2, .h2 {\n font-size: 2rem;\n }\n}\n\nh3, .h3 {\n font-size: calc(1.3rem + 0.6vw);\n}\n@media (min-width: 1200px) {\n h3, .h3 {\n font-size: 1.75rem;\n }\n}\n\nh4, .h4 {\n font-size: calc(1.275rem + 0.3vw);\n}\n@media (min-width: 1200px) {\n h4, .h4 {\n font-size: 1.5rem;\n }\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title] {\n text-decoration: underline dotted;\n cursor: help;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: 0.5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall, .small {\n font-size: 0.875em;\n}\n\nmark, .mark {\n padding: 0.1875em;\n color: var(--bs-highlight-color);\n background-color: var(--bs-highlight-bg);\n}\n\nsub,\nsup {\n position: relative;\n font-size: 0.75em;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\na {\n color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));\n text-decoration: underline;\n}\na:hover {\n --bs-link-color-rgb: var(--bs-link-hover-color-rgb);\n}\n\na:not([href]):not([class]), a:not([href]):not([class]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: var(--bs-font-monospace);\n font-size: 1em;\n}\n\npre {\n display: block;\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n font-size: 0.875em;\n}\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\ncode {\n font-size: 0.875em;\n color: var(--bs-code-color);\n word-wrap: break-word;\n}\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.1875rem 0.375rem;\n font-size: 0.875em;\n color: var(--bs-body-bg);\n background-color: var(--bs-body-color);\n border-radius: 0.25rem;\n}\nkbd kbd {\n padding: 0;\n font-size: 1em;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n color: var(--bs-secondary-color);\n text-align: left;\n}\n\nth {\n text-align: inherit;\n text-align: -webkit-match-parent;\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\nlabel {\n display: inline-block;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\n[role=button] {\n cursor: pointer;\n}\n\nselect {\n word-wrap: normal;\n}\nselect:disabled {\n opacity: 1;\n}\n\n[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {\n display: none !important;\n}\n\nbutton,\n[type=button],\n[type=reset],\n[type=submit] {\n -webkit-appearance: button;\n}\nbutton:not(:disabled),\n[type=button]:not(:disabled),\n[type=reset]:not(:disabled),\n[type=submit]:not(:disabled) {\n cursor: pointer;\n}\n\n::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ntextarea {\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n float: left;\n width: 100%;\n padding: 0;\n margin-bottom: 0.5rem;\n line-height: inherit;\n font-size: calc(1.275rem + 0.3vw);\n}\n@media (min-width: 1200px) {\n legend {\n font-size: 1.5rem;\n }\n}\nlegend + * {\n clear: left;\n}\n\n::-webkit-datetime-edit-fields-wrapper,\n::-webkit-datetime-edit-text,\n::-webkit-datetime-edit-minute,\n::-webkit-datetime-edit-hour-field,\n::-webkit-datetime-edit-day-field,\n::-webkit-datetime-edit-month-field,\n::-webkit-datetime-edit-year-field {\n padding: 0;\n}\n\n::-webkit-inner-spin-button {\n height: auto;\n}\n\n[type=search] {\n -webkit-appearance: textfield;\n outline-offset: -2px;\n}\n[type=search]::-webkit-search-cancel-button {\n cursor: pointer;\n filter: grayscale(1);\n}\n\n/* rtl:raw:\n[type=\"tel\"],\n[type=\"url\"],\n[type=\"email\"],\n[type=\"number\"] {\n direction: ltr;\n}\n*/\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-color-swatch-wrapper {\n padding: 0;\n}\n\n::file-selector-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\niframe {\n border: 0;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[hidden] {\n display: none !important;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-weight: 300;\n line-height: 1.2;\n font-size: calc(1.625rem + 4.5vw);\n}\n@media (min-width: 1200px) {\n .display-1 {\n font-size: 5rem;\n }\n}\n\n.display-2 {\n font-weight: 300;\n line-height: 1.2;\n font-size: calc(1.575rem + 3.9vw);\n}\n@media (min-width: 1200px) {\n .display-2 {\n font-size: 4.5rem;\n }\n}\n\n.display-3 {\n font-weight: 300;\n line-height: 1.2;\n font-size: calc(1.525rem + 3.3vw);\n}\n@media (min-width: 1200px) {\n .display-3 {\n font-size: 4rem;\n }\n}\n\n.display-4 {\n font-weight: 300;\n line-height: 1.2;\n font-size: calc(1.475rem + 2.7vw);\n}\n@media (min-width: 1200px) {\n .display-4 {\n font-size: 3.5rem;\n }\n}\n\n.display-5 {\n font-weight: 300;\n line-height: 1.2;\n font-size: calc(1.425rem + 2.1vw);\n}\n@media (min-width: 1200px) {\n .display-5 {\n font-size: 3rem;\n }\n}\n\n.display-6 {\n font-weight: 300;\n line-height: 1.2;\n font-size: calc(1.375rem + 1.5vw);\n}\n@media (min-width: 1200px) {\n .display-6 {\n font-size: 2.5rem;\n }\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 0.875em;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n.blockquote > :last-child {\n margin-bottom: 0;\n}\n\n.blockquote-footer {\n margin-top: -1rem;\n margin-bottom: 1rem;\n font-size: 0.875em;\n color: #6c757d;\n}\n.blockquote-footer::before {\n content: \"— \";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: var(--bs-body-bg);\n border: var(--bs-border-width) solid var(--bs-border-color);\n border-radius: var(--bs-border-radius);\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 0.875em;\n color: var(--bs-secondary-color);\n}\n\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n width: 100%;\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container-sm, .container {\n max-width: 540px;\n }\n}\n@media (min-width: 768px) {\n .container-md, .container-sm, .container {\n max-width: 720px;\n }\n}\n@media (min-width: 992px) {\n .container-lg, .container-md, .container-sm, .container {\n max-width: 960px;\n }\n}\n@media (min-width: 1200px) {\n .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1140px;\n }\n}\n@media (min-width: 1400px) {\n .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1320px;\n }\n}\n:root {\n --bs-breakpoint-xs: 0;\n --bs-breakpoint-sm: 576px;\n --bs-breakpoint-md: 768px;\n --bs-breakpoint-lg: 992px;\n --bs-breakpoint-xl: 1200px;\n --bs-breakpoint-xxl: 1400px;\n}\n\n.row {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(-1 * var(--bs-gutter-y));\n margin-right: calc(-0.5 * var(--bs-gutter-x));\n margin-left: calc(-0.5 * var(--bs-gutter-x));\n}\n.row > * {\n flex-shrink: 0;\n width: 100%;\n max-width: 100%;\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n margin-top: var(--bs-gutter-y);\n}\n\n.col {\n flex: 1 0 0;\n}\n\n.row-cols-auto > * {\n flex: 0 0 auto;\n width: auto;\n}\n\n.row-cols-1 > * {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 auto;\n width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n}\n\n.col-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n}\n\n.col-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-3 {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.col-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.col-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n}\n\n.col-6 {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.col-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n}\n\n.col-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n}\n\n.col-9 {\n flex: 0 0 auto;\n width: 75%;\n}\n\n.col-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n}\n\n.col-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n}\n\n.col-12 {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.offset-1 {\n margin-left: 8.33333333%;\n}\n\n.offset-2 {\n margin-left: 16.66666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.33333333%;\n}\n\n.offset-5 {\n margin-left: 41.66666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.33333333%;\n}\n\n.offset-8 {\n margin-left: 66.66666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.33333333%;\n}\n\n.offset-11 {\n margin-left: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex: 1 0 0;\n }\n .row-cols-sm-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-sm-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-sm-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-sm-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-sm-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-sm-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-sm-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-sm-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-sm-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-sm-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.33333333%;\n }\n .offset-sm-2 {\n margin-left: 16.66666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.33333333%;\n }\n .offset-sm-5 {\n margin-left: 41.66666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.33333333%;\n }\n .offset-sm-8 {\n margin-left: 66.66666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.33333333%;\n }\n .offset-sm-11 {\n margin-left: 91.66666667%;\n }\n .g-sm-0,\n .gx-sm-0 {\n --bs-gutter-x: 0;\n }\n .g-sm-0,\n .gy-sm-0 {\n --bs-gutter-y: 0;\n }\n .g-sm-1,\n .gx-sm-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-sm-1,\n .gy-sm-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-sm-2,\n .gx-sm-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-sm-2,\n .gy-sm-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-sm-3,\n .gx-sm-3 {\n --bs-gutter-x: 1rem;\n }\n .g-sm-3,\n .gy-sm-3 {\n --bs-gutter-y: 1rem;\n }\n .g-sm-4,\n .gx-sm-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-sm-4,\n .gy-sm-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-sm-5,\n .gx-sm-5 {\n --bs-gutter-x: 3rem;\n }\n .g-sm-5,\n .gy-sm-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 768px) {\n .col-md {\n flex: 1 0 0;\n }\n .row-cols-md-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-md-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-md-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-md-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-md-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-md-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-md-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-md-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-md-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-md-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-md-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-md-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-md-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-md-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.33333333%;\n }\n .offset-md-2 {\n margin-left: 16.66666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.33333333%;\n }\n .offset-md-5 {\n margin-left: 41.66666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.33333333%;\n }\n .offset-md-8 {\n margin-left: 66.66666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.33333333%;\n }\n .offset-md-11 {\n margin-left: 91.66666667%;\n }\n .g-md-0,\n .gx-md-0 {\n --bs-gutter-x: 0;\n }\n .g-md-0,\n .gy-md-0 {\n --bs-gutter-y: 0;\n }\n .g-md-1,\n .gx-md-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-md-1,\n .gy-md-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-md-2,\n .gx-md-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-md-2,\n .gy-md-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-md-3,\n .gx-md-3 {\n --bs-gutter-x: 1rem;\n }\n .g-md-3,\n .gy-md-3 {\n --bs-gutter-y: 1rem;\n }\n .g-md-4,\n .gx-md-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-md-4,\n .gy-md-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-md-5,\n .gx-md-5 {\n --bs-gutter-x: 3rem;\n }\n .g-md-5,\n .gy-md-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 992px) {\n .col-lg {\n flex: 1 0 0;\n }\n .row-cols-lg-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-lg-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-lg-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-lg-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-lg-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-lg-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-lg-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-lg-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-lg-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-lg-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.33333333%;\n }\n .offset-lg-2 {\n margin-left: 16.66666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.33333333%;\n }\n .offset-lg-5 {\n margin-left: 41.66666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.33333333%;\n }\n .offset-lg-8 {\n margin-left: 66.66666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.33333333%;\n }\n .offset-lg-11 {\n margin-left: 91.66666667%;\n }\n .g-lg-0,\n .gx-lg-0 {\n --bs-gutter-x: 0;\n }\n .g-lg-0,\n .gy-lg-0 {\n --bs-gutter-y: 0;\n }\n .g-lg-1,\n .gx-lg-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-lg-1,\n .gy-lg-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-lg-2,\n .gx-lg-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-lg-2,\n .gy-lg-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-lg-3,\n .gx-lg-3 {\n --bs-gutter-x: 1rem;\n }\n .g-lg-3,\n .gy-lg-3 {\n --bs-gutter-y: 1rem;\n }\n .g-lg-4,\n .gx-lg-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-lg-4,\n .gy-lg-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-lg-5,\n .gx-lg-5 {\n --bs-gutter-x: 3rem;\n }\n .g-lg-5,\n .gy-lg-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1200px) {\n .col-xl {\n flex: 1 0 0;\n }\n .row-cols-xl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-xl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-xl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-xl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-xl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-xl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-xl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-xl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-xl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.33333333%;\n }\n .offset-xl-2 {\n margin-left: 16.66666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.33333333%;\n }\n .offset-xl-5 {\n margin-left: 41.66666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.33333333%;\n }\n .offset-xl-8 {\n margin-left: 66.66666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.33333333%;\n }\n .offset-xl-11 {\n margin-left: 91.66666667%;\n }\n .g-xl-0,\n .gx-xl-0 {\n --bs-gutter-x: 0;\n }\n .g-xl-0,\n .gy-xl-0 {\n --bs-gutter-y: 0;\n }\n .g-xl-1,\n .gx-xl-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-xl-1,\n .gy-xl-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-xl-2,\n .gx-xl-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-xl-2,\n .gy-xl-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-xl-3,\n .gx-xl-3 {\n --bs-gutter-x: 1rem;\n }\n .g-xl-3,\n .gy-xl-3 {\n --bs-gutter-y: 1rem;\n }\n .g-xl-4,\n .gx-xl-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-xl-4,\n .gy-xl-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-xl-5,\n .gx-xl-5 {\n --bs-gutter-x: 3rem;\n }\n .g-xl-5,\n .gy-xl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl {\n flex: 1 0 0;\n }\n .row-cols-xxl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n .row-cols-xxl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n .row-cols-xxl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n .row-cols-xxl-3 > * {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .row-cols-xxl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n .row-cols-xxl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n .row-cols-xxl-6 > * {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xxl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n .col-xxl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n .col-xxl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n .col-xxl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n .col-xxl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n .col-xxl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n .col-xxl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n .col-xxl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n .col-xxl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n .col-xxl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n .col-xxl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n .col-xxl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n .col-xxl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n .offset-xxl-0 {\n margin-left: 0;\n }\n .offset-xxl-1 {\n margin-left: 8.33333333%;\n }\n .offset-xxl-2 {\n margin-left: 16.66666667%;\n }\n .offset-xxl-3 {\n margin-left: 25%;\n }\n .offset-xxl-4 {\n margin-left: 33.33333333%;\n }\n .offset-xxl-5 {\n margin-left: 41.66666667%;\n }\n .offset-xxl-6 {\n margin-left: 50%;\n }\n .offset-xxl-7 {\n margin-left: 58.33333333%;\n }\n .offset-xxl-8 {\n margin-left: 66.66666667%;\n }\n .offset-xxl-9 {\n margin-left: 75%;\n }\n .offset-xxl-10 {\n margin-left: 83.33333333%;\n }\n .offset-xxl-11 {\n margin-left: 91.66666667%;\n }\n .g-xxl-0,\n .gx-xxl-0 {\n --bs-gutter-x: 0;\n }\n .g-xxl-0,\n .gy-xxl-0 {\n --bs-gutter-y: 0;\n }\n .g-xxl-1,\n .gx-xxl-1 {\n --bs-gutter-x: 0.25rem;\n }\n .g-xxl-1,\n .gy-xxl-1 {\n --bs-gutter-y: 0.25rem;\n }\n .g-xxl-2,\n .gx-xxl-2 {\n --bs-gutter-x: 0.5rem;\n }\n .g-xxl-2,\n .gy-xxl-2 {\n --bs-gutter-y: 0.5rem;\n }\n .g-xxl-3,\n .gx-xxl-3 {\n --bs-gutter-x: 1rem;\n }\n .g-xxl-3,\n .gy-xxl-3 {\n --bs-gutter-y: 1rem;\n }\n .g-xxl-4,\n .gx-xxl-4 {\n --bs-gutter-x: 1.5rem;\n }\n .g-xxl-4,\n .gy-xxl-4 {\n --bs-gutter-y: 1.5rem;\n }\n .g-xxl-5,\n .gx-xxl-5 {\n --bs-gutter-x: 3rem;\n }\n .g-xxl-5,\n .gy-xxl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n.table {\n --bs-table-color-type: initial;\n --bs-table-bg-type: initial;\n --bs-table-color-state: initial;\n --bs-table-bg-state: initial;\n --bs-table-color: var(--bs-emphasis-color);\n --bs-table-bg: var(--bs-body-bg);\n --bs-table-border-color: var(--bs-border-color);\n --bs-table-accent-bg: transparent;\n --bs-table-striped-color: var(--bs-emphasis-color);\n --bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05);\n --bs-table-active-color: var(--bs-emphasis-color);\n --bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1);\n --bs-table-hover-color: var(--bs-emphasis-color);\n --bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075);\n width: 100%;\n margin-bottom: 1rem;\n vertical-align: top;\n border-color: var(--bs-table-border-color);\n}\n.table > :not(caption) > * > * {\n padding: 0.5rem 0.5rem;\n color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));\n background-color: var(--bs-table-bg);\n border-bottom-width: var(--bs-border-width);\n box-shadow: inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg)));\n}\n.table > tbody {\n vertical-align: inherit;\n}\n.table > thead {\n vertical-align: bottom;\n}\n\n.table-group-divider {\n border-top: calc(var(--bs-border-width) * 2) solid currentcolor;\n}\n\n.caption-top {\n caption-side: top;\n}\n\n.table-sm > :not(caption) > * > * {\n padding: 0.25rem 0.25rem;\n}\n\n.table-bordered > :not(caption) > * {\n border-width: var(--bs-border-width) 0;\n}\n.table-bordered > :not(caption) > * > * {\n border-width: 0 var(--bs-border-width);\n}\n\n.table-borderless > :not(caption) > * > * {\n border-bottom-width: 0;\n}\n.table-borderless > :not(:first-child) {\n border-top-width: 0;\n}\n\n.table-striped > tbody > tr:nth-of-type(odd) > * {\n --bs-table-color-type: var(--bs-table-striped-color);\n --bs-table-bg-type: var(--bs-table-striped-bg);\n}\n\n.table-striped-columns > :not(caption) > tr > :nth-child(even) {\n --bs-table-color-type: var(--bs-table-striped-color);\n --bs-table-bg-type: var(--bs-table-striped-bg);\n}\n\n.table-active {\n --bs-table-color-state: var(--bs-table-active-color);\n --bs-table-bg-state: var(--bs-table-active-bg);\n}\n\n.table-hover > tbody > tr:hover > * {\n --bs-table-color-state: var(--bs-table-hover-color);\n --bs-table-bg-state: var(--bs-table-hover-bg);\n}\n\n.table-primary {\n --bs-table-color: #000;\n --bs-table-bg: #cfe2ff;\n --bs-table-border-color: #a6b5cc;\n --bs-table-striped-bg: #c5d7f2;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #bacbe6;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #bfd1ec;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-secondary {\n --bs-table-color: #000;\n --bs-table-bg: #e2e3e5;\n --bs-table-border-color: #b5b6b7;\n --bs-table-striped-bg: #d7d8da;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #cbccce;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #d1d2d4;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-success {\n --bs-table-color: #000;\n --bs-table-bg: #d1e7dd;\n --bs-table-border-color: #a7b9b1;\n --bs-table-striped-bg: #c7dbd2;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #bcd0c7;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #c1d6cc;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-info {\n --bs-table-color: #000;\n --bs-table-bg: #cff4fc;\n --bs-table-border-color: #a6c3ca;\n --bs-table-striped-bg: #c5e8ef;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #badce3;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #bfe2e9;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-warning {\n --bs-table-color: #000;\n --bs-table-bg: #fff3cd;\n --bs-table-border-color: #ccc2a4;\n --bs-table-striped-bg: #f2e7c3;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #e6dbb9;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #ece1be;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-danger {\n --bs-table-color: #000;\n --bs-table-bg: #f8d7da;\n --bs-table-border-color: #c6acae;\n --bs-table-striped-bg: #eccccf;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #dfc2c4;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #e5c7ca;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-light {\n --bs-table-color: #000;\n --bs-table-bg: #f8f9fa;\n --bs-table-border-color: #c6c7c8;\n --bs-table-striped-bg: #ecedee;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #dfe0e1;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #e5e6e7;\n --bs-table-hover-color: #000;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-dark {\n --bs-table-color: #fff;\n --bs-table-bg: #212529;\n --bs-table-border-color: #4d5154;\n --bs-table-striped-bg: #2c3034;\n --bs-table-striped-color: #fff;\n --bs-table-active-bg: #373b3e;\n --bs-table-active-color: #fff;\n --bs-table-hover-bg: #323539;\n --bs-table-hover-color: #fff;\n color: var(--bs-table-color);\n border-color: var(--bs-table-border-color);\n}\n\n.table-responsive {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 767.98px) {\n .table-responsive-md {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 1399.98px) {\n .table-responsive-xxl {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n.form-label {\n margin-bottom: 0.5rem;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + var(--bs-border-width));\n padding-bottom: calc(0.375rem + var(--bs-border-width));\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + var(--bs-border-width));\n padding-bottom: calc(0.5rem + var(--bs-border-width));\n font-size: 1.25rem;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + var(--bs-border-width));\n padding-bottom: calc(0.25rem + var(--bs-border-width));\n font-size: 0.875rem;\n}\n\n.form-text {\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: var(--bs-secondary-color);\n}\n\n.form-control {\n display: block;\n width: 100%;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: var(--bs-body-color);\n appearance: none;\n background-color: var(--bs-body-bg);\n background-clip: padding-box;\n border: var(--bs-border-width) solid var(--bs-border-color);\n border-radius: var(--bs-border-radius);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n.form-control[type=file] {\n overflow: hidden;\n}\n.form-control[type=file]:not(:disabled):not([readonly]) {\n cursor: pointer;\n}\n.form-control:focus {\n color: var(--bs-body-color);\n background-color: var(--bs-body-bg);\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-control::-webkit-date-and-time-value {\n min-width: 85px;\n height: 1.5em;\n margin: 0;\n}\n.form-control::-webkit-datetime-edit {\n display: block;\n padding: 0;\n}\n.form-control::placeholder {\n color: var(--bs-secondary-color);\n opacity: 1;\n}\n.form-control:disabled {\n background-color: var(--bs-secondary-bg);\n opacity: 1;\n}\n.form-control::file-selector-button {\n padding: 0.375rem 0.75rem;\n margin: -0.375rem -0.75rem;\n margin-inline-end: 0.75rem;\n color: var(--bs-body-color);\n background-color: var(--bs-tertiary-bg);\n pointer-events: none;\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n border-inline-end-width: var(--bs-border-width);\n border-radius: 0;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control::file-selector-button {\n transition: none;\n }\n}\n.form-control:hover:not(:disabled):not([readonly])::file-selector-button {\n background-color: var(--bs-secondary-bg);\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding: 0.375rem 0;\n margin-bottom: 0;\n line-height: 1.5;\n color: var(--bs-body-color);\n background-color: transparent;\n border: solid transparent;\n border-width: var(--bs-border-width) 0;\n}\n.form-control-plaintext:focus {\n outline: 0;\n}\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: var(--bs-border-radius-sm);\n}\n.form-control-sm::file-selector-button {\n padding: 0.25rem 0.5rem;\n margin: -0.25rem -0.5rem;\n margin-inline-end: 0.5rem;\n}\n\n.form-control-lg {\n min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: var(--bs-border-radius-lg);\n}\n.form-control-lg::file-selector-button {\n padding: 0.5rem 1rem;\n margin: -0.5rem -1rem;\n margin-inline-end: 1rem;\n}\n\ntextarea.form-control {\n min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));\n}\ntextarea.form-control-sm {\n min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));\n}\ntextarea.form-control-lg {\n min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));\n}\n\n.form-control-color {\n width: 3rem;\n height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));\n padding: 0.375rem;\n}\n.form-control-color:not(:disabled):not([readonly]) {\n cursor: pointer;\n}\n.form-control-color::-moz-color-swatch {\n border: 0 !important;\n border-radius: var(--bs-border-radius);\n}\n.form-control-color::-webkit-color-swatch {\n border: 0 !important;\n border-radius: var(--bs-border-radius);\n}\n.form-control-color.form-control-sm {\n height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));\n}\n.form-control-color.form-control-lg {\n height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));\n}\n\n.form-select {\n --bs-form-select-bg-img: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e\");\n display: block;\n width: 100%;\n padding: 0.375rem 2.25rem 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: var(--bs-body-color);\n appearance: none;\n background-color: var(--bs-body-bg);\n background-image: var(--bs-form-select-bg-img), var(--bs-form-select-bg-icon, none);\n background-repeat: no-repeat;\n background-position: right 0.75rem center;\n background-size: 16px 12px;\n border: var(--bs-border-width) solid var(--bs-border-color);\n border-radius: var(--bs-border-radius);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-select {\n transition: none;\n }\n}\n.form-select:focus {\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-select[multiple], .form-select[size]:not([size=\"1\"]) {\n padding-right: 0.75rem;\n background-image: none;\n}\n.form-select:disabled {\n background-color: var(--bs-secondary-bg);\n}\n.form-select:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 var(--bs-body-color);\n}\n\n.form-select-sm {\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n border-radius: var(--bs-border-radius-sm);\n}\n\n.form-select-lg {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n border-radius: var(--bs-border-radius-lg);\n}\n\n[data-bs-theme=dark] .form-select {\n --bs-form-select-bg-img: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e\");\n}\n\n.form-check {\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5em;\n margin-bottom: 0.125rem;\n}\n.form-check .form-check-input {\n float: left;\n margin-left: -1.5em;\n}\n\n.form-check-reverse {\n padding-right: 1.5em;\n padding-left: 0;\n text-align: right;\n}\n.form-check-reverse .form-check-input {\n float: right;\n margin-right: -1.5em;\n margin-left: 0;\n}\n\n.form-check-input {\n --bs-form-check-bg: var(--bs-body-bg);\n flex-shrink: 0;\n width: 1em;\n height: 1em;\n margin-top: 0.25em;\n vertical-align: top;\n appearance: none;\n background-color: var(--bs-form-check-bg);\n background-image: var(--bs-form-check-bg-image);\n background-repeat: no-repeat;\n background-position: center;\n background-size: contain;\n border: var(--bs-border-width) solid var(--bs-border-color);\n print-color-adjust: exact;\n}\n.form-check-input[type=checkbox] {\n border-radius: 0.25em;\n}\n.form-check-input[type=radio] {\n border-radius: 50%;\n}\n.form-check-input:active {\n filter: brightness(90%);\n}\n.form-check-input:focus {\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-check-input:checked {\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.form-check-input:checked[type=checkbox] {\n --bs-form-check-bg-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e\");\n}\n.form-check-input:checked[type=radio] {\n --bs-form-check-bg-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e\");\n}\n.form-check-input[type=checkbox]:indeterminate {\n background-color: #0d6efd;\n border-color: #0d6efd;\n --bs-form-check-bg-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e\");\n}\n.form-check-input:disabled {\n pointer-events: none;\n filter: none;\n opacity: 0.5;\n}\n.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label {\n cursor: default;\n opacity: 0.5;\n}\n\n.form-switch {\n padding-left: 2.5em;\n}\n.form-switch .form-check-input {\n --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e\");\n width: 2em;\n margin-left: -2.5em;\n background-image: var(--bs-form-switch-bg);\n background-position: left center;\n border-radius: 2em;\n transition: background-position 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-switch .form-check-input {\n transition: none;\n }\n}\n.form-switch .form-check-input:focus {\n --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e\");\n}\n.form-switch .form-check-input:checked {\n background-position: right center;\n --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\");\n}\n.form-switch.form-check-reverse {\n padding-right: 2.5em;\n padding-left: 0;\n}\n.form-switch.form-check-reverse .form-check-input {\n margin-right: -2.5em;\n margin-left: 0;\n}\n\n.form-check-inline {\n display: inline-block;\n margin-right: 1rem;\n}\n\n.btn-check {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.btn-check[disabled] + .btn, .btn-check:disabled + .btn {\n pointer-events: none;\n filter: none;\n opacity: 0.65;\n}\n\n[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus) {\n --bs-form-switch-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e\");\n}\n\n.form-range {\n width: 100%;\n height: 1.5rem;\n padding: 0;\n appearance: none;\n background-color: transparent;\n}\n.form-range:focus {\n outline: 0;\n}\n.form-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-range::-moz-focus-outer {\n border: 0;\n}\n.form-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n appearance: none;\n background-color: #0d6efd;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-range::-webkit-slider-thumb {\n transition: none;\n }\n}\n.form-range::-webkit-slider-thumb:active {\n background-color: #b6d4fe;\n}\n.form-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: var(--bs-secondary-bg);\n border-color: transparent;\n border-radius: 1rem;\n}\n.form-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n appearance: none;\n background-color: #0d6efd;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-range::-moz-range-thumb {\n transition: none;\n }\n}\n.form-range::-moz-range-thumb:active {\n background-color: #b6d4fe;\n}\n.form-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: var(--bs-secondary-bg);\n border-color: transparent;\n border-radius: 1rem;\n}\n.form-range:disabled {\n pointer-events: none;\n}\n.form-range:disabled::-webkit-slider-thumb {\n background-color: var(--bs-secondary-color);\n}\n.form-range:disabled::-moz-range-thumb {\n background-color: var(--bs-secondary-color);\n}\n\n.form-floating {\n position: relative;\n}\n.form-floating > .form-control,\n.form-floating > .form-control-plaintext,\n.form-floating > .form-select {\n height: calc(3.5rem + calc(var(--bs-border-width) * 2));\n min-height: calc(3.5rem + calc(var(--bs-border-width) * 2));\n line-height: 1.25;\n}\n.form-floating > label {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 2;\n max-width: 100%;\n height: 100%;\n padding: 1rem 0.75rem;\n overflow: hidden;\n color: rgba(var(--bs-body-color-rgb), 0.65);\n text-align: start;\n text-overflow: ellipsis;\n white-space: nowrap;\n pointer-events: none;\n border: var(--bs-border-width) solid transparent;\n transform-origin: 0 0;\n transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-floating > label {\n transition: none;\n }\n}\n.form-floating > .form-control,\n.form-floating > .form-control-plaintext {\n padding: 1rem 0.75rem;\n}\n.form-floating > .form-control::placeholder,\n.form-floating > .form-control-plaintext::placeholder {\n color: transparent;\n}\n.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown),\n.form-floating > .form-control-plaintext:focus,\n.form-floating > .form-control-plaintext:not(:placeholder-shown) {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:-webkit-autofill,\n.form-floating > .form-control-plaintext:-webkit-autofill {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-select {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n padding-left: 0.75rem;\n}\n.form-floating > .form-control:focus ~ label,\n.form-floating > .form-control:not(:placeholder-shown) ~ label,\n.form-floating > .form-control-plaintext ~ label,\n.form-floating > .form-select ~ label {\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n.form-floating > .form-control:-webkit-autofill ~ label {\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n.form-floating > textarea:focus ~ label::after,\n.form-floating > textarea:not(:placeholder-shown) ~ label::after {\n position: absolute;\n inset: 1rem 0.375rem;\n z-index: -1;\n height: 1.5em;\n content: \"\";\n background-color: var(--bs-body-bg);\n border-radius: var(--bs-border-radius);\n}\n.form-floating > textarea:disabled ~ label::after {\n background-color: var(--bs-secondary-bg);\n}\n.form-floating > .form-control-plaintext ~ label {\n border-width: var(--bs-border-width) 0;\n}\n.form-floating > :disabled ~ label,\n.form-floating > .form-control:disabled ~ label {\n color: #6c757d;\n}\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n width: 100%;\n}\n.input-group > .form-control,\n.input-group > .form-select,\n.input-group > .form-floating {\n position: relative;\n flex: 1 1 auto;\n width: 1%;\n min-width: 0;\n}\n.input-group > .form-control:focus,\n.input-group > .form-select:focus,\n.input-group > .form-floating:focus-within {\n z-index: 5;\n}\n.input-group .btn {\n position: relative;\n z-index: 2;\n}\n.input-group .btn:focus {\n z-index: 5;\n}\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: var(--bs-body-color);\n text-align: center;\n white-space: nowrap;\n background-color: var(--bs-tertiary-bg);\n border: var(--bs-border-width) solid var(--bs-border-color);\n border-radius: var(--bs-border-radius);\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .form-select,\n.input-group-lg > .input-group-text,\n.input-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: var(--bs-border-radius-lg);\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .form-select,\n.input-group-sm > .input-group-text,\n.input-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: var(--bs-border-radius-sm);\n}\n\n.input-group-lg > .form-select,\n.input-group-sm > .form-select {\n padding-right: 3rem;\n}\n\n.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),\n.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3),\n.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control,\n.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),\n.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4),\n.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-control,\n.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-select {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {\n margin-left: calc(-1 * var(--bs-border-width));\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.input-group > .form-floating:not(:first-child) > .form-control,\n.input-group > .form-floating:not(:first-child) > .form-select {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: var(--bs-form-valid-color);\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: 0.1rem;\n font-size: 0.875rem;\n color: #fff;\n background-color: var(--bs-success);\n border-radius: var(--bs-border-radius);\n}\n\n.was-validated :valid ~ .valid-feedback,\n.was-validated :valid ~ .valid-tooltip,\n.is-valid ~ .valid-feedback,\n.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: var(--bs-form-valid-border-color);\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: var(--bs-form-valid-border-color);\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .form-select:valid, .form-select.is-valid {\n border-color: var(--bs-form-valid-border-color);\n}\n.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size=\"1\"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size=\"1\"] {\n --bs-form-select-bg-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e\");\n padding-right: 4.125rem;\n background-position: right 0.75rem center, center right 2.25rem;\n background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-select:valid:focus, .form-select.is-valid:focus {\n border-color: var(--bs-form-valid-border-color);\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);\n}\n\n.was-validated .form-control-color:valid, .form-control-color.is-valid {\n width: calc(3rem + calc(1.5em + 0.75rem));\n}\n\n.was-validated .form-check-input:valid, .form-check-input.is-valid {\n border-color: var(--bs-form-valid-border-color);\n}\n.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked {\n background-color: var(--bs-form-valid-color);\n}\n.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus {\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);\n}\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: var(--bs-form-valid-color);\n}\n\n.form-check-inline .form-check-input ~ .valid-feedback {\n margin-left: 0.5em;\n}\n\n.was-validated .input-group > .form-control:not(:focus):valid, .input-group > .form-control:not(:focus).is-valid,\n.was-validated .input-group > .form-select:not(:focus):valid,\n.input-group > .form-select:not(:focus).is-valid,\n.was-validated .input-group > .form-floating:not(:focus-within):valid,\n.input-group > .form-floating:not(:focus-within).is-valid {\n z-index: 3;\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: var(--bs-form-invalid-color);\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: 0.1rem;\n font-size: 0.875rem;\n color: #fff;\n background-color: var(--bs-danger);\n border-radius: var(--bs-border-radius);\n}\n\n.was-validated :invalid ~ .invalid-feedback,\n.was-validated :invalid ~ .invalid-tooltip,\n.is-invalid ~ .invalid-feedback,\n.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: var(--bs-form-invalid-border-color);\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: var(--bs-form-invalid-border-color);\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .form-select:invalid, .form-select.is-invalid {\n border-color: var(--bs-form-invalid-border-color);\n}\n.was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size=\"1\"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size=\"1\"] {\n --bs-form-select-bg-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n padding-right: 4.125rem;\n background-position: right 0.75rem center, center right 2.25rem;\n background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus {\n border-color: var(--bs-form-invalid-border-color);\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);\n}\n\n.was-validated .form-control-color:invalid, .form-control-color.is-invalid {\n width: calc(3rem + calc(1.5em + 0.75rem));\n}\n\n.was-validated .form-check-input:invalid, .form-check-input.is-invalid {\n border-color: var(--bs-form-invalid-border-color);\n}\n.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked {\n background-color: var(--bs-form-invalid-color);\n}\n.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus {\n box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25);\n}\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: var(--bs-form-invalid-color);\n}\n\n.form-check-inline .form-check-input ~ .invalid-feedback {\n margin-left: 0.5em;\n}\n\n.was-validated .input-group > .form-control:not(:focus):invalid, .input-group > .form-control:not(:focus).is-invalid,\n.was-validated .input-group > .form-select:not(:focus):invalid,\n.input-group > .form-select:not(:focus).is-invalid,\n.was-validated .input-group > .form-floating:not(:focus-within):invalid,\n.input-group > .form-floating:not(:focus-within).is-invalid {\n z-index: 4;\n}\n\n.btn {\n --bs-btn-padding-x: 0.75rem;\n --bs-btn-padding-y: 0.375rem;\n --bs-btn-font-family: ;\n --bs-btn-font-size: 1rem;\n --bs-btn-font-weight: 400;\n --bs-btn-line-height: 1.5;\n --bs-btn-color: var(--bs-body-color);\n --bs-btn-bg: transparent;\n --bs-btn-border-width: var(--bs-border-width);\n --bs-btn-border-color: transparent;\n --bs-btn-border-radius: var(--bs-border-radius);\n --bs-btn-hover-border-color: transparent;\n --bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n --bs-btn-disabled-opacity: 0.65;\n --bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);\n display: inline-block;\n padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);\n font-family: var(--bs-btn-font-family);\n font-size: var(--bs-btn-font-size);\n font-weight: var(--bs-btn-font-weight);\n line-height: var(--bs-btn-line-height);\n color: var(--bs-btn-color);\n text-align: center;\n text-decoration: none;\n vertical-align: middle;\n cursor: pointer;\n user-select: none;\n border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);\n border-radius: var(--bs-btn-border-radius);\n background-color: var(--bs-btn-bg);\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n.btn:hover {\n color: var(--bs-btn-hover-color);\n background-color: var(--bs-btn-hover-bg);\n border-color: var(--bs-btn-hover-border-color);\n}\n.btn-check + .btn:hover {\n color: var(--bs-btn-color);\n background-color: var(--bs-btn-bg);\n border-color: var(--bs-btn-border-color);\n}\n.btn:focus-visible {\n color: var(--bs-btn-hover-color);\n background-color: var(--bs-btn-hover-bg);\n border-color: var(--bs-btn-hover-border-color);\n outline: 0;\n box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn-check:focus-visible + .btn {\n border-color: var(--bs-btn-hover-border-color);\n outline: 0;\n box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn-check:checked + .btn, :not(.btn-check) + .btn:active, .btn:first-child:active, .btn.active, .btn.show {\n color: var(--bs-btn-active-color);\n background-color: var(--bs-btn-active-bg);\n border-color: var(--bs-btn-active-border-color);\n}\n.btn-check:checked + .btn:focus-visible, :not(.btn-check) + .btn:active:focus-visible, .btn:first-child:active:focus-visible, .btn.active:focus-visible, .btn.show:focus-visible {\n box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn-check:checked:focus-visible + .btn {\n box-shadow: var(--bs-btn-focus-box-shadow);\n}\n.btn:disabled, .btn.disabled, fieldset:disabled .btn {\n color: var(--bs-btn-disabled-color);\n pointer-events: none;\n background-color: var(--bs-btn-disabled-bg);\n border-color: var(--bs-btn-disabled-border-color);\n opacity: var(--bs-btn-disabled-opacity);\n}\n\n.btn-primary {\n --bs-btn-color: #fff;\n --bs-btn-bg: #0d6efd;\n --bs-btn-border-color: #0d6efd;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #0b5ed7;\n --bs-btn-hover-border-color: #0a58ca;\n --bs-btn-focus-shadow-rgb: 49, 132, 253;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #0a58ca;\n --bs-btn-active-border-color: #0a53be;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #0d6efd;\n --bs-btn-disabled-border-color: #0d6efd;\n}\n\n.btn-secondary {\n --bs-btn-color: #fff;\n --bs-btn-bg: #6c757d;\n --bs-btn-border-color: #6c757d;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #5c636a;\n --bs-btn-hover-border-color: #565e64;\n --bs-btn-focus-shadow-rgb: 130, 138, 145;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #565e64;\n --bs-btn-active-border-color: #51585e;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #6c757d;\n --bs-btn-disabled-border-color: #6c757d;\n}\n\n.btn-success {\n --bs-btn-color: #fff;\n --bs-btn-bg: #198754;\n --bs-btn-border-color: #198754;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #157347;\n --bs-btn-hover-border-color: #146c43;\n --bs-btn-focus-shadow-rgb: 60, 153, 110;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #146c43;\n --bs-btn-active-border-color: #13653f;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #198754;\n --bs-btn-disabled-border-color: #198754;\n}\n\n.btn-info {\n --bs-btn-color: #000;\n --bs-btn-bg: #0dcaf0;\n --bs-btn-border-color: #0dcaf0;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #31d2f2;\n --bs-btn-hover-border-color: #25cff2;\n --bs-btn-focus-shadow-rgb: 11, 172, 204;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #3dd5f3;\n --bs-btn-active-border-color: #25cff2;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #000;\n --bs-btn-disabled-bg: #0dcaf0;\n --bs-btn-disabled-border-color: #0dcaf0;\n}\n\n.btn-warning {\n --bs-btn-color: #000;\n --bs-btn-bg: #ffc107;\n --bs-btn-border-color: #ffc107;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #ffca2c;\n --bs-btn-hover-border-color: #ffc720;\n --bs-btn-focus-shadow-rgb: 217, 164, 6;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #ffcd39;\n --bs-btn-active-border-color: #ffc720;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #000;\n --bs-btn-disabled-bg: #ffc107;\n --bs-btn-disabled-border-color: #ffc107;\n}\n\n.btn-danger {\n --bs-btn-color: #fff;\n --bs-btn-bg: #dc3545;\n --bs-btn-border-color: #dc3545;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #bb2d3b;\n --bs-btn-hover-border-color: #b02a37;\n --bs-btn-focus-shadow-rgb: 225, 83, 97;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #b02a37;\n --bs-btn-active-border-color: #a52834;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #dc3545;\n --bs-btn-disabled-border-color: #dc3545;\n}\n\n.btn-light {\n --bs-btn-color: #000;\n --bs-btn-bg: #f8f9fa;\n --bs-btn-border-color: #f8f9fa;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #d3d4d5;\n --bs-btn-hover-border-color: #c6c7c8;\n --bs-btn-focus-shadow-rgb: 211, 212, 213;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #c6c7c8;\n --bs-btn-active-border-color: #babbbc;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #000;\n --bs-btn-disabled-bg: #f8f9fa;\n --bs-btn-disabled-border-color: #f8f9fa;\n}\n\n.btn-dark {\n --bs-btn-color: #fff;\n --bs-btn-bg: #212529;\n --bs-btn-border-color: #212529;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #424649;\n --bs-btn-hover-border-color: #373b3e;\n --bs-btn-focus-shadow-rgb: 66, 70, 73;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #4d5154;\n --bs-btn-active-border-color: #373b3e;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #fff;\n --bs-btn-disabled-bg: #212529;\n --bs-btn-disabled-border-color: #212529;\n}\n\n.btn-outline-primary {\n --bs-btn-color: #0d6efd;\n --bs-btn-border-color: #0d6efd;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #0d6efd;\n --bs-btn-hover-border-color: #0d6efd;\n --bs-btn-focus-shadow-rgb: 13, 110, 253;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #0d6efd;\n --bs-btn-active-border-color: #0d6efd;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #0d6efd;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #0d6efd;\n --bs-gradient: none;\n}\n\n.btn-outline-secondary {\n --bs-btn-color: #6c757d;\n --bs-btn-border-color: #6c757d;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #6c757d;\n --bs-btn-hover-border-color: #6c757d;\n --bs-btn-focus-shadow-rgb: 108, 117, 125;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #6c757d;\n --bs-btn-active-border-color: #6c757d;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #6c757d;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #6c757d;\n --bs-gradient: none;\n}\n\n.btn-outline-success {\n --bs-btn-color: #198754;\n --bs-btn-border-color: #198754;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #198754;\n --bs-btn-hover-border-color: #198754;\n --bs-btn-focus-shadow-rgb: 25, 135, 84;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #198754;\n --bs-btn-active-border-color: #198754;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #198754;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #198754;\n --bs-gradient: none;\n}\n\n.btn-outline-info {\n --bs-btn-color: #0dcaf0;\n --bs-btn-border-color: #0dcaf0;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #0dcaf0;\n --bs-btn-hover-border-color: #0dcaf0;\n --bs-btn-focus-shadow-rgb: 13, 202, 240;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #0dcaf0;\n --bs-btn-active-border-color: #0dcaf0;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #0dcaf0;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #0dcaf0;\n --bs-gradient: none;\n}\n\n.btn-outline-warning {\n --bs-btn-color: #ffc107;\n --bs-btn-border-color: #ffc107;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #ffc107;\n --bs-btn-hover-border-color: #ffc107;\n --bs-btn-focus-shadow-rgb: 255, 193, 7;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #ffc107;\n --bs-btn-active-border-color: #ffc107;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #ffc107;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #ffc107;\n --bs-gradient: none;\n}\n\n.btn-outline-danger {\n --bs-btn-color: #dc3545;\n --bs-btn-border-color: #dc3545;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #dc3545;\n --bs-btn-hover-border-color: #dc3545;\n --bs-btn-focus-shadow-rgb: 220, 53, 69;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #dc3545;\n --bs-btn-active-border-color: #dc3545;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #dc3545;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #dc3545;\n --bs-gradient: none;\n}\n\n.btn-outline-light {\n --bs-btn-color: #f8f9fa;\n --bs-btn-border-color: #f8f9fa;\n --bs-btn-hover-color: #000;\n --bs-btn-hover-bg: #f8f9fa;\n --bs-btn-hover-border-color: #f8f9fa;\n --bs-btn-focus-shadow-rgb: 248, 249, 250;\n --bs-btn-active-color: #000;\n --bs-btn-active-bg: #f8f9fa;\n --bs-btn-active-border-color: #f8f9fa;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #f8f9fa;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #f8f9fa;\n --bs-gradient: none;\n}\n\n.btn-outline-dark {\n --bs-btn-color: #212529;\n --bs-btn-border-color: #212529;\n --bs-btn-hover-color: #fff;\n --bs-btn-hover-bg: #212529;\n --bs-btn-hover-border-color: #212529;\n --bs-btn-focus-shadow-rgb: 33, 37, 41;\n --bs-btn-active-color: #fff;\n --bs-btn-active-bg: #212529;\n --bs-btn-active-border-color: #212529;\n --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n --bs-btn-disabled-color: #212529;\n --bs-btn-disabled-bg: transparent;\n --bs-btn-disabled-border-color: #212529;\n --bs-gradient: none;\n}\n\n.btn-link {\n --bs-btn-font-weight: 400;\n --bs-btn-color: var(--bs-link-color);\n --bs-btn-bg: transparent;\n --bs-btn-border-color: transparent;\n --bs-btn-hover-color: var(--bs-link-hover-color);\n --bs-btn-hover-border-color: transparent;\n --bs-btn-active-color: var(--bs-link-hover-color);\n --bs-btn-active-border-color: transparent;\n --bs-btn-disabled-color: #6c757d;\n --bs-btn-disabled-border-color: transparent;\n --bs-btn-box-shadow: 0 0 0 #000;\n --bs-btn-focus-shadow-rgb: 49, 132, 253;\n text-decoration: underline;\n}\n.btn-link:focus-visible {\n color: var(--bs-btn-color);\n}\n.btn-link:hover {\n color: var(--bs-btn-hover-color);\n}\n\n.btn-lg, .btn-group-lg > .btn {\n --bs-btn-padding-y: 0.5rem;\n --bs-btn-padding-x: 1rem;\n --bs-btn-font-size: 1.25rem;\n --bs-btn-border-radius: var(--bs-border-radius-lg);\n}\n\n.btn-sm, .btn-group-sm > .btn {\n --bs-btn-padding-y: 0.25rem;\n --bs-btn-padding-x: 0.5rem;\n --bs-btn-font-size: 0.875rem;\n --bs-btn-border-radius: var(--bs-border-radius-sm);\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n@media (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n.collapsing.collapse-horizontal {\n width: 0;\n height: auto;\n transition: width 0.35s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .collapsing.collapse-horizontal {\n transition: none;\n }\n}\n\n.dropup,\n.dropend,\n.dropdown,\n.dropstart,\n.dropup-center,\n.dropdown-center {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n}\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n --bs-dropdown-zindex: 1000;\n --bs-dropdown-min-width: 10rem;\n --bs-dropdown-padding-x: 0;\n --bs-dropdown-padding-y: 0.5rem;\n --bs-dropdown-spacer: 0.125rem;\n --bs-dropdown-font-size: 1rem;\n --bs-dropdown-color: var(--bs-body-color);\n --bs-dropdown-bg: var(--bs-body-bg);\n --bs-dropdown-border-color: var(--bs-border-color-translucent);\n --bs-dropdown-border-radius: var(--bs-border-radius);\n --bs-dropdown-border-width: var(--bs-border-width);\n --bs-dropdown-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width));\n --bs-dropdown-divider-bg: var(--bs-border-color-translucent);\n --bs-dropdown-divider-margin-y: 0.5rem;\n --bs-dropdown-box-shadow: var(--bs-box-shadow);\n --bs-dropdown-link-color: var(--bs-body-color);\n --bs-dropdown-link-hover-color: var(--bs-body-color);\n --bs-dropdown-link-hover-bg: var(--bs-tertiary-bg);\n --bs-dropdown-link-active-color: #fff;\n --bs-dropdown-link-active-bg: #0d6efd;\n --bs-dropdown-link-disabled-color: var(--bs-tertiary-color);\n --bs-dropdown-item-padding-x: 1rem;\n --bs-dropdown-item-padding-y: 0.25rem;\n --bs-dropdown-header-color: #6c757d;\n --bs-dropdown-header-padding-x: 1rem;\n --bs-dropdown-header-padding-y: 0.5rem;\n position: absolute;\n z-index: var(--bs-dropdown-zindex);\n display: none;\n min-width: var(--bs-dropdown-min-width);\n padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);\n margin: 0;\n font-size: var(--bs-dropdown-font-size);\n color: var(--bs-dropdown-color);\n text-align: left;\n list-style: none;\n background-color: var(--bs-dropdown-bg);\n background-clip: padding-box;\n border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);\n border-radius: var(--bs-dropdown-border-radius);\n}\n.dropdown-menu[data-bs-popper] {\n top: 100%;\n left: 0;\n margin-top: var(--bs-dropdown-spacer);\n}\n\n.dropdown-menu-start {\n --bs-position: start;\n}\n.dropdown-menu-start[data-bs-popper] {\n right: auto;\n left: 0;\n}\n\n.dropdown-menu-end {\n --bs-position: end;\n}\n.dropdown-menu-end[data-bs-popper] {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-start {\n --bs-position: start;\n }\n .dropdown-menu-sm-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-sm-end {\n --bs-position: end;\n }\n .dropdown-menu-sm-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 768px) {\n .dropdown-menu-md-start {\n --bs-position: start;\n }\n .dropdown-menu-md-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-md-end {\n --bs-position: end;\n }\n .dropdown-menu-md-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 992px) {\n .dropdown-menu-lg-start {\n --bs-position: start;\n }\n .dropdown-menu-lg-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-lg-end {\n --bs-position: end;\n }\n .dropdown-menu-lg-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 1200px) {\n .dropdown-menu-xl-start {\n --bs-position: start;\n }\n .dropdown-menu-xl-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-xl-end {\n --bs-position: end;\n }\n .dropdown-menu-xl-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 1400px) {\n .dropdown-menu-xxl-start {\n --bs-position: start;\n }\n .dropdown-menu-xxl-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n .dropdown-menu-xxl-end {\n --bs-position: end;\n }\n .dropdown-menu-xxl-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n.dropup .dropdown-menu[data-bs-popper] {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: var(--bs-dropdown-spacer);\n}\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropend .dropdown-menu[data-bs-popper] {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: var(--bs-dropdown-spacer);\n}\n.dropend .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n.dropend .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n.dropend .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropstart .dropdown-menu[data-bs-popper] {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: var(--bs-dropdown-spacer);\n}\n.dropstart .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n.dropstart .dropdown-toggle::after {\n display: none;\n}\n.dropstart .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n.dropstart .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n.dropstart .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-divider {\n height: 0;\n margin: var(--bs-dropdown-divider-margin-y) 0;\n overflow: hidden;\n border-top: 1px solid var(--bs-dropdown-divider-bg);\n opacity: 1;\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);\n clear: both;\n font-weight: 400;\n color: var(--bs-dropdown-link-color);\n text-align: inherit;\n text-decoration: none;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n border-radius: var(--bs-dropdown-item-border-radius, 0);\n}\n.dropdown-item:hover, .dropdown-item:focus {\n color: var(--bs-dropdown-link-hover-color);\n background-color: var(--bs-dropdown-link-hover-bg);\n}\n.dropdown-item.active, .dropdown-item:active {\n color: var(--bs-dropdown-link-active-color);\n text-decoration: none;\n background-color: var(--bs-dropdown-link-active-bg);\n}\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: var(--bs-dropdown-link-disabled-color);\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);\n margin-bottom: 0;\n font-size: 0.875rem;\n color: var(--bs-dropdown-header-color);\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);\n color: var(--bs-dropdown-link-color);\n}\n\n.dropdown-menu-dark {\n --bs-dropdown-color: #dee2e6;\n --bs-dropdown-bg: #343a40;\n --bs-dropdown-border-color: var(--bs-border-color-translucent);\n --bs-dropdown-box-shadow: ;\n --bs-dropdown-link-color: #dee2e6;\n --bs-dropdown-link-hover-color: #fff;\n --bs-dropdown-divider-bg: var(--bs-border-color-translucent);\n --bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15);\n --bs-dropdown-link-active-color: #fff;\n --bs-dropdown-link-active-bg: #0d6efd;\n --bs-dropdown-link-disabled-color: #adb5bd;\n --bs-dropdown-header-color: #adb5bd;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n flex: 1 1 auto;\n}\n.btn-group > .btn-check:checked + .btn,\n.btn-group > .btn-check:focus + .btn,\n.btn-group > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn-check:checked + .btn,\n.btn-group-vertical > .btn-check:focus + .btn,\n.btn-group-vertical > .btn:hover,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n}\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group {\n border-radius: var(--bs-border-radius);\n}\n.btn-group > :not(.btn-check:first-child) + .btn,\n.btn-group > .btn-group:not(:first-child) {\n margin-left: calc(-1 * var(--bs-border-width));\n}\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn.dropdown-toggle-split:first-child,\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn:nth-child(n+3),\n.btn-group > :not(.btn-check) + .btn,\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n.dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropend .dropdown-toggle-split::after {\n margin-left: 0;\n}\n.dropstart .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: calc(-1 * var(--bs-border-width));\n}\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:nth-child(n+3),\n.btn-group-vertical > :not(.btn-check) + .btn,\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav {\n --bs-nav-link-padding-x: 1rem;\n --bs-nav-link-padding-y: 0.5rem;\n --bs-nav-link-font-weight: ;\n --bs-nav-link-color: var(--bs-link-color);\n --bs-nav-link-hover-color: var(--bs-link-hover-color);\n --bs-nav-link-disabled-color: var(--bs-secondary-color);\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);\n font-size: var(--bs-nav-link-font-size);\n font-weight: var(--bs-nav-link-font-weight);\n color: var(--bs-nav-link-color);\n text-decoration: none;\n background: none;\n border: 0;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .nav-link {\n transition: none;\n }\n}\n.nav-link:hover, .nav-link:focus {\n color: var(--bs-nav-link-hover-color);\n}\n.nav-link:focus-visible {\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.nav-link.disabled, .nav-link:disabled {\n color: var(--bs-nav-link-disabled-color);\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n --bs-nav-tabs-border-width: var(--bs-border-width);\n --bs-nav-tabs-border-color: var(--bs-border-color);\n --bs-nav-tabs-border-radius: var(--bs-border-radius);\n --bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);\n --bs-nav-tabs-link-active-color: var(--bs-emphasis-color);\n --bs-nav-tabs-link-active-bg: var(--bs-body-bg);\n --bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);\n border-bottom: var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color);\n}\n.nav-tabs .nav-link {\n margin-bottom: calc(-1 * var(--bs-nav-tabs-border-width));\n border: var(--bs-nav-tabs-border-width) solid transparent;\n border-top-left-radius: var(--bs-nav-tabs-border-radius);\n border-top-right-radius: var(--bs-nav-tabs-border-radius);\n}\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n isolation: isolate;\n border-color: var(--bs-nav-tabs-link-hover-border-color);\n}\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: var(--bs-nav-tabs-link-active-color);\n background-color: var(--bs-nav-tabs-link-active-bg);\n border-color: var(--bs-nav-tabs-link-active-border-color);\n}\n.nav-tabs .dropdown-menu {\n margin-top: calc(-1 * var(--bs-nav-tabs-border-width));\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills {\n --bs-nav-pills-border-radius: var(--bs-border-radius);\n --bs-nav-pills-link-active-color: #fff;\n --bs-nav-pills-link-active-bg: #0d6efd;\n}\n.nav-pills .nav-link {\n border-radius: var(--bs-nav-pills-border-radius);\n}\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: var(--bs-nav-pills-link-active-color);\n background-color: var(--bs-nav-pills-link-active-bg);\n}\n\n.nav-underline {\n --bs-nav-underline-gap: 1rem;\n --bs-nav-underline-border-width: 0.125rem;\n --bs-nav-underline-link-active-color: var(--bs-emphasis-color);\n gap: var(--bs-nav-underline-gap);\n}\n.nav-underline .nav-link {\n padding-right: 0;\n padding-left: 0;\n border-bottom: var(--bs-nav-underline-border-width) solid transparent;\n}\n.nav-underline .nav-link:hover, .nav-underline .nav-link:focus {\n border-bottom-color: currentcolor;\n}\n.nav-underline .nav-link.active,\n.nav-underline .show > .nav-link {\n font-weight: 700;\n color: var(--bs-nav-underline-link-active-color);\n border-bottom-color: currentcolor;\n}\n\n.nav-fill > .nav-link,\n.nav-fill .nav-item {\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified > .nav-link,\n.nav-justified .nav-item {\n flex-grow: 1;\n flex-basis: 0;\n text-align: center;\n}\n\n.nav-fill .nav-item .nav-link,\n.nav-justified .nav-item .nav-link {\n width: 100%;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n --bs-navbar-padding-x: 0;\n --bs-navbar-padding-y: 0.5rem;\n --bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65);\n --bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), 0.8);\n --bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3);\n --bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1);\n --bs-navbar-brand-padding-y: 0.3125rem;\n --bs-navbar-brand-margin-end: 1rem;\n --bs-navbar-brand-font-size: 1.25rem;\n --bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1);\n --bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1);\n --bs-navbar-nav-link-padding-x: 0.5rem;\n --bs-navbar-toggler-padding-y: 0.25rem;\n --bs-navbar-toggler-padding-x: 0.75rem;\n --bs-navbar-toggler-font-size: 1.25rem;\n --bs-navbar-toggler-icon-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n --bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15);\n --bs-navbar-toggler-border-radius: var(--bs-border-radius);\n --bs-navbar-toggler-focus-width: 0.25rem;\n --bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x);\n}\n.navbar > .container,\n.navbar > .container-fluid,\n.navbar > .container-sm,\n.navbar > .container-md,\n.navbar > .container-lg,\n.navbar > .container-xl,\n.navbar > .container-xxl {\n display: flex;\n flex-wrap: inherit;\n align-items: center;\n justify-content: space-between;\n}\n.navbar-brand {\n padding-top: var(--bs-navbar-brand-padding-y);\n padding-bottom: var(--bs-navbar-brand-padding-y);\n margin-right: var(--bs-navbar-brand-margin-end);\n font-size: var(--bs-navbar-brand-font-size);\n color: var(--bs-navbar-brand-color);\n text-decoration: none;\n white-space: nowrap;\n}\n.navbar-brand:hover, .navbar-brand:focus {\n color: var(--bs-navbar-brand-hover-color);\n}\n\n.navbar-nav {\n --bs-nav-link-padding-x: 0;\n --bs-nav-link-padding-y: 0.5rem;\n --bs-nav-link-font-weight: ;\n --bs-nav-link-color: var(--bs-navbar-color);\n --bs-nav-link-hover-color: var(--bs-navbar-hover-color);\n --bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n.navbar-nav .nav-link.active, .navbar-nav .nav-link.show {\n color: var(--bs-navbar-active-color);\n}\n.navbar-nav .dropdown-menu {\n position: static;\n}\n\n.navbar-text {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n color: var(--bs-navbar-color);\n}\n.navbar-text a,\n.navbar-text a:hover,\n.navbar-text a:focus {\n color: var(--bs-navbar-active-color);\n}\n\n.navbar-collapse {\n flex-grow: 1;\n flex-basis: 100%;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);\n font-size: var(--bs-navbar-toggler-font-size);\n line-height: 1;\n color: var(--bs-navbar-color);\n background-color: transparent;\n border: var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);\n border-radius: var(--bs-navbar-toggler-border-radius);\n transition: var(--bs-navbar-toggler-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .navbar-toggler {\n transition: none;\n }\n}\n.navbar-toggler:hover {\n text-decoration: none;\n}\n.navbar-toggler:focus {\n text-decoration: none;\n outline: 0;\n box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width);\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n background-image: var(--bs-navbar-toggler-icon-bg);\n background-repeat: no-repeat;\n background-position: center;\n background-size: 100%;\n}\n\n.navbar-nav-scroll {\n max-height: var(--bs-scroll-height, 75vh);\n overflow-y: auto;\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-sm .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-sm .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n .navbar-expand-sm .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-sm .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-sm .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 768px) {\n .navbar-expand-md {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-md .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-md .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n .navbar-expand-md .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-md .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-md .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 992px) {\n .navbar-expand-lg {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-lg .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-lg .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n .navbar-expand-lg .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-lg .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-lg .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-xl .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-xl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n .navbar-expand-xl .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-xl .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-xl .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 1400px) {\n .navbar-expand-xxl {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xxl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xxl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xxl .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n }\n .navbar-expand-xxl .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-xxl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xxl .navbar-toggler {\n display: none;\n }\n .navbar-expand-xxl .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n }\n .navbar-expand-xxl .offcanvas .offcanvas-header {\n display: none;\n }\n .navbar-expand-xxl .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n.navbar-expand {\n flex-wrap: nowrap;\n justify-content: flex-start;\n}\n.navbar-expand .navbar-nav {\n flex-direction: row;\n}\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n.navbar-expand .navbar-nav .nav-link {\n padding-right: var(--bs-navbar-nav-link-padding-x);\n padding-left: var(--bs-navbar-nav-link-padding-x);\n}\n.navbar-expand .navbar-nav-scroll {\n overflow: visible;\n}\n.navbar-expand .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n}\n.navbar-expand .navbar-toggler {\n display: none;\n}\n.navbar-expand .offcanvas {\n position: static;\n z-index: auto;\n flex-grow: 1;\n width: auto !important;\n height: auto !important;\n visibility: visible !important;\n background-color: transparent !important;\n border: 0 !important;\n transform: none !important;\n transition: none;\n}\n.navbar-expand .offcanvas .offcanvas-header {\n display: none;\n}\n.navbar-expand .offcanvas .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n}\n\n.navbar-dark,\n.navbar[data-bs-theme=dark] {\n --bs-navbar-color: rgba(255, 255, 255, 0.55);\n --bs-navbar-hover-color: rgba(255, 255, 255, 0.75);\n --bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);\n --bs-navbar-active-color: #fff;\n --bs-navbar-brand-color: #fff;\n --bs-navbar-brand-hover-color: #fff;\n --bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);\n --bs-navbar-toggler-icon-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n[data-bs-theme=dark] .navbar-toggler-icon {\n --bs-navbar-toggler-icon-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.card {\n --bs-card-spacer-y: 1rem;\n --bs-card-spacer-x: 1rem;\n --bs-card-title-spacer-y: 0.5rem;\n --bs-card-title-color: ;\n --bs-card-subtitle-color: ;\n --bs-card-border-width: var(--bs-border-width);\n --bs-card-border-color: var(--bs-border-color-translucent);\n --bs-card-border-radius: var(--bs-border-radius);\n --bs-card-box-shadow: ;\n --bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));\n --bs-card-cap-padding-y: 0.5rem;\n --bs-card-cap-padding-x: 1rem;\n --bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03);\n --bs-card-cap-color: ;\n --bs-card-height: ;\n --bs-card-color: ;\n --bs-card-bg: var(--bs-body-bg);\n --bs-card-img-overlay-padding: 1rem;\n --bs-card-group-margin: 0.75rem;\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n height: var(--bs-card-height);\n color: var(--bs-body-color);\n word-wrap: break-word;\n background-color: var(--bs-card-bg);\n background-clip: border-box;\n border: var(--bs-card-border-width) solid var(--bs-card-border-color);\n border-radius: var(--bs-card-border-radius);\n}\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n.card > .list-group {\n border-top: inherit;\n border-bottom: inherit;\n}\n.card > .list-group:first-child {\n border-top-width: 0;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\n}\n.card > .list-group:last-child {\n border-bottom-width: 0;\n border-bottom-right-radius: var(--bs-card-inner-border-radius);\n border-bottom-left-radius: var(--bs-card-inner-border-radius);\n}\n.card > .card-header + .list-group,\n.card > .list-group + .card-footer {\n border-top: 0;\n}\n\n.card-body {\n flex: 1 1 auto;\n padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x);\n color: var(--bs-card-color);\n}\n\n.card-title {\n margin-bottom: var(--bs-card-title-spacer-y);\n color: var(--bs-card-title-color);\n}\n\n.card-subtitle {\n margin-top: calc(-0.5 * var(--bs-card-title-spacer-y));\n margin-bottom: 0;\n color: var(--bs-card-subtitle-color);\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link + .card-link {\n margin-left: var(--bs-card-spacer-x);\n}\n\n.card-header {\n padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);\n margin-bottom: 0;\n color: var(--bs-card-cap-color);\n background-color: var(--bs-card-cap-bg);\n border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color);\n}\n.card-header:first-child {\n border-radius: var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0;\n}\n\n.card-footer {\n padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);\n color: var(--bs-card-cap-color);\n background-color: var(--bs-card-cap-bg);\n border-top: var(--bs-card-border-width) solid var(--bs-card-border-color);\n}\n.card-footer:last-child {\n border-radius: 0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius);\n}\n\n.card-header-tabs {\n margin-right: calc(-0.5 * var(--bs-card-cap-padding-x));\n margin-bottom: calc(-1 * var(--bs-card-cap-padding-y));\n margin-left: calc(-0.5 * var(--bs-card-cap-padding-x));\n border-bottom: 0;\n}\n.card-header-tabs .nav-link.active {\n background-color: var(--bs-card-bg);\n border-bottom-color: var(--bs-card-bg);\n}\n\n.card-header-pills {\n margin-right: calc(-0.5 * var(--bs-card-cap-padding-x));\n margin-left: calc(-0.5 * var(--bs-card-cap-padding-x));\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: var(--bs-card-img-overlay-padding);\n border-radius: var(--bs-card-inner-border-radius);\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n width: 100%;\n}\n\n.card-img,\n.card-img-top {\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\n}\n\n.card-img,\n.card-img-bottom {\n border-bottom-right-radius: var(--bs-card-inner-border-radius);\n border-bottom-left-radius: var(--bs-card-inner-border-radius);\n}\n\n.card-group > .card {\n margin-bottom: var(--bs-card-group-margin);\n}\n@media (min-width: 576px) {\n .card-group {\n display: flex;\n flex-flow: row wrap;\n }\n .card-group > .card {\n flex: 1 0 0;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:last-child) > .card-img-top,\n .card-group > .card:not(:last-child) > .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:not(:last-child) > .card-img-bottom,\n .card-group > .card:not(:last-child) > .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:not(:first-child) > .card-img-top,\n .card-group > .card:not(:first-child) > .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:not(:first-child) > .card-img-bottom,\n .card-group > .card:not(:first-child) > .card-footer {\n border-bottom-left-radius: 0;\n }\n}\n\n.accordion {\n --bs-accordion-color: var(--bs-body-color);\n --bs-accordion-bg: var(--bs-body-bg);\n --bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;\n --bs-accordion-border-color: var(--bs-border-color);\n --bs-accordion-border-width: var(--bs-border-width);\n --bs-accordion-border-radius: var(--bs-border-radius);\n --bs-accordion-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));\n --bs-accordion-btn-padding-x: 1.25rem;\n --bs-accordion-btn-padding-y: 1rem;\n --bs-accordion-btn-color: var(--bs-body-color);\n --bs-accordion-btn-bg: var(--bs-accordion-bg);\n --bs-accordion-btn-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e\");\n --bs-accordion-btn-icon-width: 1.25rem;\n --bs-accordion-btn-icon-transform: rotate(-180deg);\n --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;\n --bs-accordion-btn-active-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e\");\n --bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n --bs-accordion-body-padding-x: 1.25rem;\n --bs-accordion-body-padding-y: 1rem;\n --bs-accordion-active-color: var(--bs-primary-text-emphasis);\n --bs-accordion-active-bg: var(--bs-primary-bg-subtle);\n}\n\n.accordion-button {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);\n font-size: 1rem;\n color: var(--bs-accordion-btn-color);\n text-align: left;\n background-color: var(--bs-accordion-btn-bg);\n border: 0;\n border-radius: 0;\n overflow-anchor: none;\n transition: var(--bs-accordion-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .accordion-button {\n transition: none;\n }\n}\n.accordion-button:not(.collapsed) {\n color: var(--bs-accordion-active-color);\n background-color: var(--bs-accordion-active-bg);\n box-shadow: inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color);\n}\n.accordion-button:not(.collapsed)::after {\n background-image: var(--bs-accordion-btn-active-icon);\n transform: var(--bs-accordion-btn-icon-transform);\n}\n.accordion-button::after {\n flex-shrink: 0;\n width: var(--bs-accordion-btn-icon-width);\n height: var(--bs-accordion-btn-icon-width);\n margin-left: auto;\n content: \"\";\n background-image: var(--bs-accordion-btn-icon);\n background-repeat: no-repeat;\n background-size: var(--bs-accordion-btn-icon-width);\n transition: var(--bs-accordion-btn-icon-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .accordion-button::after {\n transition: none;\n }\n}\n.accordion-button:hover {\n z-index: 2;\n}\n.accordion-button:focus {\n z-index: 3;\n outline: 0;\n box-shadow: var(--bs-accordion-btn-focus-box-shadow);\n}\n\n.accordion-header {\n margin-bottom: 0;\n}\n\n.accordion-item {\n color: var(--bs-accordion-color);\n background-color: var(--bs-accordion-bg);\n border: var(--bs-accordion-border-width) solid var(--bs-accordion-border-color);\n}\n.accordion-item:first-of-type {\n border-top-left-radius: var(--bs-accordion-border-radius);\n border-top-right-radius: var(--bs-accordion-border-radius);\n}\n.accordion-item:first-of-type > .accordion-header .accordion-button {\n border-top-left-radius: var(--bs-accordion-inner-border-radius);\n border-top-right-radius: var(--bs-accordion-inner-border-radius);\n}\n.accordion-item:not(:first-of-type) {\n border-top: 0;\n}\n.accordion-item:last-of-type {\n border-bottom-right-radius: var(--bs-accordion-border-radius);\n border-bottom-left-radius: var(--bs-accordion-border-radius);\n}\n.accordion-item:last-of-type > .accordion-header .accordion-button.collapsed {\n border-bottom-right-radius: var(--bs-accordion-inner-border-radius);\n border-bottom-left-radius: var(--bs-accordion-inner-border-radius);\n}\n.accordion-item:last-of-type > .accordion-collapse {\n border-bottom-right-radius: var(--bs-accordion-border-radius);\n border-bottom-left-radius: var(--bs-accordion-border-radius);\n}\n\n.accordion-body {\n padding: var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x);\n}\n\n.accordion-flush > .accordion-item {\n border-right: 0;\n border-left: 0;\n border-radius: 0;\n}\n.accordion-flush > .accordion-item:first-child {\n border-top: 0;\n}\n.accordion-flush > .accordion-item:last-child {\n border-bottom: 0;\n}\n.accordion-flush > .accordion-item > .accordion-collapse,\n.accordion-flush > .accordion-item > .accordion-header .accordion-button,\n.accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed {\n border-radius: 0;\n}\n\n[data-bs-theme=dark] .accordion-button::after {\n --bs-accordion-btn-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e\");\n --bs-accordion-btn-active-icon: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e\");\n}\n\n.breadcrumb {\n --bs-breadcrumb-padding-x: 0;\n --bs-breadcrumb-padding-y: 0;\n --bs-breadcrumb-margin-bottom: 1rem;\n --bs-breadcrumb-bg: ;\n --bs-breadcrumb-border-radius: ;\n --bs-breadcrumb-divider-color: var(--bs-secondary-color);\n --bs-breadcrumb-item-padding-x: 0.5rem;\n --bs-breadcrumb-item-active-color: var(--bs-secondary-color);\n display: flex;\n flex-wrap: wrap;\n padding: var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);\n margin-bottom: var(--bs-breadcrumb-margin-bottom);\n font-size: var(--bs-breadcrumb-font-size);\n list-style: none;\n background-color: var(--bs-breadcrumb-bg);\n border-radius: var(--bs-breadcrumb-border-radius);\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: var(--bs-breadcrumb-item-padding-x);\n}\n.breadcrumb-item + .breadcrumb-item::before {\n float: left;\n padding-right: var(--bs-breadcrumb-item-padding-x);\n color: var(--bs-breadcrumb-divider-color);\n content: var(--bs-breadcrumb-divider, \"/\") /* rtl: var(--bs-breadcrumb-divider, \"/\") */;\n}\n.breadcrumb-item.active {\n color: var(--bs-breadcrumb-item-active-color);\n}\n\n.pagination {\n --bs-pagination-padding-x: 0.75rem;\n --bs-pagination-padding-y: 0.375rem;\n --bs-pagination-font-size: 1rem;\n --bs-pagination-color: var(--bs-link-color);\n --bs-pagination-bg: var(--bs-body-bg);\n --bs-pagination-border-width: var(--bs-border-width);\n --bs-pagination-border-color: var(--bs-border-color);\n --bs-pagination-border-radius: var(--bs-border-radius);\n --bs-pagination-hover-color: var(--bs-link-hover-color);\n --bs-pagination-hover-bg: var(--bs-tertiary-bg);\n --bs-pagination-hover-border-color: var(--bs-border-color);\n --bs-pagination-focus-color: var(--bs-link-hover-color);\n --bs-pagination-focus-bg: var(--bs-secondary-bg);\n --bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n --bs-pagination-active-color: #fff;\n --bs-pagination-active-bg: #0d6efd;\n --bs-pagination-active-border-color: #0d6efd;\n --bs-pagination-disabled-color: var(--bs-secondary-color);\n --bs-pagination-disabled-bg: var(--bs-secondary-bg);\n --bs-pagination-disabled-border-color: var(--bs-border-color);\n display: flex;\n padding-left: 0;\n list-style: none;\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);\n font-size: var(--bs-pagination-font-size);\n color: var(--bs-pagination-color);\n text-decoration: none;\n background-color: var(--bs-pagination-bg);\n border: var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .page-link {\n transition: none;\n }\n}\n.page-link:hover {\n z-index: 2;\n color: var(--bs-pagination-hover-color);\n background-color: var(--bs-pagination-hover-bg);\n border-color: var(--bs-pagination-hover-border-color);\n}\n.page-link:focus {\n z-index: 3;\n color: var(--bs-pagination-focus-color);\n background-color: var(--bs-pagination-focus-bg);\n outline: 0;\n box-shadow: var(--bs-pagination-focus-box-shadow);\n}\n.page-link.active, .active > .page-link {\n z-index: 3;\n color: var(--bs-pagination-active-color);\n background-color: var(--bs-pagination-active-bg);\n border-color: var(--bs-pagination-active-border-color);\n}\n.page-link.disabled, .disabled > .page-link {\n color: var(--bs-pagination-disabled-color);\n pointer-events: none;\n background-color: var(--bs-pagination-disabled-bg);\n border-color: var(--bs-pagination-disabled-border-color);\n}\n\n.page-item:not(:first-child) .page-link {\n margin-left: calc(-1 * var(--bs-border-width));\n}\n.page-item:first-child .page-link {\n border-top-left-radius: var(--bs-pagination-border-radius);\n border-bottom-left-radius: var(--bs-pagination-border-radius);\n}\n.page-item:last-child .page-link {\n border-top-right-radius: var(--bs-pagination-border-radius);\n border-bottom-right-radius: var(--bs-pagination-border-radius);\n}\n\n.pagination-lg {\n --bs-pagination-padding-x: 1.5rem;\n --bs-pagination-padding-y: 0.75rem;\n --bs-pagination-font-size: 1.25rem;\n --bs-pagination-border-radius: var(--bs-border-radius-lg);\n}\n\n.pagination-sm {\n --bs-pagination-padding-x: 0.5rem;\n --bs-pagination-padding-y: 0.25rem;\n --bs-pagination-font-size: 0.875rem;\n --bs-pagination-border-radius: var(--bs-border-radius-sm);\n}\n\n.badge {\n --bs-badge-padding-x: 0.65em;\n --bs-badge-padding-y: 0.35em;\n --bs-badge-font-size: 0.75em;\n --bs-badge-font-weight: 700;\n --bs-badge-color: #fff;\n --bs-badge-border-radius: var(--bs-border-radius);\n display: inline-block;\n padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x);\n font-size: var(--bs-badge-font-size);\n font-weight: var(--bs-badge-font-weight);\n line-height: 1;\n color: var(--bs-badge-color);\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: var(--bs-badge-border-radius);\n}\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.alert {\n --bs-alert-bg: transparent;\n --bs-alert-padding-x: 1rem;\n --bs-alert-padding-y: 1rem;\n --bs-alert-margin-bottom: 1rem;\n --bs-alert-color: inherit;\n --bs-alert-border-color: transparent;\n --bs-alert-border: var(--bs-border-width) solid var(--bs-alert-border-color);\n --bs-alert-border-radius: var(--bs-border-radius);\n --bs-alert-link-color: inherit;\n position: relative;\n padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x);\n margin-bottom: var(--bs-alert-margin-bottom);\n color: var(--bs-alert-color);\n background-color: var(--bs-alert-bg);\n border: var(--bs-alert-border);\n border-radius: var(--bs-alert-border-radius);\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n color: var(--bs-alert-link-color);\n}\n\n.alert-dismissible {\n padding-right: 3rem;\n}\n.alert-dismissible .btn-close {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n padding: 1.25rem 1rem;\n}\n\n.alert-primary {\n --bs-alert-color: var(--bs-primary-text-emphasis);\n --bs-alert-bg: var(--bs-primary-bg-subtle);\n --bs-alert-border-color: var(--bs-primary-border-subtle);\n --bs-alert-link-color: var(--bs-primary-text-emphasis);\n}\n\n.alert-secondary {\n --bs-alert-color: var(--bs-secondary-text-emphasis);\n --bs-alert-bg: var(--bs-secondary-bg-subtle);\n --bs-alert-border-color: var(--bs-secondary-border-subtle);\n --bs-alert-link-color: var(--bs-secondary-text-emphasis);\n}\n\n.alert-success {\n --bs-alert-color: var(--bs-success-text-emphasis);\n --bs-alert-bg: var(--bs-success-bg-subtle);\n --bs-alert-border-color: var(--bs-success-border-subtle);\n --bs-alert-link-color: var(--bs-success-text-emphasis);\n}\n\n.alert-info {\n --bs-alert-color: var(--bs-info-text-emphasis);\n --bs-alert-bg: var(--bs-info-bg-subtle);\n --bs-alert-border-color: var(--bs-info-border-subtle);\n --bs-alert-link-color: var(--bs-info-text-emphasis);\n}\n\n.alert-warning {\n --bs-alert-color: var(--bs-warning-text-emphasis);\n --bs-alert-bg: var(--bs-warning-bg-subtle);\n --bs-alert-border-color: var(--bs-warning-border-subtle);\n --bs-alert-link-color: var(--bs-warning-text-emphasis);\n}\n\n.alert-danger {\n --bs-alert-color: var(--bs-danger-text-emphasis);\n --bs-alert-bg: var(--bs-danger-bg-subtle);\n --bs-alert-border-color: var(--bs-danger-border-subtle);\n --bs-alert-link-color: var(--bs-danger-text-emphasis);\n}\n\n.alert-light {\n --bs-alert-color: var(--bs-light-text-emphasis);\n --bs-alert-bg: var(--bs-light-bg-subtle);\n --bs-alert-border-color: var(--bs-light-border-subtle);\n --bs-alert-link-color: var(--bs-light-text-emphasis);\n}\n\n.alert-dark {\n --bs-alert-color: var(--bs-dark-text-emphasis);\n --bs-alert-bg: var(--bs-dark-bg-subtle);\n --bs-alert-border-color: var(--bs-dark-border-subtle);\n --bs-alert-link-color: var(--bs-dark-text-emphasis);\n}\n\n@keyframes progress-bar-stripes {\n 0% {\n background-position-x: var(--bs-progress-height);\n }\n}\n.progress,\n.progress-stacked {\n --bs-progress-height: 1rem;\n --bs-progress-font-size: 0.75rem;\n --bs-progress-bg: var(--bs-secondary-bg);\n --bs-progress-border-radius: var(--bs-border-radius);\n --bs-progress-box-shadow: var(--bs-box-shadow-inset);\n --bs-progress-bar-color: #fff;\n --bs-progress-bar-bg: #0d6efd;\n --bs-progress-bar-transition: width 0.6s ease;\n display: flex;\n height: var(--bs-progress-height);\n overflow: hidden;\n font-size: var(--bs-progress-font-size);\n background-color: var(--bs-progress-bg);\n border-radius: var(--bs-progress-border-radius);\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n overflow: hidden;\n color: var(--bs-progress-bar-color);\n text-align: center;\n white-space: nowrap;\n background-color: var(--bs-progress-bar-bg);\n transition: var(--bs-progress-bar-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: var(--bs-progress-height) var(--bs-progress-height);\n}\n\n.progress-stacked > .progress {\n overflow: visible;\n}\n\n.progress-stacked > .progress > .progress-bar {\n width: 100%;\n}\n\n.progress-bar-animated {\n animation: 1s linear infinite progress-bar-stripes;\n}\n@media (prefers-reduced-motion: reduce) {\n .progress-bar-animated {\n animation: none;\n }\n}\n\n.list-group {\n --bs-list-group-color: var(--bs-body-color);\n --bs-list-group-bg: var(--bs-body-bg);\n --bs-list-group-border-color: var(--bs-border-color);\n --bs-list-group-border-width: var(--bs-border-width);\n --bs-list-group-border-radius: var(--bs-border-radius);\n --bs-list-group-item-padding-x: 1rem;\n --bs-list-group-item-padding-y: 0.5rem;\n --bs-list-group-action-color: var(--bs-secondary-color);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-tertiary-bg);\n --bs-list-group-action-active-color: var(--bs-body-color);\n --bs-list-group-action-active-bg: var(--bs-secondary-bg);\n --bs-list-group-disabled-color: var(--bs-secondary-color);\n --bs-list-group-disabled-bg: var(--bs-body-bg);\n --bs-list-group-active-color: #fff;\n --bs-list-group-active-bg: #0d6efd;\n --bs-list-group-active-border-color: #0d6efd;\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n border-radius: var(--bs-list-group-border-radius);\n}\n\n.list-group-numbered {\n list-style-type: none;\n counter-reset: section;\n}\n.list-group-numbered > .list-group-item::before {\n content: counters(section, \".\") \". \";\n counter-increment: section;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);\n color: var(--bs-list-group-color);\n text-decoration: none;\n background-color: var(--bs-list-group-bg);\n border: var(--bs-list-group-border-width) solid var(--bs-list-group-border-color);\n}\n.list-group-item:first-child {\n border-top-left-radius: inherit;\n border-top-right-radius: inherit;\n}\n.list-group-item:last-child {\n border-bottom-right-radius: inherit;\n border-bottom-left-radius: inherit;\n}\n.list-group-item.disabled, .list-group-item:disabled {\n color: var(--bs-list-group-disabled-color);\n pointer-events: none;\n background-color: var(--bs-list-group-disabled-bg);\n}\n.list-group-item.active {\n z-index: 2;\n color: var(--bs-list-group-active-color);\n background-color: var(--bs-list-group-active-bg);\n border-color: var(--bs-list-group-active-border-color);\n}\n.list-group-item + .list-group-item {\n border-top-width: 0;\n}\n.list-group-item + .list-group-item.active {\n margin-top: calc(-1 * var(--bs-list-group-border-width));\n border-top-width: var(--bs-list-group-border-width);\n}\n\n.list-group-item-action {\n width: 100%;\n color: var(--bs-list-group-action-color);\n text-align: inherit;\n}\n.list-group-item-action:not(.active):hover, .list-group-item-action:not(.active):focus {\n z-index: 1;\n color: var(--bs-list-group-action-hover-color);\n text-decoration: none;\n background-color: var(--bs-list-group-action-hover-bg);\n}\n.list-group-item-action:not(.active):active {\n color: var(--bs-list-group-action-active-color);\n background-color: var(--bs-list-group-action-active-bg);\n}\n\n.list-group-horizontal {\n flex-direction: row;\n}\n.list-group-horizontal > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n}\n.list-group-horizontal > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n}\n.list-group-horizontal > .list-group-item.active {\n margin-top: 0;\n}\n.list-group-horizontal > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n}\n.list-group-horizontal > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n}\n\n@media (min-width: 576px) {\n .list-group-horizontal-sm {\n flex-direction: row;\n }\n .list-group-horizontal-sm > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n@media (min-width: 768px) {\n .list-group-horizontal-md {\n flex-direction: row;\n }\n .list-group-horizontal-md > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n@media (min-width: 992px) {\n .list-group-horizontal-lg {\n flex-direction: row;\n }\n .list-group-horizontal-lg > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n@media (min-width: 1200px) {\n .list-group-horizontal-xl {\n flex-direction: row;\n }\n .list-group-horizontal-xl > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n@media (min-width: 1400px) {\n .list-group-horizontal-xxl {\n flex-direction: row;\n }\n .list-group-horizontal-xxl > .list-group-item:first-child:not(:last-child) {\n border-bottom-left-radius: var(--bs-list-group-border-radius);\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xxl > .list-group-item:last-child:not(:first-child) {\n border-top-right-radius: var(--bs-list-group-border-radius);\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xxl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xxl > .list-group-item + .list-group-item {\n border-top-width: var(--bs-list-group-border-width);\n border-left-width: 0;\n }\n .list-group-horizontal-xxl > .list-group-item + .list-group-item.active {\n margin-left: calc(-1 * var(--bs-list-group-border-width));\n border-left-width: var(--bs-list-group-border-width);\n }\n}\n.list-group-flush {\n border-radius: 0;\n}\n.list-group-flush > .list-group-item {\n border-width: 0 0 var(--bs-list-group-border-width);\n}\n.list-group-flush > .list-group-item:last-child {\n border-bottom-width: 0;\n}\n\n.list-group-item-primary {\n --bs-list-group-color: var(--bs-primary-text-emphasis);\n --bs-list-group-bg: var(--bs-primary-bg-subtle);\n --bs-list-group-border-color: var(--bs-primary-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-primary-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-primary-border-subtle);\n --bs-list-group-active-color: var(--bs-primary-bg-subtle);\n --bs-list-group-active-bg: var(--bs-primary-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-primary-text-emphasis);\n}\n\n.list-group-item-secondary {\n --bs-list-group-color: var(--bs-secondary-text-emphasis);\n --bs-list-group-bg: var(--bs-secondary-bg-subtle);\n --bs-list-group-border-color: var(--bs-secondary-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-secondary-border-subtle);\n --bs-list-group-active-color: var(--bs-secondary-bg-subtle);\n --bs-list-group-active-bg: var(--bs-secondary-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-secondary-text-emphasis);\n}\n\n.list-group-item-success {\n --bs-list-group-color: var(--bs-success-text-emphasis);\n --bs-list-group-bg: var(--bs-success-bg-subtle);\n --bs-list-group-border-color: var(--bs-success-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-success-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-success-border-subtle);\n --bs-list-group-active-color: var(--bs-success-bg-subtle);\n --bs-list-group-active-bg: var(--bs-success-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-success-text-emphasis);\n}\n\n.list-group-item-info {\n --bs-list-group-color: var(--bs-info-text-emphasis);\n --bs-list-group-bg: var(--bs-info-bg-subtle);\n --bs-list-group-border-color: var(--bs-info-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-info-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-info-border-subtle);\n --bs-list-group-active-color: var(--bs-info-bg-subtle);\n --bs-list-group-active-bg: var(--bs-info-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-info-text-emphasis);\n}\n\n.list-group-item-warning {\n --bs-list-group-color: var(--bs-warning-text-emphasis);\n --bs-list-group-bg: var(--bs-warning-bg-subtle);\n --bs-list-group-border-color: var(--bs-warning-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-warning-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-warning-border-subtle);\n --bs-list-group-active-color: var(--bs-warning-bg-subtle);\n --bs-list-group-active-bg: var(--bs-warning-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-warning-text-emphasis);\n}\n\n.list-group-item-danger {\n --bs-list-group-color: var(--bs-danger-text-emphasis);\n --bs-list-group-bg: var(--bs-danger-bg-subtle);\n --bs-list-group-border-color: var(--bs-danger-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-danger-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-danger-border-subtle);\n --bs-list-group-active-color: var(--bs-danger-bg-subtle);\n --bs-list-group-active-bg: var(--bs-danger-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-danger-text-emphasis);\n}\n\n.list-group-item-light {\n --bs-list-group-color: var(--bs-light-text-emphasis);\n --bs-list-group-bg: var(--bs-light-bg-subtle);\n --bs-list-group-border-color: var(--bs-light-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-light-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-light-border-subtle);\n --bs-list-group-active-color: var(--bs-light-bg-subtle);\n --bs-list-group-active-bg: var(--bs-light-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-light-text-emphasis);\n}\n\n.list-group-item-dark {\n --bs-list-group-color: var(--bs-dark-text-emphasis);\n --bs-list-group-bg: var(--bs-dark-bg-subtle);\n --bs-list-group-border-color: var(--bs-dark-border-subtle);\n --bs-list-group-action-hover-color: var(--bs-emphasis-color);\n --bs-list-group-action-hover-bg: var(--bs-dark-border-subtle);\n --bs-list-group-action-active-color: var(--bs-emphasis-color);\n --bs-list-group-action-active-bg: var(--bs-dark-border-subtle);\n --bs-list-group-active-color: var(--bs-dark-bg-subtle);\n --bs-list-group-active-bg: var(--bs-dark-text-emphasis);\n --bs-list-group-active-border-color: var(--bs-dark-text-emphasis);\n}\n\n.btn-close {\n --bs-btn-close-color: #000;\n --bs-btn-close-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e\");\n --bs-btn-close-opacity: 0.5;\n --bs-btn-close-hover-opacity: 0.75;\n --bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n --bs-btn-close-focus-opacity: 1;\n --bs-btn-close-disabled-opacity: 0.25;\n box-sizing: content-box;\n width: 1em;\n height: 1em;\n padding: 0.25em 0.25em;\n color: var(--bs-btn-close-color);\n background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat;\n filter: var(--bs-btn-close-filter);\n border: 0;\n border-radius: 0.375rem;\n opacity: var(--bs-btn-close-opacity);\n}\n.btn-close:hover {\n color: var(--bs-btn-close-color);\n text-decoration: none;\n opacity: var(--bs-btn-close-hover-opacity);\n}\n.btn-close:focus {\n outline: 0;\n box-shadow: var(--bs-btn-close-focus-shadow);\n opacity: var(--bs-btn-close-focus-opacity);\n}\n.btn-close:disabled, .btn-close.disabled {\n pointer-events: none;\n user-select: none;\n opacity: var(--bs-btn-close-disabled-opacity);\n}\n\n.btn-close-white {\n --bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%);\n}\n\n:root,\n[data-bs-theme=light] {\n --bs-btn-close-filter: ;\n}\n\n[data-bs-theme=dark] {\n --bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%);\n}\n\n.toast {\n --bs-toast-zindex: 1090;\n --bs-toast-padding-x: 0.75rem;\n --bs-toast-padding-y: 0.5rem;\n --bs-toast-spacing: 1.5rem;\n --bs-toast-max-width: 350px;\n --bs-toast-font-size: 0.875rem;\n --bs-toast-color: ;\n --bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85);\n --bs-toast-border-width: var(--bs-border-width);\n --bs-toast-border-color: var(--bs-border-color-translucent);\n --bs-toast-border-radius: var(--bs-border-radius);\n --bs-toast-box-shadow: var(--bs-box-shadow);\n --bs-toast-header-color: var(--bs-secondary-color);\n --bs-toast-header-bg: rgba(var(--bs-body-bg-rgb), 0.85);\n --bs-toast-header-border-color: var(--bs-border-color-translucent);\n width: var(--bs-toast-max-width);\n max-width: 100%;\n font-size: var(--bs-toast-font-size);\n color: var(--bs-toast-color);\n pointer-events: auto;\n background-color: var(--bs-toast-bg);\n background-clip: padding-box;\n border: var(--bs-toast-border-width) solid var(--bs-toast-border-color);\n box-shadow: var(--bs-toast-box-shadow);\n border-radius: var(--bs-toast-border-radius);\n}\n.toast.showing {\n opacity: 0;\n}\n.toast:not(.show) {\n display: none;\n}\n\n.toast-container {\n --bs-toast-zindex: 1090;\n position: absolute;\n z-index: var(--bs-toast-zindex);\n width: max-content;\n max-width: 100%;\n pointer-events: none;\n}\n.toast-container > :not(:last-child) {\n margin-bottom: var(--bs-toast-spacing);\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x);\n color: var(--bs-toast-header-color);\n background-color: var(--bs-toast-header-bg);\n background-clip: padding-box;\n border-bottom: var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);\n border-top-left-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));\n border-top-right-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));\n}\n.toast-header .btn-close {\n margin-right: calc(-0.5 * var(--bs-toast-padding-x));\n margin-left: var(--bs-toast-padding-x);\n}\n\n.toast-body {\n padding: var(--bs-toast-padding-x);\n word-wrap: break-word;\n}\n\n.modal {\n --bs-modal-zindex: 1055;\n --bs-modal-width: 500px;\n --bs-modal-padding: 1rem;\n --bs-modal-margin: 0.5rem;\n --bs-modal-color: var(--bs-body-color);\n --bs-modal-bg: var(--bs-body-bg);\n --bs-modal-border-color: var(--bs-border-color-translucent);\n --bs-modal-border-width: var(--bs-border-width);\n --bs-modal-border-radius: var(--bs-border-radius-lg);\n --bs-modal-box-shadow: var(--bs-box-shadow-sm);\n --bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));\n --bs-modal-header-padding-x: 1rem;\n --bs-modal-header-padding-y: 1rem;\n --bs-modal-header-padding: 1rem 1rem;\n --bs-modal-header-border-color: var(--bs-border-color);\n --bs-modal-header-border-width: var(--bs-border-width);\n --bs-modal-title-line-height: 1.5;\n --bs-modal-footer-gap: 0.5rem;\n --bs-modal-footer-bg: ;\n --bs-modal-footer-border-color: var(--bs-border-color);\n --bs-modal-footer-border-width: var(--bs-border-width);\n position: fixed;\n top: 0;\n left: 0;\n z-index: var(--bs-modal-zindex);\n display: none;\n width: 100%;\n height: 100%;\n overflow-x: hidden;\n overflow-y: auto;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: var(--bs-modal-margin);\n pointer-events: none;\n}\n.modal.fade .modal-dialog {\n transform: translate(0, -50px);\n transition: transform 0.3s ease-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n.modal.show .modal-dialog {\n transform: none;\n}\n.modal.modal-static .modal-dialog {\n transform: scale(1.02);\n}\n\n.modal-dialog-scrollable {\n height: calc(100% - var(--bs-modal-margin) * 2);\n}\n.modal-dialog-scrollable .modal-content {\n max-height: 100%;\n overflow: hidden;\n}\n.modal-dialog-scrollable .modal-body {\n overflow-y: auto;\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: calc(100% - var(--bs-modal-margin) * 2);\n}\n\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n color: var(--bs-modal-color);\n pointer-events: auto;\n background-color: var(--bs-modal-bg);\n background-clip: padding-box;\n border: var(--bs-modal-border-width) solid var(--bs-modal-border-color);\n border-radius: var(--bs-modal-border-radius);\n outline: 0;\n}\n\n.modal-backdrop {\n --bs-backdrop-zindex: 1050;\n --bs-backdrop-bg: #000;\n --bs-backdrop-opacity: 0.5;\n position: fixed;\n top: 0;\n left: 0;\n z-index: var(--bs-backdrop-zindex);\n width: 100vw;\n height: 100vh;\n background-color: var(--bs-backdrop-bg);\n}\n.modal-backdrop.fade {\n opacity: 0;\n}\n.modal-backdrop.show {\n opacity: var(--bs-backdrop-opacity);\n}\n\n.modal-header {\n display: flex;\n flex-shrink: 0;\n align-items: center;\n padding: var(--bs-modal-header-padding);\n border-bottom: var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);\n border-top-left-radius: var(--bs-modal-inner-border-radius);\n border-top-right-radius: var(--bs-modal-inner-border-radius);\n}\n.modal-header .btn-close {\n padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5);\n margin-top: calc(-0.5 * var(--bs-modal-header-padding-y));\n margin-right: calc(-0.5 * var(--bs-modal-header-padding-x));\n margin-bottom: calc(-0.5 * var(--bs-modal-header-padding-y));\n margin-left: auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: var(--bs-modal-title-line-height);\n}\n\n.modal-body {\n position: relative;\n flex: 1 1 auto;\n padding: var(--bs-modal-padding);\n}\n\n.modal-footer {\n display: flex;\n flex-shrink: 0;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-end;\n padding: calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * 0.5);\n background-color: var(--bs-modal-footer-bg);\n border-top: var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);\n border-bottom-right-radius: var(--bs-modal-inner-border-radius);\n border-bottom-left-radius: var(--bs-modal-inner-border-radius);\n}\n.modal-footer > * {\n margin: calc(var(--bs-modal-footer-gap) * 0.5);\n}\n\n@media (min-width: 576px) {\n .modal {\n --bs-modal-margin: 1.75rem;\n --bs-modal-box-shadow: var(--bs-box-shadow);\n }\n .modal-dialog {\n max-width: var(--bs-modal-width);\n margin-right: auto;\n margin-left: auto;\n }\n .modal-sm {\n --bs-modal-width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg,\n .modal-xl {\n --bs-modal-width: 800px;\n }\n}\n@media (min-width: 1200px) {\n .modal-xl {\n --bs-modal-width: 1140px;\n }\n}\n.modal-fullscreen {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n}\n.modal-fullscreen .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n}\n.modal-fullscreen .modal-header,\n.modal-fullscreen .modal-footer {\n border-radius: 0;\n}\n.modal-fullscreen .modal-body {\n overflow-y: auto;\n}\n\n@media (max-width: 575.98px) {\n .modal-fullscreen-sm-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-sm-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-sm-down .modal-header,\n .modal-fullscreen-sm-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-sm-down .modal-body {\n overflow-y: auto;\n }\n}\n@media (max-width: 767.98px) {\n .modal-fullscreen-md-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-md-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-md-down .modal-header,\n .modal-fullscreen-md-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-md-down .modal-body {\n overflow-y: auto;\n }\n}\n@media (max-width: 991.98px) {\n .modal-fullscreen-lg-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-lg-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-lg-down .modal-header,\n .modal-fullscreen-lg-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-lg-down .modal-body {\n overflow-y: auto;\n }\n}\n@media (max-width: 1199.98px) {\n .modal-fullscreen-xl-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-xl-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-xl-down .modal-header,\n .modal-fullscreen-xl-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-xl-down .modal-body {\n overflow-y: auto;\n }\n}\n@media (max-width: 1399.98px) {\n .modal-fullscreen-xxl-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-xxl-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-xxl-down .modal-header,\n .modal-fullscreen-xxl-down .modal-footer {\n border-radius: 0;\n }\n .modal-fullscreen-xxl-down .modal-body {\n overflow-y: auto;\n }\n}\n.tooltip {\n --bs-tooltip-zindex: 1080;\n --bs-tooltip-max-width: 200px;\n --bs-tooltip-padding-x: 0.5rem;\n --bs-tooltip-padding-y: 0.25rem;\n --bs-tooltip-margin: ;\n --bs-tooltip-font-size: 0.875rem;\n --bs-tooltip-color: var(--bs-body-bg);\n --bs-tooltip-bg: var(--bs-emphasis-color);\n --bs-tooltip-border-radius: var(--bs-border-radius);\n --bs-tooltip-opacity: 0.9;\n --bs-tooltip-arrow-width: 0.8rem;\n --bs-tooltip-arrow-height: 0.4rem;\n z-index: var(--bs-tooltip-zindex);\n display: block;\n margin: var(--bs-tooltip-margin);\n font-family: var(--bs-font-sans-serif);\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n white-space: normal;\n word-spacing: normal;\n line-break: auto;\n font-size: var(--bs-tooltip-font-size);\n word-wrap: break-word;\n opacity: 0;\n}\n.tooltip.show {\n opacity: var(--bs-tooltip-opacity);\n}\n.tooltip .tooltip-arrow {\n display: block;\n width: var(--bs-tooltip-arrow-width);\n height: var(--bs-tooltip-arrow-height);\n}\n.tooltip .tooltip-arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow {\n bottom: calc(-1 * var(--bs-tooltip-arrow-height));\n}\n.bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before {\n top: -1px;\n border-width: var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0;\n border-top-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow {\n left: calc(-1 * var(--bs-tooltip-arrow-height));\n width: var(--bs-tooltip-arrow-height);\n height: var(--bs-tooltip-arrow-width);\n}\n.bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before {\n right: -1px;\n border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0;\n border-right-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:end:ignore */\n.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow {\n top: calc(-1 * var(--bs-tooltip-arrow-height));\n}\n.bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before {\n bottom: -1px;\n border-width: 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height);\n border-bottom-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow {\n right: calc(-1 * var(--bs-tooltip-arrow-height));\n width: var(--bs-tooltip-arrow-height);\n height: var(--bs-tooltip-arrow-width);\n}\n.bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before {\n left: -1px;\n border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height);\n border-left-color: var(--bs-tooltip-bg);\n}\n\n/* rtl:end:ignore */\n.tooltip-inner {\n max-width: var(--bs-tooltip-max-width);\n padding: var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);\n color: var(--bs-tooltip-color);\n text-align: center;\n background-color: var(--bs-tooltip-bg);\n border-radius: var(--bs-tooltip-border-radius);\n}\n\n.popover {\n --bs-popover-zindex: 1070;\n --bs-popover-max-width: 276px;\n --bs-popover-font-size: 0.875rem;\n --bs-popover-bg: var(--bs-body-bg);\n --bs-popover-border-width: var(--bs-border-width);\n --bs-popover-border-color: var(--bs-border-color-translucent);\n --bs-popover-border-radius: var(--bs-border-radius-lg);\n --bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width));\n --bs-popover-box-shadow: var(--bs-box-shadow);\n --bs-popover-header-padding-x: 1rem;\n --bs-popover-header-padding-y: 0.5rem;\n --bs-popover-header-font-size: 1rem;\n --bs-popover-header-color: inherit;\n --bs-popover-header-bg: var(--bs-secondary-bg);\n --bs-popover-body-padding-x: 1rem;\n --bs-popover-body-padding-y: 1rem;\n --bs-popover-body-color: var(--bs-body-color);\n --bs-popover-arrow-width: 1rem;\n --bs-popover-arrow-height: 0.5rem;\n --bs-popover-arrow-border: var(--bs-popover-border-color);\n z-index: var(--bs-popover-zindex);\n display: block;\n max-width: var(--bs-popover-max-width);\n font-family: var(--bs-font-sans-serif);\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n white-space: normal;\n word-spacing: normal;\n line-break: auto;\n font-size: var(--bs-popover-font-size);\n word-wrap: break-word;\n background-color: var(--bs-popover-bg);\n background-clip: padding-box;\n border: var(--bs-popover-border-width) solid var(--bs-popover-border-color);\n border-radius: var(--bs-popover-border-radius);\n}\n.popover .popover-arrow {\n display: block;\n width: var(--bs-popover-arrow-width);\n height: var(--bs-popover-arrow-height);\n}\n.popover .popover-arrow::before, .popover .popover-arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n border-width: 0;\n}\n\n.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow {\n bottom: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n}\n.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before, .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after {\n border-width: var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0;\n}\n.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before {\n bottom: 0;\n border-top-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after {\n bottom: var(--bs-popover-border-width);\n border-top-color: var(--bs-popover-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow {\n left: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n width: var(--bs-popover-arrow-height);\n height: var(--bs-popover-arrow-width);\n}\n.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before, .bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after {\n border-width: calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0;\n}\n.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before {\n left: 0;\n border-right-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after {\n left: var(--bs-popover-border-width);\n border-right-color: var(--bs-popover-bg);\n}\n\n/* rtl:end:ignore */\n.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow {\n top: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n}\n.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before, .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after {\n border-width: 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height);\n}\n.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before {\n top: 0;\n border-bottom-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after {\n top: var(--bs-popover-border-width);\n border-bottom-color: var(--bs-popover-bg);\n}\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=bottom] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: var(--bs-popover-arrow-width);\n margin-left: calc(-0.5 * var(--bs-popover-arrow-width));\n content: \"\";\n border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-header-bg);\n}\n\n/* rtl:begin:ignore */\n.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow {\n right: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));\n width: var(--bs-popover-arrow-height);\n height: var(--bs-popover-arrow-width);\n}\n.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before, .bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after {\n border-width: calc(var(--bs-popover-arrow-width) * 0.5) 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height);\n}\n.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before {\n right: 0;\n border-left-color: var(--bs-popover-arrow-border);\n}\n.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after {\n right: var(--bs-popover-border-width);\n border-left-color: var(--bs-popover-bg);\n}\n\n/* rtl:end:ignore */\n.popover-header {\n padding: var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);\n margin-bottom: 0;\n font-size: var(--bs-popover-header-font-size);\n color: var(--bs-popover-header-color);\n background-color: var(--bs-popover-header-bg);\n border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-border-color);\n border-top-left-radius: var(--bs-popover-inner-border-radius);\n border-top-right-radius: var(--bs-popover-inner-border-radius);\n}\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);\n color: var(--bs-popover-body-color);\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n backface-visibility: hidden;\n transition: transform 0.6s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next:not(.carousel-item-start),\n.active.carousel-item-end {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-end),\n.active.carousel-item-start {\n transform: translateX(-100%);\n}\n\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n}\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-start,\n.carousel-fade .carousel-item-prev.carousel-item-end {\n z-index: 1;\n opacity: 1;\n}\n.carousel-fade .active.carousel-item-start,\n.carousel-fade .active.carousel-item-end {\n z-index: 0;\n opacity: 0;\n transition: opacity 0s 0.6s;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-start,\n .carousel-fade .active.carousel-item-end {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 15%;\n padding: 0;\n color: #fff;\n text-align: center;\n background: none;\n filter: var(--bs-carousel-control-icon-filter);\n border: 0;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n .carousel-control-next {\n transition: none;\n }\n}\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n background-repeat: no-repeat;\n background-position: 50%;\n background-size: 100% 100%;\n}\n\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e\") /*rtl:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e\")*/;\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e\") /*rtl:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e\")*/;\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 2;\n display: flex;\n justify-content: center;\n padding: 0;\n margin-right: 15%;\n margin-bottom: 1rem;\n margin-left: 15%;\n}\n.carousel-indicators [data-bs-target] {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n padding: 0;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: var(--bs-carousel-indicator-active-bg);\n background-clip: padding-box;\n border: 0;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: 0.5;\n transition: opacity 0.6s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-indicators [data-bs-target] {\n transition: none;\n }\n}\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 1.25rem;\n left: 15%;\n padding-top: 1.25rem;\n padding-bottom: 1.25rem;\n color: var(--bs-carousel-caption-color);\n text-align: center;\n}\n\n.carousel-dark {\n --bs-carousel-indicator-active-bg: #000;\n --bs-carousel-caption-color: #000;\n --bs-carousel-control-icon-filter: invert(1) grayscale(100);\n}\n\n:root,\n[data-bs-theme=light] {\n --bs-carousel-indicator-active-bg: #fff;\n --bs-carousel-caption-color: #fff;\n --bs-carousel-control-icon-filter: ;\n}\n\n[data-bs-theme=dark] {\n --bs-carousel-indicator-active-bg: #000;\n --bs-carousel-caption-color: #000;\n --bs-carousel-control-icon-filter: invert(1) grayscale(100);\n}\n\n.spinner-grow,\n.spinner-border {\n display: inline-block;\n flex-shrink: 0;\n width: var(--bs-spinner-width);\n height: var(--bs-spinner-height);\n vertical-align: var(--bs-spinner-vertical-align);\n border-radius: 50%;\n animation: var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name);\n}\n\n@keyframes spinner-border {\n to {\n transform: rotate(360deg) /* rtl:ignore */;\n }\n}\n.spinner-border {\n --bs-spinner-width: 2rem;\n --bs-spinner-height: 2rem;\n --bs-spinner-vertical-align: -0.125em;\n --bs-spinner-border-width: 0.25em;\n --bs-spinner-animation-speed: 0.75s;\n --bs-spinner-animation-name: spinner-border;\n border: var(--bs-spinner-border-width) solid currentcolor;\n border-right-color: transparent;\n}\n\n.spinner-border-sm {\n --bs-spinner-width: 1rem;\n --bs-spinner-height: 1rem;\n --bs-spinner-border-width: 0.2em;\n}\n\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n transform: none;\n }\n}\n.spinner-grow {\n --bs-spinner-width: 2rem;\n --bs-spinner-height: 2rem;\n --bs-spinner-vertical-align: -0.125em;\n --bs-spinner-animation-speed: 0.75s;\n --bs-spinner-animation-name: spinner-grow;\n background-color: currentcolor;\n opacity: 0;\n}\n\n.spinner-grow-sm {\n --bs-spinner-width: 1rem;\n --bs-spinner-height: 1rem;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .spinner-border,\n .spinner-grow {\n --bs-spinner-animation-speed: 1.5s;\n }\n}\n.offcanvas, .offcanvas-xxl, .offcanvas-xl, .offcanvas-lg, .offcanvas-md, .offcanvas-sm {\n --bs-offcanvas-zindex: 1045;\n --bs-offcanvas-width: 400px;\n --bs-offcanvas-height: 30vh;\n --bs-offcanvas-padding-x: 1rem;\n --bs-offcanvas-padding-y: 1rem;\n --bs-offcanvas-color: var(--bs-body-color);\n --bs-offcanvas-bg: var(--bs-body-bg);\n --bs-offcanvas-border-width: var(--bs-border-width);\n --bs-offcanvas-border-color: var(--bs-border-color-translucent);\n --bs-offcanvas-box-shadow: var(--bs-box-shadow-sm);\n --bs-offcanvas-transition: transform 0.3s ease-in-out;\n --bs-offcanvas-title-line-height: 1.5;\n}\n\n@media (max-width: 575.98px) {\n .offcanvas-sm {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 575.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-sm {\n transition: none;\n }\n}\n@media (max-width: 575.98px) {\n .offcanvas-sm.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n .offcanvas-sm.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n .offcanvas-sm.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n .offcanvas-sm.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n .offcanvas-sm.showing, .offcanvas-sm.show:not(.hiding) {\n transform: none;\n }\n .offcanvas-sm.showing, .offcanvas-sm.hiding, .offcanvas-sm.show {\n visibility: visible;\n }\n}\n@media (min-width: 576px) {\n .offcanvas-sm {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-sm .offcanvas-header {\n display: none;\n }\n .offcanvas-sm .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n@media (max-width: 767.98px) {\n .offcanvas-md {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 767.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-md {\n transition: none;\n }\n}\n@media (max-width: 767.98px) {\n .offcanvas-md.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n .offcanvas-md.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n .offcanvas-md.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n .offcanvas-md.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n .offcanvas-md.showing, .offcanvas-md.show:not(.hiding) {\n transform: none;\n }\n .offcanvas-md.showing, .offcanvas-md.hiding, .offcanvas-md.show {\n visibility: visible;\n }\n}\n@media (min-width: 768px) {\n .offcanvas-md {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-md .offcanvas-header {\n display: none;\n }\n .offcanvas-md .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n@media (max-width: 991.98px) {\n .offcanvas-lg {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 991.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-lg {\n transition: none;\n }\n}\n@media (max-width: 991.98px) {\n .offcanvas-lg.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n .offcanvas-lg.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n .offcanvas-lg.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n .offcanvas-lg.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n .offcanvas-lg.showing, .offcanvas-lg.show:not(.hiding) {\n transform: none;\n }\n .offcanvas-lg.showing, .offcanvas-lg.hiding, .offcanvas-lg.show {\n visibility: visible;\n }\n}\n@media (min-width: 992px) {\n .offcanvas-lg {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-lg .offcanvas-header {\n display: none;\n }\n .offcanvas-lg .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n@media (max-width: 1199.98px) {\n .offcanvas-xl {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 1199.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-xl {\n transition: none;\n }\n}\n@media (max-width: 1199.98px) {\n .offcanvas-xl.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n .offcanvas-xl.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n .offcanvas-xl.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n .offcanvas-xl.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n .offcanvas-xl.showing, .offcanvas-xl.show:not(.hiding) {\n transform: none;\n }\n .offcanvas-xl.showing, .offcanvas-xl.hiding, .offcanvas-xl.show {\n visibility: visible;\n }\n}\n@media (min-width: 1200px) {\n .offcanvas-xl {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-xl .offcanvas-header {\n display: none;\n }\n .offcanvas-xl .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n@media (max-width: 1399.98px) {\n .offcanvas-xxl {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n }\n}\n@media (max-width: 1399.98px) and (prefers-reduced-motion: reduce) {\n .offcanvas-xxl {\n transition: none;\n }\n}\n@media (max-width: 1399.98px) {\n .offcanvas-xxl.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n }\n .offcanvas-xxl.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n }\n .offcanvas-xxl.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n }\n .offcanvas-xxl.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n }\n .offcanvas-xxl.showing, .offcanvas-xxl.show:not(.hiding) {\n transform: none;\n }\n .offcanvas-xxl.showing, .offcanvas-xxl.hiding, .offcanvas-xxl.show {\n visibility: visible;\n }\n}\n@media (min-width: 1400px) {\n .offcanvas-xxl {\n --bs-offcanvas-height: auto;\n --bs-offcanvas-border-width: 0;\n background-color: transparent !important;\n }\n .offcanvas-xxl .offcanvas-header {\n display: none;\n }\n .offcanvas-xxl .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n background-color: transparent !important;\n }\n}\n\n.offcanvas {\n position: fixed;\n bottom: 0;\n z-index: var(--bs-offcanvas-zindex);\n display: flex;\n flex-direction: column;\n max-width: 100%;\n color: var(--bs-offcanvas-color);\n visibility: hidden;\n background-color: var(--bs-offcanvas-bg);\n background-clip: padding-box;\n outline: 0;\n transition: var(--bs-offcanvas-transition);\n}\n@media (prefers-reduced-motion: reduce) {\n .offcanvas {\n transition: none;\n }\n}\n.offcanvas.offcanvas-start {\n top: 0;\n left: 0;\n width: var(--bs-offcanvas-width);\n border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(-100%);\n}\n.offcanvas.offcanvas-end {\n top: 0;\n right: 0;\n width: var(--bs-offcanvas-width);\n border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateX(100%);\n}\n.offcanvas.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(-100%);\n}\n.offcanvas.offcanvas-bottom {\n right: 0;\n left: 0;\n height: var(--bs-offcanvas-height);\n max-height: 100%;\n border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);\n transform: translateY(100%);\n}\n.offcanvas.showing, .offcanvas.show:not(.hiding) {\n transform: none;\n}\n.offcanvas.showing, .offcanvas.hiding, .offcanvas.show {\n visibility: visible;\n}\n\n.offcanvas-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n.offcanvas-backdrop.fade {\n opacity: 0;\n}\n.offcanvas-backdrop.show {\n opacity: 0.5;\n}\n\n.offcanvas-header {\n display: flex;\n align-items: center;\n padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);\n}\n.offcanvas-header .btn-close {\n padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5);\n margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y));\n margin-right: calc(-0.5 * var(--bs-offcanvas-padding-x));\n margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y));\n margin-left: auto;\n}\n\n.offcanvas-title {\n margin-bottom: 0;\n line-height: var(--bs-offcanvas-title-line-height);\n}\n\n.offcanvas-body {\n flex-grow: 1;\n padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);\n overflow-y: auto;\n}\n\n.placeholder {\n display: inline-block;\n min-height: 1em;\n vertical-align: middle;\n cursor: wait;\n background-color: currentcolor;\n opacity: 0.5;\n}\n.placeholder.btn::before {\n display: inline-block;\n content: \"\";\n}\n\n.placeholder-xs {\n min-height: 0.6em;\n}\n\n.placeholder-sm {\n min-height: 0.8em;\n}\n\n.placeholder-lg {\n min-height: 1.2em;\n}\n\n.placeholder-glow .placeholder {\n animation: placeholder-glow 2s ease-in-out infinite;\n}\n\n@keyframes placeholder-glow {\n 50% {\n opacity: 0.2;\n }\n}\n.placeholder-wave {\n mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);\n mask-size: 200% 100%;\n animation: placeholder-wave 2s linear infinite;\n}\n\n@keyframes placeholder-wave {\n 100% {\n mask-position: -200% 0%;\n }\n}\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.text-bg-primary {\n color: #fff !important;\n background-color: RGBA(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-secondary {\n color: #fff !important;\n background-color: RGBA(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-success {\n color: #fff !important;\n background-color: RGBA(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-info {\n color: #000 !important;\n background-color: RGBA(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-warning {\n color: #000 !important;\n background-color: RGBA(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-danger {\n color: #fff !important;\n background-color: RGBA(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-light {\n color: #000 !important;\n background-color: RGBA(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.text-bg-dark {\n color: #fff !important;\n background-color: RGBA(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important;\n}\n\n.link-primary {\n color: RGBA(var(--bs-primary-rgb), var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-primary:hover, .link-primary:focus {\n color: RGBA(10, 88, 202, var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(10, 88, 202, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-secondary {\n color: RGBA(var(--bs-secondary-rgb), var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-secondary:hover, .link-secondary:focus {\n color: RGBA(86, 94, 100, var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(86, 94, 100, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-success {\n color: RGBA(var(--bs-success-rgb), var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-success:hover, .link-success:focus {\n color: RGBA(20, 108, 67, var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(20, 108, 67, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-info {\n color: RGBA(var(--bs-info-rgb), var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-info:hover, .link-info:focus {\n color: RGBA(61, 213, 243, var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(61, 213, 243, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-warning {\n color: RGBA(var(--bs-warning-rgb), var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-warning:hover, .link-warning:focus {\n color: RGBA(255, 205, 57, var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(255, 205, 57, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-danger {\n color: RGBA(var(--bs-danger-rgb), var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-danger:hover, .link-danger:focus {\n color: RGBA(176, 42, 55, var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(176, 42, 55, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-light {\n color: RGBA(var(--bs-light-rgb), var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-light:hover, .link-light:focus {\n color: RGBA(249, 250, 251, var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-dark {\n color: RGBA(var(--bs-dark-rgb), var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-dark:hover, .link-dark:focus {\n color: RGBA(26, 30, 33, var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(26, 30, 33, var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-body-emphasis {\n color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 1)) !important;\n text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n.link-body-emphasis:hover, .link-body-emphasis:focus {\n color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 0.75)) !important;\n text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important;\n}\n\n.focus-ring:focus {\n outline: 0;\n box-shadow: var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color);\n}\n\n.icon-link {\n display: inline-flex;\n gap: 0.375rem;\n align-items: center;\n text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5));\n text-underline-offset: 0.25em;\n backface-visibility: hidden;\n}\n.icon-link > .bi {\n flex-shrink: 0;\n width: 1em;\n height: 1em;\n fill: currentcolor;\n transition: 0.2s ease-in-out transform;\n}\n@media (prefers-reduced-motion: reduce) {\n .icon-link > .bi {\n transition: none;\n }\n}\n\n.icon-link-hover:hover > .bi, .icon-link-hover:focus-visible > .bi {\n transform: var(--bs-icon-link-transform, translate3d(0.25em, 0, 0));\n}\n\n.ratio {\n position: relative;\n width: 100%;\n}\n.ratio::before {\n display: block;\n padding-top: var(--bs-aspect-ratio);\n content: \"\";\n}\n.ratio > * {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n\n.ratio-1x1 {\n --bs-aspect-ratio: 100%;\n}\n\n.ratio-4x3 {\n --bs-aspect-ratio: 75%;\n}\n\n.ratio-16x9 {\n --bs-aspect-ratio: 56.25%;\n}\n\n.ratio-21x9 {\n --bs-aspect-ratio: 42.8571428571%;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n.sticky-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n}\n\n.sticky-bottom {\n position: sticky;\n bottom: 0;\n z-index: 1020;\n}\n\n@media (min-width: 576px) {\n .sticky-sm-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-sm-bottom {\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 768px) {\n .sticky-md-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-md-bottom {\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 992px) {\n .sticky-lg-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-lg-bottom {\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 1200px) {\n .sticky-xl-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-xl-bottom {\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 1400px) {\n .sticky-xxl-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n .sticky-xxl-bottom {\n position: sticky;\n bottom: 0;\n z-index: 1020;\n }\n}\n.hstack {\n display: flex;\n flex-direction: row;\n align-items: center;\n align-self: stretch;\n}\n\n.vstack {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n align-self: stretch;\n}\n\n.visually-hidden,\n.visually-hidden-focusable:not(:focus):not(:focus-within) {\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip: rect(0, 0, 0, 0) !important;\n white-space: nowrap !important;\n border: 0 !important;\n}\n.visually-hidden:not(caption),\n.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) {\n position: absolute !important;\n}\n.visually-hidden *,\n.visually-hidden-focusable:not(:focus):not(:focus-within) * {\n overflow: hidden !important;\n}\n\n.stretched-link::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n content: \"\";\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.vr {\n display: inline-block;\n align-self: stretch;\n width: var(--bs-border-width);\n min-height: 1em;\n background-color: currentcolor;\n opacity: 0.25;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.float-start {\n float: left !important;\n}\n\n.float-end {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n.object-fit-contain {\n object-fit: contain !important;\n}\n\n.object-fit-cover {\n object-fit: cover !important;\n}\n\n.object-fit-fill {\n object-fit: fill !important;\n}\n\n.object-fit-scale {\n object-fit: scale-down !important;\n}\n\n.object-fit-none {\n object-fit: none !important;\n}\n\n.opacity-0 {\n opacity: 0 !important;\n}\n\n.opacity-25 {\n opacity: 0.25 !important;\n}\n\n.opacity-50 {\n opacity: 0.5 !important;\n}\n\n.opacity-75 {\n opacity: 0.75 !important;\n}\n\n.opacity-100 {\n opacity: 1 !important;\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.overflow-visible {\n overflow: visible !important;\n}\n\n.overflow-scroll {\n overflow: scroll !important;\n}\n\n.overflow-x-auto {\n overflow-x: auto !important;\n}\n\n.overflow-x-hidden {\n overflow-x: hidden !important;\n}\n\n.overflow-x-visible {\n overflow-x: visible !important;\n}\n\n.overflow-x-scroll {\n overflow-x: scroll !important;\n}\n\n.overflow-y-auto {\n overflow-y: auto !important;\n}\n\n.overflow-y-hidden {\n overflow-y: hidden !important;\n}\n\n.overflow-y-visible {\n overflow-y: visible !important;\n}\n\n.overflow-y-scroll {\n overflow-y: scroll !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-grid {\n display: grid !important;\n}\n\n.d-inline-grid {\n display: inline-grid !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n.d-none {\n display: none !important;\n}\n\n.shadow {\n box-shadow: var(--bs-box-shadow) !important;\n}\n\n.shadow-sm {\n box-shadow: var(--bs-box-shadow-sm) !important;\n}\n\n.shadow-lg {\n box-shadow: var(--bs-box-shadow-lg) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.focus-ring-primary {\n --bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-secondary {\n --bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-success {\n --bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-info {\n --bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-warning {\n --bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-danger {\n --bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-light {\n --bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity));\n}\n\n.focus-ring-dark {\n --bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity));\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: sticky !important;\n}\n\n.top-0 {\n top: 0 !important;\n}\n\n.top-50 {\n top: 50% !important;\n}\n\n.top-100 {\n top: 100% !important;\n}\n\n.bottom-0 {\n bottom: 0 !important;\n}\n\n.bottom-50 {\n bottom: 50% !important;\n}\n\n.bottom-100 {\n bottom: 100% !important;\n}\n\n.start-0 {\n left: 0 !important;\n}\n\n.start-50 {\n left: 50% !important;\n}\n\n.start-100 {\n left: 100% !important;\n}\n\n.end-0 {\n right: 0 !important;\n}\n\n.end-50 {\n right: 50% !important;\n}\n\n.end-100 {\n right: 100% !important;\n}\n\n.translate-middle {\n transform: translate(-50%, -50%) !important;\n}\n\n.translate-middle-x {\n transform: translateX(-50%) !important;\n}\n\n.translate-middle-y {\n transform: translateY(-50%) !important;\n}\n\n.border {\n border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top {\n border-top: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-end {\n border-right: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-end-0 {\n border-right: 0 !important;\n}\n\n.border-bottom {\n border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-start {\n border-left: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;\n}\n\n.border-start-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-secondary {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-success {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-info {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-warning {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-danger {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-light {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-dark {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-black {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-black-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-white {\n --bs-border-opacity: 1;\n border-color: rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important;\n}\n\n.border-primary-subtle {\n border-color: var(--bs-primary-border-subtle) !important;\n}\n\n.border-secondary-subtle {\n border-color: var(--bs-secondary-border-subtle) !important;\n}\n\n.border-success-subtle {\n border-color: var(--bs-success-border-subtle) !important;\n}\n\n.border-info-subtle {\n border-color: var(--bs-info-border-subtle) !important;\n}\n\n.border-warning-subtle {\n border-color: var(--bs-warning-border-subtle) !important;\n}\n\n.border-danger-subtle {\n border-color: var(--bs-danger-border-subtle) !important;\n}\n\n.border-light-subtle {\n border-color: var(--bs-light-border-subtle) !important;\n}\n\n.border-dark-subtle {\n border-color: var(--bs-dark-border-subtle) !important;\n}\n\n.border-1 {\n border-width: 1px !important;\n}\n\n.border-2 {\n border-width: 2px !important;\n}\n\n.border-3 {\n border-width: 3px !important;\n}\n\n.border-4 {\n border-width: 4px !important;\n}\n\n.border-5 {\n border-width: 5px !important;\n}\n\n.border-opacity-10 {\n --bs-border-opacity: 0.1;\n}\n\n.border-opacity-25 {\n --bs-border-opacity: 0.25;\n}\n\n.border-opacity-50 {\n --bs-border-opacity: 0.5;\n}\n\n.border-opacity-75 {\n --bs-border-opacity: 0.75;\n}\n\n.border-opacity-100 {\n --bs-border-opacity: 1;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n justify-content: space-evenly !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n.order-first {\n order: -1 !important;\n}\n\n.order-0 {\n order: 0 !important;\n}\n\n.order-1 {\n order: 1 !important;\n}\n\n.order-2 {\n order: 2 !important;\n}\n\n.order-3 {\n order: 3 !important;\n}\n\n.order-4 {\n order: 4 !important;\n}\n\n.order-5 {\n order: 5 !important;\n}\n\n.order-last {\n order: 6 !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mx-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n}\n\n.mx-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n}\n\n.mx-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n}\n\n.mx-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n}\n\n.mx-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n}\n\n.mx-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n}\n\n.mx-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n}\n\n.my-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n}\n\n.my-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n}\n\n.my-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n}\n\n.my-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n}\n\n.mt-0 {\n margin-top: 0 !important;\n}\n\n.mt-1 {\n margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n margin-top: 1rem !important;\n}\n\n.mt-4 {\n margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n margin-top: 3rem !important;\n}\n\n.mt-auto {\n margin-top: auto !important;\n}\n\n.me-0 {\n margin-right: 0 !important;\n}\n\n.me-1 {\n margin-right: 0.25rem !important;\n}\n\n.me-2 {\n margin-right: 0.5rem !important;\n}\n\n.me-3 {\n margin-right: 1rem !important;\n}\n\n.me-4 {\n margin-right: 1.5rem !important;\n}\n\n.me-5 {\n margin-right: 3rem !important;\n}\n\n.me-auto {\n margin-right: auto !important;\n}\n\n.mb-0 {\n margin-bottom: 0 !important;\n}\n\n.mb-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n margin-bottom: auto !important;\n}\n\n.ms-0 {\n margin-left: 0 !important;\n}\n\n.ms-1 {\n margin-left: 0.25rem !important;\n}\n\n.ms-2 {\n margin-left: 0.5rem !important;\n}\n\n.ms-3 {\n margin-left: 1rem !important;\n}\n\n.ms-4 {\n margin-left: 1.5rem !important;\n}\n\n.ms-5 {\n margin-left: 3rem !important;\n}\n\n.ms-auto {\n margin-left: auto !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.px-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n}\n\n.px-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n}\n\n.px-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n}\n\n.px-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n}\n\n.px-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n}\n\n.px-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n}\n\n.py-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n}\n\n.py-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n}\n\n.py-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n padding-top: 0 !important;\n}\n\n.pt-1 {\n padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n padding-top: 1rem !important;\n}\n\n.pt-4 {\n padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n padding-top: 3rem !important;\n}\n\n.pe-0 {\n padding-right: 0 !important;\n}\n\n.pe-1 {\n padding-right: 0.25rem !important;\n}\n\n.pe-2 {\n padding-right: 0.5rem !important;\n}\n\n.pe-3 {\n padding-right: 1rem !important;\n}\n\n.pe-4 {\n padding-right: 1.5rem !important;\n}\n\n.pe-5 {\n padding-right: 3rem !important;\n}\n\n.pb-0 {\n padding-bottom: 0 !important;\n}\n\n.pb-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n padding-left: 0 !important;\n}\n\n.ps-1 {\n padding-left: 0.25rem !important;\n}\n\n.ps-2 {\n padding-left: 0.5rem !important;\n}\n\n.ps-3 {\n padding-left: 1rem !important;\n}\n\n.ps-4 {\n padding-left: 1.5rem !important;\n}\n\n.ps-5 {\n padding-left: 3rem !important;\n}\n\n.gap-0 {\n gap: 0 !important;\n}\n\n.gap-1 {\n gap: 0.25rem !important;\n}\n\n.gap-2 {\n gap: 0.5rem !important;\n}\n\n.gap-3 {\n gap: 1rem !important;\n}\n\n.gap-4 {\n gap: 1.5rem !important;\n}\n\n.gap-5 {\n gap: 3rem !important;\n}\n\n.row-gap-0 {\n row-gap: 0 !important;\n}\n\n.row-gap-1 {\n row-gap: 0.25rem !important;\n}\n\n.row-gap-2 {\n row-gap: 0.5rem !important;\n}\n\n.row-gap-3 {\n row-gap: 1rem !important;\n}\n\n.row-gap-4 {\n row-gap: 1.5rem !important;\n}\n\n.row-gap-5 {\n row-gap: 3rem !important;\n}\n\n.column-gap-0 {\n column-gap: 0 !important;\n}\n\n.column-gap-1 {\n column-gap: 0.25rem !important;\n}\n\n.column-gap-2 {\n column-gap: 0.5rem !important;\n}\n\n.column-gap-3 {\n column-gap: 1rem !important;\n}\n\n.column-gap-4 {\n column-gap: 1.5rem !important;\n}\n\n.column-gap-5 {\n column-gap: 3rem !important;\n}\n\n.font-monospace {\n font-family: var(--bs-font-monospace) !important;\n}\n\n.fs-1 {\n font-size: calc(1.375rem + 1.5vw) !important;\n}\n\n.fs-2 {\n font-size: calc(1.325rem + 0.9vw) !important;\n}\n\n.fs-3 {\n font-size: calc(1.3rem + 0.6vw) !important;\n}\n\n.fs-4 {\n font-size: calc(1.275rem + 0.3vw) !important;\n}\n\n.fs-5 {\n font-size: 1.25rem !important;\n}\n\n.fs-6 {\n font-size: 1rem !important;\n}\n\n.fst-italic {\n font-style: italic !important;\n}\n\n.fst-normal {\n font-style: normal !important;\n}\n\n.fw-lighter {\n font-weight: lighter !important;\n}\n\n.fw-light {\n font-weight: 300 !important;\n}\n\n.fw-normal {\n font-weight: 400 !important;\n}\n\n.fw-medium {\n font-weight: 500 !important;\n}\n\n.fw-semibold {\n font-weight: 600 !important;\n}\n\n.fw-bold {\n font-weight: 700 !important;\n}\n\n.fw-bolder {\n font-weight: bolder !important;\n}\n\n.lh-1 {\n line-height: 1 !important;\n}\n\n.lh-sm {\n line-height: 1.25 !important;\n}\n\n.lh-base {\n line-height: 1.5 !important;\n}\n\n.lh-lg {\n line-height: 2 !important;\n}\n\n.text-start {\n text-align: left !important;\n}\n\n.text-end {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-decoration-underline {\n text-decoration: underline !important;\n}\n\n.text-decoration-line-through {\n text-decoration: line-through !important;\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n/* rtl:begin:remove */\n.text-break {\n word-wrap: break-word !important;\n word-break: break-word !important;\n}\n\n/* rtl:end:remove */\n.text-primary {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-secondary {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-success {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-info {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-warning {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-danger {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-light {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-dark {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-black {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-white {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-body {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-muted {\n --bs-text-opacity: 1;\n color: var(--bs-secondary-color) !important;\n}\n\n.text-black-50 {\n --bs-text-opacity: 1;\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n --bs-text-opacity: 1;\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-body-secondary {\n --bs-text-opacity: 1;\n color: var(--bs-secondary-color) !important;\n}\n\n.text-body-tertiary {\n --bs-text-opacity: 1;\n color: var(--bs-tertiary-color) !important;\n}\n\n.text-body-emphasis {\n --bs-text-opacity: 1;\n color: var(--bs-emphasis-color) !important;\n}\n\n.text-reset {\n --bs-text-opacity: 1;\n color: inherit !important;\n}\n\n.text-opacity-25 {\n --bs-text-opacity: 0.25;\n}\n\n.text-opacity-50 {\n --bs-text-opacity: 0.5;\n}\n\n.text-opacity-75 {\n --bs-text-opacity: 0.75;\n}\n\n.text-opacity-100 {\n --bs-text-opacity: 1;\n}\n\n.text-primary-emphasis {\n color: var(--bs-primary-text-emphasis) !important;\n}\n\n.text-secondary-emphasis {\n color: var(--bs-secondary-text-emphasis) !important;\n}\n\n.text-success-emphasis {\n color: var(--bs-success-text-emphasis) !important;\n}\n\n.text-info-emphasis {\n color: var(--bs-info-text-emphasis) !important;\n}\n\n.text-warning-emphasis {\n color: var(--bs-warning-text-emphasis) !important;\n}\n\n.text-danger-emphasis {\n color: var(--bs-danger-text-emphasis) !important;\n}\n\n.text-light-emphasis {\n color: var(--bs-light-text-emphasis) !important;\n}\n\n.text-dark-emphasis {\n color: var(--bs-dark-text-emphasis) !important;\n}\n\n.link-opacity-10 {\n --bs-link-opacity: 0.1;\n}\n\n.link-opacity-10-hover:hover {\n --bs-link-opacity: 0.1;\n}\n\n.link-opacity-25 {\n --bs-link-opacity: 0.25;\n}\n\n.link-opacity-25-hover:hover {\n --bs-link-opacity: 0.25;\n}\n\n.link-opacity-50 {\n --bs-link-opacity: 0.5;\n}\n\n.link-opacity-50-hover:hover {\n --bs-link-opacity: 0.5;\n}\n\n.link-opacity-75 {\n --bs-link-opacity: 0.75;\n}\n\n.link-opacity-75-hover:hover {\n --bs-link-opacity: 0.75;\n}\n\n.link-opacity-100 {\n --bs-link-opacity: 1;\n}\n\n.link-opacity-100-hover:hover {\n --bs-link-opacity: 1;\n}\n\n.link-offset-1 {\n text-underline-offset: 0.125em !important;\n}\n\n.link-offset-1-hover:hover {\n text-underline-offset: 0.125em !important;\n}\n\n.link-offset-2 {\n text-underline-offset: 0.25em !important;\n}\n\n.link-offset-2-hover:hover {\n text-underline-offset: 0.25em !important;\n}\n\n.link-offset-3 {\n text-underline-offset: 0.375em !important;\n}\n\n.link-offset-3-hover:hover {\n text-underline-offset: 0.375em !important;\n}\n\n.link-underline-primary {\n --bs-link-underline-opacity: 1;\n text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-secondary {\n --bs-link-underline-opacity: 1;\n text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-success {\n --bs-link-underline-opacity: 1;\n text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-info {\n --bs-link-underline-opacity: 1;\n text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-warning {\n --bs-link-underline-opacity: 1;\n text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-danger {\n --bs-link-underline-opacity: 1;\n text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-light {\n --bs-link-underline-opacity: 1;\n text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline-dark {\n --bs-link-underline-opacity: 1;\n text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important;\n}\n\n.link-underline {\n --bs-link-underline-opacity: 1;\n text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important;\n}\n\n.link-underline-opacity-0 {\n --bs-link-underline-opacity: 0;\n}\n\n.link-underline-opacity-0-hover:hover {\n --bs-link-underline-opacity: 0;\n}\n\n.link-underline-opacity-10 {\n --bs-link-underline-opacity: 0.1;\n}\n\n.link-underline-opacity-10-hover:hover {\n --bs-link-underline-opacity: 0.1;\n}\n\n.link-underline-opacity-25 {\n --bs-link-underline-opacity: 0.25;\n}\n\n.link-underline-opacity-25-hover:hover {\n --bs-link-underline-opacity: 0.25;\n}\n\n.link-underline-opacity-50 {\n --bs-link-underline-opacity: 0.5;\n}\n\n.link-underline-opacity-50-hover:hover {\n --bs-link-underline-opacity: 0.5;\n}\n\n.link-underline-opacity-75 {\n --bs-link-underline-opacity: 0.75;\n}\n\n.link-underline-opacity-75-hover:hover {\n --bs-link-underline-opacity: 0.75;\n}\n\n.link-underline-opacity-100 {\n --bs-link-underline-opacity: 1;\n}\n\n.link-underline-opacity-100-hover:hover {\n --bs-link-underline-opacity: 1;\n}\n\n.bg-primary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-secondary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-success {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-info {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-warning {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-danger {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-light {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-dark {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-black {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-white {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-body {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-transparent {\n --bs-bg-opacity: 1;\n background-color: transparent !important;\n}\n\n.bg-body-secondary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-body-tertiary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-opacity-10 {\n --bs-bg-opacity: 0.1;\n}\n\n.bg-opacity-25 {\n --bs-bg-opacity: 0.25;\n}\n\n.bg-opacity-50 {\n --bs-bg-opacity: 0.5;\n}\n\n.bg-opacity-75 {\n --bs-bg-opacity: 0.75;\n}\n\n.bg-opacity-100 {\n --bs-bg-opacity: 1;\n}\n\n.bg-primary-subtle {\n background-color: var(--bs-primary-bg-subtle) !important;\n}\n\n.bg-secondary-subtle {\n background-color: var(--bs-secondary-bg-subtle) !important;\n}\n\n.bg-success-subtle {\n background-color: var(--bs-success-bg-subtle) !important;\n}\n\n.bg-info-subtle {\n background-color: var(--bs-info-bg-subtle) !important;\n}\n\n.bg-warning-subtle {\n background-color: var(--bs-warning-bg-subtle) !important;\n}\n\n.bg-danger-subtle {\n background-color: var(--bs-danger-bg-subtle) !important;\n}\n\n.bg-light-subtle {\n background-color: var(--bs-light-bg-subtle) !important;\n}\n\n.bg-dark-subtle {\n background-color: var(--bs-dark-bg-subtle) !important;\n}\n\n.bg-gradient {\n background-image: var(--bs-gradient) !important;\n}\n\n.user-select-all {\n user-select: all !important;\n}\n\n.user-select-auto {\n user-select: auto !important;\n}\n\n.user-select-none {\n user-select: none !important;\n}\n\n.pe-none {\n pointer-events: none !important;\n}\n\n.pe-auto {\n pointer-events: auto !important;\n}\n\n.rounded {\n border-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.rounded-1 {\n border-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-2 {\n border-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-3 {\n border-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-4 {\n border-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-5 {\n border-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-top {\n border-top-left-radius: var(--bs-border-radius) !important;\n border-top-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-top-0 {\n border-top-left-radius: 0 !important;\n border-top-right-radius: 0 !important;\n}\n\n.rounded-top-1 {\n border-top-left-radius: var(--bs-border-radius-sm) !important;\n border-top-right-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-top-2 {\n border-top-left-radius: var(--bs-border-radius) !important;\n border-top-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-top-3 {\n border-top-left-radius: var(--bs-border-radius-lg) !important;\n border-top-right-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-top-4 {\n border-top-left-radius: var(--bs-border-radius-xl) !important;\n border-top-right-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-top-5 {\n border-top-left-radius: var(--bs-border-radius-xxl) !important;\n border-top-right-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-top-circle {\n border-top-left-radius: 50% !important;\n border-top-right-radius: 50% !important;\n}\n\n.rounded-top-pill {\n border-top-left-radius: var(--bs-border-radius-pill) !important;\n border-top-right-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-end {\n border-top-right-radius: var(--bs-border-radius) !important;\n border-bottom-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-end-0 {\n border-top-right-radius: 0 !important;\n border-bottom-right-radius: 0 !important;\n}\n\n.rounded-end-1 {\n border-top-right-radius: var(--bs-border-radius-sm) !important;\n border-bottom-right-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-end-2 {\n border-top-right-radius: var(--bs-border-radius) !important;\n border-bottom-right-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-end-3 {\n border-top-right-radius: var(--bs-border-radius-lg) !important;\n border-bottom-right-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-end-4 {\n border-top-right-radius: var(--bs-border-radius-xl) !important;\n border-bottom-right-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-end-5 {\n border-top-right-radius: var(--bs-border-radius-xxl) !important;\n border-bottom-right-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-end-circle {\n border-top-right-radius: 50% !important;\n border-bottom-right-radius: 50% !important;\n}\n\n.rounded-end-pill {\n border-top-right-radius: var(--bs-border-radius-pill) !important;\n border-bottom-right-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: var(--bs-border-radius) !important;\n border-bottom-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-bottom-0 {\n border-bottom-right-radius: 0 !important;\n border-bottom-left-radius: 0 !important;\n}\n\n.rounded-bottom-1 {\n border-bottom-right-radius: var(--bs-border-radius-sm) !important;\n border-bottom-left-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-bottom-2 {\n border-bottom-right-radius: var(--bs-border-radius) !important;\n border-bottom-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-bottom-3 {\n border-bottom-right-radius: var(--bs-border-radius-lg) !important;\n border-bottom-left-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-bottom-4 {\n border-bottom-right-radius: var(--bs-border-radius-xl) !important;\n border-bottom-left-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-bottom-5 {\n border-bottom-right-radius: var(--bs-border-radius-xxl) !important;\n border-bottom-left-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-bottom-circle {\n border-bottom-right-radius: 50% !important;\n border-bottom-left-radius: 50% !important;\n}\n\n.rounded-bottom-pill {\n border-bottom-right-radius: var(--bs-border-radius-pill) !important;\n border-bottom-left-radius: var(--bs-border-radius-pill) !important;\n}\n\n.rounded-start {\n border-bottom-left-radius: var(--bs-border-radius) !important;\n border-top-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-start-0 {\n border-bottom-left-radius: 0 !important;\n border-top-left-radius: 0 !important;\n}\n\n.rounded-start-1 {\n border-bottom-left-radius: var(--bs-border-radius-sm) !important;\n border-top-left-radius: var(--bs-border-radius-sm) !important;\n}\n\n.rounded-start-2 {\n border-bottom-left-radius: var(--bs-border-radius) !important;\n border-top-left-radius: var(--bs-border-radius) !important;\n}\n\n.rounded-start-3 {\n border-bottom-left-radius: var(--bs-border-radius-lg) !important;\n border-top-left-radius: var(--bs-border-radius-lg) !important;\n}\n\n.rounded-start-4 {\n border-bottom-left-radius: var(--bs-border-radius-xl) !important;\n border-top-left-radius: var(--bs-border-radius-xl) !important;\n}\n\n.rounded-start-5 {\n border-bottom-left-radius: var(--bs-border-radius-xxl) !important;\n border-top-left-radius: var(--bs-border-radius-xxl) !important;\n}\n\n.rounded-start-circle {\n border-bottom-left-radius: 50% !important;\n border-top-left-radius: 50% !important;\n}\n\n.rounded-start-pill {\n border-bottom-left-radius: var(--bs-border-radius-pill) !important;\n border-top-left-radius: var(--bs-border-radius-pill) !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n.z-n1 {\n z-index: -1 !important;\n}\n\n.z-0 {\n z-index: 0 !important;\n}\n\n.z-1 {\n z-index: 1 !important;\n}\n\n.z-2 {\n z-index: 2 !important;\n}\n\n.z-3 {\n z-index: 3 !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-start {\n float: left !important;\n }\n .float-sm-end {\n float: right !important;\n }\n .float-sm-none {\n float: none !important;\n }\n .object-fit-sm-contain {\n object-fit: contain !important;\n }\n .object-fit-sm-cover {\n object-fit: cover !important;\n }\n .object-fit-sm-fill {\n object-fit: fill !important;\n }\n .object-fit-sm-scale {\n object-fit: scale-down !important;\n }\n .object-fit-sm-none {\n object-fit: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-grid {\n display: grid !important;\n }\n .d-sm-inline-grid {\n display: inline-grid !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n .d-sm-none {\n display: none !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .justify-content-sm-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n .order-sm-first {\n order: -1 !important;\n }\n .order-sm-0 {\n order: 0 !important;\n }\n .order-sm-1 {\n order: 1 !important;\n }\n .order-sm-2 {\n order: 2 !important;\n }\n .order-sm-3 {\n order: 3 !important;\n }\n .order-sm-4 {\n order: 4 !important;\n }\n .order-sm-5 {\n order: 5 !important;\n }\n .order-sm-last {\n order: 6 !important;\n }\n .m-sm-0 {\n margin: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mx-sm-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-sm-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-sm-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-sm-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-sm-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-sm-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-sm-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-sm-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-sm-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-sm-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-sm-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-sm-0 {\n margin-top: 0 !important;\n }\n .mt-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mt-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mt-sm-3 {\n margin-top: 1rem !important;\n }\n .mt-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mt-sm-5 {\n margin-top: 3rem !important;\n }\n .mt-sm-auto {\n margin-top: auto !important;\n }\n .me-sm-0 {\n margin-right: 0 !important;\n }\n .me-sm-1 {\n margin-right: 0.25rem !important;\n }\n .me-sm-2 {\n margin-right: 0.5rem !important;\n }\n .me-sm-3 {\n margin-right: 1rem !important;\n }\n .me-sm-4 {\n margin-right: 1.5rem !important;\n }\n .me-sm-5 {\n margin-right: 3rem !important;\n }\n .me-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-0 {\n margin-bottom: 0 !important;\n }\n .mb-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-sm-3 {\n margin-bottom: 1rem !important;\n }\n .mb-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-sm-5 {\n margin-bottom: 3rem !important;\n }\n .mb-sm-auto {\n margin-bottom: auto !important;\n }\n .ms-sm-0 {\n margin-left: 0 !important;\n }\n .ms-sm-1 {\n margin-left: 0.25rem !important;\n }\n .ms-sm-2 {\n margin-left: 0.5rem !important;\n }\n .ms-sm-3 {\n margin-left: 1rem !important;\n }\n .ms-sm-4 {\n margin-left: 1.5rem !important;\n }\n .ms-sm-5 {\n margin-left: 3rem !important;\n }\n .ms-sm-auto {\n margin-left: auto !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .px-sm-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-sm-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-sm-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-sm-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-sm-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-sm-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-sm-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-sm-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-sm-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-sm-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-sm-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-sm-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-sm-0 {\n padding-top: 0 !important;\n }\n .pt-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pt-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pt-sm-3 {\n padding-top: 1rem !important;\n }\n .pt-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pt-sm-5 {\n padding-top: 3rem !important;\n }\n .pe-sm-0 {\n padding-right: 0 !important;\n }\n .pe-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pe-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pe-sm-3 {\n padding-right: 1rem !important;\n }\n .pe-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pe-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-0 {\n padding-bottom: 0 !important;\n }\n .pb-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pb-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-sm-5 {\n padding-bottom: 3rem !important;\n }\n .ps-sm-0 {\n padding-left: 0 !important;\n }\n .ps-sm-1 {\n padding-left: 0.25rem !important;\n }\n .ps-sm-2 {\n padding-left: 0.5rem !important;\n }\n .ps-sm-3 {\n padding-left: 1rem !important;\n }\n .ps-sm-4 {\n padding-left: 1.5rem !important;\n }\n .ps-sm-5 {\n padding-left: 3rem !important;\n }\n .gap-sm-0 {\n gap: 0 !important;\n }\n .gap-sm-1 {\n gap: 0.25rem !important;\n }\n .gap-sm-2 {\n gap: 0.5rem !important;\n }\n .gap-sm-3 {\n gap: 1rem !important;\n }\n .gap-sm-4 {\n gap: 1.5rem !important;\n }\n .gap-sm-5 {\n gap: 3rem !important;\n }\n .row-gap-sm-0 {\n row-gap: 0 !important;\n }\n .row-gap-sm-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-sm-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-sm-3 {\n row-gap: 1rem !important;\n }\n .row-gap-sm-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-sm-5 {\n row-gap: 3rem !important;\n }\n .column-gap-sm-0 {\n column-gap: 0 !important;\n }\n .column-gap-sm-1 {\n column-gap: 0.25rem !important;\n }\n .column-gap-sm-2 {\n column-gap: 0.5rem !important;\n }\n .column-gap-sm-3 {\n column-gap: 1rem !important;\n }\n .column-gap-sm-4 {\n column-gap: 1.5rem !important;\n }\n .column-gap-sm-5 {\n column-gap: 3rem !important;\n }\n .text-sm-start {\n text-align: left !important;\n }\n .text-sm-end {\n text-align: right !important;\n }\n .text-sm-center {\n text-align: center !important;\n }\n}\n@media (min-width: 768px) {\n .float-md-start {\n float: left !important;\n }\n .float-md-end {\n float: right !important;\n }\n .float-md-none {\n float: none !important;\n }\n .object-fit-md-contain {\n object-fit: contain !important;\n }\n .object-fit-md-cover {\n object-fit: cover !important;\n }\n .object-fit-md-fill {\n object-fit: fill !important;\n }\n .object-fit-md-scale {\n object-fit: scale-down !important;\n }\n .object-fit-md-none {\n object-fit: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-grid {\n display: grid !important;\n }\n .d-md-inline-grid {\n display: inline-grid !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n .d-md-none {\n display: none !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .justify-content-md-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n .order-md-first {\n order: -1 !important;\n }\n .order-md-0 {\n order: 0 !important;\n }\n .order-md-1 {\n order: 1 !important;\n }\n .order-md-2 {\n order: 2 !important;\n }\n .order-md-3 {\n order: 3 !important;\n }\n .order-md-4 {\n order: 4 !important;\n }\n .order-md-5 {\n order: 5 !important;\n }\n .order-md-last {\n order: 6 !important;\n }\n .m-md-0 {\n margin: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mx-md-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-md-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-md-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-md-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-md-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-md-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-md-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-md-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-md-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-md-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-md-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-md-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-md-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-md-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-md-0 {\n margin-top: 0 !important;\n }\n .mt-md-1 {\n margin-top: 0.25rem !important;\n }\n .mt-md-2 {\n margin-top: 0.5rem !important;\n }\n .mt-md-3 {\n margin-top: 1rem !important;\n }\n .mt-md-4 {\n margin-top: 1.5rem !important;\n }\n .mt-md-5 {\n margin-top: 3rem !important;\n }\n .mt-md-auto {\n margin-top: auto !important;\n }\n .me-md-0 {\n margin-right: 0 !important;\n }\n .me-md-1 {\n margin-right: 0.25rem !important;\n }\n .me-md-2 {\n margin-right: 0.5rem !important;\n }\n .me-md-3 {\n margin-right: 1rem !important;\n }\n .me-md-4 {\n margin-right: 1.5rem !important;\n }\n .me-md-5 {\n margin-right: 3rem !important;\n }\n .me-md-auto {\n margin-right: auto !important;\n }\n .mb-md-0 {\n margin-bottom: 0 !important;\n }\n .mb-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-md-3 {\n margin-bottom: 1rem !important;\n }\n .mb-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-md-5 {\n margin-bottom: 3rem !important;\n }\n .mb-md-auto {\n margin-bottom: auto !important;\n }\n .ms-md-0 {\n margin-left: 0 !important;\n }\n .ms-md-1 {\n margin-left: 0.25rem !important;\n }\n .ms-md-2 {\n margin-left: 0.5rem !important;\n }\n .ms-md-3 {\n margin-left: 1rem !important;\n }\n .ms-md-4 {\n margin-left: 1.5rem !important;\n }\n .ms-md-5 {\n margin-left: 3rem !important;\n }\n .ms-md-auto {\n margin-left: auto !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .px-md-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-md-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-md-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-md-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-md-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-md-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-md-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-md-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-md-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-md-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-md-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-md-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-md-0 {\n padding-top: 0 !important;\n }\n .pt-md-1 {\n padding-top: 0.25rem !important;\n }\n .pt-md-2 {\n padding-top: 0.5rem !important;\n }\n .pt-md-3 {\n padding-top: 1rem !important;\n }\n .pt-md-4 {\n padding-top: 1.5rem !important;\n }\n .pt-md-5 {\n padding-top: 3rem !important;\n }\n .pe-md-0 {\n padding-right: 0 !important;\n }\n .pe-md-1 {\n padding-right: 0.25rem !important;\n }\n .pe-md-2 {\n padding-right: 0.5rem !important;\n }\n .pe-md-3 {\n padding-right: 1rem !important;\n }\n .pe-md-4 {\n padding-right: 1.5rem !important;\n }\n .pe-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-0 {\n padding-bottom: 0 !important;\n }\n .pb-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-md-3 {\n padding-bottom: 1rem !important;\n }\n .pb-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-md-5 {\n padding-bottom: 3rem !important;\n }\n .ps-md-0 {\n padding-left: 0 !important;\n }\n .ps-md-1 {\n padding-left: 0.25rem !important;\n }\n .ps-md-2 {\n padding-left: 0.5rem !important;\n }\n .ps-md-3 {\n padding-left: 1rem !important;\n }\n .ps-md-4 {\n padding-left: 1.5rem !important;\n }\n .ps-md-5 {\n padding-left: 3rem !important;\n }\n .gap-md-0 {\n gap: 0 !important;\n }\n .gap-md-1 {\n gap: 0.25rem !important;\n }\n .gap-md-2 {\n gap: 0.5rem !important;\n }\n .gap-md-3 {\n gap: 1rem !important;\n }\n .gap-md-4 {\n gap: 1.5rem !important;\n }\n .gap-md-5 {\n gap: 3rem !important;\n }\n .row-gap-md-0 {\n row-gap: 0 !important;\n }\n .row-gap-md-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-md-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-md-3 {\n row-gap: 1rem !important;\n }\n .row-gap-md-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-md-5 {\n row-gap: 3rem !important;\n }\n .column-gap-md-0 {\n column-gap: 0 !important;\n }\n .column-gap-md-1 {\n column-gap: 0.25rem !important;\n }\n .column-gap-md-2 {\n column-gap: 0.5rem !important;\n }\n .column-gap-md-3 {\n column-gap: 1rem !important;\n }\n .column-gap-md-4 {\n column-gap: 1.5rem !important;\n }\n .column-gap-md-5 {\n column-gap: 3rem !important;\n }\n .text-md-start {\n text-align: left !important;\n }\n .text-md-end {\n text-align: right !important;\n }\n .text-md-center {\n text-align: center !important;\n }\n}\n@media (min-width: 992px) {\n .float-lg-start {\n float: left !important;\n }\n .float-lg-end {\n float: right !important;\n }\n .float-lg-none {\n float: none !important;\n }\n .object-fit-lg-contain {\n object-fit: contain !important;\n }\n .object-fit-lg-cover {\n object-fit: cover !important;\n }\n .object-fit-lg-fill {\n object-fit: fill !important;\n }\n .object-fit-lg-scale {\n object-fit: scale-down !important;\n }\n .object-fit-lg-none {\n object-fit: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-grid {\n display: grid !important;\n }\n .d-lg-inline-grid {\n display: inline-grid !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n .d-lg-none {\n display: none !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .justify-content-lg-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n .order-lg-first {\n order: -1 !important;\n }\n .order-lg-0 {\n order: 0 !important;\n }\n .order-lg-1 {\n order: 1 !important;\n }\n .order-lg-2 {\n order: 2 !important;\n }\n .order-lg-3 {\n order: 3 !important;\n }\n .order-lg-4 {\n order: 4 !important;\n }\n .order-lg-5 {\n order: 5 !important;\n }\n .order-lg-last {\n order: 6 !important;\n }\n .m-lg-0 {\n margin: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mx-lg-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-lg-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-lg-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-lg-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-lg-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-lg-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-lg-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-lg-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-lg-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-lg-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-lg-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-lg-0 {\n margin-top: 0 !important;\n }\n .mt-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mt-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mt-lg-3 {\n margin-top: 1rem !important;\n }\n .mt-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mt-lg-5 {\n margin-top: 3rem !important;\n }\n .mt-lg-auto {\n margin-top: auto !important;\n }\n .me-lg-0 {\n margin-right: 0 !important;\n }\n .me-lg-1 {\n margin-right: 0.25rem !important;\n }\n .me-lg-2 {\n margin-right: 0.5rem !important;\n }\n .me-lg-3 {\n margin-right: 1rem !important;\n }\n .me-lg-4 {\n margin-right: 1.5rem !important;\n }\n .me-lg-5 {\n margin-right: 3rem !important;\n }\n .me-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-0 {\n margin-bottom: 0 !important;\n }\n .mb-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-lg-3 {\n margin-bottom: 1rem !important;\n }\n .mb-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-lg-5 {\n margin-bottom: 3rem !important;\n }\n .mb-lg-auto {\n margin-bottom: auto !important;\n }\n .ms-lg-0 {\n margin-left: 0 !important;\n }\n .ms-lg-1 {\n margin-left: 0.25rem !important;\n }\n .ms-lg-2 {\n margin-left: 0.5rem !important;\n }\n .ms-lg-3 {\n margin-left: 1rem !important;\n }\n .ms-lg-4 {\n margin-left: 1.5rem !important;\n }\n .ms-lg-5 {\n margin-left: 3rem !important;\n }\n .ms-lg-auto {\n margin-left: auto !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .px-lg-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-lg-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-lg-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-lg-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-lg-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-lg-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-lg-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-lg-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-lg-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-lg-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-lg-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-lg-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-lg-0 {\n padding-top: 0 !important;\n }\n .pt-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pt-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pt-lg-3 {\n padding-top: 1rem !important;\n }\n .pt-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pt-lg-5 {\n padding-top: 3rem !important;\n }\n .pe-lg-0 {\n padding-right: 0 !important;\n }\n .pe-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pe-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pe-lg-3 {\n padding-right: 1rem !important;\n }\n .pe-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pe-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-0 {\n padding-bottom: 0 !important;\n }\n .pb-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pb-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-lg-5 {\n padding-bottom: 3rem !important;\n }\n .ps-lg-0 {\n padding-left: 0 !important;\n }\n .ps-lg-1 {\n padding-left: 0.25rem !important;\n }\n .ps-lg-2 {\n padding-left: 0.5rem !important;\n }\n .ps-lg-3 {\n padding-left: 1rem !important;\n }\n .ps-lg-4 {\n padding-left: 1.5rem !important;\n }\n .ps-lg-5 {\n padding-left: 3rem !important;\n }\n .gap-lg-0 {\n gap: 0 !important;\n }\n .gap-lg-1 {\n gap: 0.25rem !important;\n }\n .gap-lg-2 {\n gap: 0.5rem !important;\n }\n .gap-lg-3 {\n gap: 1rem !important;\n }\n .gap-lg-4 {\n gap: 1.5rem !important;\n }\n .gap-lg-5 {\n gap: 3rem !important;\n }\n .row-gap-lg-0 {\n row-gap: 0 !important;\n }\n .row-gap-lg-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-lg-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-lg-3 {\n row-gap: 1rem !important;\n }\n .row-gap-lg-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-lg-5 {\n row-gap: 3rem !important;\n }\n .column-gap-lg-0 {\n column-gap: 0 !important;\n }\n .column-gap-lg-1 {\n column-gap: 0.25rem !important;\n }\n .column-gap-lg-2 {\n column-gap: 0.5rem !important;\n }\n .column-gap-lg-3 {\n column-gap: 1rem !important;\n }\n .column-gap-lg-4 {\n column-gap: 1.5rem !important;\n }\n .column-gap-lg-5 {\n column-gap: 3rem !important;\n }\n .text-lg-start {\n text-align: left !important;\n }\n .text-lg-end {\n text-align: right !important;\n }\n .text-lg-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1200px) {\n .float-xl-start {\n float: left !important;\n }\n .float-xl-end {\n float: right !important;\n }\n .float-xl-none {\n float: none !important;\n }\n .object-fit-xl-contain {\n object-fit: contain !important;\n }\n .object-fit-xl-cover {\n object-fit: cover !important;\n }\n .object-fit-xl-fill {\n object-fit: fill !important;\n }\n .object-fit-xl-scale {\n object-fit: scale-down !important;\n }\n .object-fit-xl-none {\n object-fit: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-grid {\n display: grid !important;\n }\n .d-xl-inline-grid {\n display: inline-grid !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n .d-xl-none {\n display: none !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .justify-content-xl-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n .order-xl-first {\n order: -1 !important;\n }\n .order-xl-0 {\n order: 0 !important;\n }\n .order-xl-1 {\n order: 1 !important;\n }\n .order-xl-2 {\n order: 2 !important;\n }\n .order-xl-3 {\n order: 3 !important;\n }\n .order-xl-4 {\n order: 4 !important;\n }\n .order-xl-5 {\n order: 5 !important;\n }\n .order-xl-last {\n order: 6 !important;\n }\n .m-xl-0 {\n margin: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mx-xl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-xl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-xl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-xl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-xl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-xl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-xl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-xl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-xl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-xl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-xl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-xl-0 {\n margin-top: 0 !important;\n }\n .mt-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mt-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mt-xl-3 {\n margin-top: 1rem !important;\n }\n .mt-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mt-xl-5 {\n margin-top: 3rem !important;\n }\n .mt-xl-auto {\n margin-top: auto !important;\n }\n .me-xl-0 {\n margin-right: 0 !important;\n }\n .me-xl-1 {\n margin-right: 0.25rem !important;\n }\n .me-xl-2 {\n margin-right: 0.5rem !important;\n }\n .me-xl-3 {\n margin-right: 1rem !important;\n }\n .me-xl-4 {\n margin-right: 1.5rem !important;\n }\n .me-xl-5 {\n margin-right: 3rem !important;\n }\n .me-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-0 {\n margin-bottom: 0 !important;\n }\n .mb-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-xl-3 {\n margin-bottom: 1rem !important;\n }\n .mb-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-xl-5 {\n margin-bottom: 3rem !important;\n }\n .mb-xl-auto {\n margin-bottom: auto !important;\n }\n .ms-xl-0 {\n margin-left: 0 !important;\n }\n .ms-xl-1 {\n margin-left: 0.25rem !important;\n }\n .ms-xl-2 {\n margin-left: 0.5rem !important;\n }\n .ms-xl-3 {\n margin-left: 1rem !important;\n }\n .ms-xl-4 {\n margin-left: 1.5rem !important;\n }\n .ms-xl-5 {\n margin-left: 3rem !important;\n }\n .ms-xl-auto {\n margin-left: auto !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .px-xl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-xl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-xl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-xl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-xl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-xl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-xl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-xl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-xl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-xl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-xl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-xl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-xl-0 {\n padding-top: 0 !important;\n }\n .pt-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pt-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pt-xl-3 {\n padding-top: 1rem !important;\n }\n .pt-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pt-xl-5 {\n padding-top: 3rem !important;\n }\n .pe-xl-0 {\n padding-right: 0 !important;\n }\n .pe-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pe-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pe-xl-3 {\n padding-right: 1rem !important;\n }\n .pe-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pe-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-0 {\n padding-bottom: 0 !important;\n }\n .pb-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pb-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-xl-5 {\n padding-bottom: 3rem !important;\n }\n .ps-xl-0 {\n padding-left: 0 !important;\n }\n .ps-xl-1 {\n padding-left: 0.25rem !important;\n }\n .ps-xl-2 {\n padding-left: 0.5rem !important;\n }\n .ps-xl-3 {\n padding-left: 1rem !important;\n }\n .ps-xl-4 {\n padding-left: 1.5rem !important;\n }\n .ps-xl-5 {\n padding-left: 3rem !important;\n }\n .gap-xl-0 {\n gap: 0 !important;\n }\n .gap-xl-1 {\n gap: 0.25rem !important;\n }\n .gap-xl-2 {\n gap: 0.5rem !important;\n }\n .gap-xl-3 {\n gap: 1rem !important;\n }\n .gap-xl-4 {\n gap: 1.5rem !important;\n }\n .gap-xl-5 {\n gap: 3rem !important;\n }\n .row-gap-xl-0 {\n row-gap: 0 !important;\n }\n .row-gap-xl-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-xl-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-xl-3 {\n row-gap: 1rem !important;\n }\n .row-gap-xl-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-xl-5 {\n row-gap: 3rem !important;\n }\n .column-gap-xl-0 {\n column-gap: 0 !important;\n }\n .column-gap-xl-1 {\n column-gap: 0.25rem !important;\n }\n .column-gap-xl-2 {\n column-gap: 0.5rem !important;\n }\n .column-gap-xl-3 {\n column-gap: 1rem !important;\n }\n .column-gap-xl-4 {\n column-gap: 1.5rem !important;\n }\n .column-gap-xl-5 {\n column-gap: 3rem !important;\n }\n .text-xl-start {\n text-align: left !important;\n }\n .text-xl-end {\n text-align: right !important;\n }\n .text-xl-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1400px) {\n .float-xxl-start {\n float: left !important;\n }\n .float-xxl-end {\n float: right !important;\n }\n .float-xxl-none {\n float: none !important;\n }\n .object-fit-xxl-contain {\n object-fit: contain !important;\n }\n .object-fit-xxl-cover {\n object-fit: cover !important;\n }\n .object-fit-xxl-fill {\n object-fit: fill !important;\n }\n .object-fit-xxl-scale {\n object-fit: scale-down !important;\n }\n .object-fit-xxl-none {\n object-fit: none !important;\n }\n .d-xxl-inline {\n display: inline !important;\n }\n .d-xxl-inline-block {\n display: inline-block !important;\n }\n .d-xxl-block {\n display: block !important;\n }\n .d-xxl-grid {\n display: grid !important;\n }\n .d-xxl-inline-grid {\n display: inline-grid !important;\n }\n .d-xxl-table {\n display: table !important;\n }\n .d-xxl-table-row {\n display: table-row !important;\n }\n .d-xxl-table-cell {\n display: table-cell !important;\n }\n .d-xxl-flex {\n display: flex !important;\n }\n .d-xxl-inline-flex {\n display: inline-flex !important;\n }\n .d-xxl-none {\n display: none !important;\n }\n .flex-xxl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xxl-row {\n flex-direction: row !important;\n }\n .flex-xxl-column {\n flex-direction: column !important;\n }\n .flex-xxl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xxl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xxl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xxl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xxl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xxl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .flex-xxl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xxl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xxl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .justify-content-xxl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xxl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xxl-center {\n justify-content: center !important;\n }\n .justify-content-xxl-between {\n justify-content: space-between !important;\n }\n .justify-content-xxl-around {\n justify-content: space-around !important;\n }\n .justify-content-xxl-evenly {\n justify-content: space-evenly !important;\n }\n .align-items-xxl-start {\n align-items: flex-start !important;\n }\n .align-items-xxl-end {\n align-items: flex-end !important;\n }\n .align-items-xxl-center {\n align-items: center !important;\n }\n .align-items-xxl-baseline {\n align-items: baseline !important;\n }\n .align-items-xxl-stretch {\n align-items: stretch !important;\n }\n .align-content-xxl-start {\n align-content: flex-start !important;\n }\n .align-content-xxl-end {\n align-content: flex-end !important;\n }\n .align-content-xxl-center {\n align-content: center !important;\n }\n .align-content-xxl-between {\n align-content: space-between !important;\n }\n .align-content-xxl-around {\n align-content: space-around !important;\n }\n .align-content-xxl-stretch {\n align-content: stretch !important;\n }\n .align-self-xxl-auto {\n align-self: auto !important;\n }\n .align-self-xxl-start {\n align-self: flex-start !important;\n }\n .align-self-xxl-end {\n align-self: flex-end !important;\n }\n .align-self-xxl-center {\n align-self: center !important;\n }\n .align-self-xxl-baseline {\n align-self: baseline !important;\n }\n .align-self-xxl-stretch {\n align-self: stretch !important;\n }\n .order-xxl-first {\n order: -1 !important;\n }\n .order-xxl-0 {\n order: 0 !important;\n }\n .order-xxl-1 {\n order: 1 !important;\n }\n .order-xxl-2 {\n order: 2 !important;\n }\n .order-xxl-3 {\n order: 3 !important;\n }\n .order-xxl-4 {\n order: 4 !important;\n }\n .order-xxl-5 {\n order: 5 !important;\n }\n .order-xxl-last {\n order: 6 !important;\n }\n .m-xxl-0 {\n margin: 0 !important;\n }\n .m-xxl-1 {\n margin: 0.25rem !important;\n }\n .m-xxl-2 {\n margin: 0.5rem !important;\n }\n .m-xxl-3 {\n margin: 1rem !important;\n }\n .m-xxl-4 {\n margin: 1.5rem !important;\n }\n .m-xxl-5 {\n margin: 3rem !important;\n }\n .m-xxl-auto {\n margin: auto !important;\n }\n .mx-xxl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n .mx-xxl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n .mx-xxl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n .mx-xxl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n .mx-xxl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n .mx-xxl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n .mx-xxl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n .my-xxl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n .my-xxl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n .my-xxl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n .my-xxl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n .my-xxl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n .my-xxl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n .my-xxl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n .mt-xxl-0 {\n margin-top: 0 !important;\n }\n .mt-xxl-1 {\n margin-top: 0.25rem !important;\n }\n .mt-xxl-2 {\n margin-top: 0.5rem !important;\n }\n .mt-xxl-3 {\n margin-top: 1rem !important;\n }\n .mt-xxl-4 {\n margin-top: 1.5rem !important;\n }\n .mt-xxl-5 {\n margin-top: 3rem !important;\n }\n .mt-xxl-auto {\n margin-top: auto !important;\n }\n .me-xxl-0 {\n margin-right: 0 !important;\n }\n .me-xxl-1 {\n margin-right: 0.25rem !important;\n }\n .me-xxl-2 {\n margin-right: 0.5rem !important;\n }\n .me-xxl-3 {\n margin-right: 1rem !important;\n }\n .me-xxl-4 {\n margin-right: 1.5rem !important;\n }\n .me-xxl-5 {\n margin-right: 3rem !important;\n }\n .me-xxl-auto {\n margin-right: auto !important;\n }\n .mb-xxl-0 {\n margin-bottom: 0 !important;\n }\n .mb-xxl-1 {\n margin-bottom: 0.25rem !important;\n }\n .mb-xxl-2 {\n margin-bottom: 0.5rem !important;\n }\n .mb-xxl-3 {\n margin-bottom: 1rem !important;\n }\n .mb-xxl-4 {\n margin-bottom: 1.5rem !important;\n }\n .mb-xxl-5 {\n margin-bottom: 3rem !important;\n }\n .mb-xxl-auto {\n margin-bottom: auto !important;\n }\n .ms-xxl-0 {\n margin-left: 0 !important;\n }\n .ms-xxl-1 {\n margin-left: 0.25rem !important;\n }\n .ms-xxl-2 {\n margin-left: 0.5rem !important;\n }\n .ms-xxl-3 {\n margin-left: 1rem !important;\n }\n .ms-xxl-4 {\n margin-left: 1.5rem !important;\n }\n .ms-xxl-5 {\n margin-left: 3rem !important;\n }\n .ms-xxl-auto {\n margin-left: auto !important;\n }\n .p-xxl-0 {\n padding: 0 !important;\n }\n .p-xxl-1 {\n padding: 0.25rem !important;\n }\n .p-xxl-2 {\n padding: 0.5rem !important;\n }\n .p-xxl-3 {\n padding: 1rem !important;\n }\n .p-xxl-4 {\n padding: 1.5rem !important;\n }\n .p-xxl-5 {\n padding: 3rem !important;\n }\n .px-xxl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n .px-xxl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n .px-xxl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n .px-xxl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n .px-xxl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n .px-xxl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n .py-xxl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n .py-xxl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n .py-xxl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n .py-xxl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n .py-xxl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n .py-xxl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n .pt-xxl-0 {\n padding-top: 0 !important;\n }\n .pt-xxl-1 {\n padding-top: 0.25rem !important;\n }\n .pt-xxl-2 {\n padding-top: 0.5rem !important;\n }\n .pt-xxl-3 {\n padding-top: 1rem !important;\n }\n .pt-xxl-4 {\n padding-top: 1.5rem !important;\n }\n .pt-xxl-5 {\n padding-top: 3rem !important;\n }\n .pe-xxl-0 {\n padding-right: 0 !important;\n }\n .pe-xxl-1 {\n padding-right: 0.25rem !important;\n }\n .pe-xxl-2 {\n padding-right: 0.5rem !important;\n }\n .pe-xxl-3 {\n padding-right: 1rem !important;\n }\n .pe-xxl-4 {\n padding-right: 1.5rem !important;\n }\n .pe-xxl-5 {\n padding-right: 3rem !important;\n }\n .pb-xxl-0 {\n padding-bottom: 0 !important;\n }\n .pb-xxl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pb-xxl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pb-xxl-3 {\n padding-bottom: 1rem !important;\n }\n .pb-xxl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pb-xxl-5 {\n padding-bottom: 3rem !important;\n }\n .ps-xxl-0 {\n padding-left: 0 !important;\n }\n .ps-xxl-1 {\n padding-left: 0.25rem !important;\n }\n .ps-xxl-2 {\n padding-left: 0.5rem !important;\n }\n .ps-xxl-3 {\n padding-left: 1rem !important;\n }\n .ps-xxl-4 {\n padding-left: 1.5rem !important;\n }\n .ps-xxl-5 {\n padding-left: 3rem !important;\n }\n .gap-xxl-0 {\n gap: 0 !important;\n }\n .gap-xxl-1 {\n gap: 0.25rem !important;\n }\n .gap-xxl-2 {\n gap: 0.5rem !important;\n }\n .gap-xxl-3 {\n gap: 1rem !important;\n }\n .gap-xxl-4 {\n gap: 1.5rem !important;\n }\n .gap-xxl-5 {\n gap: 3rem !important;\n }\n .row-gap-xxl-0 {\n row-gap: 0 !important;\n }\n .row-gap-xxl-1 {\n row-gap: 0.25rem !important;\n }\n .row-gap-xxl-2 {\n row-gap: 0.5rem !important;\n }\n .row-gap-xxl-3 {\n row-gap: 1rem !important;\n }\n .row-gap-xxl-4 {\n row-gap: 1.5rem !important;\n }\n .row-gap-xxl-5 {\n row-gap: 3rem !important;\n }\n .column-gap-xxl-0 {\n column-gap: 0 !important;\n }\n .column-gap-xxl-1 {\n column-gap: 0.25rem !important;\n }\n .column-gap-xxl-2 {\n column-gap: 0.5rem !important;\n }\n .column-gap-xxl-3 {\n column-gap: 1rem !important;\n }\n .column-gap-xxl-4 {\n column-gap: 1.5rem !important;\n }\n .column-gap-xxl-5 {\n column-gap: 3rem !important;\n }\n .text-xxl-start {\n text-align: left !important;\n }\n .text-xxl-end {\n text-align: right !important;\n }\n .text-xxl-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1200px) {\n .fs-1 {\n font-size: 2.5rem !important;\n }\n .fs-2 {\n font-size: 2rem !important;\n }\n .fs-3 {\n font-size: 1.75rem !important;\n }\n .fs-4 {\n font-size: 1.5rem !important;\n }\n}\n@media print {\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-grid {\n display: grid !important;\n }\n .d-print-inline-grid {\n display: inline-grid !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n .d-print-none {\n display: none !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap.css.map */\n","@mixin bsBanner($file) {\n /*!\n * Bootstrap #{$file} v5.3.8 (https://getbootstrap.com/)\n * Copyright 2011-2025 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n}\n",":root,\n[data-bs-theme=\"light\"] {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$prefix}#{$color}-rgb: #{$value};\n }\n\n @each $color, $value in $theme-colors-text {\n --#{$prefix}#{$color}-text-emphasis: #{$value};\n }\n\n @each $color, $value in $theme-colors-bg-subtle {\n --#{$prefix}#{$color}-bg-subtle: #{$value};\n }\n\n @each $color, $value in $theme-colors-border-subtle {\n --#{$prefix}#{$color}-border-subtle: #{$value};\n }\n\n --#{$prefix}white-rgb: #{to-rgb($white)};\n --#{$prefix}black-rgb: #{to-rgb($black)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$prefix}gradient: #{$gradient};\n\n // Root and body\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$prefix}root-font-size: #{$font-size-root};\n }\n --#{$prefix}body-font-family: #{inspect($font-family-base)};\n @include rfs($font-size-base, --#{$prefix}body-font-size);\n --#{$prefix}body-font-weight: #{$font-weight-base};\n --#{$prefix}body-line-height: #{$line-height-base};\n @if $body-text-align != null {\n --#{$prefix}body-text-align: #{$body-text-align};\n }\n\n --#{$prefix}body-color: #{$body-color};\n --#{$prefix}body-color-rgb: #{to-rgb($body-color)};\n --#{$prefix}body-bg: #{$body-bg};\n --#{$prefix}body-bg-rgb: #{to-rgb($body-bg)};\n\n --#{$prefix}emphasis-color: #{$body-emphasis-color};\n --#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color)};\n\n --#{$prefix}secondary-color: #{$body-secondary-color};\n --#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color)};\n --#{$prefix}secondary-bg: #{$body-secondary-bg};\n --#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg)};\n\n --#{$prefix}tertiary-color: #{$body-tertiary-color};\n --#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color)};\n --#{$prefix}tertiary-bg: #{$body-tertiary-bg};\n --#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg)};\n // scss-docs-end root-body-variables\n\n --#{$prefix}heading-color: #{$headings-color};\n\n --#{$prefix}link-color: #{$link-color};\n --#{$prefix}link-color-rgb: #{to-rgb($link-color)};\n --#{$prefix}link-decoration: #{$link-decoration};\n\n --#{$prefix}link-hover-color: #{$link-hover-color};\n --#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color)};\n\n @if $link-hover-decoration != null {\n --#{$prefix}link-hover-decoration: #{$link-hover-decoration};\n }\n\n --#{$prefix}code-color: #{$code-color};\n --#{$prefix}highlight-color: #{$mark-color};\n --#{$prefix}highlight-bg: #{$mark-bg};\n\n // scss-docs-start root-border-var\n --#{$prefix}border-width: #{$border-width};\n --#{$prefix}border-style: #{$border-style};\n --#{$prefix}border-color: #{$border-color};\n --#{$prefix}border-color-translucent: #{$border-color-translucent};\n\n --#{$prefix}border-radius: #{$border-radius};\n --#{$prefix}border-radius-sm: #{$border-radius-sm};\n --#{$prefix}border-radius-lg: #{$border-radius-lg};\n --#{$prefix}border-radius-xl: #{$border-radius-xl};\n --#{$prefix}border-radius-xxl: #{$border-radius-xxl};\n --#{$prefix}border-radius-2xl: var(--#{$prefix}border-radius-xxl); // Deprecated in v5.3.0 for consistency\n --#{$prefix}border-radius-pill: #{$border-radius-pill};\n // scss-docs-end root-border-var\n\n --#{$prefix}box-shadow: #{$box-shadow};\n --#{$prefix}box-shadow-sm: #{$box-shadow-sm};\n --#{$prefix}box-shadow-lg: #{$box-shadow-lg};\n --#{$prefix}box-shadow-inset: #{$box-shadow-inset};\n\n // Focus styles\n // scss-docs-start root-focus-variables\n --#{$prefix}focus-ring-width: #{$focus-ring-width};\n --#{$prefix}focus-ring-opacity: #{$focus-ring-opacity};\n --#{$prefix}focus-ring-color: #{$focus-ring-color};\n // scss-docs-end root-focus-variables\n\n // scss-docs-start root-form-validation-variables\n --#{$prefix}form-valid-color: #{$form-valid-color};\n --#{$prefix}form-valid-border-color: #{$form-valid-border-color};\n --#{$prefix}form-invalid-color: #{$form-invalid-color};\n --#{$prefix}form-invalid-border-color: #{$form-invalid-border-color};\n // scss-docs-end root-form-validation-variables\n}\n\n@if $enable-dark-mode {\n @include color-mode(dark, true) {\n color-scheme: dark;\n\n // scss-docs-start root-dark-mode-vars\n --#{$prefix}body-color: #{$body-color-dark};\n --#{$prefix}body-color-rgb: #{to-rgb($body-color-dark)};\n --#{$prefix}body-bg: #{$body-bg-dark};\n --#{$prefix}body-bg-rgb: #{to-rgb($body-bg-dark)};\n\n --#{$prefix}emphasis-color: #{$body-emphasis-color-dark};\n --#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color-dark)};\n\n --#{$prefix}secondary-color: #{$body-secondary-color-dark};\n --#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color-dark)};\n --#{$prefix}secondary-bg: #{$body-secondary-bg-dark};\n --#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg-dark)};\n\n --#{$prefix}tertiary-color: #{$body-tertiary-color-dark};\n --#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color-dark)};\n --#{$prefix}tertiary-bg: #{$body-tertiary-bg-dark};\n --#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg-dark)};\n\n @each $color, $value in $theme-colors-text-dark {\n --#{$prefix}#{$color}-text-emphasis: #{$value};\n }\n\n @each $color, $value in $theme-colors-bg-subtle-dark {\n --#{$prefix}#{$color}-bg-subtle: #{$value};\n }\n\n @each $color, $value in $theme-colors-border-subtle-dark {\n --#{$prefix}#{$color}-border-subtle: #{$value};\n }\n\n --#{$prefix}heading-color: #{$headings-color-dark};\n\n --#{$prefix}link-color: #{$link-color-dark};\n --#{$prefix}link-hover-color: #{$link-hover-color-dark};\n --#{$prefix}link-color-rgb: #{to-rgb($link-color-dark)};\n --#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color-dark)};\n\n --#{$prefix}code-color: #{$code-color-dark};\n --#{$prefix}highlight-color: #{$mark-color-dark};\n --#{$prefix}highlight-bg: #{$mark-bg-dark};\n\n --#{$prefix}border-color: #{$border-color-dark};\n --#{$prefix}border-color-translucent: #{$border-color-translucent-dark};\n\n --#{$prefix}form-valid-color: #{$form-valid-color-dark};\n --#{$prefix}form-valid-border-color: #{$form-valid-border-color-dark};\n --#{$prefix}form-invalid-color: #{$form-invalid-color-dark};\n --#{$prefix}form-invalid-border-color: #{$form-invalid-border-color-dark};\n // scss-docs-end root-dark-mode-vars\n }\n}\n","// stylelint-disable scss/dimension-no-non-numeric-values\n\n// SCSS RFS mixin\n//\n// Automated responsive values for font sizes, paddings, margins and much more\n//\n// Licensed under MIT (https://github.com/twbs/rfs/blob/main/LICENSE)\n\n// Configuration\n\n// Base value\n$rfs-base-value: 1.25rem !default;\n$rfs-unit: rem !default;\n\n@if $rfs-unit != rem and $rfs-unit != px {\n @error \"`#{$rfs-unit}` is not a valid unit for $rfs-unit. Use `px` or `rem`.\";\n}\n\n// Breakpoint at where values start decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n@if $rfs-breakpoint-unit != px and $rfs-breakpoint-unit != em and $rfs-breakpoint-unit != rem {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n}\n\n// Resize values based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != number or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Mode. Possibilities: \"min-media-query\", \"max-media-query\"\n$rfs-mode: min-media-query !default;\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-rfs to false\n$enable-rfs: true !default;\n\n// Cache $rfs-base-value unit\n$rfs-base-value-unit: unit($rfs-base-value);\n\n@function divide($dividend, $divisor, $precision: 10) {\n $sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1);\n $dividend: abs($dividend);\n $divisor: abs($divisor);\n @if $dividend == 0 {\n @return 0;\n }\n @if $divisor == 0 {\n @error \"Cannot divide by 0\";\n }\n $remainder: $dividend;\n $result: 0;\n $factor: 10;\n @while ($remainder > 0 and $precision >= 0) {\n $quotient: 0;\n @while ($remainder >= $divisor) {\n $remainder: $remainder - $divisor;\n $quotient: $quotient + 1;\n }\n $result: $result * 10 + $quotient;\n $factor: $factor * .1;\n $remainder: $remainder * 10;\n $precision: $precision - 1;\n @if ($precision < 0 and $remainder >= $divisor * 5) {\n $result: $result + 1;\n }\n }\n $result: $result * $factor * $sign;\n $dividend-unit: unit($dividend);\n $divisor-unit: unit($divisor);\n $unit-map: (\n \"px\": 1px,\n \"rem\": 1rem,\n \"em\": 1em,\n \"%\": 1%\n );\n @if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) {\n $result: $result * map-get($unit-map, $dividend-unit);\n }\n @return $result;\n}\n\n// Remove px-unit from $rfs-base-value for calculations\n@if $rfs-base-value-unit == px {\n $rfs-base-value: divide($rfs-base-value, $rfs-base-value * 0 + 1);\n}\n@else if $rfs-base-value-unit == rem {\n $rfs-base-value: divide($rfs-base-value, divide($rfs-base-value * 0 + 1, $rfs-rem-value));\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == px {\n $rfs-breakpoint: divide($rfs-breakpoint, $rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == rem or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: divide($rfs-breakpoint, divide($rfs-breakpoint * 0 + 1, $rfs-rem-value));\n}\n\n// Calculate the media query value\n$rfs-mq-value: if($rfs-breakpoint-unit == px, #{$rfs-breakpoint}px, #{divide($rfs-breakpoint, $rfs-rem-value)}#{$rfs-breakpoint-unit});\n$rfs-mq-property-width: if($rfs-mode == max-media-query, max-width, min-width);\n$rfs-mq-property-height: if($rfs-mode == max-media-query, max-height, min-height);\n\n// Internal mixin used to determine which media query needs to be used\n@mixin _rfs-media-query {\n @if $rfs-two-dimensional {\n @if $rfs-mode == max-media-query {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}), (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) and (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) {\n @content;\n }\n }\n}\n\n// Internal mixin that adds disable classes to the selector if needed.\n@mixin _rfs-rule {\n @if $rfs-class == disable and $rfs-mode == max-media-query {\n // Adding an extra class increases specificity, which prevents the media query to override the property\n &,\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @else if $rfs-class == enable and $rfs-mode == min-media-query {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Internal mixin that adds enable classes to the selector if needed.\n@mixin _rfs-media-query-rule {\n\n @if $rfs-class == enable {\n @if $rfs-mode == min-media-query {\n @content;\n }\n\n @include _rfs-media-query () {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n }\n }\n @else {\n @if $rfs-class == disable and $rfs-mode == min-media-query {\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @include _rfs-media-query () {\n @content;\n }\n }\n}\n\n// Helper function to get the formatted non-responsive value\n@function rfs-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: \"\";\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + \" 0\";\n }\n @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n @if $unit == px {\n // Convert to rem if needed\n $val: $val + \" \" + if($rfs-unit == rem, #{divide($value, $value * 0 + $rfs-rem-value)}rem, $value);\n }\n @else if $unit == rem {\n // Convert to px if needed\n $val: $val + \" \" + if($rfs-unit == px, #{divide($value, $value * 0 + 1) * $rfs-rem-value}px, $value);\n } @else {\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n $val: $val + \" \" + $value;\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// Helper function to get the responsive value calculated by RFS\n@function rfs-fluid-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: \"\";\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + \" 0\";\n } @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $unit or $unit != px and $unit != rem {\n $val: $val + \" \" + $value;\n } @else {\n // Remove unit from $value for calculations\n $value: divide($value, $value * 0 + if($unit == px, 1, divide(1, $rfs-rem-value)));\n\n // Only add the media query if the value is greater than the minimum value\n @if abs($value) <= $rfs-base-value or not $enable-rfs {\n $val: $val + \" \" + if($rfs-unit == rem, #{divide($value, $rfs-rem-value)}rem, #{$value}px);\n }\n @else {\n // Calculate the minimum value\n $value-min: $rfs-base-value + divide(abs($value) - $rfs-base-value, $rfs-factor);\n\n // Calculate difference between $value and the minimum value\n $value-diff: abs($value) - $value-min;\n\n // Base value formatting\n $min-width: if($rfs-unit == rem, #{divide($value-min, $rfs-rem-value)}rem, #{$value-min}px);\n\n // Use negative value if needed\n $min-width: if($value < 0, -$min-width, $min-width);\n\n // Use `vmin` if two-dimensional is enabled\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{divide($value-diff * 100, $rfs-breakpoint)}#{$variable-unit};\n\n // Return the calculated value\n $val: $val + \" calc(\" + $min-width + if($value < 0, \" - \", \" + \") + $variable-width + \")\";\n }\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// RFS mixin\n@mixin rfs($values, $property: font-size) {\n @if $values != null {\n $val: rfs-value($values);\n $fluid-val: rfs-fluid-value($values);\n\n // Do not print the media query if responsive & non-responsive values are the same\n @if $val == $fluid-val {\n #{$property}: $val;\n }\n @else {\n @include _rfs-rule () {\n #{$property}: if($rfs-mode == max-media-query, $val, $fluid-val);\n\n // Include safari iframe resize fix if needed\n min-width: if($rfs-safari-iframe-resize-bug-fix, (0 * 1vw), null);\n }\n\n @include _rfs-media-query-rule () {\n #{$property}: if($rfs-mode == max-media-query, $fluid-val, $val);\n }\n }\n }\n}\n\n// Shorthand helper mixins\n@mixin font-size($value) {\n @include rfs($value);\n}\n\n@mixin padding($value) {\n @include rfs($value, padding);\n}\n\n@mixin padding-top($value) {\n @include rfs($value, padding-top);\n}\n\n@mixin padding-right($value) {\n @include rfs($value, padding-right);\n}\n\n@mixin padding-bottom($value) {\n @include rfs($value, padding-bottom);\n}\n\n@mixin padding-left($value) {\n @include rfs($value, padding-left);\n}\n\n@mixin margin($value) {\n @include rfs($value, margin);\n}\n\n@mixin margin-top($value) {\n @include rfs($value, margin-top);\n}\n\n@mixin margin-right($value) {\n @include rfs($value, margin-right);\n}\n\n@mixin margin-bottom($value) {\n @include rfs($value, margin-bottom);\n}\n\n@mixin margin-left($value) {\n @include rfs($value, margin-left);\n}\n","// scss-docs-start color-mode-mixin\n@mixin color-mode($mode: light, $root: false) {\n @if $color-mode-type == \"media-query\" {\n @if $root == true {\n @media (prefers-color-scheme: $mode) {\n :root {\n @content;\n }\n }\n } @else {\n @media (prefers-color-scheme: $mode) {\n @content;\n }\n }\n } @else {\n [data-bs-theme=\"#{$mode}\"] {\n @content;\n }\n }\n}\n// scss-docs-end color-mode-mixin\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n @include font-size(var(--#{$prefix}root-font-size));\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$prefix}body-font-family);\n @include font-size(var(--#{$prefix}body-font-size));\n font-weight: var(--#{$prefix}body-font-weight);\n line-height: var(--#{$prefix}body-line-height);\n color: var(--#{$prefix}body-color);\n text-align: var(--#{$prefix}body-text-align);\n background-color: var(--#{$prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n opacity: $hr-opacity;\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: var(--#{$prefix}heading-color);\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 2. Add explicit cursor to indicate changed behavior.\n// 3. Prevent the text-decoration to be skipped.\n\nabbr[title] {\n text-decoration: underline dotted; // 1\n cursor: help; // 2\n text-decoration-skip-ink: none; // 3\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n color: var(--#{$prefix}highlight-color);\n background-color: var(--#{$prefix}highlight-bg);\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-opacity, 1));\n text-decoration: $link-decoration;\n\n &:hover {\n --#{$prefix}link-color-rgb: var(--#{$prefix}link-hover-color-rgb);\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: var(--#{$prefix}code-color);\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-` +

+ + + + +<% if app.github_repo? %> +
+

+ <%= t(".repository") %> + + +

+ + +
+<% end %> + +<% if app.problems.any? %> +

<%= t(".errors") %>

+ +
+ <%= render "problems/search", all_errs: all_errs, app_id: app.id, params_sort: params_sort, params_order: params_order %> +
+ +
+ +
+
+ <%= render "problems/table", problems: problems %> +
+
+<% else %> +

<%= t(".no_error_yet") %>

+ + <%= render "configuration_instructions_wrapper", app: app %> +<% end %> diff --git a/app/views/apps/show.html.haml b/app/views/apps/show.html.haml deleted file mode 100644 index ba537977e3..0000000000 --- a/app/views/apps/show.html.haml +++ /dev/null @@ -1,72 +0,0 @@ -- content_for :title, app.name -- content_for :head do - = auto_discovery_link_tag :atom, app_path(app, User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => t('.atom_title', :name => app.name, :host => request.host) -- content_for :meta do - - if app.current_app_version.present? - %strong="Latest App Version:" - = app.current_app_version - %strong=t('.errors_caught') - = app.problems.count - %strong=t('.api_key') - = app.api_key -- content_for :action_bar do - - if current_user.admin? - = link_to t('.edit'), edit_app_path(app), :class => 'button' - - if all_errs - = link_to t('.unresolved_errs'), app_path(app, sort: params_sort, order: params_order), :class => 'button' - - else - = link_to t('.all_errs'), app_path(app, all_errs: true, sort: params_sort, order: params_order), :class => 'button' - - - if app.watched_by?(current_user) - = link_to t('.unwatch'), app_watcher_path(app_id: app, id: current_user.id), method: :delete, class: 'button' - - else - = link_to t('.watch'), app_watcher_path(app_id: app, id: current_user.id), method: :put, class: 'button' - -%h3#watchers_toggle - =t('.watchers') - %span.click_span=t('.show_hide') -#watchers_div - - if app.notify_all_users - %table.watchers - %thead - %tr - %th=t('.all_users_notified') - - else - %table.watchers - %thead - %tr - %th=t('.user_or_email') - %tbody - - app.watchers.each do |watcher| - %tr - %td= watcher.label - - if app.watchers.none? - %tr - %td - %em= t('.no_watcher') - -- if app.github_repo? - %h3#repository_toggle - =t('.repository') - %span.click_span=t('.show_hide') - #repository_div - %table.repository - %thead - %tr - %th=t('.github_repo') - %tbody - %tr - %td= link_to(app.github_repo, app.github_url, :target => '_blank') - -- if app.problems.any? - %h3.clear=t('.errors') - %section - = render 'problems/search', :all_errs => all_errs, :app_id => app.id - %br - %section - .problem_table{:id => 'problem_table'} - = render 'problems/table', :problems => problems -- else - %h3.clear=t('.no_error_yet') - = render 'configuration_instructions', :app => app - diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 0000000000..75124d7537 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,29 @@ + + + + + + + + + + + + +
+
+

Hello,

+ +

We hear you'd like to change your password. You can do that by visiting the link below:

+ +

<%= edit_password_url @resource, reset_password_token: @token %>

+
+
+
+

If you didn't request this, please ignore this email.

+ +

Your password won't change unless you click the link above and set a new one.

+
+
+ + diff --git a/app/views/devise/mailer/reset_password_instructions.html.haml b/app/views/devise/mailer/reset_password_instructions.html.haml deleted file mode 100644 index afa7eeecfd..0000000000 --- a/app/views/devise/mailer/reset_password_instructions.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -%tr - %td.section - %table(cellpadding="0" cellspacing="0" border="0" align="left") - %tbody - %tr - %td.content(valign="top") - %div - %p - Hello, - %p - We hear you'd like to change your password. You can do that by visiting the link below: - %p - = edit_password_url @resource, :reset_password_token => @token - %tr - %td.content(valign="top") - %div - %p - If you didn't request this, please ignore this email. - %p - Your password won't change unless you click the link above and set a new one. - diff --git a/app/views/devise/mailer/reset_password_instructions.text.erb b/app/views/devise/mailer/reset_password_instructions.text.erb index 4679e51f54..e1b0506d32 100644 --- a/app/views/devise/mailer/reset_password_instructions.text.erb +++ b/app/views/devise/mailer/reset_password_instructions.text.erb @@ -2,7 +2,7 @@ Hello, We hear you'd like to change your password. You can do that by visiting the link below: - <%= edit_password_url(@resource, :reset_password_token => @token) %> + <%= edit_password_url(@resource, reset_password_token: @token) %> If you didn't request this, please ignore this email. Your password won't change unless you access the link above and create a new one. diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb new file mode 100644 index 0000000000..a23252b4f5 --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,23 @@ +<% content_for :title, "Change your password" %> + +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :patch }) do |f| %> + <%= errors_for resource %> + + <%= f.hidden_field :reset_password_token %> + +
+ <%= f.label :password, "New password" %> + + <%= f.password_field :password, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation, "Type your new password again" %> + + <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.submit "Change my password" %> +
+<% end %> diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml deleted file mode 100644 index b409a61c66..0000000000 --- a/app/views/devise/passwords/edit.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- content_for :title, "Change your password" - -= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put }) do |f| - = errors_for resource - = f.hidden_field :reset_password_token - .required - = f.label :password, 'New password' - = f.password_field :password - .required - = f.label :password_confirmation, 'Type your new password again' - = f.password_field :password_confirmation - %div.buttons= f.submit "Change my password" - diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb new file mode 100644 index 0000000000..34581f7170 --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,15 @@ +<% content_for :title, "Forgot your password?" %> + +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: {method: :post}) do |f| %> + <%= errors_for resource %> + +
+ <%= f.label :email %> + + <%= f.text_field :email, value: params[:email], autocomplete: "email" %> +
+ +
+ <%= f.submit "Send me reset password instructions" %> +
+<% end %> diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml deleted file mode 100644 index 6a7544aac8..0000000000 --- a/app/views/devise/passwords/new.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- content_for :title, "Forgot your password?" -= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post }) do |f| - = errors_for resource - .required - = f.label :email - = f.text_field :email, :value => params[:email] - - %div.buttons= f.submit "Send me reset password instructions" - diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb new file mode 100644 index 0000000000..34a5a3701c --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1,52 @@ +<% content_for :title, "Sign in" %> + +<% auth_key = Devise.authentication_keys.first %> +<% auth_key_autocomplete = Errbit::Config.user_has_username ? "username" : "email" %> + +<% if Errbit::Config.github_authentication || Errbit::Config.google_authentication %> + <% content_for :action_bar do %> +
+ <% if Errbit::Config.github_authentication %> + + <%= link_to "Sign in with #{Errbit::Config.github_site_title}", user_github_omniauth_authorize_path, method: :post %> + + <% end %> + + <% if Errbit::Config.google_authentication %> + + <%= link_to "Sign in with #{Errbit::Config.google_site_title}", user_google_oauth2_omniauth_authorize_path, method: :post %> + + <% end %> +
+ <% end %> +<% end %> + +<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> +
+ <%= f.label auth_key %> + + <%= f.text_field auth_key, type: (Errbit::Config.user_has_username ? "text" : "email"), autocomplete: auth_key_autocomplete, tabindex: 1, autofocus: true %> +
+ +
+ <%= link_to "forgot it?", new_password_path(resource_name), class: "float-right", id: "forgot_password" %> + + <%= f.label :password %> + + <%= f.password_field :password, autocomplete: "current-password", tabindex: 2 %> +
+ + <% if devise_mapping.rememberable? %> +
+ <%= f.check_box :remember_me, tabindex: 3 %> + + <%= f.label :remember_me %> +
+ <% end %> + +
+ +
+<% end %> diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml deleted file mode 100644 index 4de68247b6..0000000000 --- a/app/views/devise/sessions/new.html.haml +++ /dev/null @@ -1,36 +0,0 @@ -- content_for :title, 'Sign in' -- auth_key = Devise.authentication_keys.first - -- if Errbit::Config.github_authentication || Errbit::Config.google_authentication - - content_for :action_bar do - %div.action-bar - - if Errbit::Config.github_authentication - %span.github= link_to "Sign in with #{Errbit::Config.github_site_title}", user_github_omniauth_authorize_path - - if Errbit::Config.google_authentication - %span.google= link_to "Sign in with #{Errbit::Config.google_site_title}", user_google_oauth2_omniauth_authorize_path - -= form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| - .required - = f.label auth_key - = f.text_field auth_key, :type => (Errbit::Config.user_has_username ? 'text' : 'email'), :tabindex => 1, :autofocus => true - - .required - = link_to 'forgot it?', new_password_path(resource_name), :class => 'float-right', :id => "forgot_password" - = f.label :password - = f.password_field :password, :tabindex => 2 - - - if devise_mapping.rememberable? - %div.checkbox - = f.check_box :remember_me, :tabindex => 3 - = f.label :remember_me - - %div.buttons - %button{:type => 'submit', :class => 'sign_in'}=t('.sign_in') - -:javascript - $('a#forgot_password').click(function(){ - // Set email field on password reset page to email that user entered on this page - location.href = $(this).attr('href') + "?email=" + $('#user_#{auth_key}').val(); - return false; - }); - diff --git a/app/views/issue_trackers/issue.txt.erb b/app/views/issue_trackers/issue.txt.erb deleted file mode 100644 index ca28aa7f35..0000000000 --- a/app/views/issue_trackers/issue.txt.erb +++ /dev/null @@ -1,21 +0,0 @@ -Errbit link: <%= app_problem_url problem.app, problem %> -<% if notice = problem.notices.first %> -<% notice.message %> - -Summary -------- -<% if notice.request['url'].present? %> -URL: <%= notice.request['url'] %> -<% end %> -Where: <%= notice.where %> -Occured: <%= notice.created_at.to_s(:micro) %> -Similar: <%= (notice.problem.notices_count - 1).to_s %> -Params: <%= pretty_hash notice.params %> -Session: <%= pretty_hash notice.session %> -Env: <%= pretty_hash notice.env_vars %> - -Backtrace ---------- -<% notice.backtrace.lines.each do |line| %><%= sprintf('%5d: %s **%s', line.number, line.file_relative, line.method) %> -<% end %> -<% end %> diff --git a/app/views/issue_trackers/issue.md.erb b/app/views/issue_trackers/markdown.erb similarity index 82% rename from app/views/issue_trackers/issue.md.erb rename to app/views/issue_trackers/markdown.erb index 6b39c6183e..cc4f370687 100644 --- a/app/views/issue_trackers/issue.md.erb +++ b/app/views/issue_trackers/markdown.erb @@ -2,15 +2,15 @@ <% if notice = problem.notices.first %> # <%= notice.message %> # ## Summary ## -<% if notice.request['url'].present? %> +<% if notice.request["url"].present? %> ### URL ### -[<%= notice.request['url'] %>](<%= notice.request['url'] %>)" +[<%= notice.request["url"] %>](<%= notice.request["url"] %>) <% end %> ### Where ### <%= notice.where %> -### Occured ### -<%= notice.created_at.to_s(:micro) %> +### Occurred ### +<%= notice.created_at.to_fs(:micro) %> ### Similar ### <%= (notice.problem.notices_count - 1).to_s %> diff --git a/app/views/issue_trackers/text.erb b/app/views/issue_trackers/text.erb new file mode 100644 index 0000000000..ce47de5ca0 --- /dev/null +++ b/app/views/issue_trackers/text.erb @@ -0,0 +1,21 @@ +Errbit link: <%= app_problem_url problem.app, problem %> +<% if notice = problem.notices.first %> +<% notice.message %> + +Summary +------- +<% if notice.request["url"].present? %> +URL: <%= notice.request["url"] %> +<% end %> +Where: <%= notice.where %> +Occurred: <%= notice.created_at.to_fs(:micro) %> +Similar: <%= (notice.problem.notices_count - 1).to_s %> +Params: <%= pretty_hash notice.params %> +Session: <%= pretty_hash notice.session %> +Env: <%= pretty_hash notice.env_vars %> + +Backtrace +--------- +<% notice.backtrace.lines.each do |line| %><%= sprintf("%5d: %s **%s", line.number, line.file_relative, line.method) %> +<% end %> +<% end %> diff --git a/app/views/kaminari/_first_page.html.erb b/app/views/kaminari/_first_page.html.erb new file mode 100644 index 0000000000..b40a23dcb6 --- /dev/null +++ b/app/views/kaminari/_first_page.html.erb @@ -0,0 +1,11 @@ +<%# Link to the "First" page + - available local variables + url: url to the first page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> + + <%= link_to_unless current_page.first?, t("views.pagination.first").html_safe, url, remote: remote %> + diff --git a/app/views/kaminari/_first_page.html.haml b/app/views/kaminari/_first_page.html.haml deleted file mode 100644 index 09ca22527b..0000000000 --- a/app/views/kaminari/_first_page.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Link to the "First" page --# available local variables --# url: url to the first page --# current_page: a page object for the currently displayed page --# total_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%span.first - = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote diff --git a/app/views/kaminari/_gap.html.erb b/app/views/kaminari/_gap.html.erb new file mode 100644 index 0000000000..9ed07ace0a --- /dev/null +++ b/app/views/kaminari/_gap.html.erb @@ -0,0 +1,8 @@ +<%# Non-link tag that stands for skipped pages... + - available local variables + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +<%= t("views.pagination.truncate").html_safe %> diff --git a/app/views/kaminari/_gap.html.haml b/app/views/kaminari/_gap.html.haml deleted file mode 100644 index 40c954b2be..0000000000 --- a/app/views/kaminari/_gap.html.haml +++ /dev/null @@ -1,8 +0,0 @@ --# Non-link tag that stands for skipped pages... --# available local variables --# current_page: a page object for the currently displayed page --# total_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%span.page.gap - = raw(t 'views.pagination.truncate') diff --git a/app/views/kaminari/_last_page.html.erb b/app/views/kaminari/_last_page.html.erb new file mode 100644 index 0000000000..7cdc9a4759 --- /dev/null +++ b/app/views/kaminari/_last_page.html.erb @@ -0,0 +1,11 @@ +<%# Link to the "Last" page + - available local variables + url: url to the last page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> + + <%= link_to_unless current_page.last?, t("views.pagination.last").html_safe, url, remote: remote %> + diff --git a/app/views/kaminari/_last_page.html.haml b/app/views/kaminari/_last_page.html.haml deleted file mode 100644 index c4b449237c..0000000000 --- a/app/views/kaminari/_last_page.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Link to the "Last" page --# available local variables --# url: url to the last page --# current_page: a page object for the currently displayed page --# total_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%span.last - = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote} diff --git a/app/views/kaminari/_next_page.html.erb b/app/views/kaminari/_next_page.html.erb new file mode 100644 index 0000000000..c87d1fce50 --- /dev/null +++ b/app/views/kaminari/_next_page.html.erb @@ -0,0 +1,11 @@ +<%# Link to the "Next" page + - available local variables + url: url to the next page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> + + <%= link_to_unless current_page.last?, t("views.pagination.next").html_safe, url, rel: "next", remote: remote %> + diff --git a/app/views/kaminari/_next_page.html.haml b/app/views/kaminari/_next_page.html.haml deleted file mode 100644 index e8eb955d5c..0000000000 --- a/app/views/kaminari/_next_page.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Link to the "Next" page --# available local variables --# url: url to the next page --# current_page: a page object for the currently displayed page --# total_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%span.next - = link_to_unless current_page.last?, raw('Next »'), url, :rel => 'next' diff --git a/app/views/kaminari/_page.html.erb b/app/views/kaminari/_page.html.erb new file mode 100644 index 0000000000..0d4d131254 --- /dev/null +++ b/app/views/kaminari/_page.html.erb @@ -0,0 +1,12 @@ +<%# Link showing page number + - available local variables + page: a page object for "this" page + url: url to this page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +"> + <%= link_to_unless page.current?, page, url, {remote: remote, rel: page.rel} %> + diff --git a/app/views/kaminari/_page.html.haml b/app/views/kaminari/_page.html.haml deleted file mode 100644 index 08cc5350bb..0000000000 --- a/app/views/kaminari/_page.html.haml +++ /dev/null @@ -1,10 +0,0 @@ --# Link showing page number --# available local variables --# page: a page object for "this" page --# url: url to this page --# current_page: a page object for the currently displayed page --# total_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%span{:class => "page#{' current' if page.current?}"} - = link_to_unless page.current?, page, url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil} diff --git a/app/views/kaminari/_paginator.html.erb b/app/views/kaminari/_paginator.html.erb new file mode 100644 index 0000000000..5525f48f4a --- /dev/null +++ b/app/views/kaminari/_paginator.html.erb @@ -0,0 +1,25 @@ +<%# The container tag + - available local variables + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote + paginator: the paginator that renders the pagination tags inside +-%> +<%= paginator.render do -%> + +<% end -%> diff --git a/app/views/kaminari/_paginator.html.haml b/app/views/kaminari/_paginator.html.haml deleted file mode 100644 index 65d872fbd1..0000000000 --- a/app/views/kaminari/_paginator.html.haml +++ /dev/null @@ -1,16 +0,0 @@ --# The container tag --# available local variables --# current_page: a page object for the currently displayed page --# total_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote --# paginator: the paginator that renders the pagination tags inside -= paginator.render do - %nav.pagination - = prev_page_tag - - each_page do |page| - - if page.left_outer? || page.right_outer? || page.inside_window? - = page_tag page - - elsif !page.was_truncated? - = gap_tag - = next_page_tag diff --git a/app/views/kaminari/_prev_page.html.erb b/app/views/kaminari/_prev_page.html.erb new file mode 100644 index 0000000000..faf2c4585c --- /dev/null +++ b/app/views/kaminari/_prev_page.html.erb @@ -0,0 +1,11 @@ +<%# Link to the "Previous" page + - available local variables + url: url to the previous page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> + + <%= link_to_unless current_page.first?, t("views.pagination.previous").html_safe, url, rel: "prev", remote: remote %> + diff --git a/app/views/kaminari/_prev_page.html.haml b/app/views/kaminari/_prev_page.html.haml deleted file mode 100644 index 25d1ab822b..0000000000 --- a/app/views/kaminari/_prev_page.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Link to the "Previous" page --# available local variables --# url: url to the previous page --# current_page: a page object for the currently displayed page --# total_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%span.prev - = link_to_unless current_page.first?, raw('« Previous'), url, :rel => 'prev' diff --git a/app/views/kaminari/notices/_first_page.html.erb b/app/views/kaminari/notices/_first_page.html.erb new file mode 100644 index 0000000000..b40a23dcb6 --- /dev/null +++ b/app/views/kaminari/notices/_first_page.html.erb @@ -0,0 +1,11 @@ +<%# Link to the "First" page + - available local variables + url: url to the first page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> + + <%= link_to_unless current_page.first?, t("views.pagination.first").html_safe, url, remote: remote %> + diff --git a/app/views/kaminari/notices/_first_page.html.haml b/app/views/kaminari/notices/_first_page.html.haml deleted file mode 100644 index 09ca22527b..0000000000 --- a/app/views/kaminari/notices/_first_page.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Link to the "First" page --# available local variables --# url: url to the first page --# current_page: a page object for the currently displayed page --# total_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%span.first - = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote diff --git a/app/views/kaminari/notices/_gap.html.erb b/app/views/kaminari/notices/_gap.html.erb new file mode 100644 index 0000000000..9ed07ace0a --- /dev/null +++ b/app/views/kaminari/notices/_gap.html.erb @@ -0,0 +1,8 @@ +<%# Non-link tag that stands for skipped pages... + - available local variables + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +<%= t("views.pagination.truncate").html_safe %> diff --git a/app/views/kaminari/notices/_gap.html.haml b/app/views/kaminari/notices/_gap.html.haml deleted file mode 100644 index 40c954b2be..0000000000 --- a/app/views/kaminari/notices/_gap.html.haml +++ /dev/null @@ -1,8 +0,0 @@ --# Non-link tag that stands for skipped pages... --# available local variables --# current_page: a page object for the currently displayed page --# total_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%span.page.gap - = raw(t 'views.pagination.truncate') diff --git a/app/views/kaminari/notices/_last_page.html.erb b/app/views/kaminari/notices/_last_page.html.erb new file mode 100644 index 0000000000..7cdc9a4759 --- /dev/null +++ b/app/views/kaminari/notices/_last_page.html.erb @@ -0,0 +1,11 @@ +<%# Link to the "Last" page + - available local variables + url: url to the last page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> + + <%= link_to_unless current_page.last?, t("views.pagination.last").html_safe, url, remote: remote %> + diff --git a/app/views/kaminari/notices/_last_page.html.haml b/app/views/kaminari/notices/_last_page.html.haml deleted file mode 100644 index c4b449237c..0000000000 --- a/app/views/kaminari/notices/_last_page.html.haml +++ /dev/null @@ -1,9 +0,0 @@ --# Link to the "Last" page --# available local variables --# url: url to the last page --# current_page: a page object for the currently displayed page --# total_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%span.last - = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote} diff --git a/app/views/kaminari/notices/_next_page.html.erb b/app/views/kaminari/notices/_next_page.html.erb new file mode 100644 index 0000000000..6903c3ecc5 --- /dev/null +++ b/app/views/kaminari/notices/_next_page.html.erb @@ -0,0 +1,13 @@ +<%# Link to the "Next" page + - available local variables + url: url to the next page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> + + <%= link_to_unless current_page.last?, "← Older".html_safe, url, class: "next_page", rel: "next", remote: remote do |name| %> + <%= content_tag :span, name, class: "next_page disabled" %> + <% end %> + diff --git a/app/views/kaminari/notices/_next_page.html.haml b/app/views/kaminari/notices/_next_page.html.haml deleted file mode 100644 index e76e84008c..0000000000 --- a/app/views/kaminari/notices/_next_page.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -~ link_to_unless current_page.last?, raw('← Older'), url, :class => "next_page", :rel => "next", :remote => remote do |name| - ~ content_tag :span, name, :class => 'next_page disabled' diff --git a/app/views/kaminari/notices/_page.html.erb b/app/views/kaminari/notices/_page.html.erb new file mode 100644 index 0000000000..b58efbe1fa --- /dev/null +++ b/app/views/kaminari/notices/_page.html.erb @@ -0,0 +1,12 @@ +<%# Link showing page number + - available local variables + page: a page object for "this" page + url: url to this page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +"> + <%= link_to_unless page.current?, page, url, rel: page.rel, remote: remote %> + diff --git a/app/views/kaminari/notices/_page.html.haml b/app/views/kaminari/notices/_page.html.haml deleted file mode 100644 index 08cc5350bb..0000000000 --- a/app/views/kaminari/notices/_page.html.haml +++ /dev/null @@ -1,10 +0,0 @@ --# Link showing page number --# available local variables --# page: a page object for "this" page --# url: url to this page --# current_page: a page object for the currently displayed page --# total_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote -%span{:class => "page#{' current' if page.current?}"} - = link_to_unless page.current?, page, url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil} diff --git a/app/views/kaminari/notices/_paginator.html.erb b/app/views/kaminari/notices/_paginator.html.erb new file mode 100644 index 0000000000..510243de4d --- /dev/null +++ b/app/views/kaminari/notices/_paginator.html.erb @@ -0,0 +1,21 @@ +<%# The container tag + - available local variables + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote + paginator: the paginator that renders the pagination tags inside +-%> +<%= paginator.render do -%> +
+ <%= next_page_tag %> | <%= prev_page_tag %> +
+<% end -%> + +
+ <%= image_tag "loader.gif" %> +
+ +
+ <%= "#{t("paginator.viewing_occurrence")} #{page_count_from_end(current_page, total_pages)} #{t("paginator.of")} #{total_pages}" %> +
diff --git a/app/views/kaminari/notices/_paginator.html.haml b/app/views/kaminari/notices/_paginator.html.haml deleted file mode 100644 index 1541a39adc..0000000000 --- a/app/views/kaminari/notices/_paginator.html.haml +++ /dev/null @@ -1,14 +0,0 @@ --# The container tag --# available local variables --# current_page: a page object for the currently displayed page --# total_pages: total number of pages --# per_page: number of items to fetch per page --# remote: data-remote --# paginator: the paginator that renders the pagination tags inside -= paginator.render do - .notice-pagination - = next_page_tag - | - = prev_page_tag -.notice-pagination-loader= image_tag 'loader.gif' -%div #{I18n.t('paginator.viewing_occurrence')} #{page_count_from_end(current_page, total_pages)} #{I18n.t('paginator.of')} #{total_pages} diff --git a/app/views/kaminari/notices/_prev_page.html.erb b/app/views/kaminari/notices/_prev_page.html.erb new file mode 100644 index 0000000000..28e46cece4 --- /dev/null +++ b/app/views/kaminari/notices/_prev_page.html.erb @@ -0,0 +1,13 @@ +<%# Link to the "Previous" page + - available local variables + url: url to the previous page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> + + <%= link_to_unless current_page.first?, "Newer →".html_safe, url, class: "previous_page", rel: "prev", remote: remote do |name| %> + <%= content_tag :span, name, class: "previous_page disabled" %> + <% end %> + diff --git a/app/views/kaminari/notices/_prev_page.html.haml b/app/views/kaminari/notices/_prev_page.html.haml deleted file mode 100644 index 74b429a0dd..0000000000 --- a/app/views/kaminari/notices/_prev_page.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -~ link_to_unless current_page.first?, raw('Newer →'), url, :class => "previous_page", :rel => "prev", :remote => remote do |name| - ~ content_tag :span, name, :class => 'previous_page disabled' diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..370cbff69e --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,66 @@ + + + + + <%= t(".title") %> + — + <%= yield(:page_title).present? ? yield(:page_title) : yield(:title) %> + + + + + <%= favicon_link_tag %> + <%= csrf_meta_tag %> + <%= csp_meta_tag %> + <%= stylesheet_link_tag :application, integrity: true %> + <%= javascript_include_tag :app %> + <%= javascript_importmap_tags %> + + <%= yield :head %> + + + + + +
+
+ <%= yield :title_avatar %> +

<%= yield :title %>

+ + <%= yield :meta %> + + <% if (action_bar = yield(:action_bar)).present? %> +
+ <%= action_bar %> +
+ <% end %> +
+ +
+ <%= render "shared/flash_messages" %> + + <%= yield %> +
+ + <% if content_for?(:comments) %> +
+ <%= yield :comments %> +
+ <% end %> +
+ + + + <%= yield :scripts %> + + diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml deleted file mode 100644 index 420afe3125..0000000000 --- a/app/views/layouts/application.html.haml +++ /dev/null @@ -1,38 +0,0 @@ -!!! html -%html - %head - %title - = t('.title') - — - = yield(:page_title).present? ? yield(:page_title) : yield(:title) - %meta{ :content => "text/html; charset=utf-8", "http-equiv" => "content-type" }/ - = favicon_link_tag - = csrf_meta_tag - = stylesheet_link_tag :application - = javascript_include_tag :application - - = yield :head - %body{:id => controller.controller_name, :class => controller.action_name} - #header - %div - = link_to t('.errbit'), root_path, :id => 'site-name' - = render 'shared/navigation' if current_user - = render 'shared/session' - #content-wrapper - #content-title{ :class => (yield :title_css_class).to_s, :style => (yield :title_style) } - %h1= yield :title - %span.meta= yield :meta - - if (action_bar = yield(:action_bar)).present? - #action-bar - = action_bar - #content - = render 'shared/flash_messages' - = yield - - if content_for?(:comments) - #content-comments - = yield :comments - #footer - = t('.powered_html', :link => link_to(t('.errbit'), 'http://github.com/errbit/errbit', :target => '_blank')) - %br - = Errbit::VERSION - = yield :scripts diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000000..850d9b981c --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,36 @@ + + + + + <%= stylesheet_link_tag "mailers/mailer" %> + + <%# Include CSS inline %> + + + + + +
+ + + + + + + <%= yield %> + + + + + +
+ <%= link_to image_tag(URI.join(root_url, ActionController::Base.helpers.asset_path("images/logo.png")).to_s, alt: "Errbit"), root_url %> +
+
+ + diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml deleted file mode 100644 index 0a96e34140..0000000000 --- a/app/views/layouts/mailer.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -%html - %head - - =# stylesheet_link_tag 'mailers/mailer' - -# Include CSS inline - %style= File.read(Rails.root.join('app/assets/stylesheets/mailers/mailer.css')) - %body(leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0") - %center - %table(cellpadding="0" cellspacing="0" border="0" width="100%") - %tbody - %tr - %td.header - = link_to image_tag(URI.join(root_url, ActionController::Base.helpers.asset_path("images/logo.png")).to_s, :alt => "Errbit"), root_url - - = yield - - %tr - %td.footer - Your loyal servant, - = link_to 'Errbit', root_url - diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb index b050bc8319..d48d18d8f8 100644 --- a/app/views/layouts/mailer.text.erb +++ b/app/views/layouts/mailer.text.erb @@ -3,4 +3,3 @@ Your loyal servant, Errbit - diff --git a/app/views/mailer/comment_notification.html.erb b/app/views/mailer/comment_notification.html.erb new file mode 100644 index 0000000000..c7d2f73005 --- /dev/null +++ b/app/views/mailer/comment_notification.html.erb @@ -0,0 +1,88 @@ + + + + + + + + +
+
+

+ <%= @user.name %> + has just commented on an error that occurred in + <%= link_to(@app.name, app_url(@app), class: "bold") << "," %> + on the + <%= @problem.environment %> + environment. +
+ This error has occurred <%= pluralize(@problem.notices_count, "time") %>. +

+ +

+ <%= link_to("Click here to view the error and add a comment on Errbit", app_problem_url(@app, @problem), class: "bold") << "." %> +

+
+
+ + + + + + + + + + + +
+
+

COMMENT:

+ +
+ + <%= simple_format(@comment.body) %> +
+
+ + + + + + + + + + + +
+
+

ERROR MESSAGE:

+ +

<%= @problem.message %>

+ +

WHERE:

+ +

+ <%= @problem.where %> +

+ +

URL:

+ +

+ <% if @notice.request["url"].present? %> + <%= link_to @notice.request["url"], @notice.request["url"] %> + <% end %> +

+ +

BROWSER:

+ +

+ <%= user_agent_graph(@problem) %> +

+ +
+
+
+ + diff --git a/app/views/mailer/comment_notification.html.haml b/app/views/mailer/comment_notification.html.haml deleted file mode 100644 index 5ecdb481e8..0000000000 --- a/app/views/mailer/comment_notification.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -%tr - %td.section - %table(cellpadding="0" cellspacing="0" border="0" align="left") - %tbody - %tr - %td.content(valign="top") - %div - %p - = @user.name - has just commented on an error that occurred in - = link_to(@app.name, app_url(@app), :class => "bold") << "," - on the - %span.bold= @problem.environment - environment. - %br - This error has occurred #{pluralize @problem.notices_count, 'time'}. - %p - = link_to("Click here to view the error and add a comment on Errbit", app_problem_url(@app, @problem), :class => "bold") << "." - -%tr - %td.section - %table(cellpadding="0" cellspacing="0" border="0" align="left") - %tbody - %tr - %td.content(valign="top") - %div - %p.heading COMMENT: - %br - %p= @comment.body.to_s.gsub("\n", "
").html_safe - -%tr - %td.section - %table(cellpadding="0" cellspacing="0" border="0" align="left") - %tbody - %tr - %td.content(valign="top") - %div - %p.heading ERROR MESSAGE: - %p= @problem.message - %p.heading WHERE: - %p.monospace - = @problem.where - %p.heading URL: - %p.monospace - - if @notice.request['url'].present? - = link_to @notice.request['url'], @notice.request['url'] - %p.heading BROWSER: - %p.monospace - = user_agent_graph(@problem) - %br diff --git a/app/views/mailer/comment_notification.text.erb b/app/views/mailer/comment_notification.text.erb index 9347fda74e..c5c01d7c5c 100644 --- a/app/views/mailer/comment_notification.text.erb +++ b/app/views/mailer/comment_notification.text.erb @@ -1,6 +1,6 @@ <%= @user.name %> has just commented on an error that occurred in <%= @notice.environment_name %>: <%= raw(@notice.message) %> -This error has occurred <%= pluralize @notice.problem.notices_count, 'time' %>. You should really look into it here: +This error has occurred <%= pluralize @notice.problem.notices_count, "time" %>. You should really look into it here: <%= app_problem_url(@app, @notice.problem) %> @@ -28,7 +28,7 @@ WHERE: URL: -<%= @notice.request['url'] %> +<%= @notice.request["url"] %> BROWSER: diff --git a/app/views/mailer/err_notification.html.erb b/app/views/mailer/err_notification.html.erb new file mode 100644 index 0000000000..99260e29e9 --- /dev/null +++ b/app/views/mailer/err_notification.html.erb @@ -0,0 +1,133 @@ + + + + + + + + +
+
+

+ An error has just occurred in + <%= link_to(@app.name, app_url(@app), class: "bold") << "," %> + on the + <%= @notice.environment_name %> + environment. +
+ This error has occurred <%= pluralize(@notice.problem.notices_count, "time") %>. +

+ +

+ <%= link_to("Click here to view the error on Errbit", app_problem_url(@app, @notice.problem), class: "bold") << "." %> +

+
+
+ + + + + + + + + + + +
+
+

ERROR MESSAGE:

+ +

<%= @notice.message %>

+ +

WHERE:

+ +

+ <%= @notice.where %> +

+ + <% @notice.backtrace.lines.each do |line| %> + <% next unless line.in_app? %> + +

+ <%= line.link_to_source_file(@app) do %> + <%= line %> + <% end %> +

+ <% end %> + +
+ + <% if @notice.app_version.present? %> +

APP VERSION:

+ +

+ <%= @notice.app_version %> +

+ +
+ <% end %> + +
+ + <% if @notice.server_environment.present? && @notice.server_environment["hostname"].present? %> +

APP SERVER:

+ +

+ <%= @notice.server_environment["hostname"] %> +

+ +
+ <% end %> + +

URL:

+ +

+ <% if @notice.request["url"].present? %> + <%= link_to @notice.request["url"], @notice.request["url"] %> + <% end %> +

+ +

BROWSER:

+ +

+ <%= user_agent_graph(@notice.problem) %> +

+ +
+ + <% if @notice.user_attributes.present? %> +

USER:

+ + + <% @notice.user_attributes.each do |key, value| %> + + + + + + <% end %> +
<%= key.to_s.titleize %>:<%= auto_link(value.to_s) %>
+ +
+ <% end %> + + <% if @notice.backtrace.lines.any? %> +
+ +

FULL BACKTRACE:

+ + <% @notice.backtrace.lines.each do |line| %> +

+ <%= line.link_to_source_file(@app) do %> + <%= line %> + <% end %> +

+ <% end %> + +
+ <% end %> +
+
+ + diff --git a/app/views/mailer/err_notification.html.haml b/app/views/mailer/err_notification.html.haml deleted file mode 100644 index 83b026acfc..0000000000 --- a/app/views/mailer/err_notification.html.haml +++ /dev/null @@ -1,70 +0,0 @@ -%tr - %td.section - %table(cellpadding="0" cellspacing="0" border="0" align="left") - %tbody - %tr - %td.content(valign="top") - %div - %p - An error has just occurred in - = link_to(@app.name, app_url(@app), :class => "bold") << "," - on the - %span.bold= @notice.environment_name - environment. - %br - This error has occurred #{pluralize @notice.problem.notices_count, 'time'}. - %p - = link_to("Click here to view the error on Errbit", app_problem_url(@app, @notice.problem), :class => "bold") << "." -%tr - %td.section - %table(cellpadding="0" cellspacing="0" border="0" align="left") - %tbody - %tr - %td.content(valign="top") - %div - %p.heading ERROR MESSAGE: - %p= @notice.message - %p.heading WHERE: - %p.monospace - = @notice.where - - @notice.backtrace.lines.each do |line| - - next unless line.in_app? - %p.backtrace - = line.link_to_source_file(@app) do - = line.to_s - %br - - if @notice.app_version.present? - %p.heading APP VERSION: - %p.monospace - = @notice.app_version - %br - %br - - if @notice.server_environment.present? && @notice.server_environment["hostname"].present? - %p.heading APP SERVER: - %p.monospace - = @notice.server_environment["hostname"] - %br - %p.heading URL: - %p.monospace - - if @notice.request['url'].present? - = link_to @notice.request['url'], @notice.request['url'] - %p.heading BROWSER: - %p.monospace - = user_agent_graph(@notice.problem) - %br - - if @notice.user_attributes.present? - %p.heading USER: - %table - - @notice.user_attributes.each do |key, value| - %tr - %td(style="text-align: right; padding-right: 10px; color: #6a6a6a;")= key.to_s.titleize + ":" - %td= auto_link(value.to_s) - %br - - if @notice.backtrace.lines.any? - %br - %p.heading FULL BACKTRACE: - - @notice.backtrace.lines.each do |line| - %p.backtrace - = line.link_to_source_file(@app) do - = line.to_s - %br diff --git a/app/views/mailer/err_notification.text.erb b/app/views/mailer/err_notification.text.erb index 5b12e56d69..6faf2b791e 100644 --- a/app/views/mailer/err_notification.text.erb +++ b/app/views/mailer/err_notification.text.erb @@ -1,6 +1,6 @@ An error has just occurred in <%= @notice.environment_name %>: <%= raw(@notice.message) %> -This error has occurred <%= pluralize @notice.problem.notices_count, 'time' %>. You should really look into it here: +This error has occurred <%= pluralize @notice.problem.notices_count, "time" %>. You should really look into it here: <%= app_problem_url(@app, @notice.problem) %> @@ -33,7 +33,7 @@ APP SERVER: URL: -<%= @notice.request['url'] %> +<%= @notice.request["url"] %> BROWSER: @@ -55,4 +55,3 @@ BACKTRACE: <% @notice.backtrace.lines.each do |line| %> <%= line %> <% end %> - diff --git a/app/views/notices/_atom_entry.atom.erb b/app/views/notices/_atom_entry.atom.erb new file mode 100644 index 0000000000..8792c45e43 --- /dev/null +++ b/app/views/notices/_atom_entry.atom.erb @@ -0,0 +1,70 @@ +

<%= notice.message %>

+ +

Summary

+ +<% if notice.request["url"].present? %> +

+ URL: + + <%= link_to(notice.request["url"], notice.request["url"]) %> +

+<% end %> + +

+ Where: + + <%= notice.where %> +

+ +

+ Occurred: + + <%= notice.created_at.to_fs(:micro) %> +

+ +

+ Similar: + + <%= notice.problem.notices_count - 1 %> +

+ +

Params

+ +

+ <%= pretty_hash(notice.params) %> +

+ +

Session

+ +

+ <%= pretty_hash(notice.session) %> +

+ +

Backtrace

+ + + <% for line in notice.backtrace_lines %> + + + + + + <% end %> +
+ <%= "#{line["number"]}:" %> +    + + <%= raw "#{h line["file_relative"]} -> #{content_tag :strong, h(line["method"])}" %> +
+ +

Environment

+ + + <% for key, value in notice.env_vars %> + + + + + + <% end %> +
<%= h(key) %><%= h(value) %>
diff --git a/app/views/notices/_atom_entry.atom.haml b/app/views/notices/_atom_entry.atom.haml deleted file mode 100644 index 1e15b520e4..0000000000 --- a/app/views/notices/_atom_entry.atom.haml +++ /dev/null @@ -1,41 +0,0 @@ -%h2= notice.message -%h3 Summary -- if notice.request['url'].present? - %p - %strong URL: - = link_to(notice.request['url'], notice.request['url']) -%p - %strong Where: - = notice.where -%p - %strong Occured: - = notice.created_at.to_s(:micro) -%p - %strong Similar: - = notice.problem.notices_count - 1 - -%h3 Params -%p= pretty_hash(notice.params) - -%h3 Session -%p= pretty_hash(notice.session) - -%h3 Backtrace -%table - - for line in notice.backtrace_lines - %tr - %td - = "#{line.number}:" -    - %td - = raw "#{h line.file_relative} -> #{content_tag :strong, h(line.method)}" - -%h3 Environment -%table - - for key, val in notice.env_vars - %tr - %td - = h key - %td - = h val - diff --git a/app/views/notices/_backtrace.html.erb b/app/views/notices/_backtrace.html.erb new file mode 100644 index 0000000000..d3e434e698 --- /dev/null +++ b/app/views/notices/_backtrace.html.erb @@ -0,0 +1,25 @@ +<%# locals: (backtrace:) %> + +
+ + <%# Group lines into internal / external so we can toggle the external backtrace %> + <%# Includes a margin of x lines that are not toggled. %> + + <% em = 4 # (external backtrace margin) %> + + <% backtrace.grouped_lines.each do |in_app, line_group| %> + <% if !in_app && line_group.size > (em * 3) %> + <%= render partial: "notices/backtrace_line", collection: line_group[0, em], as: :line %> + <%= render partial: "notices/backtrace_line", collection: line_group[em, line_group.size - (em * 2)], as: :line, locals: {row_class: "toggle_external_backtrace hidden_external_backtrace"} %> + + + + <%= render partial: "notices/backtrace_line", collection: line_group[em * -1, em], as: :line %> + <% else %> + <%= render partial: "notices/backtrace_line", collection: line_group, as: :line %> + <% end %> + <% end %> +
+ ... +
+
diff --git a/app/views/notices/_backtrace.html.haml b/app/views/notices/_backtrace.html.haml deleted file mode 100644 index 991d551587..0000000000 --- a/app/views/notices/_backtrace.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -.window - %table.backtrace - -# Group lines into internal / external so we can toggle the external backtrace - -# Includes a margin of x lines that are not toggled. - - em = 4 # (external backtrace margin) - - backtrace.grouped_lines.each do |in_app, line_group| - - if !in_app && line_group.size > (em * 3) - = render :partial => 'notices/backtrace_line', :collection => line_group[0, em], :as => :line - = render :partial => 'notices/backtrace_line', :collection => line_group[em, line_group.size - (em * 2)], :as => :line, :locals => {:row_class => 'toggle_external_backtrace'} - %tr - %td.line.backtrace_separator - %span ... - = render :partial => 'notices/backtrace_line', :collection => line_group[em * -1, em], :as => :line - - - else - = render :partial => "notices/backtrace_line", :collection => line_group, :as => :line diff --git a/app/views/notices/_backtrace_line.html.erb b/app/views/notices/_backtrace_line.html.erb new file mode 100644 index 0000000000..f75676640c --- /dev/null +++ b/app/views/notices/_backtrace_line.html.erb @@ -0,0 +1,21 @@ + + "> + <%= line.link_to_source_file(app) do %> + <%= sanitize(line.decorated_path, tags: ["strong"], attributes: []) %> + + <%= line.file_name %> + + <% if line.number.present? %> + <%= ":#{line.number}" %> + <% end %> + + <% if line.column.present? %> + <%= ":#{line.column}" %> + <% end %> + <% end %> + + → + + <%= line.method %> + + diff --git a/app/views/notices/_backtrace_line.html.haml b/app/views/notices/_backtrace_line.html.haml deleted file mode 100644 index 966bb1c8c8..0000000000 --- a/app/views/notices/_backtrace_line.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -%tr{:class => defined?(row_class) && row_class} - %td.line{:class => line.in_app? && 'in-app' } - = line.link_to_source_file(app) do - %span.path>= raw line.decorated_path - %span.file>= line.file_name - - if line.number.present? - %span.number>= ":#{line.number}" - - if line.column.present? - %span.number>= ":#{line.column}" - → - %span.method= line.method diff --git a/app/views/notices/_environment.html.erb b/app/views/notices/_environment.html.erb new file mode 100644 index 0000000000..c54e5d1cf8 --- /dev/null +++ b/app/views/notices/_environment.html.erb @@ -0,0 +1,14 @@ +<%# locals: (notice:) %> + +
+ + <% html_entities = HTMLEntities.new %> + + <% notice.env_vars.sort_by {|pair| pair[0]}.each do |key, value| %> + + + + + <% end %> +
<%= h(html_entities.decode(key)) %><%= h(html_entities.decode(value)) %>
+
diff --git a/app/views/notices/_environment.html.haml b/app/views/notices/_environment.html.haml deleted file mode 100644 index 59ef2c46db..0000000000 --- a/app/views/notices/_environment.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.window - %table.environment - - html_entities = HTMLEntities.new - - notice.env_vars.sort_by {|pair| pair[0]}.each do |key, value| - %tr - %th= h(html_entities.decode(key)) - %td.main= h(html_entities.decode(value)) - diff --git a/app/views/notices/_params.html.erb b/app/views/notices/_params.html.erb new file mode 100644 index 0000000000..2cef955c6e --- /dev/null +++ b/app/views/notices/_params.html.erb @@ -0,0 +1,7 @@ +<%# locals: (notice:) %> + +
+
+
<%= pretty_hash(notice.params) %>
+
+
diff --git a/app/views/notices/_params.html.haml b/app/views/notices/_params.html.haml deleted file mode 100644 index ad02dcdf08..0000000000 --- a/app/views/notices/_params.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -.window - .raw_data - %pre.hash= pretty_hash notice.params - diff --git a/app/views/notices/_session.html.erb b/app/views/notices/_session.html.erb new file mode 100644 index 0000000000..3b32cd2735 --- /dev/null +++ b/app/views/notices/_session.html.erb @@ -0,0 +1,7 @@ +<%# locals: (notice:) %> + +
+
+
<%= pretty_hash(notice.session) %>
+
+
diff --git a/app/views/notices/_session.html.haml b/app/views/notices/_session.html.haml deleted file mode 100644 index 4d3e947462..0000000000 --- a/app/views/notices/_session.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -.window - .raw_data - %pre.hash= pretty_hash notice.session diff --git a/app/views/notices/_summary.html.erb b/app/views/notices/_summary.html.erb new file mode 100644 index 0000000000..dd3b71ad2e --- /dev/null +++ b/app/views/notices/_summary.html.erb @@ -0,0 +1,75 @@ +<%# locals: (notice:, problem:) %> + +
+ + + + + + + + + + + + <% if notice.request["url"].present? %> + + + + + <% end %> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <% if notice.server_environment && notice.server_environment["hostname"] %> + + + + + <% end %> + + <% if notice.app_version.present? %> + + + + + <% end %> + + <% if notice.framework %> + + + + + <% end %> + + + + + +
<%= t(".message") %><%= message_graph(problem) %>
<%= t(".error_class") %><%= notice.error_class %>
<%= t(".url") %><%= link_to notice.request["url"], notice.request["url"] %>
<%= t(".where") %><%= notice.where %>
<%= t(".occurred") %><%= notice.created_at.to_fs(:precise) %>
<%= t(".similar") %><%= problem.notices_count - 1 %>
<%= t(".browser") %><%= user_agent_graph(problem) %>
<%= t(".tenant") %><%= tenant_graph(problem) %>
<%= t(".app_server") %><%= notice.server_environment && notice.server_environment["hostname"] %>
<%= t(".app_version") %><%= notice.app_version %>
<%= t(".framework") %><%= notice.framework %>
<%= t(".relative_directory") %><%= notice.server_environment && notice.server_environment["project-root"] %>
+
diff --git a/app/views/notices/_summary.html.haml b/app/views/notices/_summary.html.haml deleted file mode 100644 index 8c0d53332c..0000000000 --- a/app/views/notices/_summary.html.haml +++ /dev/null @@ -1,48 +0,0 @@ -.window - %table.summary - %tr - %th= t('.message') - %td.main.nowrap= message_graph(problem) - %tr - %th= t('.error_class') - %td= notice.error_class - - if notice.request['url'].present? - %tr - %th= t('.url') - %td.nowrap= link_to notice.request['url'], notice.request['url'] - %tr - %th= t('.where') - %td= notice.where - %tr - %th= t('.occurred') - %td= notice.created_at.to_s(:precise) - %tr - %th= t('.similar') - %td= problem.notices_count - 1 - %tr#sparkline-placeholder - %tr - %th= t('.browser') - %td= user_agent_graph(problem) - %tr - %th= t('.tenant') - %td= tenant_graph(problem) - - if notice.server_environment && notice.server_environment["hostname"] - %tr - %th= t('.app_server') - %td= notice.server_environment && notice.server_environment["hostname"] - - if notice.app_version.present? - %tr - %th= t('.app_version') - %td= notice.app_version - - if notice.framework - %tr - %th= t('.framework') - %td= notice.framework - %tr - %th= t('.relative_directory') - %td= notice.server_environment && notice.server_environment["project-root"] - -:javascript - $.ajax({url: '/apps/#{notice.app.id}/problems/#{problem.id}/xhr_sparkline'}).then(function(resp) { - $("#sparkline-placeholder").replaceWith(resp); - }); diff --git a/app/views/notices/_user_attributes.html.erb b/app/views/notices/_user_attributes.html.erb new file mode 100644 index 0000000000..97a62bc3cb --- /dev/null +++ b/app/views/notices/_user_attributes.html.erb @@ -0,0 +1,21 @@ +<%# locals: (user_attributes:) %> + +
+ + + + + + <% user_attributes.each do |key, value| %> + + + + <% value = pretty_hash(value) if value.is_a?(Hash) %> + + + + <% end %> +
+ The user who experienced the error: +
<%= key %><%= auto_link(ERB::Util.h(value)).html_safe %>
+
diff --git a/app/views/notices/_user_attributes.html.haml b/app/views/notices/_user_attributes.html.haml deleted file mode 100644 index ee8e9060f6..0000000000 --- a/app/views/notices/_user_attributes.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -.window - %table.user_attributes - %tr - %td - %strong The user who experienced the error: - - user.each do |key, value| - %tr - %th= key - %td= auto_link(auto_link(value.to_s, :urls, :target => "_blank"), :email_addresses).html_safe diff --git a/app/views/problems/_issue_tracker_links.html.erb b/app/views/problems/_issue_tracker_links.html.erb new file mode 100644 index 0000000000..9e37dabe91 --- /dev/null +++ b/app/views/problems/_issue_tracker_links.html.erb @@ -0,0 +1,55 @@ +<%# locals: (app:, problem:) %> + +<% if app.issue_tracker_configured? %> + <% tracker = app.issue_tracker %> + <% tracker_type = tracker.type %> + + <% if problem.issue_link.present? %> + + <%= link_to problem.issue_link, class: "goto-issue issue-tracker-button" do %> + + go to issue + <% end %> + + + + <%= link_to unlink_issue_app_problem_path(app, problem), + method: :delete, + data: { confirm: "Unlink err issues?" }, + class: "unlink-issue issue-tracker-button" do %> + + unlink issue + <% end %> + + + <% if tracker.tracker.respond_to?(:close_issue) %> + + <%= link_to close_issue_app_problem_path(app, problem), + method: :post, + class: "close-issue" do %> + + close issue + <% end %> + + <% end %> + + <% elsif problem.issue_link == "pending" %> + + <%= link_to "#", class: "create-issue issue-tracker-button" do %> + + creating... + <% end %> + + <%= link_to "retry", create_issue_app_problem_path(app, problem), method: :post %> + + <% else %> + + <%= link_to create_issue_app_problem_path(app, problem), + method: :post, + class: "create-issue issue-tracker-button" do %> + + create issue + <% end %> + + <% end %> +<% end %> diff --git a/app/views/problems/_issue_tracker_links.html.haml b/app/views/problems/_issue_tracker_links.html.haml deleted file mode 100644 index c5d516a89d..0000000000 --- a/app/views/problems/_issue_tracker_links.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -- if app.issue_tracker_configured? - - tracker = app.issue_tracker - - tracker_type = tracker.type - - if problem.issue_link.present? - %span - = link_to problem.issue_link, :class => "goto-issue issue-tracker-button" do - %img.button-icon{"src" => tracker_type.icons[:goto]} - go to issue - %span - = link_to unlink_issue_app_problem_path(app, problem), :method => :delete, :data => { :confirm => "Unlink err issues?" }, :class => "unlink-issue issue-tracker-button" do - %img.button-icon{"src" => tracker_type.icons[:create]} - unlink issue - - if tracker.tracker.respond_to? :close_issue - %span - = link_to close_issue_app_problem_path(app, problem), method: :post, :class => "close-issue" do - %img.button-icon{"src" => tracker_type.icons[:create]} - close issue - - elsif problem.issue_link == "pending" - %span.disabled - = link_to '#', :class => "create-issue issue-tracker-button" do - %img.button-icon{"src" => tracker_type.icons[:inactive]} - creating... - = link_to 'retry', create_issue_app_problem_path(app, problem), :method => :post - - else - %span - = link_to create_issue_app_problem_path(app, problem), method: :post, :class => "create-issue issue-tracker-button" do - %img.button-icon{"src" => tracker_type.icons[:create]} - create issue diff --git a/app/views/problems/_list.atom.builder b/app/views/problems/_list.atom.builder index 36191a3d46..7207561223 100644 --- a/app/views/problems/_list.atom.builder +++ b/app/views/problems/_list.atom.builder @@ -1,15 +1,17 @@ -feed.updated(problems.first.try(:created_at) || Time.now) +# frozen_string_literal: true + +feed.updated(problems.first.try(:created_at) || Time.current) problems.each do |problem| notice = problem.notices.first - feed.entry(problem, :url => app_problem_url(problem.app.to_param, problem.to_param)) do |entry| - entry.title "[#{ problem.where }] #{problem.message.to_s.truncate(27)}" + feed.entry(problem, url: app_problem_url(problem.app.to_param, problem.to_param)) do |entry| + entry.title "[#{problem.where}] #{problem.message.to_s.truncate(27)}" entry.author do |author| - author.name "#{ problem.app.name } [#{ problem.environment }]" + author.name "#{problem.app.name} [#{problem.environment}]" end if notice - entry.summary(notice_atom_summary(notice), :type => "html") + entry.summary(notice_atom_summary(notice), type: "html") end end end diff --git a/app/views/problems/_search.html.erb b/app/views/problems/_search.html.erb new file mode 100644 index 0000000000..8ab44078d5 --- /dev/null +++ b/app/views/problems/_search.html.erb @@ -0,0 +1,6 @@ +<%# locals: (all_errs:, app_id:, params_sort:, params_order:) %> +<%= form_tag search_problems_path(all_errs: all_errs, app_id: app_id, sort: params_sort, order: params_order), + method: :get, remote: true, class: "search-form", + data: {type: "html", search_target: "#problem_table"} do %> + <%= text_field_tag :search, params[:search], placeholder: t(".search_placeholder") %> +<% end %> diff --git a/app/views/problems/_search.html.haml b/app/views/problems/_search.html.haml deleted file mode 100644 index 4d834a5114..0000000000 --- a/app/views/problems/_search.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= form_tag search_problems_path(:all_errs => all_errs, :app_id => app_id, :sort => params_sort, :order => params_order), :method => :get, :remote => true do - = text_field_tag :search, params[:search], :placeholder => t('.search_placeholder') diff --git a/app/views/problems/_sparkline.html.erb b/app/views/problems/_sparkline.html.erb new file mode 100644 index 0000000000..b9644d6def --- /dev/null +++ b/app/views/problems/_sparkline.html.erb @@ -0,0 +1,16 @@ +<%# locals: (problem:) %> + +<% now = Time.current %> + +<% two_week_percentages = problem.grouped_notice_count_relative_percentages((now - 13.days).at_beginning_of_day, "day") %> +<% twenty_four_hour_percentages = problem.grouped_notice_count_relative_percentages((now - 23.hours).at_beginning_of_hour, "hour") %> + + + <%= t(".graph-2-weeks") %> + <%= Sparklines.for_relative_percentages(two_week_percentages) %> + + + + <%= t(".graph-24-hours") %> + <%= Sparklines.for_relative_percentages(twenty_four_hour_percentages) %> + diff --git a/app/views/problems/_sparkline.html.haml b/app/views/problems/_sparkline.html.haml deleted file mode 100644 index c909057ef5..0000000000 --- a/app/views/problems/_sparkline.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -= now = Time.current -%tr - - two_week_percentages = problem.grouped_notice_count_relative_percentages((now - 13.days).at_beginning_of_day, 'day') - - twenty_four_hour_percentages = problem.grouped_notice_count_relative_percentages((now - 23.hours).at_beginning_of_hour, 'hour') - %th= t('.graph-2-weeks') - %td= Sparklines.for_relative_percentages(two_week_percentages) -%tr - %th= t('.graph (24 hours)') - %td= Sparklines.for_relative_percentages(twenty_four_hour_percentages) diff --git a/app/views/problems/_table.html.erb b/app/views/problems/_table.html.erb new file mode 100644 index 0000000000..4db7acbb51 --- /dev/null +++ b/app/views/problems/_table.html.erb @@ -0,0 +1,132 @@ +<%# locals: (problems:) %> + +<% any_issue_links = problems.any? { |e| e.issue_link.present? && e.issue_link != "pending" } %> + +<% on_page_that_can_include_multiple_different_apps = params[:controller] == "problems" %> + +<%= form_tag({}, authenticity_token: form_authenticity_token) do %> + + + + + <% if on_page_that_can_include_multiple_different_apps %> + + <% else %> + + <% end %> + + + + <% if any_issue_links %> + + <% end %> + + + + + + <% ProblemDecorator.decorate_collection(problems).each do |problem| %> + "> + + + + + + + + + + <% end %> + + + + + <% end %> + + <% if problems.none? %> + + + + <% end %> + +
<%= check_box_tag "toggle_problems_checkboxes" %><%= link_for_sort t(".app") %><%= link_for_sort t(".env"), "environment" %><%= link_for_sort t(".what_and_where_html"), "message" %><%= link_for_sort t(".latest"), "last_notice_at" %><%= link_for_sort t(".count") %>Issue<%= t(".resolve") %>
+
+ <%= check_box_tag "problems[]", problem.id, selected_problems.member?(problem.id.to_s) %> +
+
+
+ <% if on_page_that_can_include_multiple_different_apps %> + <%= link_to problem.app.name, app_path(problem.app), class: "name nowrap", title: problem.app.name %> + + <%= link_to problem.environment, problems_path(environment: problem.environment) %> + + <% else %> + + <%= link_to problem.environment, app_path(problem.app, environment: problem.environment) %> + + <% end %> +
+
+
+ <%= link_to problem.link_text, app_problem_path(problem.app, problem), class: "nowrap", title: problem.message %> + <%= problem.where %> + <% if problem.comments_count > 0 %> + <% comment = problem.comments.last %> +
+
+ <% if comment.user %> + + <%= (Errbit::Config.user_has_username ? comment.user.username : comment.user.email).to_s %>: + + <% end %> + <%= truncate(comment.body, length: 100, separator: " ") %> +
+ <% end %> +
+
+ <% value = "#{time_ago_in_words(problem.last_notice_at)} #{t("problems.table.ago")}" %> +
<%= value %>
+
+
+ <%= link_to problem.notices_count, app_problem_path(problem.app, problem) %> +
+ + <%# TODO: this if looks like wrong and should be up for one element (td in td?). %> + <% if any_issue_links %> +
+
+ <% if problem.unresolved? %> + <%= link_to image_tag("thumbs-up.png"), + resolve_app_problem_path(problem.app, problem), + title: "Resolve", + method: :patch, + data: { confirm: problem_confirm("resolve_one") }, + class: "resolve" %> + <% end %> +
+
+ No errs here +
+ + <%= paginate problems %> + +
+
    + <% [:merge, :unmerge, :resolve, :unresolve, :delete].each do |action| %> +
  • + <%= submit_tag t(".#{action}").capitalize, + id: "#{action}_problems", + class: "button", + data: { + action: polymorphic_path([action == :delete ? :destroy : action, :several_problems]), + confirm: problem_confirm(action) + } %> +
  • + <% end %> +
+
+<% end %> diff --git a/app/views/problems/_table.html.haml b/app/views/problems/_table.html.haml deleted file mode 100644 index 292b973315..0000000000 --- a/app/views/problems/_table.html.haml +++ /dev/null @@ -1,67 +0,0 @@ -- any_issue_links = problems.any?{|e| e.issue_link.present? && e.issue_link != 'pending' } -- on_page_that_can_include_multiple_different_apps = params[:controller] == 'problems' -=form_tag do - %table.errs.selectable - %thead - %tr - %th= check_box_tag "toggle_problems_checkboxes" - - if on_page_that_can_include_multiple_different_apps - %th= link_for_sort t('.app') - - else - %th= link_for_sort t('.env'), 'environment' - %th= link_for_sort t('.what_&_where').html_safe, "message" - %th= link_for_sort t('.latest'), "last_notice_at" - %th.count-header= link_for_sort t('.count') - - if any_issue_links - %th Issue - %th= t('.resolve') - %tbody - - problems.each do |problem| - %tr{:class => problem.resolved? ? 'resolved' : 'unresolved'} - %td.select.narrow-as-possible - %div.td-container - = check_box_tag "problems[]", problem.id, selected_problems.member?(problem.id.to_s) - %td.app - %div.td-container - - if on_page_that_can_include_multiple_different_apps - = link_to problem.app.name, app_path(problem.app), class: 'name nowrap', title: problem.app.name - %span.environment{class: 'nowrap', title: problem.environment} - = link_to problem.environment, problems_path(:environment => problem.environment) - - else - %span.environment{class: 'nowrap', title: problem.environment} - = link_to problem.environment, app_path(problem.app, :environment => problem.environment) - %td.message - %div.td-container - = link_to problem.message, app_problem_path(problem.app, problem), class: 'nowrap', title: problem.message - %em{class: 'nowrap', title: problem.where}= problem.where - - if problem.comments_count > 0 - - comment = problem.comments.last - %br - .inline_comment{class: 'nowrap'} - - if comment.user - %em.commenter= (Errbit::Config.user_has_username ? comment.user.username : comment.user.email).to_s << ":" - %em= truncate(comment.body, :length => 100, :separator => ' ') - %td.latest - - value = "#{time_ago_in_words(problem.last_notice_at)} #{I18n.t('problems.table.ago')}" - %div.td-container{class: 'nowrap', title: value}= value - %td.count.narrow-as-possible - %div.td-container= link_to problem.notices_count, app_problem_path(problem.app, problem) - - if any_issue_links - %td.issue_link.narrow-as-possible - - if problem.app.issue_tracker_configured? && problem.issue_link.present? && problem.issue_link != 'pending' - = link_to image_tag("#{problem.issue_type}_goto.png"), problem.issue_link, :target => "_blank" - %td.resolve.narrow-as-possible - %div.td-container= link_to image_tag("thumbs-up.png"), resolve_app_problem_path(problem.app, problem), - :title => "Resolve", :method => :put, :data => { :confirm => problem_confirm('resolve_one') }, - :class => 'resolve' if problem.unresolved? - - if problems.none? - %tr - %td{:colspan => 100} - %em No errs here - = paginate problems - .tab-bar - %ul - - %w(merge unmerge resolve unresolve delete).each do |action| - %li= submit_tag t(".#{action}").capitalize, :id => "#{action}_problems", :class => 'button', - :data => { :action => polymorphic_path([action == 'delete' ? 'destroy' : action, 'several_problems']), - :confirm => problem_confirm(action) } diff --git a/app/views/problems/_tally_table.html.erb b/app/views/problems/_tally_table.html.erb new file mode 100644 index 0000000000..99dbd5e9b4 --- /dev/null +++ b/app/views/problems/_tally_table.html.erb @@ -0,0 +1,35 @@ +<%# locals: (rows:) %> + +
+ + + <% head(rows).each do |row| %> + + + + + <% end %> + + + <% if rows.size > head_size %> + + + + + + <% end %> +
<%= number_to_percentage(row[0], precision: 1) %><%= row[1] %>
+ <%= link_to "Show more...", "#", class: "show_tail" %> +
+ + + + <% tail(rows).each do |row| %> + + + + + <% end %> + + +
diff --git a/app/views/problems/_tally_table.html.haml b/app/views/problems/_tally_table.html.haml deleted file mode 100644 index bd03ede36b..0000000000 --- a/app/views/problems/_tally_table.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -.head_and_tail - %table.tally.head - %tbody - - head(rows).each do |row| - %tr - %td.percent= number_to_percentage(row[0], :precision => 1) - %th.value= row[1] - - if rows.size > head_size - %tfoot - %tr - %td{:colspan => 2} - = link_to 'Show more...', '#', :class => :show_tail - %table.tally.tail{:style => "display: none"} - %tbody - - tail(rows).each do |row| - %tr - %td.percent= number_to_percentage(row[0], :precision => 1) - %th.value= row[1] diff --git a/app/views/problems/index.atom.builder b/app/views/problems/index.atom.builder index aa0710b062..eb7b1215ce 100644 --- a/app/views/problems/index.atom.builder +++ b/app/views/problems/index.atom.builder @@ -1,4 +1,6 @@ +# frozen_string_literal: true + atom_feed do |feed| feed.title("Errbit notices at #{root_url}") - render "problems/list", :feed => feed + render "problems/list", feed: feed end diff --git a/app/views/problems/index.html.erb b/app/views/problems/index.html.erb new file mode 100644 index 0000000000..2e9782b9ca --- /dev/null +++ b/app/views/problems/index.html.erb @@ -0,0 +1,25 @@ +<% content_for :title, (all_errs ? t(".all_errors") : t(".unresolved_errors")) %> + +<% content_for :head do %> + <%= auto_discovery_link_tag :atom, problems_path(User.token_authentication_key => current_user.authentication_token, format: :atom), title: "Errbit notices at #{request.host}" %> +<% end %> + +<% content_for :action_bar do %> + <% if all_errs %> + <%= link_to t(".hide_resolved"), problems_path(sort: params_sort, order: params_order), class: "button" %> + <% else %> + <%= link_to t(".show_resolved"), problems_path(all_errs: true, sort: params_sort, order: params_order), class: "button" %> + <% end %> +<% end %> + +
+ <%= render "problems/search", all_errs: all_errs, app_id: nil, params_sort: params_sort, params_order: params_order %> +
+ +
+ +
+
+ <%= render "problems/table", problems: problems %> +
+
diff --git a/app/views/problems/index.html.haml b/app/views/problems/index.html.haml deleted file mode 100644 index dab4624fce..0000000000 --- a/app/views/problems/index.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- content_for :title, all_errs ? t('.all_errors') : t('.unresolved_errors') -- content_for :head do - = auto_discovery_link_tag :atom, problems_path(User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices at #{request.host}" - -- content_for :action_bar do - - if all_errs - = link_to t('.hide_resolved'), problems_path(sort: params_sort, order: params_order), :class => 'button' - - else - = link_to t('.show_resolved'), problems_path(all_errs: true, sort: params_sort, order: params_order), :class => 'button' - -%section - = render 'problems/search', :all_errs => all_errs, :app_id => nil -%br -%section - #problem_table.problem_table - = render 'problems/table' diff --git a/app/views/problems/search.js.haml b/app/views/problems/search.js.haml deleted file mode 100644 index 32d63a1ea5..0000000000 --- a/app/views/problems/search.js.haml +++ /dev/null @@ -1,4 +0,0 @@ -$("#problem_table").empty().append("#{escape_javascript(render('problems/table', :problems => problems))}"); -$("#flash-messages").empty(); -toggleProblemsCheckboxes(); -bindProblemButtonsActions(); diff --git a/app/views/problems/show.html.erb b/app/views/problems/show.html.erb new file mode 100644 index 0000000000..1c9cabb064 --- /dev/null +++ b/app/views/problems/show.html.erb @@ -0,0 +1,134 @@ +<% content_for :page_title, problem.message %> +<% content_for :title_css_class, "err_show #{"resolved" if problem.resolved?}" %> +<% content_for :title, problem.error_class || truncate(problem.message, length: 32) %> + +<% content_for :meta do %> + <%= t(".app") %>: + <%= link_to app.name, app %> + <%= t(".where") %>: + <%= problem.where %>
+ <%= t(".environment") %>: + <%= problem.environment %> + <%= t(".first_notice") %>: + <%= problem.first_notice_at.to_fs(:precise) %> + <%= t(".last_notice") %>: + <%= problem.last_notice_at.to_fs(:precise) %> +<% end %> + +<% content_for :action_bar do %> + <% if problem.unresolved? %> + <%= link_to t(".resolve"), [:resolve, app, problem], method: :patch, + data: { confirm: problem_confirm("resolve_one") }, class: "resolve" %> + <% end %> + + <% if current_user.authentication_token %> + + <%= link_to polymorphic_path([app, problem], format: :ics, auth_token: current_user.authentication_token) do %> + + iCal + <% end %> + + <% end %> + + <%= link_to t(".up"), (request.env["HTTP_REFERER"] ? :back : app_problems_path(app)), class: "up" %> +
+ <%= render "issue_tracker_links", app: app, problem: problem %> +<% end %> + +<% content_for :comments do %> +

<%= t(".comments") %>

+ + <% problem.comments.ordered.each do |comment| %> +
+ + + + + + + + +
+ <% if comment.user %> + <% if Errbit::Config.use_gravatar %> + <%= gravatar_tag(comment.user.email, s: 24) %> + <% end %> + + <%= time_ago_in_words(comment.created_at, include_seconds: true) %> <%= t(".ago_by") %> + <%= link_to comment.user.email, comment.user %> + + <% else %> + + <%= time_ago_in_words(comment.created_at, include_seconds: true) %> <%= t(".ago_by_unknown_user") %> + + <% end %> + + <%= link_to "✘".html_safe, [app, problem, comment], + method: :delete, data: { confirm: t("comments.confirm_delete") }, + class: "destroy-comment" %> + +
<%= auto_link_format(comment.body) %>
+
+ <% end %> + + <%= form_for [app, problem, @comment] do |comment_form| %> +

<%= t(".add_a_comment") %>

+ <%= comment_form.text_area :body %> + <%= comment_form.submit t(".save_comment") %> + <% end %> +<% end %> + +

<%= @notice&.message %>

+ +<% if params[:notice_id] %> +

<%= t(".notice_by_id", notice_id: params[:notice_id]) %>

+<% else %> + <%= paginate @notices, param_name: :notice, theme: :notices %> +<% end %> + +
+
    +
  • <%= link_to t(".summary"), "#summary", rel: "summary", class: "button" %>
  • +
  • <%= link_to t(".backtrace"), "#backtrace", rel: "backtrace", class: "button" %>
  • + <% if @notice && @notice.user_attributes.present? %> +
  • <%= link_to t(".user"), "#user_attributes", rel: "user_attributes", class: "button" %>
  • + <% end %> +
  • <%= link_to t(".environment"), "#environment", rel: "environment", class: "button" %>
  • +
  • <%= link_to t(".parameters"), "#params", rel: "params", class: "button" %>
  • +
  • <%= link_to t(".session"), "#session", rel: "session", class: "button" %>
  • +
+
+ +<% if @notice %> +
+

<%= t(".summary") %>

+ <%= render "notices/summary", notice: @notice, problem: problem %> +
+ +
+

Backtrace

+ <%= render "notices/backtrace", backtrace: @notice.backtrace %> +
+ + <% if @notice.user_attributes.present? %> +
+

User

+ <%= render "notices/user_attributes", user_attributes: @notice.user_attributes %> +
+ <% end %> + +
+

Environment

+ <%= render "notices/environment", notice: @notice %> +
+ +
+

Parameters

+ <%= render "notices/params", notice: @notice %> +
+ +
+

Session

+ <%= render "notices/session", notice: @notice %> +
+<% end %> diff --git a/app/views/problems/show.html.haml b/app/views/problems/show.html.haml deleted file mode 100644 index 3b33cbce96..0000000000 --- a/app/views/problems/show.html.haml +++ /dev/null @@ -1,92 +0,0 @@ -- content_for :page_title, problem.message -- content_for :title_css_class, "err_show #{'resolved' if problem.resolved?}" -- content_for :title, problem.error_class || truncate(problem.message, :length => 32) -- content_for :meta do - %strong= t('.app') + ':' - = link_to app.name, app - %strong= t('.where') + ':' - = problem.where - %br - %strong= t('.environment') + ':' - = problem.environment - %strong= t('.first_notice') + ':' - = problem.first_notice_at.to_s(:precise) - %strong= t('.last_notice') + ':' - = problem.last_notice_at.to_s(:precise) -- content_for :action_bar do - - if problem.unresolved? - %span= link_to t('.resolve'), [:resolve, app, problem], :method => :put, - :data => { :confirm => problem_confirm('resolve_one') }, :class => 'resolve' - - if current_user.authentication_token - %span - = link_to polymorphic_path([app, problem], :format => "ics", :auth_token => current_user.authentication_token) do - %i.fa.fa-calendar - iCal - %span>= link_to t('.up'), (request.env['HTTP_REFERER'] ? :back : app_problems_path(app)), :class => 'up' - %br - = render "issue_tracker_links" - -- content_for :comments do - %h3= t('.comments') - - problem.comments.each do |comment| - .window - %table.comment - %tr - %th - - if comment.user - - if Errbit::Config.use_gravatar - = gravatar_tag comment.user.email, :s => 24 - %span.comment-info - = time_ago_in_words(comment.created_at, include_seconds: true) << " " + t('.ago_by') - = link_to comment.user.email, comment.user - - else - %span.comment-info - = time_ago_in_words(comment.created_at, include_seconds: true) << " " + t('.ago_by_unknown_user') - %span.delete= link_to '✘'.html_safe, [app, problem, comment], :method => :delete, - :data => { :confirm => t("comments.confirm_delete") }, :class => "destroy-comment" - %tr - %td= simple_format comment.body - = form_for [app, problem, @comment] do |comment_form| - %p= t('.add_a_comment') - = comment_form.text_area :body - = comment_form.submit t('.save_comment') - -%h4= @notice.try(:message) - -= paginate @notices, :param_name => :notice, :theme => :notices - -.tab-bar - %ul - %li= link_to t('.summary'), '#summary', :rel => 'summary', :class => 'button' - %li= link_to t('.backtrace'), '#backtrace', :rel => 'backtrace', :class => 'button' - - if @notice && @notice.user_attributes.present? - %li= link_to t('.user'), '#user_attributes', :rel => 'user_attributes', :class => 'button' - %li= link_to t('.environment'), '#environment', :rel => 'environment', :class => 'button' - %li= link_to t('.parameters'), '#params', :rel => 'params', :class => 'button' - %li= link_to t('.session'), '#session', :rel => 'session', :class => 'button' - -- if @notice - #summary - %h3= t('.summary') - = render 'notices/summary', :notice => @notice - - #backtrace - %h3 Backtrace - = render 'notices/backtrace', :backtrace => @notice.backtrace - - - if @notice.user_attributes.present? - #user_attributes - %h3 User - = render 'notices/user_attributes', :user => @notice.user_attributes - - #environment - %h3 Environment - = render 'notices/environment', :notice => @notice - - #params - %h3 Parameters - = render 'notices/params', :notice => @notice - - #session - %h3 Session - = render 'notices/session', :notice => @notice diff --git a/app/views/problems/show.ics.erb b/app/views/problems/show.ics.erb new file mode 100644 index 0000000000..b2098056bf --- /dev/null +++ b/app/views/problems/show.ics.erb @@ -0,0 +1 @@ +<%= generate_problem_ical(problem.notices.object.order_by(:created_at.asc)) %> diff --git a/app/views/problems/show.ics.haml b/app/views/problems/show.ics.haml deleted file mode 100644 index 0463a10777..0000000000 --- a/app/views/problems/show.ics.haml +++ /dev/null @@ -1 +0,0 @@ -= generate_problem_ical(problem.notices.object.order_by(:created_at.asc)) diff --git a/app/views/shared/_flash_messages.html.erb b/app/views/shared/_flash_messages.html.erb new file mode 100644 index 0000000000..ecba49cf21 --- /dev/null +++ b/app/views/shared/_flash_messages.html.erb @@ -0,0 +1,7 @@ +<% unless flash.keys.empty? %> + +<% end %> diff --git a/app/views/shared/_flash_messages.html.haml b/app/views/shared/_flash_messages.html.haml deleted file mode 100644 index 206cd795bd..0000000000 --- a/app/views/shared/_flash_messages.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- unless flash.keys.empty? - %ul#flash-messages - - flash.each do |type, message| - %li{:class => type}= message diff --git a/app/views/shared/_link_github_account.html.erb b/app/views/shared/_link_github_account.html.erb new file mode 100644 index 0000000000..cd987656df --- /dev/null +++ b/app/views/shared/_link_github_account.html.erb @@ -0,0 +1,15 @@ +<%# locals: (user:) %> +<% if Errbit::Config.github_authentication && user == current_user %> + + <% if user.github_account? %> + <%= link_to "Unlink #{Errbit::Config.github_site_title} account", + user_unlink_github_path(user), + method: :patch, + data: { confirm: "Are you sure?" } %> + <% else %> + <%= link_to "Link #{Errbit::Config.github_site_title} account", + user_github_omniauth_authorize_path, + method: :post %> + <% end %> + +<% end %> diff --git a/app/views/shared/_link_github_account.html.haml b/app/views/shared/_link_github_account.html.haml deleted file mode 100644 index fd8bb38b2e..0000000000 --- a/app/views/shared/_link_github_account.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -- if Errbit::Config.github_authentication && user == current_user - - if user.github_account? - %span.github= link_to "Unlink #{Errbit::Config.github_site_title} account", unlink_github_user_path(user), :method => :delete, :data => { :confirm => "Are you sure?" } - - else - %span.github= link_to "Link #{Errbit::Config.github_site_title} account", user_github_omniauth_authorize_path diff --git a/app/views/shared/_link_google_account.html.erb b/app/views/shared/_link_google_account.html.erb new file mode 100644 index 0000000000..af1bfabb74 --- /dev/null +++ b/app/views/shared/_link_google_account.html.erb @@ -0,0 +1,15 @@ +<%# locals: (user:) %> +<% if Errbit::Config.google_authentication && user == current_user %> + + <% if user.google_account? %> + <%= link_to "Unlink #{Errbit::Config.google_site_title} account", + user_unlink_google_path(user), + method: :patch, + data: { confirm: "Are you sure?" } %> + <% else %> + <%= link_to "Link #{Errbit::Config.google_site_title} account", + user_google_oauth2_omniauth_authorize_path, + method: :post %> + <% end %> + +<% end %> diff --git a/app/views/shared/_link_google_account.html.haml b/app/views/shared/_link_google_account.html.haml deleted file mode 100644 index 971d4525f5..0000000000 --- a/app/views/shared/_link_google_account.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -- if Errbit::Config.google_authentication && user == current_user - - if user.google_account? - %span.google= link_to "Unlink #{Errbit::Config.google_site_title} account", unlink_google_user_path, method: :delete, data: { confirm: 'Are you sure?' } - - else - %span.google= link_to "Link #{Errbit::Config.google_site_title} account", user_google_oauth2_omniauth_authorize_path diff --git a/app/views/shared/_navigation.html.erb b/app/views/shared/_navigation.html.erb new file mode 100644 index 0000000000..2512918539 --- /dev/null +++ b/app/views/shared/_navigation.html.erb @@ -0,0 +1,31 @@ + diff --git a/app/views/shared/_navigation.html.haml b/app/views/shared/_navigation.html.haml deleted file mode 100644 index d901b8b805..0000000000 --- a/app/views/shared/_navigation.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -#nav-bar - %ul - %li{:class => active_if_here(:apps)} - = link_to apps_path do - %i.fa.fa-briefcase - = t('.apps') - %li{:class => active_if_here(:problems)} - = link_to problems_path do - %i.fa.fa-warning - = t('.errors') - - if user_signed_in? && current_user.admin? - %li{:class => active_if_here(:users)} - = link_to users_path do - %i.fa.fa-users - = t('.users') - %li{:class => active_if_here(:site_config)} - = link_to site_config_index_path do - %i.fa.fa-wrench - = t('.config') - %div.clear diff --git a/app/views/shared/_notice_fingerprinter.html.erb b/app/views/shared/_notice_fingerprinter.html.erb new file mode 100644 index 0000000000..bcea935799 --- /dev/null +++ b/app/views/shared/_notice_fingerprinter.html.erb @@ -0,0 +1,32 @@ +<%# locals: (f:) %> +<%= f.fields_for :notice_fingerprinter do |g| %> +
+ <%= g.check_box :error_class %> + <%= g.label :error_class, t(".error_class") %> +
+ +
+ <%= g.check_box :message %> + <%= g.label :message, t(".message") %> +
+ +
+ <%= g.label :backtrace_lines, t(".backtrace_lines") %> + <%= g.text_field :backtrace_lines %> +
+ +
+ <%= g.check_box :component %> + <%= g.label :component, t(".component") %> +
+ +
+ <%= g.check_box :action %> + <%= g.label :action, t(".action") %> +
+ +
+ <%= g.check_box :environment_name %> + <%= g.label :environment_name, t(".environment_name") %> +
+<% end %> diff --git a/app/views/shared/_notice_fingerprinter.html.haml b/app/views/shared/_notice_fingerprinter.html.haml deleted file mode 100644 index 64db1511d1..0000000000 --- a/app/views/shared/_notice_fingerprinter.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -= f.fields_for :notice_fingerprinter do |g| - .checkbox - = g.check_box :error_class - = g.label :error_class, t('.error_class') - - .checkbox - = g.check_box :message - = g.label :message, t('.message') - - %div - = g.label :backtrace_lines, t('.backtrace_lines') - = g.text_field :backtrace_lines - - .checkbox - = g.check_box :component - = g.label :component, t('.component') - - .checkbox - = g.check_box :action - = g.label :action, t('.action') - - .checkbox - = g.check_box :environment_name - = g.label :environment_name, t('.environment_name') \ No newline at end of file diff --git a/app/views/shared/_session.html.erb b/app/views/shared/_session.html.erb new file mode 100644 index 0000000000..f0dc998fed --- /dev/null +++ b/app/views/shared/_session.html.erb @@ -0,0 +1,13 @@ +<% if current_user %> + +<% end %> diff --git a/app/views/shared/_session.html.haml b/app/views/shared/_session.html.haml deleted file mode 100644 index 778f07e248..0000000000 --- a/app/views/shared/_session.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -- if current_user - %ul#session-links - %li - = link_to destroy_session_path(:user), :id => 'sign-out', :method => :delete do - %i.fa.fa-circle - = t('.sign_out') - - %li= link_to t('.edit_profile'), edit_user_path(current_user), :id => 'edit-profile' diff --git a/app/views/site_config/index.html.erb b/app/views/site_config/index.html.erb new file mode 100644 index 0000000000..2042b35a07 --- /dev/null +++ b/app/views/site_config/index.html.erb @@ -0,0 +1,20 @@ +<% content_for :title, t(".title") %> + +<% content_for :action_bar do %> + <%= link_to t(".cancel"), users_path, class: "button" %> +<% end %> + +<%= form_for @config, url: site_config_index_path, method: :patch do |f| %> + <%= errors_for @config %> + +
+ <%= t("notice_fingerprinter.title") %> +

<%= t("notice_fingerprinter.text") %>

+ + <%= render "shared/notice_fingerprinter", f: f %> +
+ +
+ <%= f.submit t(".update_config") %> +
+<% end %> diff --git a/app/views/site_config/index.html.haml b/app/views/site_config/index.html.haml deleted file mode 100644 index ebc4216eb9..0000000000 --- a/app/views/site_config/index.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- content_for :title, t('.title') - -- content_for :action_bar, link_to(t('.cancel'), users_path, :class => 'button') - -= form_for @config, url: site_config_index_path, method: 'put' do |f| - = errors_for @config - - %fieldset - %legend= t('notice_fingerprinter.title') - %p= t('notice_fingerprinter.text') - - = render 'shared/notice_fingerprinter', :f => f - - %div.buttons= f.submit t('.update_config') diff --git a/app/views/users/_fields.html.erb b/app/views/users/_fields.html.erb new file mode 100644 index 0000000000..b72d125a11 --- /dev/null +++ b/app/views/users/_fields.html.erb @@ -0,0 +1,54 @@ +<%# locals: (f:, user:) %> + +<%= errors_for user %> + +
+ <%= f.label :name, t(".name") %> + <%= f.text_field :name %> +
+ +<% if Errbit::Config.user_has_username %> +
+ <%= f.label :username, t(".username") %> + <%= f.text_field :username, autocomplete: "username" %> +
+<% end %> + +
+ <%= f.label :email, t(".email") %> + <%= f.text_field :email, autocomplete: "email" %> +
+ +<% if Errbit::Config.github_authentication %> +
+ <%= f.label :github_login, "#{Errbit::Config.github_site_title} login" %> + <%= f.text_field :github_login %> +
+<% end %> + +
+ <%= f.label :per_page, t(".entries") %> + <%= f.select :per_page, [10, 20, 30, 50, 75, 100] %> +
+ +
+ <%= f.label :time_zone, t(".time_zone") %> + <%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.us_zones %> +
+ +
+ <%= f.label :password, t(".password") %> + <%= f.password_field :password, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation, t(".password_confirmation") %> + <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +<% if current_user.admin? %> +
+ <%= f.check_box :admin %> + <%= f.label :admin, t(".admin") %> +
+<% end %> diff --git a/app/views/users/_fields.html.haml b/app/views/users/_fields.html.haml deleted file mode 100644 index 33b25d4eb2..0000000000 --- a/app/views/users/_fields.html.haml +++ /dev/null @@ -1,40 +0,0 @@ -= errors_for user - -.required - = f.label :name, t('.name') - = f.text_field :name - -- if Errbit::Config.user_has_username - .required - = f.label :username, t('.username') - = f.text_field :username - -.required - = f.label :email, t('.email') - = f.text_field :email - -- if Errbit::Config.github_authentication - = f.label :github_login, "#{Errbit::Config.github_site_title} login" - = f.text_field :github_login - -.required - = f.label :per_page, t('.entries') - = f.select :per_page, [10, 20, 30, 50, 75, 100] - -.required - = f.label :time_zone, t('.time_zone') - = f.time_zone_select :time_zone, ActiveSupport::TimeZone.us_zones - -.required - = f.label :password, t('.password') - = f.password_field :password, :autocomplete => "off" - -.required - = f.label :password_confirmation, t('.password_confirmation') - = f.password_field :password_confirmation - -- if current_user.admin? - .checkbox - = f.check_box :admin - = f.label :admin, t('.admin') - diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb new file mode 100644 index 0000000000..2c92cdacf7 --- /dev/null +++ b/app/views/users/edit.html.erb @@ -0,0 +1,15 @@ +<% content_for :title, "#{t(".title")} #{@user.name}" %> + +<% content_for :action_bar do %> + <%= render "shared/link_github_account", user: @user %> + <%= render "shared/link_google_account", user: @user %> + <%= link_to t(".cancel"), user_path(@user), class: "button" %> +<% end %> + +<%= form_for @user, html: {autocomplete: "off"} do |f| %> + <%= render "fields", f: f, user: @user %> + +
+ <%= f.submit t(".update_user") %> +
+<% end %> diff --git a/app/views/users/edit.html.haml b/app/views/users/edit.html.haml deleted file mode 100644 index 0792cdc576..0000000000 --- a/app/views/users/edit.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -- content_for :title, "#{I18n.t('users.edit.title')} #{user.name}" -- content_for :action_bar do - = render 'shared/link_github_account', :user => user - = render 'shared/link_google_account' - = link_to(t('.cancel'), user_path(user), :class => 'button') - -= form_for user, :html => {:autocomplete => "off"} do |f| - = render 'fields', :f => f - - %div.buttons= f.submit t('.update_user') diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb new file mode 100644 index 0000000000..472964bc7e --- /dev/null +++ b/app/views/users/index.html.erb @@ -0,0 +1,44 @@ +<% content_for :title, t(".title") %> + +<% content_for :action_bar do %> + + <%= link_to new_user_path do %> + + <%= t(".new") %> + <% end %> + +<% end %> + + + + + <% if Errbit::Config.use_gravatar %> + + <% end %> + + <% if Errbit::Config.user_has_username %> + + <% end %> + + + + + + + <% @users.each do |user| %> + + <% if Errbit::Config.use_gravatar %> + + <% end %> + + <% if Errbit::Config.user_has_username %> + + <% end %> + + + + <% end %> + +
 <%= t(".name") %><%= t(".username") %><%= t(".email") %><%= t(".admin") %>
<%= gravatar_tag(user.email, s: 24) %><%= link_to user.name, user_path(user) %><%= user.username %><%= user.email %><%= user.admin? ? t(".admin_yes") : t(".admin_no") %>
+ +<%= paginate @users %> diff --git a/app/views/users/index.html.haml b/app/views/users/index.html.haml deleted file mode 100644 index 6201d97901..0000000000 --- a/app/views/users/index.html.haml +++ /dev/null @@ -1,29 +0,0 @@ -- content_for :title, t('.title') -- content_for :action_bar do - %span - = link_to(new_user_path) do - %i.fa.fa-plus-circle - = t('.new') - -%table.users - %thead - %tr - - if Errbit::Config.use_gravatar - %th - %th= t('.name') - - if Errbit::Config.user_has_username - %th= t('.username') - %th.main= t('.email') - %th= t('.admin') - %tbody - - users.each do |user| - %tr.user_list - - if Errbit::Config.use_gravatar - %td= gravatar_tag user.email, :s => 24 - %td.nowrap= link_to user.name, user_path(user) - - if Errbit::Config.user_has_username - %td= user.username - %td= user.email - %td= user.admin? ? t('.admin_yes') : t('.admin_no') -= paginate users - diff --git a/app/views/users/new.html.erb b/app/views/users/new.html.erb new file mode 100644 index 0000000000..90c033c704 --- /dev/null +++ b/app/views/users/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, t(".title") %> + +<% content_for :action_bar, link_to(t(".cancel"), users_path, class: "button") %> + +<%= form_for @user do |f| %> + <%= render "fields", f: f, user: @user %> + +
+ <%= f.submit t(".add_user") %> +
+<% end %> diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml deleted file mode 100644 index 1cb5539861..0000000000 --- a/app/views/users/new.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -- content_for :title, t('.title') -- content_for :action_bar, link_to(t('.cancel'), users_path, :class => 'button') - -= form_for user do |f| - - = render 'fields', :f => f - - %div.buttons= f.submit t('.add_user') diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb new file mode 100644 index 0000000000..3ae98c305b --- /dev/null +++ b/app/views/users/show.html.erb @@ -0,0 +1,50 @@ +<% content_for :title, @user.name %> + +<% if Errbit::Config.use_gravatar && (gravatar = gravatar_url(@user.email, s: 86)) %> + <% content_for :title_avatar do %> + <%= image_tag gravatar, class: "title-avatar", alt: "" %> + <% end %> +<% end %> + +<% content_for :action_bar do %> + <%= render "shared/link_github_account", user: @user %> + <%= render "shared/link_google_account", user: @user %> + <%= link_to t(".edit"), edit_user_path(@user), class: "button" %> + <%= link_to t(".destroy"), user_path(@user), + method: :delete, + data: { + confirm: t(".confirm_delete") + }, + class: "delete button" %> +<% end %> + + + + + + + + <% if Errbit::Config.user_has_username %> + + + + + <% end %> + + <% if Errbit::Config.github_authentication && @user.github_login.present? %> + + + + + <% end %> + + + + + + + + + + +
<%= t(".email") %><%= @user.email %>
<%= t(".username") %><%= @user.username %>
<%= "#{Errbit::Config.github_site_title} Login" %><%= link_to @user.github_login, "#{Errbit::Config.github_url}/#{@user.github_login}" %>
<%= t(".admin") %><%= @user.admin? ? t(".admin_yes") : t(".admin_no") %>
<%= t(".created") %><%= @user.created_at.to_fs(:micro) %>
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml deleted file mode 100644 index 085d0244b0..0000000000 --- a/app/views/users/show.html.haml +++ /dev/null @@ -1,33 +0,0 @@ -- content_for :title, user.name - -- if Errbit::Config.use_gravatar && gravatar = gravatar_url(user.email, :s => 86) - - content_for :title_style do - background: url('#{gravatar}') no-repeat; - padding-left: 106px; - -- content_for :action_bar do - = render 'shared/link_github_account' - = render 'shared/link_google_account' - = link_to t('.edit'), edit_user_path(user), :class => 'button' - = link_to t('.destroy'), user_path(user), :method => :delete, - :data => { :confirm => t('users.confirm_delete') }, :class => 'delete button' - -%table.single_user - %tr - %th= t('.email') - %td.main= user.email - - if Errbit::Config.user_has_username - %tr - %th= t('.username') - %td.main= user.username - - if Errbit::Config.github_authentication && user.github_login.present? - %tr - %th= "#{Errbit::Config.github_site_title} Login" - %td.main= link_to user.github_login, "#{Errbit::Config.github_url}/#{user.github_login}" - %tr - %th= t('.admin') - %td= user.admin? ? 'Y' : 'N' - %tr - %th= t('.created') - %td= user.created_at.to_s(:micro) - diff --git a/autotest/discover.rb b/autotest/discover.rb deleted file mode 100644 index f421dc5ff9..0000000000 --- a/autotest/discover.rb +++ /dev/null @@ -1,2 +0,0 @@ -Autotest.add_discovery { "rails" } -Autotest.add_discovery { "rspec2" } diff --git a/bin/brakeman b/bin/brakeman new file mode 100755 index 0000000000..a2eeb02f0c --- /dev/null +++ b/bin/brakeman @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'brakeman' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("brakeman", "brakeman") diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 0000000000..42c7fd7c5c --- /dev/null +++ b/bin/bundle @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/bundle-audit b/bin/bundle-audit new file mode 100755 index 0000000000..a0e7ba0e8b --- /dev/null +++ b/bin/bundle-audit @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle-audit' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("bundler-audit", "bundle-audit") diff --git a/bin/bundler-audit b/bin/bundler-audit new file mode 100755 index 0000000000..e2ef22690c --- /dev/null +++ b/bin/bundler-audit @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "bundler/audit/cli" + +ARGV.concat %w[ --config config/bundler-audit.yml ] if ARGV.empty? || ARGV.include?("check") +Bundler::Audit::CLI.start diff --git a/bin/ci b/bin/ci new file mode 100755 index 0000000000..4137ad5bb0 --- /dev/null +++ b/bin/ci @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "active_support/continuous_integration" + +CI = ActiveSupport::ContinuousIntegration +require_relative "../config/ci.rb" diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000000..5f91c20545 --- /dev/null +++ b/bin/dev @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +exec "./bin/rails", "server", *ARGV diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 0000000000..cbb10b281d --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ] && [ -f /usr/lib/*/libjemalloc.so.2 ]; then + export LD_PRELOAD="$(echo /usr/lib/*/libjemalloc.so.2)" +fi + +exec "${@}" diff --git a/bin/herb b/bin/herb new file mode 100755 index 0000000000..e19a3c8394 --- /dev/null +++ b/bin/herb @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'herb' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("herb", "herb") diff --git a/bin/importmap b/bin/importmap new file mode 100755 index 0000000000..36502ab16c --- /dev/null +++ b/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/bin/puma b/bin/puma new file mode 100755 index 0000000000..ef0fb295ea --- /dev/null +++ b/bin/puma @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'puma' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("puma", "puma") diff --git a/bin/pumactl b/bin/pumactl new file mode 100755 index 0000000000..51971fd480 --- /dev/null +++ b/bin/pumactl @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'pumactl' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("puma", "pumactl") diff --git a/bin/rails b/bin/rails index bd79dce518..efc0377492 100755 --- a/bin/rails +++ b/bin/rails @@ -1,6 +1,4 @@ #!/usr/bin/env ruby -# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. - -APP_PATH = File.expand_path('../../config/application', __FILE__) -require File.expand_path('../../config/boot', __FILE__) -require 'rails/commands' +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000000..4fbf10b960 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 0000000000..93e191c2ff --- /dev/null +++ b/bin/rspec @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rspec' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rspec-core", "rspec") diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 0000000000..d73598dc22 --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rubocop' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000000..2285724c40 --- /dev/null +++ b/bin/setup @@ -0,0 +1,26 @@ +#!/usr/bin/env ruby +require "fileutils" + +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system("bundle check") || system!("bundle install") + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end +end diff --git a/bin/standardize b/bin/standardize new file mode 100755 index 0000000000..b3b149aba5 --- /dev/null +++ b/bin/standardize @@ -0,0 +1,8 @@ +#! /bin/bash + +set -eux + +yarn prettier --write "*.{yml,yaml}" \ + ".github/*.{yml,yaml}" \ + ".github/**/*.{yml,yaml}" \ + "config/**/*.{yml,yaml}" diff --git a/bin/standardrb b/bin/standardrb new file mode 100755 index 0000000000..3b77a1565e --- /dev/null +++ b/bin/standardrb @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'standardrb' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("standard", "standardrb") diff --git a/bin/thrust b/bin/thrust new file mode 100755 index 0000000000..b1c45d18de --- /dev/null +++ b/bin/thrust @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'thrust' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thruster", "thrust") diff --git a/bin/update-bootstrap-icons b/bin/update-bootstrap-icons new file mode 100755 index 0000000000..6c850198bd --- /dev/null +++ b/bin/update-bootstrap-icons @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# e.g. "v1.13.1" +VERSION=$1 + +if [ -z "$VERSION" ]; then + echo "Usage: $0 v. e.g. v1.13.1" + exit 1 +fi + +mkdir -p app/assets/images/icons + +curl -s "https://raw.githubusercontent.com/twbs/icons/refs/tags/$VERSION/icons/arrow-repeat.svg" > app/assets/images/icons/arrow-repeat.svg +curl -s "https://raw.githubusercontent.com/twbs/icons/refs/tags/$VERSION/icons/check-circle.svg" > app/assets/images/icons/check-circle.svg +curl -s "https://raw.githubusercontent.com/twbs/icons/refs/tags/$VERSION/icons/floppy.svg" > app/assets/images/icons/floppy.svg +curl -s "https://raw.githubusercontent.com/twbs/icons/refs/tags/$VERSION/icons/github.svg" > app/assets/images/icons/github.svg +curl -s "https://raw.githubusercontent.com/twbs/icons/refs/tags/$VERSION/icons/google.svg" > app/assets/images/icons/google.svg +curl -s "https://raw.githubusercontent.com/twbs/icons/refs/tags/$VERSION/icons/hand-thumbs-up-fill.svg" > app/assets/images/icons/hand-thumbs-up-fill.svg +curl -s "https://raw.githubusercontent.com/twbs/icons/refs/tags/$VERSION/icons/pencil-square.svg" > app/assets/images/icons/pencil-square.svg +curl -s "https://raw.githubusercontent.com/twbs/icons/refs/tags/$VERSION/icons/person-fill-add.svg" > app/assets/images/icons/person-fill-add.svg +curl -s "https://raw.githubusercontent.com/twbs/icons/refs/tags/$VERSION/icons/plus-circle.svg" > app/assets/images/icons/plus-circle.svg +curl -s "https://raw.githubusercontent.com/twbs/icons/refs/tags/$VERSION/icons/trash.svg" > app/assets/images/icons/trash.svg +curl -s "https://raw.githubusercontent.com/twbs/icons/refs/tags/$VERSION/icons/x-circle.svg" > app/assets/images/icons/x-circle.svg diff --git a/config.ru b/config.ru index 76d9bb006c..2e0308469b 100644 --- a/config.ru +++ b/config.ru @@ -1,5 +1,8 @@ +# frozen_string_literal: true + # This file is used by Rack-based servers to start the application. -require ::File.expand_path('../config/environment', __FILE__) -use Rack::Deflater +require_relative "config/environment" + run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb index 9e209c6087..4c6541869a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,9 +1,23 @@ -require File.expand_path('../boot', __FILE__) +# frozen_string_literal: true -require 'action_controller/railtie' -require 'action_mailer/railtie' -# require 'mongoid/railtie' -require 'sprockets/railtie' +require_relative "boot" + +require "rails" +# Pick the frameworks you want: +require "active_model/railtie" +require "active_job/railtie" +# require "active_record/railtie" +# require "active_storage/engine" +require "action_controller/railtie" +require "action_mailer/railtie" +# require "action_mailbox/engine" +# require "action_text/engine" +require "action_view/railtie" +# require "action_cable/engine" +# require "rails/test_unit/railtie" + +require_relative "../app/lib/errbit/version" +require_relative "../lib/errbit/self_error_reporter" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. @@ -11,44 +25,70 @@ module Errbit class Application < Rails::Application - # Settings in config/environments/* take precedence over those specified here. - # Application configuration should go into files in config/initializers - # -- all .rb files in that directory are automatically loaded. + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.1 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: ["assets", "tasks"]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + + # Don't generate system test files. + config.generators.system_tests = nil # Custom directories with classes and modules you want to eager load. - config.eager_load_paths << Rails.root.join('lib').to_s + config.eager_load_paths << Rails.root.join("lib").to_s - config.before_initialize do - config.secret_key_base = Errbit::Config.secret_key_base - config.serve_static_files = Errbit::Config.serve_static_assets + config.middleware.insert_after( + ActionDispatch::DebugExceptions, + Errbit::SelfErrorReporter::Middleware + ) + + initializer "errbit.airbrake" do + Airbrake.configure do |config| + config.environment = Rails.env.to_s + config.root_directory = Rails.root + config.app_version = Errbit::Version.to_s + end end - initializer 'errbit.mongoid', before: 'mongoid.load-config' do - require Rails.root.join('config/mongo') + initializer "errbit.disable_external_airbrake_middleware", after: "airbrake.middleware" do |app| + app.config.middleware.delete(Airbrake::Rack::Middleware) end - # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. - # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - # config.time_zone = 'Central Time (US & Canada)' + config.before_initialize do + config.secret_key_base = Errbit::Config.secret_key_base + end - # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. - # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] - # config.i18n.default_locale = :de + initializer "errbit.mongoid", before: "mongoid.load-config" do + require Rails.root.join("config/mongo") + end # > rails generate - config config.generators do |g| g.orm :mongoid - g.template_engine :haml + g.template_engine :erb g.test_framework :rspec, fixture: false - g.fixture_replacement :fabrication + g.fixture_replacement :factory_bot end # IssueTracker subclasses use inheritance, so preloading models provides querying consistency in dev mode. config.mongoid.preload_models = true # Configure Devise mailer to use our mailer layout. - config.to_prepare { Devise::Mailer.layout 'mailer' } + config.to_prepare { Devise::Mailer.layout "mailer" } + + config.active_job.queue_adapter = :async - config.active_job.queue_adapter = :sucker_punch + # Compiles the ERB template with the frozen_string_literal true magic comment + config.action_view.frozen_string_literal = true end end diff --git a/config/boot.rb b/config/boot.rb index 52c08dc45d..aef6d031ee 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,22 +1,6 @@ -require 'rubygems' +# frozen_string_literal: true -detected_ruby_version = Gem::Version.new(RUBY_VERSION.dup) -required_ruby_version = Gem::Version.new('2.1.0') # minimum supported version +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) -if detected_ruby_version < required_ruby_version - fail "RUBY_VERSION must be at least #{required_ruby_version}, " \ - "detected RUBY_VERSION #{RUBY_VERSION}" -end - -# Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) - -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) -require 'json' - -begin - # try to use Yajl, the json_gem compatibility layer must be loaded after json - require 'yajl/json_gem' -rescue LoadError - warn "JSON: unable to load Yajl; just using the json gem" -end +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/bundler-audit.yml b/config/bundler-audit.yml new file mode 100644 index 0000000000..e74b3af949 --- /dev/null +++ b/config/bundler-audit.yml @@ -0,0 +1,5 @@ +# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit. +# CVEs that are not relevant to the application can be enumerated on the ignore list below. + +ignore: + - CVE-THAT-DOES-NOT-APPLY diff --git a/config/ci.rb b/config/ci.rb new file mode 100644 index 0000000000..6aca6b5fab --- /dev/null +++ b/config/ci.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Run using bin/ci + +CI.run do + step "Setup", "bin/setup --skip-server" + + step "Style: Ruby", "bin/rubocop" + + step "Security: Gem audit", "bin/bundler-audit" + step "Security: Importmap vulnerability audit", "bin/importmap audit" + step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" + + # Optional: set a green GitHub commit status to unblock PR merge. + # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`. + # if success? + # step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" + # else + # failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again." + # end +end diff --git a/config/environment.rb b/config/environment.rb index fc46334ad7..47c9982f52 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,19 +1,21 @@ +# frozen_string_literal: true + # Load the Rails application. -require File.expand_path('../application', __FILE__) +require_relative "application" # Load up Errbit::Config with values from the environment -require Rails.root.join('config/load') +require Rails.root.join("config/load") + +case Errbit::Config.log_location +when "STDOUT" + # Skip. This is rails default behavior +when "Syslog::Logger" + require "syslog/logger" -if Errbit::Config.log_location == 'STDOUT' - Rails.logger = ActiveSupport::Logger.new STDOUT -elsif Errbit::Config.log_location == 'Syslog::Logger' - require 'syslog/logger' - Rails.logger = Syslog::Logger.new('errbit', Syslog::LOG_LOCAL0) + Rails.logger = Syslog::Logger.new("errbit", Syslog::LOG_LOCAL0) else - Rails.logger = ActiveSupport::Logger.new Errbit::Config.log_location + Rails.logger = ActiveSupport::Logger.new(Errbit::Config.log_location) end -Rails.logger.level = Errbit::Config.log_level.to_sym - # Initialize the Rails application. Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb index 98b1e563c7..fa7934d629 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,37 +1,69 @@ +# frozen_string_literal: true + +require "active_support/core_ext/integer/time" + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # In the development environment your application's code is reloaded on - # every request. This slows down response time but is perfect for development - # since you don't have to restart the web server when you make code changes. - config.cache_classes = false + # Make code changes take effect immediately without server restart. + config.enable_reloading = true # Do not eager load code on boot. config.eager_load = false - # Show full error reports and disable caching. - config.consider_all_requests_local = true - config.action_controller.perform_caching = false + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing. + config.server_timing = true + + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + config.public_file_server.headers = {"cache-control" => "public, max-age=#{2.days.to_i}"} + else + config.action_controller.perform_caching = false + end + + # Change to :null_store to avoid any caching. + config.cache_store = :memory_store # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false + # Make template changes take effect immediately. + config.action_mailer.perform_caching = false + + # Set localhost to be used by links generated in mailer templates. + config.action_mailer.default_url_options = {host: "localhost", port: 3000} + # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log - # Raise an error on page load if there are pending migrations. - # config.active_record.migration_error = :page_load + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Highlight code that triggered redirect in logs. + config.action_dispatch.verbose_redirect_logs = true + + # Raises error for missing translations. + config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true - # Debug mode disables concatenation and preprocessing of assets. - # This option may cause significant delays in view rendering with a large - # number of complex assets. - config.assets.debug = true + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + # config.generators.apply_rubocop_autocorrect_after_generate! - # Adds additional error checking when serving assets at runtime. - # Checks for improperly declared sprockets dependencies. - # Raises helpful error messages. - config.assets.raise_runtime_errors = false + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + config.file_watcher = ActiveSupport::EventedFileUpdateChecker - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true + # rails mail preview + config.action_mailer.preview_paths << "#{Rails.root}/spec/mailers/previews" end diff --git a/config/environments/production.rb b/config/environments/production.rb index 3cda108049..9173c75e26 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,67 +1,89 @@ +# frozen_string_literal: true + +require "active_support/core_ext/integer/time" + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. - config.cache_classes = true + config.enable_reloading = false - # Eager load code on boot. This eager loads most of Rails and - # your application in memory, allowing both threaded web servers - # and those relying on copy on write to perform better. - # Rake tasks automatically ignore this option for performance. + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). config.eager_load = true - # Full error reports are disabled and caching is turned on. - config.consider_all_requests_local = false + # Full error reports are disabled. + config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. config.action_controller.perform_caching = true - # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier - # config.assets.css_compressor = :sass + # Cache assets for far-future expiry since they are all digest stamped. + config.public_file_server.headers = {"cache-control" => "public, max-age=#{1.year.to_i}"} - # Do not fallback to assets pipeline if a precompiled asset is missed. - config.assets.compile = false + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + config.assume_ssl = true - # Generate digests for assets URLs. - config.assets.digest = true + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # + # Disable force_ssl for case where the app is running behind a reverse proxy + # that terminates SSL and forwards requests to the app over HTTP. This is + # common in containerized deployments where SSL termination is handled by + # a load balancer or reverse proxy. + # + # Default: force_ssl is true. + config.force_ssl = ENV.fetch("ERRBIT_DISABLE_FORCE_SSL") == "false" - # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } - # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + # Log to STDOUT with the current request id as a default log tag. + config.log_tags = [:request_id] + config.logger = ActiveSupport::TaggedLogging.logger($stdout) - # Set to :debug to see everything in the log. - config.log_level = :info + # Change to "debug" to log everything (including potentially personally-identifiable information!). + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") - # Prepend all log lines with the following tags. - # config.log_tags = [ :subdomain, :uuid ] + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = "/up" - # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + # Don't log any deprecations. + config.active_support.report_deprecations = false - # Use a different cache store in production. + # Replace the default in-process memory cache store with a durable alternative. # config.cache_store = :mem_cache_store - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = "http://assets.example.com" + # Replace the default in-process and non-durable queuing backend for Active Job. + # config.active_job.queue_adapter = :resque # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false + # Set host to be used by links generated in mailer templates. + # config.action_mailer.default_url_options = { host: "example.com" } + + # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit. + # config.action_mailer.smtp_settings = { + # user_name: Rails.application.credentials.dig(:smtp, :user_name), + # password: Rails.application.credentials.dig(:smtp, :password), + # address: "smtp.example.com", + # port: 587, + # authentication: :plain + # } + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true - # Send deprecation notices to registered listeners. - config.active_support.deprecation = :notify - - # Disable automatic flushing of the log to improve performance. - # config.autoflush_log = false - - # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new - - # Do not dump schema after migrations. - # config.active_record.dump_schema_after_migration = false + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } end diff --git a/config/environments/test.rb b/config/environments/test.rb index 81be655707..fadddbb184 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,27 +1,31 @@ +# frozen_string_literal: true + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! - config.cache_classes = true + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false - # Do not eager load code on boot. This avoids loading your whole application - # just for the purpose of running a single test. If you are using a tool that - # preloads Rails for running tests, you may have to set it to true. - config.eager_load = false + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? - # Configure static asset server for tests with Cache-Control for performance. - config.serve_static_files = true - config.static_cache_control = 'public, max-age=3600' + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = {"cache-control" => "public, max-age=3600"} - # Show full error reports and disable caching. - config.consider_all_requests_local = true - config.action_controller.perform_caching = false + # Show full error reports. + config.consider_all_requests_local = true + config.cache_store = :null_store - # Raise exceptions instead of rendering exception templates. - config.action_dispatch.show_exceptions = false + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false @@ -31,9 +35,18 @@ # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = {host: "example.com"} + # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true + # Raises error for missing translations. + config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true end diff --git a/config/importmap.rb b/config/importmap.rb new file mode 100644 index 0000000000..684c40489c --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Pin npm packages by running ./bin/importmap + +# Enable integrity calculation globally +enable_integrity! + +pin "application" +pin "@hotwired/stimulus", to: "stimulus.min.js" +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" + +pin_all_from "app/javascript/controllers", under: "controllers" + +pin "@stimulus-components/reveal", to: "@stimulus-components--reveal.js" # @5.0.0 diff --git a/config/initializers/action_mailer.rb b/config/initializers/action_mailer.rb index 4728504a57..fe83728b14 100644 --- a/config/initializers/action_mailer.rb +++ b/config/initializers/action_mailer.rb @@ -1,15 +1,17 @@ +# frozen_string_literal: true + # Set SMTP settings if given. if Errbit::Config.email_delivery_method == :smtp ActionMailer::Base.delivery_method = :smtp ActionMailer::Base.smtp_settings = { - address: Errbit::Config.smtp_address, - port: Errbit::Config.smtp_port, - authentication: Errbit::Config.smtp_authentication, - user_name: Errbit::Config.smtp_user_name, - password: Errbit::Config.smtp_password, - domain: Errbit::Config.smtp_domain, + address: Errbit::Config.smtp_address, + port: Errbit::Config.smtp_port, + authentication: Errbit::Config.smtp_authentication, + user_name: Errbit::Config.smtp_user_name, + password: Errbit::Config.smtp_password, + domain: Errbit::Config.smtp_domain, enable_starttls_auto: Errbit::Config.smtp_enable_starttls_auto, - openssl_verify_mode: Errbit::Config.smtp_openssl_verify_mode + openssl_verify_mode: Errbit::Config.smtp_openssl_verify_mode } end @@ -25,10 +27,8 @@ # Set config specific values (ActionMailer::Base.default_url_options ||= {}).tap do |default| options_from_config = { - host: Errbit::Config.host, - port: Errbit::Config.port, - protocol: Errbit::Config.protocol - }.select { |_k, v| v } + host: Errbit::Config.host + }.select { |_, v| v } default.reverse_merge!(options_from_config) end diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000000..6d56e43900 --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index d2f4ec33a6..8544c07c55 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = '1.0' +Rails.application.config.assets.version = "1.0" -# Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -# Rails.application.config.assets.precompile += %w( search.js ) +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb index 59385cdf37..74f30e8875 100644 --- a/config/initializers/backtrace_silencers.rb +++ b/config/initializers/backtrace_silencers.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. -# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } +# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } -# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. -# Rails.backtrace_cleaner.remove_silencers! +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code +# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". +Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000000..67407ba500 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +Rails.application.configure do + config.content_security_policy do |policy| + policy.default_src :self + policy.base_uri :self + policy.connect_src :self + policy.font_src :self + policy.form_action :self + policy.frame_src :self + policy.frame_ancestors :self + policy.img_src :self, "https://secure.gravatar.com", :data + policy.object_src :none + policy.script_src :self + policy.style_src :self + policy.upgrade_insecure_requests if Rails.env.production? + end + + config.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) } + config.content_security_policy_nonce_directives = ["script-src", "style-src"] + config.content_security_policy_nonce_auto = true +end diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb index 54516e3f23..ee8dff9c99 100644 --- a/config/initializers/cookies_serializer.rb +++ b/config/initializers/cookies_serializer.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. -Rails.application.config.action_dispatch.cookies_serializer = :hybrid +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb new file mode 100644 index 0000000000..f37dc58eba --- /dev/null +++ b/config/initializers/deprecations.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +deprecator = ActiveSupport::Deprecation.new(nil, "Errbit") + +if ENV["USER_GEMFILE"].present? + # Make it error in v0.11.0 release and remove in v0.12.0 + deprecator.warn( + "ENV['USER_GEMFILE'] support is removed and has no effect in Errbit v0.10.0. " \ + "Remove it from configuration. " \ + "From Errbit v0.10.0+ it's always 'UserGemfile'." + ) +end + +if ENV["SERVE_STATIC_ASSETS"].present? + # Make it error in v0.11.0 release and remove in v0.12.0 + deprecator.warn( + "ENV['SERVE_STATIC_ASSETS'] support is removed and has no effect in Errbit v0.10.0. " \ + "Remove it from configuration." + ) +end + +if ENV["RACK_ENV"].present? + # Make it error in v0.11.0 release and remove in v0.12.0 + deprecator.warn( + "ENV['RACK_ENV'] support is removed and has no effect in Errbit v0.10.0. " \ + "Replace it with build-in Ruby on Rails ENV['RAILS_ENV']." + ) +end + +if ENV["ERRBIT_ENFORCE_SSL"].present? + # Make it error in v0.11.0 release and remove in v0.12.0 + deprecator.warn( + "ENV['ERRBIT_ENFORCE_SSL'] support is removed and has no effect in Errbit v0.10.0. " \ + "You should run Errbit behind reverse proxy with HTTPS support. " \ + "e.g. Traefik." + ) +end + +if ENV["ERRBIT_PROTOCOL"].present? + # Make it error in v0.11.0 release and remove in v0.12.0 + deprecator.warn( + "ENV['ERRBIT_PROTOCOL'] support is removed and has no effect in Errbit v0.10.0. " \ + "When you are running Errbit behind reverse proxy with HTTPS support, protocol is already set to https " \ + "and can't be changed." + ) +end + +if ENV["ERRBIT_PORT"].present? + # Make it error in v0.11.0 release and remove in v0.12.0 + deprecator.warn( + "ENV['ERRBIT_PORT'] support is removed and has no effect in Errbit v0.10.0. " \ + "When you are running Errbit behind reverse proxy with HTTPS support, port is already set to 443 " \ + "and can't be changed." + ) +end + +if ENV["ERRBIT_LOG_LEVEL"].present? + # Make it error in v0.11.0 release and remove in v0.12.0 + deprecator.warn( + "ENV['ERRBIT_LOG_LEVEL'] support is removed and has no effect in Errbit v0.10.0. " \ + "Replace it with build-in Ruby on Rails ENV['RAILS_LOG_LEVEL']." + ) +end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 004816601e..1ed024ca2d 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,5 +1,11 @@ # frozen_string_literal: true +# Assuming you have not yet modified this file, each configuration option below +# is set to its default value. Note that some are commented out while others +# are not: uncommented lines are intended to protect your configuration from +# breaking changes in upgrades (i.e., in the event that future versions of +# Devise change the default values for those options). +# # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. Devise.setup do |config| @@ -10,6 +16,10 @@ # by default. You can change it below and use your own secret key. # config.secret_key = 'af6696b9c6d6e06a49eaf5aa9bee53425587a825c4bda9cae0fd1b494daf52dc75dff57e0dbed67fe99a87684d87f87cc0ab75b93ee9308f4ad08a425ddb3cd7' + # ==> Controller configuration + # Configure the parent class to the devise controllers. + # config.parent_controller = 'DeviseController' + # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class @@ -26,7 +36,7 @@ # Load and configure the ORM. Supports :active_record (default) and # :mongoid (bson_ext recommended) by default. Other ORMs may be # available as additional gems. - require 'devise/orm/mongoid' + require "devise/orm/mongoid" # ==> Configuration for any authentication mechanism # Configure which keys are used when authenticating a user. The default is @@ -64,7 +74,10 @@ # Tell if authentication through HTTP Auth is enabled. False by default. # It can be set to an array that will enable http authentication only for the # given strategies, for example, `config.http_authenticatable = [:database]` will - # enable it only for database authentication. The supported strategies are: + # enable it only for database authentication. + # For API-only applications to support authentication "out-of-the-box", you will likely want to + # enable this with :database unless you are using a custom strategy. + # The supported strategies are: # :database = Support basic authentication with authentication key + password # config.http_authenticatable = false @@ -99,15 +112,18 @@ # config.reload_routes = true # ==> Configuration for :database_authenticatable - # For bcrypt, this is the cost for hashing the password and defaults to 11. If + # For bcrypt, this is the cost for hashing the password and defaults to 12. If # using other algorithms, it sets how many times you want the password to be hashed. + # The number of stretches used for generating the hashed password are stored + # with the hashed password. This allows you to change the stretches without + # invalidating existing passwords. # # Limiting the stretches to just one in testing will increase the performance of # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use # a value less than 10 in other environments. Note that, for bcrypt (the default # algorithm), the cost increases exponentially with the number of stretches (e.g. # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). - config.stretches = Rails.env.test? ? 1 : 11 + config.stretches = Rails.env.test? ? 1 : 12 # Set up a pepper to generate the hashed password. # config.pepper = 'b4ea5c51c5d1b3dfd9601cdfc6ef6f281744d90fac9e68a8247fc732bad2113b8074fd45da01dbc910392a66fcf9d2bb3374c2ae3f8192108496392b94fdfeb7' @@ -122,8 +138,11 @@ # A period that the user is allowed to access the website even without # confirming their account. For instance, if set to 2.days, the user will be # able to access the website for two days without confirming their account, - # access will be blocked just in the third day. Default is 0.days, meaning - # the user cannot access the website without confirming their account. + # access will be blocked just in the third day. + # You can also set it to nil, which will allow the user to access the website + # without confirming their account. + # Default is 0.days, meaning the user cannot access the website without + # confirming their account. # config.allow_unconfirmed_access_for = 2.days # A period that the user is allowed to confirm their account before their @@ -159,7 +178,7 @@ # ==> Configuration for :validatable # Range for password length. - config.password_length = 6..128 + config.password_length = 8..128 # Email regex used to validate email formats. It simply asserts that # one (and only one) @ exists in the given string. This is mainly @@ -238,14 +257,14 @@ # ==> Navigation configuration # Lists the formats that should be treated as navigational. Formats like - # :html, should redirect to the sign in page when the user does not have + # :html should redirect to the sign in page when the user does not have # access, but formats like :xml or :json, should return 401. # # If you have any extra navigational formats, like :iphone or :mobile, you # should add them to the navigational formats lists. # # The "*/*" below is required to match Internet Explorer requests. - # config.navigational_formats = ['*/*', :html] + # config.navigational_formats = ['*/*', :html, :turbo_stream] # The default HTTP method used to sign out a resource. Default is :delete. config.sign_out_via = :delete @@ -257,12 +276,12 @@ if Errbit::Config.github_authentication || Rails.env.test? github_options = { - scope: Errbit::Config.github_access_scope.join(','), - skip_info: true, + scope: Errbit::Config.github_access_scope.join(","), + skip_info: true, client_options: { - site: Errbit::Config.github_api_url, + site: Errbit::Config.github_api_url, authorize_url: "#{Errbit::Config.github_url}/login/oauth/authorize", - token_url: "#{Errbit::Config.github_url}/login/oauth/access_token" + token_url: "#{Errbit::Config.github_url}/login/oauth/access_token" } } @@ -305,4 +324,19 @@ # When using OmniAuth, Devise cannot automatically set OmniAuth path, # so you need to do it manually. For the users scope, it would be: # config.omniauth_path_prefix = '/my_engine/users/auth' + + # ==> Hotwire/Turbo configuration + # When using Devise with Hotwire/Turbo, the http status for error responses + # and some redirects must match the following. The default in Devise for existing + # apps is `200 OK` and `302 Found` respectively, but new apps are generated with + # these new defaults that match Hotwire/Turbo behavior. + # Note: These might become the new default in future versions of Devise. + # config.responder.error_status = :unprocessable_content + # config.responder.redirect_status = :see_other + + # ==> Configuration for :registerable + + # When set to false, does not sign a user in automatically after their password is + # changed. Defaults to true, so a user is signed in automatically after changing a password. + # config.sign_in_after_change_password = true end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 4a994e1e7b..497ac1325c 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,4 +1,10 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. -# Configure sensitive parameters which will be filtered from the log file. -Rails.application.config.filter_parameters += [:password] +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc +] diff --git a/lib/recurse.rb b/config/initializers/hash.rb similarity index 94% rename from lib/recurse.rb rename to config/initializers/hash.rb index cfc046742f..24a6570f54 100644 --- a/lib/recurse.rb +++ b/config/initializers/hash.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Hash # Apply a block to hash, and recursively apply that block # to each sub-hash or +types+. diff --git a/config/initializers/hoptoad.rb b/config/initializers/hoptoad.rb deleted file mode 100644 index 004eb2adfd..0000000000 --- a/config/initializers/hoptoad.rb +++ /dev/null @@ -1,8 +0,0 @@ -HoptoadNotifier.configure do |config| - # Internal Errbit errors are stored locally, but we need - # to set a dummy API key so that HoptoadNotifier doesn't complain. - config.api_key = "---------" - - # Don't log error that causes 404 page - config.ignore << "Mongoid::Errors::DocumentNotFound" -end diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index ac033bf9dc..9e049dcc91 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,16 +1,18 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.plural /^(ox)$/i, '\1en' -# inflect.singular /^(ox)en/i, '\1' -# inflect.irregular 'person', 'people' +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym 'RESTful' +# inflect.acronym "RESTful" # end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index dc1899682b..be6fedc535 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: diff --git a/config/initializers/notification_services.rb b/config/initializers/notification_services.rb deleted file mode 100644 index a92a29effd..0000000000 --- a/config/initializers/notification_services.rb +++ /dev/null @@ -1,2 +0,0 @@ -# Include nested notification services models -include NotificationServices diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 550ccdede8..af1caf117c 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + OmniAuth.config.logger = Rails.logger diff --git a/config/initializers/overrides.rb b/config/initializers/overrides.rb deleted file mode 100644 index 906fa91baa..0000000000 --- a/config/initializers/overrides.rb +++ /dev/null @@ -1,7 +0,0 @@ -require Rails.root.join('lib/overrides/hoptoad_notifier/hoptoad_notifier') - -class BSON::ObjectId - def as_json(*_args) - to_s - end -end diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb new file mode 100644 index 0000000000..73acd708df --- /dev/null +++ b/config/initializers/permissions_policy.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# Define an application-wide HTTP permissions policy. For further +# information see: https://developers.google.com/web/updates/2018/06/feature-policy + +Rails.application.config.permissions_policy do |policy| + policy.camera :none + policy.gyroscope :none + policy.microphone :none + policy.usb :none + policy.fullscreen :none + policy.payment :none +end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index b00aaf017c..d674410e0f 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,8 +1,5 @@ -# Be sure to restart your server when you modify this file. +# frozen_string_literal: true -Rails.application.config.session_store :cookie_store, key: '_errbit_session' +# Be sure to restart your server when you modify this file. -# Use the database for sessions instead of the cookie-based default, -# which shouldn't be used to store highly confidential information -# (create the session table with "rake db:sessions:create") -# Rails.application.config.session_store :active_record_store +Rails.application.config.session_store :cookie_store, key: "_errbit_session" diff --git a/config/initializers/ssl_enforcer.rb b/config/initializers/ssl_enforcer.rb deleted file mode 100644 index f7edef7d4b..0000000000 --- a/config/initializers/ssl_enforcer.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Enforce SSL connections, if configured -if Errbit::Config.enforce_ssl - require 'rack/ssl-enforcer' - ActionMailer::Base.default_url_options[:protocol] = 'https://' - Rails.application.configure do - config.middleware.use Rack::SslEnforcer - end -end diff --git a/config/initializers/sucker_punch.rb b/config/initializers/sucker_punch.rb deleted file mode 100644 index 755f0e89a7..0000000000 --- a/config/initializers/sucker_punch.rb +++ /dev/null @@ -1 +0,0 @@ -require 'sucker_punch/async_syntax' diff --git a/config/initializers/time_formats.rb b/config/initializers/time_formats.rb index f4d3054083..f86b6fdc12 100644 --- a/config/initializers/time_formats.rb +++ b/config/initializers/time_formats.rb @@ -1,2 +1,4 @@ -Time::DATE_FORMATS[:micro] = '%b %d %l:%M%P' -Time::DATE_FORMATS[:precise] = '%b %d %H:%M:%S' +# frozen_string_literal: true + +Time::DATE_FORMATS[:micro] = "%b %d %l:%M%P" +Time::DATE_FORMATS[:precise] = "%b %d %H:%M:%S" diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index 33725e95fd..e7148a3680 100644 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which @@ -5,10 +7,5 @@ # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [:json] if respond_to?(:wrap_parameters) + wrap_parameters format: [:json] end - -# To enable root element in JSON for ActiveRecord objects. -# ActiveSupport.on_load(:active_record) do -# self.include_root_in_json = true -# end diff --git a/config/initializers/xml_backend.rb b/config/initializers/xml_backend.rb index b0b780f037..21530e91a8 100644 --- a/config/initializers/xml_backend.rb +++ b/config/initializers/xml_backend.rb @@ -1 +1,4 @@ -ActiveSupport::XmlMini.backend = 'Nokogiri' +# frozen_string_literal: true + +# We use nokogiri for performance +ActiveSupport::XmlMini.backend = "Nokogiri" diff --git a/config/load.rb b/config/load.rb index dccca993a5..9a45003c01 100644 --- a/config/load.rb +++ b/config/load.rb @@ -1,74 +1,77 @@ +# frozen_string_literal: true + # load default ENV values (without overwriting any existing value) -Dotenv.load('.env.default') +Dotenv.load(".env.default") -require_relative '../lib/configurator' +require_relative "../app/lib/configurator" # map config keys to environment variables # # We use the first non-nil environment variable in the list. If the last array # element is a proc, it runs at the end, overriding the config value Errbit::Config = Configurator.run( - host: ['ERRBIT_HOST'], - protocol: ['ERRBIT_PROTOCOL'], - port: ['ERRBIT_PORT'], - enforce_ssl: ['ERRBIT_ENFORCE_SSL'], - confirm_err_actions: ['ERRBIT_CONFIRM_ERR_ACTIONS'], - user_has_username: ['ERRBIT_USER_HAS_USERNAME'], - use_gravatar: ['ERRBIT_USE_GRAVATAR'], - gravatar_default: ['ERRBIT_GRAVATAR_DEFAULT'], - email_from: ['ERRBIT_EMAIL_FROM'], - email_at_notices: ['ERRBIT_EMAIL_AT_NOTICES'], - per_app_email_at_notices: ['ERRBIT_PER_APP_EMAIL_AT_NOTICES'], - notify_at_notices: ['ERRBIT_NOTIFY_AT_NOTICES'], - per_app_notify_at_notices: ['ERRBIT_PER_APP_NOTIFY_AT_NOTICES'], - log_location: ['ERRBIT_LOG_LOCATION'], - log_level: ['ERRBIT_LOG_LEVEL'], - notice_deprecation_days: ['ERRBIT_PROBLEM_DESTROY_AFTER_DAYS'], + host: ["ERRBIT_HOST"], + confirm_err_actions: ["ERRBIT_CONFIRM_ERR_ACTIONS"], + user_has_username: ["ERRBIT_USER_HAS_USERNAME"], + use_gravatar: ["ERRBIT_USE_GRAVATAR"], + gravatar_default: ["ERRBIT_GRAVATAR_DEFAULT"], + email_from: ["ERRBIT_EMAIL_FROM"], + email_at_notices: ["ERRBIT_EMAIL_AT_NOTICES"], + per_app_email_at_notices: ["ERRBIT_PER_APP_EMAIL_AT_NOTICES"], + notify_at_notices: ["ERRBIT_NOTIFY_AT_NOTICES"], + per_app_notify_at_notices: ["ERRBIT_PER_APP_NOTIFY_AT_NOTICES"], + log_location: ["ERRBIT_LOG_LOCATION"], + notice_deprecation_days: ["ERRBIT_PROBLEM_DESTROY_AFTER_DAYS"], - serve_static_assets: ['SERVE_STATIC_ASSETS'], - secret_key_base: ['SECRET_KEY_BASE'], - mongo_url: %w(MONGODB_URI MONGOLAB_URI MONGOHQ_URL MONGODB_URL MONGO_URL), + secret_key_base: ["SECRET_KEY_BASE"], + mongo_url: ["MONGODB_URI", "MONGOLAB_URI", "MONGOHQ_URL", "MONGODB_URL", "MONGO_URL"], # github - github_url: ['GITHUB_URL', lambda do |values| - values[:github_url].gsub(%r{/*\z}, '') + github_url: ["GITHUB_URL", lambda do |values| + values[:github_url].gsub(%r{/*\z}, "") end], - github_authentication: ['GITHUB_AUTHENTICATION'], - github_client_id: ['GITHUB_CLIENT_ID'], - github_secret: ['GITHUB_SECRET'], - github_org_id: ['GITHUB_ORG_ID'], - github_access_scope: ['GITHUB_ACCESS_SCOPE'], - github_api_url: ['GITHUB_API_URL'], - github_site_title: ['GITHUB_SITE_TITLE'], + github_authentication: ["GITHUB_AUTHENTICATION"], + github_client_id: ["GITHUB_CLIENT_ID"], + github_secret: ["GITHUB_SECRET"], + github_org_id: ["GITHUB_ORG_ID"], + github_access_scope: ["GITHUB_ACCESS_SCOPE"], + github_api_url: ["GITHUB_API_URL"], + github_site_title: ["GITHUB_SITE_TITLE"], # google - google_authentication: ['GOOGLE_AUTHENTICATION'], - google_auto_provision: ['GOOGLE_AUTO_PROVISION'], - google_site_title: ['GOOGLE_SITE_TITLE'], - google_client_id: ['GOOGLE_CLIENT_ID'], - google_secret: ['GOOGLE_SECRET'], - google_redirect_uri: ['GOOGLE_REDIRECT_URI'], - google_authorized_domains: ['GOOGLE_AUTHORIZED_DOMAINS'], + google_authentication: ["GOOGLE_AUTHENTICATION"], + google_auto_provision: ["GOOGLE_AUTO_PROVISION"], + google_site_title: ["GOOGLE_SITE_TITLE"], + google_client_id: ["GOOGLE_CLIENT_ID"], + google_secret: ["GOOGLE_SECRET"], + google_redirect_uri: ["GOOGLE_REDIRECT_URI"], + google_authorized_domains: ["GOOGLE_AUTHORIZED_DOMAINS"], + + email_delivery_method: ["EMAIL_DELIVERY_METHOD", lambda do |values| + email_delivery_method = values[:email_delivery_method] - email_delivery_method: ['EMAIL_DELIVERY_METHOD', lambda do |values| - values[:email_delivery_method] && values[:email_delivery_method].to_sym + if email_delivery_method.present? + if email_delivery_method.is_a?(Symbol) || email_delivery_method.is_a?(String) + email_delivery_method.to_sym + end + end end], - # smtp settings - smtp_address: ['SMTP_SERVER'], - smtp_port: ['SMTP_PORT'], - smtp_authentication: ['SMTP_AUTHENTICATION'], - smtp_enable_starttls_auto: ['SMTP_ENABLE_STARTTLS_AUTO'], - smtp_openssl_verify_mode: ['SMTP_OPENSSL_VERIFY_MODE'], - smtp_user_name: %w(SMTP_USERNAME SENDGRID_USERNAME), - smtp_password: %w(SMTP_PASSWORD SENDGRID_PASSWORD), - smtp_domain: ['SMTP_DOMAIN', 'SENDGRID_DOMAIN', lambda do |values| + # SMTP settings + smtp_address: ["SMTP_SERVER"], + smtp_port: ["SMTP_PORT"], + smtp_authentication: ["SMTP_AUTHENTICATION"], + smtp_enable_starttls_auto: ["SMTP_ENABLE_STARTTLS_AUTO"], + smtp_openssl_verify_mode: ["SMTP_OPENSSL_VERIFY_MODE"], + smtp_user_name: ["SMTP_USERNAME", "SENDGRID_USERNAME"], + smtp_password: ["SMTP_PASSWORD", "SENDGRID_PASSWORD"], + smtp_domain: ["SMTP_DOMAIN", "SENDGRID_DOMAIN", lambda do |values| values[:smtp_domain] || - (values[:email_from] && values[:email_from].split('@').last) || nil + (values[:email_from] && values[:email_from].split("@").last) || nil end], # sendmail settings - sendmail_location: ['SENDMAIL_LOCATION'], - sendmail_arguments: ['SENDMAIL_ARGUMENTS'], + sendmail_location: ["SENDMAIL_LOCATION"], + sendmail_arguments: ["SENDMAIL_ARGUMENTS"], - devise_modules: ['DEVISE_MODULES'] + devise_modules: ["DEVISE_MODULES"] ) diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 84abe77d1f..53110eebde 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -1,4 +1,4 @@ -# Additional translations at https://github.com/plataformatec/devise/wiki/I18n +# Additional translations at https://github.com/heartcombo/devise/wiki/I18n en: devise: @@ -16,9 +16,6 @@ en: timeout: "Your session expired. Please sign in again to continue." unauthenticated: "You need to sign in or sign up before continuing." unconfirmed: "You have to confirm your email address before continuing." - user: - email_invalid: "Invalid Email or password." - username_invalid: 'Invalid username or password.' mailer: confirmation_instructions: subject: "Confirmation instructions" @@ -31,7 +28,7 @@ en: password_change: subject: "Password Changed" omniauth_callbacks: - failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + failure: 'Could not authenticate you from %{kind} because "%{reason}".' success: "Successfully authenticated from %{kind} account." passwords: no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." @@ -45,8 +42,9 @@ en: signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." - update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." updated: "Your account has been updated successfully." + updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again." sessions: signed_in: "Signed in successfully." signed_out: "Signed out successfully." diff --git a/config/locales/devise.pt-BR.yml b/config/locales/devise.pt-BR.yml index cab7db87d7..2ad37b920d 100644 --- a/config/locales/devise.pt-BR.yml +++ b/config/locales/devise.pt-BR.yml @@ -3,50 +3,50 @@ pt-BR: devise: confirmations: - confirmed: 'Sua conta foi confirmada com sucesso. Você está logado.' - send_instructions: 'Dentro de minutos, você receberá um e-mail com instruções para a confirmação da sua conta.' - send_paranoid_instructions: 'Se o seu endereço de e-mail estiver cadastrado, você receberá uma mensagem com instruções para confirmação da sua conta.' + confirmed: "Sua conta foi confirmada com sucesso. Você está logado." + send_instructions: "Dentro de minutos, você receberá um e-mail com instruções para a confirmação da sua conta." + send_paranoid_instructions: "Se o seu endereço de e-mail estiver cadastrado, você receberá uma mensagem com instruções para confirmação da sua conta." failure: - already_authenticated: 'Você já está logado.' - inactive: 'Sua conta ainda não foi ativada.' - invalid: 'E-mail ou senha inválidos.' - invalid_token: 'O token de autenticação não é válido.' - locked: 'Sua conta está bloqueada.' - not_found_in_database: 'E-mail ou senha inválidos.' - timeout: 'Sua sessão expirou, por favor, efetue login novamente para continuar.' - unauthenticated: 'Para continuar, efetue login ou registre-se.' - unconfirmed: 'Antes de continuar, confirme a sua conta.' + already_authenticated: "Você já está logado." + inactive: "Sua conta ainda não foi ativada." + invalid: "E-mail ou senha inválidos." + invalid_token: "O token de autenticação não é válido." + locked: "Sua conta está bloqueada." + not_found_in_database: "E-mail ou senha inválidos." + timeout: "Sua sessão expirou, por favor, efetue login novamente para continuar." + unauthenticated: "Para continuar, efetue login ou registre-se." + unconfirmed: "Antes de continuar, confirme a sua conta." mailer: confirmation_instructions: - subject: 'Instruções de confirmação' + subject: "Instruções de confirmação" reset_password_instructions: - subject: 'Instruções de troca de senha' + subject: "Instruções de troca de senha" unlock_instructions: - subject: 'Instruções de desbloqueio' + subject: "Instruções de desbloqueio" omniauth_callbacks: failure: 'Não foi possível autenticá-lo como %{kind} porque "%{reason}".' - success: 'Autenticado com sucesso com uma conta de %{kind}.' + success: "Autenticado com sucesso com uma conta de %{kind}." passwords: no_token: "Você só pode acessar essa página através de um e-mail de troca de senha. Se já estiver acessando por um e-mail, verifique se a URL fornecida está completa." - send_instructions: 'Dentro de minutos, você receberá um e-mail com instruções para a troca da sua senha.' - send_paranoid_instructions: 'Se o seu endereço de e-mail estiver cadastrado, você receberá um link de recuperação da senha via e-mail.' - updated: 'Sua senha foi alterada com sucesso. Você está logado.' - updated_not_active: 'Sua senha foi alterada com sucesso.' + send_instructions: "Dentro de minutos, você receberá um e-mail com instruções para a troca da sua senha." + send_paranoid_instructions: "Se o seu endereço de e-mail estiver cadastrado, você receberá um link de recuperação da senha via e-mail." + updated: "Sua senha foi alterada com sucesso. Você está logado." + updated_not_active: "Sua senha foi alterada com sucesso." registrations: - destroyed: 'Tchau! Sua conta foi cancelada com sucesso. Esperamos vê-lo novamente em breve.' - signed_up: 'Login efetuado com sucesso. Se não foi autorizado, a confirmação será enviada por e-mail.' - signed_up_but_inactive: 'Você foi cadastrado com sucesso. No entanto, não foi possível efetuar login, pois sua conta não foi ativada.' - signed_up_but_locked: 'Você foi cadastrado com sucesso. No entanto, não foi possível efetuar login, pois sua conta está bloqueada.' - signed_up_but_unconfirmed: 'Uma mensagem com um link de confirmação foi enviada para o seu endereço de e-mail. Por favor, abra o link para confirmar a sua conta.' - update_needs_confirmation: 'Você atualizou a sua conta com sucesso, mas o seu novo endereço de e-mail precisa ser confirmado. Por favor, acesse-o e clique no link de confirmação que enviamos.' - updated: 'Sua conta foi atualizada com sucesso.' + destroyed: "Tchau! Sua conta foi cancelada com sucesso. Esperamos vê-lo novamente em breve." + signed_up: "Login efetuado com sucesso. Se não foi autorizado, a confirmação será enviada por e-mail." + signed_up_but_inactive: "Você foi cadastrado com sucesso. No entanto, não foi possível efetuar login, pois sua conta não foi ativada." + signed_up_but_locked: "Você foi cadastrado com sucesso. No entanto, não foi possível efetuar login, pois sua conta está bloqueada." + signed_up_but_unconfirmed: "Uma mensagem com um link de confirmação foi enviada para o seu endereço de e-mail. Por favor, abra o link para confirmar a sua conta." + update_needs_confirmation: "Você atualizou a sua conta com sucesso, mas o seu novo endereço de e-mail precisa ser confirmado. Por favor, acesse-o e clique no link de confirmação que enviamos." + updated: "Sua conta foi atualizada com sucesso." sessions: - signed_in: 'Login efetuado com sucesso!' - signed_out: 'Saiu com sucesso.' + signed_in: "Login efetuado com sucesso!" + signed_out: "Saiu com sucesso." unlocks: - send_instructions: 'Dentro de minutos, você receberá um email com instruções para o desbloqueio da sua conta.' - send_paranoid_instructions: 'Se sua conta existir, você receberá um e-mail com instruções para desbloqueá-la em alguns minutos.' - unlocked: 'Sua conta foi desbloqueada com sucesso. Efetue login para continuar.' + send_instructions: "Dentro de minutos, você receberá um email com instruções para o desbloqueio da sua conta." + send_paranoid_instructions: "Se sua conta existir, você receberá um e-mail com instruções para desbloqueá-la em alguns minutos." + unlocked: "Sua conta foi desbloqueada com sucesso. Efetue login para continuar." errors: messages: @@ -57,4 +57,4 @@ pt-BR: not_locked: "não foi bloqueado" not_saved: one: "Não foi possível salvar %{resource}: 1 erro" - other: "Não foi possível salvar %{resource}: %{count} erros." \ No newline at end of file + other: "Não foi possível salvar %{resource}: %{count} erros." diff --git a/config/locales/en.yml b/config/locales/en.yml index 9e54051cfd..94453787c8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,5 +1,33 @@ -# Sample localization file for English. Add more files in this directory for other locales. -# See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. en: flash: @@ -11,11 +39,11 @@ en: destroy: success: "'%{app_name}' was successfully destroyed." n_errs: - one: "%{count} error" + one: "%{count} error" other: "%{count} errors" will_be_deleted: will be deleted n_errs_have: - one: "%{count} error has" + one: "%{count} error has" other: "%{count} errors have" been_resolved: been resolved been_unresolved: been unresolved @@ -23,61 +51,61 @@ en: been_deleted: been deleted layouts: application: - title: 'Errbit' - errbit: 'Errbit' + title: "Errbit" + errbit: "Errbit" powered_html: "Powered by %{link} : the open source error catcher." shared: navigation: - apps: 'Apps' - errors: 'Errors' - users: 'Users' - config: 'Config' + apps: "Apps" + errors: "Errors" + users: "Users" + config: "Config" session: - sign_out: 'Sign out' - edit_profile: 'Edit profile' + sign_out: "Sign out" + edit_profile: "Edit profile" + notice_fingerprinter: + error_class: Error Class + message: Error Message + backtrace_lines: Number of backtrace lines (-1 for unlimited) + component: Component (or controller) + action: Action + environment_name: Environment name controllers: + application: + require_admin: "Sorry, you don't have permission to do that" + user_not_authorized: "You are not authorized to perform this action." apps: flash: create: success: "Your app was successfully created." - error: "Your app could not be created." + error: "Your app could not be created." update: success: "Your app was successfully updated." error: "Your app could not be updated." destroy: success: "Your app was successfully destroyed." error: "Your app could not be destroyed." - - users: - flash: - destroy: - success: "That's sad. %{name} is no longer part of your team." - error: "You can't delete yourself" - update: - success: "%{name}'s information was successfully updated." problems: flash: no_select_problem: "You have not selected any errors" need_two_errors_merge: "You must select at least two errors to merge" merge_several: success: "%{nb} errors have been merged." - devise: registrations: - signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.' - signed_up_but_inactive: 'You have signed up successfully. However, we could not sign you in because your account is not yet activated.' - signed_up_but_locked: 'You have signed up successfully. However, we could not sign you in because your account is locked.' + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please open the link to activate your account." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." omniauth_callbacks: - failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + failure: 'Could not authenticate you from %{kind} because "%{reason}".' success: "Successfully authenticated from %{kind} account." google_login: domain_unauthorized: "Account's email domain is not authorized for login. Please log in with an account from a trusted domain." sessions: new: sign_in: "Sign in" - problems: index: all_errors: "All Errors" @@ -85,17 +113,17 @@ en: hide_resolved: "hide resolved" show_resolved: "show resolved" table: - app: App - env: Env - what_&_where: 'Where & What' - latest: Latest - count: Count - merge: Merge - unmerge: Unmerge - resolve: Resolve - unresolve: Unresolve - delete: Delete - ago: ago + app: App + env: Env + what_and_where_html: "Where & What" + latest: Latest + count: Count + merge: Merge + unmerge: Unmerge + resolve: Resolve + unresolve: Unresolve + delete: Delete + ago: ago confirm: delete: "Permanently delete selected issues?" resolve: "Resolve selected issues? They can be unresolved later." @@ -104,9 +132,9 @@ en: unmerge: "Unmerge selected issues? They can be re-merged later." unresolve: "Unresolve selected issues? They can be resolved again later." search: - search_placeholder: 'Search for issues' + search_placeholder: "Search for issues" resolve: - the_error_has_been_resolved: 'Great news everyone! The error has been resolved.' + the_error_has_been_resolved: "Great news everyone! The error has been resolved." show: comments: Comments add_a_comment: Add a comment @@ -117,7 +145,7 @@ en: parameters: Parameters session: Session ago_by: ago by - ago_by_unkown_user: ago by [Unkown User] + ago_by_unknown_user: ago by [Unknown User] first_notice: First Notice last_notice: Last Notice environment: Environment @@ -125,10 +153,19 @@ en: where: Where up: up resolve: resolve + notice_by_id: "Notice: %{notice_id}" + sparkline: + graph-2-weeks: Graph (2 weeks) + graph-24-hours: Graph (24 hours) comments: confirm_delete: "Permanently delete this comment?" + create: + success: "Comment saved!" + error: "I'm sorry, your comment was blank! Try again?" + destroy: + success: "Comment deleted!" + error: "Sorry, I couldn't delete your comment for some reason. I hope you don't have any sensitive information in there!" users: - confirm_delete: "Permanently delete this user?" index: new: Add a New User title: Users @@ -136,50 +173,64 @@ en: username: Username email: Email admin: Admin? - admin_yes: Yes - admin_no: No + admin_yes: "Yes" + admin_no: "No" + show: + name: Name + email: Email + username: Username + admin: Admin? + admin_yes: "Yes" + admin_no: "No" + created: Created + edit: edit + destroy: destroy + confirm_delete: "Permanently delete this user?" + new: + title: New User + cancel: cancel + add_user: Add User edit: title: Edit cancel: cancel update_user: Update User + create: + success: "%{name} is now part of the team. Be sure to add them as a project watcher." + update: + success: "%{name}'s information was successfully updated." + destroy: + success: "That's sad. %{name} is no longer part of your team." + error: "You can't delete yourself" fields: name: Name + username: Username email: Email entries: Entries per page time_zone: Time Zone password: Password password_confirmation: Password Confirmation admin: Admin? - new: - title: New User - cancel: cancel - add_user: Add User - show: - name: Name - email: Email - admin: Admin? - created: Created - edit: edit - destroy: destroy apps: confirm_delete: "Permanently delete this app?" confirm_destroy_all_problems: "Permanently delete all of this app's errors?" index: + title: Apps + new_app: Add a New App + table: + name: Name + repository: Repository notify: Notification Service tracker: Tracker errors: Errors - name: Name - repository: Repository - title: Apps - new_app: Add a New App - no_apps: 'No apps here.' - click_to_create: 'Click here to create your first one' + no_apps: "No apps here." + click_to_create: "Click here to create your first one" fields: name: Name api_key: API Key repository_branch: Repo Branch github_repo: GitHub Repo bitbucket_repo: BitBucket Repo + custom_backtrace_url_template: Custom Backtrace URL Template asset_host: Asset Host asset_host_help: Used to generate links for JavaScript errors asset_host_placeholder: "e.g. https://assets.example.com" @@ -190,11 +241,12 @@ en: notify_on_errors: Notify on errors notify_all_users: Send notifications to all users regenerate_api_key: "Regenerate API Key" + use_site_fingerprinter: "Use Site Fingerprinter" show: all_errs: all errors all_users_notified: "All users will be notified when something happens." api_key: "API Key:" - are_you_sure: 'Are you sure?' + are_you_sure: "Are you sure?" atom_title: "Errbit notices for %{name} at %{host}" edit: edit environment: Environment @@ -203,7 +255,7 @@ en: github_repo: GitHub Repo message: Message no_error_yet: "No errors have been caught yet, make sure you set up your app" - no_watcher: "Sadly, no one is watching this app" + no_watchers: "Sadly, no one is watching this app" repository: Repository revision: Revision show_hide: "(show/hide)" @@ -212,7 +264,7 @@ en: user_or_email: User or Email watch: watch watchers: Watchers - when: When + when: When who: Who issue_tracker_fields: legend: Issue Tracker @@ -221,18 +273,18 @@ en: cancel: "cancel" title: "Add App" edit: - title: 'Edit App' - destroy: 'destroy application' - cancel: 'cancel' - seriously: 'Seriously?' - update: 'Update App' + title: "Edit App" + destroy: "destroy application" + cancel: "cancel" + seriously: "Seriously?" + update: "Update App" search: - search_placeholder: 'Search for application' + search_placeholder: "Search for application" watchers: + create: + success: "You are now watching %{app}" destroy: success: "You are no longer watching %{app}" - update: - success: "You are now watching %{app}" site_config: index: title: Config @@ -244,6 +296,8 @@ en: action: Action environment_name: Environment name update_config: Update Config + update: + success: "Updated site config" notices: summary: message: Message @@ -254,8 +308,6 @@ en: similar: Similar browser: Browser tenant: Origin - graph-2-weeks: Graph (2 weeks) - graph-24-hours: Graph (24 hours) app_server: App Server app_version: App Version framework: Framework diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 016669f603..e840bd8684 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -10,13 +10,12 @@ pt-BR: success: "Boas notícias! '%{app_name}' foi atualizado com sucesso." destroy: success: "'%{app_name}' foi destruído com sucesso." - n_errs: - one: "%{count} erro" + one: "%{count} erro" other: "%{count} erros" will_be_deleted: serà excluído(s) n_errs_have: - one: "%{count} erro foi" + one: "%{count} erro foi" other: "%{count} erros foram" been_resolved: resolvido(s) been_unresolved: tornado(s) não resolvido(s) @@ -24,40 +23,42 @@ pt-BR: been_deleted: excluído(s) layouts: application: - title: 'Errbit' - errbit: 'Errbit' + title: "Errbit" + errbit: "Errbit" powered_html: "Powered by %{link} : the open source error catcher." shared: navigation: - apps: 'Aplicações' - errors: 'Erros' - users: 'Usuários' - config: 'Configurações' + apps: "Aplicações" + errors: "Erros" + users: "Usuários" + config: "Configurações" session: - sign_out: 'Sair' - edit_profile: 'Editar conta' + sign_out: "Sair" + edit_profile: "Editar conta" + notice_fingerprinter: + error_class: Classe do erro + message: Mensagem de erro + backtrace_lines: Número de linhas do 'backtrace' (-1 para ilimitado) + component: Componente (ou Controlador) + action: Ação + environment_name: Nome do Ambiente controllers: + application: + require_admin: "Sorry, you don't have permission to do that" + user_not_authorized: "You are not authorized to perform this action." apps: flash: create: success: "Sua aplicação foi criado com sucesso." - error: "Sua aplicação não pôde ser criada." + error: "Sua aplicação não pôde ser criada." update: success: "Sua aplicação foi atualizada com sucesso." error: "Sua aplicação não pôde ser atualizada." destroy: success: "Sua aplicação foi destruída com sucesso." error: "Sua aplicação não pôde ser destruída." - - users: - flash: - destroy: - success: "Isto é triste. %{name} não faz mais parte do seu time." - error: "Você não pode excluir a si mesmo" - update: - success: "As informações de %{name} foram atualizadas com sucesso." problems: flash: no_select_problem: "Você não selecionou nenhum erro" @@ -66,16 +67,17 @@ pt-BR: success: "%{nb} erros foram mesclados." devise: registrations: - signed_up_but_unconfirmed: 'Uma mensagem foi enviada para o seu e-mail com um link de confirmação. Acesse o link para ativar sua conta.' - signed_up_but_inactive: 'Você se registrou com sucesso. Entretanto, só será possível entrar no sistema quando sua conta for ativada.' - signed_up_but_locked: 'Você se registrou com sucesso. Entratanto, não será possível entrar no sistema porque sua conta está bloqueada.' + signed_up_but_unconfirmed: "Uma mensagem foi enviada para o seu e-mail com um link de confirmação. Acesse o link para ativar sua conta." + signed_up_but_inactive: "Você se registrou com sucesso. Entretanto, só será possível entrar no sistema quando sua conta for ativada." + signed_up_but_locked: "Você se registrou com sucesso. Entratanto, não será possível entrar no sistema porque sua conta está bloqueada." omniauth_callbacks: - failure: "Não foi possível autenticá-lo a partir da conta %{kind} porque \"%{reason}\"." + failure: 'Não foi possível autenticá-lo a partir da conta %{kind} porque "%{reason}".' success: "Autenticado com sucesso a partir da conta %{kind}." + google_login: + domain_unauthorized: "Account's email domain is not authorized for login. Please log in with an account from a trusted domain." sessions: new: sign_in: "Entrar" - problems: index: all_errors: "Todos os erros" @@ -83,17 +85,17 @@ pt-BR: hide_resolved: "ocultar resolvidos" show_resolved: "exibir resolvidos" table: - app: Aplicação - env: Ambiente - what_&_where: 'O quê & onde' - latest: Último - count: Total - merge: Mesclar - unmerge: Separar - resolve: Resolver - unresolve: Tornar não resolvido - delete: Excluir - ago: atrás + app: Aplicação + env: Ambiente + what_and_where_html: "O quê & onde" + latest: Último + count: Total + merge: Mesclar + unmerge: Separar + resolve: Resolver + unresolve: Tornar não resolvido + delete: Excluir + ago: atrás confirm: delete: "Excluir permanentemente os itens selecionados?" resolve: "Resolver os itens selecionados? Isto não poderá ser alterado depois." @@ -102,9 +104,9 @@ pt-BR: unmerge: "Separar os itens selecionados? Isto não poderá ser alterado depois." unresolve: "Tornar não resolvidos os itens selecionados? Isto não poderá ser alterado depois." search: - search_placeholder: 'Pesquisar por itens' + search_placeholder: "Pesquisar por itens" resolve: - the_error_has_been_resolved: 'Boas notícias! O erro foi resolvido.' + the_error_has_been_resolved: "Boas notícias! O erro foi resolvido." show: comments: Comentários add_a_comment: Adicionar um comentário @@ -115,7 +117,7 @@ pt-BR: parameters: Parâmetros session: Sessão ago_by: atrás por - ago_by_unkown_user: atrás por [Usuário Desconhecido] + ago_by_unknown_user: atrás por [Usuário Desconhecido] first_notice: Primeiro notificação last_notice: Última notificação environment: Ambiente @@ -123,10 +125,19 @@ pt-BR: where: Onde up: subir resolve: resolver + notice_by_id: "notificação: %{notice_id}" + sparkline: + graph-2-weeks: Graph (2 weeks) + graph-24-hours: Graph (24 hours) comments: confirm_delete: "Excluir permanentemente este comentário?" + create: + success: "Comment saved!" + error: "I'm sorry, your comment was blank! Try again?" + destroy: + success: "Comment deleted!" + error: "Sorry, I couldn't delete your comment for some reason. I hope you don't have any sensitive information in there!" users: - confirm_delete: "Excluir permanentemente este usuário?" index: new: Adicionar novo usuário title: Usuários @@ -136,48 +147,62 @@ pt-BR: admin: Administrador? admin_yes: Sim admin_no: Não + show: + name: Nome + email: E-mail + username: Usuário + admin: Adminstrador? + admin_yes: Sim + admin_no: Não + created: Criado em + edit: editar + destroy: excluir + confirm_delete: "Excluir permanentemente este usuário?" + new: + title: Novo Usuário + cancel: cancelar + add_user: Adicionar usuário edit: title: Editar cancel: cancelar update_user: Atualizar usuário + create: + success: "%{name} is now part of the team. Be sure to add them as a project watcher." + update: + success: "As informações de %{name} foram atualizadas com sucesso." + destroy: + success: "Isto é triste. %{name} não faz mais parte do seu time." + error: "Você não pode excluir a si mesmo" fields: name: Nome + username: Usuário email: E-mail entries: Registros por página time_zone: Fuso horário password: Senha password_confirmation: Confirmação de Senha admin: Adminstrador? - new: - title: Novo Usuário - cancel: cancelar - add_user: Adicionar usuário - show: - name: Nome - email: E-mail - admin: Adminstrador? - created: Criado em - edit: editar - destroy: excluir apps: confirm_delete: "Excluir permanentemente esta aplicação?" confirm_destroy_all_problems: "Excluir permanentemente todos os erros desta aplicação?" index: + title: Aplicações + new_app: Adicionar nova aplicação + table: + name: Nome + repository: Repositório notify: Serviços de Notificação tracker: Rastreador errors: Erros - name: Nome - repository: Repositório - title: Aplicações - new_app: Adicionar nova aplicação - no_apps: 'Nenhuma aplicação aqui.' - click_to_create: 'Clique aqui para criar a sua primeira' + no_apps: "Nenhuma aplicação aqui." + click_to_create: "Clique aqui para criar a sua primeira" fields: name: Name api_key: Chave de Acesso repository_branch: Ramo do repositório github_repo: Repositório do GitHub bitbucket_repo: Repositório do BitBucket + custom_backtrace_url_template: Custom Backtrace URL Template asset_host: Servidor de 'Assets' asset_host_help: Usado para gerar links dos erros JavaScript asset_host_placeholder: "ex.: https://assets.example.com" @@ -188,11 +213,12 @@ pt-BR: notify_on_errors: Notificar em caso de erros notify_all_users: Enviar notificações para todos os usuários regenerate_api_key: "Re-gerar chave de acesso" + use_site_fingerprinter: "Use Site Fingerprinter" show: all_errs: todos os errors all_users_notified: "Todos os usuários serão notificados quando algo acontecer." api_key: "Chave de Acesso da API:" - are_you_sure: 'Está certo disto?' + are_you_sure: "Está certo disto?" atom_title: "Notificações do Errbit para %{name} em %{host}" edit: editar environment: Ambiente @@ -201,7 +227,7 @@ pt-BR: github_repo: Repositório GitHub message: Mensagem no_error_yet: "Nenhum erro foi capturado até então, certifique-se de que sua aplicação está configurada" - no_watcher: "Infelizmente, ninguém está observando esta aplicação" + no_watchers: "Infelizmente, ninguém está observando esta aplicação" repository: Repositório revision: Versão show_hide: "(exibir/ocultar)" @@ -210,7 +236,7 @@ pt-BR: user_or_email: Usuário ou E-mail watch: observar watchers: Observadores - when: Quando + when: Quando who: Quem issue_tracker_fields: legend: Rastreador de defeitos @@ -219,22 +245,22 @@ pt-BR: cancel: "cancelar" title: "Adicionar Aplicação" edit: - title: 'Editar Aplicação' - destroy: 'destruir aplicação' - cancel: 'cancelar' - seriously: 'Sério?' - update: 'Atualizar Aplicação' + title: "Editar Aplicação" + destroy: "destruir aplicação" + cancel: "cancelar" + seriously: "Sério?" + update: "Atualizar Aplicação" + search: + search_placeholder: "Search for application" watchers: + create: + success: "Agora você está observando %{app}" destroy: success: "Você não está mais observando %{app}" - update: - success: "Agora você está observando %{app}" site_config: index: title: Configurações cancel: cancelar - notice_fingerprinter: Impressão digital da notificação - notice_fingerprinter_text: A impressão digital da notificação determina como as notificações serão agrupadas. Quando habilitado, cada item compõe a unicidade do erro. error_class: Classe do erro message: Mensagem de erro backtrace_lines: Número de linhas do 'backtrace' (-1 para ilimitado) @@ -242,6 +268,8 @@ pt-BR: action: Ação environment_name: Nome do Ambiente update_config: Atualizar configurações + update: + success: "Updated site config" notices: summary: message: Mensagem @@ -256,6 +284,9 @@ pt-BR: app_version: Versão da Aplicação framework: Framework relative_directory: Diretório relativo + notice_fingerprinter: + title: Notice Fingerprinter + text: The notice fingerprinter governs how error notifications are grouped. Each item counts toward an error's uniqueness if enabled. paginator: viewing_occurrence: visualizando ocorrência of: de diff --git a/config/locales/rails.pt-BR.yml b/config/locales/rails.pt-BR.yml index fc2fdef2c0..6ae918b575 100644 --- a/config/locales/rails.pt-BR.yml +++ b/config/locales/rails.pt-BR.yml @@ -2,63 +2,63 @@ pt-BR: activerecord: errors: messages: - record_invalid: 'A validação falhou: %{errors}' + record_invalid: "A validação falhou: %{errors}" restrict_dependent_destroy: has_one: Não é possível excluir o registro pois existe um %{record} dependente has_many: Não é possível excluir o registro pois existem %{record} dependentes date: abbr_day_names: - - Dom - - Seg - - Ter - - Qua - - Qui - - Sex - - Sáb + - Dom + - Seg + - Ter + - Qua + - Qui + - Sex + - Sáb abbr_month_names: - - - - Jan - - Fev - - Mar - - Abr - - Mai - - Jun - - Jul - - Ago - - Set - - Out - - Nov - - Dez + - + - Jan + - Fev + - Mar + - Abr + - Mai + - Jun + - Jul + - Ago + - Set + - Out + - Nov + - Dez day_names: - - Domingo - - Segunda-feira - - Terça-feira - - Quarta-feira - - Quinta-feira - - Sexta-feira - - Sábado + - Domingo + - Segunda-feira + - Terça-feira + - Quarta-feira + - Quinta-feira + - Sexta-feira + - Sábado formats: default: "%d/%m/%Y" long: "%d de %B de %Y" short: "%d de %B" month_names: - - - - Janeiro - - Fevereiro - - Março - - Abril - - Maio - - Junho - - Julho - - Agosto - - Setembro - - Outubro - - Novembro - - Dezembro + - + - Janeiro + - Fevereiro + - Março + - Abril + - Maio + - Junho + - Julho + - Agosto + - Setembro + - Outubro + - Novembro + - Dezembro order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -119,27 +119,27 @@ pt-BR: invalid: não é válido less_than: deve ser menor que %{count} less_than_or_equal_to: deve ser menor ou igual a %{count} - model_invalid: 'A validação falhou: %{errors}' + model_invalid: "A validação falhou: %{errors}" not_a_number: não é um número not_an_integer: não é um número inteiro odd: deve ser ímpar required: deve existir taken: já está em uso too_long: - one: 'é muito longo (máximo: 1 caracter)' - other: 'é muito longo (máximo: %{count} caracteres)' + one: "é muito longo (máximo: 1 caracter)" + other: "é muito longo (máximo: %{count} caracteres)" too_short: - one: 'é muito curto (mínimo: 1 caracter)' - other: 'é muito curto (mínimo: %{count} caracteres)' + one: "é muito curto (mínimo: 1 caracter)" + other: "é muito curto (mínimo: %{count} caracteres)" wrong_length: one: não possui o tamanho esperado (1 caracter) other: não possui o tamanho esperado (%{count} caracteres) other_than: deve ser diferente de %{count} template: - body: 'Por favor, verifique o(s) seguinte(s) campo(s):' + body: "Por favor, verifique o(s) seguinte(s) campo(s):" header: - one: 'Não foi possível gravar %{model}: 1 erro' - other: 'Não foi possível gravar %{model}: %{count} erros' + one: "Não foi possível gravar %{model}: 1 erro" + other: "Não foi possível gravar %{model}: %{count} erros" helpers: select: prompt: Por favor selecione @@ -180,7 +180,7 @@ pt-BR: trillion: one: trilhão other: trilhões - unit: '' + unit: "" format: delimiter: "." precision: 2 @@ -209,9 +209,9 @@ pt-BR: two_words_connector: " e " words_connector: ", " time: - am: '' + am: "" formats: default: "%a, %d de %B de %Y, %H:%M:%S %z" long: "%d de %B de %Y, %H:%M" short: "%d de %B, %H:%M" - pm: '' \ No newline at end of file + pm: "" diff --git a/config/mongo.rb b/config/mongo.rb index 47727a384d..273c4b7652 100644 --- a/config/mongo.rb +++ b/config/mongo.rb @@ -1,23 +1,24 @@ -log_level = Logger.const_get Errbit::Config.log_level.upcase +# frozen_string_literal: true -Mongoid.logger.level = log_level -Mongo::Logger.level = log_level +# log_level = "info" +# +# logger = Logger.const_get log_level.upcase +# +# Mongoid.logger.level = logger +# Mongo::Logger.level = logger Mongoid.configure do |config| - uri = if Errbit::Config.mongo_url == 'mongodb://localhost' - "mongodb://localhost/errbit_#{Rails.env}" - else - Errbit::Config.mongo_url - end + uri = if Errbit::Config.mongo_url == "mongodb://localhost" + "mongodb://localhost/errbit_#{Rails.env}" + else + Errbit::Config.mongo_url + end config.load_configuration( clients: { default: { uri: uri } - }, - options: { - use_activesupport_time_zone: true } ) end diff --git a/config/newrelic.example.yml b/config/newrelic.example.yml deleted file mode 100644 index 8f37f2629d..0000000000 --- a/config/newrelic.example.yml +++ /dev/null @@ -1,226 +0,0 @@ -# -# This file configures the New Relic Agent. New Relic monitors -# Ruby, Java, .NET, PHP, and Python applications with deep visibility and low overhead. -# For more information, visit www.newrelic.com. -# -# Generated October 18, 2011 -# -# This configuration file is custom generated for errbit - -# Here are the settings that are common to all environments: -common: &default_settings - # ============================== LICENSE KEY =============================== - - # You must specify the license key associated with your New Relic - # account. This key binds your Agent's data to your account in the - # New Relic service. - license_key: <%= ENV["NEW_RELIC_LICENSE_KEY"] %> - - # Agent Enabled (Ruby/Rails Only) - # Use this setting to force the agent to run or not run. - # Default is 'auto' which means the agent will install and run only - # if a valid dispatcher such as Mongrel is running. This prevents - # it from running with Rake or the console. Set to false to - # completely turn the agent off regardless of the other settings. - # Valid values are true, false and auto. - # agent_enabled: auto - - # Application Name - # Set this to be the name of your application as you'd like it show - # up in New Relic. New Relic will then auto-map instances of your application - # into a New Relic "application" on your home dashboard page. If you want - # to map this instance into multiple apps, like "AJAX Requests" and - # "All UI" then specify a semicolon-separated list of up to three - # distinct names. If you comment this out, it defaults to the - # capitalized RAILS_ENV (i.e., Production, Staging, etc) - app_name: <%= ENV["NEW_RELIC_APP_NAME"] %> - - # When "true", the agent collects performance data about your - # application and reports this data to the New Relic service at - # newrelic.com. This global switch is normally overridden for each - # environment below. (formerly called 'enabled') - monitor_mode: true - - # Developer mode should be off in every environment but - # development as it has very high overhead in memory. - developer_mode: false - - # The newrelic agent generates its own log file to keep its logging - # information separate from that of your application. Specify its - # log level here. - log_level: info - - # The newrelic agent communicates with the New Relic service via http by - # default. If you want to communicate via https to increase - # security, then turn on SSL by setting this value to true. Note, - # this will result in increased CPU overhead to perform the - # encryption involved in SSL communication, but this work is done - # asynchronously to the threads that process your application code, - # so it should not impact response times. - ssl: false - - # EXPERIMENTAL: enable verification of the SSL certificate sent by - # the server. This setting has no effect unless SSL is enabled - # above. This may block your application. Only enable it if the data - # you send us needs end-to-end verified certificates. - # - # This means we cannot cache the DNS lookup, so each request to the - # New Relic service will perform a lookup. It also means that we cannot - # use a non-blocking lookup, so in a worst case, if you have DNS - # problems, your app may block indefinitely. - # verify_certificate: true - - # Set your application's Apdex threshold value with the 'apdex_t' - # setting, in seconds. The apdex_t value determines the buckets used - # to compute your overall Apdex score. - # Requests that take less than apdex_t seconds to process will be - # classified as Satisfying transactions; more than apdex_t seconds - # as Tolerating transactions; and more than four times the apdex_t - # value as Frustrating transactions. - # For more about the Apdex standard, see - # http://support.newrelic.com/faqs/general/apdex - apdex_t: 0.5 - - # Proxy settings for connecting to the New Relic server. - # - # If a proxy is used, the host setting is required. Other settings - # are optional. Default port is 8080. - # - # proxy_host: hostname - # proxy_port: 8080 - # proxy_user: - # proxy_pass: - - # Tells transaction tracer and error collector (when enabled) - # whether or not to capture HTTP params. When true, frameworks can - # exclude HTTP parameters from being captured. - # Rails: the RoR filter_parameter_logging excludes parameters - # Java: create a config setting called "ignored_params" and set it to - # a comma separated list of HTTP parameter names. - # ex: ignored_params: credit_card, ssn, password - capture_params: false - - # Transaction tracer captures deep information about slow - # transactions and sends this to the New Relic service once a - # minute. Included in the transaction is the exact call sequence of - # the transactions including any SQL statements issued. - transaction_tracer: - - # Transaction tracer is enabled by default. Set this to false to - # turn it off. This feature is only available at the Professional - # product level. - enabled: true - - # Threshold in seconds for when to collect a transaction - # trace. When the response time of a controller action exceeds - # this threshold, a transaction trace will be recorded and sent to - # New Relic. Valid values are any float value, or (default) "apdex_f", - # which will use the threshold for an dissatisfying Apdex - # controller action - four times the Apdex T value. - transaction_threshold: apdex_f - - # When transaction tracer is on, SQL statements can optionally be - # recorded. The recorder has three modes, "off" which sends no - # SQL, "raw" which sends the SQL statement in its original form, - # and "obfuscated", which strips out numeric and string literals. - record_sql: obfuscated - - # Threshold in seconds for when to collect stack trace for a SQL - # call. In other words, when SQL statements exceed this threshold, - # then capture and send to New Relic the current stack trace. This is - # helpful for pinpointing where long SQL calls originate from. - stack_trace_threshold: 0.500 - - # Determines whether the agent will capture query plans for slow - # SQL queries. Only supported in mysql and postgres. Should be - # set to false when using other adapters. - # explain_enabled: true - - # Threshold for query execution time below which query plans will not - # not be captured. Relevant only when `explain_enabled` is true. - # explain_threshold: 0.5 - - # Error collector captures information about uncaught exceptions and - # sends them to New Relic for viewing - error_collector: - - # Error collector is enabled by default. Set this to false to turn - # it off. This feature is only available at the Professional - # product level. - enabled: true - - # Rails Only - tells error collector whether or not to capture a - # source snippet around the place of the error when errors are View - # related. - capture_source: true - - # To stop specific errors from reporting to New Relic, set this property - # to comma-separated values. Default is to ignore routing errors, - # which are how 404's get triggered. - ignore_errors: ActionController::RoutingError - - # (Advanced) Uncomment this to ensure the CPU and memory samplers - # won't run. Useful when you are using the agent to monitor an - # external resource - # disable_samplers: true - - # If you aren't interested in visibility in these areas, you can - # disable the instrumentation to reduce overhead. - # - # disable_view_instrumentation: true - # disable_activerecord_instrumentation: true - # disable_memcache_instrumentation: true - # disable_dj: true - - # Certain types of instrumentation such as GC stats will not work if - # you are running multi-threaded. Please let us know. - # multi_threaded = false - -# Application Environments -# ------------------------------------------ -# Environment-specific settings are in this section. -# For Rails applications, RAILS_ENV is used to determine the environment. -# For Java applications, pass -Dnewrelic.environment to set -# the environment. - -# NOTE if your application has other named environments, you should -# provide newrelic configuration settings for these environments here. - -development: - <<: *default_settings - # Turn off communication to New Relic service in development mode (also - # 'enabled'). - # NOTE: for initial evaluation purposes, you may want to temporarily - # turn agent communication on in development mode. - monitor_mode: false - - # Rails Only - when running in Developer Mode, the New Relic Agent will - # present performance information on the last 100 transactions you have - # executed since starting the app server. - # NOTE: There is substantial overhead when running in developer mode. - # Do not use for production or load testing. - developer_mode: true - - # Enable textmate links - # textmate: true - -test: - <<: *default_settings - # It almost never makes sense to turn on the agent when running - # unit, functional or integration tests or the like. - monitor_mode: false - -# Turn on the agent in production for 24x7 monitoring. New Relic -# testing shows an average performance impact of < 5 ms per -# transaction, so you can leave this on all the time without -# incurring any user-visible performance degradation. -production: - <<: *default_settings - monitor_mode: true - -# Many applications have a staging environment which behaves -# identically to production. Support for that environment is provided -# here. By default, the staging environment has the agent turned on. -staging: - <<: *default_settings - monitor_mode: true diff --git a/config/puma.default.rb b/config/puma.default.rb deleted file mode 100644 index e38b1cbffd..0000000000 --- a/config/puma.default.rb +++ /dev/null @@ -1,19 +0,0 @@ -# https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server - -workers Integer(ENV['WEB_CONCURRENCY'] || 2) -threads_count = Integer(ENV['MAX_THREADS'] || 5) -threads threads_count, threads_count - -preload_app! - -rackup DefaultRackup -port ENV['PORT'] || 8080 -environment ENV['RACK_ENV'] || 'development' - -on_worker_boot do - # Worker specific setup for Rails 4.1+ - # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot - ActiveSupport.on_load(:active_record) do - ActiveRecord::Base.establish_connection - end -end diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000000..4c27d35c1a --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. You can set it to `auto` to automatically start a worker +# for each available processor. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/config/routes.rb b/config/routes.rb index 6f3d43d9de..3a93f604c2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,25 +1,32 @@ +# frozen_string_literal: true + Rails.application.routes.draw do - devise_for :users, controllers: { omniauth_callbacks: "users/omniauth_callbacks" } + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. + # Can be used by load balancers and uptime monitors to verify that the app is live. + get "up" => "rails/health#show", :as => :rails_health_check + + devise_for :users, controllers: {omniauth_callbacks: "users/omniauth_callbacks"} # Hoptoad Notifier Routes - match '/notifier_api/v2/notices' => 'notices#create', via: [:get, :post] - get '/locate/:id' => 'notices#locate', :as => :locate + match "/notifier_api/v2/notices" => "notices#create", :via => [:get, :post] + get "/locate/:id" => "notices#locate", :as => :locate + get "/notices/:id" => "notices#show_by_id", :as => :show_notice_by_id + + resources :notices, only: :show - resources :notices, only: [:show] resources :users do - member do - delete :unlink_github - delete :unlink_google - end + resource :unlink_github, only: :update + + resource :unlink_google, only: :update end - resources :site_config, only: [:index] do + resources :site_config, only: :index do collection do - put :update + patch :update end end - resources :problems, only: [:index] do + resources :problems, only: :index do collection do post :destroy_several post :resolve_several @@ -33,6 +40,7 @@ resources :apps do resources :problems do resources :notices + resources :comments, only: [:create, :destroy] collection do @@ -41,31 +49,38 @@ member do get :xhr_sparkline - put :resolve - put :unresolve + patch :resolve + patch :unresolve post :create_issue post :close_issue delete :unlink_issue end end - resources :watchers, only: [:destroy, :update] + + resource :watchers, only: [:create, :destroy] + member do post :regenerate_api_key end + collection do get :search end end - get 'health/readiness' => 'health#readiness' - get 'health/liveness' => 'health#liveness' - get 'health/api-key-tester' => 'health#api_key_tester' + get "problems/:id" => "problems#show_by_id" + + get "health/api-key-tester" => "health#api_key_tester" namespace :api do namespace :v1 do - resources :problems, only: [:index, :show], defaults: { format: 'json' } - resources :notices, only: [:index], defaults: { format: 'json' } - resources :stats, only: [], defaults: { format: 'json' } do + resources :problems, only: [:index, :show], defaults: {format: "json"} do + resources :comments, only: [:index, :create], defaults: {format: "json"} + end + + resources :notices, only: :index, defaults: {format: "json"} + + resources :stats, only: [], defaults: {format: "json"} do collection do get :app end @@ -73,8 +88,9 @@ end end - match '/api/v3/projects/:project_id/create-notice' => 'api/v3/notices#create', via: [:post] - match '/api/v3/projects/:project_id/notices' => 'api/v3/notices#create', via: [:post, :options] + post "/api/v3/projects/:project_id/create-notice" => "api/v3/notices#create" + + match "/api/v3/projects/:project_id/notices" => "api/v3/notices#create", :via => [:post, :options] - root to: 'apps#index' + root "apps#index" end diff --git a/db/migrate/20150527202629_add_v1_to_hipchat_notification_services.rb b/db/migrate/20150527202629_add_v1_to_hipchat_notification_services.rb deleted file mode 100644 index 451ccefc26..0000000000 --- a/db/migrate/20150527202629_add_v1_to_hipchat_notification_services.rb +++ /dev/null @@ -1,13 +0,0 @@ -class AddV1ToHipchatNotificationServices < Mongoid::Migration - def self.up - App.all.each do |app| - ns = app.notification_service - if ns.is_a?(NotificationServices::HipchatService) && ns.service.blank? - app.notification_service.update_attribute(:service, 'v1') - end - end - end - - def self.down - end -end diff --git a/db/migrate/20150926035420_cache_app_on_notice.rb b/db/migrate/20150926035420_cache_app_on_notice.rb deleted file mode 100644 index c5e7052ab1..0000000000 --- a/db/migrate/20150926035420_cache_app_on_notice.rb +++ /dev/null @@ -1,11 +0,0 @@ -class CacheAppOnNotice < Mongoid::Migration - def self.up - Notice.no_timeout.each do |n| - n.app_id = n.try(:err).try(:problem).try(:app_id) - n.save! - end - end - - def self.down - end -end diff --git a/db/migrate/201510290041_extract_issue_tracker.rb b/db/migrate/201510290041_extract_issue_tracker.rb deleted file mode 100644 index b5353a1797..0000000000 --- a/db/migrate/201510290041_extract_issue_tracker.rb +++ /dev/null @@ -1,43 +0,0 @@ -class ExtractIssueTracker < Mongoid::Migration - TRACKER_MAPPING = { - 'ErrbitTracPlugin::IssueTracker' => 'trac', - 'IssueTrackers::BitbucketIssuesTracker' => 'bitbucket', - 'IssueTrackers::FogbugzTracker' => 'fogbugz', - 'IssueTrackers::GithubIssuesTracker' => 'github', - 'IssueTrackers::GitlabTracker' => 'gitlab', - 'IssueTrackers::JiraTracker' => 'jira', - 'IssueTrackers::LighthouseTracker' => 'lighthouse', - 'IssueTrackers::PivotalLabsTracker' => 'pivotal', - 'IssueTrackers::RedmineTracker' => 'redmine', - 'IssueTrackers::UnfuddleTracker' => 'unfuddle' - } - - def self.up - App.all.each do |app| - next unless app.attributes['issue_tracker'].present? - next unless app.attributes['issue_tracker']['_type'].present? - - options = app['issue_tracker'].dup - options.delete('_type') - options.delete('_id') - - type = app.attributes['issue_tracker']['_type'] - updated_at = options.delete('updated_at') - created_at = options.delete('created_at') - - next unless TRACKER_MAPPING.include?(type) - - tracker = { - 'type_tracker' => TRACKER_MAPPING[type], - 'options' => options, - 'updated_at' => updated_at, - 'created_at' => created_at - } - - App.where(_id: app.id).update("$set" => { issue_tracker: tracker }) - end - end - - def self.down - end -end diff --git a/db/seeds.rb b/db/seeds.rb index 4edb3b8a97..fbf62bf130 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,21 +1,23 @@ -require 'securerandom' +# frozen_string_literal: true + +require "securerandom" puts "Seeding database" puts "-------------------------------" # Create an initial Admin User -admin_username = ENV['ERRBIT_ADMIN_USER'] || "errbit" +admin_username = ENV["ERRBIT_ADMIN_USER"] || "errbit" def admin_email - return 'admin@example.com' if heroku_pr_review_app? + return "admin@example.com" if heroku_pr_review_app? - ENV['ERRBIT_ADMIN_EMAIL'] || "errbit@#{Errbit::Config.host}" + ENV["ERRBIT_ADMIN_EMAIL"] || "errbit@#{Errbit::Config.host}" end def admin_pass - return 'demo-admin' if heroku_pr_review_app? + return "demo-admin" if heroku_pr_review_app? - @admin_pass ||= ENV['ERRBIT_ADMIN_PASSWORD'] || SecureRandom.urlsafe_base64(12)[0, 12] + @admin_pass ||= ENV["ERRBIT_ADMIN_PASSWORD"] || SecureRandom.urlsafe_base64(12)[0, 12] end def heroku_pr_review_app? @@ -33,7 +35,7 @@ def heroku_pr_review_app? user = User.find_or_initialize_by(email: admin_email) -user.name = 'Errbit Admin' +user.name = "Errbit Admin" user.password = admin_pass user.password_confirmation = admin_pass user.username = admin_username if Errbit::Config.user_has_username diff --git a/docker-compose.yml b/docker-compose.yml index 44ef190bcf..0ff7b611db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,11 @@ -version: "3" +volumes: + errbit_mongodb_data: + external: true services: - errbit: - image: errbit/errbit:latest - ports: - - "8080:8080" - depends_on: - - mongo - environment: - - PORT=8080 - - RACK_ENV=production - - MONGO_URL=mongodb://mongo:27017/errbit mongo: - image: mongo:4.1 + image: "mongo:8.3" ports: - - "27017" + - "27017:27017" + volumes: + - "errbit_mongodb_data:/data/db:rw" diff --git a/docs/DEVELOPER-ADVANCED.md b/docs/DEVELOPER-ADVANCED.md index b242023412..e2f54b43bd 100644 --- a/docs/DEVELOPER-ADVANCED.md +++ b/docs/DEVELOPER-ADVANCED.md @@ -1,31 +1,38 @@ # Some Tips to help you when you develop on Errbit -## Running spec on multi-threaded mode +## Install direnv -Running the complete test suite can be really long. You can running it -on multi-fork system with the wonderfull gem of -[@tmm1](http://github.com/tmm1), [test-queue](http://github.com/tmm1/test-queue) +[direnv](https://github.com/direnv/direnv) awesome tool. We recommend +install and use it for development. -If you want do it, you need install in first the gem 'test-queue' +## Configure HTTPS for localhost -``` -gem install test-queue +### Install software + +[mkcert](https://github.com/FiloSottile/mkcert) + +```shell +brew install mkcert nss ``` -After you just need launch the script with adapting runner of mongoid. +### Install root cert +```shell +mkcert -install ``` -./script/rspec_queue_mongoid.rb spec + +### Generate certificates + +```shell +mkcert errbit.lvh.me localhost 127.0.0.1 ::1 ``` -In my case, the complete test suite down to 2min after a 16min long -before. +Rename the two generated files to `errbit.lvh.me.pem` and `errbit.lvh.me.key.pem` respectively. -## Avoid running acceptance test with phantomjs +### Update Procfile.dev -Some acceptance test use phantomjs to interpret the Javascript in page. -To avoid this test you can launch your test by skipping js tag +Uncomment `web-https` and comment `web`. -``` -bundle exec rspec spec --tag="~js" -``` +### Run + +TODO diff --git a/docs/api/stats.md b/docs/api/stats.md index a2781c336c..d888e7ea34 100644 --- a/docs/api/stats.md +++ b/docs/api/stats.md @@ -1,36 +1,28 @@ # Statistics API Documentation - ## Get Stats Represent error statistics. Render JSON if no extension specified on path. - ### Request Example: -```sh - -curl 'http://myerrbit.com/api/v1/stats/app?api_key=6423d563e5285b34cb117f757b2bc7b1' - +```shell +curl 'https://myerrbit.com/api/v1/stats/app?api_key=6423d563e5285b34cb117f757b2bc7b1' ``` Parameters: - **api_key** - required, this endpoints require authentication with an API Key. - ### Response - -```javascript - +```json { - name: "sample app", - id: "552941336a756e4e71012345", - last_error_time: "2015-04-12T08:43:47.480+00:00", - unresolved_errors: 4 + "name": "sample app", + "id": "552941336a756e4e71012345", + "last_error_time": "2015-04-12T08:43:47.480+00:00", + "unresolved_errors": 4 } - ``` diff --git a/docs/apps.md b/docs/apps.md index b84e160e4f..d4da236016 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -1,18 +1,21 @@ # Apps + An Errbit app is a place to collect error notifications from your external application deployments. Each one has a name and a unique API key that your notifiers can use to send notices to Errbit. ## Old Application Versions + You may have many versions of an application running at a given time and some of them may be old enough that you no longer care about errors from those applications. If that's the case, set the LATEST APP VERSION field for your Errbit app, and Errbit will ignore notices from older application versions. Be sure your notifier is setting the context.version field in its notifications (see -[https://airbrake.io/docs/](https://airbrake.io/docs/)). +[https://docs.airbrake.io/](https://docs.airbrake.io/)). ## Excluding some apps when viewing problems (plus other awesome filtering in future) + Normally when you visit the /problems page, you see the most recently received problems from all apps. Let's say you have a situation where you have three apps in the system: awesomeapp, noisy_app, and @@ -23,3 +26,22 @@ to the problems page and exclude the noisy apps from view like this: /problems?filter=-app:noisy_app%20-app:another_noisy_app There is no UI for this feature, just the query param. + +## Linking backtraces to custom repositories + +Errbit builds backtraces with clickable links to GitHub and Bitbucket +repositories. The entry 'custom backtrace URL template' can be used to +support clickable backtraces with other repositories. + +The following fields are available for this template: + +- `%{branch}`: The repo branch name +- `%{file}`: The relative file/path name of the backtrace file +- `%{line}`: The line number the backtrace occurred +- `%{ebranch}`: The URI escaped version of the branch name +- `%{efile}`: The URI escaped version of the file name + +A few examples: + +- Gitea: `https://errbit.example.com/repo/name/src/branch/%{branch}/%{file}#L%{line}` +- Gitlab: `https://errbit.example.com/repo/name/-/blob/%{branch}/%{file}#L%{line}` diff --git a/docs/configuration.md b/docs/configuration.md index 5a6f3bba06..0a3baddc63 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,33 +1,121 @@ # Configuring Errbit -Following the recommendation of [12factor.net](http://12factor.net/config), + +Following the recommendation of [12factor.net](https://12factor.net/config), Errbit takes all of its configuration from environment variables. You can use -[dotenv](https://github.com/bkeepers/dotenv), which is included in the Gemfile, -to fill in any values that you can't or won't supply through the environment. +[dotenv](https://github.com/bkeepers/dotenv), which is included in the +`Gemfile`, to fill in any values that you can't or won't supply through the +environment. In order of precedence Errbit uses: + 1. Environment variables (for example MY_VALUE=abc bundle exec puma) -2. Values provided in a .env file -3. Default values from .env.default +2. Values provided in a `.env` file +3. Default values from `.env.default` file ## Configuration Parameters + +### Ruby on Rails / Puma environment variables + +#### `RAILS_ENV` + +Environment. Common values include `production`, `development`, and `test`. + +Use `production` to run Errbit in production. This is the default in the +container. + +Default: `development`. + +Default in container: `production`. + +#### `PORT` + +Port for Puma (Rails server) to listen on. + +Default: `3000`. + +Default in container: Puma listens on `3000` by default, but the official Docker +image exposes port `80` (via Thruster) for incoming traffic. + +#### `RAILS_LOG_LEVEL` + +Log level for Rails in production (read from `RAILS_LOG_LEVEL` in +`config/environments/production.rb`). Can be `debug`, `info`, `warn`, +`error`, or `fatal`. + +Default in production: `info`. + +Default in container: `info`. + +#### `WEB_CONCURRENCY` + +Puma workers (processes). Note: Errbit's `config/puma.rb` does not set +`workers`, so `WEB_CONCURRENCY` is ignored unless you customize Puma config. + +Default: not set. + +Default in container: not set. + +#### `RAILS_MAX_THREADS` + +Maximum number of threads to use for Puma. + +Default: `3`. + +Default in container: same as default value. + +TIP: The Errbit team recommends setting `RAILS_MAX_THREADS` to `2` in production. + +### `thruster` (gem) environment variables + +#### `THRUSTER_TLS_DOMAIN` + +Domain name to get certificate, e.g., `errbit.example.com`. + +Default: not set. + +Default in container: not set. + +You can learn more about thruster environment variables [here](https://github.com/basecamp/thruster). + +### `rack-timeout` environment variables + +[Here](./rack-timeout.md). + +### Application environment variables + +#### `MONGO_URL` + +MongoDB connection string in the form +`mongodb://username:password@example.com:port/database`. To make it easier to +connect to third-party MongoDB providers, you can supply this value under any of +these names (the first one set wins): `MONGODB_URI`, `MONGOLAB_URI`, +`MONGOHQ_URL`, `MONGODB_URL`, or `MONGO_URL`. + +Default: `mongodb://localhost` (resolves to `mongodb://localhost/errbit_`). + +Default in container: same as default value. + +| Environment variable | Description | Default | Default in container | +|------------------------------|----------------------------------------------------|----------------------|----------------------| +| `SECRET_KEY_BASE` | | | | +| `ERRBIT_HOST` | Hostname to use when building links back to Errbit | `errbit.example.com` | as default | +| `ERRBIT_ADMIN_EMAIL` | | | | +| `ERRBIT_ADMIN_PASSWORD` | | | | +| `ERRBIT_ADMIN_USER` | | | | +| `ERRBIT_CONFIRM_ERR_ACTIONS` | | | | +| `ERRBIT_USER_HAS_USERNAME` | | | | +| `ERRBIT_USE_GRAVATAR` | | | | +| `ERRBIT_GRAVATAR_DEFAULT` | | | | +| `ERRBIT_EMAIL_FROM` | | | | + +
-
ERRBIT_HOST -
Hostname to use when building links back to Errbit -
defaults to errbit.example.com -
ERRBIT_PROTOCOL -
Protocol to use when building links back to Errbit (http or https) -
defaults to http -
ERRBIT_PORT -
TCP port to use when building links back to Errbit -
ERRBIT_ENFORCE_SSL -
When enabled, Errbit forces all traffic over https -
defaults to false
ERRBIT_ADMIN_EMAIL
E-Mail address of initial admin user
defaults to `errbit@errbit.example.com`
ERRBIT_ADMIN_PASSWORD
Password of initial admin user -
defaults to some random string (see output of `$ rake db:seed`) +
defaults to some random string (see output of `$ bundle exec rails db:seed`)
ERRBIT_ADMIN_USER
Username of initial admin user
defaults to `errbit` @@ -47,46 +135,40 @@ In order of precedence Errbit uses:
The value that should be set in the 'from' field for outgoing emails
defaults to errbit@example.com
ERRBIT_EMAIL_AT_NOTICES -
Errbit notifies watchers via email after the set number of occurances of the same error. [0] means notify on every occurance. +
Errbit notifies watchers via email after the set number of occurrences of the same error. [0] means notify on every occurrence.
defaults to [1,10,100]
ERRBIT_PER_APP_EMAIL_AT_NOTICES -
Let every application have it's own configuration rather than using ERRBIT_EMAIL_AT_NOTICES. If this value is true, you can configure each app using the web UI. +
Let every application have it's own configuration rather than using `ERRBIT_EMAIL_AT_NOTICES`. If this value is true, you can configure each app using the web UI.
defaults to false
ERRBIT_NOTIFY_AT_NOTICES -
Notify each application's configured notification service after the set number of occurances of the same error. [0] means notify on every occurance. +
Notify each application's configured notification service after the set number of occurrences of the same error. [0] means notify on every occurrence.
defaults to [0]
ERRBIT_PER_APP_NOTIFY_AT_NOTICES -
Let every application have it's own configuration rather than using ERRBIT_NOTIFY_AT_NOTICES. If this value is set to true, you can configure each app using the web UI. +
Let every application have its own configuration rather than using `ERRBIT_NOTIFY_AT_NOTICES`. If this value is set to true, you can configure each app using the web UI.
defaults to false
ERRBIT_PROBLEM_DESTROY_AFTER_DAYS -
Number of days to keep errors in the database when running rake errbit:clear_outdated +
Number of days to keep errors in the database when running `bundle exec rails errbit:clear_outdated`
defaults to nil (off) -
SERVE_STATIC_ASSETS -
Allow Rails to serve static assets. For most production environments, this should be false because your web server should be configured to serve static assets for you. But some environments like Heroku require this to be true. -
defaults to true
SECRET_KEY_BASE -
For production environments, you should run `rake secret` to generate a secret, unique key for this parameter +
For production environments, you should run `bundle exec rails secret` to generate a secret, unique key for this parameter
defaults to f258ed69266dc8ad0ca79363c3d2f945c388a9c5920fc9a1ae99a98fbb619f135001c6434849b625884a9405a60cd3d50fc3e3b07ecd38cbed7406a4fccdb59c -
MONGO_URL -
URL connection string for mongo in the form mongodb://username:password@example.com:port To more easily set up connections to third party mongo providers, you can call this value MONGODB_URI, MONGOLAB_URI, MONGOHQ_URL, MONGODB_URL or MONGO_URL -
defaults to mongodb://localhost/errbit_<Rails.env>
GITHUB_URL -
Use this URL for interacting github. This is useful if you have a github enterprise account and you're using a URL other than https://github.com +
Use this URL for interacting GitHub. This is useful if you have a GitHub enterprise account and you're using a URL other than https://github.com
defaults to https://github.com
GITHUB_API_URL
-
For github enterprise accounts, the API URL could be something like https://github.example.com/api/v3
+
For GitHub enterprise accounts, the API URL could be something like https://github.example.com/api/v3
defaults to https://api.github.com
GITHUB_AUTHENTICATION -
Allow github sign-in via OAuth +
Allow GitHub sign-in via OAuth
defaults to true
GITHUB_CLIENT_ID -
Client id of your github application +
Client id of your GitHub application
GITHUB_SECRET -
Secret key for your github application +
Secret key for your GitHub application
GITHUB_ORG_ID -
ID of your github organization. If set, Errbit will create user accounts for users in your github organization who sign into Errbit without having a user account +
ID of your GitHub organization. If set, Errbit will create user accounts for users in your GitHub organization who sign into Errbit without having a user account
GITHUB_ACCESS_SCOPE -
OAuth scope to request from users when they sign-in through github +
OAuth scope to request from users when they sign in through GitHub
defaults to [repo]
GITHUB_SITE_TITLE
The title to use for GitHub. This value is whatever you want displayed in the Errbit UI when referring to GitHub.
@@ -115,7 +197,7 @@ In order of precedence Errbit uses:
SMTP_PORT
Server port for outgoing SMTP messages
SMTP_AUTHENTICATION -
Authentication method for the SMTP connection (see http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration) +
Authentication method for the SMTP connection (see https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration)
SMTP_USERNAME
Username for SMTP auth, you could also set SENDGRID_USERNAME
SMTP_PASSWORD diff --git a/docs/deployment.md b/docs/deployment.md index 7191ad5994..c135ea35ab 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,17 +1,14 @@ # Deployment Notes + There are any number of ways to deploy Errbit, but official support is limited to Heroku. See specific notes on deployment via: -- [heroku](deployment/heroku.md) -- [dokku](deployment/dokku.md) -- [docker](deployment/docker.md) -- [kubernetes (experimental)](deployment/kubernetes.md) -You can use a process manager to deploy Errbit, but Errbit doesn't maintain -support for any specific process manager. But if you use systemd, @nofxx has -been kind enough to share: -- [systemd config](https://gist.github.com/nofxx/f01dcfe3e9d504181d76) +* [Heroku](deployment/heroku.md) +* [Dokku](deployment/dokku.md) +* [Docker](deployment/docker.md) +* [Kubernetes (experimental)](deployment/kubernetes.md) ## HTTPS @@ -25,22 +22,22 @@ HTTP. Alternatively, Errbit's Puma can be configured to serve HTTPS directly. Instead of starting Errbit with the command: -```bash -bundle exec puma -C config/puma.default.rb + +```shell +bundle exec puma -C config/puma.rb ``` + start it with: -```bash -bundle exec puma -b "ssl://0.0.0.0:443?key=server.key&cert=server.crt" -C config/puma.default.rb + +```shell +bundle exec puma -b "ssl://0.0.0.0:443?key=server.key&cert=server.crt" -C config/puma.rb ``` + Where `server.key` is a path to the TLS private key and `server.crt` is the path to the TLS certificate. ## Health Checks -If deploying with a system that can check if the app is running as expected then -there are two endpoints that can be used: -- `/health/readiness` - suitable for checking if app is ready to receive - requests. If response status is 200 and body contains `{ "ok": true, -"details": [etc...] }` then the app is ready. -- `/health/liveness` - suitable for pinging periodically to check if app is still - alive. Expected result is `{ "ok": true }`. +Errbit exposes the default Rails health check at `/up`, which returns 200 if +the app boots with no exceptions, otherwise 500. You can use this endpoint for +load balancer or container orchestration health checks. diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index 3ff414b169..283fd6fdec 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -2,42 +2,50 @@ Errbit provides official [Docker](https://www.docker.com/) images to make Docker deployment easy. You can pass all of [Errbit's -configuration](docs/configuration.md) to the Docker container using +configuration](/docs/configuration.md) to the Docker container using `docker run -e`. -When running Errbit using `docker run` you must specify a MONGO_URL. If you're -running in a production environment, you should also specify -RACK_ENV=production and SECRET_KEY_BASE=some-secret-key. +When running Errbit using `docker run` you must specify a `MONGO_URL`. If +you're running in a production environment, you should also specify +`RAILS_ENV=production` and `SECRET_KEY_BASE=some-secret-key`. -If you don't already have one, you can generate a suitable SECRET_KEY_BASE +If you don't already have one, you can generate a suitable `SECRET_KEY_BASE` with: -```bash -docker run --rm errbit/errbit rake secret + +```shell +docker run --rm errbit/errbit bundle exec rails secret ``` +## Which image version should I use? + +As of 2023, we are not releasing non-latest tags to docker hub (last release was v0.9.0 in 2020). +Please use [errbit/errbit:latest](https://hub.docker.com/r/errbit/errbit/tags), which is updated on main builds. +If you are interested in using official release tags of errbit, contributions to the CI process and shipping stable releases would be welcome. + ## Standalone Errbit App Assuming you have a mongo host available, you can run errbit using `docker -run`, exposing its HTTP interface on port 8080: -```bash +run`, exposing its HTTP interface on port 3000: + +```shell docker run \ - -e "RACK_ENV=production" \ + -e "RAILS_ENV=production" \ -e "MONGO_URL=mongodb://my-mongo-host" \ -e "SECRET_KEY_BASE=my$ecre7key123" \ - -p 8080:8080 \ + -p 3000:3000 \ errbit/errbit:latest ``` -Now run `rake errbit:bootstrap` to bootstrap the Errbit db within an ephemeral +Now run `bundle exec rails errbit:bootstrap` to bootstrap the Errbit db within an ephemeral Docker container: -```bash +```shell docker run \ --rm \ - -e "RACK_ENV=production" \ + -e "RAILS_ENV=production" \ -e "MONGO_URL=mongodb://my-mongo-host" \ errbit/errbit:latest \ - rake errbit:bootstrap + bundle exec rails errbit:bootstrap ``` ## Errbit + Dependencies via Docker Compose @@ -45,13 +53,13 @@ docker run \ Docker compose can take care of starting up a mongo container along with the Errbit application container and linking the two containers together: -```bash +```shell docker-compose up -e "SECRET_KEY_BASE=my$ecre7key123" ``` -Now run `rake errbit:bootstrap` to bootstrap the Errbit db within an ephemeral +Now run `bundle exec rails errbit:bootstrap` to bootstrap the Errbit db within an ephemeral Docker container: -```bash -docker exec errbit_errbit_1 rake errbit:bootstrap +```shell +docker exec errbit_errbit_1 bundle exec rails errbit:bootstrap ``` diff --git a/docs/deployment/dokku.md b/docs/deployment/dokku.md index 944eb4028c..b48cfe517e 100644 --- a/docs/deployment/dokku.md +++ b/docs/deployment/dokku.md @@ -1,11 +1,11 @@ # Deploy to Dokku -Deployment to Dokku is very similiar to Heroku. +Deployment to Dokku is very similar to Heroku. For more details see [Heroku](heroku.md) guide. ## Create an app on dokku and push the source code -```bash +```shell dokku apps:create errbit dokku plugin:install https://github.com/dokku/dokku-mongo.git mongo dokku mongo:create errbit errbit @@ -13,18 +13,18 @@ dokku mongo:link errbit errbit # Override the automatic Dockerfile deployment detection dokku config:set errbit BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-ruby.git dokku config:set errbit HEROKU=1 -dokku config:set errbit GEMFILE_RUBY_VERSION=2.5.1 -dokku config:set errbit SECRET_KEY_BASE="$(bundle exec rake secret)" +dokku config:set errbit SECRET_KEY_BASE="$(bundle exec rails secret)" dokku config:set errbit ERRBIT_HOST=some-hostname.example.com dokku config:set errbit ERRBIT_EMAIL_FROM=example@example.com -dokku config:set errbit EMAIL_DELIVERY_METHOD=smtp SMTP_SERVER=172.17.42.1 +dokku config:set errbit EMAIL_DELIVERY_METHOD=smtp +dokku config:set errbit SMTP_SERVER=172.17.42.1 git remote add dokku dokku@:errbit -git push dokku master +git push dokku main ``` -### Prepare the DB +## Prepare the DB -```bash -dokku run errbit rake errbit:bootstrap +```shell +dokku run errbit rails errbit:bootstrap ``` diff --git a/docs/deployment/example/atlas1.jpg b/docs/deployment/example/atlas1.jpg new file mode 100644 index 0000000000..5ebaca55bf Binary files /dev/null and b/docs/deployment/example/atlas1.jpg differ diff --git a/docs/deployment/example/atlas2.png b/docs/deployment/example/atlas2.png new file mode 100644 index 0000000000..4b00df97d2 Binary files /dev/null and b/docs/deployment/example/atlas2.png differ diff --git a/docs/deployment/example/atlas3.png b/docs/deployment/example/atlas3.png new file mode 100644 index 0000000000..30f07464d1 Binary files /dev/null and b/docs/deployment/example/atlas3.png differ diff --git a/docs/deployment/example/atlas4.png b/docs/deployment/example/atlas4.png new file mode 100644 index 0000000000..d5fd5950b4 Binary files /dev/null and b/docs/deployment/example/atlas4.png differ diff --git a/docs/deployment/example/atlas5.png b/docs/deployment/example/atlas5.png new file mode 100644 index 0000000000..e67ca89cad Binary files /dev/null and b/docs/deployment/example/atlas5.png differ diff --git a/docs/deployment/example/atlas6.png b/docs/deployment/example/atlas6.png new file mode 100644 index 0000000000..d3ae697e26 Binary files /dev/null and b/docs/deployment/example/atlas6.png differ diff --git a/docs/deployment/example/atlas7.png b/docs/deployment/example/atlas7.png new file mode 100644 index 0000000000..8a18ad6b29 Binary files /dev/null and b/docs/deployment/example/atlas7.png differ diff --git a/docs/deployment/example/kubernetes/rc.yml b/docs/deployment/example/kubernetes/rc.yml index f0e7e29d45..b88cf3ff53 100644 --- a/docs/deployment/example/kubernetes/rc.yml +++ b/docs/deployment/example/kubernetes/rc.yml @@ -17,12 +17,12 @@ spec: - image: errbit/errbit:latest name: errbit ports: - - containerPort: 8080 + - containerPort: 3000 name: http livenessProbe: httpGet: - path: /robots.txt - port: 8080 + path: /up + port: 3000 initialDelaySeconds: 60 periodSeconds: 10 successThreshold: 1 @@ -36,37 +36,35 @@ spec: memory: "512Mi" cpu: "50m" env: - - name: PORT - value: '8080' - - name: MONGO_URL - value: '' - - name: GOOGLE_AUTHENTICATION - value: 'true' - - name: GOOGLE_CLIENT_ID - value: '' - - name: GOOGLE_SECRET - value: '' - - name: RACK_ENV - value: production - - name: ERRBIT_USE_GRAVATAR - value: 'true' - - name: GITHUB_AUTHENTICATION - value: 'true' - - name: ERRBIT_EMAIL_FROM - value: 'alarm@example.com' - - name: SMTP_SERVER - value: smtp.mailgun.org - - name: SMTP_PORT - value: '587' - - name: SMTP_USERNAME - value: 'postmaster@example.com' - - name: SMTP_PASSWORD - value: '' - - name: SMTP_DOMAIN - value: 'example.com' - - name: ERRBIT_HOST - value: 'errbit.example.com' - - name: ERRBIT_PROTOCOL - value: 'https' - - name: ERRBIT_PORT - value: '443' \ No newline at end of file + - name: "PORT" + value: "3000" + - name: "MONGO_URL" + value: "" + - name: "GOOGLE_AUTHENTICATION" + value: "true" + - name: "GOOGLE_CLIENT_ID" + value: "" + - name: "GOOGLE_SECRET" + value: "" + - name: "RAILS_ENV" + value: "production" + - name: "ERRBIT_USE_GRAVATAR" + value: "true" + - name: "GITHUB_AUTHENTICATION" + value: "true" + - name: "EMAIL_DELIVERY_METHOD" + value: "smtp" + - name: "ERRBIT_EMAIL_FROM" + value: "errbit@errbit.example.org" + - name: "SMTP_SERVER" + value: "smtp.mailgun.org" + - name: "SMTP_PORT" + value: "587" + - name: "SMTP_USERNAME" + value: "postmaster@example.org" + - name: "SMTP_PASSWORD" + value: "" + - name: "SMTP_DOMAIN" + value: "example.org" + - name: "ERRBIT_HOST" + value: "errbit.example.org" diff --git a/docs/deployment/example/kubernetes/svc.yml b/docs/deployment/example/kubernetes/service.yml similarity index 86% rename from docs/deployment/example/kubernetes/svc.yml rename to docs/deployment/example/kubernetes/service.yml index de155619c9..bd5a3116a8 100644 --- a/docs/deployment/example/kubernetes/svc.yml +++ b/docs/deployment/example/kubernetes/service.yml @@ -7,7 +7,7 @@ metadata: spec: ports: - port: 80 - targetPort: 8080 + targetPort: 3000 protocol: TCP selector: name: errbit diff --git a/docs/deployment/heroku.md b/docs/deployment/heroku.md index a52948ed76..da8c8d9680 100644 --- a/docs/deployment/heroku.md +++ b/docs/deployment/heroku.md @@ -5,10 +5,58 @@ If you just want to get started with Errbit and you're not sure how to proceed, you can use this deploy button to get a basic deployment running on Heroku. -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/errbit/errbit/tree/master) +You will need to spin up a MongoDB server if you don't have one at your disposal already. +Free clusters up to 512MB are available with MongoDB Atlas [Here](https://cloud.mongodb.com) -After deploying the application, you still need to run `heroku run rake errbit:bootstrap` -to create indexes and get your admin user set up. +## MongoDB Atlas Setup for Quick Deployment + +Upon creating an account, you will be greeted with this page. Make sure you select Starter Cluster. + +![Cluster selection](./example/atlas1.jpg) + +After this, you will be taken to the below screen: + +![Server selection](./example/atlas2.png) + +Select one of the servers marked with `FREE TIER AVAILABLE` and click on `Create Cluster`. It will only take a couple of minutes to configure everything for you. + +Follow the "Getting Started" tutorial on the bottom left. + +### Create a database user + +Go to the `Database Access` section in the `security` tab. Click on `+ Add New User` to create a new user, whereupon a new screen will pop up. Select `Read and write to any database`, so the bot can properly store the data. Choose a username and password, but make sure they both **don't contain any special character** like `!`, `-`, `?`. Copy the password into your notepad. + +Finally, click `Add User` to finish the creation. + +![Create User](./example/atlas3.png) + +### Whitelist all IP's + +Go to the `Network Access` section in the `security` tab. Click on `+ Add IP Address` to add an IP address, whereupon a new screen will pop up. Click the `Allow Access From Everywhere` button and `0.0.0.0/0` should appear in the `Whitelist Entry`. Otherwise, make sure to put input that manually. Finally, click `Confirm` to confirm your changes. + +![Whitelisting IPs](./example/atlas4.png) + +### Obtain a connection string + +The last part is to generate a Mongo URI. Go to the `Clusters` section in the `Atlas` tab. Click on `Connect` on the left side of your Cluster dashboard. This will open up a new screen where you have three options. For our purposes, select the middle option `Connect Your Application`. + +![Connection String](./example/atlas5.png) + +Be sure to select Ruby and Version 2.7 or later. +You need to copy the connection string, which can be easily done by clicking the `Copy` button. Then replace `` with the password for your user, which you set earlier. Paste the URI in your notepad. + +The final URI looks similar to this: `mongodb+srv://Username:MyPassword@modmail-kjvn21.mongodb.net/`. + +![Connection String - 2](./example/atlas6.png) + +Now that you have your connection URI, click the deploy button below and start your app deployment. Paste the entire URI you obtained from above into the `MONGODB_URI` Text box. + +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/errbit/errbit/tree/main) + +After deploying the application, you still need to run `bundle exec rails errbit:bootstrap` +to create indexes and get your admin user set up. You can do this by clicking the `Run Console` button in the More dropdown button of your new Heroku app. Then paste the rake command above and be sure to copy your admin credentials! + +![Run Console](./example/atlas7.png) ## The Hard Way @@ -16,7 +64,8 @@ We designed Errbit to work well with Heroku. These instructions should result in a working deploy, but you should modify them to suit your needs: ### Clone and prepare the source code repository -```bash + +```shell git clone git@github.com:errbit/errbit.git cd errbit ``` @@ -24,57 +73,61 @@ cd errbit - Update `db/seeds.rb` with admin credentials for your initial login Commit the results: -```bash + +```shell git commit -m "Update db/seeds.rb with initial login" ``` ### Install the heroku toolbelt + [toolbelt.heroku.com](https://toolbelt.heroku.com/) -### Create an app on Heroku and push the source code -```bash +### Create an app on Heroku and push the source code. Use the above documentation to obtain a MongoDB URI from MongoDB Atlas if you don't have one. + +```shell heroku apps:create -heroku addons:create mongolab:sandbox heroku addons:create sendgrid:starter -heroku config:set GEMFILE_RUBY_VERSION=2.5.1 -heroku config:set SECRET_KEY_BASE="$(bundle exec rake secret)" +heroku config:set SECRET_KEY_BASE="$(bundle exec rails secret)" +heroku config:set MONGODB_URI="" heroku config:set ERRBIT_HOST=some-hostname.example.com heroku config:set ERRBIT_EMAIL_FROM=example@example.com -heroku config:set EMAIL_DELIVERY_METHOD=smtp SMTP_SERVER=smtp.sendgrid.net -git push heroku master +heroku config:set EMAIL_DELIVERY_METHOD=smtp +heroku config:set SMTP_SERVER=smtp.sendgrid.net +git push heroku main ``` ### Prepare the DB -```bash -heroku run rake errbit:bootstrap +```shell +heroku run rails errbit:bootstrap ``` ### Schedule recurring tasks + You may want to periodically clear resolved errors to free up space. For that you have a few options: Option 1. With the heroku-scheduler add-on (replacement for cron): -```bash +```shell # Install the heroku scheduler add-on heroku addons:create scheduler:standard # Go open the dashboard to schedule the job. You should use -# 'rake errbit:clear_resolved' as the task command, and schedule it -# at whatever frequency you like (once/day should work great). +# 'bundle exec rails errbit:clear_resolved' as the task command, and schedule +# it at whatever frequency you like (once/day should work great). heroku addons:create scheduler ``` Option 2. With the cron add-on: -```bash +```shell # Install the heroku cron addon, to clear resolved errors daily: heroku addons:create cron:daily ``` Option 3. Clear resolved errors manually: -```bash -heroku run rake errbit:clear_resolved +```shell +heroku run rails errbit:clear_resolved ``` diff --git a/docs/deployment/kubernetes.md b/docs/deployment/kubernetes.md index 1fc0c35ac7..c2e03869f2 100644 --- a/docs/deployment/kubernetes.md +++ b/docs/deployment/kubernetes.md @@ -1,17 +1,20 @@ # Deploy to Kubernetes (experimental) -### Clone and prepare the source code repository -```bash +## Clone and prepare the source code repository + +```shell git clone git@github.com:errbit/errbit.git cd errbit ``` -### Create replication controller -```bash +## Create replication controller + +```shell kubectl create -f docs/deployment/example/kubernetes/rc.yml ``` -### Create svc -```bash -kubectl create -f docs/deployment/example/kubernetes/svc.yml +## Create service + +```shell +kubectl create -f docs/deployment/example/kubernetes/service.yml ``` diff --git a/docs/notices.md b/docs/notices.md new file mode 100644 index 0000000000..9a8d3ca623 --- /dev/null +++ b/docs/notices.md @@ -0,0 +1,17 @@ +# Notices + +This page describes how to look up notices in the Errbit UI. + +## /locate/[notice_id] + +Errbit points clients to this path (in the response body) when they successfully +submit a notice. When you hit this path in a browser (you have to be +authenticated), it will redirect you to the associated Problem. Using the +language used in Errbit code, Notices belong to Errs, Problems can have many +Errs, and Problems belong to Apps. + +## /notices/[notice_id] + +If you happen to know the specific notice_id (perhaps you are a javascript +client, and the server told you, like above), you can look up the specific +notice using this path in a browser. diff --git a/docs/notifications/flowdock/flow_token_api.jpg b/docs/notifications/flowdock/flow_token_api.jpg deleted file mode 100644 index 7feefee6e4..0000000000 Binary files a/docs/notifications/flowdock/flow_token_api.jpg and /dev/null differ diff --git a/docs/notifications/flowdock/index.md b/docs/notifications/flowdock/index.md deleted file mode 100644 index 2caa8f2b75..0000000000 --- a/docs/notifications/flowdock/index.md +++ /dev/null @@ -1,21 +0,0 @@ -# Flowdock - -The flowdock notification send to [flowdock](https://www.flowdock.com/). - -## Configuration - -### Api token - -You need define the API token. -It is purposely called Flow API Token and not Flowdock API Token to make -it more obvious for user which one of the two tokens we expect: - -![flow token](flow_token_api.jpg) - -### Sender ( not configure ) - -If you noticed default Gravatar icon in your Flowdock notifications you -may want to [add Errbit icon](http://gravatar.com) for email that is -set in ERRBIT_EMAIL_FROM. -You don't need to approve or authorize it on Flowdock because it is -used only for an icon. diff --git a/docs/notifications/slack/index.md b/docs/notifications/slack/index.md index 314c4a02ba..cb7ed15d6c 100644 --- a/docs/notifications/slack/index.md +++ b/docs/notifications/slack/index.md @@ -1,6 +1,6 @@ # Slack -The slack notification sends to [Slack](https://www.slack.com/). +The Slack notification sends to [Slack](https://www.slack.com/). ## Configuration @@ -19,4 +19,4 @@ Copy the Hook URL specified by the Slack service. On the App Edit Page, click to highlight the slack integration. Input the hook url from above into the field and click save. -![Errbit Notification Setup](errbit_notification.png) \ No newline at end of file +![Errbit Notification Setup](errbit_notification.png) diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000000..670a0334f5 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,107 @@ +# Quickstart + +This is a quickstart guide to get you up and running with the Errbit. + +## Prerequisites + +* Linux server. We will use Debian 12. +* Docker with Docker Compose plugin +* Public IPv4 address +* (Optional) Public IPv6 address +* Domain name for Errbit. E.g. `errbit.example.com` + +## Installing dependencies + +* [Install Docker Engine on Debian](https://docs.docker.com/engine/install/debian/) +* [Install the Docker Compose plugin](https://docs.docker.com/compose/install/) + +## Run Errbit with Docker + +### Option 1: Rails native with Thruster (and without reverse proxy) + +Create `docker-compose.yml` with the following content: + +```yaml +services: + errbit: + image: "docker.io/errbit/errbit:0.11" + container_name: "errbit" + restart: "unless-stopped" + environment: + MONGO_URL: "mongodb://host:27017/errbit_production" # Replace with URL to your MongoDB instance + SECRET_KEY_BASE: "secret-key-base" # Replace with a secure secret key. You can generate new one with `rails secret` + RAILS_MAX_THREADS: "2" + ERRBIT_HOST: "errbit.example.com" + THRUSTER_TLS_DOMAIN: "errbit.example.com" + ports: + - "80:80" # Listen for HTTP traffic + - "443:443" # Listen for HTTPS traffic + volumes: + - "./thruster:/rails/storage/thruster:rw" # Volume for storing ACME certificate +``` + +### Option 2: with Traefik as reverse proxy + +Create `docker-compose.yml` with the following content: + +```yaml +services: + traefik: + image: "docker.io/library/traefik:3.7.6" + container_name: "traefik" + restart: "unless-stopped" + command: + - "--accesslog=true" + - "--entryPoints.web.address=:80" + - "--entryPoints.websecure.address=:443" + - "--providers.docker=true" + - "--providers.docker.exposedByDefault=false" + - "--certificatesresolvers.letsencrypt.acme.email=me@example.com" # Replace `me@example.com` with your email address + - "--certificatesresolvers.letsencrypt.acme.storage=/acme.json" + - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" + - "--entryPoints.web.http.redirections.entrypoint.to=websecure" + - "--entryPoints.web.http.redirections.entrypoint.scheme=https" + ports: + - "80:80" # Listen for HTTP traffic + - "443:443" # Listen for HTTPS traffic + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" # Traefik needs access to Docker socket to discover containers + - "./acme.json:/acme.json" # Here we store ACME certificates + + errbit: + image: "docker.io/errbit/errbit:0.11" + container_name: "errbit" + restart: "unless-stopped" + environment: + MONGO_URL: "mongodb://host:27017/errbit_production" # Replace with URL to your MongoDB instance + SECRET_KEY_BASE: "secret-key-base" # Replace with a secure secret key. You can generate new one with `rails secret` + RAILS_MAX_THREADS: "2" + ERRBIT_HOST: "errbit.example.com" # Replace with your domain name + labels: + - "traefik.enable=true" + - "traefik.http.routers.errbit.rule=Host(`errbit.example.com`)" # Replace `errbit.example.com` with your domain name + - "traefik.http.routers.errbit.tls=true" + - "traefik.http.routers.errbit.tls.certresolver=letsencrypt" + - "traefik.http.routers.errbit.entrypoints=websecure" +``` + +Run with: + +```shell +docker compose pull +docker compose up -d +``` + +Stop with: + +```shell +docker compose down +``` + +If you are updating Errbit: + +```shell +docker compose down +docker compose pull +docker compose up -d +``` diff --git a/docs/rack-timeout.md b/docs/rack-timeout.md new file mode 100644 index 0000000000..4ac60ea61b --- /dev/null +++ b/docs/rack-timeout.md @@ -0,0 +1,35 @@ +# Configuring `rack-timeout` gem + +## `RACK_TIMEOUT_SERVICE_TIMEOUT` + +Default: `15` (seconds). + +Default in container: same as default value. + +## `RACK_TIMEOUT_WAIT_TIMEOUT` + +Default: `30` (seconds). + +Default in container: same as default value. + +## `RACK_TIMEOUT_WAIT_OVERTIME` + +Default: `60` (seconds). + +Default in container: same as default value. + +## `RACK_TIMEOUT_SERVICE_PAST_WAIT` + +Default: `false`. + +Default in container: same as default value. + +## `RACK_TIMEOUT_TERM_ON_TIMEOUT` + +Default: `0`. + +Default in container: same as default value. + +## Links + +For more information, refer to [rack-timeout documentation](https://github.com/zombocom/rack-timeout). diff --git a/docs/relnotes/v0.10.0.md b/docs/relnotes/v0.10.0.md new file mode 100644 index 0000000000..3a6d5f8f4f --- /dev/null +++ b/docs/relnotes/v0.10.0.md @@ -0,0 +1,107 @@ +# v0.10.0 Release notes + +Hello, + +I am Igor (Ihor) Zubkov, new maintainer of Errbit. + +## Notable changes + +### Improvements + +* Upgraded Ruby and Ruby on Rails gives us huge performance boost. +* Less memory, faster application due `# frozen_string_literal: true`. +* Mostly everything, upgraded. No more known CVE in dependencies. + +## Security Improvements + +* Devise: change bcrypt cost from `11` to `12`. +* Devise: change password_length from `6..128` to `8..128`. +* Bundler: enable checksums. + +## All changes + +1. Upgrade Ruby from 2.5.1 to 3.4.3 +2. Upgrade Ruby on Rails from 4.2.11.1 to 7.2.2.1 +3. Upgrade RubyGems to 3.6.8 +4. Upgrade Bundler to 2.6.8 +5. Enable Bundler 2.6 feature checksums +6. Enable bootsnap +7. Migrate to zeitwerk +8. Migrate to `# frozen_string_literal: true`. Less memory, faster application. +9. You can run Errbit with JRuby 9.4. Experimental. We need help with docker jruby container to mark it as production-ready. +10. Flowdock support was removed. This service is no longer running. +11. We migrate from Circle CI (Sorry, Circle CI!) to GitHub Actions +12. Test suite much faster and stable. But, we are still working on stability. Especially, for JRuby 9.4. +13. We integrate `vcr` and `webmock` to test suite to external calls. +14. Remove all bundler binstubs that is not in use +15. Dependabot: take care of any security and not security updates +16. Integrate thruster in docker image. So, you can run Errbit in docker without reverse proxy. + +## Deprecations and removals + +### Custom user gemfile name + +Support of custom user gemfile name deprecated and removed. From now, +always use `UserGemfile` as custom user gemfile. So, if you have +`USER_GEMFILE` env, just remove it. And rename custom user gemfile to +`UserGemfile`. + +### Force SSL/TLS + +From v0.10.0, Errbit requires reverse proxy with SSL/TLS support. + +So, we enable `config.force_ssl = true` in `config/environments/production.rb`. + +`ERRBIT_ENFORCE_SSL` env support and related code removed. + +We recommend to use Traefik as reverse proxy. Or, you can run +Errbit with [`thruster`](https://github.com/basecamp/thruster). +Thruster integrated in our docker image. + +`ERRBIT_PROTOCOL` and `ERRBIT_PORT` env support and related code was +removed too. It doesn't have any sense. `ERRBIT_PROTOCOL` is always +`https` and `ERRBIT_PORT` is always `443`. + +### `SERVE_STATIC_ASSETS` env + +Support of `SERVE_STATIC_ASSETS` env was removed. Just remove it +from configuration. + +If you run Errbit behind reverse proxy in docker container, +thruster will serve static assets. + +If you run Errbit without reverse proxy, docker container has +thruster inside. It will serve static assets and take care +about HTTPS (ACME). + +### `RACK_ENV` env + +`RACK_ENV` env support was removed. Use `RAILS_ENV` env for this. + +### `ERRBIT_LOG_LEVEL` env + +`ERRBIT_LOG_LEVEL` env support was removed. Use `RAILS_LOG_LEVEL` +for this. + +### MongoDB support + +* MongoDB 4.0 is reached EOL on 21 Jun 2018. 4.0 support will be removed after v0.10.0 release. +* MongoDB 4.2 is reached EOL on 09 Aug 2019. 4.2 support will be removed after v0.10.0 release. +* MongoDB 4.4 is reached EOL an 25 Jul 2020. 4.4 support will be removed after v0.10.0 release. +* MongoDB 5.0 is reached EOL on 08 Jul 2021. 5.0 support will be removed after v0.10.0 release. + +So, after upgrading Errbit to v0.10.0, upgrade MongoDB to 6.0 or later. + +## Docker + +Docker image is recommended way to run Errbit. + +You can run Errbit with reverse proxy like Traefik or with +thruster. Thruster is integrated in our docker image. + +Read our new Quick Start Guide. + +## Support plan + +We plan support Errbit v0.10 branch for 6 months til next release. Bug-fixes +and security. And 3 month after release of v0.11 with security fixes. diff --git a/docs/relnotes/v0.10.1.md b/docs/relnotes/v0.10.1.md new file mode 100644 index 0000000000..848ec13119 --- /dev/null +++ b/docs/relnotes/v0.10.1.md @@ -0,0 +1,12 @@ +# v0.10.1 Release notes + +This is security and bug-fix release. + +## Bug-fixes + +1. Update airbrake installation instructions + +## Security + +1. Bump thruster from 0.1.12 to 0.1.13 +2. Bump nokogiri from 1.18.7 to 1.18.8 ([CVE-2025-32414](https://github.com/advisories/GHSA-mfrm-w63c-3x58) and [CVE-2025-32415](https://github.com/advisories/GHSA-w8fw-fj9q-vcjj) in libxml2) diff --git a/docs/relnotes/v0.10.10.md b/docs/relnotes/v0.10.10.md new file mode 100644 index 0000000000..0a120e84fd --- /dev/null +++ b/docs/relnotes/v0.10.10.md @@ -0,0 +1,9 @@ +# v0.10.10 Release notes + +This is a security release. + +## Security + +1. Update `nokogiri` gem from 1.19.2 to 1.19.3: + * [GHSA-c4rq-3m3g-8wgx](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-c4rq-3m3g-8wgx) + * [GHSA-v2fc-qm4h-8hqv](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-v2fc-qm4h-8hqv) diff --git a/docs/relnotes/v0.10.11.md b/docs/relnotes/v0.10.11.md new file mode 100644 index 0000000000..ec387d0b5c --- /dev/null +++ b/docs/relnotes/v0.10.11.md @@ -0,0 +1,28 @@ +# v0.10.11 Release notes + +This is a security and bug-fix release. + +## Security + +1. Update `devise` gem from 5.0.3 to 5.0.4. [CVE-2026-40295](https://github.com/advisories/GHSA-jp94-3292-c3xv) +2. Update `faraday` gem from 2.14.1 to 2.14.2. [CVE-2026-33637](https://github.com/advisories/GHSA-5rv5-xj5j-3484) +3. Update `jwt` gem from 3.1.2 to 3.2.0. [CVE-2026-44351](https://github.com/advisories/GHSA-c32j-vqhx-rx3x) +4. Update `oauth2` gem from 2.0.18 to 2.0.19. [Redact sensitive values from debug logging output, including Authorization headers and common token/secret fields in headers, query strings, form bodies, and JSON payloads](https://github.com/ruby-oauth/oauth2/releases/tag/v2.0.19) +5. Update `rack` gem from 3.1.20 to 3.1.21: + * [CVE-2026-34763](https://github.com/advisories/GHSA-7mqq-6cf9-v2qp) + * [CVE-2026-34230](https://github.com/advisories/GHSA-v569-hp3g-36wr) + * [CVE-2026-32762](https://github.com/advisories/GHSA-qfgr-crr9-7r49) + * [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) + * [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) + * [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) + * [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) + * [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) + * [CVE-2026-34830](https://github.com/advisories/GHSA-qv7j-4883-hwh7) + * [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) + * [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) + * [CVE-2026-34827](https://github.com/advisories/GHSA-v6x5-cg8r-vv6x) + +## Bug-fixes + +1. Update `thruster` gem from 0.1.20 to 0.1.21 +2. Sync GitHub Actions with main. diff --git a/docs/relnotes/v0.10.12.md b/docs/relnotes/v0.10.12.md new file mode 100644 index 0000000000..dca1f33363 --- /dev/null +++ b/docs/relnotes/v0.10.12.md @@ -0,0 +1,40 @@ +# v0.10.12 Release notes + +This is a security and bug-fix release. + +## Security + +1. Update `puma` gem from 6.6.0 to 8.0.2: + - [CVE-2026-47736](https://github.com/puma/puma/security/advisories/GHSA-qpgp-93vx-g8v8) + - [CVE-2026-47737](https://github.com/puma/puma/security/advisories/GHSA-2vqw-3mp8-cgmx) +2. Update `net-imap` gem from 0.6.4 to 0.6.4.1: + - [CVE-2026-47240](https://github.com/advisories/GHSA-8p34-64r3-mwg8) + - [CVE-2026-47241](https://github.com/advisories/GHSA-c4fp-cxrr-mj66) + - [CVE-2026-47242](https://github.com/advisories/GHSA-46q3-7gv7-qmgg) +3. Update `css_parser` gem from 1.21.1 to 3.0.0: + - [CVE-2026-44312](https://github.com/premailer/css_parser/security/advisories/GHSA-ff6c-w6qf-7xqc) + - [CVE-2026-53727](https://github.com/premailer/css_parser/security/advisories/GHSA-9pmc-p236-855h) +4. Update `nokogiri` gem from 1.19.3 to 1.19.4: + - [GHSA-g9g8-vgvw-g3vf](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-g9g8-vgvw-g3vf) + - [GHSA-p67v-3w7g-wjg7](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-p67v-3w7g-wjg7) + - [GHSA-wfpw-mmfh-qq69](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-wfpw-mmfh-qq69) + - [GHSA-wjv4-x9w8-wm3h](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-wjv4-x9w8-wm3h) + - [GHSA-phwj-rprq-35pp](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-phwj-rprq-35pp) + - [GHSA-9cv2-cfxc-v4v2](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-9cv2-cfxc-v4v2) + - [GHSA-5v8h-3h3q-446p](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-5v8h-3h3q-446p) + - [GHSA-5prr-v3j2-97mh](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-5prr-v3j2-97mh) +5. Update `faraday` gem from 2.14.2 to 2.14.3: + - [CVE-2026-54297](https://github.com/lostisland/faraday/security/advisories/GHSA-98m9-hrrm-r99r) +6. Update `json` gem from 2.10.2 to 2.19.9: + - [CVE-2026-54696](https://github.com/ruby/json/security/advisories/GHSA-x2f5-4prf-w687) +7. Update `oauth2` gem from 2.0.19 to 2.0.24: + - [GHSA-pp92-crg2-gfv9](https://github.com/ruby-oauth/oauth2/security/advisories/GHSA-pp92-crg2-gfv9) +8. Update `concurrent-ruby` gem from 1.3.5 to 1.3.7: + - [CVE-2026-54904](https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-h8w8-99g7-qmvj) + - [CVE-2026-54905](https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-wv3x-4vxv-whpp) + - [CVE-2026-54906](https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-6wx8-w4f5-wwcr) + +## Bug-fixes + +1. Update `brakeman` gem from 8.0.4 to 8.0.5. +2. Sync GitHub Actions with main. diff --git a/docs/relnotes/v0.10.2.md b/docs/relnotes/v0.10.2.md new file mode 100644 index 0000000000..9a0ba4bf76 --- /dev/null +++ b/docs/relnotes/v0.10.2.md @@ -0,0 +1,7 @@ +# v0.10.2 Release notes + +This is security release. + +## Security + +1. Bump net-imap from 0.5.6 to 0.5.8 ([CVE-2025-43857](https://github.com/advisories/GHSA-j3g3-5qv5-52mj)) diff --git a/docs/relnotes/v0.10.3.md b/docs/relnotes/v0.10.3.md new file mode 100644 index 0000000000..a24aa8933a --- /dev/null +++ b/docs/relnotes/v0.10.3.md @@ -0,0 +1,9 @@ +# v0.10.3 Release notes + +This is security release. + +## Security + +1. Bump rack-session from 2.1.0 to 2.1.1 ([CVE-2025-46336](https://github.com/rack/rack-session/security/advisories/GHSA-9j94-67jr-4cqj)) +2. Bump rack from 3.1.13 to 3.1.14 ([CVE-2025-46727](https://github.com/rack/rack/security/advisories/GHSA-gjh7-p2fx-99vx)) +3. JRuby 9.4.12.1 ([CVE-2025-46551](https://github.com/jruby/jruby-openssl/security/advisories/GHSA-72qj-48g4-5xgx)) diff --git a/docs/relnotes/v0.10.4.md b/docs/relnotes/v0.10.4.md new file mode 100644 index 0000000000..7e81c0ea5b --- /dev/null +++ b/docs/relnotes/v0.10.4.md @@ -0,0 +1,7 @@ +# v0.10.4 Release notes + +This is security release. + +## Security + +1. Bump rack from 3.1.14 to 3.1.16 ([CVE-2025-49007](https://github.com/advisories/GHSA-47m2-26rw-j2jw)) diff --git a/docs/relnotes/v0.10.5.md b/docs/relnotes/v0.10.5.md new file mode 100644 index 0000000000..061ce8ba31 --- /dev/null +++ b/docs/relnotes/v0.10.5.md @@ -0,0 +1,18 @@ +# v0.10.5 Release notes + +This is security and bug-fix release. + +## Security + +1. Update Ruby On Rails to 7.2.2.2. [CVE-2025-55193](https://github.com/advisories/GHSA-76r7-hhxj-r776) and [CVE-2025-24293](https://github.com/advisories/GHSA-r4mg-4433-c7g3). +2. Update nokogiri gem to 1.18.9. [Multiple CVEs](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-353f-x4gh-cqq8). +3. Update thruster gem to 0.1.15. + +## Bug-fixes + +1. Ruby 3.4.5. +2. JRuby 9.4.13.0. +3. Update base ruby docker image. +4. Update RubyGems to 3.6.9. +5. Update Bundler to 2.6.9. +6. Fix deprecations. Fix [#2042](https://github.com/errbit/errbit/issues/2042). diff --git a/docs/relnotes/v0.10.6.md b/docs/relnotes/v0.10.6.md new file mode 100644 index 0000000000..edd584cab0 --- /dev/null +++ b/docs/relnotes/v0.10.6.md @@ -0,0 +1,22 @@ +# v0.10.6 Release notes + +This is security and bug-fix release. + +## Security + +1. Update uri gem to 1.0.4. Fix: [CVE-2025-61594](https://www.ruby-lang.org/en/news/2025/10/07/uri-cve-2025-61594/). +2. Update rexml gem from 3.4.1 to 3.4.4. Fix: [CVE-2025-58767](https://github.com/ruby/rexml/security/advisories/GHSA-c2f4-jgmc-q2r5). +3. Update rack gem to 3.1.18. Fixes: + * [CVE-2025-61770](https://github.com/rack/rack/security/advisories/GHSA-p543-xpfm-54cp) + * [CVE-2025-61771](https://github.com/rack/rack/security/advisories/GHSA-w9pc-fmgc-vxvw) + * [CVE-2025-61772](https://github.com/rack/rack/security/advisories/GHSA-wpv5-97wm-hp9c) + * [CVE-2025-59830](https://github.com/rack/rack/security/advisories/GHSA-625h-95r8-8xpm) + * [CVE-2025-61919](https://github.com/rack/rack/security/advisories/GHSA-6xw4-3v39-52mm) +4. Update rack-protection gem to 4.2.1. Fix: [CVE-2025-61921](https://github.com/sinatra/sinatra/security/advisories/GHSA-mr3q-g2mv-mr4q). + +## Bug-fixes + +1. Ruby 3.4.7. +2. JRuby 9.4.14.0. +3. Sync GitHub Actions with main. +4. Update base ruby docker image. diff --git a/docs/relnotes/v0.10.7.md b/docs/relnotes/v0.10.7.md new file mode 100644 index 0000000000..2044b2f6c2 --- /dev/null +++ b/docs/relnotes/v0.10.7.md @@ -0,0 +1,18 @@ +# v0.10.7 Release notes + +This is security and bug-fix release. + +## Security + +1. Update httparty gem to 0.24.2. Fix: [CVE-2025-68696](https://github.com/advisories/GHSA-hm5p-x4rq-38w4). +2. Brakeman: set `Rails.application.config.action_dispatch.cookies_serializer` to `:json` (#2425). +3. Brakeman: add `rel: "noopener noreferrer"` to all places with `target: "_blank"` (#2425). + +## Bug-fixes + +1. Update `brakeman` gem from 7.0.2 to 7.1.1. +2. Update `thruster` gem from 0.1.15 to 0.1.17. +3. Update tzinfo-data gem from 1.2025.2 to 1.2025.3. +4. Ruby 3.4.8. +5. Sync GitHub Actions with main. +6. Fix API JSON serialization for Mongoid documents (PR #2601) by @elektronaut diff --git a/docs/relnotes/v0.10.8.md b/docs/relnotes/v0.10.8.md new file mode 100644 index 0000000000..6b00f69913 --- /dev/null +++ b/docs/relnotes/v0.10.8.md @@ -0,0 +1,12 @@ +# v0.10.8 Release notes + +This is security and bug-fix release. + +## Security + +1. Update `rack` gem from 3.1.18 to 3.1.20. [CVE-2026-25500](https://github.com/rack/rack/security/advisories/GHSA-whrj-4476-wvmp) and [CVE-2026-22860](https://github.com/rack/rack/security/advisories/GHSA-mxw3-3hh2-x2mh). +2. Update `faraday` gem from 2.13.0 to 2.14.1 [CVE-2026-25765](https://github.com/lostisland/faraday/security/advisories/GHSA-33mh-2634-fwr2). + +## Bug-fixes + +1. Sync GitHub Actions with main. diff --git a/docs/relnotes/v0.10.9.md b/docs/relnotes/v0.10.9.md new file mode 100644 index 0000000000..0c8dfb3b06 --- /dev/null +++ b/docs/relnotes/v0.10.9.md @@ -0,0 +1,25 @@ +# v0.10.9 Release notes + +This is a security and bug-fix release. + +## Compatibility + +NOTE: This release no longer supports JRuby. + +## Security + +1. Ruby 3.4.9. [CVE-2026-27820 in zlib gem](https://www.ruby-lang.org/en/news/2026/03/11/ruby-3-4-9-released/) +2. Update `bcrypt` gem from 3.1.20 to 3.1.22. [CVE-2026-33306](https://github.com/bcrypt-ruby/bcrypt-ruby/security/advisories/GHSA-f27w-vcwj-c954) +3. Update `nokogiri` gem from 1.18.9 to 1.19.2. [GHSA-wx95-c6cv-8532](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-wx95-c6cv-8532) +4. Update `loofah` gem from 2.24.0 to 2.25.1. [GHSA-46fp-8f5p-pf2m](https://github.com/flavorjones/loofah/security/advisories/GHSA-46fp-8f5p-pf2m) +5. Update `devise` gem from 4.9.4 to 5.0.3. [CVE-2026-32700](https://github.com/heartcombo/devise/security/advisories/GHSA-57hq-95w6-v4fc) +6. Update `addressable` gem from 2.8.7 to 2.9.0. [CVE-2026-35611](https://github.com/sporkmonger/addressable/security/advisories/GHSA-h27x-rffw-24p4) +7. Update `rack-session` gem from 2.1.1 to 2.1.2. [CVE-2026-39324](https://github.com/rack/rack-session/security/advisories/GHSA-33qg-7wpp-89cq) +8. Update Ruby on Rails from 7.2.2.2 to 7.2.3.1. [CVE-2026-33176](https://github.com/advisories/GHSA-2j26-frm8-cmj9), [CVE-2026-33170](https://github.com/advisories/GHSA-89vf-4333-qx8v), [CVE-2026-33169](https://github.com/advisories/GHSA-cg4j-q9v8-6v38), [CVE-2026-33168](https://github.com/advisories/GHSA-v55j-83pf-r9cq), [CVE-2026-33173](https://github.com/advisories/GHSA-qcfx-2mfw-w4cg), [CVE-2026-33174](https://github.com/advisories/GHSA-r46p-8f7g-vvvg), [CVE-2026-33658](https://github.com/advisories/GHSA-p9fm-f462-ggrg), [CVE-2026-33195](https://github.com/advisories/GHSA-9xrj-h377-fr87), [CVE-2026-33202](https://github.com/advisories/GHSA-73f9-jhhh-hr5m). + +## Bug-fixes + +1. Update `thruster` gem from 0.1.17 to 0.1.20. +2. Update `bundler-audit` gem from 0.9.2 to 0.9.3. +3. Update `brakeman` gem from 7.1.1 to 8.0.4. +4. Sync GitHub Actions with main. diff --git a/docs/relnotes/v0.11.0.md b/docs/relnotes/v0.11.0.md new file mode 100644 index 0000000000..f612b36cf9 --- /dev/null +++ b/docs/relnotes/v0.11.0.md @@ -0,0 +1,37 @@ +# v0.11.0 Release notes + +Notable changes: + +1. This is the last release that supports MongoDB +2. Upgrade to Ruby 4.0.5 +3. Upgrade to RubyGems 4.0.13 +4. Upgrade to Bundler 4.0.13 +5. Upgrade to Rails 8.1.3 +6. Drop JRuby support +7. Remove Gtalk notification service (#2517). Gtalk is dead. +8. Remove HipChat notification service (#2524). HipChat is dead. +9. Drop support for MongoDB 4.0, 4.2, 4.4, 5.0 and 6.0. +10. Add support for MongoDB 8.2. +11. `Dockerfile.alpine` removed. We don't want to support two docker builds. + +Fixes: + +1. Fix API JSON serialization for Mongoid documents (PR #2597) by @elektronaut +2. Fix `ResolvedProblemClearer`. Compact can fail (second case for #1551). PR #1905. + +Changes: + +1. Migrate `app/views/**/*.haml.erb` to `app/views/**/*.html.erb`. Thanks, @ramonetorres. (#1853) +2. Strict locals for ActionView (#1889, #1897). +3. Add GitHub Action for Bundler Audit (#1901). +4. Replace Fabrication with FactoryBot (#1862). +5. Replace outdated `ri_cal` gem with `icalendar` (#2407). +6. Replace outdated `rushover` gem with `pushover2` (#1852). +7. Start integrating the `pundit` gem. From now on, only admin users can see all users. +8. Add Brakeman GitHub Action. +9. Add `.editorconfig` +10. Integrate Trivy for container image scanning. +11. The deprecated `sucker_punch` gem was removed. Active Job now uses the `async` queue adapter instead. +12. Remove dead assets for Gtalk: `app/assets/images/gtalk_create.png`, `app/assets/images/gtalk_goto.png` and `app/assets/images/gtalk_inactive.png`. +13. Remove dead assets for HipChat: `app/assets/images/hipchat_create.png`, `app/assets/images/hipchat_goto.png` and `app/assets/images/hipchat_inactive.png`. +14. Add mailer previews. diff --git a/docs/relnotes/v0.11.1.md b/docs/relnotes/v0.11.1.md new file mode 100644 index 0000000000..3acb1abb30 --- /dev/null +++ b/docs/relnotes/v0.11.1.md @@ -0,0 +1,29 @@ +# v0.11.1 Release notes + +## Stack updates + +1. Update `rubygems` from 4.0.13 to 4.0.14. +2. Update `bundler` from 4.0.13 to 4.0.14. +3. Add support for MongoDB 8.3. + +## Security + +1. Update `msgpack` gem from 1.8.1 to 1.8.3. [GHSA-4mrv-5p47-p938](https://github.com/msgpack/msgpack-ruby/security/advisories/GHSA-4mrv-5p47-p938). +2. Update `net-imap` gem from 0.6.4 to 0.6.4.1. [CVE-2026-47240](https://github.com/advisories/GHSA-8p34-64r3-mwg8), [CVE-2026-47241](https://github.com/advisories/GHSA-c4fp-cxrr-mj66), [CVE-2026-47242](https://github.com/advisories/GHSA-46q3-7gv7-qmgg). +3. Update `json` gem from 2.19.8 to 2.19.9. [GHSA-x2f5-4prf-w687](https://github.com/ruby/json/security/advisories/GHSA-x2f5-4prf-w687). +4. Update `faraday` gem from 2.14.2 to 2.14.3. [CVE-2026-54297](https://github.com/lostisland/faraday/security/advisories/GHSA-98m9-hrrm-r99r). +5. Update `nokogiri` gem from 1.19.3 to 1.19.4: + - [GHSA-g9g8-vgvw-g3vf](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-g9g8-vgvw-g3vf) + - [GHSA-p67v-3w7g-wjg7](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-p67v-3w7g-wjg7) + - [GHSA-wfpw-mmfh-qq69](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-wfpw-mmfh-qq69) + - [GHSA-wjv4-x9w8-wm3h](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-wjv4-x9w8-wm3h) + - [GHSA-phwj-rprq-35pp](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-phwj-rprq-35pp) + - [GHSA-9cv2-cfxc-v4v2](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-9cv2-cfxc-v4v2) + - [GHSA-5v8h-3h3q-446p](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-5v8h-3h3q-446p) + - [GHSA-5prr-v3j2-97mh](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-5prr-v3j2-97mh) + - [GHSA-8678-w3jw-xfc2](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-8678-w3jw-xfc2) + +## Changes + +1. Add `rubocop-erb` gem and fix RuboCop warnings (#2940). +2. Add proper `lang="en"` to HTML (#2920). diff --git a/docs/relnotes/v0.11.2.md b/docs/relnotes/v0.11.2.md new file mode 100644 index 0000000000..cf604b204b --- /dev/null +++ b/docs/relnotes/v0.11.2.md @@ -0,0 +1,20 @@ +# v0.11.2 Release notes + +## Stack updates + +1. Update `rubygems` from 4.0.14 to 4.0.15. +2. Update `bundler` from 4.0.14 to 4.0.15. + +## New features + +1. Add `ERRBIT_DISABLE_FORCE_SSL` environment variable. Set to `true` to disable force SSL. + Issue [#2938](https://github.com/errbit/errbit/issues/2938), PR [#2987](https://github.com/errbit/errbit/pull/2987). + +## Security + +1. Update `websocket-driver` gem from 0.8.1 to 0.8.2. [GHSA-2x63-gw47-w4mm](https://github.com/faye/websocket-driver-ruby/security/advisories/GHSA-2x63-gw47-w4mm). +2. Update `crass` gem from 1.0.6 to 1.0.7: + - [GHSA-6wmf-3r64-vcwv](https://github.com/rgrove/crass/security/advisories/GHSA-6wmf-3r64-vcwv) + - [GHSA-6jxj-px6v-747w](https://github.com/rgrove/crass/security/advisories/GHSA-6jxj-px6v-747w) + - [GHSA-wwpr-jff3-395c](https://github.com/rgrove/crass/security/advisories/GHSA-wwpr-jff3-395c) + - [GHSA-8vfg-2r28-hvhj](https://github.com/rgrove/crass/security/advisories/GHSA-8vfg-2r28-hvhj) diff --git a/docs/relnotes/v0.11.3.md b/docs/relnotes/v0.11.3.md new file mode 100644 index 0000000000..dc876f121b --- /dev/null +++ b/docs/relnotes/v0.11.3.md @@ -0,0 +1,22 @@ +# v0.11.3 Release notes + +## Stack updates + +1. Update `rubygems` from 4.0.15 to 4.0.16. +2. Update `bundler` from 4.0.15 to 4.0.16. +3. Update Ruby from 4.0.5 to 4.0.6. + +## New features + +1. Enable `rack-timeout` gem. + +## Security + +1. Update `loofah` gem from 2.25.1 to 2.25.2: + - [GHSA-5qhf-9phg-95m2](https://github.com/flavorjones/loofah/security/advisories/GHSA-5qhf-9phg-95m2) + - [GHSA-8whx-365g-h9vv](https://github.com/flavorjones/loofah/security/advisories/GHSA-8whx-365g-h9vv) + - [GHSA-9wjq-cp2p-hrgf](https://github.com/flavorjones/loofah/security/advisories/GHSA-9wjq-cp2p-hrgf) +2. Update `rails-html-sanitizer` gem from 1.7.0 to 1.7.1: + - [GHSA-cj75-f6xr-r4g7](https://github.com/rails/rails-html-sanitizer/security/advisories/GHSA-cj75-f6xr-r4g7) +3. Update `thruster` gem from 0.1.23 to 0.1.24: + - [CVE-2026-42505](https://github.com/advisories/GHSA-ff52-ph69-cf7x) diff --git a/docs/relnotes/v0.11.4.md b/docs/relnotes/v0.11.4.md new file mode 100644 index 0000000000..53ca9d2945 --- /dev/null +++ b/docs/relnotes/v0.11.4.md @@ -0,0 +1,17 @@ +# v0.11.4 Release notes + +## Stack updates + +1. Update `rubygems` from 4.0.16 to 4.0.18. +2. Update `bundler` from 4.0.16 to 4.0.18. + +## New features + +None. + +## Security + +1. Update Ruby on Rails from 8.1.3 to 8.1.3.1: + - [CVE-2026-66066](https://github.com/rails/rails/security/advisories/GHSA-xr9x-r78c-5hrm) +2. Update `json` gem from 2.21.1 to 2.21.2: + - [GHSA-9hj4-r449-hfvc](https://github.com/ruby/json/security/advisories/GHSA-9hj4-r449-hfvc) diff --git a/docs/relnotes/v0.11.5.md b/docs/relnotes/v0.11.5.md new file mode 100644 index 0000000000..f4856af2a8 --- /dev/null +++ b/docs/relnotes/v0.11.5.md @@ -0,0 +1,23 @@ +# v0.11.5 Release notes + +## Stack updates + +1. Update `rubygems` from 4.0.18 to 4.0.19 by @biow0lf in [errbit/errbit#3103](https://github.com/errbit/errbit/pull/3103) +2. Update `bundler` from 4.0.18 to 4.0.19 by @biow0lf in [errbit/errbit#3103](https://github.com/errbit/errbit/pull/3103) +3. Update `thruster` gem from 0.1.25 to 0.1.26 by dependabot in [errbit/errbit#3101](https://github.com/errbit/errbit/pull/3101) + +## New features + +1. Configure [HTTP Permissions Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy) by @biow0lf in [errbit/errbit#3108](https://github.com/errbit/errbit/pull/3108) + +## Security + +1. Enable a strict application-wide [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) with per-request nonces and CSP-compatible UI behavior by @mikelolasagasti in [errbit/errbit#3109](https://github.com/errbit/errbit/pull/3109) +2. Update `omniauth-google-oauth2` gem from 1.2.2 to 1.2.3 by dependabot in [errbit/errbit#3122](https://github.com/errbit/errbit/pull/3122) +3. Fix XSS in comment notification email by @mikelolasagasti in [errbit/errbit#3094](https://github.com/errbit/errbit/pull/3094). [GHSA-623h-px8r-9643](https://github.com/errbit/errbit/security/advisories/GHSA-623h-px8r-9643). CVE is pending. +4. XSS via notice backtrace file path. Fixed by @mikelolasagasti in [errbit/errbit#3109](https://github.com/errbit/errbit/pull/3109). [GHSA-p452-6f7x-vcqx](https://github.com/errbit/errbit/security/advisories/GHSA-p452-6f7x-vcqx). CVE is pending. + +## Thanks + +1. @saidM for reporting [GHSA-p452-6f7x-vcqx](https://github.com/errbit/errbit/security/advisories/GHSA-p452-6f7x-vcqx). +2. @mikelolasagasti for helping with the new release. By the way, welcome to the team! diff --git a/docs/relnotes/v0.11.6.md b/docs/relnotes/v0.11.6.md new file mode 100644 index 0000000000..71897d8acb --- /dev/null +++ b/docs/relnotes/v0.11.6.md @@ -0,0 +1,42 @@ +# v0.11.6 Release notes + +> **Draft** + +## Stack updates + +1. Update `rubygems` from 4.0.19 to 4.0.20 by @biow0lf in [errbit/errbit#3138](https://github.com/errbit/errbit/pull/3138) +2. Update `bundler` from 4.0.19 to 4.0.20 by @biow0lf in [errbit/errbit#3138](https://github.com/errbit/errbit/pull/3138) +3. Update Airbrake from 4.3.8 to 13.0.5 and use Airbrake v3 JSON notice compatibility coverage by @mikelolasagasti in [errbit/errbit#3076](https://github.com/errbit/errbit/pull/3076) +4. Update `rubygems` from 4.0.20 to 4.0.21 by @biow0lf in [errbit/errbit#3182](https://github.com/errbit/errbit/pull/3182) +5. Update `bundler` from 4.0.20 to 4.0.21 by @biow0lf in [errbit/errbit#3182](https://github.com/errbit/errbit/pull/3182) +6. Update Ruby to 4.0.7 by @biow0lf and dependabot in [errbit/errbit#3178](https://github.com/errbit/errbit/pull/3178) +7. Update Ruby on Rails to 8.1.4 by @biow0lf in [errbit/errbit#3209](https://github.com/errbit/errbit/pull/3209) +8. Update `rubygems` from 4.0.21 to 4.0.22 by @biow0lf in [errbit/errbit#3219](https://github.com/errbit/errbit/pull/3219) +9. Update `bundler` from 4.0.21 to 4.0.22 by @biow0lf in [errbit/errbit#3219](https://github.com/errbit/errbit/pull/3219) + +## New features + +1. Replace legacy `hoptoad_notifier` self-reporting with `Errbit::SelfErrorReporter`, preserving self-error application settings and routing local Airbrake v3 notices through Errbit's existing pipeline by @mikelolasagasti in [errbit/errbit#3078](https://github.com/errbit/errbit/pull/3078) +2. Add semantic autocomplete attributes to authentication and password-reset forms to improve browser autofill and password-manager support by @mikelolasagasti in [errbit/errbit#3110](https://github.com/errbit/errbit/pull/3110) +3. Drop the stray quote after the request url in issue reports by @bmwiedemann [errbit/errbit#3123](https://github.com/errbit/errbit/pull/3123) +4. Report Errbit version for self-reported errors by @mikelolasagasti in [errbit/errbit#3147](https://github.com/errbit/errbit/pull/3147) +5. Use `Time.current` for Atom feed timestamps by @mikelolasagasti in [errbit/errbit#3126](https://github.com/errbit/errbit/pull/3126) +6. Add MongoDB 9.0 support by @biow0lf in [errbit/errbit#3210](https://github.com/errbit/errbit/pull/3210) + +## Bug fixes + +1. Fix bottom button after search by @bmwiedemann in [errbit/errbit#3159](https://github.com/errbit/errbit/pull/3159) + +## Security + +1. Enable stylesheets integrity by @biow0lf in [errbit/errbit#3174](https://github.com/errbit/errbit/pull/3174) +2. Integrate Zizmor as GitHub Action by @biow0lf in [errbit/errbit#3190](https://github.com/errbit/errbit/pull/3190) +3. Dependabot: enable 7 days cooldown by @biow0lf in [errbit/errbit#3190](https://github.com/errbit/errbit/pull/3190) +4. Add `yarn.lock` by @biow0lf in [errbit/errbit#3188](https://github.com/errbit/errbit/pull/3188) +5. Lock rubygems and bundler for GitHub Actions by @biow0lf in [errbit/errbit#3191](https://github.com/errbit/errbit/pull/3191) +6. Update `css_parser` gem to 3.2.0. Fix ReDoS. [GHSA-84w7-hpvm-rxx2](https://github.com/premailer/css_parser/security/advisories/GHSA-84w7-hpvm-rxx2). By @biow0lf and @dependabot in [errbit/errbit#3200](https://github.com/errbit/errbit/pull/3200) + +## Thanks + +1. @mikelolasagasti +2. @bmwiedemann diff --git a/docs/upgrading.md b/docs/upgrading.md index 040b0e957d..2a00913fd7 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -1,25 +1,59 @@ -### Upgrading errbit beyond v0.4.0 +# Upgrading Errbit + +## Upgrading Errbit from v0.11.0 to v0.11.1 + +* Add support for MongoDB 8.3. + +## Upgrading Errbit from v0.10.0 to v0.11.0 + +* Add support for MongoDB 8.2. +* Support for MongoDB 6.0 has been removed. +* Deprecated support for MongoDB 4.0, 4.2, 4.4 and 5.0 has been removed. + +## Upgrading Errbit from v0.9.0 to v0.10.0 + +* Remove `USER_GEMFILE` env support. From now, only `UserGemfile` is + supported. +* Remove `SERVE_STATIC_ASSETS` env support. Just remove it. +* Remove `RACK_ENV` env support. Use `RAILS_ENV` env for this. Official + docker image set `RAILS_ENV` to `production`. +* Remove `ERRBIT_ENFORCE_SSL` env support. From now, Errbit always + should be behind reverse proxy with HTTPS support (ACME). Like, e.g., + Traefik. +* Remove `ERRBIT_PROTOCOL` env support. +* Remove `ERRBIT_PORT` env support. +* MongoDB 4.0 reached EOL on 21 Jun 2018. 4.0 support will be removed after v0.10.0 release. +* MongoDB 4.2 reached EOL on 09 Aug 2019. 4.2 support will be removed after v0.10.0 release. +* MongoDB 4.4 reached EOL on 25 Jul 2020. 4.4 support will be removed after v0.10.0 release. +* MongoDB 5.0 reached EOL on 08 Jul 2021. 5.0 support will be removed after v0.10.0 release. + +## Upgrading Errbit beyond v0.8.0 + +* Note: There are no migrations to run and the rake task for running migrations + has been removed. + +## Upgrading Errbit from v0.4.0 up to v0.8.0 * You must have already run migrations at least up to v0.3.0. Check to make sure you're schema version is at least 20131011155638 by running rake db:version before you upgrade beyond v0.4.0 * Notice fingerprinting has changed and is now easy to configure. But this means you'll have to regenerate fingerprints on old notices in order to for - them to be properly grouped with new notices. To do this run: `rake - errbit:notice_refingerprint`. If you were using a custom fingerprinter class - in a previous version, be aware that it will no longer have any effect. + them to be properly grouped with new notices. To do this run: `bundle exec + rake errbit:notice_refingerprint`. If you were using a custom fingerprinter + class in a previous version, be aware that it will no longer have any effect. Fingerprinting is now configurable within the Errbit UI. * Prior to v0.4.0, users were only able to see apps they were watching. All users can now see all apps and they can watch or unwatch any app. If you were using the watch feature to hide secret apps, you should not upgrade beyond v0.4.0. -### Upgrading errbit from v0.3.0 to v0.4.0 +## Upgrading Errbit from v0.3.0 to v0.4.0 * All configuration is now done through the environment. See [configuration](docs/configuration.md) -* Ruby 1.9 and 2.0 are no longer offically supported. Please upgrade to Ruby +* Ruby 1.9 and 2.0 are no longer officially supported. Please upgrade to Ruby 2.1+ -* Errbit now maintains an issue tracker only for github. If you're using +* Errbit now maintains an issue tracker only for GitHub. If you're using another issue tracker integration, you may need to maintain it yourself. See [Issue Trackers](#issue-trackers) diff --git a/lib/airbrake_api/params_error.rb b/lib/airbrake_api/params_error.rb deleted file mode 100644 index d0e276b139..0000000000 --- a/lib/airbrake_api/params_error.rb +++ /dev/null @@ -1,3 +0,0 @@ -module AirbrakeApi - class ParamsError < StandardError; end -end diff --git a/lib/airbrake_api/v3/notice_parser.rb b/lib/airbrake_api/v3/notice_parser.rb deleted file mode 100644 index bb316c2a0a..0000000000 --- a/lib/airbrake_api/v3/notice_parser.rb +++ /dev/null @@ -1,98 +0,0 @@ -module AirbrakeApi - module V3 - class NoticeParser - class ParamsError < StandardError; end - - attr_reader :params, :error - - def initialize(params) - @params = params || {} - end - - def attributes - { - error_class: error['type'], - message: error['message'], - backtrace: backtrace, - request: request, - server_environment: server_environment, - api_key: params['key'].present? ? params['key'] : params['project_id'], - notifier: context['notifier'] || params['notifier'], - user_attributes: user_attributes - } - end - - def report - ErrorReport.new(attributes) - end - - private - - def error - fail AirbrakeApi::ParamsError unless params.key?('errors') && params['errors'].any? - @error ||= params['errors'].first - end - - def backtrace - (error['backtrace'] || []).map do |backtrace_line| - { - method: backtrace_line['function'], - file: backtrace_line['file'], - number: backtrace_line['line'], - column: backtrace_line['column'] - } - end - end - - def server_environment - { - 'environment-name' => context['environment'], - 'hostname' => hostname, - 'project-root' => context['rootDirectory'], - 'app-version' => context['version'] - } - end - - def request - environment = (params['environment'] || {}).merge!( - 'HTTP_USER_AGENT' => context['userAgent'] - ) - - { - 'cgi-data' => environment, - 'session' => params['session'], - 'params' => params['params'], - 'url' => url, - 'component' => context['component'], - 'action' => context['action'] - } - end - - def user_attributes - user = context['user'] - return user.is_a?(Hash) ? user : { user: user } if user - - { - 'id' => context['userId'], - 'name' => context['userName'], - 'email' => context['userEmail'], - 'username' => context['userUsername'] - }.compact - end - - def url - context['url'] - end - - def hostname - context['hostname'] || URI.parse(url).hostname - rescue URI::InvalidURIError - '' - end - - def context - @context = params['context'] || {} - end - end - end -end diff --git a/lib/errbit/self_error_reporter.rb b/lib/errbit/self_error_reporter.rb new file mode 100644 index 0000000000..68065c59c6 --- /dev/null +++ b/lib/errbit/self_error_reporter.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +require "airbrake" +require "airbrake/rack/middleware" + +module Errbit + module SelfErrorReporter + SELF_APP_NAME = "Self.Errbit" + # Preserve the previous notifier's public-environment behavior. + NON_PUBLIC_ENVIRONMENTS = ["development", "test"].freeze + + class Middleware < Airbrake::Rack::Middleware + private + + def notify_airbrake(exception) + SelfErrorReporter.notify( + exception, + request: Airbrake::Rack::RequestStore[:request] + ) + end + end + + REQUEST_FILTERS = [ + Airbrake::Rack::RouteFilter.new, + Airbrake::Rack::ContextFilter.new, + Airbrake::Rack::HttpHeadersFilter.new, + Airbrake::Rack::SessionFilter.new + ].freeze + + module_function + + def notify(exception, request: nil) + return unless public_environment? + return if ignored_exception?(exception) + + app = self_app + return unless app + + notice = build_notice(exception, request) + return unless notice + + payload = notice_payload(notice, app) + + AirbrakeApi::V3::NoticeParser.new(payload).report.generate_notice! + rescue => e + Rails.logger.error("Errbit::SelfErrorReporter failed: #{e.class} - #{e.message}") + end + + def ignored_exception?(exception) + exception.is_a?(Mongoid::Errors::DocumentNotFound) + end + + def public_environment? + NON_PUBLIC_ENVIRONMENTS.exclude?(Rails.env.to_s) + end + + def self_app + App.where(name: SELF_APP_NAME).first_or_create do |app| + app.github_repo = "errbit/errbit" + end + end + + def notice_payload(notice, app) + sanitize_notice(notice) + payload = JSON.parse(notice.to_json) + payload["environment"] = notice[:environment].merge(request_environment(notice)) + payload.merge("key" => app.api_key) + end + + def sanitize_notice(notice) + [:errors, :context, :environment, :session, :params].each do |section| + value = notice[section] + value.replace(scrub_values(value)) if value.respond_to?(:replace) + end + end + + def request_environment(notice) + request = notice.stash[:rack_request] + return {} unless request + + request.env.each_with_object({}) do |(key, value), environment| + environment[key] = value.nil? ? nil : scrub_values(value.to_s) + end + end + + def build_notice(exception, request) + notice = Airbrake.build_notice(exception) + return unless notice + + notice.stash[:rack_request] = request if request + refine_notice(notice) + notice + end + + def refine_notice(notice) + REQUEST_FILTERS.each do |filter| + filter.call(notice) + end + + request = notice.stash[:rack_request] + notice[:params].merge!(safe_request_params(request)) if request + end + + def safe_request_params(request) + form_params = request.request_parameters + query_params = Rack::Utils.parse_query(request.env["QUERY_STRING"].to_s) + scrub_values(form_params.merge(query_params)) + rescue ActionController::BadRequest, ActionDispatch::Http::Parameters::ParseError + scrub_values(query_params || {}) + end + + def scrub_values(value) + case value + when Hash + scrub_hash(value) + when Array + value.map { |item| scrub_values(item) } + when String + value.dup.force_encoding(Encoding::UTF_8).scrub + else + value + end + end + + def scrub_hash(value) + value.to_h.each_with_object({}) do |(key, item), scrubbed| + scrubbed[scrub_values(key)] = scrub_values(item) + end + end + end +end diff --git a/lib/errbit/version.rb b/lib/errbit/version.rb deleted file mode 100644 index 08cc1758ec..0000000000 --- a/lib/errbit/version.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Errbit - class Version - def full_version - [ - '0.9.0', - 'dev', - source_version - ].compact.join('-') - end - - def source_version - source_version = ENV['SOURCE_VERSION'] - source_version[0...8] if source_version.present? - end - end - - VERSION = Version.new.full_version -end diff --git a/lib/hoptoad.rb b/lib/hoptoad.rb deleted file mode 100644 index 83bb0207b4..0000000000 --- a/lib/hoptoad.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'hoptoad/v2' - -module Hoptoad - class ApiVersionError < StandardError - def initialize - super "Wrong API Version: Expecting 2.0, 2.1, 2.2, 2.3 or 2.4" - end - end - - class << self - def parse_xml!(xml) - parsed = ActiveSupport::XmlMini.backend.parse(xml)['notice'] || fail(ApiVersionError) - processor = get_version_processor(parsed['version']) - processor.process_notice(parsed) - end - - private - - def get_version_processor(version) - case version - when /2\.[01234]/ then Hoptoad::V2 - else; fail ApiVersionError - end - end - end -end diff --git a/lib/hoptoad/v2.rb b/lib/hoptoad/v2.rb deleted file mode 100644 index 51cc708e46..0000000000 --- a/lib/hoptoad/v2.rb +++ /dev/null @@ -1,75 +0,0 @@ -module Hoptoad - module V2 - def self.process_notice(parsed) - for_errbit_api( - normalize( - rekey(parsed))) - end - - private - - def self.rekey(node) - case node - when Hash - if node.key?('var') && node.key?('key') - { normalize_key(node['key']) => rekey(node['var']) } - elsif node.key?('var') - rekey(node['var']) - elsif node.key?('__content__') && node.key?('key') - { normalize_key(node['key']) => rekey(node['__content__']) } - elsif node.key?('__content__') - rekey(node['__content__']) - elsif node.key?('key') - { normalize_key(node['key']) => nil } - else - node.inject({}) do |rekeyed, (key, val)| - rekeyed.merge!(normalize_key(key) => rekey(val)) - end - end - when Array - if node.first.key?('key') - node.inject({}) do |rekeyed, keypair| - rekeyed.merge!(rekey(keypair)) - end - else - node.map { |n| rekey(n) } - end - else - node - end - end - - def self.normalize_key(key) - key.tr('.', '_') - end - - def self.normalize(notice) - error = notice['error'] - backtrace = error['backtrace'] - backtrace['line'] = [backtrace['line']] unless backtrace['line'].is_a?(Array) - - notice['request'] ||= {} - notice['request']['component'] = 'unknown' if notice['request']['component'].blank? - notice['request']['action'] = nil if notice['request']['action'].blank? - - notice - end - - def self.for_errbit_api(notice) - { - error_class: notice['error']['class'] || notice['error']['key'], - message: notice['error']['message'], - backtrace: notice['error']['backtrace']['line'], - - request: notice['request'], - server_environment: notice['server-environment'], - - api_key: notice['api-key'], - notifier: notice['notifier'], - # 'current-user' from airbrake, 'user-attributes' from airbrake_user_attributes gem - user_attributes: notice['current-user'] || notice['user-attributes'] || {}, - framework: notice['framework'] - } - end - end -end diff --git a/lib/overrides/hoptoad_notifier/hoptoad_notifier.rb b/lib/overrides/hoptoad_notifier/hoptoad_notifier.rb deleted file mode 100644 index 839d767bad..0000000000 --- a/lib/overrides/hoptoad_notifier/hoptoad_notifier.rb +++ /dev/null @@ -1,23 +0,0 @@ -# Override the 'hoptoad_notifier' gem's 'send_notice' method for internal errors. -# Find or create a 'Self.Errbit' app, and save the error internally -# unless errors should be sent to a different Errbit instance. - -HoptoadNotifier.module_eval do - class << self - private def send_notice(notice) - # Log the error internally if we are not in a development environment. - return unless configuration.public? - - app = App.find_or_initialize_by(name: "Self.Errbit") - app.github_repo = "errbit/errbit" - app.save! - notice.send("api_key=", app.api_key) - - # Create notice internally. - report = ErrorReport.new(notice.to_xml) - report.generate_notice! - - logger.info "Internal error was logged to 'Self.Errbit' app." - end - end -end diff --git a/lib/tasks/errbit/bootstrap.rake b/lib/tasks/errbit/bootstrap.rake index ffe8c7d7af..b7ed3b2c21 100644 --- a/lib/tasks/errbit/bootstrap.rake +++ b/lib/tasks/errbit/bootstrap.rake @@ -1,6 +1,6 @@ -require 'fileutils' +# frozen_string_literal: true namespace :errbit do desc "Seed and index the DB" - task bootstrap: %w(db:seed db:mongoid:create_indexes) + task bootstrap: ["db:seed", "db:mongoid:create_indexes"] end diff --git a/lib/tasks/errbit/database.rake b/lib/tasks/errbit/database.rake index be2b5609a4..04bcd6c425 100644 --- a/lib/tasks/errbit/database.rake +++ b/lib/tasks/errbit/database.rake @@ -1,3 +1,5 @@ +# frozen_string_literal: true + namespace :errbit do desc "Updates cached attributes on Problem" task problem_recache: :environment do @@ -6,13 +8,11 @@ namespace :errbit do desc "Delete resolved errors from the database. (Useful for limited heroku databases)" task clear_resolved: :environment do - require 'resolved_problem_clearer' puts "=== Cleared #{ResolvedProblemClearer.new.execute} resolved errors from the database." end desc "Delete old errors from the database. (Useful for limited heroku databases)" task clear_outdated: :environment do - require 'outdated_problem_clearer' if Errbit::Config.notice_deprecation_days.present? puts "=== Cleared #{OutdatedProblemClearer.new.execute} outdated errors from the database." else diff --git a/lib/tasks/errbit/demo.rake b/lib/tasks/errbit/demo.rake index fdd8fa1ecc..d7f033a50e 100644 --- a/lib/tasks/errbit/demo.rake +++ b/lib/tasks/errbit/demo.rake @@ -1,31 +1,33 @@ +# frozen_string_literal: true + namespace :errbit do desc "Add a demo app & errors to your database (for testing)" task demo: :environment do - require 'fabrication' + require "factory_bot_rails" - app = Fabricate(:app, name: "Demo App #{Time.zone.now.strftime('%N')}") + app = FactoryBot.create(:app, name: "Demo App #{Time.zone.now.strftime("%N")}") # Report a number of errors for the application app.problems.delete_all errors = [{ error_class: "ArgumentError", - message: "wrong number of arguments (3 for 0)" + message: "wrong number of arguments (3 for 0)" }, { error_class: "RuntimeError", - message: "Could not find Red October" + message: "Could not find Red October" }, { error_class: "TypeError", - message: "can't convert Symbol into Integer" + message: "can't convert Symbol into Integer" }, { error_class: "ActiveRecord::RecordNotFound", - message: "could not find a record with the id 5" + message: "could not find a record with the id 5" }, { error_class: "NameError", - message: "uninitialized constant Tag" + message: "uninitialized constant Tag" }, { error_class: "SyntaxError", - message: "unexpected tSTRING_BEG, expecting keyword_do or '{' or '('" + message: "unexpected tSTRING_BEG, expecting keyword_do or '{' or '('" }] RANDOM_METHODS = ActiveSupport.methods.shuffle[1..8] @@ -34,9 +36,9 @@ namespace :errbit do backtrace = [] 99.times do |t| backtrace << { - 'number' => t.hash % 1000, - 'file' => "/path/to/file.rb", - 'method' => RANDOM_METHODS.sample.to_s + "number" => t.hash % 1000, + "file" => "/path/to/file.rb", + "method" => RANDOM_METHODS.sample.to_s } end backtrace @@ -46,29 +48,32 @@ namespace :errbit do rand(34).times do ErrorReport.new( error_template.reverse_merge( - api_key: app.api_key, - error_class: "StandardError", - message: "Oops. Something went wrong!", - backtrace: random_backtrace, - request: { - 'component' => 'main', - 'action' => 'error', - 'url' => "http://example.com/post/#{[111, 222, 333].sample}" + api_key: app.api_key, + error_class: "StandardError", + message: "Oops. Something went wrong!", + backtrace: random_backtrace, + request: { + "component" => "main", + "action" => "error", + "url" => "http://example.com/post/#{[111, 222, 333].sample}" }, - server_environment: { 'environment-name' => Rails.env.to_s }, - notifier: { name: "seeds.rb" }, - app_user: { - id: "1234", + server_environment: {"environment-name" => Rails.env.to_s}, + notifier: {name: "seeds.rb"}, + app_user: { + id: "1234", username: "jsmith", - name: "John Smith", - url: "http://www.example.com/users/jsmith" + name: "John Smith", + url: "http://www.example.com/users/jsmith" } ) ).generate_notice! end end - Fabricate(:notice, err: Fabricate(:err, problem: Fabricate(:problem, app: app))) + problem = FactoryBot.create(:problem, app: app) + err = FactoryBot.create(:err, problem: problem) + FactoryBot.create(:notice, err: err) + puts "=== Created demo app: '#{app.name}', with example errors." end end diff --git a/lib/tasks/heroku/cron.rake b/lib/tasks/heroku/cron.rake index a6898edcd9..2249e46d73 100644 --- a/lib/tasks/heroku/cron.rake +++ b/lib/tasks/heroku/cron.rake @@ -1,3 +1,5 @@ +# frozen_string_literal: true + desc "This task is called by the Heroku cron add-on" task cron: :environment do Rake::Task["errbit:clear_resolved"].invoke diff --git a/app/assets/javascripts/.gitkeep b/log/.keep similarity index 100% rename from app/assets/javascripts/.gitkeep rename to log/.keep diff --git a/package.json b/package.json new file mode 100644 index 0000000000..1bd72260ce --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "errbit", + "private": true, + "version": "1.0.0", + "devDependencies": { + "prettier": ">= 3.9.9" + } +} diff --git a/public/400.html b/public/400.html new file mode 100644 index 0000000000..640de03397 --- /dev/null +++ b/public/400.html @@ -0,0 +1,135 @@ + + + + + + + The server cannot process the request due to a client error (400 Bad Request) + + + + + + + + + + + + + +
+
+ +
+
+

The server cannot process the request due to a client error. Please check the request and try again. If you're the application owner check the logs for more information.

+
+
+ + + + diff --git a/public/404.html b/public/404.html index 9a48320a5f..d7f0f14222 100644 --- a/public/404.html +++ b/public/404.html @@ -1,26 +1,135 @@ - - - - The page you were looking for doesn't exist (404) - - - - - -
-

The page you were looking for doesn't exist.

-

You may have mistyped the address or the page may have moved.

-
- + + + + + + + The page you were looking for doesn't exist (404 Not found) + + + + + + + + + + + + + +
+
+ +
+
+

The page you were looking for doesn't exist. You may have mistyped the address or the page may have moved. If you're the application owner check the logs for more information.

+
+
+ + + diff --git a/public/406-unsupported-browser.html b/public/406-unsupported-browser.html new file mode 100644 index 0000000000..43d2811e8c --- /dev/null +++ b/public/406-unsupported-browser.html @@ -0,0 +1,135 @@ + + + + + + + Your browser is not supported (406 Not Acceptable) + + + + + + + + + + + + + +
+
+ +
+
+

Your browser is not supported.
Please upgrade your browser to continue.

+
+
+ + + + diff --git a/public/422.html b/public/422.html index 83660ab187..f12fb4aa17 100644 --- a/public/422.html +++ b/public/422.html @@ -1,26 +1,135 @@ - - - - The change you wanted was rejected (422) - - - - - -
-

The change you wanted was rejected.

-

Maybe you tried to change something you didn't have access to.

-
- + + + + + + + The change you wanted was rejected (422 Unprocessable Entity) + + + + + + + + + + + + + +
+
+ +
+
+

The change you wanted was rejected. Maybe you tried to change something you didn't have access to. If you're the application owner check the logs for more information.

+
+
+ + + diff --git a/public/500.html b/public/500.html index b80307fc16..e4eb18a759 100644 --- a/public/500.html +++ b/public/500.html @@ -1,26 +1,135 @@ - - - - We're sorry, but something went wrong (500) - - - - - -
-

We're sorry, but something went wrong.

-

We've been notified about this issue and we'll take a look at it shortly.

-
- + + + + + + + We're sorry, but something went wrong (500 Internal Server Error) + + + + + + + + + + + + + +
+
+ +
+
+

We're sorry, but something went wrong.
If you're the application owner check the logs for more information.

+
+
+ + + diff --git a/app/assets/stylesheets/.gitkeep b/public/apple-touch-icon-precomposed.png similarity index 100% rename from app/assets/stylesheets/.gitkeep rename to public/apple-touch-icon-precomposed.png diff --git a/lib/tasks/.gitkeep b/public/apple-touch-icon.png similarity index 100% rename from lib/tasks/.gitkeep rename to public/apple-touch-icon.png diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000000..c4c9dbfbbd Binary files /dev/null and b/public/icon.png differ diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000000..04b34bf83f --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/robots.txt b/public/robots.txt index fe8ce0077d..18e117ba2c 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,3 +1,3 @@ -# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file User-Agent: * Disallow: / diff --git a/script/rspec_queue_mongoid.rb b/script/rspec_queue_mongoid.rb deleted file mode 100755 index 2a226a466c..0000000000 --- a/script/rspec_queue_mongoid.rb +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env ruby -require 'rubygems' -require 'test_queue' -require 'test_queue/runner/rspec' - -require 'bundler' -Bundler.setup(:default, :development, :test) - -class MongoidRspecRunner < TestQueue::Runner::RSpec - def after_fork(num) - super - Mongoid.override_database( - "#{Mongoid.client(:default).options[:database]}_#{num}" - ) - end -end - -MongoidRspecRunner.new.execute diff --git a/spec/acceptance/acceptance_helper.rb b/spec/acceptance/acceptance_helper.rb deleted file mode 100644 index 5ce356a9da..0000000000 --- a/spec/acceptance/acceptance_helper.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'phantomjs/poltergeist' -require 'capybara/rspec' -require 'capybara/poltergeist' - -Capybara.javascript_driver = :poltergeist - -OmniAuth.config.test_mode = true - -def mock_auth(user = "test_user", token = "abcdef") - OmniAuth.config.mock_auth[:github] = Hashie::Mash.new( - 'provider' => 'github', - 'uid' => '1763', - 'extra' => { - 'raw_info' => { - 'login' => user - } - }, - 'credentials' => { - 'token' => token - } - ) - - OmniAuth.config.mock_auth[:google_oauth2] = Hashie::Mash.new( - provider: 'google_oauth2', - uid: user, - info: { email: 'errbit@errbit.example.com', name: "Existing User" } - ) -end - -def log_in(user) - visit '/' - fill_in :user_email, with: user.email - fill_in :user_password, with: 'password' - click_on I18n.t('devise.sessions.new.sign_in') -end diff --git a/spec/acceptance/acceptance_helper.rb.disable b/spec/acceptance/acceptance_helper.rb.disable new file mode 100644 index 0000000000..30c63f4039 --- /dev/null +++ b/spec/acceptance/acceptance_helper.rb.disable @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# require "capybara/rspec" + +def log_in(user) + visit "/" + fill_in :user_email, with: user.email + fill_in :user_password, with: "password" + click_on I18n.t("devise.sessions.new.sign_in") +end diff --git a/spec/acceptance/app_regenerate_api_key_spec.rb b/spec/acceptance/app_regenerate_api_key_spec.rb deleted file mode 100644 index a2645cd7a7..0000000000 --- a/spec/acceptance/app_regenerate_api_key_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -require 'acceptance/acceptance_helper' - -feature "Regeneration api_Key" do - let(:app) { Fabricate(:app) } - let(:admin) { Fabricate(:admin) } - let(:user) do - Fabricate(:user_watcher, app: app).user - end - - before do - app && admin - end - - scenario "an admin change api_key" do - visit '/' - log_in admin - click_link app.name - click_link I18n.t('apps.show.edit') - expect do - click_link I18n.t('apps.fields.regenerate_api_key') - end.to change { - app.reload.api_key - } - click_link I18n.t('shared.navigation.apps') - click_link I18n.t('apps.index.new_app') - expect(page).to_not have_button I18n.t('apps.fields.regenerate_api_key') - end - - scenario "a user cannot access to edit page" do - visit '/' - log_in user - click_link app.name if page.current_url != app_url(app) - expect(page).to_not have_button I18n.t('apps.show.edit') - end -end - -feature "Create an application" do - let(:admin) { Fabricate(:admin) } - let(:user) do - Fabricate(:user_watcher, app: app).user - end - - before do - admin - end - - scenario "create an apps without issue tracker and edit it" do - visit '/' - log_in admin - click_on I18n.t('apps.index.new_app') - fill_in 'app_name', with: 'My new app' - click_on I18n.t('apps.new.add_app') - page.has_content?(I18n.t('controllers.apps.flash.create.success')) - expect(App.where(name: 'My new app').count).to eq 1 - expect(App.where(name: 'My new app 2').count).to eq 0 - - click_on I18n.t('shared.navigation.apps') - click_on 'My new app' - click_link I18n.t('apps.show.edit') - fill_in 'app_name', with: 'My new app 2' - click_on I18n.t('apps.edit.update') - page.has_content?(I18n.t('controllers.apps.flash.update.success')) - expect(App.where(name: 'My new app').count).to eq 0 - expect(App.where(name: 'My new app 2').count).to eq 1 - end - - scenario "create an apps with issue tracker and edit it", js: true do - visit '/' - log_in admin - click_on I18n.t('apps.index.new_app') - fill_in 'app_name', with: 'My new app' - find('.label_radio.github').click - - fill_in 'app_github_repo', with: 'foo/bar' - within ".github.tracker_params" do - fill_in 'app_issue_tracker_attributes_options_username', with: 'token' - fill_in 'app_issue_tracker_attributes_options_password', with: 'pass' - end - click_on I18n.t('apps.new.add_app') - expect(page.has_content?(I18n.t('controllers.apps.flash.create.success'))).to eql true - app = App.where(name: 'My new app').first - expect(app.issue_tracker.type_tracker).to eql 'github' - expect(app.issue_tracker.options['username']).to eql 'token' - expect(app.issue_tracker.options['password']).to eql 'pass' - - click_on I18n.t('shared.navigation.apps') - click_on 'My new app' - click_link I18n.t('apps.show.edit') - find('.issue_tracker .label_radio.none').click - click_on I18n.t('apps.edit.update') - expect(page.has_content?(I18n.t('controllers.apps.flash.update.success'))).to eql true - app = App.where(name: 'My new app').first - expect(app.issue_tracker.tracker).to be_a ErrbitPlugin::NoneIssueTracker - end -end diff --git a/spec/acceptance/app_regenerate_api_key_spec.rb.disable b/spec/acceptance/app_regenerate_api_key_spec.rb.disable new file mode 100644 index 0000000000..2031141bc1 --- /dev/null +++ b/spec/acceptance/app_regenerate_api_key_spec.rb.disable @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require "acceptance/acceptance_helper" + +RSpec.feature "Regeneration api_Key" do + let(:app) { create(:app) } + let(:admin) { create(:user, admin: true) } + let(:user) { create(:user_watcher, app: app).user } + + before do + app && admin + end + + scenario "an admin change api_key" do + visit "/" + log_in admin + click_link app.name + click_link I18n.t("apps.show.edit") + expect do + click_link I18n.t("apps.fields.regenerate_api_key") + end.to change { + app.reload.api_key + } + click_link I18n.t("shared.navigation.apps") + click_link I18n.t("apps.index.new_app") + expect(page).not_to have_button I18n.t("apps.fields.regenerate_api_key") + end + + scenario "a user cannot access to edit page" do + visit "/" + log_in user + click_link app.name if page.current_url != app_url(app) + expect(page).not_to have_button I18n.t("apps.show.edit") + end +end + +RSpec.feature "Create an application" do + let(:admin) { create(:user, admin: true) } + let(:user) { create(:user_watcher, app: app).user } + + before do + admin + end + + scenario "create an apps without issue tracker and edit it" do + visit "/" + log_in admin + click_on I18n.t("apps.index.new_app") + fill_in "app_name", with: "My new app" + click_on I18n.t("apps.new.add_app") + page.has_content?(I18n.t("controllers.apps.flash.create.success")) + expect(App.where(name: "My new app").count).to eq(1) + expect(App.where(name: "My new app 2").count).to eq(0) + + click_on I18n.t("shared.navigation.apps") + click_on "My new app" + click_link I18n.t("apps.show.edit") + fill_in "app_name", with: "My new app 2" + click_on I18n.t("apps.edit.update") + page.has_content?(I18n.t("controllers.apps.flash.update.success")) + expect(App.where(name: "My new app").count).to eq(0) + expect(App.where(name: "My new app 2").count).to eq(1) + end + + scenario "create an apps with issue tracker and edit it", js: true do + visit "/" + log_in admin + click_on I18n.t("apps.index.new_app") + fill_in "app_name", with: "My new app" + find(".label_radio.github").click + + fill_in "app_github_repo", with: "foo/bar" + within ".github.tracker_params" do + fill_in "app_issue_tracker_attributes_options_username", with: "token" + fill_in "app_issue_tracker_attributes_options_password", with: "pass" + end + click_on I18n.t("apps.new.add_app") + expect(page.has_content?(I18n.t("controllers.apps.flash.create.success"))).to eq(true) + app = App.where(name: "My new app").first + expect(app.issue_tracker.type_tracker).to eq("github") + expect(app.issue_tracker.options["username"]).to eq("token") + expect(app.issue_tracker.options["password"]).to eq("pass") + + click_on I18n.t("shared.navigation.apps") + click_on "My new app" + click_link I18n.t("apps.show.edit") + find(".issue_tracker .label_radio.none").click + click_on I18n.t("apps.edit.update") + expect(page.has_content?(I18n.t("controllers.apps.flash.update.success"))).to eq(true) + app = App.where(name: "My new app").first + expect(app.issue_tracker.tracker).to be_a(ErrbitPlugin::NoneIssueTracker) + end +end diff --git a/spec/acceptance/reset_password_token.rb b/spec/acceptance/reset_password_token.rb deleted file mode 100644 index 930515660d..0000000000 --- a/spec/acceptance/reset_password_token.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'acceptance/acceptance_helper' - -feature 'password reset token' do - let(:user) { Fabricate :user } - - scenario 'receives correct password reset token' do - host = ActionMailer::Base.default_url_options.values_at(:host).first - port = ActionMailer::Base.default_url_options.values_at(:port).first - port = port.blank? ? '' : ':' + port - regex = %r{http://#{host}#{port}/users/password/edit\?reset_password_token=([A-Za-z0-9\-_]+)} - - visit 'https://brighterr.herokuapp.com/users/password/new' - fill_in 'Email', with: user.email - click_button 'Send me reset password instructions' - expect(page).to have_content I18n.t('devise.passwords.send_instructions') - - mail = ActionMailer::Base.deliveries.last - expect(mail.subject).to match(/Reset password instructions/) - expect(mail.body.encoded).to_not be_empty - expect(mail.body.encoded).to match(/change your password/) - expect(mail.body.encoded).to match(regex) - if mail.body.encoded =~ regex - visit "/users/password/edit?reset_password_token=#{Regexp.last_match(1)}" - expect(page).to have_content 'Change your password' - fill_in 'New password', with: 'test12345' - fill_in 'Type your new password again', with: 'test12345' - click_button 'Change my password' - expect(page).to_not have_content 'Reset password token is invalid' - end - end -end diff --git a/spec/acceptance/reset_password_token_spec.rb.disable b/spec/acceptance/reset_password_token_spec.rb.disable new file mode 100644 index 0000000000..384c7a5e24 --- /dev/null +++ b/spec/acceptance/reset_password_token_spec.rb.disable @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "acceptance/acceptance_helper" + +RSpec.feature "password reset token" do + let(:user) { create(:user) } + + scenario "receives correct password reset token" do + host = ActionMailer::Base.default_url_options.values_at(:host).first + port = ActionMailer::Base.default_url_options.values_at(:port).first + port = port.blank? ? "" : ":" + port + regex = %r{http://#{host}#{port}/users/password/edit\?reset_password_token=([A-Za-z0-9\-_]+)} + + visit "https://brighterr.herokuapp.com/users/password/new" + fill_in "Email", with: user.email + click_button "Send me reset password instructions" + expect(page).to have_content I18n.t("devise.passwords.send_instructions") + + mail = ActionMailer::Base.deliveries.last + expect(mail.subject).to match(/Reset password instructions/) + expect(mail.body.encoded).not_to be_empty + expect(mail.body.encoded).to match(/change your password/) + expect(mail.body.encoded).to match(regex) + if mail.body.encoded =~ regex + visit "/users/password/edit?reset_password_token=#{Regexp.last_match(1)}" + expect(page).to have_content "Change your password" + fill_in "New password", with: "test12345" + fill_in "Type your new password again", with: "test12345" + click_button "Change my password" + expect(page).not_to have_content "Reset password token is invalid" + end + end +end diff --git a/spec/acceptance/sign_in_with_github_spec.rb b/spec/acceptance/sign_in_with_github_spec.rb deleted file mode 100644 index 5ba849b078..0000000000 --- a/spec/acceptance/sign_in_with_github_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'acceptance/acceptance_helper' - -feature 'Sign in with GitHub' do - background do - allow(Errbit::Config).to receive(:github_authentication).and_return(true) - Fabricate(:user, github_login: 'nashby') - end - - scenario 'log in via GitHub with recognized user' do - mock_auth('nashby') - - visit '/' - click_link 'Sign in with GitHub' - expect(page).to have_content I18n.t("devise.omniauth_callbacks.success", kind: 'GitHub') - end - - scenario 'reject unrecognized user if authenticating via GitHub' do - mock_auth('unknown_user') - - visit '/' - click_link 'Sign in with GitHub' - expect(page).to have_content 'There are no authorized users with GitHub login' - end -end diff --git a/spec/acceptance/sign_in_with_google_spec.rb b/spec/acceptance/sign_in_with_google_spec.rb deleted file mode 100644 index 0355f42796..0000000000 --- a/spec/acceptance/sign_in_with_google_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'acceptance/acceptance_helper' - -feature 'Sign in with Google' do - background do - allow(Errbit::Config).to receive(:google_authentication).and_return(true) - Fabricate(:user, google_uid: 'nashby') - visit root_path - end - - scenario 'log in via Google with recognized user' do - mock_auth('nashby') - - click_link 'Sign in with Google' - expect(page).to have_content I18n.t('devise.omniauth_callbacks.success', kind: 'Google') - end - - scenario 'reject unrecognized user if authenticating via Google' do - mock_auth('unknown_user') - - click_link 'Sign in with Google' - expect(page).to have_content 'There are no authorized users with Google login' - end -end - -feature 'Sign in with Google with auto provision' do - background do - allow(Errbit::Config).to receive(:google_authentication).and_return(true) - allow(Errbit::Config).to receive(:google_auto_provision).and_return(true) - Fabricate(:user, google_uid: 'nashby') - visit root_path - end - - scenario 'create an account for recognized user if they log in' do - mock_auth('unknown_but_valid_user') - - click_link 'Sign in with Google' - expect(page).to have_content I18n.t('devise.omniauth_callbacks.success', kind: 'Google') - end -end - -feature 'Sign in with Google with domain validation' do - background do - allow(Errbit::Config).to receive(:google_authentication).and_return(true) - allow(Errbit::Config).to receive(:google_auto_provision).and_return(true) - allow(Errbit::Config).to receive(:google_authorized_domains).and_return("errbit.example.com") - Fabricate(:user, google_uid: 'nashby') - visit root_path - end - - scenario 'create an account for recognized user if their account email is from a trusted domain' do - mock_auth('unknown_but_valid_user') - - click_link 'Sign in with Google' - expect(page).to have_content I18n.t('devise.omniauth_callbacks.success', kind: 'Google') - end - - scenario "don't create an account for user if their account email is from an unauthorized domain" do - allow(Errbit::Config).to receive(:google_authorized_domains).and_return("example.com") - mock_auth('unknown_but_invalid_user') - - click_link 'Sign in with Google' - expect(page).to have_text I18n.t('devise.google_login.domain_unauthorized') - end -end diff --git a/spec/acceptance/watch_unwatch_app_spec.rb b/spec/acceptance/watch_unwatch_app_spec.rb deleted file mode 100644 index cc06b5a2e9..0000000000 --- a/spec/acceptance/watch_unwatch_app_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'acceptance/acceptance_helper' - -feature 'A user can watch and unwatch an application' do - let!(:app) { Fabricate(:app) } - let!(:user) { Fabricate(:user) } - - scenario 'log in and unwatch a project' do - app.watchers.create!(user_id: user.id) - user.reload - - log_in user - click_on app.name - click_on I18n.t('apps.show.unwatch') - expect(page).to have_content( - I18n.t('watchers.destroy.success', app: app.name)) - end - - scenario 'log in and watch a project' do - log_in user - click_on app.name - click_on I18n.t('apps.show.watch') - expect(page).to have_content( - I18n.t('watchers.update.success', app: app.name)) - end -end diff --git a/spec/controllers/api/v1/comments_controller_spec.rb b/spec/controllers/api/v1/comments_controller_spec.rb new file mode 100644 index 0000000000..a7b3663b22 --- /dev/null +++ b/spec/controllers/api/v1/comments_controller_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Api::V1::CommentsController, type: :controller do + context "when logged in" do + before do + @user = create(:user) + end + + describe "GET /api/v1/problems/:problem_id/comments" do + before do + @problem = create(:problem) + create(:comment, err: @problem) + create(:comment, err: @problem) + create(:comment) + end + + it "should return JSON if JSON is requested" do + get :index, params: {problem_id: @problem.id, auth_token: @user.authentication_token, format: "json"} + + expect { response.parsed_body }.not_to raise_error + end + + it "should return XML if XML is requested" do + get :index, params: {problem_id: @problem.id, auth_token: @user.authentication_token, format: "xml"} + + expect(Nokogiri::XML(response.body).errors).to be_empty + end + + it "should return JSON by default" do + get :index, params: {problem_id: @problem.id, auth_token: @user.authentication_token} + + expect { response.parsed_body }.not_to raise_error + end + + it "should return all comments of a problem" do + get :index, params: {problem_id: @problem.id, auth_token: @user.authentication_token} + + expect(response).to be_successful + + comments = JSON.parse(response.body) + + expect(comments.length).to eq(2) + end + end + + describe "POST /api/v1/problems/:problem_id/comments" do + before do + @problem = create(:problem) + end + + context "with valid params" do + it "should create comment" do + expect do + post :create, params: {problem_id: @problem.id, auth_token: @user.authentication_token, comment: {body: "I'll take a look at it."}} + end.to change(Comment, :count) + + expect(response).to be_successful + end + end + + context "with invalid params" do + it "shouldn't create comment" do + expect do + post :create, params: {problem_id: @problem.id, auth_token: @user.authentication_token, comment: {body: nil}} + end.not_to change(Comment, :count) + + expect(response).not_to be_successful + + errors = JSON.parse(response.body) + + expect(errors).to eq("errors" => ["Body can't be blank"]) + end + end + end + end +end diff --git a/spec/controllers/api/v1/notices_controller_spec.rb b/spec/controllers/api/v1/notices_controller_spec.rb index fee8fe323c..7860edaa19 100644 --- a/spec/controllers/api/v1/notices_controller_spec.rb +++ b/spec/controllers/api/v1/notices_controller_spec.rb @@ -1,46 +1,69 @@ -describe Api::V1::NoticesController, type: 'controller' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Api::V1::NoticesController, type: :controller do context "when logged in" do before do - @user = Fabricate(:user) + @user = create(:user) end describe "GET /api/v1/notices" do before do - Fabricate(:notice, created_at: Time.zone.parse('2012-08-01')) - Fabricate(:notice, created_at: Time.zone.parse('2012-08-01')) - Fabricate(:notice, created_at: Time.zone.parse('2012-08-21')) - Fabricate(:notice, created_at: Time.zone.parse('2012-08-30')) + create(:notice, created_at: Time.zone.parse("2012-08-01")) + create(:notice, created_at: Time.zone.parse("2012-08-01")) + create(:notice, created_at: Time.zone.parse("2012-08-21")) + create(:notice, created_at: Time.zone.parse("2012-08-30")) end it "should return JSON if JSON is requested" do - get :index, auth_token: @user.authentication_token, format: "json" - expect { JSON.load(response.body) }.not_to raise_error # JSON::ParserError) + get :index, params: {auth_token: @user.authentication_token, format: "json"} + + expect { JSON.parse(response.body) }.not_to raise_error end it "should return XML if XML is requested" do - get :index, auth_token: @user.authentication_token, format: "xml" + get :index, params: {auth_token: @user.authentication_token, format: "xml"} + expect(Nokogiri::XML(response.body).errors).to be_empty end it "should return JSON by default" do - get :index, auth_token: @user.authentication_token - expect { JSON.load(response.body) }.not_to raise_error # JSON::ParserError) + get :index, params: {auth_token: @user.authentication_token} + + expect { JSON.parse(response.body) }.not_to raise_error end describe "given a date range" do it "should return only the notices created during the date range" do - get :index, auth_token: @user.authentication_token, start_date: "2012-08-01", end_date: "2012-08-27" - expect(response).to be_success - notices = JSON.load response.body - expect(notices.length).to eq 3 + get :index, params: {auth_token: @user.authentication_token, start_date: "2012-08-01", end_date: "2012-08-27"} + + expect(response).to be_successful + + notices = JSON.parse(response.body) + + expect(notices.length).to eq(3) end end it "should return all notices" do - get :index, auth_token: @user.authentication_token - expect(response).to be_success - notices = JSON.load response.body - expect(notices.length).to eq 4 + get :index, params: {auth_token: @user.authentication_token} + + expect(response).to be_successful + + notices = JSON.parse(response.body) + + expect(notices.length).to eq(4) + end + + it "should return notice objects with correct fields" do + get :index, params: {auth_token: @user.authentication_token, format: "json"} + + notices = JSON.parse(response.body) + notice = notices.first + + expect(notice).to be_a(Hash) + expect(notice.keys).to match_array(["_id", "created_at", "message", "error_class"]) end end end diff --git a/spec/controllers/api/v1/problems_controller_spec.rb b/spec/controllers/api/v1/problems_controller_spec.rb index e14bb816d1..b1ebe6fdb5 100644 --- a/spec/controllers/api/v1/problems_controller_spec.rb +++ b/spec/controllers/api/v1/problems_controller_spec.rb @@ -1,99 +1,133 @@ -describe Api::V1::ProblemsController, type: 'controller' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Api::V1::ProblemsController, type: :controller do context "when logged in" do before do - @user = Fabricate(:user) + @user = create(:user) end describe "GET /api/v1/problems/:id" do before do - notice = Fabricate(:notice) - err = Fabricate(:err, notices: [notice]) - @problem = Fabricate(:problem, errs: [err]) + notice = create(:notice) + err = create(:err, notices: [notice]) + @problem = create(:problem, errs: [err]) end it "should return JSON if JSON is requested" do - get :show, auth_token: @user.authentication_token, format: "json", id: Problem.first.id - expect { JSON.load(response.body) }.not_to raise_error # JSON::ParserError + get :show, params: {auth_token: @user.authentication_token, format: "json", id: Problem.first.id} + + expect { JSON.parse(response.body) }.not_to raise_error end it "should return XML if XML is requested" do - get :index, auth_token: @user.authentication_token, format: "xml", id: @problem.id + get :index, params: {auth_token: @user.authentication_token, format: "xml", id: @problem.id} + expect(Nokogiri::XML(response.body).errors).to be_empty end it "should return JSON by default" do - get :show, auth_token: @user.authentication_token, id: @problem.id - expect { JSON.load(response.body) }.not_to raise_error # JSON::ParserError) + get :show, params: {auth_token: @user.authentication_token, id: @problem.id} + + expect { JSON.parse(response.body) }.not_to raise_error end it "should return the correct problem" do - get :show, auth_token: @user.authentication_token, format: "json", id: @problem.id + get :show, params: {auth_token: @user.authentication_token, format: "json", id: @problem.id} returned_problem = JSON.parse(response.body) + expect(returned_problem["_id"]).to eq(@problem.id.to_s) end it "should return only the correct fields" do - get :show, auth_token: @user.authentication_token, format: "json", id: @problem.id + get :show, params: {auth_token: @user.authentication_token, format: "json", id: @problem.id} + returned_problem = JSON.parse(response.body) - expect(returned_problem.keys).to match_array(%w( - app_name - first_notice_at - message - app_id - last_notice_at - _id - resolved - resolved_at - where - notices_count - environment)) + expect(returned_problem.keys).to match_array([ + "app_name", + "first_notice_at", + "message", + "app_id", + "last_notice_at", + "_id", + "resolved", + "resolved_at", + "where", + "notices_count", + "environment" + ]) end it "returns a 404 if the problem cannot be found" do - get :show, auth_token: @user.authentication_token, format: "json", id: 'IdontExist' - expect(response.status).to eq(404) + get :show, params: {auth_token: @user.authentication_token, format: "json", id: "IdontExist"} + + expect(response).to have_http_status(:not_found) end end describe "GET /api/v1/problems" do before do - Fabricate(:problem, first_notice_at: Date.new(2012, 8, 01), resolved_at: Date.new(2012, 8, 02)) - Fabricate(:problem, first_notice_at: Date.new(2012, 8, 01), resolved_at: Date.new(2012, 8, 21)) - Fabricate(:problem, first_notice_at: Date.new(2012, 8, 21)) - Fabricate(:problem, first_notice_at: Date.new(2012, 8, 30)) + create(:problem, first_notice_at: Date.new(2012, 8, 1), resolved_at: Date.new(2012, 8, 2)) + create(:problem, first_notice_at: Date.new(2012, 8, 1), resolved_at: Date.new(2012, 8, 21)) + create(:problem, first_notice_at: Date.new(2012, 8, 21)) + create(:problem, first_notice_at: Date.new(2012, 8, 30)) end it "should return JSON if JSON is requested" do - get :index, auth_token: @user.authentication_token, format: "json" - expect { JSON.load(response.body) }.not_to raise_error # JSON::ParserError) + get :index, params: {auth_token: @user.authentication_token, format: "json"} + + expect { JSON.parse(response.body) }.not_to raise_error end it "should return XML if XML is requested" do - get :index, auth_token: @user.authentication_token, format: "xml" + get :index, params: {auth_token: @user.authentication_token, format: "xml"} + expect(Nokogiri::XML(response.body).errors).to be_empty end it "should return JSON by default" do - get :index, auth_token: @user.authentication_token - expect { JSON.load(response.body) }.not_to raise_error # JSON::ParserError) + get :index, params: {auth_token: @user.authentication_token} + + expect { JSON.parse(response.body) }.not_to raise_error end describe "given a date range" do it "should return only the problems open during the date range" do - get :index, auth_token: @user.authentication_token, start_date: "2012-08-20", end_date: "2012-08-27" - expect(response).to be_success - problems = JSON.load response.body - expect(problems.length).to eq 2 + get :index, params: {auth_token: @user.authentication_token, start_date: "2012-08-20", end_date: "2012-08-27"} + + expect(response).to be_successful + + problems = JSON.parse(response.body) + + expect(problems.length).to eq(2) end end it "should return all problems" do - get :index, auth_token: @user.authentication_token - expect(response).to be_success - problems = JSON.load response.body - expect(problems.length).to eq 4 + get :index, params: {auth_token: @user.authentication_token} + + expect(response).to be_successful + + problems = JSON.parse(response.body) + + expect(problems.length).to eq(4) + end + + it "should return problem objects with correct fields" do + get :index, params: {auth_token: @user.authentication_token, format: "json"} + + problems = JSON.parse(response.body) + problem = problems.first + + expect(problem).to be_a(Hash) + expect(problem.keys).to match_array([ + "_id", "app_id", "app_name", "environment", "message", "where", + "first_notice_at", "last_notice_at", "resolved", "resolved_at", + "notices_count" + ]) end end end diff --git a/spec/controllers/api/v1/stats_controller_spec.rb b/spec/controllers/api/v1/stats_controller_spec.rb new file mode 100644 index 0000000000..cd708eaa73 --- /dev/null +++ b/spec/controllers/api/v1/stats_controller_spec.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Api::V1::StatsController, type: :controller do +end diff --git a/spec/controllers/api/v3/notices_controller_spec.rb b/spec/controllers/api/v3/notices_controller_spec.rb index 6fae708497..54ef2849b4 100644 --- a/spec/controllers/api/v3/notices_controller_spec.rb +++ b/spec/controllers/api/v3/notices_controller_spec.rb @@ -1,81 +1,104 @@ -describe Api::V3::NoticesController, type: :controller do - let(:app) { Fabricate(:app) } +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Api::V3::NoticesController, type: :controller do + let(:app) { create(:app) } + let(:project_id) { app.api_key } - let(:legit_params) { { project_id: project_id, key: project_id } } + + let(:legit_params) { {project_id: project_id, key: project_id} } + let(:legit_body) do - Rails.root.join('spec', 'fixtures', 'api_v3_request.json').read + Rails.root.join("spec/fixtures/api_v3_request.json").read end - it 'sets CORS headers on POST request' do - post :create, project_id: 'invalid id' - expect(response.headers['Access-Control-Allow-Origin']).to eq('*') - expect(response.headers['Access-Control-Allow-Headers']).to eq('origin, content-type, accept') + it "sets CORS headers on POST request" do + post :create, params: {project_id: "invalid id"} + + expect(response.headers["Access-Control-Allow-Origin"]).to eq("*") + + expect(response.headers["Access-Control-Allow-Headers"]).to eq("origin, content-type, accept") end - it 'responds to an OPTIONS request' do - process :create, 'OPTIONS', project_id: 'nothingspecial' - expect(response.headers['Access-Control-Allow-Origin']).to eq('*') - expect(response.headers['Access-Control-Allow-Headers']).to eq('origin, content-type, accept') + it "responds to an OPTIONS request" do + process :create, method: "OPTIONS", params: {project_id: "nothingspecial"} + + expect(response.headers["Access-Control-Allow-Origin"]).to eq("*") + + expect(response.headers["Access-Control-Allow-Headers"]).to eq("origin, content-type, accept") end - it 'returns created notice id in json format' do - post :create, legit_body, legit_params + it "returns created notice id in json format" do + post :create, body: legit_body, params: {**legit_params} + notice = Notice.last - expect(JSON.parse(response.body)).to eq( - 'id' => notice.id.to_s, - 'url' => notice.problem.url + + expect(response.parsed_body).to eq( + "id" => notice.id.to_s, + "url" => notice.problem.url ) end - it 'responds with 201 created on success' do - post :create, legit_body, legit_params - expect(response.status).to be(201) + it "responds with 201 created on success" do + post :create, body: legit_body, params: {**legit_params} + + expect(response).to have_http_status(:created) end - it 'responds with 201 created on success with token in Airbrake Token header' do - request.headers['X-Airbrake-Token'] = project_id - post :create, legit_body, project_id: 123 - expect(response.status).to be(201) + it "responds with 201 created on success with token in Airbrake Token header" do + request.headers["X-Airbrake-Token"] = project_id + + post :create, body: legit_body, params: {project_id: 123} + + expect(response).to have_http_status(:created) end - it 'responds with 201 created on success with token in Authorization header' do - request.headers['Authorization'] = "Bearer #{project_id}" - post :create, legit_body, project_id: 123 - expect(response.status).to be(201) + it "responds with 201 created on success with token in Authorization header" do + request.headers["Authorization"] = "Bearer #{project_id}" + + post :create, body: legit_body, params: {project_id: 123} + + expect(response).to have_http_status(:created) end - it 'responds with 422 when Authorization header is not valid' do - request.headers['Authorization'] = "incorrect" - post :create, legit_body, project_id: 123 - expect(response.status).to be(422) + it "responds with 422 when Authorization header is not valid" do + request.headers["Authorization"] = "incorrect" + + post :create, body: legit_body, params: {project_id: 123} + + expect(response).to have_http_status(:unprocessable_content) end - it 'responds with 400 when request attributes are not valid' do - allow_any_instance_of(AirbrakeApi::V3::NoticeParser). - to receive(:report).and_raise(AirbrakeApi::ParamsError) - post :create, project_id: 'ID' - expect(response.status).to eq(400) - expect(response.body).to eq('Invalid request') + it "responds with 400 when request attributes are not valid" do + allow_any_instance_of(AirbrakeApi::V3::NoticeParser) + .to receive(:report).and_raise(AirbrakeApi::ParamsError) + + post :create, params: {project_id: "ID"} + + expect(response).to have_http_status(:bad_request) + + expect(response.body).to eq("Invalid request") end - it 'responds with 422 when notice comes from an old app' do - app.current_app_version = '1.1.0' + it "responds with 422 when notice comes from an old app" do + app.current_app_version = "1.1.0" app.save! - post :create, legit_body, legit_params - expect(response.status).to eq(422) + post :create, body: legit_body, params: {**legit_params} + expect(response).to have_http_status(:unprocessable_content) end - it 'responds with 422 when project_id is invalid' do - post :create, legit_body, project_id: 'hm?', key: 'wha?' + it "responds with 422 when project_id is invalid" do + post :create, body: legit_body, params: {project_id: "hm?", key: "wha?"} - expect(response.status).to eq(422) - expect(response.body).to eq('Your API key is unknown') + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to eq("Your API key is unknown") end - it 'ignores notices for older api' do - app = Fabricate(:app, current_app_version: '2.0') - post :create, legit_body, project_id: app.api_key, key: app.api_key - expect(response.body).to eq('Notice for old app version ignored') + it "ignores notices for older api" do + app = create(:app, current_app_version: "2.0") + post :create, body: legit_body, params: {project_id: app.api_key, key: app.api_key} + expect(response.body).to eq("Notice for old app version ignored") expect(Notice.count).to eq(0) end end diff --git a/spec/controllers/apps_controller_spec.rb b/spec/controllers/apps_controller_spec.rb index 338bb17a3c..3899813c3e 100644 --- a/spec/controllers/apps_controller_spec.rb +++ b/spec/controllers/apps_controller_spec.rb @@ -1,33 +1,37 @@ -describe AppsController, type: 'controller' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe AppsController, type: :controller do it_requires_authentication - it_requires_admin_privileges for: { new: :get, edit: :get, create: :post, update: :put, destroy: :delete } + it_requires_admin_privileges for: {new: :get, edit: :get, create: :post, update: :patch, destroy: :delete} - let(:app_params) { { name: 'BestApp' } } - let(:admin) { Fabricate(:admin) } - let(:user) { Fabricate(:user) } - let(:watcher) { Fabricate(:user_watcher, app: app, user: user) } - let(:unwatched_app) { Fabricate(:app) } + let(:app_params) { {name: "BestApp"} } + let(:admin) { create(:user, admin: true) } + let(:user) { create(:user) } + let(:watcher) { create(:user_watcher, app: app, user: user) } + let(:unwatched_app) { create(:app) } let(:app) { unwatched_app } - let(:watched_app1) do - a = Fabricate(:app) - Fabricate(:user_watcher, user: user, app: a) + let(:watched_app_1) do + a = create(:app) + create(:user_watcher, user: user, app: a) a end - let(:watched_app2) do - a = Fabricate(:app) - Fabricate(:user_watcher, user: user, app: a) + let(:watched_app_2) do + a = create(:app) + create(:user_watcher, user: user, app: a) a end let(:err) do - Fabricate(:err, problem: problem) + create(:err, problem: problem) end let(:notice) do - Fabricate(:notice, err: err) + create(:notice, err: err) end let(:problem) do - Fabricate(:problem, app: app) + create(:problem, app: app) end - let(:problem_resolved) { Fabricate(:problem_resolved, app: app) } + let(:problem_resolved) { create(:problem_resolved, app: app) } let(:notice_fingerprinter) do nf = SiteConfig.document.notice_fingerprinter nf.backtrace_lines = 10 @@ -35,184 +39,196 @@ end describe "GET /apps" do - context 'when logged in as an admin' do - it 'finds all apps' do + context "when logged in as an admin" do + it "finds all apps" do sign_in admin - unwatched_app && watched_app1 && watched_app2 + unwatched_app && watched_app_1 && watched_app_2 get :index - expect(controller.apps.entries).to eq App.all.to_a.sort.entries + expect(controller.apps.entries).to eq(App.all.to_a.sort.entries) end end - context 'when logged in as a regular user' do - it 'finds all apps' do + context "when logged in as a regular user" do + it "finds all apps" do sign_in user - unwatched_app && watched_app1 && watched_app2 + unwatched_app && watched_app_1 && watched_app_2 get :index - expect(controller.apps.entries).to eq App.all.to_a.sort.entries + expect(controller.apps.entries).to eq(App.all.to_a.sort.entries) end end end describe "GET /apps/:id" do - context 'logged in as an admin' do - before(:each) do + context "logged in as an admin" do + before do sign_in admin end - it 'finds the app' do - get :show, id: app.id - expect(controller.app).to eq app + it "finds the app" do + get :show, params: {id: app.id} + expect(controller.app).to eq(app) end it "should not raise errors for app with err without notices" do err - expect { get :show, id: app.id }.to_not raise_error + expect { get :show, params: {id: app.id} }.not_to raise_error end it "should list atom feed successfully" do - get :show, id: app.id, format: "atom" - expect(response).to be_success + get :show, params: {id: app.id, format: "atom"} + expect(response).to be_successful end it "should list available watchers by name" do - Fabricate(:user, name: "Carol") - Fabricate(:user, name: "Alice") - Fabricate(:user, name: "Betty") + create(:user, name: "Carol") + create(:user, name: "Alice") + create(:user, name: "Betty") - get :show, id: app.id + get :show, params: {id: app.id} expect(controller.users.to_a).to eq(User.all.to_a.sort_by(&:name)) end context "pagination" do - before(:each) do - 35.times { Fabricate(:err, problem: Fabricate(:problem, app: app)) } + before do + 35.times { create(:err, problem: create(:problem, app: app)) } end it "should have default per_page value for user" do - get :show, id: app.id - expect(controller.problems.to_a.size).to eq User::PER_PAGE + get :show, params: {id: app.id} + + expect(controller.problems.to_a.size).to eq(User::PER_PAGE) end it "should be able to override default per_page value" do admin.update_attribute :per_page, 10 - get :show, id: app.id - expect(controller.problems.to_a.size).to eq 10 + + get :show, params: {id: app.id} + + expect(controller.problems.to_a.size).to eq(10) end end - context 'with resolved errors' do - before(:each) do + context "with resolved errors" do + before do problem_resolved && problem end - context 'and no params' do - it 'shows only unresolved problems' do - get :show, id: app.id - expect(controller.problems.size).to eq 1 + context "and no params" do + it "shows only unresolved problems" do + get :show, params: {id: app.id} + expect(controller.problems.size).to eq(1) end end - context 'and all_problems=true params' do - it 'shows all errors' do - get :show, id: app.id, all_errs: true - expect(controller.problems.size).to eq 2 + context "and all_problems=true params" do + it "shows all errors" do + get :show, params: {id: app.id, all_errs: true} + expect(controller.problems.size).to eq(2) end end end - context 'with environment filters' do - before(:each) do - environments = %w(production test development staging) + context "with environment filters" do + before do + environments = ["production", "test", "development", "staging"] 20.times do |i| - Fabricate(:problem, app: app, environment: environments[i % environments.length]) + create(:problem, app: app, environment: environments[i % environments.length]) end end - context 'no params' do - it 'shows errs for all environments' do - get :show, id: app.id - expect(controller.problems.size).to eq 20 + context "no params" do + it "shows errs for all environments" do + get :show, params: {id: app.id} + + expect(controller.problems.size).to eq(20) end end - context 'environment production' do - it 'shows errs for just production' do - get :show, id: app.id, environment: 'production' - expect(controller.problems.size).to eq 5 + context "environment production" do + it "shows errs for just production" do + get :show, params: {id: app.id, environment: "production"} + + expect(controller.problems.size).to eq(5) end end - context 'environment staging' do - it 'shows errs for just staging' do - get :show, id: app.id, environment: 'staging' - expect(controller.problems.size).to eq 5 + context "environment staging" do + it "shows errs for just staging" do + get :show, params: {id: app.id, environment: "staging"} + + expect(controller.problems.size).to eq(5) end end - context 'environment development' do - it 'shows errs for just development' do - get :show, id: app.id, environment: 'development' - expect(controller.problems.size).to eq 5 + context "environment development" do + it "shows errs for just development" do + get :show, params: {id: app.id, environment: "development"} + + expect(controller.problems.size).to eq(5) end end - context 'environment test' do - it 'shows errs for just test' do - get :show, id: app.id, environment: 'test' - expect(controller.problems.size).to eq 5 + context "environment test" do + it "shows errs for just test" do + get :show, params: {id: app.id, environment: "test"} + + expect(controller.problems.size).to eq(5) end end end end - context 'logged in as a user' do - it 'finds the app even when not watching it' do - sign_in Fabricate(:user) - app = Fabricate(:app) + context "logged in as a user" do + it "finds the app even when not watching it" do + sign_in create(:user) + + app = create(:app) - get :show, id: app.id - expect(controller.app).to eq app + get :show, params: {id: app.id} + + expect(controller.app).to eq(app) end end end - context 'logged in as an admin' do + context "logged in as an admin" do before do sign_in admin end describe "GET /apps/new" do - it 'instantiates a new app with a prebuilt watcher' do + it "instantiates a new app with a prebuilt watcher" do get :new expect(controller.app).to be_a(App) expect(controller.app).to be_new_record - expect(controller.app.watchers).to_not be_empty + expect(controller.app.watchers).not_to be_empty end it "should copy attributes from an existing app" do - @app = Fabricate(:app, name: "do not copy", - github_repo: "test/example") - get :new, copy_attributes_from: @app.id + @app = create(:app, name: "do not copy", github_repo: "test/example") + get :new, params: {copy_attributes_from: @app.id} expect(controller.app).to be_a(App) expect(controller.app).to be_new_record expect(controller.app.name).to be_blank - expect(controller.app.github_repo).to eq "test/example" + expect(controller.app.github_repo).to eq("test/example") end end describe "GET /apps/:id/edit" do - it 'finds the correct app' do - app = Fabricate(:app) - get :edit, id: app.id - expect(controller.app).to eq app + it "finds the correct app" do + app = create(:app) + + get :edit, params: {id: app.id} + + expect(controller.app).to eq(app) end end describe "POST /apps" do before do - @app = Fabricate(:app) + @app = create(:app) + allow(App).to receive(:new).and_return(@app) end @@ -222,30 +238,30 @@ end it "should redirect to the app page" do - post :create, app: app_params + post :create, params: {app: app_params} expect(response).to redirect_to(app_path(@app)) end it "should display a message" do - post :create, app: app_params + post :create, params: {app: app_params} expect(request.flash[:success]).to match(/success/) end end end - describe "PUT /apps/:id" do + describe "PATCH /apps/:id" do before do - @app = Fabricate(:app) + @app = create(:app) end context "when the update is successful" do it "should redirect to the app page" do - put :update, id: @app.id, app: app_params + patch :update, params: {id: @app.id, app: app_params} expect(response).to redirect_to(app_path(@app)) end it "should display a message" do - put :update, id: @app.id, app: app_params + patch :update, params: {id: @app.id, app: app_params} expect(request.flash[:success]).to match(/success/) end end @@ -253,120 +269,149 @@ context "changing name" do it "should redirect to app page" do id = @app.id - put :update, id: id, app: { name: "new name" } + patch :update, params: {id: id, app: {name: "new name"}} expect(response).to redirect_to(app_path(id)) end end context "when the update is unsuccessful" do it "should render the edit page" do - put :update, id: @app.id, app: { name: '' } + patch :update, params: {id: @app.id, app: {name: ""}} expect(response).to render_template(:edit) end end context "changing email_at_notices" do before do - allow(Errbit::Config). - to receive(:per_app_email_at_notices).and_return(true) + allow(Errbit::Config) + .to receive(:per_app_email_at_notices).and_return(true) end it "should parse legal csv values" do - put :update, id: @app.id, app: { email_at_notices: '1, 4, 7,8, 10' } + patch :update, params: {id: @app.id, app: {email_at_notices: "1, 4, 7,8, 10"}} + @app.reload - expect(@app.email_at_notices).to eq [1, 4, 7, 8, 10] + + expect(@app.email_at_notices).to eq([1, 4, 7, 8, 10]) end context "failed parsing of CSV" do it "should set the default value" do - @app = Fabricate(:app, email_at_notices: [1, 2, 3, 4]) - put :update, id: @app.id, app: { email_at_notices: 'asdf, -1,0,foobar,gd00,0,abc' } + @app = create(:app, email_at_notices: [1, 2, 3, 4]) + + patch :update, params: {id: @app.id, app: {email_at_notices: "asdf, -1,0,foobar,gd00,0,abc"}} + @app.reload - expect(@app.email_at_notices).to eq Errbit::Config.email_at_notices + + expect(@app.email_at_notices).to eq(Errbit::Config.email_at_notices) end it "should display a message" do - put :update, id: @app.id, app: { email_at_notices: 'qwertyuiop' } + patch :update, params: {id: @app.id, app: {email_at_notices: "qwertyuiop"}} + expect(request.flash[:error]).to match(/Couldn't parse/) end end end - context "setting up issue tracker", cur: true do + context "setting up issue tracker" do context "unknown tracker type" do - before(:each) do - put :update, id: @app.id, app: { issue_tracker_attributes: { - type_tracker: 'unknown', options: { project_id: '1234', api_token: '123123', account: 'myapp' } - } } + before do + patch :update, params: { + id: @app.id, + app: { + issue_tracker_attributes: { + type_tracker: "unknown", + options: { + project_id: "1234", + api_token: "123123", + account: "myapp" + } + } + } + } + @app.reload end it "should not create issue tracker" do - expect(@app.issue_tracker_configured?).to eq false + expect(@app.issue_tracker_configured?).to eq(false) end end end context "selecting 'use site fingerprinter'" do - before(:each) do - SiteConfig.document.update_attributes(notice_fingerprinter: notice_fingerprinter) - put :update, id: @app.id, app: { - notice_fingerprinter_attributes: { backtrace_lines: 42 }, - use_site_fingerprinter: '1' + before do + SiteConfig.document.update!(notice_fingerprinter: notice_fingerprinter) + + patch :update, params: { + id: @app.id, + app: { + notice_fingerprinter_attributes: { + backtrace_lines: 42 + }, + use_site_fingerprinter: "1" + } } - @app.reload end it "should copy site fingerprinter into app fingerprinter" do - fingerprinter_attrs = @app.notice_fingerprinter.attributes.except('_id', 'source').to_h - expected_attrs = SiteConfig.document.notice_fingerprinter.attributes.except('_id', 'source').to_h + fingerprinter_attrs = @app.reload.notice_fingerprinter.attributes.except("_id", "source") + expected_attrs = SiteConfig.document.notice_fingerprinter.attributes.except("_id", "source") expect(fingerprinter_attrs).to eq(expected_attrs) end end context "not selecting 'use site fingerprinter'" do - before(:each) do + before do SiteConfig.document.update_attributes(notice_fingerprinter: notice_fingerprinter) - put :update, id: @app.id, app: { - notice_fingerprinter_attributes: { backtrace_lines: 42 }, - use_site_fingerprinter: '0' + + patch :update, params: { + id: @app.id, + app: { + notice_fingerprinter_attributes: { + backtrace_lines: 42 + }, + use_site_fingerprinter: "0" + } } - @app.reload end it "shouldn't copy site fingerprinter into app fingerprinter" do - fingerprinter_attrs = @app.notice_fingerprinter.attributes.except('_id', 'source').to_h - expected_attrs = SiteConfig.document.notice_fingerprinter.attributes.except('_id', 'source').to_h - expect(fingerprinter_attrs).to_not eq(expected_attrs) - expect(@app.notice_fingerprinter.backtrace_lines).to be 42 + fingerprinter_attrs = @app.reload.notice_fingerprinter.attributes.except("_id", "source") + expected_attrs = SiteConfig.document.notice_fingerprinter.attributes.except("_id", "source") + expect(fingerprinter_attrs).not_to eq(expected_attrs) + expect(@app.notice_fingerprinter.backtrace_lines).to eq(42) end end end describe "DELETE /apps/:id" do before do - @app = Fabricate(:app) + @app = create(:app) end it "should find the app" do - delete :destroy, id: @app.id - expect(controller.app).to eq @app + delete :destroy, params: {id: @app.id} + expect(controller.app).to eq(@app) end it "should destroy the app" do - delete :destroy, id: @app.id + delete :destroy, params: {id: @app.id} expect do @app.reload end.to raise_error(Mongoid::Errors::DocumentNotFound) end it "should display a message" do - delete :destroy, id: @app.id + delete :destroy, params: {id: @app.id} + expect(request.flash[:success]).to match(/success/) end it "should redirect to the apps page" do - delete :destroy, id: @app.id + delete :destroy, params: {id: @app.id} + expect(response).to redirect_to(apps_path) end end @@ -378,9 +423,10 @@ sign_in watcher.user end - it 'redirect to root with flash error' do - post :regenerate_api_key, id: 'foo' - expect(request).to redirect_to root_path + it "redirect to root with flash error" do + post :regenerate_api_key, params: {id: "foo"} + + expect(request).to redirect_to(root_path) end end @@ -389,9 +435,9 @@ sign_in admin end - it 'redirect_to app view' do + it "redirect_to app view" do expect do - post :regenerate_api_key, id: app.id + post :regenerate_api_key, params: {id: app.id} expect(request).to redirect_to edit_app_path(app) end.to change { app.reload.api_key } end @@ -401,30 +447,34 @@ describe "GET /apps/search" do before do sign_in user - @app1 = Fabricate(:app, name: 'Foo') - @app2 = Fabricate(:app, name: 'Bar') + @app_1 = create(:app, name: "Foo") + @app_2 = create(:app, name: "Bar") end it "renders successfully" do get :search - expect(response).to be_success + + expect(response).to be_successful end it "renders index template" do get :search - expect(response).to render_template('apps/index') + + expect(response).to render_template("apps/index") end it "searches problems for given string" do - get :search, search: "\"Foo\"" - expect(controller.apps).to include(@app1) - expect(controller.apps).to_not include(@app2) + get :search, params: {search: "\"Foo\""} + + expect(controller.apps).to include(@app_1) + expect(controller.apps).not_to include(@app_2) end it "works when given string is empty" do - get :search, search: "" - expect(controller.apps).to include(@app1) - expect(controller.apps).to include(@app2) + get :search, params: {search: ""} + + expect(controller.apps).to include(@app_1) + expect(controller.apps).to include(@app_2) end end end diff --git a/spec/controllers/comments_controller_spec.rb b/spec/controllers/comments_controller_spec.rb index 01226d16a0..6d1cec774b 100644 --- a/spec/controllers/comments_controller_spec.rb +++ b/spec/controllers/comments_controller_spec.rb @@ -1,26 +1,35 @@ -describe CommentsController, type: 'controller' do - let(:app) { Fabricate(:app) } - let(:err) { Fabricate(:err, problem: Fabricate(:problem, app: app, environment: "production")) } +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe CommentsController, type: :controller do + let(:app) { create(:app) } describe "POST /apps/:app_id/errs/:id/comments/create" do render_views - before(:each) do - sign_in Fabricate(:admin) + before do + sign_in create(:user, admin: true) end context "successful comment creation" do - let(:problem) { Fabricate(:problem) } - let(:user) { Fabricate(:user) } + let(:problem) { create(:problem) } + let(:user) { create(:user) } - before(:each) do - post :create, app_id: problem.app.id, problem_id: problem.id, - comment: { body: "One test comment", user_id: user.id } + before do + post :create, params: { + app_id: problem.app.id, + problem_id: problem.id, + comment: { + body: "One test comment", + user_id: user.id + } + } problem.reload end it "should create the comment" do - expect(problem.comments.size).to eq 1 + expect(problem.comments.size).to eq(1) end it "should redirect to problem page" do @@ -32,21 +41,25 @@ describe "DELETE /apps/:app_id/errs/:id/comments/:id/destroy" do render_views - before(:each) do - sign_in Fabricate(:admin) + before do + sign_in create(:user, admin: true) end context "successful comment deletion" do - let(:problem) { Fabricate(:problem_with_comments) } + let(:problem) { create(:problem_with_comments) } let(:comment) { problem.reload.comments.first } - before(:each) do - delete :destroy, app_id: problem.app.id, problem_id: problem.id, id: comment.id.to_s + before do + delete :destroy, params: { + app_id: problem.app.id, + problem_id: problem.id, + id: comment.id.to_s + } problem.reload end it "should delete the comment" do - expect(problem.comments.detect { |c| c.id.to_s == comment.id }).to be nil + expect(problem.comments.detect { |c| c.id.to_s == comment.id }).to eq(nil) end it "should redirect to problem page" do diff --git a/spec/controllers/devise/sessions_controller_spec.rb b/spec/controllers/devise/sessions_controller_spec.rb new file mode 100644 index 0000000000..add0ff23c2 --- /dev/null +++ b/spec/controllers/devise/sessions_controller_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Devise::SessionsController, type: :controller do + render_views + + describe "POST /users/sign_in" do + before do + @request.env["devise.mapping"] = Devise.mappings[:user] + end + + let(:app) { create(:app) } + + let(:user) { create(:user) } + + it "redirects to app index page if there are no apps for the user" do + post :create, params: {user: {"email" => user.email, "password" => user.password}} + + expect(response).to redirect_to(root_path) + end + + it "displays a friendly error when credentials are invalid" do + post :create, params: {user: {"email" => "whatever", "password" => "somethinginvalid"}} + + expect(request.flash["alert"]).to eq(I18n.t("devise.failure.not_found_in_database", authentication_keys: "email")) + end + end +end diff --git a/spec/controllers/devise_sessions_controller_spec.rb b/spec/controllers/devise_sessions_controller_spec.rb deleted file mode 100644 index 58061e9166..0000000000 --- a/spec/controllers/devise_sessions_controller_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -describe Devise::SessionsController, type: 'controller' do - render_views - - describe "POST /users/sign_in" do - before do - @request.env["devise.mapping"] = Devise.mappings[:user] - end - - let(:app) { Fabricate(:app) } - let(:user) { Fabricate(:user) } - - it 'redirects to app index page if there are no apps for the user' do - post :create, user: { 'email' => user.email, 'password' => user.password } - expect(response).to redirect_to(root_path) - end - - it 'displays a friendly error when credentials are invalid' do - post :create, user: { 'email' => 'whatever', 'password' => 'somethinginvalid' } - expect(request.flash["alert"]).to eq(I18n.t 'devise.failure.user.email_invalid') - end - end -end diff --git a/spec/controllers/notices_controller_spec.rb b/spec/controllers/notices_controller_spec.rb index 808ac4dc16..be1f76de83 100644 --- a/spec/controllers/notices_controller_spec.rb +++ b/spec/controllers/notices_controller_spec.rb @@ -1,18 +1,24 @@ -describe NoticesController, type: 'controller' do - it_requires_authentication for: { locate: :get } +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe NoticesController, type: :controller do + it_requires_authentication for: {locate: :get} + + let(:notice) { create(:notice) } + + let(:xml) { Rails.root.join("spec/fixtures/hoptoad_test_notice.xml").read } + + let(:app) { create(:app) } - let(:notice) { Fabricate(:notice) } - let(:xml) { Rails.root.join('spec', 'fixtures', 'hoptoad_test_notice.xml').read } - let(:app) { Fabricate(:app) } let(:error_report) { double(valid?: true, generate_notice!: true, notice: notice, should_keep?: true) } - context 'notices API' do + context "notices API" do context "with bogus xml" do it "returns an error" do - expect(request).to receive(:raw_post).and_return('notxml') - post :create, format: :xml - expect(response.status).to eq(422) - expect(response.body).to eq('The provided XML was not well-formed') + post :create, body: "notxml", format: :xml + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to eq("The provided XML was not well-formed") end end @@ -23,12 +29,11 @@ context "with xml pass in raw_port" do before do - expect(request).to receive(:raw_post).and_return(xml) - post :create, format: :xml + post :create, body: xml, format: :xml end it "generates a notice from raw xml [POST]" do - expect(response).to be_success + expect(response).to be_successful # Same RegExp from Airbrake::Sender#send_to_airbrake (https://github.com/airbrake/airbrake/blob/master/lib/airbrake/sender.rb#L53) # Inspired by https://github.com/airbrake/airbrake/blob/master/test/sender_test.rb expect(response.body).to match(%r{]*>#{notice.id}}) @@ -37,8 +42,8 @@ end it "generates a notice from xml in a data param [POST]" do - post :create, data: xml, format: :xml - expect(response).to be_success + post :create, params: {data: xml, format: :xml} + expect(response).to be_successful # Same RegExp from Airbrake::Sender#send_to_airbrake (https://github.com/airbrake/airbrake/blob/master/lib/airbrake/sender.rb#L53) # Inspired by https://github.com/airbrake/airbrake/blob/master/test/sender_test.rb expect(response.body).to match(%r{]*>#{notice.id}}) @@ -46,42 +51,61 @@ end it "generates a notice from xml [GET]" do - get :create, data: xml, format: :xml - expect(response).to be_success + get :create, params: {data: xml, format: :xml} + expect(response).to be_successful expect(response.body).to match(%r{]*>#{notice.id}}) expect(response.body).to match(%r{]*>(.+)#{locate_path(notice.id)}}) end context "with an invalid API_KEY" do let(:error_report) { double(valid?: false) } - it 'return 422' do - post :create, format: :xml, data: xml - expect(response.status).to eq 422 + + it "return 422" do + post :create, params: {format: :xml, data: xml} + expect(response).to have_http_status(:unprocessable_content) end end end context "without params needed" do - it 'return 400' do + it "return 400" do post :create, format: :xml - expect(response.status).to eq 400 - expect(response.body).to eq 'Need a data params in GET or raw post data' + expect(response).to have_http_status(:bad_request) + expect(response.body).to eq("Need a data params in GET or raw post data") end end end describe "GET /locate/:id" do - context 'when logged in as an admin' do - before(:each) do - @user = Fabricate(:admin) + context "when logged in as an admin" do + before do + @user = create(:user, admin: true) sign_in @user end it "should locate notice and redirect to problem" do - problem = Fabricate(:problem, app: app, environment: "production") - notice = Fabricate(:notice, err: Fabricate(:err, problem: problem)) - get :locate, id: notice.id + problem = create(:problem, app: app, environment: "production") + err = create(:err, problem: problem) + notice = create(:notice, err: err) + get :locate, params: {id: notice.id} expect(response).to redirect_to(app_problem_path(problem.app, problem)) end end end + + describe "GET /notices/:id" do + context "when logged in as an admin" do + before do + @user = create(:user, admin: true) + sign_in @user + end + + it "should locate notice and redirect to problem with notice_id" do + problem = create(:problem, app: app, environment: "production") + err = create(:err, problem: problem) + notice = create(:notice, err: err) + get :show_by_id, params: {id: notice.id} + expect(response).to redirect_to(app_problem_path(problem.app, problem, notice_id: notice.id)) + end + end + end end diff --git a/spec/controllers/problems_controller_spec.rb b/spec/controllers/problems_controller_spec.rb index d25aad9bd2..a7bf117065 100644 --- a/spec/controllers/problems_controller_spec.rb +++ b/spec/controllers/problems_controller_spec.rb @@ -1,77 +1,94 @@ -describe ProblemsController, type: 'controller' do - it_requires_authentication for: { - index: :get, show: :get, resolve: :put, search: :get - }, - params: { app_id: 'dummyid', id: 'dummyid' } +# frozen_string_literal: true - let(:app) { Fabricate(:app) } - let(:err) { Fabricate(:err, problem: problem) } - let(:user) { Fabricate(:user) } - let(:problem) { Fabricate(:problem, app: app, environment: "production") } +require "rails_helper" + +RSpec.describe ProblemsController, type: :controller do + it_requires_authentication( + for: { + index: :get, show: :get, resolve: :patch, search: :get + }, + params: {app_id: "dummyid", id: "dummyid"} + ) + + let(:app) { create(:app) } + + let(:err) { create(:err, problem: problem) } + + let(:user) { create(:user) } + + let(:problem) { create(:problem, app: app, environment: "production") } describe "GET /problems" do - before(:each) do + before do sign_in user - @problem = Fabricate(:notice, err: Fabricate(:err, problem: Fabricate(:problem, app: app, environment: "production"))).problem + + @problem = create(:notice, err: create(:err, problem: create(:problem, app: app, environment: "production"))).problem end context "pagination" do - before(:each) do - 35.times { Fabricate :err } - end + before { create_list(:err, 35) } it "should have default per_page value for user" do get :index - expect(controller.problems.to_a.size).to eq User::PER_PAGE + + expect(controller.problems.to_a.size).to eq(User::PER_PAGE) end it "should be able to override default per_page value" do - user.update_attribute :per_page, 10 + user.update_attribute(:per_page, 10) + get :index - expect(controller.problems.to_a.size).to eq 10 + + expect(controller.problems.to_a.size).to eq(10) end end - context 'with environment filters' do - before(:each) do - environments = %w(production test development staging) + context "with environment filters" do + before do + environments = ["production", "test", "development", "staging"] + 20.times do |i| - Fabricate(:problem, environment: environments[i % environments.length]) + create(:problem, environment: environments[i % environments.length]) end end - context 'no params' do - it 'shows problems for all environments' do + context "no params" do + it "shows problems for all environments" do get :index - expect(controller.problems.size).to eq 21 + + expect(controller.problems.size).to eq(21) end end - context 'environment production' do - it 'shows problems for just production' do - get :index, environment: 'production' - expect(controller.problems.size).to eq 6 + context "environment production" do + it "shows problems for just production" do + get :index, params: {environment: "production"} + + expect(controller.problems.size).to eq(6) end end - context 'environment staging' do - it 'shows problems for just staging' do - get :index, environment: 'staging' - expect(controller.problems.size).to eq 5 + context "environment staging" do + it "shows problems for just staging" do + get :index, params: {environment: "staging"} + + expect(controller.problems.size).to eq(5) end end - context 'environment development' do - it 'shows problems for just development' do - get :index, environment: 'development' - expect(controller.problems.size).to eq 5 + context "environment development" do + it "shows problems for just development" do + get :index, params: {environment: "development"} + + expect(controller.problems.size).to eq(5) end end - context 'environment test' do - it 'shows problems for just test' do - get :index, environment: 'test' - expect(controller.problems.size).to eq 5 + context "environment test" do + it "shows problems for just test" do + get :index, params: {environment: "test"} + + expect(controller.problems.size).to eq(5) end end end @@ -79,46 +96,74 @@ describe "GET /problems - previously all" do it "gets a paginated list of all problems" do - sign_in Fabricate(:user) + sign_in create(:user) + problems = Kaminari.paginate_array((1..30).to_a) - 3.times { problems << Fabricate(:err).problem } - 3.times { problems << Fabricate(:err, problem: Fabricate(:problem, resolved: true)).problem } + + 3.times { problems << create(:err).problem } + + 3.times { problems << create(:err, problem: create(:problem, resolved: true)).problem } + expect(Problem).to receive(:ordered_by).and_return( - double('proxy', page: double('other_proxy', per: problems)) + double("proxy", page: double("other_proxy", per: problems)) ) - get :index, all_errs: true - expect(controller.problems).to eq problems + + get :index, params: {all_errs: true} + + expect(controller.problems).to eq(problems) end end describe "GET /problems/search" do before do sign_in user - @app = Fabricate(:app) - @problem1 = Fabricate(:problem, app: @app, message: "Most important") - @problem2 = Fabricate(:problem, app: @app, message: "Very very important") + @app = create(:app) + @problem_1 = create(:problem, app: @app, message: "Most important") + @problem_2 = create(:problem, app: @app, message: "Very very important") end it "renders successfully" do get :search - expect(response).to be_success + + expect(response).to be_successful end it "renders index template" do get :search - expect(response).to render_template('problems/index') + + expect(response).to render_template("problems/index") end it "searches problems for given string" do - get :search, search: "\"Most important\"" - expect(controller.problems).to include(@problem1) - expect(controller.problems).to_not include(@problem2) + get :search, params: {search: "\"Most important\""} + + expect(controller.problems).to include(@problem_1) + + expect(controller.problems).not_to include(@problem_2) end it "works when given string is empty" do - get :search, search: "" - expect(controller.problems).to include(@problem1) - expect(controller.problems).to include(@problem2) + get :search, params: {search: ""} + + expect(controller.problems).to include(@problem_1) + + expect(controller.problems).to include(@problem_2) + end + end + + # you do not need an app id, strictly speaking, to find + # a problem, and if your metrics system does not happen + # to know the app id, but does know the problem id, + # it can be handy to have a way to link in to errbit. + describe "GET /problems/:id" do + before do + sign_in user + end + + it "should redirect to the standard problems page" do + get :show_by_id, params: {id: err.problem.id} + + expect(response).to redirect_to(app_problem_path(app, err.problem.id)) end end @@ -128,18 +173,21 @@ end it "finds the app" do - get :show, app_id: app.id, id: err.problem.id - expect(controller.app).to eq app + get :show, params: {app_id: app.id, id: err.problem.id} + + expect(controller.app).to eq(app) end it "finds the problem" do - get :show, app_id: app.id, id: err.problem.id - expect(controller.problem).to eq err.problem + get :show, params: {app_id: app.id, id: err.problem.id} + + expect(controller.problem).to eq(err.problem) end it "successfully render page" do - get :show, app_id: app.id, id: err.problem.id - expect(response).to be_success + get :show, params: {app_id: app.id, id: err.problem.id} + + expect(response).to be_successful end context "when rendering views" do @@ -147,27 +195,33 @@ it "successfully renders the view even when there are no notices attached to the problem" do expect(err.problem.notices).to be_empty - get :show, app_id: app.id, id: err.problem.id - expect(response).to be_success + + get :show, params: {app_id: app.id, id: err.problem.id} + + expect(response).to be_successful end end - context 'pagination' do + context "pagination" do let!(:notices) do 3.times.reduce([]) do |coll, i| - coll << Fabricate(:notice, err: err, created_at: (i.seconds.from_now)) + coll << create(:notice, err: err, created_at: i.seconds.from_now) end end it "paginates the notices 1 at a time, starting with the most recent" do - get :show, app_id: app.id, id: err.problem.id - expect(assigns(:notices).entries.count).to eq 1 + get :show, params: {app_id: app.id, id: err.problem.id} + + expect(assigns(:notices).entries.count).to eq(1) + expect(assigns(:notices)).to include(notices.last) end it "paginates the notices 1 at a time, based on then notice param" do - get :show, app_id: app.id, id: err.problem.id, notice: 3 - expect(assigns(:notices).entries.count).to eq 1 + get :show, params: {app_id: app.id, id: err.problem.id, notice: 3} + + expect(assigns(:notices).entries.count).to eq(1) + expect(assigns(:notices)).to include(notices.first) end end @@ -179,42 +233,51 @@ end it "renders without error" do - get :xhr_sparkline, app_id: app.id, id: err.problem.id - expect(response).to be_success + get :xhr_sparkline, params: {app_id: app.id, id: err.problem.id} + + expect(response).to be_successful end end - describe "PUT /apps/:app_id/problems/:id/resolve" do + describe "PATCH /apps/:app_id/problems/:id/resolve" do before do sign_in user - @err = Fabricate(:err) + @err = create(:err) end - it 'finds the app and the problem' do - put :resolve, app_id: @err.app.id, id: @err.problem.id + it "finds the app and the problem" do + patch :resolve, params: {app_id: @err.app.id, id: @err.problem.id} + expect(controller.app).to eq @err.app - expect(controller.problem).to eq @err.problem + expect(controller.problem).to eq(@err.problem) end it "should resolve the issue" do - put :resolve, app_id: @err.app.id, id: @err.problem.id - expect(@err.problem.reload.resolved).to be(true) + patch :resolve, params: {app_id: @err.app.id, id: @err.problem.id} + + expect(@err.problem.reload.resolved).to eq(true) end it "should display a message" do - put :resolve, app_id: @err.app.id, id: @err.problem.id + patch :resolve, params: {app_id: @err.app.id, id: @err.problem.id} + expect(request.flash[:success]).to match(/Great news/) end it "should redirect to the app page" do - put :resolve, app_id: @err.app.id, id: @err.problem.id + request.env["HTTP_REFERER"] = app_path(@err.app) + + patch :resolve, params: {app_id: @err.app.id, id: @err.problem.id} + expect(response).to redirect_to(app_path(@err.app)) end it "should redirect back to problems page" do request.env["HTTP_REFERER"] = problems_path - put :resolve, app_id: @err.app.id, id: @err.problem.id + + patch :resolve, params: {app_id: @err.app.id, id: @err.problem.id} + expect(response).to redirect_to(problems_path) end end @@ -223,10 +286,10 @@ before { sign_in user } context "when app has a issue tracker" do - let(:notice) { NoticeDecorator.new(Fabricate :notice) } + let(:notice) { NoticeDecorator.new(create(:notice)) } let(:problem) { ProblemDecorator.new(notice.problem) } let(:issue_tracker) do - Fabricate(:issue_tracker).tap do |t| + create(:issue_tracker).tap do |t| t.instance_variable_set(:@tracker, ErrbitPlugin::MockIssueTracker.new(t.options)) end end @@ -238,25 +301,29 @@ end it "should redirect to problem page" do - post :create_issue, app_id: problem.app.id, id: problem.id + post :create_issue, params: {app_id: problem.app.id, id: problem.id} + expect(response).to redirect_to(app_problem_path(problem.app, problem)) + expect(flash[:error]).to be_blank end it "should save the right title" do - post :create_issue, app_id: problem.app.id, id: problem.id + post :create_issue, params: {app_id: problem.app.id, id: problem.id} title = "[#{problem.environment}][#{problem.where}] #{problem.message.to_s.truncate(100)}" line = issue_tracker.tracker.output.shift expect(line[0]).to eq(title) end it "should renders the issue body" do - post :create_issue, app_id: problem.app.id, id: problem.id, format: 'html' - expect(response).to render_template("issue_trackers/issue") + post :create_issue, params: {app_id: problem.app.id, id: problem.id, format: "html"} + + expect(response).to render_template("issue_trackers/markdown") end it "should update the problem" do - post :create_issue, app_id: problem.app.id, id: problem.id + post :create_issue, params: {app_id: problem.app.id, id: problem.id} + expect(problem.issue_link).to eq("http://example.com/mock-errbit") expect(problem.issue_type).to eq("mock") end @@ -265,26 +332,31 @@ render_views it "should save the right body" do - post :create_issue, app_id: problem.app.id, id: problem.id, format: 'html' + post :create_issue, params: {app_id: problem.app.id, id: problem.id, format: "html"} + line = issue_tracker.tracker.output.shift - expect(line[1]).to include(app_problem_url problem.app, problem) + + expect(line[1]).to include(app_problem_url(problem.app, problem)) end it "should render whatever the issue tracker says" do allow_any_instance_of(Issue).to receive(:render_body_args).and_return( - [{ inline: 'one <%= problem.id %> two' }]) - post :create_issue, app_id: problem.app.id, id: problem.id, format: 'html' + [{inline: "one <%= problem.id %> two"}] + ) + post :create_issue, params: {app_id: problem.app.id, id: problem.id, format: "html"} line = issue_tracker.tracker.output.shift - expect(line[1]).to eq("one #{problem.id} two") + expect(line[1]).to include("one #{problem.id} two") end end end context "when app has no issue tracker" do it "should redirect to problem page" do - post :create_issue, app_id: problem.app.id, id: problem.id + post :create_issue, params: {app_id: problem.app.id, id: problem.id} + expect(response).to redirect_to(app_problem_path(problem.app, problem)) - expect(flash[:error]).to eql "This app has no issue tracker" + + expect(flash[:error]).to eq("This app has no issue tracker") end end end @@ -293,10 +365,10 @@ before { sign_in user } context "when app has a issue tracker" do - let(:notice) { NoticeDecorator.new(Fabricate :notice) } + let(:notice) { NoticeDecorator.new(create(:notice)) } let(:problem) { ProblemDecorator.new(notice.problem) } let(:issue_tracker) do - Fabricate(:issue_tracker).tap do |t| + create(:issue_tracker).tap do |t| t.instance_variable_set(:@tracker, ErrbitPlugin::MockIssueTracker.new(t.options)) end end @@ -308,7 +380,8 @@ end it "should redirect to problem page" do - post :close_issue, app_id: problem.app.id, id: problem.id + post :close_issue, params: {app_id: problem.app.id, id: problem.id} + expect(response).to redirect_to(app_problem_path(problem.app, problem)) expect(flash[:error]).to be_blank end @@ -316,23 +389,24 @@ context "when app has no issue tracker" do it "should redirect to problem page" do - post :close_issue, app_id: problem.app.id, id: problem.id + post :close_issue, params: {app_id: problem.app.id, id: problem.id} + expect(response).to redirect_to(app_problem_path(problem.app, problem)) - expect(flash[:error]).to eql "This app has no issue tracker" + expect(flash[:error]).to eq("This app has no issue tracker") end end end describe "DELETE /apps/:app_id/problems/:id/unlink_issue" do - before(:each) do + before do sign_in user end context "problem with issue" do - let(:err) { Fabricate(:err, problem: Fabricate(:problem, issue_link: "http://some.host")) } + let(:err) { create(:err, problem: create(:problem, issue_link: "http://some.host")) } - before(:each) do - delete :unlink_issue, app_id: err.app.id, id: err.problem.id + before do + delete :unlink_issue, params: {app_id: err.app.id, id: err.problem.id} err.problem.reload end @@ -341,15 +415,15 @@ end it "should clear issue link" do - expect(err.problem.issue_link).to be_nil + expect(err.problem.issue_link).to eq(nil) end end context "err without issue" do - let(:err) { Fabricate :err } + let(:err) { create(:err) } - before(:each) do - delete :unlink_issue, app_id: err.app.id, id: err.problem.id + before do + delete :unlink_issue, params: {app_id: err.app.id, id: err.problem.id} err.problem.reload end @@ -360,79 +434,87 @@ end describe "Bulk Actions" do - before(:each) do + before do sign_in user - @problem1 = Fabricate(:err, problem: Fabricate(:problem, resolved: true)).problem - @problem2 = Fabricate(:err, problem: Fabricate(:problem, resolved: false)).problem + @problem_1 = create(:err, problem: create(:problem, resolved: true)).problem + @problem_2 = create(:err, problem: create(:problem, resolved: false)).problem end context "POST /problems/merge_several" do it "should require at least two problems" do - post :merge_several, problems: [@problem1.id.to_s] - expect(request.flash[:notice]).to eql I18n.t('controllers.problems.flash.need_two_errors_merge') + post :merge_several, params: {problems: [@problem_1.id.to_s]} + + expect(request.flash[:notice]).to eq(I18n.t("controllers.problems.flash.need_two_errors_merge")) end it "should merge the problems" do expect(ProblemMerge).to receive(:new).and_return(double(merge: true)) - post :merge_several, problems: [@problem1.id.to_s, @problem2.id.to_s] + + post :merge_several, params: {problems: [@problem_1.id.to_s, @problem_2.id.to_s]} end end context "POST /problems/unmerge_several" do it "should require at least one problem" do - post :unmerge_several, problems: [] - expect(request.flash[:notice]).to eql I18n.t('controllers.problems.flash.no_select_problem') + post :unmerge_several, params: {problems: []} + expect(request.flash[:notice]).to eq(I18n.t("controllers.problems.flash.no_select_problem")) end it "should unmerge a merged problem" do - merged_problem = Problem.merge!(@problem1, @problem2) - expect(merged_problem.errs.length).to eq 2 + merged_problem = Problem.merge!(@problem_1, @problem_2) + expect(merged_problem.errs.length).to eq(2) expect do - post :unmerge_several, problems: [merged_problem.id.to_s] - expect(merged_problem.reload.errs.length).to eq 1 + post :unmerge_several, params: {problems: [merged_problem.id.to_s]} + expect(merged_problem.reload.errs.length).to eq(1) end.to change(Problem, :count).by(1) end end context "POST /problems/resolve_several" do it "should require at least one problem" do - post :resolve_several, problems: [] - expect(request.flash[:notice]).to eql I18n.t('controllers.problems.flash.no_select_problem') + post :resolve_several, params: {problems: []} + + expect(request.flash[:notice]).to eq(I18n.t("controllers.problems.flash.no_select_problem")) end it "should resolve the issue" do - post :resolve_several, problems: [@problem2.id.to_s] - expect(@problem2.reload.resolved?).to eq true + post :resolve_several, params: {problems: [@problem_2.id.to_s]} + + expect(@problem_2.reload.resolved?).to eq(true) end it "should display a message about 1 err" do - post :resolve_several, problems: [@problem2.id.to_s] + post :resolve_several, params: {problems: [@problem_2.id.to_s]} + expect(flash[:success]).to match(/1 error has been resolved/) end it "should display a message about 2 errs" do - post :resolve_several, problems: [@problem1.id.to_s, @problem2.id.to_s] + post :resolve_several, params: {problems: [@problem_1.id.to_s, @problem_2.id.to_s]} + expect(flash[:success]).to match(/2 errors have been resolved/) - expect(controller.selected_problems).to eq [@problem1, @problem2] + expect(controller.selected_problems).to eq([@problem_1, @problem_2]) end end context "POST /problems/unresolve_several" do it "should require at least one problem" do - post :unresolve_several, problems: [] - expect(request.flash[:notice]).to eql I18n.t('controllers.problems.flash.no_select_problem') + post :unresolve_several, params: {problems: []} + + expect(request.flash[:notice]).to eq(I18n.t("controllers.problems.flash.no_select_problem")) end it "should unresolve the issue" do - post :unresolve_several, problems: [@problem1.id.to_s] - expect(@problem1.reload.resolved?).to eq false + post :unresolve_several, params: {problems: [@problem_1.id.to_s]} + + expect(@problem_1.reload.resolved?).to eq(false) end end context "POST /problems/destroy_several" do it "should delete the problems" do expect do - post :destroy_several, problems: [@problem1.id.to_s] + post :destroy_several, params: {problems: [@problem_1.id.to_s]} end.to change(Problem, :count).by(-1) end end @@ -440,26 +522,30 @@ describe "POST /apps/:app_id/problems/destroy_all" do before do sign_in user - @app = Fabricate(:app) - @problem1 = Fabricate(:problem, app: @app) - @problem2 = Fabricate(:problem, app: @app) + @app = create(:app) + @problem_1 = create(:problem, app: @app) + @problem_2 = create(:problem, app: @app) end it "destroys all problems" do expect do - post :destroy_all, app_id: @app.id + post :destroy_all, params: {app_id: @app.id} end.to change(Problem, :count).by(-2) - expect(controller.app).to eq @app + + expect(controller.app).to eq(@app) end it "should display a message" do - put :destroy_all, app_id: @app.id + patch :destroy_all, params: {app_id: @app.id} + expect(request.flash[:success]).to match(/be deleted/) end it "should redirect back to the app page" do request.env["HTTP_REFERER"] = edit_app_path(@app) - put :destroy_all, app_id: @app.id + + patch :destroy_all, params: {app_id: @app.id} + expect(response).to redirect_to(edit_app_path(@app)) end end diff --git a/spec/controllers/site_config_controller_spec.rb b/spec/controllers/site_config_controller_spec.rb index be3b39b280..d03d67ea63 100644 --- a/spec/controllers/site_config_controller_spec.rb +++ b/spec/controllers/site_config_controller_spec.rb @@ -1,71 +1,93 @@ -describe SiteConfigController, type: 'controller' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe SiteConfigController, type: :controller do it_requires_admin_privileges for: { - index: :get, - update: :put + index: :get, + update: :patch } - let(:admin) { Fabricate(:admin) } + let(:admin) { create(:user, admin: true) } before { sign_in admin } - describe '#index' do - it 'has an index action' do + describe "#index" do + it "has an index action" do get :index end end - describe '#update' do - it 'updates' do - put :update, site_config: { - notice_fingerprinter_attributes: { - backtrace_lines: 3, - environment_name: false + describe "#update" do + it "updates" do + patch :update, params: { + site_config: { + notice_fingerprinter_attributes: { + backtrace_lines: 3, + environment_name: false + } } } fingerprinter = SiteConfig.document.notice_fingerprinter - expect(fingerprinter.environment_name).to be false - expect(fingerprinter.backtrace_lines).to be 3 + expect(fingerprinter.environment_name).to eq(false) + expect(fingerprinter.backtrace_lines).to eq(3) end - it 'redirects to the index' do - put :update, site_config: { - notice_fingerprinter_attributes: { error_class: true } + it "redirects to the index" do + patch :update, params: { + site_config: { + notice_fingerprinter_attributes: { + error_class: true + } + } } expect(response).to redirect_to(site_config_index_path) end - it 'flashes a confirmation' do - put :update, site_config: { - notice_fingerprinter_attributes: { error_class: true } + it "flashes a confirmation" do + patch :update, params: { + site_config: { + notice_fingerprinter_attributes: { + error_class: true + } + } } - expect(request.flash[:success]).to eq 'Updated site config' + expect(request.flash[:success]).to eq "Updated site config" end - it 'updates apps that are using site wide notice fingerprinter' do - put :update, site_config: { - notice_fingerprinter_attributes: { - backtrace_lines: 10, - environment_name: false + it "updates apps that are using site wide notice fingerprinter" do + patch :update, params: { + site_config: { + notice_fingerprinter_attributes: { + backtrace_lines: 10, + environment_name: false + } } } - app = App.new(name: 'my_app') + + app = App.new(name: "my_app") app.save - expect(app.notice_fingerprinter.backtrace_lines).to be 10 - expect(app.notice_fingerprinter.environment_name).to be false - put :update, site_config: { - notice_fingerprinter_attributes: { - backtrace_lines: 11, - environment_name: true + expect(app.notice_fingerprinter.backtrace_lines).to eq(10) + expect(app.notice_fingerprinter.environment_name).to eq(false) + + patch :update, params: { + site_config: { + notice_fingerprinter_attributes: { + backtrace_lines: 11, + environment_name: true + } } } + app.reload - expect(app.notice_fingerprinter.backtrace_lines).to be 11 - expect(app.notice_fingerprinter.environment_name).to be true + + expect(app.notice_fingerprinter.backtrace_lines).to eq(11) + expect(app.notice_fingerprinter.environment_name).to eq(true) end end end diff --git a/spec/controllers/users/omniauth_callbacks_controller_spec.rb b/spec/controllers/users/omniauth_callbacks_controller_spec.rb index 3ecbeb0f49..f2a3475a25 100644 --- a/spec/controllers/users/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/users/omniauth_callbacks_controller_spec.rb @@ -1,15 +1,16 @@ -describe Users::OmniauthCallbacksController, type: 'controller' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Users::OmniauthCallbacksController, type: :controller do def stub_env_for_github_omniauth(login, token = nil, email = "user@example.com") # This a Devise specific thing for functional tests. See https://github.com/plataformatec/devise/issues/closed#issue/608 request.env["devise.mapping"] = Devise.mappings[:user] - env = { - "omniauth.auth" => Hashie::Mash.new( - provider: 'github', - extra: { raw_info: { login: login, email: email } }, - credentials: { token: token } - ) - } - allow(@controller).to receive(:env).and_return(env) + request.env["omniauth.auth"] = Hashie::Mash.new( + provider: "github", + extra: {raw_info: {login: login, email: email}}, + credentials: {token: token} + ) end def stub_client_for_github_omniauth(emails = []) @@ -20,17 +21,17 @@ def stub_client_for_github_omniauth(emails = []) expect(Octokit::Client).to receive(:new) { mock_gh_client } end - context 'Linking a GitHub account to a signed in user' do + context "Linking a GitHub account to a signed in user" do before do - sign_in @user = Fabricate(:user) + sign_in @user = create(:user) end - it "should show an error if another user already has that github login" do - Fabricate(:user, github_login: "existing_user") + it "should show an error if another user already has that GitHub login" do + create(:user, github_login: "existing_user") stub_env_for_github_omniauth("existing_user") get :github - expect(request.flash[:error]).to include('already registered') + expect(request.flash[:error]).to include("already registered") expect(response).to redirect_to(user_path(@user)) end @@ -38,33 +39,34 @@ def stub_client_for_github_omniauth(emails = []) stub_env_for_github_omniauth("new_user") get :github - expect(request.flash[:success]).to include('Successfully linked') + expect(request.flash[:success]).to include("Successfully linked") expect(response).to redirect_to(user_path(@user)) end end - context 'Creating a new user via Github authentication' do + context "Creating a new user via GitHub authentication" do before do Errbit::Config.github_org_id = 42 end + after do Errbit::Config.github_org_id = nil end - context 'User has valid emails defined' do + context "User has valid emails defined" do it "should log in the user" do stub_env_for_github_omniauth("new_user_with_no_profile_email", nil, nil) stub_client_for_github_omniauth([OpenStruct.new(email: "user@example.com", primary: true)]) get :github - expect(request.flash[:success]).to include('Successfully authenticated from GitHub account') + expect(request.flash[:success]).to include("Successfully authenticated from GitHub account") expect(response).to redirect_to(root_path) end end - context 'User has no email defined' do - it 'should return an error' do + context "User has no email defined" do + it "should return an error" do stub_env_for_github_omniauth("new_user_with_no_profile_email", nil, nil) stub_client_for_github_omniauth @@ -76,32 +78,29 @@ def stub_client_for_github_omniauth(emails = []) end end - def stub_env_for_google_omniauth(login, _token = nil) + def stub_env_for_google_omniauth(login, _ = nil) # This a Devise specific thing for functional tests. See https://github.com/plataformatec/devise/issues/closed#issue/608 request.env["devise.mapping"] = Devise.mappings[:user] - env = { - "omniauth.auth" => Hashie::Mash.new( - credentials: { - provider: 'google_oauth2' - }, - info: { email: "#{login}@example.com", name: "John Smith" }, - uid: login - ) - } - allow(@controller).to receive(:env).and_return(env) + request.env["omniauth.auth"] = Hashie::Mash.new( + credentials: { + provider: "google_oauth2" + }, + info: {email: "#{login}@example.com", name: "John Smith"}, + uid: login + ) end - context 'Linking a Google account to a signed in user' do + context "Linking a Google account to a signed in user" do before do - sign_in @user = Fabricate(:user) + sign_in @user = create(:user) end it "should show an error if another user already has that google login" do - Fabricate(:user, google_uid: "111111111111111111111") + create(:user, google_uid: "111111111111111111111") stub_env_for_google_omniauth("111111111111111111111") get :google_oauth2 - expect(request.flash[:error]).to include('already registered') + expect(request.flash[:error]).to include("already registered") expect(response).to redirect_to(user_path(@user)) end @@ -109,10 +108,10 @@ def stub_env_for_google_omniauth(login, _token = nil) stub_env_for_google_omniauth("111111111111111111112") get :google_oauth2 - expect(request.flash[:success]).to include('Successfully linked') + expect(request.flash[:success]).to include("Successfully linked") expect(response).to redirect_to(user_path(@user)) end end - # See spec/acceptance/sign_in_with_github_spec.rb for 'Signing in with github' integration tests. + # See spec/acceptance/sign_in_with_github_spec.rb for 'Signing in with GitHub' integration tests. end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb deleted file mode 100644 index dbe0c51da5..0000000000 --- a/spec/controllers/users_controller_spec.rb +++ /dev/null @@ -1,256 +0,0 @@ -describe UsersController, type: 'controller' do - it_requires_authentication - it_requires_admin_privileges for: { - index: :get, - show: :get, - new: :get, - create: :post, - destroy: :delete - } - - let(:admin) { Fabricate(:admin) } - let(:user) { Fabricate(:user) } - let(:other_user) { Fabricate(:user) } - - context 'Signed in as a regular user' do - before do - sign_in user - end - - it "should set a time zone" do - expect(Time.zone.to_s).to match(user.time_zone) - end - - context "GET /users/:other_id/edit" do - it "redirects to the home page" do - get :edit, id: other_user.id - expect(response).to redirect_to(root_path) - end - end - - context "GET /users/:my_id/edit" do - it 'finds the user' do - get :edit, id: user.id - expect(controller.user).to eq(user) - expect(response).to render_template 'edit' - end - end - - context "PUT /users/:other_id" do - it "redirects to the home page" do - put :update, id: other_user.id - expect(response).to redirect_to(root_path) - end - end - - context "PUT /users/:my_id/id" do - context "when the update is successful" do - it "sets a message to display" do - put :update, id: user.to_param, user: { name: 'Kermit' } - expect(request.flash[:success]).to include('updated') - end - - it "redirects to the user's page" do - put :update, id: user.to_param, user: { name: 'Kermit' } - expect(response).to redirect_to(user_path(user)) - end - - it "should not be able to become an admin" do - expect do - put :update, id: user.to_param, user: { admin: true } - end.to_not change { - user.reload.admin - }.from(false) - end - - it "should be able to set per_page option" do - put :update, id: user.to_param, user: { per_page: 555 } - expect(user.reload.per_page).to eq 555 - end - - it "should be able to set time_zone option" do - put :update, id: user.to_param, user: { time_zone: "Warsaw" } - expect(user.reload.time_zone).to eq "Warsaw" - end - - it "should be able to not set github_login option" do - put :update, id: user.to_param, user: { github_login: " " } - expect(user.reload.github_login).to eq nil - end - - it "should be able to set github_login option" do - put :update, id: user.to_param, user: { github_login: "awesome_name" } - expect(user.reload.github_login).to eq "awesome_name" - end - end - - context "when the update is unsuccessful" do - it "renders the edit page" do - put :update, id: user.to_param, user: { name: nil } - expect(response).to render_template(:edit) - end - end - end - end - - context 'Signed in as an admin' do - before do - sign_in admin - end - - context "GET /users" do - it 'paginates all users' do - admin.update_attribute :per_page, 2 - 3.times { Fabricate(:user) } - get :index - expect(controller.users.to_a.size).to eq 2 - end - end - - context "GET /users/:id" do - it 'finds the user' do - get :show, id: user.id - expect(controller.user).to eq user - end - end - - context "GET /users/new" do - it 'assigns a new user' do - get :new - expect(controller.user).to be_a(User) - expect(controller.user).to be_new_record - end - end - - context "GET /users/:id/edit" do - it 'finds the user' do - get :edit, id: user.id - expect(controller.user).to eq user - end - end - - context "POST /users" do - context "when the create is successful" do - let(:attrs) { { user: Fabricate.to_params(:user) } } - - it "sets a message to display" do - post :create, attrs - expect(request.flash[:success]).to include('part of the team') - end - - it "redirects to the user's page" do - post :create, attrs - expect(response).to redirect_to(user_path(controller.user)) - end - - it "should be able to create admin" do - attrs[:user][:admin] = true - post :create, attrs - expect(response).to be_redirect - expect(User.find(controller.user.to_param).admin).to be(true) - end - - it "should has auth token" do - post :create, attrs - expect(User.last.authentication_token).to_not be_blank - end - end - - context "when the create is unsuccessful" do - let(:user) do - Struct.new(:admin, :attributes).new(true, {}) - end - before do - expect(User).to receive(:new).and_return(user) - expect(user).to receive(:save).and_return(false) - end - - it "renders the new page" do - post :create, user: { username: 'foo' } - expect(response).to render_template(:new) - end - end - end - - context "PUT /users/:id" do - context "when the update is successful" do - before do - put :update, id: user.to_param, user: user_params - end - - context "with normal params" do - let(:user_params) { { name: 'Kermit' } } - it "sets a message to display" do - expect(request.flash[:success]).to eq I18n.t('controllers.users.flash.update.success', name: user.reload.name) - expect(response).to redirect_to(user_path(user)) - end - end - end - context "when the update is unsuccessful" do - it "renders the edit page" do - put :update, id: user.to_param, user: { name: nil } - expect(response).to render_template(:edit) - end - end - end - - context "DELETE /users/:id" do - context "with a destroy success" do - let(:user_destroy) { double(destroy: true) } - - before do - expect(UserDestroy).to receive(:new).with(user).and_return(user_destroy) - delete :destroy, id: user.id - end - - it 'should destroy user' do - expect(request.flash[:success]).to eq I18n.t('controllers.users.flash.destroy.success', name: user.name) - expect(response).to redirect_to(users_path) - end - end - - context "with trying destroy himself" do - before do - expect(UserDestroy).to_not receive(:new) - delete :destroy, id: admin.id - end - - it 'should not destroy user' do - expect(response).to redirect_to(users_path) - expect(request.flash[:error]).to eq I18n.t('controllers.users.flash.destroy.error') - end - end - end - - describe "#user_params" do - context "with current user not admin" do - before do - allow(controller).to receive(:current_user).and_return(user) - allow(controller).to receive(:params).and_return( - ActionController::Parameters.new(user_param) - ) - end - let(:user_param) { { 'user' => { name: 'foo', admin: true } } } - it 'not have admin field' do - expect(controller.send(:user_params)).to eq('name' => 'foo') - end - context "with password and password_confirmation empty?" do - let(:user_param) { { 'user' => { :name => 'foo', 'password' => '', 'password_confirmation' => '' } } } - it 'not have password and password_confirmation field' do - expect(controller.send(:user_params)).to eq('name' => 'foo') - end - end - end - - context "with current user admin" do - it 'have admin field' - context "with password and password_confirmation empty?" do - it 'not have password and password_confirmation field' - end - context "on his own user" do - it 'not have admin field' - end - end - end - end -end diff --git a/spec/controllers/watchers_controller_spec.rb b/spec/controllers/watchers_controller_spec.rb index 0c4476746e..cdec783833 100644 --- a/spec/controllers/watchers_controller_spec.rb +++ b/spec/controllers/watchers_controller_spec.rb @@ -1,26 +1,27 @@ -describe WatchersController, type: 'controller' do - let(:user) { Fabricate(:user) } - let(:problem) { Fabricate(:problem) } +# frozen_string_literal: true - before(:each) { sign_in user } +require "rails_helper" - describe "#destroy" do - let(:app) do - a = Fabricate(:app) - Fabricate(:user_watcher, app: a, user: user) - a - end +RSpec.describe WatchersController, type: :controller do + let(:user) { create(:user) } + let(:problem) { create(:problem) } - context "successful watcher deletion" do - let(:watcher) { app.watchers.first } + before { sign_in user } + + describe "#create" do + let(:app) { create(:app) } + + context "successful watcher create" do + before do + post :create, params: {app_id: app.id} - before(:each) do - delete :destroy, app_id: app.id, id: watcher.user.id.to_s problem.reload end - it "should delete the watcher" do - expect(app.watchers.detect { |w| w.id.to_s == watcher.id }).to be nil + it "should be watching" do + app.reload + + expect(app.watchers.first.user_id).to eq(user.id) end it "should redirect to app page" do @@ -29,18 +30,24 @@ end end - describe "#update" do - let(:app) { Fabricate(:app) } + describe "#destroy" do + let(:app) do + a = create(:app) + create(:user_watcher, app: a, user: user) + a + end + + context "successful watcher deletion" do + let(:watcher) { app.watchers.first } + + before do + delete :destroy, params: {app_id: app.id} - context "successful watcher update" do - before(:each) do - put :update, app_id: app.id, id: user.id.to_s problem.reload end - it "should be watching" do - app.reload - expect(app.watchers.first.user_id).to eq user.id + it "should delete the watcher" do + expect(app.watchers.detect { |w| w.id.to_s == watcher.id }).to eq(nil) end it "should redirect to app page" do diff --git a/spec/decorators/app_decorator_spec.rb b/spec/decorators/app_decorator_spec.rb index e0d4b62f74..5bac5d850a 100644 --- a/spec/decorators/app_decorator_spec.rb +++ b/spec/decorators/app_decorator_spec.rb @@ -1,27 +1,65 @@ -describe AppDecorator do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe AppDecorator, type: :decorator do describe "#email_at_notices" do - it 'return the list separate by comma' do - expect(AppDecorator.new(double(email_at_notices: [2, 3])).email_at_notices).to eql '2, 3' + it "return the list separate by comma" do + expect(described_class.new(double(email_at_notices: [2, 3])).email_at_notices).to eq("2, 3") end end describe "#notify_user_display" do - it 'return display:none if notify' do - expect(AppDecorator.new(double(notify_all_users: true)).notify_user_display).to eql 'display: none;' + it "return display:none if notify" do + expect(described_class.new(double(notify_all_users: true)).notify_user_display).to eq("display: none;") + end + + it "return blank if no notify" do + expect(described_class.new(double(notify_all_users: false)).notify_user_display).to eq("") + end + end + + describe "#notify_err_class" do + it "returns hidden if no notify" do + expect(described_class.new(double(notify_on_errs: false)).notify_err_class).to eq("hidden") + end + + it "returns blank if notify" do + expect(described_class.new(double(notify_on_errs: true)).notify_err_class).to eq("") + end + end + + describe "#custom_notice_fingerprinter_class" do + it "returns hidden when using the site fingerprinter" do + fingerprinter = double(attributes: {"source" => SiteConfig::CONFIG_SOURCE_SITE}) + app = double(notice_fingerprinter: fingerprinter) + + expect(described_class.new(app).custom_notice_fingerprinter_class).to eq("hidden") end - it 'return blank if no notify' do - expect(AppDecorator.new(double(notify_all_users: false)).notify_user_display).to eql '' + it "returns blank when using a custom fingerprinter" do + fingerprinter = double(attributes: {"source" => "custom"}) + app = double(notice_fingerprinter: fingerprinter) + + expect(described_class.new(app).custom_notice_fingerprinter_class).to eq("") end end - describe "#notify_err_display" do - it 'return display:none if no notify' do - expect(AppDecorator.new(double(notify_on_errs: false)).notify_err_display).to eql 'display: none;' + describe "#custom_backtrace_url" do + it "should correctly replace the unescaped fields" do + dbl = double(repo_branch: "feature/branch", + custom_backtrace_url_template: "https://example.com/repo/name/src/branch/%{branch}/%{file}#L%{line}") + + expect(described_class.new(dbl).custom_backtrace_url("test/file.rb", 42)) + .to eq("https://example.com/repo/name/src/branch/feature/branch/test/file.rb#L42") end - it 'return blank if no notify' do - expect(AppDecorator.new(double(notify_on_errs: true)).notify_err_display).to eql '' + it "should correctly replace the escaped fields" do + dbl = double(repo_branch: "feature/branch", + custom_backtrace_url_template: "https://example.com/repo/name/src/branch/%{ebranch}/%{efile}#L%{line}") + + expect(described_class.new(dbl).custom_backtrace_url("test/file.rb", 42)) + .to eq("https://example.com/repo/name/src/branch/feature%2Fbranch/test%2Ffile.rb#L42") end end end diff --git a/spec/decorators/backtrace_decorator_spec.rb b/spec/decorators/backtrace_decorator_spec.rb index 90324b862a..21f83ad561 100644 --- a/spec/decorators/backtrace_decorator_spec.rb +++ b/spec/decorators/backtrace_decorator_spec.rb @@ -1,57 +1,61 @@ -describe BacktraceDecorator, type: :decorator do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe BacktraceDecorator, type: :decorator do let(:backtrace) do described_class.new( Backtrace.new( lines: [ - { number: 131, - file: '[PROJECT_ROOT]app/controllers/accounts_controller.rb', - method: :update_preferences }, - { number: 61, - file: '[PROJECT_ROOT]app/controllers/application_controller.rb', - method: :call }, - { number: 182, - file: '[GEM_ROOT]activesupport-2.3.18/lib/active_support/callbacks.rb', - method: :call }, - { number: 384, - file: '[PROJECT_ROOT]app/models/account.rb', - method: :update_server_tag_scope }, - { number: 182, - file: '[GEM_ROOT]activesupport-2.3.18/lib/active_support/callbacks.rb', - method: :evaluate_method }, - { number: 23, - file: '/home/rails/library/current/vendor/bundle/ruby/2.1.0/bin/rainbows', - method: '
' } + {number: 131, + file: "[PROJECT_ROOT]app/controllers/accounts_controller.rb", + method: :update_preferences}, + {number: 61, + file: "[PROJECT_ROOT]app/controllers/application_controller.rb", + method: :call}, + {number: 182, + file: "[GEM_ROOT]activesupport-2.3.18/lib/active_support/callbacks.rb", + method: :call}, + {number: 384, + file: "[PROJECT_ROOT]app/models/account.rb", + method: :update_server_tag_scope}, + {number: 182, + file: "[GEM_ROOT]activesupport-2.3.18/lib/active_support/callbacks.rb", + method: :evaluate_method}, + {number: 23, + file: "/home/rails/library/current/vendor/bundle/ruby/2.1.0/bin/rainbows", + method: "
"} ] ) ) end - describe '#grouped_lines' do + describe "#grouped_lines" do let(:grouped) { backtrace.grouped_lines.to_a } - it 'puts the first two in-app lines in separate groups' do + it "puts the first two in-app lines in separate groups" do expect(grouped[0][0]).to be_present - expect(grouped[0][1]).to eq [backtrace.lines[0]] + expect(grouped[0][1]).to eq([backtrace.lines[0]]) expect(grouped[1][0]).to be_present - expect(grouped[1][1]).to eq [backtrace.lines[1]] + expect(grouped[1][1]).to eq([backtrace.lines[1]]) end - it 'puts the first out-of-app line in its own group' do - expect(grouped[2][0]).to eq false - expect(grouped[2][1]).to eq [backtrace.lines[2]] + it "puts the first out-of-app line in its own group" do + expect(grouped[2][0]).to eq(false) + expect(grouped[2][1]).to eq([backtrace.lines[2]]) end - it 'puts the last two out-of-app lines together in one group' do - expect(grouped[4][0]).to eq false - expect(grouped[4][1]).to eq [backtrace.lines[4], backtrace.lines[5]] + it "puts the last two out-of-app lines together in one group" do + expect(grouped[4][0]).to eq(false) + expect(grouped[4][1]).to eq([backtrace.lines[4], backtrace.lines[5]]) end end - describe '#lines' do - context 'when empty' do + describe "#lines" do + context "when empty" do let(:backtrace) { described_class.new(Backtrace.new) } - it 'does not raise an NoMethodError' do + it "does not raise an NoMethodError" do expect { backtrace.lines }.not_to raise_error end end diff --git a/spec/decorators/backtrace_line_decorator_spec.rb b/spec/decorators/backtrace_line_decorator_spec.rb index 90bdaf8391..9aeeba8ede 100644 --- a/spec/decorators/backtrace_line_decorator_spec.rb +++ b/spec/decorators/backtrace_line_decorator_spec.rb @@ -1,58 +1,88 @@ -describe BacktraceLineDecorator, type: :decorator do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe BacktraceLineDecorator, type: :decorator do let(:backtrace_line) do described_class.new( number: 884, - file: '/path/to/file/ea315ea4.rb', - method: :instance_eval) + file: "/path/to/file/ea315ea4.rb", + method: :instance_eval + ) end + let(:backtrace_line_in_app) do described_class.new( number: 884, - file: '[PROJECT_ROOT]/path/to/file/ea315ea4.rb', - method: :instance_eval) + file: "[PROJECT_ROOT]/path/to/file/ea315ea4.rb", + method: :instance_eval + ) + end + + let(:backtrace_line_in_app_slashes) do + described_class.new( + number: 884, + file: "/PROJECT_ROOT/path/to/file/ea315ea4.rb", + method: :instance_eval + ) end + let(:backtrace_line_no_file) do described_class.new(number: 884, method: :instance_eval) end + let(:backtrace_line_no_object) do described_class.new(nil) end - let(:app) { Fabricate(:app, github_repo: 'foo/bar') } - describe '#to_s' do - it 'returns a nice string representation of the first line' do - expect(backtrace_line.to_s).to eq('/path/to/file/ea315ea4.rb:884') + let(:app) { create(:app, github_repo: "foo/bar", repository_branch: "main") } + + describe "#to_s" do + it "returns a nice string representation of the first line" do + expect(backtrace_line.to_s).to eq("/path/to/file/ea315ea4.rb:884") end end - describe '#file' do + describe "#file" do it 'returns "" when there is no file' do - expect(backtrace_line_no_file.file).to eq('') + expect(backtrace_line_no_file.file).to eq("") end it 'returns "" when there is no object' do - expect(backtrace_line_no_object.file).to eq('') + expect(backtrace_line_no_object.file).to eq("") end end - describe '#in_app?' do - it 'returns false when there is no file' do - expect(backtrace_line_no_file.in_app?).to be false + describe "#in_app?" do + it "returns false when there is no file" do + expect(backtrace_line_no_file.in_app?).to eq(false) end end - describe '#path' do + describe "#path" do it 'returns "" when there is no file' do - expect(backtrace_line_no_file.path).to eq '' + expect(backtrace_line_no_file.path).to eq("") + end + end + + describe "#decorated_path" do + it "parses old backtrace format with square brackets" do + expect(backtrace_line_in_app.decorated_path).to eq("path/to/file/") + end + + it "parses new backtrace format with slashes" do + expect(backtrace_line_in_app_slashes.decorated_path).to eq("path/to/file/") end end - describe '#link_to_source_file' do - it 'adds a link to the source file' do - link = backtrace_line_in_app.link_to_source_file(app) { 'mytext' } + describe "#link_to_source_file" do + it "adds a link to the source file" do + link = backtrace_line_in_app.link_to_source_file(app) { "mytext" } expect(link).to eq( - 'mytext') + 'mytext' + ) end end end diff --git a/spec/decorators/issue_tracker_decorator_spec.rb b/spec/decorators/issue_tracker_decorator_spec.rb index 2a0c550a0f..96d746c193 100644 --- a/spec/decorators/issue_tracker_decorator_spec.rb +++ b/spec/decorators/issue_tracker_decorator_spec.rb @@ -1,18 +1,22 @@ -describe IssueTrackerDecorator do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe IssueTrackerDecorator, type: :decorator do let(:fake_tracker) do klass = Class.new(ErrbitPlugin::IssueTracker) do def self.label - 'fake' + "fake" end def self.note - 'a note' + "a note" end def self.fields { - foo: { label: 'foo' }, - bar: { label: 'bar' } + foo: {label: "foo"}, + bar: {label: "bar"} } end @@ -20,21 +24,23 @@ def configured? true end end - klass.new 'nothing special' + klass.new "nothing special" end let(:issue_tracker) do - it = IssueTracker.new - allow(it).to receive(:tracker).and_return(fake_tracker) - it + instance = IssueTracker.new + + allow(instance).to receive(:tracker).and_return(fake_tracker) + + instance end let(:decorator) do - IssueTrackerDecorator.new(issue_tracker) + described_class.new(issue_tracker) end describe "#type" do - it 'returns decorator for the issue tracker class' do + it "returns decorator for the issue tracker class" do expect(decorator.type.class).to eq(IssueTrackerTypeDecorator) end end diff --git a/spec/decorators/issue_tracker_field_decorator.rb b/spec/decorators/issue_tracker_field_decorator.rb deleted file mode 100644 index c87ae1193c..0000000000 --- a/spec/decorators/issue_tracker_field_decorator.rb +++ /dev/null @@ -1,11 +0,0 @@ -describe IssueTrackerFieldDecorator do - describe "#label" do - it 'return the label of field_info by default' do - expect(IssueTrackerFieldDecorator.new(:foo, label: 'hello').label).to eq 'hello' - end - - it 'return the key of field if no label define' do - expect(IssueTrackerFieldDecorator.new(:foo, {}).label).to eq 'foo' - end - end -end diff --git a/spec/decorators/issue_tracker_field_decorator_spec.rb b/spec/decorators/issue_tracker_field_decorator_spec.rb new file mode 100644 index 0000000000..22277e776b --- /dev/null +++ b/spec/decorators/issue_tracker_field_decorator_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe IssueTrackerFieldDecorator, type: :decorator do + describe "#label" do + it "return the label of field_info by default" do + expect(described_class.new(:foo, label: "hello").label).to eq("hello") + end + + it "return the key of field if no label define" do + expect(described_class.new(:foo, {}).label).to eq("Foo") + end + end +end diff --git a/spec/decorators/issue_tracker_type_decorator_spec.rb b/spec/decorators/issue_tracker_type_decorator_spec.rb index f3adcce977..7d6739c364 100644 --- a/spec/decorators/issue_tracker_type_decorator_spec.rb +++ b/spec/decorators/issue_tracker_type_decorator_spec.rb @@ -1,25 +1,29 @@ -describe IssueTrackerDecorator do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe IssueTrackerTypeDecorator, type: :decorator do let(:fake_tracker_class) do klass = Class.new(ErrbitPlugin::IssueTracker) do def self.label - 'fake' + "fake" end def self.note - 'a note' + "a note" end def self.fields { - foo: { label: 'foo' }, - bar: { label: 'bar' } + foo: {label: "foo"}, + bar: {label: "bar"} } end def self.icons { - one: ['text/plain', 'all your base are belong to us'], - two: ['application/xml', ''] + one: ["text/plain", "all your base are belong to us"], + two: ["application/xml", ""] } end end @@ -32,39 +36,39 @@ def self.icons let(:fake_tracker) { IssueTrackerDecorator.new(fake_tracker_class.new) } let(:decorator) { IssueTrackerTypeDecorator.new(fake_tracker_class) } - describe "::note" do - it 'return the html_safe of Note' do - expect(decorator.note).to eql fake_tracker_class.note + describe ".note" do + it "return the html_safe of Note" do + expect(decorator.note).to eq(fake_tracker_class.note) end end describe "#fields" do - it 'return all FIELDS define decorate' do + it "return all FIELDS define decorate" do decorator.fields do |itf| expect(itf).to be_a(IssueTrackerFieldDecorator) expect([:foo, :bar]).to be_include(itf.object) - expect([{ label: 'foo' }, { label: 'bar' }]).to be_include(itf.field_info) + expect([{label: "foo"}, {label: "bar"}]).to be_include(itf.field_info) end end end describe "#params_class" do - it 'adds the label in class' do + it "adds the label in class" do tracker = IssueTrackerDecorator.new( - IssueTracker.new(type_tracker: 'none') + IssueTracker.new(type_tracker: "none") ) - expect(decorator.params_class(tracker)).to eql 'fake' + expect(decorator.params_class(tracker)).to eq("fake") end - it 'adds chosen class if type is same' do + it "adds chosen class if type is same" do expect( - decorator. - params_class( + decorator + .params_class( IssueTracker.new( - type_tracker: 'fake' + type_tracker: "fake" ).decorate ) - ).to eql 'chosen fake' + ).to eq("chosen fake") end end end diff --git a/spec/decorators/notice_decorator_spec.rb b/spec/decorators/notice_decorator_spec.rb new file mode 100644 index 0000000000..4cbae1e6ff --- /dev/null +++ b/spec/decorators/notice_decorator_spec.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe NoticeDecorator, type: :decorator do # rubocop:disable Lint/EmptyBlock, RSpec/EmptyExampleGroup +end diff --git a/spec/decorators/problem_decorator_spec.rb b/spec/decorators/problem_decorator_spec.rb new file mode 100644 index 0000000000..76aa41033f --- /dev/null +++ b/spec/decorators/problem_decorator_spec.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ProblemDecorator, type: :decorator do # rubocop:disable Lint/EmptyBlock, RSpec/EmptyExampleGroup +end diff --git a/spec/decorators/watcher_decorator_spec.rb b/spec/decorators/watcher_decorator_spec.rb deleted file mode 100644 index 7691f133ee..0000000000 --- a/spec/decorators/watcher_decorator_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -describe WatcherDecorator do - describe "#email_choosen" do - context "with email define" do - it 'return blank' do - expect(WatcherDecorator.new(double(email: 'foo')).email_choosen).to eql '' - end - end - - context "without email define" do - it 'return choosen' do - expect(WatcherDecorator.new(double(email: '')).email_choosen).to eql 'chosen' - end - end - end -end diff --git a/spec/errbit_plugin/mock_issue_tracker.rb b/spec/errbit_plugin/mock_issue_tracker.rb index 1925dc562e..cb0fcc0d2f 100644 --- a/spec/errbit_plugin/mock_issue_tracker.rb +++ b/spec/errbit_plugin/mock_issue_tracker.rb @@ -1,17 +1,19 @@ +# frozen_string_literal: true + module ErrbitPlugin class MockIssueTracker < IssueTracker def self.label - 'mock' + "mock" end def self.note - 'A fake issue tracker to help in testing purpose' + "A fake issue tracker to help in testing purpose" end def self.fields { - foo: { label: 'foo' }, - bar: { label: 'bar' } + foo: {label: "foo"}, + bar: {label: "bar"} } end @@ -23,13 +25,13 @@ def initialize(*) end def configured? - !errors.any? + errors.none? end def errors errors = [] - errors << [:base, 'foo is required'] unless options[:foo] - errors << [:base, 'bar is required'] unless options[:bar] + errors << [:base, "foo is required"] unless options[:foo] + errors << [:base, "bar is required"] unless options[:bar] errors end @@ -44,7 +46,7 @@ def close_issue(url, user) end def url - '' + "" end def comments_allowed? diff --git a/spec/fabricators/app_fabricator.rb b/spec/fabricators/app_fabricator.rb deleted file mode 100644 index 58a219f0e8..0000000000 --- a/spec/fabricators/app_fabricator.rb +++ /dev/null @@ -1,21 +0,0 @@ -Fabricator(:app) do - name { sequence(:app_name) { |n| "App ##{n}" } } - repository_branch 'master' -end - -Fabricator(:app_with_watcher, from: :app) do - watchers(count: 1) do |parent, _i| - Fabricate.build(:watcher, app: parent) - end -end - -Fabricator(:watcher) do - app - watcher_type 'email' - email { sequence(:email) { |n| "email#{n}@example.com" } } -end - -Fabricator(:user_watcher, from: :watcher) do - user - watcher_type 'user' -end diff --git a/spec/fabricators/backtrace_fabricator.rb b/spec/fabricators/backtrace_fabricator.rb deleted file mode 100644 index ea9358a56e..0000000000 --- a/spec/fabricators/backtrace_fabricator.rb +++ /dev/null @@ -1,9 +0,0 @@ -Fabricator :backtrace do - lines(count: 99) do - { - number: rand(999), - file: "/path/to/file/#{SecureRandom.hex(4)}.rb", - method: ActiveSupport.methods.sample - } - end -end diff --git a/spec/fabricators/comment_fabricator.rb b/spec/fabricators/comment_fabricator.rb deleted file mode 100644 index ac6e1f9963..0000000000 --- a/spec/fabricators/comment_fabricator.rb +++ /dev/null @@ -1,5 +0,0 @@ -Fabricator :comment do - user - body 'Test comment' - err(fabricator: :problem) -end diff --git a/spec/fabricators/err_fabricator.rb b/spec/fabricators/err_fabricator.rb deleted file mode 100644 index 103557c03d..0000000000 --- a/spec/fabricators/err_fabricator.rb +++ /dev/null @@ -1,4 +0,0 @@ -Fabricator :err do - problem - fingerprint 'some-finger-print' -end diff --git a/spec/fabricators/issue_tracker_fabricator.rb b/spec/fabricators/issue_tracker_fabricator.rb deleted file mode 100644 index 1d5e664125..0000000000 --- a/spec/fabricators/issue_tracker_fabricator.rb +++ /dev/null @@ -1,10 +0,0 @@ -Fabricator :issue_tracker do - type_tracker 'mock' - options do - { - foo: 'one', - bar: 'two' - } - end - app -end diff --git a/spec/fabricators/notice_fabricator.rb b/spec/fabricators/notice_fabricator.rb deleted file mode 100644 index 31880182af..0000000000 --- a/spec/fabricators/notice_fabricator.rb +++ /dev/null @@ -1,15 +0,0 @@ -Fabricator :notice do - app - err - error_class 'FooError' - message 'FooError: Too Much Bar' - backtrace - server_environment { { 'environment-name' => 'production' } } - request { { 'component' => 'foo', 'action' => 'bar' } } - notifier { { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' } } - - after_create do - Problem.cache_notice(err.problem_id, self) - problem.reload - end -end diff --git a/spec/fabricators/notification_service_fabricator.rb b/spec/fabricators/notification_service_fabricator.rb deleted file mode 100644 index 403611f2cc..0000000000 --- a/spec/fabricators/notification_service_fabricator.rb +++ /dev/null @@ -1,26 +0,0 @@ -Fabricator :notification_service do - app - room_id { sequence :word } - api_token { sequence :word } - subdomain { sequence :word } - notify_at_notices { sequence { |_a| [0] } } -end - -Fabricator :gtalk_notification_service, from: :notification_service, class_name: "NotificationServices::GtalkService" do - user_id { sequence :word } - service_url { sequence :word } - service { sequence :word } -end - -Fabricator :slack_notification_service, from: :notification_service, class_name: "NotificationServices::SlackService" do - service_url { sequence :word } - room_id { sequence(:room_id) { |i| "#room-#{i}" } } -end - -Fabricator :hipchat_notification_service, from: :notification_service, class_name: "NotificationServices::HipchatService" do - service { 'v2' } -end - -%w(campfire flowdock hoiio hubot pushover webhook).each do |t| - Fabricator "#{t}_notification_service".to_sym, from: :notification_service, class_name: "NotificationServices::#{t.camelcase}Service" -end diff --git a/spec/fabricators/problem_fabricator.rb b/spec/fabricators/problem_fabricator.rb deleted file mode 100644 index 0519ca752b..0000000000 --- a/spec/fabricators/problem_fabricator.rb +++ /dev/null @@ -1,29 +0,0 @@ -Fabricator(:problem) do - app { Fabricate(:app) } - comments { [] } - error_class 'FooError' - environment 'production' -end - -Fabricator(:problem_with_comments, from: :problem) do - after_create do |parent| - 3.times do - Fabricate(:comment, err: parent) - end - end -end - -Fabricator(:problem_with_errs, from: :problem) do - after_create do |parent| - 3.times do - Fabricate(:err, problem: parent) - end - end -end - -Fabricator(:problem_resolved, from: :problem) do - after_create do |pr| - Fabricate(:notice, err: Fabricate(:err, problem: pr)) - pr.resolve! - end -end diff --git a/spec/fabricators/sequences_fabricator.rb b/spec/fabricators/sequences_fabricator.rb deleted file mode 100644 index 233e7f2540..0000000000 --- a/spec/fabricators/sequences_fabricator.rb +++ /dev/null @@ -1,2 +0,0 @@ -Fabricate.sequence(:name) { |n| "John #{n} Doe" } -Fabricate.sequence(:word) { |n| "word#{n}" } diff --git a/spec/fabricators/user_fabricator.rb b/spec/fabricators/user_fabricator.rb deleted file mode 100644 index eb5fab6f84..0000000000 --- a/spec/fabricators/user_fabricator.rb +++ /dev/null @@ -1,10 +0,0 @@ -Fabricator :user do - name 'Clyde Frog' - email { sequence(:user_email) { |n| "user.#{n}@example.com" } } - password 'password' - password_confirmation 'password' -end - -Fabricator :admin, from: :user do - admin true -end diff --git a/spec/factories/app_factory.rb b/spec/factories/app_factory.rb new file mode 100644 index 0000000000..4dd9c44b46 --- /dev/null +++ b/spec/factories/app_factory.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :app do + sequence(:name) { |n| "App ##{n}" } + + repository_branch { ["main", "master"].sample } + end + + factory :app_with_watcher, parent: :app do + after(:build) do |app| + build(:watcher, app: app) + end + end +end diff --git a/spec/factories/backtrace_factory.rb b/spec/factories/backtrace_factory.rb new file mode 100644 index 0000000000..bbcd1af5a3 --- /dev/null +++ b/spec/factories/backtrace_factory.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :backtrace do + lines do + 99.times.map do + { + number: rand(999), + file: "/path/to/file/#{SecureRandom.hex(4)}.rb", + method: ActiveSupport.methods.sample + } + end + end + end +end diff --git a/spec/factories/comment_factory.rb b/spec/factories/comment_factory.rb new file mode 100644 index 0000000000..32aa25bb99 --- /dev/null +++ b/spec/factories/comment_factory.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :comment do + user + + body { "Test comment" } + + err factory: :problem + end +end diff --git a/spec/factories/err_factory.rb b/spec/factories/err_factory.rb new file mode 100644 index 0000000000..f4f10e6678 --- /dev/null +++ b/spec/factories/err_factory.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :err do + problem + + fingerprint { "some-finger-print" } + end +end diff --git a/spec/factories/issue_tracker_factory.rb b/spec/factories/issue_tracker_factory.rb new file mode 100644 index 0000000000..bbf05804e5 --- /dev/null +++ b/spec/factories/issue_tracker_factory.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :issue_tracker do + app + + type_tracker { "mock" } + + options { {foo: "one", bar: "two"} } + end +end diff --git a/spec/factories/notice_factory.rb b/spec/factories/notice_factory.rb new file mode 100644 index 0000000000..d21aa53b2d --- /dev/null +++ b/spec/factories/notice_factory.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :notice do + app + + err + + error_class { "FooError" } + + message { "FooError: Too Much Bar" } + + backtrace + + server_environment { {"environment-name" => "production"} } + + request { {"component" => "foo", "action" => "bar"} } + + notifier { {"name" => "Notifier", "version" => "1", "url" => "http://toad.com"} } + + after(:create) do |notice| + Problem.cache_notice(notice.err.problem_id, notice) + + notice.problem.reload + end + end +end diff --git a/spec/factories/notification_service_factory.rb b/spec/factories/notification_service_factory.rb new file mode 100644 index 0000000000..c315f53760 --- /dev/null +++ b/spec/factories/notification_service_factory.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :notification_service do + app + + sequence(:room_id) { |n| "word#{n}" } + + sequence(:api_token) { |n| "word#{n}" } + + sequence(:subdomain) { |n| "word#{n}" } + end + + factory :gtalk_notification_service, parent: :notification_service, class: "NotificationServices::GtalkService" do + sequence(:user_id) { |n| "word#{n}" } + + sequence(:service_url) { |n| "word#{n}" } + + sequence(:service) { |n| "word#{n}" } + end + + factory :slack_notification_service, parent: :notification_service, class: "NotificationServices::SlackService" do + sequence(:service_url) { |n| "word#{n}" } + + sequence(:room_id) { |i| "#room-#{i}" } + end + + factory :campfire_notification_service, parent: :notification_service, class: "NotificationServices::CampfireService" + + factory :hoiio_notification_service, parent: :notification_service, class: "NotificationServices::HoiioService" + + factory :hubot_notification_service, parent: :notification_service, class: "NotificationServices::HubotService" + + factory :pushover_notification_service, parent: :notification_service, class: "NotificationServices::PushoverService" + + factory :webhook_notification_service, parent: :notification_service, class: "NotificationServices::WebhookService" +end diff --git a/spec/factories/problem_factory.rb b/spec/factories/problem_factory.rb new file mode 100644 index 0000000000..15224d77ae --- /dev/null +++ b/spec/factories/problem_factory.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :problem do + app + + comments { [] } + + error_class { "FooError" } + + environment { "production" } + end + + factory :problem_with_comments, parent: :problem do + after(:create) do |problem| + create_list(:comment, 3, err: problem) + end + end + + factory :problem_with_errs, parent: :problem do + after(:create) do |problem| + create_list(:err, 3, problem: problem) + end + end + + factory :problem_resolved, parent: :problem do + after(:create) do |problem| + err = create(:err, problem: problem) + + create(:notice, err: err) + + problem.resolve! + end + end +end diff --git a/spec/factories/user_factory.rb b/spec/factories/user_factory.rb new file mode 100644 index 0000000000..d36c2f5c36 --- /dev/null +++ b/spec/factories/user_factory.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :user do + email { Faker::Internet.unique.email } + + name { Faker::Name.unique.name } + + password { Faker::Internet.password } + end +end diff --git a/spec/factories/watcher_factory.rb b/spec/factories/watcher_factory.rb new file mode 100644 index 0000000000..3b40586017 --- /dev/null +++ b/spec/factories/watcher_factory.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :watcher do + app + + watcher_type { "email" } + + sequence(:email) { |n| "email#{n}@example.com" } + end + + factory :user_watcher, parent: :watcher do + user + + watcher_type { "user" } + end +end diff --git a/spec/fixtures/api_v3_request_without_backtrace.json b/spec/fixtures/api_v3_request_without_backtrace.json index 1ede3ad990..6856d8564c 100644 --- a/spec/fixtures/api_v3_request_without_backtrace.json +++ b/spec/fixtures/api_v3_request_without_backtrace.json @@ -12,7 +12,7 @@ "sourceMapEnabled":true, "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.99 Safari/537.36", "url":"http://localhost:3000/kontakt", - "userId":1,"userUsername":"john", + "userId":1, "userName":"John Doe", "userUsername": "john", "userEmail":"john.doe@example.org", diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 9550b0b119..3b378162cd 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -1,7 +1,12 @@ -describe ApplicationHelper do - let(:notice) { Fabricate(:notice) } +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ApplicationHelper, type: :helper do + let(:notice) { create(:notice) } + describe "#generate_problem_ical" do - it 'return the ical format' do + it "return the ical format" do helper.generate_problem_ical([notice]) end end diff --git a/spec/helpers/hash_helper_spec.rb b/spec/helpers/hash_helper_spec.rb new file mode 100644 index 0000000000..a552329720 --- /dev/null +++ b/spec/helpers/hash_helper_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe HashHelper, type: :helper do + describe ".pretty_hash" do + let(:expected_pretty_hash) do + <<~HASH.chomp + { + "action" => "some_action", + "controller" => "some_controller" + } + HASH + end + + it "is expected to prettify hashes" do + expect(pretty_hash("controller" => "some_controller", "action" => "some_action")).to eq expected_pretty_hash + end + + it "is expected to handle empty hashes" do + expect(pretty_hash({})).to eq "{}" + end + + context "with string, instead of hash" do + it "is expected not to raise exception" do + expect { pretty_hash("This is a string.") }.not_to raise_exception + end + + it "is expected to handle strings" do + expect(pretty_hash('{ a_string: "But it looks like a hash"}')).to eq "{}" + end + end + end +end diff --git a/spec/helpers/navigation_helper_spec.rb b/spec/helpers/navigation_helper_spec.rb index 716ec165ac..cee02f3b6f 100644 --- a/spec/helpers/navigation_helper_spec.rb +++ b/spec/helpers/navigation_helper_spec.rb @@ -1,13 +1,17 @@ -describe NavigationHelper do - describe '#page_count_from_end' do - it 'returns the page number when counting from the last occurrence of a notice' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe NavigationHelper, type: :helper do + describe "#page_count_from_end" do + it "returns the page number when counting from the last occurrence of a notice" do expect(page_count_from_end(1, 6)).to eq 6 expect(page_count_from_end(6, 6)).to eq 1 expect(page_count_from_end(2, 6)).to eq 5 end - it 'properly handles strings for input' do - expect(page_count_from_end('2', '6')).to eq 5 + it "properly handles strings for input" do + expect(page_count_from_end("2", "6")).to eq 5 end end end diff --git a/spec/helpers/problems_helper_spec.rb b/spec/helpers/problems_helper_spec.rb index 9edc91a3c8..7b86bcefb6 100644 --- a/spec/helpers/problems_helper_spec.rb +++ b/spec/helpers/problems_helper_spec.rb @@ -1,13 +1,29 @@ -describe ProblemsHelper do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ProblemsHelper, type: :helper do + describe "#auto_link_format" do + it "handles links with target and wraps paragraph" do + expect( + helper.auto_link_format("Goto https://errbit.com/ and say hello to team@errbit.invalid") + ).to eq("

Goto https://errbit.com/ and say hello to team@errbit.invalid

") + end + + it "sanitizes body of html tags" do + expect(helper.auto_link_format("Hello, World!")).to eq("

Hello, World!

") + end + end + describe "#gravatar_tag" do let(:email) { "gravatar@example.com" } - let(:email_hash) { Digest::MD5.hexdigest email } - let(:base_url) { "http://www.gravatar.com/avatar/#{email_hash}" } + let(:email_hash) { Digest::MD5.hexdigest(email) } + let(:base_url) { "https://secure.gravatar.com/avatar/#{email_hash}" } context "default config" do before do allow(Errbit::Config).to receive(:use_gravatar).and_return(true) - allow(Errbit::Config).to receive(:gravatar_default).and_return('identicon') + allow(Errbit::Config).to receive(:gravatar_default).and_return("identicon") end it "should render image_tag with correct alt and src" do @@ -17,13 +33,13 @@ it "should override :d" do expected = "\"#{email}\"" - expect(helper.gravatar_tag(email, d: 'retro', s: 48)).to eq(expected) + expect(helper.gravatar_tag(email, d: "retro", s: 48)).to eq(expected) end end context "no email" do it "should not render the tag" do - expect(helper.gravatar_tag(nil)).to be_nil + expect(helper.gravatar_tag(nil)).to eq(nil) end end end @@ -33,25 +49,15 @@ let(:email) { nil } it "should return nil" do - expect(helper.gravatar_url(email)).to be_nil - end - end - - context "without ssl" do - let(:email) { "gravatar@example.com" } - let(:email_hash) { Digest::MD5.hexdigest email } - - it "should return the http url" do - expect(helper.gravatar_url(email)).to eq("http://www.gravatar.com/avatar/#{email_hash}?d=identicon") + expect(helper.gravatar_url(email)).to eq(nil) end end - context "with ssl" do + context "with email" do let(:email) { "gravatar@example.com" } - let(:email_hash) { Digest::MD5.hexdigest email } + let(:email_hash) { Digest::MD5.hexdigest(email) } - it "should return the http url" do - allow(controller.request).to receive(:ssl?).and_return(true) + it "should return the https url" do expect(helper.gravatar_url(email)).to eq("https://secure.gravatar.com/avatar/#{email_hash}?d=identicon") end end diff --git a/spec/initializers/action_mailer_spec.rb b/spec/initializers/action_mailer_spec.rb index 9472e5e32e..51d2d8f494 100644 --- a/spec/initializers/action_mailer_spec.rb +++ b/spec/initializers/action_mailer_spec.rb @@ -1,50 +1,54 @@ -describe 'initializers/action_mailer' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "initializers/action_mailer" do def load_initializer - load File.join(Rails.root, 'config', 'initializers', 'action_mailer.rb') + load File.join(Rails.root, "config", "initializers", "action_mailer.rb") end after do ActionMailer::Base.delivery_method = :test end - describe 'delivery method' do - it 'sets the delivery method to :smtp' do + describe "delivery method" do + it "sets the delivery method to :smtp" do allow(Errbit::Config).to receive(:email_delivery_method).and_return(:smtp) load_initializer - expect(ActionMailer::Base.delivery_method).to be(:smtp) + expect(ActionMailer::Base.delivery_method).to eq(:smtp) end - it 'sets the delivery method to :sendmail' do + it "sets the delivery method to :sendmail" do allow(Errbit::Config).to receive(:email_delivery_method).and_return(:sendmail) load_initializer - expect(ActionMailer::Base.delivery_method).to be(:sendmail) + expect(ActionMailer::Base.delivery_method).to eq(:sendmail) end end - describe 'smtp settings' do - it 'lets smtp settings be set' do + describe "smtp settings" do + it "lets smtp settings be set" do allow(Errbit::Config).to receive(:email_delivery_method).and_return(:smtp) - allow(Errbit::Config).to receive(:smtp_address).and_return('smtp.somedomain.com') + allow(Errbit::Config).to receive(:smtp_address).and_return("smtp.somedomain.com") allow(Errbit::Config).to receive(:smtp_port).and_return(998) allow(Errbit::Config).to receive(:smtp_authentication).and_return(:login) - allow(Errbit::Config).to receive(:smtp_user_name).and_return('my-username') - allow(Errbit::Config).to receive(:smtp_password).and_return('my-password') - allow(Errbit::Config).to receive(:smtp_domain).and_return('someotherdomain.com') + allow(Errbit::Config).to receive(:smtp_user_name).and_return("my-username") + allow(Errbit::Config).to receive(:smtp_password).and_return("my-password") + allow(Errbit::Config).to receive(:smtp_domain).and_return("someotherdomain.com") allow(Errbit::Config).to receive(:smtp_enable_starttls_auto).and_return(true) - allow(Errbit::Config).to receive(:smtp_openssl_verify_mode).and_return('peer') + allow(Errbit::Config).to receive(:smtp_openssl_verify_mode).and_return("peer") load_initializer expect(ActionMailer::Base.smtp_settings).to eq( - address: 'smtp.somedomain.com', - port: 998, - authentication: :login, - user_name: 'my-username', - password: 'my-password', - domain: 'someotherdomain.com', + address: "smtp.somedomain.com", + port: 998, + authentication: :login, + user_name: "my-username", + password: "my-password", + domain: "someotherdomain.com", enable_starttls_auto: true, - openssl_verify_mode: 'peer' + openssl_verify_mode: "peer" ) end end diff --git a/spec/initializers/devise_spec.rb b/spec/initializers/devise_spec.rb index 5c51dfc339..735eed7cc1 100644 --- a/spec/initializers/devise_spec.rb +++ b/spec/initializers/devise_spec.rb @@ -1,6 +1,10 @@ -describe 'initializers/devise' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Devise do def load_initializer - load File.join(Rails.root, 'config', 'initializers', 'devise.rb') + load File.join(Rails.root, "config", "initializers", "devise.rb") end after do @@ -8,29 +12,31 @@ def load_initializer load_initializer end - describe 'omniauth github' do - it 'sets the client options correctly for the default github_url' do + describe "omniauth github" do + it "sets the client options correctly for the default github_url" do load_initializer options = Devise.omniauth_configs[:github].options expect(options).to have_key(:client_options) expect(options[:client_options]).to eq( - site: 'https://api.github.com', - authorize_url: 'https://github.com/login/oauth/authorize', - token_url: 'https://github.com/login/oauth/access_token') + site: "https://api.github.com", + authorize_url: "https://github.com/login/oauth/authorize", + token_url: "https://github.com/login/oauth/access_token" + ) end - it 'sets the client options correctly for the a GitHub Enterprise github_url' do - allow(Errbit::Config).to receive(:github_url).and_return('https://github.example.com') - allow(Errbit::Config).to receive(:github_api_url).and_return('https://github.example.com/api/v3') + it "sets the client options correctly for the a GitHub Enterprise github_url" do + allow(Errbit::Config).to receive(:github_url).and_return("https://github.example.com") + allow(Errbit::Config).to receive(:github_api_url).and_return("https://github.example.com/api/v3") load_initializer options = Devise.omniauth_configs[:github].options expect(options).to have_key(:client_options) expect(options[:client_options]).to eq( - site: 'https://github.example.com/api/v3', - authorize_url: 'https://github.example.com/login/oauth/authorize', - token_url: 'https://github.example.com/login/oauth/access_token') + site: "https://github.example.com/api/v3", + authorize_url: "https://github.example.com/login/oauth/authorize", + token_url: "https://github.example.com/login/oauth/access_token" + ) end end end diff --git a/spec/interactors/notice_refingerprinter_spec.rb b/spec/interactors/notice_refingerprinter_spec.rb index afbec40bba..9f33362e57 100644 --- a/spec/interactors/notice_refingerprinter_spec.rb +++ b/spec/interactors/notice_refingerprinter_spec.rb @@ -1,51 +1,57 @@ -describe NoticeRefingerprinter do - let(:app) { Fabricate(:app) } - let(:backtrace) do - Fabricate(:backtrace) - end +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe NoticeRefingerprinter do + let(:app) { create(:app) } + + let(:backtrace) { create(:backtrace) } before do notices end - context 'identical backtraces' do + context "identical backtraces" do let(:notices) do 5.times.map do - notice = Fabricate(:notice, backtrace: backtrace, app: app) + notice = create(:notice, backtrace: backtrace, app: app) notice.save! notice end end - it 'has only one err' do + it "has only one err" do described_class.run + expect(Err.count).to eq(1) end end - context 'minor backtrace differences' do + context "minor backtrace differences" do let(:notices) do line_numbers = [1, 1, 2, 2, 3] 5.times.map do b = backtrace.clone b.lines[5][:number] = line_numbers.shift b.save! - notice = Fabricate(:notice, backtrace: b, app: app) + notice = create(:notice, backtrace: b, app: app) notice.save! end end - it 'has three errs with default fingerprinter' do + it "has three errs with default fingerprinter" do described_class.run + expect(Err.count).to eq(3) end - it 'has one err when limiting backtrace line count' do + it "has one err when limiting backtrace line count" do fingerprinter = app.notice_fingerprinter fingerprinter.backtrace_lines = 4 fingerprinter.save! described_class.run + expect(Err.count).to eq(1) end end diff --git a/spec/interactors/outdated_problem_clearer_spec.rb b/spec/interactors/outdated_problem_clearer_spec.rb index ab7be2ec5e..33a6fa2407 100644 --- a/spec/interactors/outdated_problem_clearer_spec.rb +++ b/spec/interactors/outdated_problem_clearer_spec.rb @@ -1,55 +1,50 @@ -describe OutdatedProblemClearer do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe OutdatedProblemClearer do before do allow(Errbit::Config).to receive(:notice_deprecation_days).and_return(7) end - let(:outdated_problem_clearer) do - OutdatedProblemClearer.new - end describe "#execute" do - let!(:problems) do - [ - Fabricate(:problem), - Fabricate(:problem), - Fabricate(:problem) - ] - end - context 'without old problems' do - it 'do nothing' do + let!(:problems) { create_list(:problem, 3) } + + context "without old problems" do + it "do nothing" do expect do - expect(outdated_problem_clearer.execute).to eq 0 - end.to_not change { - Problem.count - } + expect(subject.execute).to eq(0) + end.not_to change(Problem, :count) end - it 'not repair database' do + + it "not compact database" do allow(Mongoid.default_client).to receive(:command).and_call_original - expect(Mongoid.default_client).to_not receive(:command).with(repairDatabase: 1) - outdated_problem_clearer.execute + expect(Mongoid.default_client).not_to receive(:command).with(compact: an_instance_of(String)) + subject.execute end end context "with old problems" do before do allow(Mongoid.default_client).to receive(:command).and_call_original - allow(Mongoid.default_client).to receive(:command).with(repairDatabase: 1) + allow(Mongoid.default_client).to receive(:command).with(compact: an_instance_of(String)).at_least(1) problems.first.update(last_notice_at: Time.zone.at(946_684_800.0)) problems.second.update(last_notice_at: Time.zone.at(946_684_800.0)) end - it 'deletes old problems' do + it "deletes old problems" do expect do - expect(outdated_problem_clearer.execute).to eq 2 - end.to change { - Problem.count - }.by(-2) - expect(Problem.where(_id: problems.first.id).first).to be_nil - expect(Problem.where(_id: problems.second.id).first).to be_nil + expect(subject.execute).to eq(2) + end.to change(Problem, :count).by(-2) + + expect(Problem.where(_id: problems.first.id).first).to eq(nil) + expect(Problem.where(_id: problems.second.id).first).to eq(nil) end - it 'repair database' do - expect(Mongoid.default_client).to receive(:command).with(repairDatabase: 1) - outdated_problem_clearer.execute + it "compact database" do + expect(Mongoid.default_client).to receive(:command).with(compact: an_instance_of(String)).at_least(1) + + subject.execute end end end diff --git a/spec/interactors/problem_destroy_spec.rb b/spec/interactors/problem_destroy_spec.rb index 9cf52479d8..8936f9bf54 100644 --- a/spec/interactors/problem_destroy_spec.rb +++ b/spec/interactors/problem_destroy_spec.rb @@ -1,4 +1,8 @@ -describe ProblemDestroy do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ProblemDestroy do let(:problem_destroy) do ProblemDestroy.new(problem) end @@ -18,46 +22,46 @@ let(:comment_2) { Comment.new } describe "#initialize" do - it 'take a problem like args' do + it "take a problem like args" do expect(problem_destroy.problem).to eq problem end end describe "#execute" do - it 'destroy the problem himself' do + it "destroy the problem himself" do expect(problem).to receive(:delete).and_return(true) problem_destroy.execute end - it 'delete all errs associate' do - expect(Err).to receive(:delete_all).with(_id: { '$in' => [err_1.id, err_2.id] }) + it "delete all errs associate" do + expect(Err).to receive(:delete_all).with(_id: {"$in" => [err_1.id, err_2.id]}) problem_destroy.execute end - it 'delete all comments associate' do - expect(Comment).to receive(:delete_all).with(_id: { '$in' => [comment_1.id, comment_2.id] }) + it "delete all comments associate" do + expect(Comment).to receive(:delete_all).with(_id: {"$in" => [comment_1.id, comment_2.id]}) problem_destroy.execute end - it 'delete all notice of associate to this errs' do - expect(Notice).to receive(:delete_all).with(err_id: { '$in' => [err_1.id, err_2.id] }) + it "delete all notice of associate to this errs" do + expect(Notice).to receive(:delete_all).with(err_id: {"$in" => [err_1.id, err_2.id]}) problem_destroy.execute end end end context "in integration way" do - let!(:problem) { Fabricate(:problem) } - let!(:comment_1) { Fabricate(:comment, err: problem) } - let!(:comment_2) { Fabricate(:comment, err: problem) } - let!(:err_1) { Fabricate(:err, problem: problem) } - let!(:err_2) { Fabricate(:err, problem: problem) } - let!(:notice_1_1) { Fabricate(:notice, err: err_1) } - let!(:notice_1_2) { Fabricate(:notice, err: err_1) } - let!(:notice_2_1) { Fabricate(:notice, err: err_2) } - let!(:notice_2_2) { Fabricate(:notice, err: err_2) } + let!(:problem) { create(:problem) } + let!(:comment_1) { create(:comment, err: problem) } + let!(:comment_2) { create(:comment, err: problem) } + let!(:err_1) { create(:err, problem: problem) } + let!(:err_2) { create(:err, problem: problem) } + let!(:notice_1_1) { create(:notice, err: err_1) } + let!(:notice_1_2) { create(:notice, err: err_1) } + let!(:notice_2_1) { create(:notice, err: err_2) } + let!(:notice_2_2) { create(:notice, err: err_2) } - it 'should all destroy' do + it "should all destroy" do problem_destroy.execute expect(Problem.where(_id: problem.id).entries).to be_empty expect(Err.where(_id: err_1.id).entries).to be_empty diff --git a/spec/interactors/problem_merge_spec.rb b/spec/interactors/problem_merge_spec.rb index 1bf42efc4b..3386499bb6 100644 --- a/spec/interactors/problem_merge_spec.rb +++ b/spec/interactors/problem_merge_spec.rb @@ -1,21 +1,27 @@ -describe ProblemMerge do - let(:problem) { Fabricate(:problem_with_errs) } - let(:problem_1) { Fabricate(:problem_with_errs) } +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ProblemMerge do + let(:problem) { create(:problem_with_errs) } + + let(:problem_1) { create(:problem_with_errs) } describe "#initialize" do - it 'failed if less than 2 uniq problem pass in args' do + it "failed if less than 2 uniq problem pass in args" do expect do ProblemMerge.new(problem) end.to raise_error(ArgumentError) end - it 'extract first problem like merged_problem' do + it "extract first problem like merged_problem" do problem_merge = ProblemMerge.new(problem, problem, problem_1) - expect(problem_merge.merged_problem).to eql problem + expect(problem_merge.merged_problem).to eq(problem) end - it 'extract other problem like child_problems' do + + it "extract other problem like child_problems" do problem_merge = ProblemMerge.new(problem, problem, problem_1) - expect(problem_merge.child_problems).to eql [problem_1] + expect(problem_merge.child_problems).to eq([problem_1]) end end @@ -23,44 +29,50 @@ let!(:problem_merge) do ProblemMerge.new(problem, problem_1) end + let(:first_errs) { problem.errs } + let(:merged_errs) { problem_1.errs } - let!(:notice) { Fabricate(:notice, err: first_errs.first) } - let!(:notice_1) { Fabricate(:notice, err: merged_errs.first) } - it 'delete one of problem' do + let!(:notice) { create(:notice, err: first_errs.first) } + + let!(:notice_1) { create(:notice, err: merged_errs.first) } + + it "delete one of problem" do expect do problem_merge.merge end.to change(Problem, :count).by(-1) end - it 'move all err in one problem' do + it "move all err in one problem" do problem_merge.merge expect(problem.reload.errs.map(&:id).sort).to eq((first_errs | merged_errs).map(&:id).sort) end - it 'keeps the issue link' do - problem.update_attributes(issue_link: 'http://foo.com', issue_type: 'mock') + it "keeps the issue link" do + problem.update_attributes(issue_link: "http://foo.com", issue_type: "mock") problem_merge.merge - expect(problem.reload.issue_link).to eq 'http://foo.com' - expect(problem.reload.issue_type).to eq 'mock' + expect(problem.reload.issue_link).to eq("http://foo.com") + expect(problem.reload.issue_type).to eq("mock") end - it 'update problem cache' do + it "update problem cache" do expect(problem).to receive(:recache) problem_merge.merge end context "with problem with comment" do - let!(:comment) { Fabricate(:comment, err: problem) } - let!(:comment_2) { Fabricate(:comment, err: problem_1, user: comment.user) } - it 'merge comment' do + let!(:comment) { create(:comment, err: problem) } + + let!(:comment_2) { create(:comment, err: problem_1, user: comment.user) } + + it "merge comment" do expect do problem_merge.merge end.to change { problem.comments.size }.from(1).to(2) - expect(comment_2.reload.err).to eq problem + expect(comment_2.reload.err).to eq(problem) end end end diff --git a/spec/interactors/problem_recacher_spec.rb b/spec/interactors/problem_recacher_spec.rb index 8c364fc679..d1e861aaf0 100644 --- a/spec/interactors/problem_recacher_spec.rb +++ b/spec/interactors/problem_recacher_spec.rb @@ -1,35 +1,40 @@ -describe ProblemRecacher do - let(:app) { Fabricate(:app) } - let(:backtrace) do - Fabricate(:backtrace) - end +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ProblemRecacher do + let(:app) { create(:app) } + + let(:backtrace) { create(:backtrace) } before do notices NoticeRefingerprinter.run + described_class.run end - context 'minor backtrace differences' do + context "minor backtrace differences" do let(:notices) do line_numbers = [1, 1, 2, 2, 3] 5.times.map do b = backtrace.clone b.lines[5][:number] = line_numbers.shift b.save! - notice = Fabricate(:notice, backtrace: b, app: app) + notice = create(:notice, backtrace: b, app: app) notice.save! notice end end - it 'has three problems for the five notices' do + it "has three problems for the five notices" do expect(Notice.count).to eq(5) + expect(Problem.count).to eq(3) end - it 'the problems have the right cached attributes' do + it "the problems have the right cached attributes" do problem = notices.first.reload.problem expect(problem.notices_count).to eq(2) diff --git a/spec/interactors/resolved_problem_clearer_spec.rb b/spec/interactors/resolved_problem_clearer_spec.rb index dca7c7c247..e02ba2f7a5 100644 --- a/spec/interactors/resolved_problem_clearer_spec.rb +++ b/spec/interactors/resolved_problem_clearer_spec.rb @@ -1,51 +1,46 @@ -describe ResolvedProblemClearer do - let(:resolved_problem_clearer) do - ResolvedProblemClearer.new - end +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ResolvedProblemClearer do describe "#execute" do - let!(:problems) do - [ - Fabricate(:problem), - Fabricate(:problem), - Fabricate(:problem) - ] - end - context 'without problem resolved' do - it 'do nothing' do + let!(:problems) { create_list(:problem, 3) } + + context "without problem resolved" do + it "do nothing" do expect do - expect(resolved_problem_clearer.execute).to eq 0 - end.to_not change { - Problem.count - } + expect(subject.execute).to eq(0) + end.not_to change(Problem, :count) end - it 'not repair database' do + + it "not compact database" do allow(Mongoid.default_client).to receive(:command).and_call_original - expect(Mongoid.default_client).to_not receive(:command).with(repairDatabase: 1) - resolved_problem_clearer.execute + expect(Mongoid.default_client).not_to receive(:command).with(compact: an_instance_of(String)) + subject.execute end end context "with problem resolve" do before do allow(Mongoid.default_client).to receive(:command).and_call_original - allow(Mongoid.default_client).to receive(:command).with(repairDatabase: 1) + allow(Mongoid.default_client).to receive(:command).with(compact: an_instance_of(String)).at_least(1) problems.first.resolve! problems.second.resolve! end - it 'delete problem resolve' do + it "delete problem resolve" do expect do - expect(resolved_problem_clearer.execute).to eq 2 - end.to change { - Problem.count - }.by(-2) - expect(Problem.where(_id: problems.first.id).first).to be_nil - expect(Problem.where(_id: problems.second.id).first).to be_nil + expect(subject.execute).to eq(2) + end.to change(Problem, :count).by(-2) + + expect(Problem.where(_id: problems.first.id).first).to eq(nil) + expect(Problem.where(_id: problems.second.id).first).to eq(nil) end - it 'repair database' do - expect(Mongoid.default_client).to receive(:command).with(repairDatabase: 1) - resolved_problem_clearer.execute + it "compact database" do + expect(Mongoid.default_client).to receive(:command).with(compact: an_instance_of(String)).at_least(1) + + subject.execute end end end diff --git a/spec/interactors/user_destroy_spec.rb b/spec/interactors/user_destroy_spec.rb index 20f9450323..1701bb003e 100644 --- a/spec/interactors/user_destroy_spec.rb +++ b/spec/interactors/user_destroy_spec.rb @@ -1,25 +1,24 @@ -describe UserDestroy do - let(:app) do - Fabricate( - :app, - watchers: [ - Fabricate.build(:user_watcher, user: user) - ]) - end +# frozen_string_literal: true + +require "rails_helper" +RSpec.describe UserDestroy do describe "#destroy" do - let!(:user) { Fabricate(:user) } - it 'should delete user' do + let!(:app) { create(:app) } + let!(:user) { create(:user) } + let!(:user_watcher) { create(:user_watcher, app: app, user: user) } + + it "is expected to delete user" do expect do - UserDestroy.new(user).destroy - end.to change(User, :count) + described_class.new(user).destroy + end.to change(User, :count).from(1).to(0) end - it 'should delete watcher' do + it "is expected to delete watcher" do expect do - UserDestroy.new(user).destroy + described_class.new(user).destroy end.to change { - app.reload.watchers.where(user_id: user.id).count + app.reload.watchers.where(user: user).count }.from(1).to(0) end end diff --git a/spec/jobs/application_job_spec.rb b/spec/jobs/application_job_spec.rb new file mode 100644 index 0000000000..8fa073aa39 --- /dev/null +++ b/spec/jobs/application_job_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ApplicationJob, type: :job do + it { expect(subject).to be_an(ActiveJob::Base) } +end diff --git a/spec/jobs/destroy_problems_by_app_job_spec.rb b/spec/jobs/destroy_problems_by_app_job_spec.rb index 1585233844..871d62a1a2 100644 --- a/spec/jobs/destroy_problems_by_app_job_spec.rb +++ b/spec/jobs/destroy_problems_by_app_job_spec.rb @@ -1,33 +1,28 @@ +# frozen_string_literal: true + +require "rails_helper" + RSpec.describe DestroyProblemsByAppJob, type: :job do - before do - @app = Fabricate(:app) - @problem1 = Fabricate(:problem, app: @app) - @problem2 = Fabricate(:problem, app: @app) - end + it { expect(subject).to be_an(ApplicationJob) } it "destroys all problems" do - expect do - DestroyProblemsByAppJob.perform_later(@app.id) - end.to change(Problem, :count).by(-2) - expect(@app.problems.count).to eq 0 - end + app = create(:app) + create(:problem, app: app) - it "destroys all problems, even with a large amount of them" do - app2 = Fabricate(:app) - 500.times do - Fabricate(:problem, app: app2) - end expect do - DestroyProblemsByAppJob.perform_later(app2.id) - end.to change(Problem, :count).by(-500) - expect(app2.problems.count).to eq 0 + described_class.perform_later(app.id) + end.to change(Problem, :count).by(-1) + + expect(app.problems.count).to eq(0) end it "should work with a fresh new application" do - app2 = Fabricate(:app) + app = create(:app) + expect do - DestroyProblemsByAppJob.perform_later(app2.id) - end.to change(Problem, :count).by(0) - expect(app2.problems.count).to eq 0 + described_class.perform_later(app.id) + end.not_to change(Problem, :count) + + expect(app.problems.count).to eq(0) end end diff --git a/spec/jobs/destroy_problems_by_id_job_spec.rb b/spec/jobs/destroy_problems_by_id_job_spec.rb index 012d7174fd..5d84006bc7 100644 --- a/spec/jobs/destroy_problems_by_id_job_spec.rb +++ b/spec/jobs/destroy_problems_by_id_job_spec.rb @@ -1,37 +1,28 @@ +# frozen_string_literal: true + +require "rails_helper" + RSpec.describe DestroyProblemsByIdJob, type: :job do - before do - @app = Fabricate(:app) - @problem1 = Fabricate(:problem, app: @app) - @problem2 = Fabricate(:problem, app: @app) - @problem3 = Fabricate(:problem, app: @app) - @problem4 = Fabricate(:problem, app: @app) - @problem5 = Fabricate(:problem, app: @app) - @problem6 = Fabricate(:problem, app: @app) - end + it { expect(subject).to be_an(ApplicationJob) } it "destroys all selected problems" do - expect do - DestroyProblemsByIdJob.perform_later([@problem2.id.to_s, @problem5.id.to_s, @problem1.id.to_s]) - end.to change(Problem, :count).by(-3) - expect(@app.problems.count).to eq 3 - end + app = create(:app) + problem = create(:problem, app: app) - it "destroys all selected problems, even with a large amount of them" do - app2 = Fabricate(:app) - 500.times do - Fabricate(:problem, app: app2) - end expect do - DestroyProblemsByIdJob.perform_later(app2.problems.to_a.sample(100).map(&:id).map(&:to_s)) - end.to change(Problem, :count).by(-100) - expect(app2.problems.count).to eq 400 + described_class.perform_later([problem.id]) + end.to change(Problem, :count).by(-1) + + expect(app.problems.count).to eq(0) end it "should work with a fresh new application" do - app2 = Fabricate(:app) + app = create(:app) + expect do - DestroyProblemsByIdJob.perform_later([]) - end.to change(Problem, :count).by(0) - expect(app2.problems.count).to eq 0 + described_class.perform_later([]) + end.not_to change(Problem, :count) + + expect(app.problems.count).to eq(0) end end diff --git a/spec/lib/airbrake_api/v3/notice_parser_spec.rb b/spec/lib/airbrake_api/v3/notice_parser_spec.rb index cd903c9068..369a45834b 100644 --- a/spec/lib/airbrake_api/v3/notice_parser_spec.rb +++ b/spec/lib/airbrake_api/v3/notice_parser_spec.rb @@ -1,130 +1,138 @@ -describe AirbrakeApi::V3::NoticeParser do - let(:app) { Fabricate(:app) } +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe AirbrakeApi::V3::NoticeParser do + let(:app) { create(:app) } + let(:notifier_params) do { - 'name' => 'notifiername', - 'version' => 'notifierversion', - 'url' => 'notifierurl' + "name" => "notifiername", + "version" => "notifierversion", + "url" => "notifierurl" } end - it 'raises error when errors attribute is missing' do + it "raises error when errors attribute is missing" do expect do described_class.new({}).report end.to raise_error(AirbrakeApi::ParamsError) expect do - described_class.new('errors' => []).report + described_class.new("errors" => []).report end.to raise_error(AirbrakeApi::ParamsError) end - it 'does not raise an error for the optional environment field' do + it "does not raise an error for the optional environment field" do expect do - described_class.new('errors' => ['MyError']).report + described_class.new("errors" => ["MyError"]).report end.not_to raise_error end - it 'parses JSON payload and returns ErrorReport' do - params = build_params_for('api_v3_request.json', key: app.api_key) + it "parses JSON payload and returns ErrorReport" do + params = build_params_for("api_v3_request.json", key: app.api_key) report = described_class.new(params).report notice = report.generate_notice! - expect(report.error_class).to eq('Error') - expect(report.message).to eq('Error: TestError') + expect(report.error_class).to eq("Error") + expect(report.message).to eq("Error: TestError") expect(report.backtrace.lines.size).to eq(9) expect(notice.user_attributes).to include( - 'id' => 1, - 'name' => 'John Doe', - 'email' => 'john.doe@example.org', - 'username' => 'john' + "id" => 1, + "name" => "John Doe", + "email" => "john.doe@example.org", + "username" => "john" ) - expect(notice.session).to include('isAdmin' => true) - expect(notice.params).to include('returnTo' => 'dashboard') + expect(notice.session).to include("isAdmin" => true) + expect(notice.params).to include("returnTo" => "dashboard") expect(notice.env_vars).to include( - 'navigator_vendor' => 'Google Inc.', - 'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.99 Safari/537.36' + "navigator_vendor" => "Google Inc.", + "HTTP_USER_AGENT" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.99 Safari/537.36" ) end - it 'parses JSON payload when api_key is missing but project_id is present' do - params = build_params_for('api_v3_request.json', key: nil, project_id: app.api_key) + it "parses JSON payload when api_key is missing but project_id is present" do + params = build_params_for("api_v3_request.json", key: nil, project_id: app.api_key) report = described_class.new(params).report - expect(report).to be_valid + expect(report.valid?).to eq(true) end - it 'parses JSON payload with missing backtrace' do - json = Rails.root.join('spec', 'fixtures', 'api_v3_request_without_backtrace.json').read + it "parses JSON payload with missing backtrace" do + json = Rails.root.join("spec/fixtures/api_v3_request_without_backtrace.json").read params = JSON.parse(json) - params['key'] = app.api_key + params["key"] = app.api_key report = described_class.new(params).report report.generate_notice! - expect(report.error_class).to eq('Error') - expect(report.message).to eq('Error: TestError') + expect(report.error_class).to eq("Error") + expect(report.message).to eq("Error: TestError") expect(report.backtrace.lines.size).to eq(0) end - it 'parses JSON payload with deprecated user keys' do - params = build_params_for('api_v3_request_with_deprecated_user_keys.json', key: app.api_key) + it "parses JSON payload with deprecated user keys" do + params = build_params_for("api_v3_request_with_deprecated_user_keys.json", key: app.api_key) report = AirbrakeApi::V3::NoticeParser.new(params).report notice = report.generate_notice! expect(notice.user_attributes).to include( - 'id' => 1, - 'name' => 'John Doe', - 'email' => 'john.doe@example.org', - 'username' => 'john' + "id" => 1, + "name" => "John Doe", + "email" => "john.doe@example.org", + "username" => "john" ) end - it 'takes the notifier from root' do + it "takes the notifier from root" do parser = described_class.new( - 'errors' => ['MyError'], - 'notifier' => notifier_params, - 'environment' => {}) + "errors" => ["MyError"], + "notifier" => notifier_params, + "environment" => {} + ) expect(parser.attributes[:notifier]).to eq(notifier_params) end - it 'takes the notifier from the context' do + it "takes the notifier from the context" do parser = described_class.new( - 'errors' => ['MyError'], - 'context' => { 'notifier' => notifier_params }, - 'environment' => {}) + "errors" => ["MyError"], + "context" => {"notifier" => notifier_params}, + "environment" => {} + ) expect(parser.attributes[:notifier]).to eq(notifier_params) end - it 'takes the hostname from the context' do + it "takes the hostname from the context" do parser = described_class.new( - 'errors' => ['MyError'], - 'context' => { 'hostname' => 'app01.infra.example.com', 'url' => 'http://example.com/some-page' }, - 'environment' => {}) - expect(parser.attributes[:server_environment]['hostname']).to eq('app01.infra.example.com') + "errors" => ["MyError"], + "context" => {"hostname" => "app01.infra.example.com", "url" => "http://example.com/some-page"}, + "environment" => {} + ) + expect(parser.attributes[:server_environment]["hostname"]).to eq("app01.infra.example.com") end - describe '#user_attributes' do - it 'returns a user context hash' do - user_hash = { id: 1, name: 'John Doe' } - parser = described_class.new('context' => { 'user' => user_hash }) + describe "#user_attributes" do + it "returns a user context hash" do + user_hash = {id: 1, name: "John Doe"} + parser = described_class.new("context" => {"user" => user_hash}) expect(parser.send(:user_attributes)).to eq(user_hash) end - it 'returns a hash for a user context string' do - user_string = '[Filtered]' - parser = described_class.new('context' => { 'user' => user_string }) + it "returns a hash for a user context string" do + user_string = "[Filtered]" + parser = described_class.new("context" => {"user" => user_string}) expect(parser.send(:user_attributes)).to eq(user: user_string) end end def build_params_for(fixture, options = {}) - json = Rails.root.join('spec', 'fixtures', fixture).read + json = Rails.root.join("spec", "fixtures", fixture).read data = JSON.parse(json) - data['key'] = options[:key] if options.key?(:key) - data['project_id'] = options[:project_id] if options.key?(:project_id) + data["key"] = options[:key] if options.key?(:key) + data["project_id"] = options[:project_id] if options.key?(:project_id) data end diff --git a/spec/lib/configurator_spec.rb b/spec/lib/configurator_spec.rb index 6948254c83..16e366da53 100644 --- a/spec/lib/configurator_spec.rb +++ b/spec/lib/configurator_spec.rb @@ -1,63 +1,82 @@ -describe Configurator do - before(:each) do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Configurator do + before do allow(ENV).to receive(:[]).and_return(nil) - allow(ENV).to receive(:[]).with('VARONE').and_return('zoom') - allow(ENV).to receive(:[]).with('VARTHREE').and_return('zipp') + allow(ENV).to receive(:[]).with("VARONE").and_return("zoom") + allow(ENV).to receive(:[]).with("VARTHREE").and_return("zipp") end - it 'takes the first existing env, second item' do - result = Configurator.run(two: %w(VARTWO VARTHREE)) - expect(result.two).to eq('zipp') + it "takes the first existing env, second item" do + result = described_class.run(two: ["VARTWO", "VARTHREE"]) + + expect(result.two).to eq("zipp") end - it 'takes the first existing env, first item' do - result = Configurator.run(three: %w(VARTHREE VARONE)) - expect(result.three).to eq('zipp') + it "takes the first existing env, first item" do + result = described_class.run(three: ["VARTHREE", "VARONE"]) + + expect(result.three).to eq("zipp") end - it 'provides nothing for missing variables' do - result = Configurator.run(four: ['VAREIGHTY']) - expect(result.four).to be_nil + it "provides nothing for missing variables" do + result = described_class.run(four: ["VAREIGHTY"]) + + expect(result.four).to eq(nil) end - it 'overrides existing variables' do - result = Configurator.run(one: ['VARONE', ->(_values) { 'oveRIIIDE' }]) - expect(result.one).to eq('oveRIIIDE') + it "overrides existing variables" do + result = described_class.run(one: ["VARONE", ->(_values) { "oveRIIIDE" }]) + + expect(result.one).to eq("oveRIIIDE") end - it 'overrides can refer to other values' do - result = Configurator.run(one: ['VARONE', ->(values) { values[:one] }], - three: ['VARTHREE']) - expect(result.one).to eq('zoom') + it "overrides can refer to other values" do + result = described_class.run(one: ["VARONE", ->(values) { values[:one] }], + three: ["VARTHREE"]) + + expect(result.one).to eq("zoom") end - it 'extracts symbol values' do - allow(ENV).to receive(:[]).with('MYSYMBOL').and_return(':asymbol') - result = Configurator.run(mysymbol: ['MYSYMBOL']) - expect(result.mysymbol).to be(:asymbol) + it "extracts symbol values" do + allow(ENV).to receive(:[]).with("MYSYMBOL").and_return(":asymbol") + + result = described_class.run(mysymbol: ["MYSYMBOL"]) + + expect(result.mysymbol).to eq(:asymbol) end - it 'extracts array values' do - allow(ENV).to receive(:[]).with('MYARRAY').and_return('[one,two,three]') - result = Configurator.run(myarray: ['MYARRAY']) - expect(result.myarray).to eq(%w(one two three)) + it "extracts array values" do + allow(ENV).to receive(:[]).with("MYARRAY").and_return("[one,two,three]") + + result = described_class.run(myarray: ["MYARRAY"]) + + expect(result.myarray).to eq(["one", "two", "three"]) end - it 'extracts booleans' do - allow(ENV).to receive(:[]).with('MYBOOLEAN').and_return('true') - result = Configurator.run(myboolean: ['MYBOOLEAN']) - expect(result.myboolean).to be(true) + it "extracts booleans" do + allow(ENV).to receive(:[]).with("MYBOOLEAN").and_return("true") + + result = described_class.run(myboolean: ["MYBOOLEAN"]) + + expect(result.myboolean).to eq(true) end - it 'extracts numbers' do - allow(ENV).to receive(:[]).with('MYNUMBER').and_return('0') - result = Configurator.run(mynumber: ['MYNUMBER']) - expect(result.mynumber).to be(0) + it "extracts numbers" do + allow(ENV).to receive(:[]).with("MYNUMBER").and_return("0") + + result = described_class.run(mynumber: ["MYNUMBER"]) + + expect(result.mynumber).to eq(0) end - it 'parses empty variables' do - allow(ENV).to receive(:[]).with('EMPTYVAR').and_return('') - result = Configurator.run(emptyvar: ['EMPTYVAR']) - expect(result.emptyvar).to eq('') + it "parses empty variables" do + allow(ENV).to receive(:[]).with("EMPTYVAR").and_return("") + + result = described_class.run(emptyvar: ["EMPTYVAR"]) + + expect(result.emptyvar).to eq("") end end diff --git a/spec/lib/errbit/self_error_reporter_spec.rb b/spec/lib/errbit/self_error_reporter_spec.rb new file mode 100644 index 0000000000..7a5fb9bc44 --- /dev/null +++ b/spec/lib/errbit/self_error_reporter_spec.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Errbit::SelfErrorReporter do + let(:exception) do + StandardError.new("self error").tap do |error| + error.set_backtrace(["#{Rails.root}/app/models/error_report.rb:12:in `generate_notice!'"]) + end + end + + after { Airbrake.reset } + + before do + allow(described_class).to receive(:public_environment?).and_return(true) + end + + ["development", "test"].each do |environment| + it "does not report in #{environment}" do + allow(described_class).to receive(:public_environment?).and_call_original + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new(environment)) + + expect(Airbrake).not_to receive(:build_notice) + + described_class.notify(exception) + end + end + + it "creates the Self.Errbit app when missing" do + expect do + described_class.notify(exception) + end.to change { App.where(name: "Self.Errbit").count }.by(1) + end + + it "reuses an existing Self.Errbit app without overwriting custom settings" do + app = create(:app, name: "Self.Errbit", github_repo: "custom/repo") + + described_class.notify(exception) + + expect(app.reload.github_repo).to eq("custom/repo") + end + + it "reuses the same Self.Errbit app for different exceptions" do + described_class.notify(exception) + described_class.notify(RuntimeError.new("another self error")) + + expect(App.where(name: "Self.Errbit").count).to eq(1) + end + + it "creates a notice and problem for the exception" do + expect do + described_class.notify(exception) + end.to change(Notice, :count).by(1) + .and change(Problem, :count).by(1) + + notice = Notice.last + expect(notice.error_class).to eq("StandardError") + expect(notice.message).to include("self error") + expect(notice.app_version).to eq(Errbit::Version.to_s) + end + + it "passes the Self.Errbit app API key to the Airbrake v3 parser" do + app = create(:app, name: "Self.Errbit") + report = instance_double(ErrorReport, generate_notice!: true) + parser = instance_double(AirbrakeApi::V3::NoticeParser, report: report) + + expect(AirbrakeApi::V3::NoticeParser).to receive(:new) do |payload| + expect(payload["key"]).to eq(app.api_key) + parser + end + + described_class.notify(exception) + end + + it "ignores Mongoid document not found errors" do + exception = Mongoid::Errors::DocumentNotFound.new(App, {id: "missing"}, nil) + + expect(Airbrake).not_to receive(:build_notice) + + described_class.notify(exception) + end + + it "does not invoke Airbrake delivery APIs" do + expect(Airbrake).not_to receive(:notify) + expect(Airbrake).not_to receive(:notify_sync) + + described_class.notify(exception) + end + + describe described_class::Middleware do + let(:env) { Rack::MockRequest.env_for("/") } + + it "reports exceptions stored by Rails and returns the response" do + env["action_dispatch.exception"] = exception + app = ->(_env) { [400, {}, []] } + middleware = described_class.new(app) + + expect(described_class.superclass).to eq(Airbrake::Rack::Middleware) + expect(Errbit::SelfErrorReporter).to receive(:notify).with( + exception, + request: instance_of(ActionDispatch::Request) + ) + + expect(middleware.call(env)).to eq([400, {}, []]) + end + + it "reports exceptions raised by the application and re-raises them" do + app = ->(_env) { raise exception } + middleware = described_class.new(app) + + expect(Errbit::SelfErrorReporter).to receive(:notify).with( + exception, + request: instance_of(ActionDispatch::Request) + ) + + expect { middleware.call(env) }.to raise_error(exception) + end + end + + it "does not parse a payload when Airbrake does not build a notice" do + allow(Airbrake).to receive(:build_notice).and_return(nil) + + expect(AirbrakeApi::V3::NoticeParser).not_to receive(:new) + + described_class.notify(exception) + end + + it "logs and swallows failures while reporting" do + allow(AirbrakeApi::V3::NoticeParser).to receive(:new).and_raise(StandardError, "parser failed") + + expect(Rails.logger).to receive(:error).with("Errbit::SelfErrorReporter failed: StandardError - parser failed") + + expect do + described_class.notify(exception) + end.not_to raise_error + end + + it "does not build a notice when the Self.Errbit app cannot be loaded" do + allow(App).to receive(:where).and_raise(StandardError, "database unavailable") + allow(Rails.logger).to receive(:error) + + expect(Airbrake).not_to receive(:build_notice) + + expect do + described_class.notify(exception) + end.not_to raise_error + end +end diff --git a/spec/lib/errbit/version_spec.rb b/spec/lib/errbit/version_spec.rb index ddc2fe0ad8..01e5fbb227 100644 --- a/spec/lib/errbit/version_spec.rb +++ b/spec/lib/errbit/version_spec.rb @@ -1,13 +1,41 @@ -describe Errbit::VERSION do - subject { Errbit::Version.new.full_version } +# frozen_string_literal: true - it 'handles a missing commit sha' do - allow(ENV).to receive(:[]).with('SOURCE_VERSION') { nil } - expect(subject).to end_with('dev') +require "rails_helper" + +RSpec.describe Errbit::Version do + let(:version) { "0.0.0" } + + context "release version" do + subject { described_class.new(version).full_version } + + it "generates a release version" do + expect(subject).to eq(version) + end + + it "does not use a commit sha" do + expect(ENV).not_to receive(:fetch).with("SOURCE_VERSION", nil) + + expect(subject).to eq(version) + end end - it 'shortens a present commit sha' do - allow(ENV).to receive(:[]).with('SOURCE_VERSION') { 'abcd1234efgh56789' } - expect(subject).to end_with('dev-abcd1234') + context "dev version" do + subject { described_class.new(version, true).full_version } + + it "generates a dev version" do + expect(subject).to end_with("dev") + end + + it "handles a missing commit sha" do + expect(ENV).to receive(:fetch).with("SOURCE_VERSION", nil).and_return(nil) + + expect(subject).to end_with("dev") + end + + it "shortens a present commit sha" do + expect(ENV).to receive(:fetch).with("SOURCE_VERSION", nil).and_return("abcd1234efgh56789") + + expect(subject).to end_with("dev-abcd1234") + end end end diff --git a/spec/lib/sparklines_spec.rb b/spec/lib/sparklines_spec.rb index e07187b713..a191b961c4 100644 --- a/spec/lib/sparklines_spec.rb +++ b/spec/lib/sparklines_spec.rb @@ -1,16 +1,21 @@ -describe Sparklines do - it 'includes each percentage and adds a percent sign' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Sparklines do + it "includes a height class for each percentage" do percentages = [33, 75, 100] - sparklines_html = Sparklines.for_relative_percentages(percentages) + html = described_class.for_relative_percentages(percentages) percentages.each do |percentage| - expect(sparklines_html).to include("#{percentage}%") + expect(html).to include("height-#{percentage}") end end - it 'has the right number of i tags' do + it "has the right number of i tags" do percentages = [75, 100] - sparklines_html = Sparklines.for_relative_percentages(percentages) - number_of_i_tags = sparklines_html.scan(/path/to/file.js') + expect(email).to have_body_text(' path/to/file.js') end it "should have the error count in the subject" do expect(email.subject).to match(/^\(3\) /) end - context 'with a very long message' do - let(:notice) { Fabricate(:notice, message: 6.times.collect { |_a| "0123456789" }.join('')) } + context "with a very long message" do + let(:notice) { create(:notice, message: 6.times.collect { |_a| "0123456789" }.join("")) } + it "should truncate the long message" do expect(email.subject).to match(/ \d{47}\.{3}$/) end @@ -96,19 +105,22 @@ include EmailSpec::Helpers include EmailSpec::Matchers - let!(:notice) { Fabricate(:notice) } - let!(:comment) { Fabricate(:comment, err: notice.problem) } - let!(:watcher) { Fabricate(:watcher, app: comment.app) } - let(:recipients) { ['recipient@example.com', 'another@example.com'] } + let!(:notice) { create(:notice) } + + let!(:comment) { create(:comment, err: notice.problem) } + + let!(:watcher) { create(:watcher, app: comment.app) } + + let(:recipients) { ["recipient@example.com", "another@example.com"] } before do expect(comment).to receive(:notification_recipients).and_return(recipients) - Fabricate(:notice, err: notice.err) - @email = Mailer.comment_notification(comment).deliver_now + create(:notice, err: notice.err) + @email = Mailer.with(comment: comment).comment_notification.deliver_now end it "should be sent to comment notification recipients" do - expect(@email.to).to eq recipients + expect(@email.to).to eq(recipients) end it "should have the notices count in the body" do @@ -118,5 +130,21 @@ it "should have the comment body" do expect(@email).to have_body_text(comment.body) end + + context "when the comment body contains HTML" do + let!(:comment) do + create(:comment, + err: notice.problem, + body: "\n\nA second paragraph") + end + + it "should escape the HTML and preserve paragraphs" do + html_body = @email.body.parts.detect { |part| part.content_type.match(/html/) }.body.raw_source + + expect(html_body).not_to include("") + expect(html_body).to include("alert('xss')") + expect(html_body).to match(/]*>A second paragraph<\/p>/) + end + end end end diff --git a/spec/mailers/previews/devise/mailer_preview.rb b/spec/mailers/previews/devise/mailer_preview.rb new file mode 100644 index 0000000000..2086462478 --- /dev/null +++ b/spec/mailers/previews/devise/mailer_preview.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Devise + class MailerPreview < ActionMailer::Preview + def reset_password_instructions + record = FactoryBot.create(:user, reset_password_token: "reset-password-token") + + token = record.reset_password_token + + Devise::Mailer.reset_password_instructions(record, token) + end + end +end diff --git a/spec/mailers/previews/mailer_preview.rb b/spec/mailers/previews/mailer_preview.rb new file mode 100644 index 0000000000..ffd3b0a652 --- /dev/null +++ b/spec/mailers/previews/mailer_preview.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class MailerPreview < ActionMailer::Preview + def err_notification + error_report = nil + + Mailer.with(error_report: error_report).err_notification + end + + def comment_notification + user = FactoryBot.create(:user) + # problem + comment = FactoryBot.create(:comment, user: user) + + Mailer.with(comment: comment).comment_notification + end +end diff --git a/spec/models/app_spec.rb b/spec/models/app_spec.rb index d8dcb5c438..e26398383f 100644 --- a/spec/models/app_spec.rb +++ b/spec/models/app_spec.rb @@ -1,193 +1,225 @@ -describe App, type: 'model' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe App, type: :model do context "Attributes" do it { is_expected.to have_field(:_id).of_type(String) } + it { is_expected.to have_field(:name).of_type(String) } + it { is_expected.to have_fields(:api_key, :github_repo, :bitbucket_repo, :asset_host, :repository_branch) } + it { is_expected.to have_fields(:notify_all_users, :notify_on_errs).of_type(Mongoid::Boolean) } + it { is_expected.to have_field(:email_at_notices).of_type(Array).with_default_value_of(Errbit::Config.email_at_notices) } end - context 'validations' do - it 'requires a name' do - app = Fabricate.build(:app, name: nil) - expect(app).to_not be_valid + context "validations" do + it "requires a name" do + app = build(:app, name: nil) + + expect(app.valid?).to eq(false) + expect(app.errors[:name]).to include("can't be blank") end - it 'requires unique names' do - Fabricate(:app, name: 'Errbit') - app = Fabricate.build(:app, name: 'Errbit') - expect(app).to_not be_valid - expect(app.errors[:name]).to include('is already taken') + it "requires unique names" do + create(:app, name: "Errbit") + + app = build(:app, name: "Errbit") + + expect(app.valid?).to eq(false) + + expect(app.errors[:name]).to eq(["has already been taken"]) end - it 'requires unique api_keys' do - Fabricate(:app, api_key: 'APIKEY') - app = Fabricate.build(:app, api_key: 'APIKEY') - expect(app).to_not be_valid - expect(app.errors[:api_key]).to include('is already taken') + it "requires unique api_keys" do + create(:app, api_key: "APIKEY") + app = build(:app, api_key: "APIKEY") + expect(app.valid?).to eq(false) + expect(app.errors[:api_key]).to eq(["has already been taken"]) end end - describe '<=>' do - it 'is compared by unresolved count' do - app_0 = stub_model(App, name: 'app', unresolved_count: 1, problem_count: 1) - app_1 = stub_model(App, name: 'app', unresolved_count: 0, problem_count: 1) + describe "<=>" do + it "is compared by unresolved count" do + app_0 = stub_model(App, name: "app", unresolved_count: 1, problem_count: 1) + app_1 = stub_model(App, name: "app", unresolved_count: 0, problem_count: 1) expect(app_0).to be < app_1 expect(app_1).to be > app_0 end - it 'is compared by problem count' do - app_0 = stub_model(App, name: 'app', unresolved_count: 0, problem_count: 1) - app_1 = stub_model(App, name: 'app', unresolved_count: 0, problem_count: 0) + it "is compared by problem count" do + app_0 = stub_model(App, name: "app", unresolved_count: 0, problem_count: 1) + app_1 = stub_model(App, name: "app", unresolved_count: 0, problem_count: 0) expect(app_0).to be < app_1 expect(app_1).to be > app_0 end - it 'is compared by name' do - app_0 = stub_model(App, name: 'app_0', unresolved_count: 0, problem_count: 0) - app_1 = stub_model(App, name: 'app_1', unresolved_count: 0, problem_count: 0) + it "is compared by name" do + app_0 = stub_model(App, name: "app_0", unresolved_count: 0, problem_count: 0) + app_1 = stub_model(App, name: "app_1", unresolved_count: 0, problem_count: 0) expect(app_0).to be < app_1 expect(app_1).to be > app_0 end end - context 'being created' do - it 'generates a new api-key' do - app = Fabricate.build(:app) - expect(app.api_key).to be_nil + context "being created" do + it "generates a new api-key" do + app = build(:app) + + expect(app.api_key).to eq(nil) + app.save - expect(app.api_key).to_not be_nil + + expect(app.api_key).not_to eq(nil) end - it 'generates a correct api-key' do - app = Fabricate(:app) + it "generates a correct api-key" do + app = create(:app) + expect(app.api_key).to match(/^[a-f0-9]{32}$/) end - it 'is fine with blank github repos' do - app = Fabricate.build(:app, github_repo: "") + it "is fine with blank github repos" do + app = build(:app, github_repo: "") + app.save - expect(app.github_repo).to eq "" + + expect(app.github_repo).to eq("") end - it 'doesnt touch github user/repo' do - app = Fabricate.build(:app, github_repo: "errbit/errbit") + it "doesnt touch github user/repo" do + app = build(:app, github_repo: "errbit/errbit") + app.save - expect(app.github_repo).to eq "errbit/errbit" + + expect(app.github_repo).to eq("errbit/errbit") end - it 'removes domain from https github repos' do - app = Fabricate.build(:app, github_repo: "https://github.com/errbit/errbit") + it "removes domain from https github repos" do + app = build(:app, github_repo: "https://github.com/errbit/errbit") + app.save - expect(app.github_repo).to eq "errbit/errbit" + + expect(app.github_repo).to eq("errbit/errbit") end - it 'normalizes public git repo as a github repo' do - app = Fabricate.build(:app, github_repo: "https://github.com/errbit/errbit.git") + it "normalizes public git repo as a github repo" do + app = build(:app, github_repo: "https://github.com/errbit/errbit.git") + app.save - expect(app.github_repo).to eq "errbit/errbit" + + expect(app.github_repo).to eq("errbit/errbit") end - it 'normalizes private git repo as a github repo' do - app = Fabricate.build(:app, github_repo: "git@github.com:errbit/errbit.git") + it "normalizes private git repo as a github repo" do + app = build(:app, github_repo: "git@github.com:errbit/errbit.git") + app.save - expect(app.github_repo).to eq "errbit/errbit" + + expect(app.github_repo).to eq("errbit/errbit") end end - context '#github_url_to_file' do - it 'resolves to full path to file' do - app = Fabricate(:app, github_repo: "errbit/errbit") - expect(app.github_url_to_file('path/to/file')).to eq "https://github.com/errbit/errbit/blob/master/path/to/file" + describe "#github_url_to_file" do + it "resolves to full path to file" do + app = create(:app, github_repo: "errbit/errbit", repository_branch: "main") + + expect(app.github_url_to_file("path/to/file")).to eq("https://github.com/errbit/errbit/blob/main/path/to/file") end end - context '#github_repo?' do - it 'is true when there is a github_repo' do - app = Fabricate(:app, github_repo: "errbit/errbit") - expect(app.github_repo?).to be(true) + describe "#github_repo?" do + it "is true when there is a github_repo" do + app = create(:app, github_repo: "errbit/errbit") + + expect(app.github_repo?).to eq(true) end - it 'is false when no github_repo' do - app = Fabricate(:app) - expect(app.github_repo?).to be(false) + it "is false when no github_repo" do + app = create(:app) + + expect(app.github_repo?).to eq(false) end end context "notification recipients" do it "should send notices to either all users plus watchers, or the configured watchers" do - @app = Fabricate(:app) - 3.times { Fabricate(:user) } - 5.times { Fabricate(:watcher, app: @app) } - @app.notify_all_users = true - expect(@app.notification_recipients.size).to eq 8 - @app.notify_all_users = false - expect(@app.notification_recipients.size).to eq 5 + app = create(:app) + create_list(:user, 3) + create_list(:watcher, 5, app: app) + app.notify_all_users = true + expect(app.notification_recipients.size).to eq(8) + app.notify_all_users = false + expect(app.notification_recipients.size).to eq(5) end end - context "emailable?" do + describe "#emailable?" do it "should be true if notify on errs and there are notification recipients" do - app = Fabricate(:app, notify_on_errs: true, notify_all_users: false) - 2.times { Fabricate(:watcher, app: app) } - expect(app.emailable?).to be(true) + app = create(:app, notify_on_errs: true, notify_all_users: false) + + create_list(:watcher, 2, app: app) + + expect(app.emailable?).to eq(true) end it "should be false if notify on errs is disabled" do - app = Fabricate(:app, notify_on_errs: false, notify_all_users: false) - 2.times { Fabricate(:watcher, app: app) } - expect(app.emailable?).to be(false) + app = create(:app, notify_on_errs: false, notify_all_users: false) + create_list(:watcher, 2, app: app) + expect(app.emailable?).to eq(false) end it "should be false if there are no notification recipients" do - app = Fabricate(:app, notify_on_errs: true, notify_all_users: false) + app = create(:app, notify_on_errs: true, notify_all_users: false) expect(app.watchers).to be_empty - expect(app.emailable?).to be(false) + expect(app.emailable?).to eq(false) end end context "copying attributes from existing app" do it "should only copy the necessary fields" do - @app = Fabricate(:app, name: "app", github_repo: "url") - @copy_app = Fabricate(:app, name: "copy_app", github_repo: "copy url") - @copy_watcher = Fabricate(:watcher, email: "copywatcher@example.com", app: @copy_app) - @app.copy_attributes_from(@copy_app.id) - expect(@app.name).to eq "app" - expect(@app.github_repo).to eq "copy url" - expect(@app.watchers.first.email).to eq "copywatcher@example.com" + app = create(:app, name: "app", github_repo: "url") + copy_app = create(:app, name: "copy_app", github_repo: "copy url") + create(:watcher, email: "copywatcher@example.com", app: copy_app) + app.copy_attributes_from(copy_app.id) + expect(app.name).to eq("app") + expect(app.github_repo).to eq("copy url") + expect(app.watchers.first.email).to eq("copywatcher@example.com") end end - context '#find_or_create_err!' do - let(:app) { Fabricate(:app) } + describe "#find_or_create_err!" do + let(:app) { create(:app) } + let(:conditions) do { - error_class: 'Whoops', - environment: 'production', - fingerprint: 'some-finger-print' + error_class: "Whoops", + environment: "production", + fingerprint: "some-finger-print" } end - it 'returns the correct err if one already exists' do - existing = Fabricate( - :err, - problem: Fabricate(:problem, app: app), - fingerprint: conditions[:fingerprint] - ) - expect(Err.where(fingerprint: conditions[:fingerprint]).first).to eq existing - expect(app.find_or_create_err!(conditions)).to eq existing + it "returns the correct err if one already exists" do + problem = create(:problem, app: app) + existing = create(:err, + problem: problem, + fingerprint: conditions[:fingerprint]) + expect(Err.where(fingerprint: conditions[:fingerprint]).first).to eq(existing) + expect(app.find_or_create_err!(conditions)).to eq(existing) end - it 'assigns the returned err to the given app' do - expect(app.find_or_create_err!(conditions).app).to eq app + it "assigns the returned err to the given app" do + expect(app.find_or_create_err!(conditions).app).to eq(app) end - it 'creates a new problem if a matching one does not already exist' do - expect(Err.where(conditions).first).to be_nil + it "creates a new problem if a matching one does not already exist" do + expect(Err.where(conditions).first).to eq(nil) expect do app.find_or_create_err!(conditions) end.to change(Problem, :count).by(1) @@ -196,12 +228,13 @@ context "without error_class" do let(:conditions) do { - environment: 'production', - fingerprint: 'some-finger-print' + environment: "production", + fingerprint: "some-finger-print" } end - it 'save the err' do - expect(Err.where(conditions).first).to be_nil + + it "save the err" do + expect(Err.where(conditions).first).to eq(nil) expect do app.find_or_create_err!(conditions) end.to change(Problem, :count).by(1) @@ -210,38 +243,48 @@ end describe ".find_by_api_key!" do - it 'return the app with api_key' do - app = Fabricate(:app) - expect(App.find_by_api_key!(app.api_key)).to eq app + it "return the app with api_key" do + app = create(:app) + expect(App.find_by_api_key!(app.api_key)).to eq(app) end - it 'raise Mongoid::Errors::DocumentNotFound if not found' do + + it "raise Mongoid::Errors::DocumentNotFound if not found" do expect do - App.find_by_api_key!('foo') + App.find_by_api_key!("foo") end.to raise_error(Mongoid::Errors::DocumentNotFound) end end - describe '#notice_fingerprinter' do - it 'app acquires a notice_fingerprinter when it doesn\'t have one' do - app = Fabricate(:app, name: 'Errbit') + describe "#notice_fingerprinter" do + it "app acquires a notice_fingerprinter when it doesn't have one" do + app = create(:app, name: "Errbit") + app.notice_fingerprinter.delete # has a notice_fingerprinter because it's been accessed when blank expect(app.reload.notice_fingerprinter).to be_a(NoticeFingerprinter) end - it 'brand new app has a notice_fingerprinter' do - app = Fabricate(:app, name: 'Errbit') + it "brand new app has a notice_fingerprinter" do + app = create(:app, name: "Errbit") + expect(app.notice_fingerprinter).to be_a(NoticeFingerprinter) end end context "searching" do - it 'finds the correct record' do - found = Fabricate(:app, name: 'Foo') - not_found = Fabricate(:app, name: 'Brr') - expect(App.search("Foo").to_a).to include(found) - expect(App.search("Foo").to_a).to_not include(not_found) + it "finds the correct record" do + found = create(:app, name: "Foo") + + create(:app, name: "Brr") + + expect(App.search("Foo").to_a).to eq([found]) end end + + describe "#attributes_for_super_diff" do + subject { create(:app) } + + it { expect(subject.attributes_for_super_diff).to eq(id: subject.id.to_s, name: subject.name) } + end end diff --git a/spec/models/backtrace_spec.rb b/spec/models/backtrace_spec.rb index 5140aec39e..6b73d04ce1 100644 --- a/spec/models/backtrace_spec.rb +++ b/spec/models/backtrace_spec.rb @@ -1,21 +1,25 @@ -describe Backtrace, type: 'model' do - describe '.find_or_create' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Backtrace, type: :model do + describe ".find_or_create" do let(:lines) do [ - { 'number' => '123', 'file' => '/some/path/to.rb', 'method' => 'abc' }, - { 'number' => '345', 'file' => '/path/to.rb', 'method' => 'dowhat' } + {"number" => "123", "file" => "/some/path/to.rb", "method" => "abc"}, + {"number" => "345", "file" => "/path/to.rb", "method" => "dowhat"} ] end let(:fingerprint) { Backtrace.generate_fingerprint(lines) } - it 'create new backtrace' do + it "create new backtrace" do backtrace = described_class.find_or_create(lines) expect(backtrace.lines).to eq(lines) expect(backtrace.fingerprint).to eq(fingerprint) end - it 'creates one backtrace for two identical ones' do + it "creates one backtrace for two identical ones" do described_class.find_or_create(lines) described_class.find_or_create(lines) diff --git a/spec/models/comment_observer_spec.rb b/spec/models/comment_observer_spec.rb index 299928f8e4..8b302cd9ad 100644 --- a/spec/models/comment_observer_spec.rb +++ b/spec/models/comment_observer_spec.rb @@ -1,23 +1,32 @@ -describe "Callback on Comment", type: 'model' do - context 'when a Comment is saved' do - let(:comment) { Fabricate.build(:comment) } +# frozen_string_literal: true - context 'and it is emailable?' do +require "rails_helper" + +RSpec.describe "Callback on Comment", type: :model do + context "when a Comment is saved" do + let(:comment) { build(:comment) } + + context "and it is emailable?" do before { allow(comment).to receive(:emailable?).and_return(true) } - it 'should send an email notification' do - expect(Mailer).to receive(:comment_notification). - with(comment). - and_return(double('email', deliver_now: true)) + it "should send an email notification" do + expect(Mailer).to receive(:with).with(comment: comment) do + double.tap do |a| + expect(a).to receive(:comment_notification) + .and_return(double("email", deliver_now: true)) + end + end + comment.save end end - context 'and it is not emailable?' do + context "and it is not emailable?" do before { allow(comment).to receive(:emailable?).and_return(false) } - it 'should not send an email notification' do - expect(Mailer).to_not receive(:comment_notification) + it "should not send an email notification" do + expect(Mailer).not_to receive(:with) + comment.save end end diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index 7f4ab5c2a4..960508c88f 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -1,53 +1,53 @@ -describe Comment, type: 'model' do - context 'validations' do - it 'should require a body' do - comment = Fabricate.build(:comment, body: nil) - expect(comment).to_not be_valid +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Comment, type: :model do + context "validations" do + it "is expected to require a body" do + comment = build(:comment, body: nil) + expect(comment.valid?).to eq(false) expect(comment.errors[:body]).to include("can't be blank") end end - context 'notification_recipients' do - let(:app) { Fabricate(:app) } - let!(:watcher) { Fabricate(:watcher, app: app) } - let(:err) { Fabricate(:problem, app: app) } - let(:comment_user) { Fabricate(:user, email: 'author@example.com') } - let(:comment) { Fabricate.build(:comment, err: err, user: comment_user) } + describe "#notification_recipients" do + let(:app) { create(:app) } + let!(:watcher) { create(:watcher, app: app) } + let(:err) { create(:problem, app: app) } + let(:comment_user) { create(:user, email: "author@example.com") } + let(:comment) { build(:comment, err: err, user: comment_user) } - before do - Fabricate(:user_watcher, app: app, user: comment_user) - end + before { create(:user_watcher, app: app, user: comment_user) } - it 'includes app notification_recipients except user email' do - expect(comment.notification_recipients).to eq [watcher.address] + it "includes app notification_recipients except user email" do + expect(comment.notification_recipients).to eq([watcher.address]) end end - context 'emailable?' do - let(:app) { Fabricate(:app, notify_on_errs: true) } - let!(:watcher) { Fabricate(:watcher, app: app) } - let(:err) { Fabricate(:problem, app: app) } - let(:comment_user) { Fabricate(:user, email: 'author@example.com') } - let(:comment) { Fabricate.build(:comment, err: err, user: comment_user) } + describe "#emailable?" do + let(:app) { create(:app, notify_on_errs: true) } + let!(:watcher) { create(:watcher, app: app) } + let(:err) { create(:problem, app: app) } + let(:comment_user) { create(:user, email: "author@example.com") } + let(:comment) { build(:comment, err: err, user: comment_user) } - before do - Fabricate(:user_watcher, app: app, user: comment_user) - end + before { create(:user_watcher, app: app, user: comment_user) } - it 'should be true if app is emailable? and there are notification recipients' do - expect(comment.emailable?).to be(true) + it "is expected to eq true if app is emailable? and there are notification recipients" do + expect(comment.emailable?).to eq(true) end - it 'should be false if app is not emailable?' do + it "is expected to eq false if app is not emailable?" do app.update_attribute(:notify_on_errs, false) expect(comment.notification_recipients).to be_any - expect(comment.emailable?).to be(false) + expect(comment.emailable?).to eq(false) end - it 'should be false if there are no notification recipients' do + it "is expected to eq false if there are no notification recipients" do watcher.destroy - expect(app.emailable?).to be(true) - expect(comment.emailable?).to be(false) + expect(app.emailable?).to eq(true) + expect(comment.emailable?).to eq(false) end end end diff --git a/spec/models/err_spec.rb b/spec/models/err_spec.rb index 85d69f539a..6a81000777 100644 --- a/spec/models/err_spec.rb +++ b/spec/models/err_spec.rb @@ -1,14 +1,18 @@ -describe Err, type: 'model' do - context 'validations' do - it 'requires a fingerprint' do - err = Fabricate.build(:err, fingerprint: nil) - expect(err).to_not be_valid +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Err, type: :model do + context "validations" do + it "requires a fingerprint" do + err = build(:err, fingerprint: nil) + expect(err.valid?).to eq(false) expect(err.errors[:fingerprint]).to include("can't be blank") end - it 'requires a problem' do - err = Fabricate.build(:err, problem_id: nil, problem: nil) - expect(err).to_not be_valid + it "requires a problem" do + err = build(:err, problem_id: nil, problem: nil) + expect(err.valid?).to eq(false) expect(err.errors[:problem_id]).to include("can't be blank") end end diff --git a/spec/models/error_report_spec.rb b/spec/models/error_report_spec.rb index cab44faa29..cdf1c3bfc3 100644 --- a/spec/models/error_report_spec.rb +++ b/spec/models/error_report_spec.rb @@ -1,44 +1,26 @@ -require 'airbrake/version' -require 'airbrake/backtrace' -require 'airbrake/notice' -require 'airbrake/utils/params_cleaner' - -# MonkeyPatch to instanciate a Airbrake::Notice without configure -# Airbrake -# -module Airbrake - API_VERSION = '2.4' - - class Notice - def framework - 'rails' - end - end -end +# frozen_string_literal: true + +require "rails_helper" +require "airbrake" -describe ErrorReport do +RSpec.describe ErrorReport, type: :model do let(:xml) do - Rails.root.join('spec', 'fixtures', 'hoptoad_test_notice.xml').read + Rails.root.join("spec/fixtures/hoptoad_test_notice.xml").read end let(:error_report) { ErrorReport.new(xml) } - let!(:app) do - Fabricate( - :app, - api_key: 'APIKEY' - ) - end + let!(:app) { create(:app, api_key: "APIKEY") } describe "#app" do - it 'find the good app' do - expect(error_report.app).to eq app + it "find the good app" do + expect(error_report.app).to eq(app) end end describe "#backtrace" do - it 'should have valid backtrace' do - expect(error_report.backtrace).to be_valid + it "should have valid backtrace" do + expect(error_report.backtrace.valid?).to eq(true) end end @@ -46,83 +28,95 @@ def framework it "save a notice" do expect do error_report.generate_notice! - end.to change { - app.reload.problems.count - }.by(1) + end.to change(app.reload.problems, :count).by(1) end context "with a minimal notice" do let(:xml) do - Rails.root.join('spec', 'fixtures', 'minimal_test_notice.xml').read + Rails.root.join("spec/fixtures/minimal_test_notice.xml").read end - it 'save a notice' do + it "save a notice" do expect do error_report.generate_notice! - end.to change { - app.reload.problems.count - }.by(1) + end.to change(app.reload.problems, :count).by(1) end end - context "with notice generate by Airbrake gem" do - let(:xml) do - Airbrake::Notice.new( - exception: Exception.new, - api_key: 'APIKEY', - project_root: Rails.root - ).to_xml + context "with notice generated by Airbrake gem" do + after { Airbrake.reset } + + let(:exception) do + StandardError.new("TestError").tap do |error| + error.set_backtrace(["#{Rails.root}/app/models/user.rb:12:in `find'"]) + end end - it 'save a notice' do + + let(:airbrake_notice) do + JSON.parse(Airbrake.build_notice(exception).to_json) + end + + let(:params) do + airbrake_notice.merge("key" => app.api_key) + end + + let(:error_report) { AirbrakeApi::V3::NoticeParser.new(params).report } + + it "saves a notice" do expect do error_report.generate_notice! - end.to change { - app.reload.problems.count - }.by(1) + end.to change(app.reload.problems, :count).by(1) end end describe "notice create" do before { error_report.generate_notice! } + subject { error_report.notice } - it 'has correct message' do + it "has correct message" do expect(subject.message).to include('HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works') end - it 'has correct framework' do - expect(subject.framework).to eq('Rails: 3.2.11') + it "has correct framework" do + expect(subject.framework).to eq("Rails: 3.2.11") end - it 'has complete backtrace' do - expect(subject.backtrace_lines.size).to eq 73 - expect(subject.backtrace_lines.last['file']).to eq '[GEM_ROOT]/bin/rake' + it "has complete backtrace" do + expect(subject.backtrace_lines.size).to eq(73) + + expect(subject.backtrace_lines.last["file"]).to eq("[GEM_ROOT]/bin/rake") end - it 'has server_environement' do - expect(subject.server_environment['environment-name']).to eq 'development' + it "has server_environment" do + expect(subject.server_environment["environment-name"]).to eq("development") end - it 'has request' do - expect(subject.request['url']).to eq 'http://example.org/verify/cupcake=fistfight&lovebird=doomsayer' - expect(subject.request['params']['controller']).to eq 'application' + it "has request" do + expect(subject.request["url"]).to eq("http://example.org/verify/cupcake=fistfight&lovebird=doomsayer") + + expect(subject.request["params"]["controller"]).to eq("application") end - it 'has notifier' do - expect(subject.notifier['name']).to eq 'Hoptoad Notifier' + it "has notifier" do + expect(subject.notifier["name"]).to eq("Hoptoad Notifier") end - it 'get user_attributes' do - expect(subject.user_attributes['id']).to eq '123' - expect(subject.user_attributes['name']).to eq 'Mr. Bean' - expect(subject.user_attributes['email']).to eq 'mr.bean@example.com' - expect(subject.user_attributes['username']).to eq 'mrbean' + it "get user_attributes" do + expect(subject.user_attributes["id"]).to eq("123") + + expect(subject.user_attributes["name"]).to eq("Mr. Bean") + + expect(subject.user_attributes["email"]).to eq("mr.bean@example.com") + + expect(subject.user_attributes["username"]).to eq("mrbean") end - it 'valid env_vars' do + it "valid env_vars" do # XML: - expect(subject.env_vars).to have_key('SCRIPT_NAME') - expect(subject.env_vars['SCRIPT_NAME']).to be_nil # blank ends up nil + expect(subject.env_vars).to have_key("SCRIPT_NAME") + + expect(subject.env_vars["SCRIPT_NAME"]).to eq(nil) # blank ends up nil # XML representation: # @@ -134,88 +128,100 @@ def framework # # expected = { - 'secure' => 'false', - 'httponly' => 'true', - 'path' => '/', - 'expire_after' => nil, - 'domain' => nil, - 'id' => nil + "secure" => "false", + "httponly" => "true", + "path" => "/", + "expire_after" => nil, + "domain" => nil, + "id" => nil } - expect(subject.env_vars).to have_key('rack_session_options') - expect(subject.env_vars['rack_session_options']).to eql(expected) + + expect(subject.env_vars).to have_key("rack_session_options") + + expect(subject.env_vars["rack_session_options"]).to eq(expected) end end end - describe '#cache_attributes_on_problem' do - it 'sets the latest notice properties on the problem' do + describe "#cache_attributes_on_problem" do + it "sets the latest notice properties on the problem" do error_report.generate_notice! problem = error_report.problem.reload notice = error_report.notice.reload - expect(problem.environment).to eq('development') - expect(problem.error_class).to eq('HoptoadTestingException') + expect(problem.environment).to eq("development") + + expect(problem.error_class).to eq("HoptoadTestingException") + expect(problem.last_notice_at).to eq(notice.created_at) + expect(problem.message).to eq(notice.message) + expect(problem.where).to eq(notice.where) end - it 'unresolves the problem' do + it "unresolves the problem" do error_report.generate_notice! problem = error_report.problem - problem.update( + problem.update!( resolved_at: Time.zone.now, - resolved: true + resolved: true ) error_report = ErrorReport.new(xml) error_report.generate_notice! problem.reload - expect(problem.resolved_at).to be(nil) - expect(problem.resolved).to be(false) + expect(problem.resolved_at).to eq(nil) + + expect(problem.resolved).to eq(false) end - it 'caches notice counts' do + it "caches notice counts" do error_report.generate_notice! problem = error_report.problem problem.reload - expect(problem.notices_count).to be(1) - expect(problem.user_agents['382b0f5185773fa0f67a8ed8056c7759']['count']).to be(1) - expect(problem.messages['9449f087eee0499e2d9029ae3dacaf53']['count']).to be(1) - expect(problem.hosts['1bdf72e04d6b50c82a48c7e4dd38cc69']['count']).to be(1) + expect(problem.notices_count).to eq(1) + + expect(problem.user_agents["382b0f5185773fa0f67a8ed8056c7759"]["count"]).to eq(1) + + expect(problem.messages["9449f087eee0499e2d9029ae3dacaf53"]["count"]).to eq(1) + + expect(problem.hosts["1bdf72e04d6b50c82a48c7e4dd38cc69"]["count"]).to eq(1) end - it 'increments notice counts' do + it "increments notice counts" do error_report.generate_notice! error_report = ErrorReport.new(xml) error_report.generate_notice! problem = error_report.problem problem.reload - expect(problem.notices_count).to be(2) - expect(problem.user_agents['382b0f5185773fa0f67a8ed8056c7759']['count']).to be(2) - expect(problem.messages['9449f087eee0499e2d9029ae3dacaf53']['count']).to be(2) - expect(problem.hosts['1bdf72e04d6b50c82a48c7e4dd38cc69']['count']).to be(2) + expect(problem.notices_count).to eq(2) + + expect(problem.user_agents["382b0f5185773fa0f67a8ed8056c7759"]["count"]).to eq(2) + + expect(problem.messages["9449f087eee0499e2d9029ae3dacaf53"]["count"]).to eq(2) + + expect(problem.hosts["1bdf72e04d6b50c82a48c7e4dd38cc69"]["count"]).to eq(2) end end - it 'save a notice assignes to err' do + it "save a notice assignes to err" do error_report.generate_notice! expect(error_report.notice.err).to be_a(Err) end - it 'memoize the notice' do + it "memoize the notice" do expect do error_report.generate_notice! + error_report.generate_notice! - end.to change { - Notice.count - }.by(1) + end.to change(Notice, :count).by(1) end - it 'find the correct err for the notice' do + it "find the correct err for the notice" do error_report.generate_notice! error_report.problem.resolve! @@ -229,11 +235,12 @@ def framework context "with notification service configured" do before do app.notify_on_errs = true - app.watchers.build(email: 'foo@example.com') + app.save + app.watchers.build(email: "foo@example.com") app.save end - it 'send email' do + it "send email" do notice = error_report.generate_notice! email = ActionMailer::Base.deliveries.last expect(email.to).to include(app.watchers.first.email) @@ -242,26 +249,29 @@ def framework expect(email.subject).to include("[#{notice.environment_name}]") end - context 'when email_at_notices config is specified', type: :mailer do + context "when email_at_notices config is specified", type: :mailer do before do allow(Errbit::Config).to receive(:email_at_notices).and_return(email_at_notices) end - context 'as [0]' do + context "as [0]" do let(:email_at_notices) { [0] } it "sends email on 1st occurrence" do - 1.times { described_class.new(xml).generate_notice! } + described_class.new(xml).generate_notice! + expect(ActionMailer::Base.deliveries.length).to eq(1) end it "sends email on 2nd occurrence" do 2.times { described_class.new(xml).generate_notice! } + expect(ActionMailer::Base.deliveries.length).to eq(2) end it "sends email on 3rd occurrence" do 3.times { described_class.new(xml).generate_notice! } + expect(ActionMailer::Base.deliveries.length).to eq(3) end end @@ -270,17 +280,20 @@ def framework let(:email_at_notices) { [1, 3] } it "sends email on 1st occurrence" do - 1.times { described_class.new(xml).generate_notice! } + described_class.new(xml).generate_notice! + expect(ActionMailer::Base.deliveries.length).to eq(1) end it "does not send email on 2nd occurrence" do 2.times { described_class.new(xml).generate_notice! } + expect(ActionMailer::Base.deliveries.length).to eq(1) end it "sends email on 3rd occurrence" do 3.times { described_class.new(xml).generate_notice! } + expect(ActionMailer::Base.deliveries.length).to eq(2) end @@ -289,6 +302,7 @@ def framework notice = described_class.new(xml).generate_notice! notice.problem.resolve! end + expect(ActionMailer::Base.deliveries.length).to eq(3) end end @@ -296,8 +310,9 @@ def framework context "with xml without request section" do let(:xml) do - Rails.root.join('spec', 'fixtures', 'hoptoad_test_notice_without_request_section.xml').read + Rails.root.join("spec/fixtures/hoptoad_test_notice_without_request_section.xml").read end + it "save a notice" do expect do error_report.generate_notice! @@ -309,8 +324,9 @@ def framework context "with xml with only a single line of backtrace" do let(:xml) do - Rails.root.join('spec', 'fixtures', 'hoptoad_test_notice_with_one_line_of_backtrace.xml').read + Rails.root.join("spec/fixtures/hoptoad_test_notice_with_one_line_of_backtrace.xml").read end + it "save a notice" do expect do error_report.generate_notice! @@ -324,23 +340,25 @@ def framework describe "#valid?" do context "with valid error report" do it "return true" do - expect(error_report.valid?).to be true + expect(error_report.valid?).to eq(true) end end + context "with not valid api_key" do before do App.where(api_key: app.api_key).delete_all end + it "return false" do - expect(error_report.valid?).to be false + expect(error_report.valid?).to eq(false) end end end describe "#notice" do context "before generate_notice!" do - it 'return nil' do - expect(error_report.notice).to be nil + it "return nil" do + expect(error_report.notice).to eq(nil) end end @@ -349,8 +367,8 @@ def framework error_report.generate_notice! end - it 'return the notice' do - expect(error_report.notice).to be_a Notice + it "return the notice" do + expect(error_report.notice).to be_a(Notice) end end end @@ -359,27 +377,30 @@ def framework context "with current app version not set" do before do error_report.app.current_app_version = nil - error_report.server_environment['app-version'] = '1.0' + + error_report.server_environment["app-version"] = "1.0" end it "return true" do - expect(error_report.should_keep?).to be true + expect(error_report.should_keep?).to eq(true) end end context "with current app version set" do before do - error_report.app.current_app_version = '1.0' + error_report.app.current_app_version = "1.0" end it "return true if current or newer" do - error_report.server_environment['app-version'] = '1.0' - expect(error_report.should_keep?).to be true + error_report.server_environment["app-version"] = "1.0" + + expect(error_report.should_keep?).to eq(true) end it "return false if older" do - error_report.server_environment['app-version'] = '0.9' - expect(error_report.should_keep?).to be false + error_report.server_environment["app-version"] = "0.9" + + expect(error_report.should_keep?).to eq(false) end end end diff --git a/spec/models/fabricators_spec.rb b/spec/models/fabricators_spec.rb index e3a75da1e0..3d560db3c0 100644 --- a/spec/models/fabricators_spec.rb +++ b/spec/models/fabricators_spec.rb @@ -1,16 +1,21 @@ -Fabrication::Config.fabricator_path.each do |folder| - Dir.glob(File.join(Rails.root, folder, '**', '*.rb')).each do |file| - require file - end -end +# frozen_string_literal: true -describe "Fabrication" do - # TODO : when 1.8.7 drop support se directly Symbol#sort - Fabrication.manager.schematics.keys.sort.each do |fabricator_name| - context "Fabricate(:#{fabricator_name})" do - subject { Fabricate.build(fabricator_name) } +require "rails_helper" - it { is_expected.to be_valid } - end - end -end +# TODO: add factory bot factories validator/linter. + +# Fabrication::Config.fabricator_path.each do |folder| +# Dir.glob(File.join(Rails.root, folder, "**", "*.rb")).each do |file| +# require file +# end +# end + +# RSpec.describe "Fabrication", type: :model do +# Fabrication.manager.schematics.keys.sort.each do |fabricator_name| +# context "Fabricate(:#{fabricator_name})" do +# subject { Fabricate.build(fabricator_name) } +# +# it { expect(subject.valid?).to eq(true) } +# end +# end +# end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 08a9144992..7f9a07a54c 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -1,26 +1,31 @@ -describe Issue, type: 'model' do - subject(:issue) { Issue.new(problem: problem, user: user, body: body) } +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Issue, type: :model do + subject { described_class.new(problem: problem, user: user, body: body) } let(:problem) { notice.problem } - let(:notice) { Fabricate(:notice) } - let(:user) { Fabricate(:admin) } + let(:notice) { create(:notice) } + let(:user) { create(:user, admin: true) } let(:issue_tracker) do - Fabricate(:issue_tracker).tap do |t| + create(:issue_tracker).tap do |t| t.instance_variable_set(:@tracker, ErrbitPlugin::MockIssueTracker.new(t.options)) end end - let(:errors) { issue.errors[:base] } + let(:errors) { subject.errors[:base] } context "when app has no issue tracker" do let(:body) { "barrr" } - context "#save" do + describe "#save" do it "returns false" do - expect(issue.save).to be false + expect(subject.save).to eq(false) end it "returns an error" do - issue.save + subject.save + expect(errors).to include("This app has no issue tracker") end end @@ -29,13 +34,14 @@ context "when has no body" do let(:body) { nil } - context "#save" do + describe "#save" do it "returns false" do - expect(issue.save).to be false + expect(subject.save).to eq(false) end it "returns an error" do - issue.save + subject.save + expect(errors).to include("The issue has no body") end end @@ -48,64 +54,74 @@ problem.app.issue_tracker = issue_tracker end - context "#render_body_args" do + describe "#render_body_args" do it "returns custom args if they exist" do - allow(issue.tracker).to receive(:render_body_args).and_return( - ['my', { custom: 'args' }] + allow(subject.tracker).to receive(:render_body_args).and_return( + ["my", {custom: "args"}] ) - expect(issue.render_body_args).to eq ['my', { custom: 'args' }] + expect(subject.render_body_args).to eq(["my", {custom: "args"}]) end it "returns default args if none exist" do - expect(issue.render_body_args).to eq [ - 'issue_trackers/issue', formats: [:md]] + expect(subject.render_body_args) + .to eq([template: "issue_trackers/markdown"]) end end - context "#title" do + describe "#title" do it "returns custom title if it exists" do - allow(issue.tracker).to receive(:title).and_return('kustomtitle') - expect(issue.title).to eq('kustomtitle') + allow(subject.tracker).to receive(:title).and_return("kustomtitle") + expect(subject.title).to eq("kustomtitle") end it "returns default title when tracker has none" do - expect(issue.title).to include(problem.message.to_s) + expect(subject.title).to include(problem.message.to_s) end end - context "#save" do + describe "#save" do context "when issue tracker has errors" do before do issue_tracker.tracker.options.clear end - it("returns false") { expect(issue.save).to be false } + it "returns false" do + expect(subject.save).to eq(false) + end + it "adds the errors" do - issue.save + subject.save + expect(errors).to include("foo is required") + expect(errors).to include("bar is required") end end it "creates the issue" do - issue.save - expect(issue_tracker.tracker.output.count).to be 1 + subject.save + + expect(issue_tracker.tracker.output.count).to eq(1) end it "returns true" do - expect(issue.save).to be true + expect(subject.save).to eq(true) end it "sends the title" do - issue.save + subject.save + saved_issue = issue_tracker.tracker.output.first - expect(saved_issue.first).to eq issue.title + + expect(saved_issue.first).to eq(subject.title) end it "sends the body" do - issue.save + subject.save + saved_issue = issue_tracker.tracker.output.first - expect(saved_issue[1]).to be body + + expect(saved_issue[1]).to eq(body) end end end diff --git a/spec/models/issue_tracker_spec.rb b/spec/models/issue_tracker_spec.rb index 299fa74afc..2ad9c467e9 100644 --- a/spec/models/issue_tracker_spec.rb +++ b/spec/models/issue_tracker_spec.rb @@ -1,4 +1,8 @@ -describe IssueTracker, type: 'model' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe IssueTracker, type: :model do describe "Association" do it { is_expected.to be_embedded_in(:app) } end @@ -10,11 +14,11 @@ describe "#tracker" do context "with type_tracker class not exist" do - let(:app) { Fabricate(:app) } + let(:app) { create(:app) } - it 'return NoneIssueTracker' do - issue_tracker = IssueTracker.new(type_tracker: 'Foo', app: app) - expect(issue_tracker.tracker).to be_a ErrbitPlugin::NoneIssueTracker + it "return NoneIssueTracker" do + issue_tracker = IssueTracker.new(type_tracker: "Foo", app: app) + expect(issue_tracker.tracker).to be_a(ErrbitPlugin::NoneIssueTracker) end end end diff --git a/spec/models/notice_fingerprinter_spec.rb b/spec/models/notice_fingerprinter_spec.rb index cb500ec4fc..4e1cfa65e4 100644 --- a/spec/models/notice_fingerprinter_spec.rb +++ b/spec/models/notice_fingerprinter_spec.rb @@ -1,72 +1,76 @@ -describe NoticeFingerprinter, type: 'model' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe NoticeFingerprinter, type: :model do let(:fingerprinter) { described_class.new } - let(:notice) { Fabricate(:notice) } - let(:backtrace) { Fabricate(:backtrace) } - - context '#generate' do - it 'generates the same fingerprint for the same notice' do - f1 = fingerprinter.generate('123', notice, backtrace) - f2 = fingerprinter.generate('123', notice, backtrace) - expect(f1).to eq(f2) + let(:notice) { create(:notice) } + let(:backtrace) { create(:backtrace) } + + describe "#generate" do + it "generates the same fingerprint for the same notice" do + f_1 = fingerprinter.generate("123", notice, backtrace) + f_2 = fingerprinter.generate("123", notice, backtrace) + expect(f_1).to eq(f_2) end - %w(error_class message component action environment_name).each do |i| + ["error_class", "message", "component", "action", "environment_name"].each do |i| it "affects the fingerprint when #{i} is false" do - f1 = fingerprinter.generate('123', notice, backtrace) - f2 = fingerprinter.generate('123', notice, backtrace) + f_1 = fingerprinter.generate("123", notice, backtrace) + f_2 = fingerprinter.generate("123", notice, backtrace) - fingerprinter.send((i << '=').to_sym, false) - f3 = fingerprinter.generate('123', notice, backtrace) + fingerprinter.send(:"#{i}=", false) + f_3 = fingerprinter.generate("123", notice, backtrace) - expect(f1).to eq(f2) - expect(f1).to_not eq(f3) + expect(f_1).to eq(f_2) + expect(f_1).not_to eq(f_3) end end - it 'affects the fingerprint with different backtrace_lines config' do - f1 = fingerprinter.generate('123', notice, backtrace) - f2 = fingerprinter.generate('123', notice, backtrace) + it "affects the fingerprint with different backtrace_lines config" do + f_1 = fingerprinter.generate("123", notice, backtrace) + f_2 = fingerprinter.generate("123", notice, backtrace) fingerprinter.backtrace_lines = 2 - f3 = fingerprinter.generate('123', notice, backtrace) + f_3 = fingerprinter.generate("123", notice, backtrace) - expect(f1).to eq(f2) - expect(f1).to_not eq(f3) + expect(f_1).to eq(f_2) + expect(f_1).not_to eq(f_3) end - context 'two backtraces have the same first two lines' do - let(:backtrace1) { Fabricate(:backtrace) } - let(:backtrace2) { Fabricate(:backtrace) } + context "two backtraces have the same first two lines" do + let(:backtrace_1) { create(:backtrace) } + let(:backtrace_2) { create(:backtrace) } before do - backtrace1.lines[0] = backtrace2.lines[0] - backtrace1.lines[1] = backtrace2.lines[1] - backtrace1.lines[2] = { number: 1, file: 'a', method: :b } + backtrace_1.lines[0] = backtrace_2.lines[0] + backtrace_1.lines[1] = backtrace_2.lines[1] + backtrace_1.lines[2] = {number: 1, file: "a", method: :b} end - it 'has the same fingerprint when only considering two lines' do + it "has the same fingerprint when only considering two lines" do fingerprinter.backtrace_lines = 2 - f1 = fingerprinter.generate('123', notice, backtrace1) - f2 = fingerprinter.generate('123', notice, backtrace2) + f_1 = fingerprinter.generate("123", notice, backtrace_1) + f_2 = fingerprinter.generate("123", notice, backtrace_2) - expect(f1).to eq(f2) + expect(f_1).to eq(f_2) end - it 'has a different fingerprint when considering three lines' do + it "has a different fingerprint when considering three lines" do fingerprinter.backtrace_lines = 3 - f1 = fingerprinter.generate('123', notice, backtrace1) - f2 = fingerprinter.generate('123', notice, backtrace2) + f_1 = fingerprinter.generate("123", notice, backtrace_1) + f_2 = fingerprinter.generate("123", notice, backtrace_2) - expect(f1).to_not eq(f2) + expect(f_1).not_to eq(f_2) end end context "two notices with no backtrace" do it "has the same fingerprint" do - f1 = fingerprinter.generate('123', notice, nil) - f2 = fingerprinter.generate('123', notice, nil) + f_1 = fingerprinter.generate("123", notice, nil) + f_2 = fingerprinter.generate("123", notice, nil) - expect(f1).to eq(f2) + expect(f_1).to eq(f_2) end end end diff --git a/spec/models/notice_observer_spec.rb b/spec/models/notice_observer_spec.rb index 53dd75c2ff..c4e856099d 100644 --- a/spec/models/notice_observer_spec.rb +++ b/spec/models/notice_observer_spec.rb @@ -1,37 +1,41 @@ -describe "Callback on Notice", type: 'model' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Callback on Notice", type: :model do let(:notice_attrs_for) do lambda do |api_key| { - error_class: "HoptoadTestingException", - message: "some message", - backtrace: [ + error_class: "HoptoadTestingException", + message: "some message", + backtrace: [ { "number" => "425", - "file" => "[GEM_ROOT]/callbacks.rb", + "file" => "[GEM_ROOT]/callbacks.rb", "method" => "__callbacks" } ], - request: { "component" => "application" }, + request: {"component" => "application"}, server_environment: { - "project-root" => "/path/to/sample/project", + "project-root" => "/path/to/sample/project", "environment-name" => "development" }, - api_key: api_key, - notifier: { - "name" => "Hoptoad Notifier", + api_key: api_key, + notifier: { + "name" => "Hoptoad Notifier", "version" => "2.3.2", - "url" => "http://hoptoadapp.com" + "url" => "http://hoptoadapp.com" }, - framework: "Rails: 3.2.11" + framework: "Rails: 3.2.11" } end end - describe 'email notifications (configured individually for each app)' do + describe "email notifications (configured individually for each app)" do let(:notice_attrs) { notice_attrs_for.call(app.api_key) } - custom_thresholds = [2, 4, 8, 16, 32, 64] + let(:custom_thresholds) { [2, 4, 8, 16, 32, 64] } let(:app) do - Fabricate(:app_with_watcher, email_at_notices: custom_thresholds) + create(:app_with_watcher, email_at_notices: custom_thresholds) end before do @@ -43,13 +47,17 @@ after { Errbit::Config.per_app_email_at_notices = false } - custom_thresholds.each do |threshold| - it "sends an email notification after #{threshold} notice(s)" do + it "sends an email notification" do + custom_thresholds.each do |threshold| # set to just before the threshold - @problem.update_attributes notices_count: threshold - 1 + @problem.update_attributes(notices_count: threshold - 1) - expect(Mailer).to receive(:err_notification). - and_return(double('email', deliver_now: true)) + expect(Mailer).to receive(:with) do + double.tap do |a| + expect(a).to receive(:err_notification) + .and_return(double("email", deliver_now: true)) + end + end error_report = ErrorReport.new(notice_attrs) error_report.generate_notice! @@ -57,36 +65,34 @@ end it "doesn't email after 5 notices" do - @problem.update_attributes notices_count: 5 + @problem.update_attributes(notices_count: 5) - expect(Mailer).to_not receive(:err_notification) + expect(Mailer).not_to receive(:with) error_report = ErrorReport.new(notice_attrs) error_report.generate_notice! end - it 'notify self if mailer fails' do - expect(Mailer).to receive(:err_notification).and_raise(ArgumentError) - expect(HoptoadNotifier).to receive(:notify) + it "notify self if mailer fails" do + expect(Mailer).to receive(:with).and_raise(ArgumentError) + expect(Errbit::SelfErrorReporter).to receive(:notify) ErrorReport.new(notice_attrs).generate_notice! end end - describe 'email notifications for resolved issues' do - let(:notification_service) { Fabricate(:campfire_notification_service) } + describe "email notifications for resolved issues" do + let(:notification_service) { create(:campfire_notification_service) } let(:app) do - Fabricate( - :app_with_watcher, - notify_on_errs: true, - email_at_notices: [1, 100] - ) + create(:app_with_watcher, + notify_on_errs: true, + email_at_notices: [1, 100]) end let(:notice_attrs) { notice_attrs_for.call(app.api_key) } before { Errbit::Config.per_app_email_at_notices = true } after { Errbit::Config.per_app_email_at_notices = false } - it 'sends email the first time after the error is resolved' do + it "sends email the first time after the error is resolved" do error_report = ErrorReport.new(notice_attrs) error_report.generate_notice! err = error_report.notice.err @@ -94,76 +100,70 @@ err.problem.update_attributes notices_count: 99 err.problem.resolve! - expect(Mailer).to receive(:err_notification). - and_return(double('email', deliver_now: true)) + expect(Mailer).to receive(:with) do + double.tap do |a| + expect(a).to receive(:err_notification) + .and_return(double("email", deliver_now: true)) + end + end ErrorReport.new(notice_attrs).generate_notice! end end - describe 'send email when notification service is configured but fails' do - let(:notification_service) { Fabricate(:campfire_notification_service) } + describe "send email when notification service is configured but fails" do + let(:notification_service) { create(:campfire_notification_service) } + let(:app) do - Fabricate( - :app_with_watcher, - notify_on_errs: true, - notification_service: notification_service - ) + create(:app_with_watcher, + notify_on_errs: true, + notification_service: notification_service) end + let(:notice_attrs) { notice_attrs_for.call(app.api_key) } before { Errbit::Config.per_app_notify_at_notices = true } + after { Errbit::Config.per_app_notify_at_notices = false } - it 'sends email' do + it "sends email" do error_report = ErrorReport.new(notice_attrs) - expect(error_report.app.notification_service). - to receive(:create_notification).and_raise(ArgumentError) - expect(Mailer). - to receive(:err_notification).and_return(double(deliver_now: true)) - - error_report.generate_notice! - end - end - - describe 'should not send a notification if a notification service is not' \ - 'configured' do - let(:notification_service) { Fabricate(:notification_service) } - let(:app) { Fabricate(:app, notification_service: notification_service) } - let(:notice_attrs) { notice_attrs_for.call(app.api_key) } + expect(error_report.app.notification_service) + .to receive(:create_notification).and_raise(ArgumentError) - before { Errbit::Config.per_app_notify_at_notices = true } - after { Errbit::Config.per_app_notify_at_notices = false } + expect(Mailer).to receive(:with) do + double.tap do |a| + expect(a).to receive(:err_notification) + .and_return(double(deliver_now: true)) + end + end + expect(Errbit::SelfErrorReporter).to receive(:notify).with(instance_of(ArgumentError)) - it "should not create a campfire notification" do - error_report = ErrorReport.new(notice_attrs) - expect(error_report.app.notification_service).to_not receive(:create_notification) error_report.generate_notice! end end - describe 'hipcat notifications' do - let(:notification_service) { Fabricate(:hipchat_notification_service) } + describe "should not send a notification if a notification service is not configured" do + let(:notification_service) { create(:notification_service) } + let(:app) { create(:app, notification_service: notification_service) } let(:notice_attrs) { notice_attrs_for.call(app.api_key) } - let(:app) { Fabricate(:app, notification_service: notification_service) } before { Errbit::Config.per_app_notify_at_notices = true } after { Errbit::Config.per_app_notify_at_notices = false } - it 'creates a hipchat notification' do + it "should not create a campfire notification" do error_report = ErrorReport.new(notice_attrs) - expect(error_report.app.notification_service). - to receive(:create_notification) + expect(error_report.app.notification_service).not_to receive(:create_notification) error_report.generate_notice! end end describe "should send a notification at desired intervals" do let(:notification_service) do - Fabricate(:campfire_notification_service, notify_at_notices: [1, 2]) + create(:campfire_notification_service, notify_at_notices: [1, 2]) end - let(:app) { Fabricate(:app, notification_service: notification_service) } + let(:app) { create(:app, notification_service: notification_service) } let(:notice_attrs) { notice_attrs_for.call(app.api_key) } before { Errbit::Config.per_app_notify_at_notices = true } @@ -171,16 +171,16 @@ it "should create a campfire notification on first notice" do error_report = ErrorReport.new(notice_attrs) - expect(error_report.app.notification_service). - to receive(:create_notification) + expect(error_report.app.notification_service) + .to receive(:create_notification) error_report.generate_notice! # one end it "should create a campfire notification on second notice" do ErrorReport.new(notice_attrs).generate_notice! # one error_report = ErrorReport.new(notice_attrs) - expect(error_report.app.notification_service). - to receive(:create_notification) + expect(error_report.app.notification_service) + .to receive(:create_notification) error_report.generate_notice! # two end @@ -188,8 +188,8 @@ ErrorReport.new(notice_attrs).generate_notice! # one ErrorReport.new(notice_attrs).generate_notice! # two error_report = ErrorReport.new(notice_attrs) - expect(error_report.app.notification_service). - to_not receive(:create_notification) + expect(error_report.app.notification_service) + .not_to receive(:create_notification) error_report.generate_notice! # three end @@ -198,8 +198,8 @@ notice = ErrorReport.new(notice_attrs).generate_notice! # two notice.problem.resolve! error_report = ErrorReport.new(notice_attrs) - expect(error_report.app.notification_service). - to receive(:create_notification) + expect(error_report.app.notification_service) + .to receive(:create_notification) error_report.generate_notice! # three end end diff --git a/spec/models/notice_spec.rb b/spec/models/notice_spec.rb index f2845d2cce..028d75e568 100644 --- a/spec/models/notice_spec.rb +++ b/spec/models/notice_spec.rb @@ -1,147 +1,159 @@ -describe Notice, type: 'model' do - context 'validations' do - it 'requires a backtrace' do - notice = Fabricate.build(:notice, backtrace: nil) - expect(notice).to_not be_valid +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Notice, type: :model do + context "validations" do + it "requires a backtrace" do + notice = build(:notice, backtrace: nil) + expect(notice.valid?).to eq(false) expect(notice.errors[:backtrace_id]).to include("can't be blank") end - it 'requires the server_environment' do - notice = Fabricate.build(:notice, server_environment: nil) - expect(notice).to_not be_valid + it "requires the server_environment" do + notice = build(:notice, server_environment: nil) + expect(notice.valid?).to eq(false) expect(notice.errors[:server_environment]).to include("can't be blank") end - it 'requires the notifier' do - notice = Fabricate.build(:notice, notifier: nil) - expect(notice).to_not be_valid + it "requires the notifier" do + notice = build(:notice, notifier: nil) + expect(notice.valid?).to eq(false) expect(notice.errors[:notifier]).to include("can't be blank") end end - describe '#message=' do + describe "#message=" do let(:long_message) do - 'Presently I heard a slight groan, and I knew it was the groan of ' \ - 'mortal terror. It was not a groan of pain or of grief --oh, no! ' \ - '--it was the low stifled sound that arises from the bottom of the ' \ - 'soul when overcharged with awe. I knew the sound well. Many a ' \ - 'night, just at midnight, when all the world slept, it has welled ' \ - 'up from my own bosom, deepening, with its dreadful echo, the ' \ - 'terrors that distracted me. I say I knew it well. I knew what the ' \ - 'old man felt, and pitied him, although I chuckled at heart. I ' \ - 'knew that he had been lying awake ever since the first slight ' \ - 'noise, when he had turned in the bed. His fears had been ever ' \ - 'since growing upon him. He had been trying to fancy them ' \ + "Presently I heard a slight groan, and I knew it was the groan of " \ + "mortal terror. It was not a groan of pain or of grief --oh, no! " \ + "--it was the low stifled sound that arises from the bottom of the " \ + "soul when overcharged with awe. I knew the sound well. Many a " \ + "night, just at midnight, when all the world slept, it has welled " \ + "up from my own bosom, deepening, with its dreadful echo, the " \ + "terrors that distracted me. I say I knew it well. I knew what the " \ + "old man felt, and pitied him, although I chuckled at heart. I " \ + "knew that he had been lying awake ever since the first slight " \ + "noise, when he had turned in the bed. His fears had been ever " \ + "since growing upon him. He had been trying to fancy them " \ 'causeless, but could not. He had been saying to himself --"It is ' \ - 'nothing but the wind in the chimney --it is only a mouse crossing ' \ + "nothing but the wind in the chimney --it is only a mouse crossing " \ 'the floor," or "It is merely a cricket which has made a single ' \ 'chirp." Yes, he had been trying to comfort himself with these ' \ - 'suppositions: but he had found all in vain. All in vain; because ' \ - 'Death, in approaching him had stalked with his black shadow ' \ - 'before him, and enveloped the victim. And it was the mournful ' \ - 'influence of the unperceived shadow that caused him to feel ' \ - '--although he neither saw nor heard --to feel the presence of my ' \ - 'head within the room. ' + "suppositions: but he had found all in vain. All in vain; because " \ + "Death, in approaching him had stalked with his black shadow " \ + "before him, and enveloped the victim. And it was the mournful " \ + "influence of the unperceived shadow that caused him to feel " \ + "--although he neither saw nor heard --to feel the presence of my " \ + "head within the room. " end - it 'truncates the message' do - notice = Fabricate(:notice, message: long_message) + it "truncates the message" do + notice = create(:notice, message: long_message) expect(long_message.length).to be > Notice::MESSAGE_LENGTH_LIMIT - expect(notice.message.length).to eq Notice::MESSAGE_LENGTH_LIMIT + expect(notice.message.length).to eq(Notice::MESSAGE_LENGTH_LIMIT) end let(:long_mb_message) do - 'Elasticsearch::Transport::Transport::Errors::InternalServerError: ' \ + "Elasticsearch::Transport::Transport::Errors::InternalServerError: " \ '[500] {"error":"SearchPhaseExecutionException[Failed to execute phase ' \ - '[query_fetch], all shards failed; shardFailures {[abc][test][0]: ' \ - 'QueryPhaseExecutionException[[test][0]: query[function score ' \ - '(_all:t4t44äöäöäööäöäöäöäöäälüöläpläfdälfäpdlsfaäpldspsadpfäsdkfasdö' \ - 'äkfadsökfjaädsfjsdaäfjadsklfldslsäfjkläsdajfläaslhfldskhfasljdhfl444' \ - '44t44t4t4t4t44t4444tt444tt4þt444t4gt4t444t44t444g4444t4g44g4tt444g44' \ - '44tgt444ggþ444þ4t4þ4t44444t4444g4444t44gþt4t4tþg4t44t4t4444gt44t444t' \ - '4t4t444tt44t44þt4t4þt4444444þgþ4tt4t4g444gt4t4t444þ44g4t44g4tgþ4t4t4' \ - '44t4þþ444t44t4t44~2,function=script[_score * _source.boost], params ' \ - '[null])],from[0],size[10]: Query Failed [Failed to execute main ' \ - 'query]]; nested: RuntimeException[org.apache.lucene.util.automaton.' \ - 'TooComplexToDeterminizeException: Determinizing automaton would ' \ - 'result in more than 10000 states.]; nested: TooComplexToDeterminize' \ - 'Exception[Determinizing automaton would result in more than 10000 ' \ + "[query_fetch], all shards failed; shardFailures {[abc][test][0]: " \ + "QueryPhaseExecutionException[[test][0]: query[function score " \ + "(_all:t4t44äöäöäööäöäöäöäöäälüöläpläfdälfäpdlsfaäpldspsadpfäsdkfasdö" \ + "äkfadsökfjaädsfjsdaäfjadsklfldslsäfjkläsdajfläaslhfldskhfasljdhfl444" \ + "44t44t4t4t4t44t4444tt444tt4þt444t4gt4t444t44t444g4444t4g44g4tt444g44" \ + "44tgt444ggþ444þ4t4þ4t44444t4444g4444t44gþt4t4tþg4t44t4t4444gt44t444t" \ + "4t4t444tt44t44þt4t4þt4444444þgþ4tt4t4g444gt4t4t444þ44g4t44g4tgþ4t4t4" \ + "44t4þþ444t44t4t44~2,function=script[_score * _source.boost], params " \ + "[null])],from[0],size[10]: Query Failed [Failed to execute main " \ + "query]]; nested: RuntimeException[org.apache.lucene.util.automaton." \ + "TooComplexToDeterminizeException: Determinizing automaton would " \ + "result in more than 10000 states.]; nested: TooComplexToDeterminize" \ + "Exception[Determinizing automaton would result in more than 10000 " \ 'states.]; }]","status":500}' end - it 'truncates the long multibyte string message' do - notice = Fabricate(:notice, message: long_mb_message) + it "truncates the long multibyte string message" do + notice = create(:notice, message: long_mb_message) expect(long_mb_message.bytesize).to be > Notice::MESSAGE_LENGTH_LIMIT - expect(notice.message.bytesize).to eq Notice::MESSAGE_LENGTH_LIMIT + expect(notice.message.bytesize).to eq(Notice::MESSAGE_LENGTH_LIMIT) end end describe "key sanitization" do before do - @hash = { "some.key" => { "$nested.key" => { "$Path" => "/", "some$key" => "key" } } } - @hash_sanitized = { "some.key" => { "$nested.key" => { "$Path" => "/", "some$key" => "key" } } } + @hash = {"some.key" => {"$nested.key" => {"$Path" => "/", "some$key" => "key"}}} + @hash_sanitized = {"some.key" => {"$nested.key" => {"$Path" => "/", "some$key" => "key"}}} end + [:server_environment, :request, :notifier].each do |key| it "replaces . with . and $ with $ in keys used in #{key}" do - err = Fabricate(:err) - notice = Fabricate(:notice, :err => err, key => @hash) - expect(notice.send(key)).to eq @hash_sanitized + err = create(:err) + notice = create(:notice, err: err, "#{key}": @hash) + expect(notice.send(key)).to eq(@hash_sanitized) end end end describe "user agent" do it "should be parsed and human-readable" do - notice = Fabricate.build(:notice, request: { 'cgi-data' => { - 'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16' - } }) - expect(notice.user_agent.browser).to eq 'Chrome' + notice = build(:notice, request: {"cgi-data" => { + "HTTP_USER_AGENT" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16" + }}) + expect(notice.user_agent.browser).to eq("Chrome") expect(notice.user_agent.version.to_s).to match(/^10\.0/) end it "should be nil if HTTP_USER_AGENT is blank" do - notice = Fabricate.build(:notice) - expect(notice.user_agent).to eq nil + notice = build(:notice) + expect(notice.user_agent).to eq(nil) end end describe "user agent string" do it "should be parsed and human-readable" do - notice = Fabricate.build(:notice, request: { 'cgi-data' => { 'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16' } }) - expect(notice.user_agent_string).to eq 'Chrome 10.0.648.204 (OS X 10.6.7)' + notice = build(:notice, request: {"cgi-data" => {"HTTP_USER_AGENT" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16"}}) + + expect(notice.user_agent_string).to eq("Chrome 10.0.648.204 (OS X 10.6.7)") end it "should be nil if HTTP_USER_AGENT is blank" do - notice = Fabricate.build(:notice) - expect(notice.user_agent_string).to eq "N/A" + notice = build(:notice) + + expect(notice.user_agent_string).to eq("N/A") end end describe "host" do it "returns host if url is valid" do - notice = Fabricate.build(:notice, request: { 'url' => "http://example.com/resource/12" }) - expect(notice.host).to eq 'example.com' + notice = build(:notice, request: {"url" => "http://example.com/resource/12"}) + + expect(notice.host).to eq("example.com") end it "returns 'N/A' when url is not valid" do - notice = Fabricate.build(:notice, request: { 'url' => "file:///path/to/some/resource/12" }) - expect(notice.host).to eq 'N/A' + notice = build(:notice, request: {"url" => "file:///path/to/some/resource/12"}) + + expect(notice.host).to eq("N/A") end it "returns 'N/A' when url is not valid" do - notice = Fabricate.build(:notice, request: { 'url' => "some string" }) - expect(notice.host).to eq 'N/A' + notice = build(:notice, request: {"url" => "some string"}) + + expect(notice.host).to eq("N/A") end it "returns 'N/A' when url is empty" do - notice = Fabricate.build(:notice, request: {}) - expect(notice.host).to eq 'N/A' + notice = build(:notice, request: {}) + + expect(notice.host).to eq("N/A") end end describe "request" do it "returns empty hash if not set" do notice = Notice.new + expect(notice.request).to eq({}) end end @@ -149,13 +161,17 @@ describe "env_vars" do it "returns the cgi-data" do notice = Notice.new - notice.request = { 'cgi-data' => { 'ONE' => 'TWO' } } - expect(notice.env_vars).to eq('ONE' => 'TWO') + + notice.request = {"cgi-data" => {"ONE" => "TWO"}} + + expect(notice.env_vars).to eq("ONE" => "TWO") end it "always returns a hash" do notice = Notice.new - notice.request = { 'cgi-data' => [] } + + notice.request = {"cgi-data" => []} + expect(notice.env_vars).to eq({}) end end diff --git a/spec/models/notification_service/campfire_service_spec.rb b/spec/models/notification_service/campfire_service_spec.rb deleted file mode 100644 index b236c38eeb..0000000000 --- a/spec/models/notification_service/campfire_service_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -describe NotificationServices::CampfireService, type: 'model' do - it "it should send a notification to campfire" do - # setup - notice = Fabricate :notice - notification_service = Fabricate :campfire_notification_service, app: notice.app - problem = notice.problem - - # campy stubbing - campy = double('CampfireService') - allow(Campy::Room).to receive(:new).and_return(campy) - allow(campy).to receive(:speak).and_return(true) - - # assert - expect(campy).to receive(:speak) - - notification_service.create_notification(problem) - end -end diff --git a/spec/models/notification_service/flowdock_service_spec.rb b/spec/models/notification_service/flowdock_service_spec.rb deleted file mode 100644 index 641c59eaaa..0000000000 --- a/spec/models/notification_service/flowdock_service_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -describe NotificationServices::FlowdockService, type: 'model' do - let(:service) { Fabricate.build(:flowdock_notification_service) } - let(:app) { Fabricate(:app, name: 'App #3') } - let(:problem) { Fabricate(:problem, app: app, message: '<3') } - - it 'sends message in appropriate format' do - allow_any_instance_of(Flowdock::Flow).to receive(:push_to_team_inbox) do |*args| - expect(args[1][:content]).to_not include('<3') - expect(args[1][:content]).to include('<3') - expect(args[1][:project]).to eq('App3') - end - - service.create_notification(problem) - end -end diff --git a/spec/models/notification_service/gtalk_service_spec.rb b/spec/models/notification_service/gtalk_service_spec.rb deleted file mode 100644 index 40a7a6d397..0000000000 --- a/spec/models/notification_service/gtalk_service_spec.rb +++ /dev/null @@ -1,125 +0,0 @@ -describe NotificationServices::GtalkService, type: 'model' do - it "it should send a notification to gtalk" do - # setup - notice = Fabricate :notice - notice.problem - notification_service = Fabricate :gtalk_notification_service, app: notice.app - problem = notice.problem - - # gtalk stubbing - gtalk = double('GtalkService') - jid = double("jid") - message = double("message") - expect(Jabber::JID).to receive(:new).with(notification_service.subdomain).and_return(jid) - expect(Jabber::Client).to receive(:new).with(jid).and_return(gtalk) - expect(gtalk).to receive(:connect).with(notification_service.service) - expect(gtalk).to receive(:auth).with(notification_service.api_token) - message_value = """#{problem.app.name} -#{Errbit::Config.protocol}://#{Errbit::Config.host}/apps/#{problem.app.id} -#{notification_service.notification_description problem}""" - - expect(Jabber::Message).to receive(:new).with(notification_service.user_id, message_value).and_return(message) - expect(Jabber::Message).to receive(:new).with(notification_service.room_id, message_value).and_return(message) - - expect(Jabber::MUC::SimpleMUCClient).to receive(:new).and_return(gtalk) - expect(gtalk).to receive(:join).with(notification_service.room_id + "/errbit") - - # assert - expect(gtalk).to receive(:send).exactly(2).times.with(message) - expect(gtalk).to receive(:close) - - notification_service.create_notification(problem) - end - - describe "multiple users_ids" do - before(:each) do - # setup - @notice = Fabricate :notice - @notification_service = Fabricate :gtalk_notification_service, app: @notice.app - @problem = @notice.problem - @error_msg = """#{@problem.app.name} -#{Errbit::Config.protocol}://#{Errbit::Config.host}/apps/#{@problem.app.id} -#{@notification_service.notification_description @problem}""" - - # gtalk stubbing - @gtalk = double('GtalkService') - expect(@gtalk).to receive(:connect) - expect(@gtalk).to receive(:auth) - jid = double("jid") - allow(Jabber::JID).to receive(:new).with(@notification_service.subdomain).and_return(jid) - allow(Jabber::Client).to receive(:new).with(jid).and_return(@gtalk) - end - it "should send a notification to all ',' separated users" do - expect(Jabber::Message).to receive(:new).with("first@domain.org", @error_msg) - expect(Jabber::Message).to receive(:new).with("second@domain.org", @error_msg) - expect(Jabber::Message).to receive(:new).with("third@domain.org", @error_msg) - expect(Jabber::Message).to receive(:new).with("fourth@domain.org", @error_msg) - expect(Jabber::MUC::SimpleMUCClient).to_not receive(:new) - expect(@gtalk).to receive(:send).exactly(4).times - expect(@gtalk).to receive(:close) - - @notification_service.user_id = "first@domain.org,second@domain.org, third@domain.org , fourth@domain.org " - @notification_service.room_id = "" - @notification_service.create_notification(@problem) - end - it "should send a notification to all ';' separated users" do - expect(Jabber::Message).to receive(:new).with("first@domain.org", @error_msg) - expect(Jabber::Message).to receive(:new).with("second@domain.org", @error_msg) - expect(Jabber::Message).to receive(:new).with("third@domain.org", @error_msg) - expect(Jabber::Message).to receive(:new).with("fourth@domain.org", @error_msg) - expect(Jabber::MUC::SimpleMUCClient).to_not receive(:new) - expect(@gtalk).to receive(:send).exactly(4).times - expect(@gtalk).to receive(:close) - - @notification_service.user_id = "first@domain.org;second@domain.org; third@domain.org ; fourth@domain.org " - @notification_service.room_id = "" - @notification_service.create_notification(@problem) - end - it "should send a notification to all ' ' separated users" do - expect(Jabber::Message).to receive(:new).with("first@domain.org", @error_msg) - expect(Jabber::Message).to receive(:new).with("second@domain.org", @error_msg) - expect(Jabber::Message).to receive(:new).with("third@domain.org", @error_msg) - expect(Jabber::Message).to receive(:new).with("fourth@domain.org", @error_msg) - expect(Jabber::MUC::SimpleMUCClient).to_not receive(:new) - expect(@gtalk).to receive(:send).exactly(4).times - expect(@gtalk).to receive(:close) - - @notification_service.user_id = "first@domain.org second@domain.org third@domain.org fourth@domain.org " - @notification_service.room_id = "" - @notification_service.create_notification(@problem) - end - end - - it "it should send a notification to room only" do - # setup - notice = Fabricate :notice - notice.problem - notification_service = Fabricate :gtalk_notification_service, app: notice.app - problem = notice.problem - - # gtalk stubbing - gtalk = double('GtalkService') - jid = double("jid") - message = double("message") - expect(Jabber::JID).to receive(:new).with(notification_service.subdomain).and_return(jid) - expect(Jabber::Client).to receive(:new).with(jid).and_return(gtalk) - expect(gtalk).to receive(:connect) - expect(gtalk).to receive(:auth).with(notification_service.api_token) - message_value = """#{problem.app.name} -#{Errbit::Config.protocol}://#{Errbit::Config.host}/apps/#{problem.app.id} -#{notification_service.notification_description problem}""" - - expect(Jabber::Message).to receive(:new).with(notification_service.room_id, message_value).and_return(message) - - expect(Jabber::MUC::SimpleMUCClient).to receive(:new).and_return(gtalk) - expect(gtalk).to receive(:join).with(notification_service.room_id + "/errbit") - - notification_service.user_id = "" - - # assert - expect(gtalk).to receive(:send).with(message) - expect(gtalk).to receive(:close) - - notification_service.create_notification(problem) - end -end diff --git a/spec/models/notification_service/hipchat_service_spec.rb b/spec/models/notification_service/hipchat_service_spec.rb deleted file mode 100644 index b66f4c6c31..0000000000 --- a/spec/models/notification_service/hipchat_service_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -describe NotificationServices::HipchatService, type: 'model' do - let(:service) { Fabricate.build(:hipchat_notification_service) } - let(:problem) { Fabricate(:problem) } - let(:room) { double } - - before do - allow_any_instance_of(HipChat::Client).to receive(:[]).and_return(room) - end - - describe '#check_params' do - context 'empty field check' do - %w(service api_token room_id).each do |field| - it "'doesn\'t allow #{field} to be empty'" do - service[field.to_sym] = '' - service.check_params - expect(service.errors).to include(field.to_sym) - end - end - end - - context 'API version field check' do - %w(v1 v2).each do |version| - it "allows #{version}" do - service[:service] = version - service.check_params - expect(service.errors).to_not include(:service) - end - end - - it 'doesn\t allow an unknown version' do - service[:service] = 'vFOO' - service.check_params - expect(service.errors).to include(:service) - end - end - end - - it 'sends message' do - expect(room).to receive(:send) - service.create_notification(problem) - end - - it 'escapes html in message' do - allow(problem).to receive(:message).and_return('<3') - expect(room).to receive(:send) do |_, message| - expect(message).to_not include('<3') - expect(message).to include('<3') - end - service.create_notification(problem) - end -end diff --git a/spec/models/notification_service/hoiio_service_spec.rb b/spec/models/notification_service/hoiio_service_spec.rb deleted file mode 100644 index 3d823f1df9..0000000000 --- a/spec/models/notification_service/hoiio_service_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -describe NotificationServices::HoiioService, type: 'model' do - it "it should send a notification to hoiio" do - # setup - notice = Fabricate :notice - notification_service = Fabricate :hoiio_notification_service, app: notice.app - problem = notice.problem - - # hoi stubbing - sms = double('HoiioService') - allow(Hoi::SMS).to receive(:new).and_return(sms) - allow(sms).to receive(:send).and_return(true) - - # assert - expect(sms).to receive(:send) - - notification_service.create_notification(problem) - end -end diff --git a/spec/models/notification_service/hubot_service_spec.rb b/spec/models/notification_service/hubot_service_spec.rb deleted file mode 100644 index 029c6d4ac9..0000000000 --- a/spec/models/notification_service/hubot_service_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -describe NotificationServices::HubotService, type: 'model' do - it "it should send a notification to Hubot" do - # setup - notice = Fabricate :notice - notification_service = Fabricate :hubot_notification_service, app: notice.app - problem = notice.problem - - # faraday stubbing - expect(HTTParty).to receive(:post).with(notification_service.api_token, body: { message: an_instance_of(String), room: notification_service.room_id }).and_return(true) - - notification_service.create_notification(problem) - end -end diff --git a/spec/models/notification_service/pushover_service_spec.rb b/spec/models/notification_service/pushover_service_spec.rb deleted file mode 100644 index 9d006beebb..0000000000 --- a/spec/models/notification_service/pushover_service_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -describe NotificationServices::PushoverService, type: 'model' do - it "it should send a notification to Pushover" do - # setup - notice = Fabricate :notice - notification_service = Fabricate :pushover_notification_service, app: notice.app - problem = notice.problem - - # hoi stubbing - notification = double('PushoverService') - allow(Rushover::Client).to receive(:new).and_return(notification) - allow(notification).to receive(:notify).and_return(true) - - # assert - expect(notification).to receive(:notify) - - notification_service.create_notification(problem) - end -end diff --git a/spec/models/notification_service/webhook_service_spec.rb b/spec/models/notification_service/webhook_service_spec.rb deleted file mode 100644 index 9f1266b389..0000000000 --- a/spec/models/notification_service/webhook_service_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -describe NotificationServices::WebhookService, type: 'model' do - it "it should send a notification to a user-specified URL" do - notice = Fabricate :notice - notification_service = Fabricate :webhook_notification_service, app: notice.app - problem = notice.problem - - payload = notification_service.message_for_webhook(problem) - expect(HTTParty).to receive(:post).with(notification_service.api_token, headers: { 'Content-Type' => 'application/json', 'User-Agent' => 'Errbit' }, body: payload.to_json).and_return(true) - - notification_service.create_notification(problem) - end - - context "#message_for_webhook" do - it "should return an hash with all the keys nested inside 'problem' key" do - notice = Fabricate :notice - notification_service = Fabricate :webhook_notification_service, app: notice.app - problem = notice.problem - - payload = notification_service.message_for_webhook(problem) - - expect(payload[:problem]).to be_a(Hash) - end - end -end diff --git a/spec/models/notification_services/campfire_service_spec.rb b/spec/models/notification_services/campfire_service_spec.rb new file mode 100644 index 0000000000..2b86941730 --- /dev/null +++ b/spec/models/notification_services/campfire_service_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe NotificationServices::CampfireService, type: :model do + it "should send a notification to campfire" do + notice = create(:notice) + notification_service = create(:campfire_notification_service, app: notice.app) + problem = notice.problem + + campy = double("CampfireService") + allow(Campy::Room).to receive(:new).and_return(campy) + allow(campy).to receive(:speak).and_return(true) + expect(campy).to receive(:speak) + + notification_service.create_notification(problem) + end +end diff --git a/spec/models/notification_services/hoiio_service_spec.rb b/spec/models/notification_services/hoiio_service_spec.rb new file mode 100644 index 0000000000..20545dc0b0 --- /dev/null +++ b/spec/models/notification_services/hoiio_service_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe NotificationServices::HoiioService, type: :model do + it "should send a notification to hoiio" do + notice = create(:notice) + notification_service = create(:hoiio_notification_service, app: notice.app) + problem = notice.problem + + sms = double("HoiioService") + allow(Hoi::SMS).to receive(:new).and_return(sms) + allow(sms).to receive(:send).and_return(true) + + expect(sms).to receive(:send) + + notification_service.create_notification(problem) + end +end diff --git a/spec/models/notification_services/hubot_service_spec.rb b/spec/models/notification_services/hubot_service_spec.rb new file mode 100644 index 0000000000..49d2f189da --- /dev/null +++ b/spec/models/notification_services/hubot_service_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe NotificationServices::HubotService, type: :model do + it "should send a notification to Hubot" do + notice = create(:notice) + notification_service = create(:hubot_notification_service, app: notice.app) + problem = notice.problem + + expect(HTTParty).to receive(:post).with(notification_service.api_token, body: {message: an_instance_of(String), room: notification_service.room_id}).and_return(true) + + notification_service.create_notification(problem) + end +end diff --git a/spec/models/notification_services/pushover_service_spec.rb b/spec/models/notification_services/pushover_service_spec.rb new file mode 100644 index 0000000000..60e63c8913 --- /dev/null +++ b/spec/models/notification_services/pushover_service_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe NotificationServices::PushoverService, type: :model do + it "should send a notification to Pushover.net" do + notice = create(:notice) + notification_service = create(:pushover_notification_service, app: notice.app) + problem = notice.problem + + expect(Pushover2::Message).to receive(:new) + .with( + token: notification_service.subdomain, + user: notification_service.api_token, + message: notification_service.notification_description(problem), + priority: 1, + title: "Errbit Notification", + url: "https://#{Errbit::Config.host}/apps/#{problem.app.id}", + url_title: "Link to error" + ) do + double.tap do |a| + expect(a).to receive(:push) + end + end + + notification_service.create_notification(problem) + end +end diff --git a/spec/models/notification_service/slack_service_spec.rb b/spec/models/notification_services/slack_service_spec.rb similarity index 53% rename from spec/models/notification_service/slack_service_spec.rb rename to spec/models/notification_services/slack_service_spec.rb index 9b0206b074..4265617190 100644 --- a/spec/models/notification_service/slack_service_spec.rb +++ b/spec/models/notification_services/slack_service_spec.rb @@ -1,27 +1,39 @@ -describe NotificationServices::SlackService, type: 'model' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe NotificationServices::SlackService, type: :model do let(:backtrace) do - Fabricate :backtrace, lines: [ - { number: 22, file: "/path/to/file/1.rb", method: 'first_method' }, - { number: 44, file: "/path/to/file/2.rb", method: 'second_method' }, - { number: 11, file: "/path/to/file/3.rb", method: 'third_method' }, - { number: 103, file: "/path/to/file/4.rb", method: 'fourth_method' }, - { number: 923, file: "/path/to/file/5.rb", method: 'fifth_method' }, - { number: 8, file: "/path/to/file/6.rb", method: 'sixth_method' } - ] + create(:backtrace, + lines: [ + {number: 22, file: "/path/to/file/1.rb", method: "first_method"}, + {number: 44, file: "/path/to/file/2.rb", method: "second_method"}, + {number: 11, file: "/path/to/file/3.rb", method: "third_method"}, + {number: 103, file: "/path/to/file/4.rb", method: "fourth_method"}, + {number: 923, file: "/path/to/file/5.rb", method: "fifth_method"}, + {number: 8, file: "/path/to/file/6.rb", method: "sixth_method"} + ]) end - let(:notice) { Fabricate :notice, backtrace: backtrace } + + let(:notice) { create(:notice, backtrace: backtrace) } + let(:problem) { notice.problem } + let(:service_url) do "https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXX" end + let(:service) do - Fabricate :slack_notification_service, app: notice.app, - service_url: service_url, - room_id: room_id + create(:slack_notification_service, + app: notice.app, + service_url: service_url, + room_id: room_id) end + let(:room_id) do "#general" end + let(:backtrace_lines) do lines = "/path/to/file/1.rb:22 → first_method\n" \ "/path/to/file/2.rb:44 → second_method\n" \ @@ -31,21 +43,20 @@ "```#{lines}```" end - # faraday stubbing let(:payload_hash) do { - username: "Errbit", - icon_url: "https://raw.githubusercontent.com/errbit/errbit/master/docs/notifications/slack/errbit.png", - channel: room_id, + username: "Errbit", + icon_url: "https://raw.githubusercontent.com/errbit/errbit/main/docs/notifications/slack/errbit.png", + channel: room_id, attachments: [ { - fallback: service.message_for_slack(problem), - title: problem.message.to_s.truncate(100), + fallback: service.message_for_slack(problem), + title: problem.message.to_s.truncate(100), title_link: problem.url, - text: problem.where, - color: "#D00000", - mrkdwn_in: ["fields"], - fields: [ + text: problem.where, + color: "#D00000", + mrkdwn_in: ["fields"], + fields: [ { title: "Application", value: problem.app.name, @@ -63,7 +74,7 @@ }, { title: "First Noticed", - value: problem.first_notice_at.try(:localtime).try(:to_s, :db), + value: problem.first_notice_at.try(:localtime).try(:to_fs, :db), short: true }, { @@ -81,13 +92,13 @@ expect(Rails.root.join("docs/notifications/slack/errbit.png")).to exist end - context 'Validations' do - it 'validates presence of service_url' do + context "Validations" do + it "validates presence of service_url" do service.service_url = "" service.valid? - expect(service.errors[:service_url]). - to include("You must specify your Slack Hook url") + expect(service.errors[:service_url]) + .to include("You must specify your Slack Hook url") service.service_url = service_url service.valid? @@ -95,12 +106,12 @@ expect(service.errors[:service_url]).to be_blank end - it 'validates format of room_id' do + it "validates format of room_id" do service.room_id = "INVALID NAME" service.valid? - expect(service.errors[:room_id]). - to include("Slack channel name must be lowercase, with no space, special character, or periods.") + expect(service.errors[:room_id]) + .to include("Slack channel name must be lowercase, with no space, special character, or periods.") service.room_id = "#valid-room-name" service.valid? @@ -109,27 +120,27 @@ end end - context 'with room_id' do + context "with room_id" do it "should send a notification to Slack with hook url and channel" do payload = payload_hash.to_json - expect(HTTParty).to receive(:post). - with(service.service_url, body: payload, headers: { "Content-Type" => "application/json" }). - and_return(true) + expect(HTTParty).to receive(:post) + .with(service.service_url, body: payload, headers: {"Content-Type" => "application/json"}) + .and_return(true) service.create_notification(problem) end end - context 'without room_id' do + context "without room_id" do let(:room_id) { nil } it "should send a notification to Slack with hook url and without channel" do payload = payload_hash.except(:channel).to_json - expect(HTTParty).to receive(:post). - with(service.service_url, body: payload, headers: { "Content-Type" => "application/json" }). - and_return(true) + expect(HTTParty).to receive(:post) + .with(service.service_url, body: payload, headers: {"Content-Type" => "application/json"}) + .and_return(true) service.create_notification(problem) end diff --git a/spec/models/notification_services/webhook_service_spec.rb b/spec/models/notification_services/webhook_service_spec.rb new file mode 100644 index 0000000000..e44017bf49 --- /dev/null +++ b/spec/models/notification_services/webhook_service_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe NotificationServices::WebhookService, type: :model do + it "should send a notification to a user-specified URL" do + notice = create(:notice) + notification_service = create(:webhook_notification_service, app: notice.app) + problem = notice.problem + + payload = notification_service.message_for_webhook(problem) + + expect(HTTParty).to receive(:post).with(notification_service.api_token, headers: {"Content-Type" => "application/json", "User-Agent" => "Errbit"}, body: payload.to_json).and_return(true) + + notification_service.create_notification(problem) + end + + describe "#message_for_webhook" do + it "should return an hash with all the keys nested inside 'problem' key" do + notice = create(:notice) + notification_service = create(:webhook_notification_service, app: notice.app) + problem = notice.problem + + payload = notification_service.message_for_webhook(problem) + + expect(payload[:problem]).to be_a(Hash) + end + end +end diff --git a/spec/models/problem_spec.rb b/spec/models/problem_spec.rb index 70206dc718..8da72f083b 100644 --- a/spec/models/problem_spec.rb +++ b/spec/models/problem_spec.rb @@ -1,323 +1,338 @@ -describe Problem, type: 'model' do - context 'validations' do - it 'requires an environment' do - err = Fabricate.build(:problem, environment: nil) - expect(err).to_not be_valid +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Problem, type: :model do + context "validations" do + it "requires an environment" do + err = build(:problem, environment: nil) + + expect(err.valid?).to eq(false) + expect(err.errors[:environment]).to include("can't be blank") end end - describe "Fabrication" do - context "Fabricate(:problem)" do - it 'should have no comment' do - expect do - Fabricate(:problem) - end.to_not change(Comment, :count) + describe "FactoryBot" do + context "create(:problem)" do + it "should have no comment" do + expect { create(:problem) }.not_to change(Comment, :count) end end - context "Fabricate(:problem_with_comments)" do - it 'should have 3 comments' do - expect do - Fabricate(:problem_with_comments) - end.to change(Comment, :count).by(3) + context "create(:problem_with_comments)" do + it "should have 3 comments" do + expect { create(:problem_with_comments) }.to change(Comment, :count).by(3) end end - context "Fabricate(:problem_with_errs)" do - it 'should have 3 errs' do - expect do - Fabricate(:problem_with_errs) - end.to change(Err, :count).by(3) + context "create(:problem_with_errs)" do + it "should have 3 errs" do + expect { create(:problem_with_errs) }.to change(Err, :count).by(3) end end end - context '#last_notice_at' do + describe "#last_notice_at" do it "returns the created_at timestamp of the latest notice" do - err = Fabricate(:err) + err = create(:err) problem = err.problem - expect(problem).to_not be_nil + expect(problem).not_to eq(nil) - notice1 = Fabricate(:notice, err: err) - expect(problem.last_notice_at).to eq notice1.reload.created_at + notice_1 = create(:notice, err: err) + expect(problem.last_notice_at).to eq(notice_1.reload.created_at) - notice2 = Fabricate(:notice, err: err) - expect(problem.last_notice_at).to eq notice2.reload.created_at + notice_2 = create(:notice, err: err) + expect(problem.last_notice_at).to eq(notice_2.reload.created_at) end end - context '#first_notice_at' do + describe "#first_notice_at" do it "returns the created_at timestamp of the first notice" do - err = Fabricate(:err) + err = create(:err) problem = err.problem - expect(problem).to_not be_nil + expect(problem).not_to eq(nil) - notice1 = Fabricate(:notice, err: err) - expect(problem.first_notice_at.to_i).to be_within(1).of(notice1.created_at.to_i) + notice_1 = create(:notice, err: err) + expect(problem.first_notice_at.to_i).to be_within(1).of(notice_1.created_at.to_i) - Fabricate(:notice, err: err) - expect(problem.first_notice_at.to_i).to be_within(1).of(notice1.created_at.to_i) + create(:notice, err: err) + expect(problem.first_notice_at.to_i).to be_within(1).of(notice_1.created_at.to_i) end end - context '#message' do + describe "#message" do it "adding a notice caches its message" do - err = Fabricate(:err) + err = create(:err) problem = err.problem expect do - Fabricate(:notice, err: err, message: 'ERR 1') - end.to change(problem, :message).from(nil).to('ERR 1') + create(:notice, err: err, message: "ERR 1") + end.to change(problem, :message).from(nil).to("ERR 1") end end - context 'being created' do - context 'when the app has err notifications set to false' do - it 'should not send an email notification' do - app = Fabricate(:app_with_watcher, notify_on_errs: false) - expect(Mailer).to_not receive(:err_notification) - Fabricate(:problem, app: app) + context "being created" do + context "when the app has err notifications set to false" do + it "should not send an email notification" do + app = create(:app_with_watcher, notify_on_errs: false) + expect(Mailer).not_to receive(:with) + create(:problem, app: app) end end end - context "#resolved?" do + describe "#resolved?" do it "should start out as unresolved" do - problem = Problem.new - expect(problem).to_not be_resolved + problem = described_class.new + expect(problem).not_to be_resolved expect(problem).to be_unresolved end it "should be able to be resolved" do - problem = Fabricate(:problem) - expect(problem).to_not be_resolved + problem = create(:problem) + expect(problem).not_to be_resolved problem.resolve! expect(problem.reload).to be_resolved end end - context "resolve!" do + describe "#resolve!" do it "marks the problem as resolved" do - problem = Fabricate(:problem) - expect(problem).to_not be_resolved + problem = create(:problem) + expect(problem).not_to be_resolved problem.resolve! expect(problem).to be_resolved end it "should record the time when it was resolved" do - problem = Fabricate(:problem) + problem = create(:problem) expected_resolved_at = Time.zone.now - Timecop.freeze(expected_resolved_at) do + + travel_to(expected_resolved_at) do problem.resolve! end - expect(problem.resolved_at.to_s).to eq expected_resolved_at.to_s + + expect(problem.resolved_at.to_s).to eq(expected_resolved_at.to_s) end it "should not reset notice count" do - problem = Fabricate(:problem, notices_count: 1) + problem = create(:problem, notices_count: 1) original_notices_count = problem.notices_count expect(original_notices_count).to be > 0 problem.resolve! - expect(problem.notices_count).to eq original_notices_count + expect(problem.notices_count).to eq(original_notices_count) end it "should throw an err if it's not successful" do - problem = Fabricate(:problem) - expect(problem).to_not be_resolved + problem = create(:problem) + expect(problem).not_to be_resolved allow(problem).to receive(:valid?).and_return(false) ## update_attributes not test #valid? but #errors.any? # https://github.com/mongoid/mongoid/blob/master/lib/mongoid/persistence.rb#L137 er = ActiveModel::Errors.new(problem) - er.add_on_blank(:resolved) + er.add(:resolved, :blank) allow(problem).to receive(:errors).and_return(er) - expect(problem).to_not be_valid + expect(problem.valid?).to eq(false) expect do problem.resolve! end.to raise_error(Mongoid::Errors::Validations) end end - context "#unmerge!" do + describe "#unmerge!" do it "creates a separate problem for each err" do - problem1 = Fabricate(:notice).problem - problem2 = Fabricate(:notice).problem - merged_problem = Problem.merge!(problem1, problem2) - expect(merged_problem.errs.length).to eq 2 + problem_1 = create(:notice).problem + problem_2 = create(:notice).problem + merged_problem = Problem.merge!(problem_1, problem_2) + expect(merged_problem.errs.length).to eq(2) expect { merged_problem.unmerge! }.to change(Problem, :count).by(1) - expect(merged_problem.errs(true).length).to eq 1 + expect(merged_problem.errs(true).length).to eq(1) end it "runs smoothly for problem without errs" do - expect { Fabricate(:problem).unmerge! }.not_to raise_error + expect { create(:problem).unmerge! }.not_to raise_error end end context "Scopes" do context "resolved" do - it 'only finds resolved Problems' do - resolved = Fabricate(:problem, resolved: true) - unresolved = Fabricate(:problem, resolved: false) - expect(Problem.resolved.all).to include(resolved) - expect(Problem.resolved.all).to_not include(unresolved) + it "only finds resolved Problems" do + resolved = create(:problem, resolved: true) + create(:problem, resolved: false) + + expect(Problem.resolved.all).to eq([resolved]) end end context "unresolved" do - it 'only finds unresolved Problems' do - resolved = Fabricate(:problem, resolved: true) - unresolved = Fabricate(:problem, resolved: false) - expect(Problem.unresolved.all).to_not include(resolved) - expect(Problem.unresolved.all).to include(unresolved) + it "only finds unresolved Problems" do + create(:problem, resolved: true) + unresolved = create(:problem, resolved: false) + + expect(Problem.unresolved.all).to eq([unresolved]) end end context "searching" do - it 'finds the correct record' do - find = Fabricate(:problem, resolved: false, error_class: 'theErrorclass::other', - message: "other", where: 'errorclass', environment: 'development', app_name: 'other') - dont_find = Fabricate(:problem, resolved: false, error_class: "Batman", - message: 'todo', where: 'classerror', environment: 'development', app_name: 'other') + it "finds the correct record" do + find = create(:problem, resolved: false, error_class: "theErrorclass::other", + message: "other", where: "errorclass", environment: "development", app_name: "other") + dont_find = create(:problem, resolved: false, error_class: "Batman", + message: "todo", where: "classerror", environment: "development", app_name: "other") expect(Problem.search("theErrorClass").unresolved).to include(find) - expect(Problem.search("theErrorClass").unresolved).to_not include(dont_find) + expect(Problem.search("theErrorClass").unresolved).not_to include(dont_find) + end + + it "find on where message" do + problem = create(:problem, where: "cyril") + expect(Problem.search("cyril").entries).to eq([problem]) end - it 'find on where message' do - problem = Fabricate(:problem, where: 'cyril') - expect(Problem.search('cyril').entries).to eq [problem] + + it "finds with notice_id as argument" do + app = create(:app) + problem = create(:problem, app: app) + err = create(:err, problem: problem) + notice = create(:notice, err: err, message: "ERR 1") + + problem_2 = create(:problem, where: "cyril") + expect(problem_2).not_to eq(problem) + expect(Problem.search(notice.id).entries).to eq([problem]) end end end context "notice counter cache" do before do - @app = Fabricate(:app) - @problem = Fabricate(:problem, app: @app) - @err = Fabricate(:err, problem: @problem) + @app = create(:app) + @problem = create(:problem, app: @app) + @err = create(:err, problem: @problem) end it "#notices_count returns 0 by default" do - expect(@problem.notices_count).to eq 0 + expect(@problem.notices_count).to eq(0) end it "adding a notice increases #notices_count by 1" do expect do - Fabricate(:notice, err: @err, message: 'ERR 1') - end.to change(@problem.reload, :notices_count).from(0).to(1) + create(:notice, err: @err, message: "ERR 1") + end.to change(@problem.reload, :notices_count).by(1) end it "removing a notice decreases #notices_count by 1" do - Fabricate(:notice, err: @err, message: 'ERR 1') + create(:notice, err: @err, message: "ERR 1") expect do @err.notices.first.destroy @problem.reload - end.to change(@problem, :notices_count).from(1).to(0) + end.to change(@problem, :notices_count).by(-1) end end context "sparklines-related methods" do before do - @app = Fabricate(:app) - @problem = Fabricate(:problem, app: @app) - @err = Fabricate(:err, problem: @problem) + @app = create(:app) + @problem = create(:problem, app: @app) + @err = create(:err, problem: @problem) end it "gets correct notice counts when grouping by day" do now = Time.current two_weeks_ago = 13.days.ago - Fabricate(:notice, err: @err, message: 'ERR 1') - Fabricate(:notice, err: @err, message: 'ERR 2', created_at: 3.days.ago) - Fabricate(:notice, err: @err, message: 'ERR 3', created_at: 3.days.ago) + create(:notice, err: @err, message: "ERR 1") + create(:notice, err: @err, message: "ERR 2", created_at: 3.days.ago) + create(:notice, err: @err, message: "ERR 3", created_at: 3.days.ago) three_days_ago_yday = (now - 3.days).yday - three_days_ago = @problem.grouped_notice_counts(two_weeks_ago, 'day').detect { |grouping| grouping['_id']['day'] == three_days_ago_yday } - expect(three_days_ago['count']).to eq(2) - count_by_day_for_last_two_weeks = @problem.zero_filled_grouped_noticed_counts(two_weeks_ago, 'day').map { |h| h.values.first } + three_days_ago = @problem.grouped_notice_counts(two_weeks_ago, "day").detect { |grouping| grouping["_id"]["day"] == three_days_ago_yday } + expect(three_days_ago["count"]).to eq(2) + count_by_day_for_last_two_weeks = @problem.zero_filled_grouped_noticed_counts(two_weeks_ago, "day").map { |h| h.values.first } expect(count_by_day_for_last_two_weeks.size).to eq(14) expect(count_by_day_for_last_two_weeks).to eq([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 1]) end it "gets correct notice counts when grouping by hour" do twenty_four_hours_ago = 23.hours.ago - Fabricate(:notice, err: @err, message: 'ERR 1') - Fabricate(:notice, err: @err, message: 'ERR 2', created_at: 3.hours.ago) - Fabricate(:notice, err: @err, message: 'ERR 3', created_at: 3.hours.ago) - count_by_hour_for_last_24_hours = @problem.zero_filled_grouped_noticed_counts(twenty_four_hours_ago, 'hour').map { |h| h.values.first } + create(:notice, err: @err, message: "ERR 1") + create(:notice, err: @err, message: "ERR 2", created_at: 3.hours.ago) + create(:notice, err: @err, message: "ERR 3", created_at: 3.hours.ago) + count_by_hour_for_last_24_hours = @problem.zero_filled_grouped_noticed_counts(twenty_four_hours_ago, "hour").map { |h| h.values.first } expect(count_by_hour_for_last_24_hours.size).to eq(24) expect(count_by_hour_for_last_24_hours).to eq(([0] * 20) + [2, 0, 0, 1]) end it "gets correct relative percentages when grouping by hour" do two_weeks_ago = 13.days.ago - Fabricate(:notice, err: @err, message: 'ERR 1') - Fabricate(:notice, err: @err, message: 'ERR 2', created_at: 3.days.ago) - Fabricate(:notice, err: @err, message: 'ERR 3', created_at: 3.days.ago) - relative_percentages = @problem.grouped_notice_count_relative_percentages(two_weeks_ago, 'day') + create(:notice, err: @err, message: "ERR 1") + create(:notice, err: @err, message: "ERR 2", created_at: 3.days.ago) + create(:notice, err: @err, message: "ERR 3", created_at: 3.days.ago) + relative_percentages = @problem.grouped_notice_count_relative_percentages(two_weeks_ago, "day") expect(relative_percentages).to eq(([0] * 10) + [100, 0, 0, 50]) end it "gets correct relative percentages when grouping by hour" do twenty_four_hours_ago = 23.hours.ago - Fabricate(:notice, err: @err, message: 'ERR 1') - Fabricate(:notice, err: @err, message: 'ERR 2', created_at: 3.hours.ago) - Fabricate(:notice, err: @err, message: 'ERR 3', created_at: 3.hours.ago) - relative_percentages = @problem.grouped_notice_count_relative_percentages(twenty_four_hours_ago, 'hour') + create(:notice, err: @err, message: "ERR 1") + create(:notice, err: @err, message: "ERR 2", created_at: 3.hours.ago) + create(:notice, err: @err, message: "ERR 3", created_at: 3.hours.ago) + relative_percentages = @problem.grouped_notice_count_relative_percentages(twenty_four_hours_ago, "hour") expect(relative_percentages).to eq(([0] * 20) + [100, 0, 0, 50]) end it "gets correct relative percentages when all zeros for data" do two_weeks_ago = 13.days.ago - relative_percentages = @problem.grouped_notice_count_relative_percentages(two_weeks_ago, 'day') - expect(relative_percentages).to eq(([0] * 14)) + relative_percentages = @problem.grouped_notice_count_relative_percentages(two_weeks_ago, "day") + expect(relative_percentages).to eq([0] * 14) end end context "filtered" do before do - @app1 = Fabricate(:app) - @problem1 = Fabricate(:problem, app: @app1) + @app_1 = create(:app) + @problem_1 = create(:problem, app: @app_1) - @app2 = Fabricate(:app) - @problem2 = Fabricate(:problem, app: @app2) + @app_2 = create(:app) + @problem_2 = create(:problem, app: @app_2) - @app3 = Fabricate(:app) - @app3.update_attribute(:name, 'app3') + @app_3 = create(:app) + @app_3.update_attribute(:name, "app3") - @problem3 = Fabricate(:problem, app: @app3) + @problem_3 = create(:problem, app: @app_3) end it "#filtered returns problems but excludes those attached to the specified apps" do - expect(Problem.filtered("-app:'#{@app1.name}'")).to include(@problem2) - expect(Problem.filtered("-app:'#{@app1.name}'")).to_not include(@problem1) + expect(Problem.filtered("-app:'#{@app_1.name}'")).to include(@problem_2) + expect(Problem.filtered("-app:'#{@app_1.name}'")).not_to include(@problem_1) - filtered_results_with_two_exclusions = Problem.filtered("-app:'#{@app1.name}' -app:app3") - expect(filtered_results_with_two_exclusions).to_not include(@problem1) - expect(filtered_results_with_two_exclusions).to include(@problem2) - expect(filtered_results_with_two_exclusions).to_not include(@problem3) + filtered_results_with_two_exclusions = Problem.filtered("-app:'#{@app_1.name}' -app:app3") + expect(filtered_results_with_two_exclusions).not_to include(@problem_1) + expect(filtered_results_with_two_exclusions).to include(@problem_2) + expect(filtered_results_with_two_exclusions).not_to include(@problem_3) end it "#filtered does not explode if given a nil filter" do filtered_results = Problem.filtered(nil) - expect(filtered_results).to include(@problem1) - expect(filtered_results).to include(@problem2) - expect(filtered_results).to include(@problem3) + expect(filtered_results).to include(@problem_1) + expect(filtered_results).to include(@problem_2) + expect(filtered_results).to include(@problem_3) end it "#filtered does nothing for unimplemented filter types" do filtered_results = Problem.filtered("filterthatdoesnotexist:hotapp") - expect(filtered_results).to include(@problem1) - expect(filtered_results).to include(@problem2) - expect(filtered_results).to include(@problem3) + expect(filtered_results).to include(@problem_1) + expect(filtered_results).to include(@problem_2) + expect(filtered_results).to include(@problem_3) end end - context "#app_name" do - let!(:app) { Fabricate(:app) } - let!(:problem) { Fabricate(:problem, app: app) } + describe "#app_name" do + let!(:app) { create(:app) } + + let!(:problem) { create(:problem, app: app) } before { app.reload } it "is set when a problem is created" do - assert_equal app.name, problem.app_name + expect(problem.app_name).to eq(app.name) end it "is updated when an app is updated" do @@ -330,9 +345,9 @@ context "notice messages cache" do before do - @app = Fabricate(:app) - @problem = Fabricate(:problem, app: @app) - @err = Fabricate(:err, problem: @problem) + @app = create(:app) + @problem = create(:problem, app: @app) + @err = create(:err, problem: @problem) end it "#messages should be empty by default" do @@ -340,15 +355,15 @@ end it "removing a notice removes string from #messages" do - Fabricate(:notice, err: @err, message: 'ERR 1') + create(:notice, err: @err, message: "ERR 1") expect do @err.notices.first.destroy @problem.reload - end.to change(@problem, :messages).from(Digest::MD5.hexdigest('ERR 1') => { 'value' => 'ERR 1', 'count' => 1 }).to({}) + end.to change(@problem, :messages).from(Digest::MD5.hexdigest("ERR 1") => {"value" => "ERR 1", "count" => 1}).to({}) end it "removing a notice from the problem with broken counter should not raise an error" do - Fabricate(:notice, err: @err, message: 'ERR 1') + create(:notice, err: @err, message: "ERR 1") @problem.messages = {} @problem.save! expect { @err.notices.first.destroy }.not_to raise_error @@ -357,9 +372,9 @@ context "notice hosts cache" do before do - @app = Fabricate(:app) - @problem = Fabricate(:problem, app: @app) - @err = Fabricate(:err, problem: @problem) + @app = create(:app) + @problem = create(:problem, app: @app) + @err = create(:err, problem: @problem) end it "#hosts should be empty by default" do @@ -367,19 +382,19 @@ end it "removing a notice removes string from #hosts" do - Fabricate(:notice, err: @err, request: { 'url' => "http://example.com/resource/12" }) + create(:notice, err: @err, request: {"url" => "http://example.com/resource/12"}) expect do @err.notices.first.destroy @problem.reload - end.to change(@problem, :hosts).from(Digest::MD5.hexdigest('example.com') => { 'value' => 'example.com', 'count' => 1 }).to({}) + end.to change(@problem, :hosts).from(Digest::MD5.hexdigest("example.com") => {"value" => "example.com", "count" => 1}).to({}) end end context "notice user_agents cache" do before do - @app = Fabricate(:app) - @problem = Fabricate(:problem, app: @app) - @err = Fabricate(:err, problem: @problem) + @app = create(:app) + @problem = create(:problem, app: @app) + @err = create(:err, problem: @problem) end it "#user_agents should be empty by default" do @@ -387,115 +402,111 @@ end it "removing a notice removes string from #user_agents" do - Fabricate( - :notice, - err: @err, + create(:notice, + err: @err, request: { - 'cgi-data' => { - 'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16' + "cgi-data" => { + "HTTP_USER_AGENT" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16" } - } - ) + }) expect do @err.notices.first.destroy @problem.reload - end.to change(@problem, :user_agents). - from( - Digest::MD5.hexdigest('Chrome 10.0.648.204 (OS X 10.6.7)') => { - 'value' => 'Chrome 10.0.648.204 (OS X 10.6.7)', 'count' => 1 } + end.to change(@problem, :user_agents) + .from( + Digest::MD5.hexdigest("Chrome 10.0.648.204 (OS X 10.6.7)") => { + "value" => "Chrome 10.0.648.204 (OS X 10.6.7)", "count" => 1 + } ).to({}) end end context "comment counter cache" do before do - @app = Fabricate(:app) - @problem = Fabricate(:problem, app: @app) + @app = create(:app) + @problem = create(:problem, app: @app) end it "#comments_count returns 0 by default" do - expect(@problem.comments_count).to eq 0 + expect(@problem.comments_count).to eq(0) end it "adding a comment increases #comments_count by 1" do expect do - Fabricate(:comment, err: @problem) - end.to change(@problem, :comments_count).from(0).to(1) + create(:comment, err: @problem) + end.to change(@problem, :comments_count).by(1) end it "removing a comment decreases #comments_count by 1" do - Fabricate(:comment, err: @problem) + create(:comment, err: @problem) expect do @problem.reload.comments.first.destroy @problem.reload - end.to change(@problem, :comments_count).from(1).to(0) + end.to change(@problem, :comments_count).by(-1) end end describe "#issue_type" do context "without issue_type fill in Problem" do - let(:problem) do - Problem.new(app: app) - end + let(:problem) { described_class.new(app: app) } let(:app) do App.new(issue_tracker: issue_tracker) end context "without issue_tracker associate to app" do - let(:issue_tracker) do - nil - end - it 'return nil' do - expect(problem.issue_type).to be_nil + let(:issue_tracker) { nil } + + it "return nil" do + expect(problem.issue_type).to eq(nil) end end context "with issue_tracker valid associate to app" do let(:issue_tracker) do - Fabricate(:issue_tracker).tap do |t| + create(:issue_tracker).tap do |t| t.instance_variable_set(:@tracker, ErrbitPlugin::MockIssueTracker.new(t.options)) end end - it 'return the issue_tracker label' do - expect(problem.issue_type).to eql 'mock' + it "return the issue_tracker label" do + expect(problem.issue_type).to eq("mock") end end context "with issue_tracker not valid associate to app" do let(:issue_tracker) do - IssueTracker.new(type_tracker: 'fake') + IssueTracker.new(type_tracker: "fake") end - it 'return nil' do - expect(problem.issue_type).to be_nil + it "return nil" do + expect(problem.issue_type).to eq(nil) end end end context "with issue_type fill in Problem" do - it 'return the value associate' do - expect(Problem.new(issue_type: 'foo').issue_type).to eql 'foo' + it "return the value associate" do + expect(described_class.new(issue_type: "foo").issue_type).to eq("foo") end end end - describe '#recache' do - let(:problem) { Fabricate(:problem_with_errs) } + describe "#recache" do + let(:problem) { create(:problem_with_errs) } + let(:first_errs) { problem.errs } - let!(:notice) { Fabricate(:notice, err: first_errs.first) } + + let!(:notice) { create(:notice, err: first_errs.first) } before do problem.update_attribute(:notices_count, 0) end - it 'update the notice_count' do + it "update the notice_count" do expect do problem.recache - end.to change { - problem.notices_count - }.from(0).to(1) + end.to change(problem, :notices_count).from(0).to(1) end context "with only one notice" do @@ -504,92 +515,96 @@ problem.recache end - it 'update information about this notice' do - expect(problem.message).to eq notice.message - expect(problem.where).to eq notice.where + it "update information about this notice" do + expect(problem.message).to eq(notice.message) + expect(problem.where).to eq(notice.where) end - it 'update first_notice_at' do - expect(problem.first_notice_at).to eq notice.reload.created_at + it "update first_notice_at" do + expect(problem.first_notice_at).to eq(notice.reload.created_at) end - it 'update last_notice_at' do - expect(problem.last_notice_at).to eq notice.reload.created_at + it "update last_notice_at" do + expect(problem.last_notice_at).to eq(notice.reload.created_at) end - it 'update stats messages' do + it "update stats messages" do expect(problem.messages).to eq( - Digest::MD5.hexdigest(notice.message) => { 'value' => notice.message, 'count' => 1 } + Digest::MD5.hexdigest(notice.message) => {"value" => notice.message, "count" => 1} ) end - it 'update stats hosts' do + it "update stats hosts" do expect(problem.hosts).to eq( - Digest::MD5.hexdigest(notice.host) => { 'value' => notice.host, 'count' => 1 } + Digest::MD5.hexdigest(notice.host) => {"value" => notice.host, "count" => 1} ) end - it 'update stats user_agents' do + it "update stats user_agents" do expect(problem.user_agents).to eq( - Digest::MD5.hexdigest(notice.user_agent_string) => { 'value' => notice.user_agent_string, 'count' => 1 } + Digest::MD5.hexdigest(notice.user_agent_string) => {"value" => notice.user_agent_string, "count" => 1} ) end end context "with several notices" do - let!(:notice_2) { Fabricate(:notice, err: first_errs.first) } - let!(:notice_3) { Fabricate(:notice, err: first_errs.first) } + let!(:notice_2) { create(:notice, err: first_errs.first) } + + let!(:notice_3) { create(:notice, err: first_errs.first) } + before do problem.update_attributes!(messages: {}) problem.recache end - it 'update information about this notice' do - expect(problem.message).to eq notice.message - expect(problem.where).to eq notice.where + it "update information about this notice" do + expect(problem.message).to eq(notice.message) + expect(problem.where).to eq(notice.where) end - it 'update first_notice_at' do + it "update first_notice_at" do expect(problem.first_notice_at.to_i).to be_within(2).of(notice.created_at.to_i) end - it 'update last_notice_at' do + it "update last_notice_at" do expect(problem.last_notice_at.to_i).to be_within(2).of(notice.created_at.to_i) end - it 'update stats messages' do - expect(problem.messages).to eq(Digest::MD5.hexdigest(notice.message) => { 'value' => notice.message, 'count' => 3 }) + it "update stats messages" do + expect(problem.messages).to eq(Digest::MD5.hexdigest(notice.message) => {"value" => notice.message, "count" => 3}) end - it 'update stats hosts' do - expect(problem.hosts).to eq(Digest::MD5.hexdigest(notice.host) => { 'value' => notice.host, 'count' => 3 }) + it "update stats hosts" do + expect(problem.hosts).to eq(Digest::MD5.hexdigest(notice.host) => {"value" => notice.host, "count" => 3}) end - it 'update stats user_agents' do - expect(problem.user_agents).to eq(Digest::MD5.hexdigest(notice.user_agent_string) => { 'value' => notice.user_agent_string, 'count' => 3 }) + it "update stats user_agents" do + expect(problem.user_agents).to eq(Digest::MD5.hexdigest(notice.user_agent_string) => {"value" => notice.user_agent_string, "count" => 3}) end end end - context "#url" do - subject { Fabricate(:problem) } + describe "#url" do + subject { create(:problem) } - it "uses the configured protocol" do - allow(Errbit::Config).to receive(:protocol).and_return("https") + before { expect(Errbit::Config).to receive(:host).and_return("memyselfandi.com") } - expect(subject.url).to eq "https://errbit.example.com/apps/#{subject.app.id}/problems/#{subject.id}" + it "uses the configured host" do + expect(subject.url).to eq("http://memyselfandi.com/apps/#{subject.app.id}/problems/#{subject.id}") end + end - it "uses the configured host" do - allow(Errbit::Config).to receive(:host).and_return("memyselfandi.com") + describe "#link_text" do + context "when message is present" do + subject { create(:problem, message: "Test message") } - expect(subject.url).to eq "http://memyselfandi.com/apps/#{subject.app.id}/problems/#{subject.id}" + it { expect(subject.link_text).to eq("Test message") } end - it "uses the configured port" do - allow(Errbit::Config).to receive(:port).and_return(8123) + context "when message is not present" do + subject { create(:problem, message: nil, error_class: "ErrorClass") } - expect(subject.url).to eq "http://errbit.example.com:8123/apps/#{subject.app.id}/problems/#{subject.id}" + it { expect(subject.link_text).to eq("ErrorClass") } end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a8edaf69ab..374583a2c9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,51 +1,65 @@ -describe User do - context 'validations' do - it 'require that a name is present' do - user = Fabricate.build(:user, name: nil) - expect(user).to_not be_valid - expect(user.errors[:name]).to include("can't be blank") +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe User, type: :model do + context "validations" do + it "require that a name is present" do + user = build(:user, name: nil) + + expect(user.valid?).to eq(false) + + expect(user.errors[:name]).to eq(["can't be blank"]) end - it 'requires password without github login' do - user = Fabricate.build(:user, password: nil) - expect(user).to_not be_valid - expect(user.errors[:password]).to include("can't be blank") + it "requires password without github login" do + user = build(:user, password: nil) + + expect(user.valid?).to eq(false) + + expect(user.errors[:password]).to eq(["can't be blank"]) end it "doesn't require password with github login" do - user = Fabricate.build(:user, password: nil, github_login: 'nashby') - expect(user).to be_valid + user = build(:user, password: nil, github_login: "biow0lf") + + expect(user.valid?).to eq(true) end - it 'requires uniq github login' do - user1 = Fabricate(:user, github_login: 'nashby') - expect(user1).to be_valid + it "requires uniq github login" do + user_1 = create(:user, github_login: "biow0lf") + expect(user_1.valid?).to eq(true) + + user_2 = build(:user, github_login: "biow0lf") + user_2.save + expect(user_2.valid?).to eq(false) - user2 = Fabricate.build(:user, github_login: 'nashby') - user2.save - expect(user2).to_not be_valid - expect(user2.errors[:github_login]).to include("is already taken") + expect(user_2.errors[:github_login]).to eq(["has already been taken"]) end - it 'allows blank / null github_login' do - user1 = Fabricate(:user, github_login: ' ') - expect(user1).to be_valid + it "allows blank / null github_login" do + user_1 = create(:user, github_login: " ") + expect(user_1.valid?).to eq(true) - user2 = Fabricate.build(:user, github_login: ' ') - user2.save - expect(user2).to be_valid + user_2 = build(:user, github_login: " ") + user_2.save + + expect(user_2.valid?).to eq(true) end it "disables validations when reset password" do - user = Fabricate.build(:user, email: '') + user = build(:user, email: "") user.save(validate: false) - expect(user.reset_password('Password123', 'Password123')).to be_truthy + + expect(user.reset_password("Password123", "Password123")).to eq(true) end - it 'should require a password with minimum of 6 characters' do - user = Fabricate.build(:user) - user.reset_password('12345', '12345') - expect(user.errors[:password]).to include("is too short (minimum is 6 characters)", "is too short (minimum is 6 characters)") + it "should require a password with minimum of 8 characters" do + user = build(:user) + + user.reset_password("1234567", "1234578") + + expect(user.errors[:password]).to eq(["is too short (minimum is 8 characters)"]) end end @@ -53,10 +67,16 @@ it "should be created this admin access via db:seed" do expect do allow($stdout).to receive(:puts).and_return(true) - require Rails.root.join('db/seeds.rb') + require Rails.root.join("db/seeds.rb") end.to change { User.where(admin: true).count - }.from(0).to(1) + }.by(1) end end + + describe "#attributes_for_super_diff" do + subject { create(:user) } + + it { expect(subject.attributes_for_super_diff).to eq(id: subject.id.to_s, name: subject.name) } + end end diff --git a/spec/models/watcher_spec.rb b/spec/models/watcher_spec.rb index 5ba7d6a2c5..96674ab313 100644 --- a/spec/models/watcher_spec.rb +++ b/spec/models/watcher_spec.rb @@ -1,32 +1,50 @@ -describe Watcher, type: 'model' do - context 'validations' do - it 'requires an email address or an associated user' do - watcher = Fabricate.build(:watcher, email: nil, user: nil) - expect(watcher).to_not be_valid +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Watcher, type: :model do + context "validations" do + it "requires an email address or an associated user" do + watcher = build(:watcher, email: nil, user: nil) + expect(watcher.valid?).to eq(false) expect(watcher.errors[:base]).to include("You must specify either a user or an email address") - watcher.email = 'watcher@example.com' - expect(watcher).to be_valid + watcher.email = "watcher@example.com" + expect(watcher.valid?).to eq(true) watcher.email = nil - expect(watcher).to_not be_valid + expect(watcher.valid?).to eq(false) - watcher.user = Fabricate(:user) - watcher.watcher_type = 'user' - expect(watcher).to be_valid + watcher.user = create(:user) + watcher.watcher_type = "user" + expect(watcher.valid?).to eq(true) end end - context 'address' do + context "address" do it "returns the user's email address if there is a user" do - user = Fabricate(:user, email: 'foo@bar.com') - watcher = Fabricate(:user_watcher, user: user) - expect(watcher.address).to eq 'foo@bar.com' + user = create(:user, email: "foo@bar.com") + watcher = create(:user_watcher, user: user) + expect(watcher.address).to eq("foo@bar.com") end it "returns the email if there is no user" do - watcher = Fabricate(:watcher, email: 'widgets@acme.com') - expect(watcher.address).to eq 'widgets@acme.com' + watcher = create(:watcher, email: "widgets@acme.com") + expect(watcher.address).to eq("widgets@acme.com") + end + end + + describe "#email_choosen" do + context "with email define" do + it "return blank" do + expect(described_class.new(email: "foo").email_choosen).to eq("") + end + end + + context "without email define" do + it "return choosen" do + expect(described_class.new(email: "").email_choosen).to eq("chosen") + end end end end diff --git a/spec/policies/app_policy_spec.rb b/spec/policies/app_policy_spec.rb new file mode 100644 index 0000000000..7f85f48880 --- /dev/null +++ b/spec/policies/app_policy_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe AppPolicy do + subject { described_class.new(user, record) } + + describe "#initialize" do + context "when user not present" do + let(:record) { create(:app) } + + let(:user) { nil } + + it { expect { subject }.to raise_error(Pundit::NotAuthorizedError) } + end + end + + describe "#index" do + let(:record) { create(:app) } + + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + it { is_expected.to forbid_action(:index) } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + it { is_expected.to forbid_action(:index) } + end + end + + describe "#show?" do + let(:record) { create(:app) } + + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + it { is_expected.to permit_action(:show) } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + it { is_expected.to permit_action(:show) } + end + end + + describe "#create?" do + let(:record) { App.new } + + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + it { is_expected.to permit_action(:create) } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + it { is_expected.to forbid_action(:create) } + end + end + + describe "#new?" do + let(:record) { App.new } + + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + it { is_expected.to permit_action(:new) } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + it { is_expected.to forbid_action(:new) } + end + end + + describe "#update?" do + let(:record) { create(:app) } + + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + it { is_expected.to permit_action(:update) } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + it { is_expected.to forbid_action(:update) } + end + end + + describe "#edit?" do + let(:record) { create(:app) } + + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + it { is_expected.to permit_action(:edit) } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + it { is_expected.to forbid_action(:edit) } + end + end + + describe "#destroy?" do + let(:record) { create(:app) } + + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + it { is_expected.to permit_action(:destroy) } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + it { is_expected.to forbid_action(:destroy) } + end + end +end + +RSpec.describe AppPolicy::Scope do + describe "#resolve" do + let(:user) { create(:user, admin: false) } + + let!(:app) { create(:app) } + + let(:scope) { App } + + subject { described_class.new(user, scope) } + + it { expect(subject.resolve).to eq([app]) } + end +end diff --git a/spec/policies/application_policy_spec.rb b/spec/policies/application_policy_spec.rb new file mode 100644 index 0000000000..80556f41da --- /dev/null +++ b/spec/policies/application_policy_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ApplicationPolicy do + let(:record) { create(:user, admin: false) } + + subject { described_class.new(user, record) } + + context "when user present" do + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + it { is_expected.to forbid_all_actions } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + it { is_expected.to forbid_all_actions } + end + end + + context "when user not present" do + let(:user) { nil } + + it { expect { subject }.to raise_error(Pundit::NotAuthorizedError) } + end + + describe "#permitted_attributes" do + let(:user) { create(:user, admin: false) } + + it { expect { subject.permitted_attributes }.to raise_error(NotImplementedError, "You must define #permitted_attributes in ApplicationPolicy") } + end +end + +RSpec.describe ApplicationPolicy::Scope do + describe "#initialize" do + context "when user is not present" do + let(:user) { nil } + + let(:scope) { User } + + it { expect { described_class.new(user, scope) }.to raise_error(Pundit::NotAuthorizedError) } + end + + context "when user is present" do + let(:user) { create(:user, admin: false) } + + let(:scope) { double } + + it { expect { described_class.new(user, scope) }.not_to raise_error } + end + end + + describe "#resolve" do + let(:user) { create(:user, admin: false) } + + let(:scope) { double } + + subject { described_class.new(user, scope) } + + it { expect { subject.resolve }.to raise_error(NotImplementedError, "You must define #resolve in ApplicationPolicy::Scope") } + end +end diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb new file mode 100644 index 0000000000..4d5b8e185e --- /dev/null +++ b/spec/policies/user_policy_spec.rb @@ -0,0 +1,234 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe UserPolicy do + subject { described_class.new(user, record) } + + it do + expect(described_class::FIELDS) + .to eq([:name, :username, :email, :password, :github_login, :per_page, :time_zone, :password, :password_confirmation]) + end + + describe "#initialize" do + context "when user not present" do + let(:record) { create(:user) } + + let(:user) { nil } + + it { expect { subject }.to raise_error(Pundit::NotAuthorizedError) } + end + end + + describe "#index" do + let(:record) { create(:user, admin: false) } + + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + it { is_expected.to forbid_action(:index) } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + it { is_expected.to forbid_action(:index) } + end + end + + describe "#show?" do + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + let(:record) { create(:user, admin: false) } + + it { is_expected.to permit_action(:show) } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + let(:record) { create(:user, admin: false) } + + it { is_expected.to forbid_action(:show) } + end + + context "when user is an owner" do + let(:user) { create(:user, admin: false) } + + let(:record) { user } + + it { is_expected.to permit_action(:show) } + end + end + + describe "#create?" do + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + let(:record) { User.new } + + it { is_expected.to permit_action(:create) } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + let(:record) { User.new } + + it { is_expected.to forbid_action(:create) } + end + end + + describe "#new?" do + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + let(:record) { User.new } + + it { is_expected.to permit_action(:new) } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + let(:record) { User.new } + + it { is_expected.to forbid_action(:new) } + end + end + + describe "#update?" do + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + let(:record) { create(:user, admin: false) } + + it { is_expected.to permit_action(:update) } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + let(:record) { create(:user, admin: false) } + + it { is_expected.to forbid_action(:update) } + end + + context "when user is an owner" do + let(:user) { create(:user, admin: false) } + + let(:record) { user } + + it { is_expected.to permit_action(:update) } + end + end + + describe "#edit?" do + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + let(:record) { create(:user, admin: false) } + + it { is_expected.to permit_action(:edit) } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + let(:record) { create(:user, admin: false) } + + it { is_expected.to forbid_action(:edit) } + end + + context "when user is an owner" do + let(:user) { create(:user, admin: false) } + + let(:record) { user } + + it { is_expected.to permit_action(:edit) } + end + end + + describe "#destroy?" do + context "when user is an admin" do + let(:user) { create(:user, admin: true) } + + let(:record) { create(:user, admin: false) } + + it { is_expected.to permit_action(:destroy) } + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + let(:record) { create(:user, admin: false) } + + it { is_expected.to forbid_action(:destroy) } + end + + context "when user is an owner" do + let(:user) { create(:user, admin: false) } + + let(:record) { user } + + it { is_expected.to forbid_action(:destroy) } + end + end + + describe "#permitted_attributes" do + context "when user is an admin" do + context "when user is an admin and record is the same user as admin" do + let(:user) { create(:user, admin: true) } + + let(:record) { user } + + it { expect(subject.permitted_attributes).to eq(described_class::FIELDS) } + end + + context "when user is an admin and record is not the same user as admin" do + let(:user) { create(:user, admin: true) } + + let(:record) { create(:user, admin: false) } + + it { expect(subject.permitted_attributes).to eq(described_class::FIELDS + [:admin]) } + end + end + + context "when user is not an admin" do + let(:user) { create(:user, admin: false) } + + let(:record) { create(:user, admin: false) } + + it { expect(subject.permitted_attributes).to eq(described_class::FIELDS) } + end + end +end + +RSpec.describe UserPolicy::Scope do + describe "#resolve" do + context "when user is an admin" do + let(:admin) { create(:user, admin: true) } + + let(:user) { create(:user, admin: false) } + + let(:scope) { User } + + subject { described_class.new(admin, scope) } + + it { expect(subject.resolve).to eq([admin, user]) } + end + + context "when user is not an admin" do + let!(:admin) { create(:user, admin: true) } + + let(:user) { create(:user, admin: false) } + + let(:scope) { User } + + subject { described_class.new(user, scope) } + + it { expect(subject.resolve).to eq([user]) } + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000000..8e9b8f90fa --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1 @@ +# frozen_string_literal: true diff --git a/spec/requests/api_v1_spec.rb b/spec/requests/api_v1_spec.rb new file mode 100644 index 0000000000..3d1debeaf4 --- /dev/null +++ b/spec/requests/api_v1_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "API V1", type: :request do + before do + allow(Errbit::SelfErrorReporter).to receive(:public_environment?).and_return(true) + end + + it "self-reports an unhandled API V1 exception" do + user = create(:user) + notice_count = Notice.count + allow(Notice).to receive(:where).and_raise(RuntimeError, "database query failed") + + expect do + get "/api/v1/notices", params: {auth_token: user.authentication_token} + end.to raise_error(RuntimeError, "database query failed") + + expect(Notice.count).to eq(notice_count + 1) + + notice = Notice.last + expect(notice.error_class).to eq("RuntimeError") + expect(notice.request).to include( + "component" => "notices", + "action" => "index" + ) + expect(notice.request["url"]).to include("/api/v1/notices") + expect(notice.backtrace.lines).to be_present + end +end diff --git a/spec/requests/content_security_policy_spec.rb b/spec/requests/content_security_policy_spec.rb new file mode 100644 index 0000000000..cbaf44da6b --- /dev/null +++ b/spec/requests/content_security_policy_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Content Security Policy", type: :request do + it "includes the nonce on executable script tags" do + get new_user_session_path + + nonce = response.headers.fetch("Content-Security-Policy").match(/nonce-([^' ]+)/)[1] + + expect(response.body).to match(/]+nonce="#{Regexp.escape(nonce)}"/) + end + + it "generates a different nonce for each response" do + get new_user_session_path + first_nonce = response.headers.fetch("Content-Security-Policy").match(/nonce-([^' ]+)/)[1] + + get new_user_session_path + second_nonce = response.headers.fetch("Content-Security-Policy").match(/nonce-([^' ]+)/)[1] + + expect(second_nonce).not_to eq(first_nonce) + end + + it "restricts fonts and frames to the application origin" do + get new_user_session_path + + policy = response.headers.fetch("Content-Security-Policy") + + expect(policy).to include("font-src 'self'") + expect(policy).to include("frame-src 'self'") + expect(policy).not_to include("font-src 'self' https:") + expect(policy).not_to include("font-src 'self' data:") + end + + it "allows Gravatar images without allowing arbitrary HTTPS images" do + get new_user_session_path + + policy = response.headers.fetch("Content-Security-Policy") + + expect(policy).to include("img-src 'self' https://secure.gravatar.com data:") + expect(policy).not_to include("img-src 'self' https:;") + end +end diff --git a/spec/requests/health_controller_spec.rb b/spec/requests/health_controller_spec.rb index c122c4225e..e739d14e19 100644 --- a/spec/requests/health_controller_spec.rb +++ b/spec/requests/health_controller_spec.rb @@ -1,52 +1,25 @@ -describe "Health", type: 'request' do - let(:errbit_app) { Fabricate(:app, api_key: 'APIKEY') } - - describe "readiness" do - before do - if HealthController.instance_variable_defined? :@impatient_mongoid_client - HealthController.remove_instance_variable :@impatient_mongoid_client - end - end +# frozen_string_literal: true - it 'can let you know when the app is ready to receive requests' do - get '/health/readiness' - expect(response).to be_success - end +require "rails_helper" - it 'can indicate if a check fails' do - expect(Errbit::Config).to receive(:mongo_url) { - 'mongodb://localhost:27000' - } - get '/health/readiness' - expect(response).to be_error - parsed_response = JSON.parse(response.body) - expect(parsed_response['ok']).to eq false - expect(parsed_response['details'].first['check_name']).to eq 'mongo' - expect(parsed_response['details'].first['ok']).to eq false - expect(parsed_response['details'].first['error_details']).to_not be_nil - end - end - - describe "liveness" do - it 'can let you know that the app is still alive' do - get '/health/liveness' - expect(response).to be_success - end - end +RSpec.describe HealthController, type: :request do + let(:errbit_app) { create(:app, api_key: "APIKEY") } describe "api_key_tester" do - it 'will let you know when the api_key is not valid' do + it "will let you know when the api_key is not valid" do get "/health/api-key-tester?api_key=garbagekey" + expect(response).to be_forbidden - parsed_response = JSON.parse(response.body) - expect(parsed_response['ok']).to eq false + + expect(response.parsed_body["ok"]).to eq(false) end - it 'can let you know that the api_key is valid' do + it "can let you know that the api_key is valid" do get "/health/api-key-tester?api_key=#{errbit_app.api_key}" - expect(response).to be_success - parsed_response = JSON.parse(response.body) - expect(parsed_response['ok']).to eq true + + expect(response).to be_successful + + expect(response.parsed_body["ok"]).to eq(true) end end end diff --git a/spec/requests/notices_controller_spec.rb b/spec/requests/notices_controller_spec.rb index 8941ead80e..ef344ed0f5 100644 --- a/spec/requests/notices_controller_spec.rb +++ b/spec/requests/notices_controller_spec.rb @@ -1,53 +1,113 @@ -describe "Notices management", type: 'request' do - let(:errbit_app) { Fabricate(:app, api_key: 'APIKEY') } +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Notices management", type: :request do + let(:errbit_app) { create(:app, api_key: "APIKEY") } describe "create a new notice" do - context "with valide notice" do - let(:xml) { Rails.root.join('spec', 'fixtures', 'hoptoad_test_notice.xml').read } - it 'save a new notice' do + context "with valid notice" do + let(:xml) { Rails.root.join("spec/fixtures/hoptoad_test_notice.xml").read } + + it "save a new notice" do expect do - post '/notifier_api/v2/notices', data: xml - expect(response).to be_success - end.to change { - errbit_app.problems.count - }.by(1) + post "/notifier_api/v2/notices", params: {data: xml} + expect(response).to be_successful + end.to change(errbit_app.problems, :count).by(1) end end context "with notice with empty backtrace" do - let(:xml) { Rails.root.join('spec', 'fixtures', 'hoptoad_test_notice_without_line_of_backtrace.xml').read } - it 'save a new notice' do + let(:xml) { Rails.root.join("spec/fixtures/hoptoad_test_notice_without_line_of_backtrace.xml").read } + + it "save a new notice" do expect do - post '/notifier_api/v2/notices', data: xml - expect(response).to be_success - end.to change { - errbit_app.problems.count - }.by(1) + post "/notifier_api/v2/notices", params: {data: xml} + expect(response).to be_successful + end.to change(errbit_app.problems, :count).by(1) end end context "with notice with bad api_key" do - let(:errbit_app) { Fabricate(:app) } - let(:xml) { Rails.root.join('spec', 'fixtures', 'hoptoad_test_notice.xml').read } - it 'not save a new notice and return 422' do + let(:errbit_app) { create(:app) } + + let(:xml) { Rails.root.join("spec/fixtures/hoptoad_test_notice.xml").read } + + it "not save a new notice and return 422" do expect do - post '/notifier_api/v2/notices', data: xml - expect(response.status).to eq 422 - expect(response.body).to eq "Your API key is unknown" - end.to_not change(errbit_app.problems, :count) + post "/notifier_api/v2/notices", params: {data: xml} + expect(response).to have_http_status(:unprocessable_content) + expect(response.body).to eq("Your API key is unknown") + end.not_to change(errbit_app.problems, :count) end end context "with GET request" do - let(:xml) { Rails.root.join('spec', 'fixtures', 'hoptoad_test_notice.xml').read } - it 'save a new notice' do + let(:xml) { Rails.root.join("spec/fixtures/hoptoad_test_notice.xml").read } + + it "save a new notice" do expect do - get '/notifier_api/v2/notices', data: xml - expect(response).to be_success - end.to change { - errbit_app.problems.count - }.by(1) + get "/notifier_api/v2/notices", params: {data: xml} + expect(response).to be_successful + end.to change(errbit_app.problems, :count).by(1) end end end + + describe "malformed request parameters" do + before do + allow(Errbit::SelfErrorReporter).to receive(:public_environment?).and_return(true) + end + + it "records the request exception with populated backtrace lines" do + expect do + get "/?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input" + end.to change(Notice, :count).by(1) + + expect(response).to have_http_status(:bad_request) + + notice = Notice.last + expect(notice.error_class).to eq("ActionController::BadRequest") + expect(notice.problem.environment).to eq("test") + expect(notice.request["url"]).to include("%ADd+allow_url_include") + expect(notice.request["params"].keys.first).to include("allow_url_include") + expect(notice.request["cgi-data"]).to include( + "PATH_INFO" => "/", + "QUERY_STRING" => "%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input", + "REQUEST_METHOD" => "GET" + ) + expect(notice.request["cgi-data"].keys).to include(a_string_matching("secret_key_base")) + expect(notice.request["cgi-data"].keys).to include(a_string_matching("rack.*errors")) + expect(notice.request["session"]).to be_a(Hash) + + backtrace = notice.backtrace + expect(backtrace.lines).to be_present + expect(backtrace.lines).to all(satisfy { |line| line["file"].present? && line["number"].present? }) + end + + it "records malformed JSON request exceptions with populated backtrace lines" do + notice_count = Notice.count + + expect do + post "/notifier_api/v2/notices", params: "{", headers: { + "CONTENT_TYPE" => "application/json", + "ACCEPT" => "application/json" + } + end.to raise_error(ActionView::Template::Error) + + expect(Notice.count).to eq(notice_count + 1) + + notice = Notice.last + expect(notice.error_class).to eq("ActionDispatch::Http::Parameters::ParseError") + expect(notice.request["cgi-data"]).to include( + "PATH_INFO" => "/notifier_api/v2/notices", + "REQUEST_METHOD" => "POST", + "CONTENT_TYPE" => "application/json" + ) + expect(notice.backtrace.lines).to be_present + expect(notice.backtrace.lines).to all( + satisfy { |line| line["file"].present? && line["number"].present? } + ) + end + end end diff --git a/spec/requests/unlink_githubs_controller_spec.rb b/spec/requests/unlink_githubs_controller_spec.rb new file mode 100644 index 0000000000..397c6b36f6 --- /dev/null +++ b/spec/requests/unlink_githubs_controller_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe UnlinkGithubsController, type: :request do + describe "#update" do + context "when user is logged in" do + context "when user has access" do + let!(:current_user) { create(:user, github_login: "biow0lf", github_oauth_token: "token123") } + + before { sign_in(current_user) } + + before { patch user_unlink_github_path(current_user) } + + it "is expected to unlink github for a user with status found" do + expect(response).to redirect_to(user_path(assigns(:user))) + + expect(response).to have_http_status(:found) + + expect(assigns(:user).github_login).to eq(nil) + + expect(assigns(:user).github_oauth_token).to eq(nil) + end + end + + context "when user has not access" do + let!(:current_user) { create(:user, admin: false) } + + let!(:user) { create(:user) } + + before { sign_in(current_user) } + + before { patch user_unlink_github_path(user) } + + it "is expected to redirect to root path with status found" do + expect(response).to redirect_to(root_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq("You are not authorized to perform this action.") + end + end + end + + context "when user is not logged in" do + let(:user) { create(:user) } + + before { patch user_unlink_github_path(user) } + + it "is expected to redirect to new user session path with status found" do + expect(response).to redirect_to(new_user_session_path) + + expect(response).to have_http_status(:found) + end + end + end +end diff --git a/spec/requests/unlink_googles_controller_spec.rb b/spec/requests/unlink_googles_controller_spec.rb new file mode 100644 index 0000000000..bd1f943462 --- /dev/null +++ b/spec/requests/unlink_googles_controller_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe UnlinkGooglesController, type: :request do + describe "#update" do + context "when user is logged in" do + context "when user has access" do + let!(:current_user) { create(:user, google_uid: "google-uid") } + + before { sign_in(current_user) } + + before { patch user_unlink_google_path(current_user) } + + it "is expected to unlink github for a user with status found" do + expect(response).to redirect_to(user_path(assigns(:user))) + + expect(response).to have_http_status(:found) + + expect(assigns(:user).google_uid).to eq(nil) + end + end + + context "when user has not access" do + let!(:current_user) { create(:user, admin: false) } + + let!(:user) { create(:user) } + + before { sign_in(current_user) } + + before { patch user_unlink_google_path(user) } + + it "is expected to redirect to root path with status found" do + expect(response).to redirect_to(root_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq("You are not authorized to perform this action.") + end + end + end + + context "when user is not logged in" do + let(:user) { create(:user) } + + before { patch user_unlink_google_path(user) } + + it "is expected to redirect to new user session path with status found" do + expect(response).to redirect_to(new_user_session_path) + + expect(response).to have_http_status(:found) + end + end + end +end diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb new file mode 100644 index 0000000000..9c71771f6c --- /dev/null +++ b/spec/requests/users_controller_spec.rb @@ -0,0 +1,545 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe UsersController, type: :request do + # before { I18n.locale = "pt-BR" } + + describe "#index" do + context "when user is logged in" do + context "when user has access" do + let!(:current_user) { create(:user, name: "Jon Snow", admin: true) } + + let!(:user) { create(:user, name: "Tyrion Lannister") } + + before { sign_in(current_user) } + + before { get users_path } + + it "is expected to render template index with status ok" do + expect(response).to render_template(:index) + + expect(response).to have_http_status(:ok) + + expect(assigns(:users)).to eq([current_user, user]) + end + end + + context "when user has not access" do + let(:current_user) { create(:user, admin: false) } + + before { sign_in(current_user) } + + before { get users_path } + + it "is expected to render template index with status ok" do + expect(response).to render_template(:index) + + expect(response).to have_http_status(:ok) + + expect(assigns(:users)).to eq([current_user]) + end + end + end + + context "when user is not logged in" do + before { get users_path } + + it "is expected to redirect to new user session path with status found" do + expect(response).to redirect_to(new_user_session_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq(I18n.t("devise.failure.unauthenticated")) + end + end + end + + describe "#show" do + context "when user is logged in" do + context "when user has access" do + let(:current_user) { create(:user, admin: true) } + + let(:user) { create(:user, admin: false) } + + before { sign_in(current_user) } + + before { get user_path(user) } + + it "is expected to render template show with status ok" do + expect(response).to render_template(:show) + + expect(response).to have_http_status(:ok) + + expect(assigns(:user)).to eq(user) + end + end + + context "when user has not access" do + let(:current_user) { create(:user, admin: false) } + + before { sign_in(current_user) } + + let(:user) { create(:user, admin: false) } + + before { get user_path(user) } + + it "is expected to redirect to root path with status found" do + expect(response).to redirect_to(root_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq(I18n.t("controllers.application.user_not_authorized")) + end + end + end + + context "when user is not logged in" do + let!(:user) { create(:user) } + + before { get user_path(user) } + + it "is expected to redirect to new user session path with status found" do + expect(response).to redirect_to(new_user_session_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq(I18n.t("devise.failure.unauthenticated")) + end + end + end + + describe "#new" do + context "when user is logged in" do + context "when user has access" do + let(:current_user) { create(:user, admin: true) } + + before { sign_in(current_user) } + + before { get new_user_path } + + it "is expected to render template new with status ok" do + expect(response).to render_template(:new) + + expect(response).to have_http_status(:ok) + + expect(assigns(:user).new_record?).to eq(true) + form = response.parsed_body + expect(form.at_css('input[name="user[email]"][autocomplete="email"]')).not_to be_nil + expect(form.at_css('input[name="user[password]"][autocomplete="new-password"]')).not_to be_nil + expect(form.at_css('input[name="user[password_confirmation]"][autocomplete="new-password"]')).not_to be_nil + end + end + + context "when user has not access" do + let(:current_user) { create(:user, admin: false) } + + before { sign_in(current_user) } + + before { get new_user_path } + + it "is expected to redirect to root path with status found" do + expect(response).to redirect_to(root_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq(I18n.t("controllers.application.user_not_authorized")) + end + end + end + + context "when user is not logged in" do + before { get new_user_path } + + it "is expected to redirect to new user session path with status found" do + expect(response).to redirect_to(new_user_session_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq(I18n.t("devise.failure.unauthenticated")) + end + end + end + + describe "#edit" do + context "when user is logged in" do + context "when user has access" do + let(:current_user) { create(:user, admin: true) } + + let!(:user) { create(:user, admin: false) } + + before { sign_in(current_user) } + + before { get edit_user_path(user) } + + it "is expected to render template edit with status ok" do + expect(response).to render_template(:edit) + + expect(response).to have_http_status(:ok) + + expect(assigns(:user)).to eq(user) + form = response.parsed_body + expect(form.at_css('input[name="user[email]"][autocomplete="email"]')).not_to be_nil + expect(form.at_css('input[name="user[password]"][autocomplete="new-password"]')).not_to be_nil + expect(form.at_css('input[name="user[password_confirmation]"][autocomplete="new-password"]')).not_to be_nil + end + end + + context "when user has not access" do + let(:current_user) { create(:user, admin: false) } + + let(:user) { create(:user, admin: false) } + + before { sign_in(current_user) } + + before { get edit_user_path(user) } + + it "is expected to redirect to root path with status found" do + expect(response).to redirect_to(root_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq(I18n.t("controllers.application.user_not_authorized")) + end + end + end + + context "when user is not logged in" do + let(:user) { create(:user) } + + before { get edit_user_path(user) } + + it "is expected to redirect to new user session path with status found" do + expect(response).to redirect_to(new_user_session_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq(I18n.t("devise.failure.unauthenticated")) + end + end + end + + describe "#create" do + context "when user is logged in" do + context "when user has access" do + context "when new record is valid" do + let(:current_user) { create(:user, admin: true) } + + before { sign_in(current_user) } + + let(:email) { Faker::Internet.unique.email } + + let(:password) { Faker::Internet.password } + + let(:name) { Faker::Name.unique.name } + + before do + expect do + post users_path, + params: { + user: { + email: email, + name: name, + password: password, + password_confirmation: password, + admin: true + } + } + end.to change(User, :count).by(1) + end + + it "is expected to create a new user with status found" do + expect(response).to redirect_to(user_path(assigns(:user))) + + expect(response).to have_http_status(:found) + + expect(request.flash[:success]).to eq(I18n.t("users.create.success", name: name)) + + expect(assigns(:user).email).to eq(email) + + expect(assigns(:user).name).to eq(name) + + expect(assigns(:user).admin).to eq(true) + end + end + + context "when new record is not valid" do + let(:current_user) { create(:user, admin: true) } + + before { sign_in(current_user) } + + let(:email) { "" } + + let(:password) { Faker::Internet.password } + + let(:name) { Faker::Name.unique.name } + + before do + expect do + post users_path, + params: { + user: { + email: email, + name: name, + password: password, + password_confirmation: password, + admin: true + } + } + end.not_to change(User, :count) + end + + it "is expected to render template new with status ok" do + expect(response).to render_template(:new) + + expect(response).to have_http_status(:ok) + end + end + end + + context "when user has not access" do + let(:current_user) { create(:user, admin: false) } + + before { sign_in(current_user) } + + let(:email) { Faker::Internet.unique.email } + + let(:password) { Faker::Internet.password } + + let(:name) { Faker::Name.unique.name } + + before do + expect do + post users_path, + params: { + user: { + email: email, + name: name, + password: password, + password_confirmation: password, + admin: true + } + } + end.not_to change(User, :count) + end + + it "is expected to redirect to root path with status found" do + expect(response).to redirect_to(root_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq(I18n.t("controllers.application.user_not_authorized")) + end + end + end + + context "when user is not logged in" do + let(:email) { Faker::Internet.unique.email } + + let(:password) { Faker::Internet.password } + + let(:name) { Faker::Name.unique.name } + + before do + expect do + post users_path, + params: { + user: { + email: email, + name: name, + password: password, + password_confirmation: password, + admin: true + } + } + end.not_to change(User, :count) + end + + it "is expected to redirect to new user session path with status found" do + expect(response).to redirect_to(new_user_session_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq(I18n.t("devise.failure.unauthenticated")) + end + end + end + + describe "#update" do + context "when user is logged in" do + context "when user has access" do + context "when record is valid" do + let(:current_user) { create(:user, admin: true) } + + before { sign_in(current_user) } + + let(:user) { create(:user, admin: true, name: "Jon Snow") } + + before do + patch user_path(user), + params: { + user: { + name: "Tyrion Lannister", + admin: false + } + } + end + + it "is expected to redirect to user path user with status ok" do + expect(response).to redirect_to(user_path(user)) + + expect(response).to have_http_status(:found) + + expect(request.flash[:success]).to eq(I18n.t("users.update.success", name: user.reload.name)) + end + end + + context "when record is not valid" do + let(:current_user) { create(:user, admin: true) } + + before { sign_in(current_user) } + + let(:user) { create(:user) } + + before do + patch user_path(user), + params: { + user: { + name: "" + } + } + end + + it "is expected to render template new with status ok" do + expect(response).to render_template(:edit) + + expect(response).to have_http_status(:ok) + end + end + end + + context "when user has not access" do + let(:current_user) { create(:user, admin: false) } + + before { sign_in(current_user) } + + let(:user) { create(:user, admin: false, name: "Jon Snow") } + + before do + patch user_path(user), + params: { + user: { + name: "Tyrion Lannister", + admin: true + } + } + end + + it "is expected to redirect to root path with status found" do + expect(response).to redirect_to(root_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq(I18n.t("controllers.application.user_not_authorized")) + end + end + end + + context "when user is not logged in" do + let(:user) { create(:user) } + + before do + patch user_path(user), + params: { + user: { + name: Faker::Name.unique.name + } + } + end + + it "is expected to redirect to new user session path with status found" do + expect(response).to redirect_to(new_user_session_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq(I18n.t("devise.failure.unauthenticated")) + end + end + end + + describe "#destroy" do + context "when user is logged in" do + context "when user has access" do + let(:current_user) { create(:user, admin: true) } + + let!(:user) { create(:user, admin: false) } + + before { sign_in(current_user) } + + before { expect(UserDestroy).to receive(:new).with(user).and_call_original } + + before { expect { delete user_path(user) }.to change(User, :count).by(-1) } + + it "is expected to redirect to users path with status found" do + expect(response).to redirect_to(users_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:success]).to eq(I18n.t("users.destroy.success", name: user.name)) + end + end + + context "when user has not access" do + let(:current_user) { create(:user, admin: false) } + + let!(:user) { create(:user, admin: true) } + + before { sign_in(current_user) } + + before { expect(UserDestroy).not_to receive(:new) } + + before { expect { delete user_path(user) }.not_to change(User, :count) } + + it "is expected to redirect to users path with status found" do + expect(response).to redirect_to(root_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq(I18n.t("controllers.application.user_not_authorized")) + end + end + + context "when user try to destroy himself" do + let(:current_user) { create(:user) } + + before { sign_in(current_user) } + + before { expect(UserDestroy).not_to receive(:new) } + + before { expect { delete user_path(current_user) }.not_to change(User, :count) } + + it "is expected to redirect to users path with status found" do + expect(response).to redirect_to(users_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:error]).to eq(I18n.t("users.destroy.error")) + end + end + end + + context "when user is not logged in" do + let(:user) { create(:user) } + + before { delete user_path(user) } + + it "is expected to redirect to new user session path with status found" do + expect(response).to redirect_to(new_user_session_path) + + expect(response).to have_http_status(:found) + + expect(request.flash[:alert]).to eq(I18n.t("devise.failure.unauthenticated")) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7c7a608e70..f59cd29077 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,62 +1,69 @@ +# frozen_string_literal: true + # This file is copied to ~/spec when you run 'ruby script/generate rspec' # from the project root directory. -ENV["RAILS_ENV"] = 'test' -ENV["ERRBIT_LOG_LEVEL"] = 'fatal' -ENV["ERRBIT_USER_HAS_USERNAME"] = 'false' - -if ENV['COVERAGE'] - require 'coveralls' - require 'simplecov' - Coveralls.wear!('rails') do - add_filter 'bundle' - end - SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ - SimpleCov::Formatter::HTMLFormatter, - Coveralls::SimpleCov::Formatter - ]) - SimpleCov.start('rails') do - add_filter 'bundle' - end +ENV["RAILS_ENV"] = "test" + +require "simplecov" + +SimpleCov.start "rails" do + enable_coverage :branch + enable_coverage :method + enable_coverage :eval + primary_coverage :branch + + group "Decorators", "app/decorators" + group "Interactors", "app/interactors" + group "Policies", "app/policies" + group "Views", "app/views" + + skip "app/views/kaminari" end require File.expand_path("../../config/environment", __FILE__) -require 'rspec/rails' -require 'email_spec' -require 'xmpp4r' -require 'xmpp4r/muc' -require 'mongoid-rspec' -require 'fabrication' -require 'sucker_punch/testing/inline' -require 'errbit_plugin/mock_issue_tracker' +require "rspec/rails" +require "email_spec" +require "errbit_plugin/mock_issue_tracker" # Requires supporting files with custom matchers and macros, etc, # in ./support/ and its subdirectories. Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } -Mongoid::Config.truncate! -Mongoid::Tasks::Database.create_indexes -ActionMailer::Base.delivery_method = :test RSpec.configure do |config| - config.include Devise::Test::ControllerHelpers, type: :controller - config.include Mongoid::Matchers, type: :model - config.alias_example_to :fit, focused: true + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" - config.before(:each) do - Mongoid::Config.truncate! - end + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! - config.include Haml, type: :helper - config.include Haml::Helpers, type: :helper - config.before(:each, type: :helper) do |_| - init_haml_helpers + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" end - config.before(:each, type: :decorator) do |_| - Draper::ViewContext.current.class_eval { include Haml::Helpers } - Draper::ViewContext.current.instance_eval { init_haml_helpers } - end + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 - config.infer_spec_type_from_file_location! -end + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random -OmniAuth.config.test_mode = true + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +end diff --git a/spec/support/action_mailer.rb b/spec/support/action_mailer.rb new file mode 100644 index 0000000000..d59bfb4f3e --- /dev/null +++ b/spec/support/action_mailer.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +ActionMailer::Base.delivery_method = :test diff --git a/spec/support/active_job.rb b/spec/support/active_job.rb new file mode 100644 index 0000000000..b1ddcb28f1 --- /dev/null +++ b/spec/support/active_job.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +ActiveJob::Base.queue_adapter = :inline diff --git a/spec/support/active_support.rb b/spec/support/active_support.rb new file mode 100644 index 0000000000..e781b70a81 --- /dev/null +++ b/spec/support/active_support.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.include ActiveSupport::Testing::TimeHelpers +end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb new file mode 100644 index 0000000000..7fb291a867 --- /dev/null +++ b/spec/support/capybara.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# https://github.com/teamcapybara/capybara/blob/3.40.0/lib/capybara/registrations/drivers.rb#L31-L41 +Capybara.register_driver :selenium_chrome_headless do |app| + version = Capybara::Selenium::Driver.load_selenium + + options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options + + browser_options = Selenium::WebDriver::Chrome::Options.new.tap do |opts| + opts.add_argument("--headless=new") + # Chrome's sandbox often cannot initialize inside Docker/Podman test containers. + opts.add_argument("--no-sandbox") + # Avoid Chrome crashes caused by small container /dev/shm mounts. + opts.add_argument("--disable-dev-shm-usage") + opts.add_argument("--disable-gpu") if Gem.win_platform? + # Workaround https://bugs.chromium.org/p/chromedriver/issues/detail?id=2650&q=load&sort=-id&colspec=ID%20Status%20Pri%20Owner%20Summary + opts.add_argument("--disable-site-isolation-trials") + + # https://stackoverflow.com/questions/78758185/is-there-any-way-to-handle-change-your-password-prompt-in-google-chrome-using-se + opts.add_preference("profile.password_manager_leak_detection", false) + end + + Capybara::Selenium::Driver.new(app, **{:browser => :chrome, options_key => browser_options}) +end diff --git a/spec/support/devise.rb b/spec/support/devise.rb new file mode 100644 index 0000000000..dfebb787b5 --- /dev/null +++ b/spec/support/devise.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.include Devise::Test::ControllerHelpers, type: :controller + config.include Devise::Test::IntegrationHelpers, type: :system + config.include Devise::Test::IntegrationHelpers, type: :request +end diff --git a/spec/support/factory_bot_rails.rb b/spec/support/factory_bot_rails.rb new file mode 100644 index 0000000000..2e7665cc38 --- /dev/null +++ b/spec/support/factory_bot_rails.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end diff --git a/spec/support/macros.rb b/spec/support/macros.rb index aa7776d5ab..e679b82596 100644 --- a/spec/support/macros.rb +++ b/spec/support/macros.rb @@ -1,26 +1,29 @@ +# frozen_string_literal: true + def it_requires_authentication(options = {}) default_options = { - for: { - index: :get, - show: :get, - new: :get, - create: :post, - edit: :get, - update: :put, + for: { + index: :get, + show: :get, + new: :get, + create: :post, + edit: :get, + update: :patch, destroy: :delete }, - params: { id: '4c6c760494df2a18cc000015' } + params: {id: "4c6c760494df2a18cc000015"} } options.reverse_merge!(default_options) - context 'when signed out' do + context "when signed out" do before do sign_out :user end options[:for].each do |action, method| it "#{method.to_s.upcase} #{action} redirects to the sign in page" do - send(method, action, options[:params]) + send(method, action, params: {**options[:params]}) + expect(response).to redirect_to(new_user_session_path) end end @@ -29,28 +32,30 @@ def it_requires_authentication(options = {}) def it_requires_admin_privileges(options = {}) default_options = { - for: { - index: :get, - show: :get, - new: :get, - create: :post, - edit: :get, - update: :put, + for: { + index: :get, + show: :get, + new: :get, + create: :post, + edit: :get, + update: :patch, destroy: :delete }, - params: { id: 'dummyid' } + params: {id: "dummyid"} } options.reverse_merge!(default_options) - context 'when signed in as a regular user' do + context "when signed in as a regular user" do before do sign_out :user - sign_in Fabricate(:user) + + sign_in create(:user) end options[:for].each do |action, method| it "#{method.to_s.upcase} #{action} redirects to the root path" do - send(method, action, options[:params]) + send(method, action, params: {**options[:params]}) + expect(response).to redirect_to(root_path) end end diff --git a/spec/support/mongoid-rspec.rb b/spec/support/mongoid-rspec.rb new file mode 100644 index 0000000000..0cddf44af7 --- /dev/null +++ b/spec/support/mongoid-rspec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.include Mongoid::Matchers, type: :model +end diff --git a/spec/support/mongoid.rb b/spec/support/mongoid.rb new file mode 100644 index 0000000000..1bd54d3b51 --- /dev/null +++ b/spec/support/mongoid.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.before(:suite) do + Mongoid::Config.truncate! + + Mongoid::Tasks::Database.create_indexes + end + + config.before do + Mongoid::Config.truncate! + end +end diff --git a/spec/support/omniauth.rb b/spec/support/omniauth.rb new file mode 100644 index 0000000000..64aa3855a6 --- /dev/null +++ b/spec/support/omniauth.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +OmniAuth.config.test_mode = true diff --git a/spec/support/rspec-rebound.rb b/spec/support/rspec-rebound.rb new file mode 100644 index 0000000000..1f672e660f --- /dev/null +++ b/spec/support/rspec-rebound.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "rspec/rebound" + +RSpec.configure do |config| + # show retry status in spec process + config.verbose_retry = true + + # show exception that triggers a retry if verbose_retry is set to true + config.display_try_failure_messages = true + + # run retry only on features + # config.around :each, :js do |ex| + # ex.run_with_retry retry: 3 + # end + + # callback to be run when a flaky test is detected + # config.flaky_test_callback = proc do |example| + # Rspec::Watchdog::Reporter.report(example) + # end + + # callback to be run between retries + # config.retry_callback = proc do |ex| + # # run some additional clean up task - can be filtered by example metadata + # if ex.metadata[:js] + # Capybara.reset! + # end + # end + + config.retry_callback = proc do |ex| + if ex.metadata[:system] + Capybara.reset! + end + end +end diff --git a/spec/support/selenium-webdriver.rb b/spec/support/selenium-webdriver.rb new file mode 100644 index 0000000000..773d3cf9a2 --- /dev/null +++ b/spec/support/selenium-webdriver.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.before(:each, type: :system) do + # Chrome headless + driven_by :selenium_chrome_headless + # Chrome non-headless + # driven_by :selenium_chrome + # Firefox headless + # driven_by :selenium_headless + # Firefox non-headless + # driven_by :selenium + # Rack test + # driven_by :rack_test + end +end diff --git a/spec/support/super_diff.rb b/spec/support/super_diff.rb new file mode 100644 index 0000000000..3c4694824c --- /dev/null +++ b/spec/support/super_diff.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require "super_diff/rspec-rails" diff --git a/spec/support/vcr.rb b/spec/support/vcr.rb new file mode 100644 index 0000000000..ea30f80b59 --- /dev/null +++ b/spec/support/vcr.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +VCR.configure do |c| + c.cassette_library_dir = Rails.root.join("spec/cassettes") + c.hook_into :webmock + c.ignore_localhost = true + c.default_cassette_options = { + decode_compressed_response: true, + allow_unused_http_interactions: false + } +end diff --git a/spec/support/webmock.rb b/spec/support/webmock.rb new file mode 100644 index 0000000000..35f8019983 --- /dev/null +++ b/spec/support/webmock.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require "webmock/rspec" + +WebMock.disable_net_connect! diff --git a/spec/system/csp_compatibility_spec.rb b/spec/system/csp_compatibility_spec.rb new file mode 100644 index 0000000000..e6296d5755 --- /dev/null +++ b/spec/system/csp_compatibility_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "CSP-compatible browser behavior", type: :system, retry: 3 do + let!(:user) { create(:user, admin: true) } + + it "loads the notice sparkline" do + app = create(:app) + problem = create(:problem, app: app) + err = create(:err, problem: problem) + notice = create(:notice, app: app, err: err) + create(:notice, app: app, err: err) + sign_in user + + visit app_problem_path(notice.app, notice.problem) + + expect(page).to have_css(".spark") + expect(page).to have_css(".spark > i[class*='height-']") + + click_link "Older" + + expect(page).to have_css(".spark > i[class*='height-']") + end + + it "toggles conditional application fields without inline styles" do + allow(Errbit::Config).to receive(:per_app_email_at_notices).and_return(true) + app = create(:app, notify_on_errs: false, notice_fingerprinter: nil) + sign_in user + + visit edit_app_path(app) + + expect(page).to have_css(".email_at_notices_nested", visible: :hidden) + check "Notify on errors" + expect(page).to have_css(".email_at_notices_nested", visible: :visible) + uncheck "Notify on errors" + expect(page).to have_css(".email_at_notices_nested", visible: :hidden) + end + + it "updates the app search without evaluating a JavaScript response" do + create(:app, name: "Searchable Demo App") + sign_in user + + visit apps_path + fill_in "search", with: "Searchable" + find("#search").send_keys(:enter) + + expect(page).to have_css("#app_table", text: "Searchable Demo App") + end + + it "updates the problem search without evaluating a JavaScript response" do + app = create(:app) + create(:problem, app: app, message: "Searchable problem") + create(:problem, app: app, message: "Other problem") + sign_in user + + visit problems_path + fill_in "search", with: "Searchable" + find("#search").send_keys(:enter) + + expect(page).to have_css("#problem_table", text: "Searchable problem") + expect(page).not_to have_css("#problem_table", text: "Other problem") + + check "toggle_problems_checkboxes" + expect(page).to have_checked_field("problems[]") + end +end diff --git a/spec/system/sign_in_and_sign_out_spec.rb b/spec/system/sign_in_and_sign_out_spec.rb new file mode 100644 index 0000000000..e99e94a65a --- /dev/null +++ b/spec/system/sign_in_and_sign_out_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Sign in and sign out with email and password", type: :system, retry: 3 do + let!(:user) { create(:user, password: "password") } + + context "when user successful sign in and sign out" do + it "is expected to sign in user and sign out" do + visit root_path + + expect(page).to have_content(I18n.t("devise.failure.unauthenticated")) + expect(page).to have_css('input[name="user[email]"][autocomplete="email"]') + expect(page).to have_css('input[name="user[password]"][autocomplete="current-password"]') + + fill_in "Email", with: user.email + fill_in "Password", with: "password" + + click_button I18n.t("devise.sessions.new.sign_in") + + expect(page).to have_current_path(root_path) + + expect(page).to have_content(I18n.t("devise.sessions.signed_in")) + + click_link I18n.t("shared.session.sign_out") + + expect(page).to have_current_path(new_user_session_path) + + expect(page).to have_content(I18n.t("devise.failure.unauthenticated")) + end + end + + context "when user exists but password is wrong" do + it "is expected to reject user with wrong password" do + visit root_path + + expect(page).to have_content(I18n.t("devise.failure.unauthenticated")) + + fill_in "Email", with: user.email + fill_in "Password", with: "ohS4eiv4mitiG3Iu1cu3" + + click_button I18n.t("devise.sessions.new.sign_in") + + expect(page).to have_current_path(new_user_session_path) + + expect(page).to have_content(I18n.t("devise.failure.invalid", authentication_keys: "email")) + end + end + + it "carries the entered email to the password reset form" do + visit new_user_session_path + + fill_in "Email", with: user.email + click_link "forgot it?" + + expect(page).to have_current_path(new_user_password_path, ignore_query: true) + expect(page).to have_field("Email", with: user.email) + end + + it "renders the password reset email autocomplete attribute" do + visit new_user_password_path + + expect(page).to have_css('input[name="user[email]"][autocomplete="email"]') + end + + it "renders the password reset new-password autocomplete attributes" do + token = user.send_reset_password_instructions + visit edit_user_password_path(reset_password_token: token) + + expect(page).to have_css('input[name="user[password]"][autocomplete="new-password"]') + expect(page).to have_css('input[name="user[password_confirmation]"][autocomplete="new-password"]') + end + + it "renders the username autocomplete attribute when username authentication is enabled" do + allow(Errbit::Config).to receive(:user_has_username).and_return(true) + allow(Devise).to receive(:authentication_keys).and_return([:username]) + allow(User).to receive(:new).and_wrap_original do |original, *args| + original.call(*args).tap do |resource| + allow(resource).to receive(:username).and_return(nil) + end + end + + visit new_user_session_path + + expect(page).to have_css('input[name="user[username]"][autocomplete="username"]') + end +end diff --git a/spec/system/sign_in_with_github_spec.rb b/spec/system/sign_in_with_github_spec.rb new file mode 100644 index 0000000000..9a51fcad01 --- /dev/null +++ b/spec/system/sign_in_with_github_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Sign in with GitHub", type: :system, retry: 3 do + context "sign in via GitHub with recognized user" do + let!(:user) { create(:user, github_login: "biow0lf") } + + before { expect(Errbit::Config).to receive(:github_authentication).and_return(true).twice } + + before { OmniAuth.config.mock_auth[:github] = Faker::Omniauth.github(name: "biow0lf") } + + after { OmniAuth.config.mock_auth[:github] = nil } + + it "is expected to sign in user via GitHub" do + visit root_path + + click_link "Sign in with GitHub" + + expect(page).to have_content(I18n.t("devise.omniauth_callbacks.success", kind: "GitHub")) + end + end + + context "reject unrecognized user" do + it "is expected to reject unrecognized user" do + visit root_path + + click_link "Sign in with GitHub" + + expect(page).to have_content("There are no authorized users with GitHub login") + end + end +end diff --git a/spec/system/sign_in_with_google_spec.rb b/spec/system/sign_in_with_google_spec.rb new file mode 100644 index 0000000000..b03eb3fa45 --- /dev/null +++ b/spec/system/sign_in_with_google_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Sign in with Google", type: :system, retry: 3 do + context "sign in via Google with recognized user" do + let!(:user) { create(:user, google_uid: "123456789") } + + before { expect(Errbit::Config).to receive(:google_authentication).and_return(true) } + + before { OmniAuth.config.mock_auth[:google_oauth2] = Faker::Omniauth.google(uid: "123456789") } + + after { OmniAuth.config.mock_auth[:google_oauth2] = nil } + + it "is expected to sign in user via Google" do + visit root_path + + click_link "Sign in with Google" + + expect(page).to have_content(I18n.t("devise.omniauth_callbacks.success", kind: "Google")) + end + end + + context "reject unrecognized user" do + it "is expected to reject unrecognized user" do + visit root_path + + click_link "Sign in with Google" + + expect(page).to have_content("There are no authorized users with Google login") + end + end +end diff --git a/spec/system/sign_in_with_google_with_auto_provision_spec.rb b/spec/system/sign_in_with_google_with_auto_provision_spec.rb new file mode 100644 index 0000000000..c3d9843225 --- /dev/null +++ b/spec/system/sign_in_with_google_with_auto_provision_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Sign in with Google with auto provision", type: :system, retry: 3 do + context "create an account for recognized user if they log in" do + before { expect(Errbit::Config).to receive(:google_authentication).and_return(true) } + + before { expect(Errbit::Config).to receive(:google_auto_provision).and_return(true) } + + before { OmniAuth.config.mock_auth[:google_oauth2] = Faker::Omniauth.google(uid: "123456789") } + + after { OmniAuth.config.mock_auth[:google_oauth2] = nil } + + it "is expected to create an account" do + visit root_path + + click_link "Sign in with Google" + + expect(page).to have_content(I18n.t("devise.omniauth_callbacks.success", kind: "Google")) + end + end +end diff --git a/spec/system/sign_in_with_google_with_domain_validation_spec.rb b/spec/system/sign_in_with_google_with_domain_validation_spec.rb new file mode 100644 index 0000000000..75d40b621a --- /dev/null +++ b/spec/system/sign_in_with_google_with_domain_validation_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Sign in with Google with domain validation", type: :system, retry: 3 do + before { expect(Errbit::Config).to receive(:google_authentication).and_return(true).at_least(:once).times } + + before { expect(Errbit::Config).to receive(:google_auto_provision).and_return(true) } + + before { expect(Errbit::Config).to receive(:google_authorized_domains).and_return("example.com").twice } + + context "create an account for recognized user if their account email is from a trusted domain" do + before { OmniAuth.config.mock_auth[:google_oauth2] = Faker::Omniauth.google(email: "me@example.com", uid: "123456789") } + + after { OmniAuth.config.mock_auth[:google_oauth2] = nil } + + it "is expected to create an account" do + visit root_path + + click_link "Sign in with Google" + + expect(page).to have_content(I18n.t("devise.omniauth_callbacks.success", kind: "Google")) + end + end + + context "don't create an account for user if their account email is from an unauthorized domain" do + before { OmniAuth.config.mock_auth[:google_oauth2] = Faker::Omniauth.google(email: "me@mail.example.com", uid: "123456789") } + + after { OmniAuth.config.mock_auth[:google_oauth2] = nil } + + it "is expected to not create an account" do + visit root_path + + click_link "Sign in with Google" + + expect(page).to have_text(I18n.t("devise.google_login.domain_unauthorized")) + end + end +end diff --git a/spec/system/watch_unwatch_app_spec.rb b/spec/system/watch_unwatch_app_spec.rb new file mode 100644 index 0000000000..00643e1593 --- /dev/null +++ b/spec/system/watch_unwatch_app_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "A user can watch and unwatch an application", type: :system, retry: 3 do + context "when user log in and watch a project" do + it "is expected to log in and watch a project" do + current_user = create(:user) + app = create(:app) + + sign_in(current_user) + + visit root_path + + click_link app.name + click_link I18n.t("apps.show.watch") + + expect(page).to have_content(I18n.t("watchers.create.success", app: app.name)) + end + end + + context "when user log in and unwatch a project" do + it "is expected to log in and unwatch a project" do + current_user = create(:user) + app = create(:app) + + app.watchers.create!(user: current_user) + + sign_in(current_user) + + visit root_path + + click_link app.name + click_link I18n.t("apps.show.unwatch") + + expect(page).to have_content(I18n.t("watchers.destroy.success", app: app.name)) + end + end +end diff --git a/spec/views/apps/edit.html.haml_spec.rb b/spec/views/apps/edit.html.erb_spec.rb similarity index 74% rename from spec/views/apps/edit.html.haml_spec.rb rename to spec/views/apps/edit.html.erb_spec.rb index f318c20b6e..3ebd7c6da1 100644 --- a/spec/views/apps/edit.html.haml_spec.rb +++ b/spec/views/apps/edit.html.erb_spec.rb @@ -1,5 +1,10 @@ -describe "apps/edit.html.haml", type: 'view' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "apps/edit.html.erb", type: :view do let(:app) { stub_model(App) } + let(:app_decorate) { AppDecorator.new(app) } before do @@ -15,34 +20,37 @@ def action_bar it "should confirm the 'reset' link" do render + expect(action_bar).to have_selector( format( 'a.button[data-confirm="%s"]', - I18n.t('apps.confirm_destroy_all_problems') + I18n.t("apps.confirm_destroy_all_problems") ) ) end it "should confirm the 'destroy' link" do render + expect(action_bar).to have_selector( format( 'a.button[data-confirm="%s"]', - I18n.t('apps.confirm_delete') + I18n.t("apps.confirm_delete") ) ) end end - context "with unvalid app" do + context "with invalid app" do let(:app) do app = stub_model(App) - app.errors.add(:base, 'You must specify your') + app.errors.add(:base, "You must specify your") app end - it 'see the error' do + it "see the error" do render + expect(rendered).to match(/You must specify your/) end end diff --git a/spec/views/apps/index.html.erb_spec.rb b/spec/views/apps/index.html.erb_spec.rb new file mode 100644 index 0000000000..2e6c10abcf --- /dev/null +++ b/spec/views/apps/index.html.erb_spec.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "apps/index.html.erb", type: :view do +end diff --git a/spec/views/apps/new.html.haml_spec.rb b/spec/views/apps/new.html.erb_spec.rb similarity index 68% rename from spec/views/apps/new.html.haml_spec.rb rename to spec/views/apps/new.html.erb_spec.rb index b6cb58883f..7f5dfab0db 100644 --- a/spec/views/apps/new.html.haml_spec.rb +++ b/spec/views/apps/new.html.erb_spec.rb @@ -1,5 +1,10 @@ -describe "apps/new.html.haml", type: 'view' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "apps/new.html.erb", type: :view do let(:app) { stub_model(App) } + let(:app_decorate) { AppDecorator.new(app) } before do @@ -16,19 +21,20 @@ def action_bar it "should confirm the 'cancel' link" do render - expect(action_bar).to have_selector('a.button', text: 'cancel') + expect(action_bar).to have_selector("a.button", text: "cancel") end end - context "with unvalid app" do + context "with invalid app" do let(:app) do app = stub_model(App) - app.errors.add(:base, 'You must specify your') + app.errors.add(:base, "You must specify your") app end - it 'see the error' do + it "see the error" do render + expect(rendered).to match(/You must specify your/) end end diff --git a/spec/views/apps/show.atom.builder_spec.rb b/spec/views/apps/show.atom.builder_spec.rb index 28e17af953..dabe2ec53d 100644 --- a/spec/views/apps/show.atom.builder_spec.rb +++ b/spec/views/apps/show.atom.builder_spec.rb @@ -1,6 +1,13 @@ -describe "apps/show.atom.builder", type: 'view' do - let(:app) { stub_model(App) } - let(:problems) { [stub_model(Problem, message: 'foo', app: app)] } +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "apps/show.atom.builder", type: :view do + let(:notice) { create(:notice) } + + let(:app) { notice.app } + + let(:problems) { [notice.problem] } before do allow(view).to receive(:app).and_return(app) @@ -8,8 +15,9 @@ end context "with errs" do - it 'see the errs message' do + it "see the errs message" do render + expect(rendered).to match(problems.first.message) end end diff --git a/spec/views/apps/show.html.haml_spec.rb b/spec/views/apps/show.html.erb_spec.rb similarity index 61% rename from spec/views/apps/show.html.haml_spec.rb rename to spec/views/apps/show.html.erb_spec.rb index 38879e7157..ad20c61d1d 100644 --- a/spec/views/apps/show.html.haml_spec.rb +++ b/spec/views/apps/show.html.erb_spec.rb @@ -1,5 +1,10 @@ -describe "apps/show.html.haml", type: 'view' do +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "apps/show.html.erb", type: :view do let(:app) { stub_model(App) } + let(:user) { stub_model(User) } let(:action_bar) do @@ -9,8 +14,8 @@ before do allow(view).to receive(:app).and_return(app) allow(view).to receive(:all_errs).and_return(false) - allow(view).to receive(:params_order).and_return('asc') - allow(view).to receive(:params_sort).and_return('latest_notice_at') + allow(view).to receive(:params_order).and_return("asc") + allow(view).to receive(:params_sort).and_return("latest_notice_at") allow(controller).to receive(:current_user).and_return(user) end @@ -18,13 +23,14 @@ it "should confirm the 'cancel' link" do render - expect(action_bar).to have_selector('a.button', text: 'all errors') + expect(action_bar).to have_selector("a.button", text: "all errors") end end context "without errs" do - it 'see no errs' do + it "see no errs" do render + expect(rendered).to match(/No errors have been/) end end @@ -33,9 +39,11 @@ before do allow(app).to receive(:watched_by?).with(user).and_return(true) end - it 'see the unwatch button' do + + it "see the unwatch button" do render - expect(action_bar).to include(I18n.t('apps.show.unwatch')) + + expect(action_bar).to include(I18n.t("apps.show.unwatch")) end end @@ -43,9 +51,11 @@ before do allow(app).to receive(:watched_by?).with(user).and_return(false) end - it 'not see the unwatch button' do + + it "not see the unwatch button" do render - expect(action_bar).to_not include(I18n.t('apps.show.unwatch')) + + expect(action_bar).not_to include(I18n.t("apps.show.unwatch")) end end end diff --git a/spec/views/issue_trackers/issue.md.erb_spec.rb b/spec/views/issue_trackers/issue.md.erb_spec.rb deleted file mode 100644 index 6b1a6240f1..0000000000 --- a/spec/views/issue_trackers/issue.md.erb_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -describe "issue_trackers/issue.md.erb", type: 'view' do - let(:problem) do - problem = Fabricate(:problem) - Fabricate(:notice, err: Fabricate(:err, problem: problem)) - problem - end - - before do - allow(view).to receive(:problem).and_return(ProblemDecorator.new(problem)) - end - - it "has the problem url" do - render - expect(rendered).to match(app_problem_url problem.app, problem) - end -end diff --git a/spec/views/issue_trackers/issue.txt.erb_spec.rb b/spec/views/issue_trackers/issue.txt.erb_spec.rb deleted file mode 100644 index 1daf330265..0000000000 --- a/spec/views/issue_trackers/issue.txt.erb_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -describe "issue_trackers/issue.txt.erb", type: 'view' do - let(:problem) do - problem = Fabricate(:problem) - Fabricate(:notice, err: Fabricate(:err, problem: problem)) - problem - end - - before do - allow(view).to receive(:problem).and_return( - ProblemDecorator.new(problem)) - end - - it "has the problem url" do - render - expect(rendered).to match(app_problem_url problem.app, problem) - end -end diff --git a/spec/views/issue_trackers/markdown.erb_spec.rb b/spec/views/issue_trackers/markdown.erb_spec.rb new file mode 100644 index 0000000000..97c89aa661 --- /dev/null +++ b/spec/views/issue_trackers/markdown.erb_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "issue_trackers/markdown.erb", type: :view do + let(:problem) do + problem = create(:problem) + err = create(:err, problem: problem) + create(:notice, err: err) + problem + end + + before do + allow(view).to receive(:problem).and_return(ProblemDecorator.new(problem)) + end + + it "has the problem url" do + render + + expect(rendered).to match(app_problem_url(problem.app, problem)) + end +end diff --git a/spec/views/issue_trackers/text.erb_spec.rb b/spec/views/issue_trackers/text.erb_spec.rb new file mode 100644 index 0000000000..df7c570f2e --- /dev/null +++ b/spec/views/issue_trackers/text.erb_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "issue_trackers/text.erb", type: :view do + let(:problem) do + problem = create(:problem) + err = create(:err, problem: problem) + create(:notice, err: err) + problem + end + + before do + allow(view).to receive(:problem).and_return(ProblemDecorator.new(problem)) + end + + it "has the problem url" do + render + + expect(rendered).to match(app_problem_url(problem.app, problem)) + end +end diff --git a/spec/views/notices/_atom_entry.atom.erb_spec.rb b/spec/views/notices/_atom_entry.atom.erb_spec.rb new file mode 100644 index 0000000000..a75341e316 --- /dev/null +++ b/spec/views/notices/_atom_entry.atom.erb_spec.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "notices/_atom_entry.atom.erb", type: :view do +end diff --git a/spec/views/notices/_backtrace.html.erb_spec.rb b/spec/views/notices/_backtrace.html.erb_spec.rb new file mode 100644 index 0000000000..ee2bbf8118 --- /dev/null +++ b/spec/views/notices/_backtrace.html.erb_spec.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "notices/_backtrace.html.erb", type: :view do +end diff --git a/spec/views/notices/_backtrace_line.html.erb_spec.rb b/spec/views/notices/_backtrace_line.html.erb_spec.rb new file mode 100644 index 0000000000..cbc71f8c7d --- /dev/null +++ b/spec/views/notices/_backtrace_line.html.erb_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "notices/_backtrace_line.html.erb", type: :view do + let(:line) do + instance_double( + BacktraceLineDecorator, + decorated_path: "gems/foo/bar", + file_name: "example.rb", + in_app?: false, + method: "call", + number: nil, + column: nil + ) + end + + before do + allow(line).to receive(:link_to_source_file) { |&block| view.capture(&block) } + render partial: "notices/backtrace_line", locals: {line: line, app: double} + end + + it "preserves gem highlighting" do + expect(rendered).to include("gems/foo/bar") + end + + it "removes unsafe markup and attributes" do + expect(rendered).not_to include("